diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f6041eee1..bf668124e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: myrobotlab ci/cd +name: Java CI on: [push] @@ -9,19 +9,11 @@ jobs: steps: - uses: actions/checkout@v3 - - name: setup jdk 11 + - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'adopt' - # - name: Test with Maven - # run: mvn test -Dtest=!org.myrobotlab.opencv.OpenCVFilterDL4JTest - # - name: build with maven - # run: mvn --batch-mode --update-snapshots -DskipTests package - run: mvn -DskipTests package - - name: build with maven - # run: mvn --version - # run: mvn -Dtest=Service* package - # run: mvn --batch-mode --update-snapshots package - run: echo "I wish I could test here" + - name: Build with Maven + run: mvn --batch-mode -Dtest=!**/OpenCV* test -X diff --git a/.gitignore b/.gitignore index 803d7438d8..fb07e725f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,43 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +dist/ +build/ +*.egg-info/ +*.egg + +# IDEs and editors +.idea/ +.vscode/ +*.pyc + +# Local development settings +.env +.env.local +.env.*.local + +# Logs and other generated files +*.log +*.out +*.pid +*.sock + + # TODO clean this up ! /.project /.classpath /.settings /src/main/resources/resource/WebGui/react /src/main/resources/resource/Vertx/app + +# ignore all resources from seperate modular projects +/src/main/resources/resource/ProgramAB +/src/main/resources/resource/InMoov2 +/src/main/resources/resource/SpotMicro + /data /resource /bin @@ -82,3 +116,4 @@ /lastRestart.py /.factorypath start.yml +*.iml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..e101157a72 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "htmlWhitespaceSensitivity": "ignore", + "printWidth": 180 +} \ No newline at end of file diff --git a/README.md b/README.md index e16ed69735..37d18e96a6 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,6 @@ If you want to compile and skip the tests, you can use the standard maven approa * [Title Caps for field names and elements](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/bb246428(v=vs.85)?redirectedfrom=MSDN) * [No semi-colons for field names if labels exist](https://ux.stackexchange.com/questions/3611/should-label-and-field-be-separated-with-colon) - ## Contributing All development is done on the `develop` branch. To contribute code, the typical approach is to create an issue about the feature/bug you're working on. @@ -99,3 +98,191 @@ When code is ready, submit a pull request to the develop branch! Enjoy the code review, address issues and concern in the code review Reviewer merges pull request to develop. Reviewer deletes branch. + + +The following config should be useful to work directly on WebGui UI and +InMoov2 UI if the repos are checked out at the same level +```yml +!!org.myrobotlab.service.config.WebGuiConfig +autoStartBrowser: true +enableMdns: false +listeners: null +peers: null +port: 8888 +resources: + # these are the only two in usual runtime +- ./resource/WebGui/app +- ./resource + # the rest are useful when doing dev +- ../InMoov2/resource/WebGui/app +- ./src/main/resources/resource/WebGui/app +- ./src/main/resources/resource/WebGui +- ./src/main/resources/resource +- ./src/main/resources +type: WebGui +``` +```yml +!!org.myrobotlab.service.config.RuntimeConfig +enableCli: true +id: null +listeners: +locale: null +logLevel: info +peers: null +registry: +- runtime +- security +- webgui +- python +resource: src/main/resources/resource +type: Runtime +virtual: false +``` + +# Network Distributed Architecture + +## Websockets - Default Response for New Connection + +```mermaid +sequenceDiagram + autonumber + box Process Id p1 + participant runtime@p1 + end + + box Process Id p2 + participant webgui@p2 + participant runtime@p2 + end + + Note right of runtime@p1: Client runtime@p1 opens a
websocket to remote webgui + + runtime@p1->>webgui@p2: Connect
ws://localhost:8888/api/messages?user=root&pwd=pwd&session_id=2309adf3dlkdk&id=p1 + + Note left of webgui@p2: Remote webgui@p2 and runtime@p2 attempt to subscribe
to the describe method of the runtime@p1 + webgui@p2->>runtime@p1: addListener describe + Note left of webgui@p2: runtime@p2 sends a describe request to runtime@p1 + webgui@p2->>runtime@p1: describe + Note right of runtime@p1: runtime@p1 responds with a describe response + runtime@p1->>webgui@p2: onDescribe + Note left of webgui@p2: Based on the results of the describe,
more querying and subscriptions can be processed + + + %% opt Server Add Listener, Describe and Reserve + %% end + + +``` + + +### Minimal Message API Definition + +```json +{ + "name": "runtime", + "method": "connect", + "data": [ + "\"http://main.myrobotlab.org:8888\"" + ], +} + +``` + +### Path API Definition +``` +/{service-name}/{method-name}/{json-param1}/{json-param2}/{json-param3}... +``` +The Path API definition is a simple way to define a RESTful API. The path is parsed and the service name, method name, and parameters are extracted. The parameters are json encoded and converted to the correct type when the method is invoked. The response is returned as a JSON object. The REST and CLI both use this API definition. + +#### Examples +``` +http://localhost:8888/runtime/getUptime +http://localhost:8888/runtime/connect/"http://main.myrobotlab.org:8888" +http://localhost:8888/arduino/connect/"COM3" +``` +The exact same paths can be used in the CLI +``` +/runtime/getUptime +/runtime/connect/"http://main.myrobotlab.org:8888" +/arduino/connect/"COM3" +``` + + +### 1 Connection +A connection with a websocket starts with an HTTP GET +``` +ws://localhost:8888/api/messages?user=root&pwd=pwd&session_id=2309adf3dlkdk&id=p1 +``` +### 2 Subscribe to Runtime Describe +When a connection is established between two different myrobotlab instances p1 and p2, +the following messages are sent. The first message is adding a subscription to the runtime of the +other process. The subscription is for the function "describe". The second message will be a describe request. +```json +{ + "msgId": 1690145377501, + "name": "runtime", + "sender": "webgui@p2", + "sendingMethod": "", + "historyList": [], + "properties": null, + "status": null, + "encoding": "json", + "method": "addListener", + "data": [ + "{\"topicMethod\":\"describe\",\"callbackName\":\"runtime@caring-hector\",\"callbackMethod\":\"onDescribe\",\"class\":\"org.myrobotlab.framework.MRLListener\"}" + ], + "class": "org.myrobotlab.framework.Message" +} +``` +### 3 Send Describe + +```json +{ + "msgId": 1690145377501, + "name": "runtime", + "sender": "webgui@p2", + "sendingMethod": "", + "historyList": [], + "properties": null, + "status": null, + "encoding": "json", + "method": "describe", + "data": [ + "\"fill-uuid\"", + "{\"id\":\"caring-hector\",\"uuid\":\"383b4070-2848-4c3d-85f4-e7f6e081d18e\",\"platform\":{\"os\":\"linux\",\"arch\":\"x86\",\"osBitness\":64,\"jvmBitness\":64,\"lang\":\"java\",\"vmName\":\"OpenJDK 64-Bit Server VM\",\"vmVersion\":\"11\",\"mrlVersion\":\"unknownVersion\",\"isVirtual\":false,\"id\":\"caring-hector\",\"branch\":\"develop\",\"pid\":\"1500044\",\"hostname\":\"t14-gperry\",\"commit\":\"55d0163663825dd0aaa10568bc01e035c7f21532\",\"build\":null,\"motd\":\"resistance is futile, we have cookies and robots ...\",\"startTime\":1690135873670,\"manifest\":{\"git.branch\":\"develop\",\"git.build.host\":\"t14-gperry\",\"git.build.time\":\"2023-07-23T10:38:46-0700\",\"git.build.user.email\":\"grog@myrobotlab.org\",\"git.build.user.name\":\"grog\",\"git.build.version\":\"0.0.1-SNAPSHOT\",\"git.closest.tag.commit.count\":\"13447\",\"git.closest.tag.name\":\"1.0.119\",\"git.commit.author.time\":\"2023-07-22T20:15:51-0700\",\"git.commit.committer.time\":\"2023-07-22T20:15:51-0700\",\"git.commit.id\":\"55d0163663825dd0aaa10568bc01e035c7f21532\",\"git.commit.id.abbrev\":\"55d0163\",\"git.commit.id.describe\":\"1.0.119-13447-g55d0163\",\"git.commit.id.describe-short\":\"1.0.119-13447\",\"git.commit.message.full\":\"Cron enhanced 2 (#1318)\\n\\n* Improved Cron and Cron history\\r\\n\\r\\n* forgot one\\r\\n\\r\\n* Teamwork fix of Hd44780\\r\\n\\r\\n* updated from review\",\"git.commit.message.short\":\"Cron enhanced 2 (#1318)\",\"git.commit.time\":\"2023-07-22T20:15:51-0700\",\"git.commit.user.email\":\"grog@myrobotlab.org\",\"git.commit.user.name\":\"GroG\",\"git.dirty\":\"false\",\"git.local.branch.ahead\":\"0\",\"git.local.branch.behind\":\"0\",\"git.remote.origin.url\":\"git@github.com-myrobotlab:MyRobotLab/myrobotlab.git\",\"git.tags\":\"1.1.1194\",\"git.total.commit.count\":\"14104\"},\"shortCommit\":\"55d0163\",\"class\":\"org.myrobotlab.framework.Platform\"},\"class\":\"org.myrobotlab.framework.DescribeQuery\"}" + ], + "class": "org.myrobotlab.framework.Message" +} + +``` + +The describe message sends instance and platform information in return, the return data from describe, will return with an onDescribe message that contains all the service information from the remote (p2) process + +### 4 Process onDescribe Response +```json +{ + "msgId":1692478237121, + "name":"runtime@webgui-client", + "sender":"runtime@unhealthy-giddy", + "sendingMethod":"describe", + "historyList":[ + "unhealthy-giddy" + ], + "properties":null, + "status":null, + "encoding":"json", + "method":"onDescribe", + "data":[ + "{\"id\":\"unhealthy-giddy\",\"uuid\":null,\"request\":null,\"platform\":{\"os\":\"linux\",\"arch\":\"x86\",\"osBitness\":64,\"jvmBitness\":64,\"lang\":\"java\",\"vmName\":\"OpenJDK 64-Bit Server VM\",\"vmVersion\":\"11\",\"mrlVersion\":\"unknownVersion\",\"isVirtual\":false,\"id\":\"unhealthy-giddy\",\"branch\":\"grog\",\"pid\":\"2462761\",\"hostname\":\"t14-gperry\",\"commit\":\"5610a6602bef704a84160d7d067af3b417be1998\",\"build\":null,\"motd\":\"resistance is futile, we have cookies and robots ...\",\"startTime\":1692478154179,\"manifest\":{\"git.branch\":\"grog\",\"git.build.host\":\"t14-gperry\",\"git.build.time\":\"2023-08-19T09:36:36-0700\",\"git.build.user.email\":\"grog@myrobotlab.org\",\"git.build.user.name\":\"grog\",\"git.build.version\":\"0.0.1-SNAPSHOT\",\"git.closest.tag.commit.count\":\"13778\",\"git.closest.tag.name\":\"1.0.119\",\"git.commit.author.time\":\"2023-08-19T09:21:48-0700\",\"git.commit.committer.time\":\"2023-08-19T09:21:48-0700\",\"git.commit.\"class\":\"org.myrobotlab.framework.DescribeResults\"} .... WAY WAY TOO MUCH DATA !!!! ..." + ], + "class":"org.myrobotlab.framework.Message" +} +``` + +The response back will include serialized services which should be refactor to be more minimal, and more describe parameters which can return interfaces or methods +Services returned from the Describe request have been registered. By default they are only the currently "local" registered services. + +The response should be refactored so that the material returned is related to criterial requested. +For example, if only a list of services and their names are needed, that is all that is returned. If a specific service's interfaces are requested, then that is only returned. +Fill-UUID should be refactored out. + diff --git a/doc/service-life-cycle.md b/doc/service-life-cycle.md new file mode 100644 index 0000000000..8fa5b4ebdb --- /dev/null +++ b/doc/service-life-cycle.md @@ -0,0 +1,52 @@ +# Service Life Cycle +```mermaid +stateDiagram + + start: start(name, type) + load: load(name, type) + loadService: loadService(plan, name, type, true, 0) + + [*] --> start + + start --> load + load --> loadService + loadService --> getDefault + getDefault --> readServiceConfig + readServiceConfig --> loadService + loadService --> createServicesFromPlan + createServicesFromPlan --> createService + createService --> setConfig + setConfig --> apply + apply --> startService + startService --> stopService + stopService --> releaseService + releaseService --> release + release --> [*] + + [*] --> create + create --> load + +``` + +### start(name, type) +Creates and starts a service with the given name and type + +### load +Starts loading the hierarchy of configuration +FIXME - this should not be the memory plan, but should exist on the filesystem +Default config is used if no config found ~~in memory~~ on filesystem + +### loadService +Recursively loads a service config into a plan + +### createServicesFromPlan(plan, createdServices, name) +Loops through all "loaded" services in the plan and creates them all + +### createService +Instantiates instance of service + +### setConfig +Sets the config of the service + +### apply +Applies the config to the service \ No newline at end of file diff --git a/myrobotlab.sh b/myrobotlab.sh index ab69d5cbd3..9aa528ea51 100755 --- a/myrobotlab.sh +++ b/myrobotlab.sh @@ -2,8 +2,7 @@ REPO_FILE=libraries/repo.json -# fancy way to get real cwd ? -APPDIR="$(dirname -- "$(readlink -f -- "${0}")" )" +APPDIR="$(dirname -- ${0})" echo APPDIR=${APPDIR} diff --git a/pom.xml b/pom.xml index 8c710c72e4..a1507381a2 100644 --- a/pom.xml +++ b/pom.xml @@ -163,30 +163,14 @@ - - - - - - - org.boofcv - boofcv-core - 0.31 - provided - - - org.boofcv - boofcv-swing - 0.31 - provided - + org.boofcv - boofcv-openkinect - 0.31 + boofcv-all + 0.40.1 provided - + @@ -308,8 +292,58 @@ org.apache.tika tika-core - 1.22 + 2.8.0 provided + + + org.slf4j + * + + + log4j + * + + + org.apache.logging.log4j + * + + + com.fasterxml.jackson.core + * + + + io.netty + * + + + + + org.apache.tika + tika-parser-audiovideo-module + 2.8.0 + provided + + + org.slf4j + * + + + log4j + * + + + org.apache.logging.log4j + * + + + com.fasterxml.jackson.core + * + + + io.netty + * + + org.apache.opennlp @@ -488,6 +522,12 @@ 3.3.2-stable provided + + org.jmonkeyengine + jme3-plugins + 3.3.2-stable + provided + org.jmonkeyengine jme3-blender @@ -1228,6 +1268,18 @@ 0.10.9.7 provided + + org.bytedeco + cpython-platform + 3.10.8-1.5.8 + provided + + + org.bytedeco + cpython + 3.10.8-1.5.8 + provided + @@ -1279,9 +1331,9 @@ 1.2.3 - com.google.code.gson - gson - 2.8.5 + net.bytebuddy + byte-buddy + 1.12.16 com.fasterxml.jackson.core @@ -1723,6 +1775,10 @@ **/*.java + + src/main/resources + ${project.basedir} + @@ -1952,7 +2008,7 @@ org.apache.maven.plugins 2.22.2 - -Djava.library.path=libraries/native -Djna.library.path=libraries/native ${argLine} + -Djava.library.path=libraries/native -Djna.library.path=libraries/native **/*Test.java @@ -1960,12 +2016,6 @@ **/integration/* - diff --git a/publish-github.sh b/publish-github.sh index 587fef8c8e..4ab19a4e48 100755 --- a/publish-github.sh +++ b/publish-github.sh @@ -17,4 +17,4 @@ echo "build: $build"; # echo "token: $token"; # from - https://docs.github.com/en/rest/releases/releases#create-a-release -curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: token $token" https://api.github.com/repos/MyRobotLab/myrobotlab/releases -d "{\"tag_name\":\"$version\",\"target_commitish\":\"develop\",\"name\":\"$version Nixie\",\"body\":\"## MyRobotLab Nixie Release\r\n\r\nOpen Source Framework for Robotics and Creative Machine Control\r\n *You know, for robots!*\r\n\r\n* Project Website http:\/\/myrobotlab.org \r\n* Project Discord https:\/\/discord.gg\/AfScp5x8r5\r\n* Download [Nixie $version](https:\/\/build.myrobotlab.org:8443\/job\/myrobotlab\/job\/develop\/$build\/artifact\/target\/myrobotlab.zip)\r\n* [JavDocs](https:\/\/build.myrobotlab.org:8443\/job\/myrobotlab\/job\/develop\/$build\/artifact\/target\/site\/apidocs\/org\/myrobotlab\/service\/package-summary.html)\r\n## Base Requirements\r\n\r\nYou will need Java 11 or newer. If you are only running MyRobotLab you need the JRE (Java Runtime Environment.) If you are going to be building from source, you'll need the JDK (Java Development Kit) Oracle or OpenJDK will work.\r\n \",\"draft\":false,\"prerelease\":false,\"generate_release_notes\":true}" +curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: token $token" https://api.github.com/repos/MyRobotLab/myrobotlab/releases -d "{\"tag_name\":\"$version\",\"target_commitish\":\"develop\",\"name\":\"$version Nixie\",\"body\":\"## MyRobotLab Nixie Release\r\n\r\nOpen Source Framework for Robotics and Creative Machine Control\r\n *You know, for robots!*\r\n\r\n* Project Website http:\/\/myrobotlab.org \r\n* Project Discord https:\/\/discord.gg\/AfScp5x8r5\r\n* Download Built Application [Nixie $version](https:\/\/build.myrobotlab.org:8443\/job\/myrobotlab\/job\/develop\/$build\/artifact\/target\/myrobotlab.zip)\r\n* [JavDocs](https:\/\/build.myrobotlab.org:8443\/job\/myrobotlab\/job\/develop\/$build\/artifact\/target\/site\/apidocs\/org\/myrobotlab\/service\/package-summary.html)\r\n## Base Requirements\r\n\r\nYou will need Java 11 or newer. If you are only running MyRobotLab you need the JRE (Java Runtime Environment.) If you are going to be building from source, you'll need the JDK (Java Development Kit) Oracle or OpenJDK will work.\r\n \",\"draft\":false,\"prerelease\":false,\"generate_release_notes\":true}" diff --git a/src/main/java/org/myrobotlab/arduino/BoardInfo.java b/src/main/java/org/myrobotlab/arduino/BoardInfo.java index 03c6e9fcdb..517165b5cd 100644 --- a/src/main/java/org/myrobotlab/arduino/BoardInfo.java +++ b/src/main/java/org/myrobotlab/arduino/BoardInfo.java @@ -7,23 +7,47 @@ /** * BoardInfo is all info which needs to be published only once after connection + * + * FIXME - make a BoardInfo that has more generic information in common with many board types + * */ public class BoardInfo implements Serializable { private static final long serialVersionUID = 1L; transient public final static Logger log = LoggerFactory.getLogger(BoardInfo.class); - Integer version; - Integer boardTypeId; - Integer microsPerLoop; - Integer sram; - Integer activePins; - DeviceSummary[] deviceSummary; // deviceList with types + /** + * version of firmware + */ + public Integer version; + + /** + * id of board type - FIXME change to string + */ + public Integer boardTypeId; + + /** + * Number of microseconds arduino uses to pass through a + * control loop in MrlComm - very Arduino/MrlComm specific + * FIXME - make generalized BoardType that can report useful information + * from any type of board with pins + */ + public Integer microsPerLoop; + + /** + * + */ + public Integer sram; + public Integer activePins; + public DeviceSummary[] deviceSummary; // deviceList with types - String boardTypeName; + public String boardTypeName; - long heartbeatMs; - long receiveTs; + public long heartbeatMs; + public long receiveTs; + + public BoardInfo() { + } public BoardInfo(Integer version, Integer boardTypeId, String boardTypeName, Integer microsPerLoop, Integer sram, Integer activePins, DeviceSummary[] deviceSummary, long boardInfoRequestTs) { diff --git a/src/main/java/org/myrobotlab/audio/AudioProcessor.java b/src/main/java/org/myrobotlab/audio/AudioProcessor.java index 1990c057e9..2ca8f7aa28 100644 --- a/src/main/java/org/myrobotlab/audio/AudioProcessor.java +++ b/src/main/java/org/myrobotlab/audio/AudioProcessor.java @@ -247,6 +247,7 @@ public AudioData play(AudioData data) { // System.gc(); + audioFile.invoke("publishPeak", 0); audioFile.invoke("publishAudioEnd", data); synchronized (data) { diff --git a/src/main/java/org/myrobotlab/boofcv/BoofCVFilter.java b/src/main/java/org/myrobotlab/boofcv/BoofCVFilter.java new file mode 100644 index 0000000000..c8d5ff5736 --- /dev/null +++ b/src/main/java/org/myrobotlab/boofcv/BoofCVFilter.java @@ -0,0 +1,52 @@ +package org.myrobotlab.boofcv; + +import org.myrobotlab.cv.CVFilter; +import org.myrobotlab.service.BoofCV; + +import boofcv.struct.image.ImageBase; + +public abstract class BoofCVFilter implements CVFilter { + + protected String name; + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + protected boolean enabled = true; + transient BoofCV boofcv = null; + + public BoofCVFilter(String name) { + this.name = name; + } + + @Override + public void disable() { + enabled = false; + } + + @Override + public void enable() { + enabled = true; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + + public void setBoofCV(BoofCV boofcv) { + this.boofcv = boofcv; + } + + public abstract ImageBase process(ImageBase image) throws InterruptedException; + + // override if necessary + public void release() { + } + +} diff --git a/src/main/java/org/myrobotlab/boofcv/BoofCVFilterTrackerObjectQuad.java b/src/main/java/org/myrobotlab/boofcv/BoofCVFilterTrackerObjectQuad.java new file mode 100644 index 0000000000..b75014235d --- /dev/null +++ b/src/main/java/org/myrobotlab/boofcv/BoofCVFilterTrackerObjectQuad.java @@ -0,0 +1,68 @@ +package org.myrobotlab.boofcv; + +import java.awt.Dimension; +import java.awt.image.BufferedImage; + +import boofcv.abst.tracker.TrackerObjectQuad; +import boofcv.factory.tracker.FactoryTrackerObjectQuad; +import boofcv.gui.image.ShowImages; +import boofcv.gui.tracker.TrackerObjectQuadPanel; +import boofcv.struct.image.GrayU8; +import boofcv.struct.image.ImageBase; +import georegression.struct.shapes.Quadrilateral_F64; + +public class BoofCVFilterTrackerObjectQuad extends BoofCVFilter { + + protected transient TrackerObjectQuad tracker = null; + + protected Quadrilateral_F64 location = null; + + protected boolean visible = true; + + protected boolean trackerInitialized = false; + + protected transient TrackerObjectQuadPanel gui = null; + + public BoofCVFilterTrackerObjectQuad(String name) { + super(name); + } + + @Override + public ImageBase process(ImageBase frame) throws InterruptedException { + + if (!trackerInitialized && location != null) { + tracker = FactoryTrackerObjectQuad.circulant(null, GrayU8.class); + tracker.initialize(frame, location); + trackerInitialized = true; + } + + // FIXME - probably replace BoofCV servie native gui with gui supplied by + // filter + // + if (gui == null) { + gui = new TrackerObjectQuadPanel(null); + gui.setPreferredSize(new Dimension(frame.getWidth(), frame.getHeight())); + gui.setImageUI((BufferedImage) boofcv.getGuiImage()); + gui.setTarget(location, true); + ShowImages.showWindow(gui, "Tracking Results", true); + } + + if (location != null) { + visible = tracker.process(frame, location); + + if (gui != null) { + gui.setImageUI((BufferedImage)boofcv.getGuiImage()); + gui.setTarget(location, visible); + gui.repaint(); + } + } + + return frame; + } + + public void setLocation(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) { + location = new Quadrilateral_F64(x0, y0, x1, y1, x2, y2, x3, y3); + trackerInitialized = false; + } + +} diff --git a/src/main/java/org/myrobotlab/boofcv/BoofCvData.java b/src/main/java/org/myrobotlab/boofcv/BoofCvData.java deleted file mode 100644 index f29dc4e419..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/BoofCvData.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.myrobotlab.boofcv; - -import java.util.List; -import java.util.Set; - -import org.myrobotlab.cv.CvData; -import org.myrobotlab.math.geometry.PointCloud; - -public class BoofCvData extends CvData { - - private static final long serialVersionUID = 1L; - - @Override - public Set getKeySet() { - // TODO Auto-generated method stub - return null; - } - - @Override - public List getPointCloudList() { - // TODO Auto-generated method stub - return null; - } - - @Override - public PointCloud getPointCloud() { - // TODO Auto-generated method stub - return null; - } - -} diff --git a/src/main/java/org/myrobotlab/boofcv/CaptureCalibrationImagesApp.java b/src/main/java/org/myrobotlab/boofcv/CaptureCalibrationImagesApp.java deleted file mode 100644 index 6bdc9d5682..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/CaptureCalibrationImagesApp.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import org.ddogleg.struct.GrowQueue_I8; -import org.openkinect.freenect.Context; -import org.openkinect.freenect.Device; -import org.openkinect.freenect.Freenect; -import org.openkinect.freenect.Resolution; - -import com.sun.jna.NativeLibrary; - -import boofcv.gui.image.ImagePanel; -import boofcv.gui.image.ShowImages; -import boofcv.io.image.ConvertBufferedImage; -import boofcv.io.image.UtilImageIO; -import boofcv.misc.BoofMiscOps; -import boofcv.openkinect.StreamOpenKinectRgbDepth; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; - -/** - * Displays RGB image from the kinect and then pauses after a set period of - * time. At which point the user can press 'y' or 'n' to indicate yes or no for - * saving the RGB and depth images. Useful when collecting calibration images - * - * @author Peter Abeles - */ -public class CaptureCalibrationImagesApp implements KeyListener, StreamOpenKinectRgbDepth.Listener { - { - // be sure to set OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY to the - // location of your shared library! - NativeLibrary.addSearchPath("freenect", OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY); - } - - String directory; - int period; - Resolution resolution = Resolution.MEDIUM; - - BufferedImage buffRgb; - int frameNumber; - - GrowQueue_I8 buffer = new GrowQueue_I8(1); - - ImagePanel gui; - - Planar savedRgb; - GrayU16 savedDepth; - - volatile boolean updateDisplay; - volatile boolean savedImages; - volatile int userChoice; - - String text; - long timeText; - - public CaptureCalibrationImagesApp(int period, String directory) { - this.period = period; - this.directory = directory; - } - - public void process() throws IOException { - - // make sure there is a "log" directory - new File("log").mkdir(); - - int w = UtilOpenKinect.getWidth(resolution); - int h = UtilOpenKinect.getHeight(resolution); - - buffRgb = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); - - savedRgb = new Planar<>(GrayU8.class, w, h, 3); - savedDepth = new GrayU16(w, h); - - gui = ShowImages.showWindow(buffRgb, "Kinect RGB"); - gui.addKeyListener(this); - gui.requestFocus(); - - StreamOpenKinectRgbDepth stream = new StreamOpenKinectRgbDepth(); - Context kinect = Freenect.createContext(); - - if (kinect.numDevices() < 0) - throw new RuntimeException("No kinect found!"); - - Device device = kinect.openDevice(0); - - stream.start(device, resolution, this); - - long targetTime = System.currentTimeMillis() + period; - updateDisplay = true; - while (true) { - BoofMiscOps.pause(100); - - if (targetTime < System.currentTimeMillis()) { - userChoice = -1; - savedImages = false; - updateDisplay = false; - while (true) { - if (savedImages && userChoice != -1) { - if (userChoice == 1) { - UtilImageIO.savePPM(savedRgb, String.format(directory + "rgb%07d.ppm", frameNumber), buffer); - UtilOpenKinect.saveDepth(savedDepth, String.format(directory + "depth%07d.depth", frameNumber), buffer); - frameNumber++; - text = "Image Saved!"; - } else { - text = "Image Discarded!"; - } - timeText = System.currentTimeMillis() + 500; - updateDisplay = true; - targetTime = System.currentTimeMillis() + period; - break; - } - BoofMiscOps.pause(50); - } - } - } - } - - @Override - public void processKinect(Planar rgb, GrayU16 depth, long timeRgb, long timeDepth) { - - if (updateDisplay) { - ConvertBufferedImage.convertTo_U8(rgb, buffRgb, true); - - if (timeText >= System.currentTimeMillis()) { - Graphics2D g2 = buffRgb.createGraphics(); - g2.setFont(new Font(Font.MONOSPACED, Font.BOLD, 30)); - g2.setColor(Color.RED); - g2.drawString(text, rgb.width / 2 - 100, rgb.height / 2); - } - - gui.repaint(); - } else if (!savedImages) { - savedRgb.setTo(rgb); - savedDepth.setTo(depth); - savedImages = true; - } - } - - public static void main(String args[]) throws IOException { - CaptureCalibrationImagesApp app = new CaptureCalibrationImagesApp(3000, "log/"); - app.process(); - } - - @Override - public void keyTyped(KeyEvent e) { - if (e.getKeyChar() == 'y') - userChoice = 1; - else - userChoice = 0; - } - - @Override - public void keyPressed(KeyEvent e) { - } - - @Override - public void keyReleased(KeyEvent e) { - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/CreateRgbPointCloudFileApp.java b/src/main/java/org/myrobotlab/boofcv/CreateRgbPointCloudFileApp.java deleted file mode 100644 index 9cdde9d86b..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/CreateRgbPointCloudFileApp.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2011-2016, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.io.DataOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; - -import org.ddogleg.struct.FastQueue; - -import boofcv.alg.depth.VisualDepthOps; -import boofcv.io.calibration.CalibrationIO; -import boofcv.io.image.UtilImageIO; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.FastQueueArray_I32; -import boofcv.struct.calib.CameraPinholeRadial; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; -import georegression.struct.point.Point3D_F64; - -/** - * Loads kinect observations and saves a point cloud with rgb information to a - * file in a simple CSV format - * - * @author Peter Abeles - */ -public class CreateRgbPointCloudFileApp { - public static void main(String args[]) throws IOException { - String baseDir = "log/"; - - String nameRgb = baseDir + "rgb0000000.ppm"; - String nameDepth = baseDir + "depth0000000.depth"; - String nameCalib = baseDir + "intrinsic.yaml"; - - CameraPinholeRadial param = CalibrationIO.load(nameCalib); - - GrayU16 depth = new GrayU16(1, 1); - Planar rgb = new Planar<>(GrayU8.class, 1, 1, 3); - - UtilImageIO.loadPPM_U8(nameRgb, rgb, null); - UtilOpenKinect.parseDepth(nameDepth, depth, null); - - FastQueue cloud = new FastQueue(Point3D_F64.class, true); - FastQueueArray_I32 cloudColor = new FastQueueArray_I32(3); - - VisualDepthOps.depthTo3D(param, rgb, depth, cloud, cloudColor); - - DataOutputStream file = new DataOutputStream(new FileOutputStream("kinect_pointcloud.txt")); - - file.write("# Kinect RGB Point cloud. Units: millimeters. Format: X Y Z R G B\n".getBytes()); - - for (int i = 0; i < cloud.size; i++) { - Point3D_F64 p = cloud.get(i); - int[] color = cloudColor.get(i); - - String line = String.format("%.10f %.10f %.10f %d %d %d\n", p.x, p.y, p.z, color[0], color[1], color[2]); - file.write(line.getBytes()); - } - file.close(); - - System.out.println("Total points = " + cloud.size); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/DisplayKinectPointCloudApp.java b/src/main/java/org/myrobotlab/boofcv/DisplayKinectPointCloudApp.java deleted file mode 100644 index 8f394ac13d..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/DisplayKinectPointCloudApp.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2011-2018, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.Dimension; -import java.io.File; -import java.io.IOException; - -import org.ddogleg.struct.FastQueue; -import org.myrobotlab.framework.Service; -import org.myrobotlab.service.BoofCv; - -import boofcv.alg.depth.VisualDepthOps; -import boofcv.alg.geo.PerspectiveOps; -import boofcv.gui.image.ShowImages; -import boofcv.io.calibration.CalibrationIO; -import boofcv.io.image.UtilImageIO; -import boofcv.struct.FastQueueArray_I32; -import boofcv.struct.calib.CameraPinholeRadial; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.ImageType; -import boofcv.struct.image.Planar; -import boofcv.visualize.PointCloudViewer; -import boofcv.visualize.VisualizeData; -import georegression.struct.point.Point3D_F64; - -/** - * Loads kinect data from two files and displays the cloud in a 3D simple - * viewer. - * - * @author Peter Abeles - */ -public class DisplayKinectPointCloudApp { - - public static void main(String args[]) throws IOException { - // String baseDir = UtilIO.pathExample("kinect/basket"); - String baseDir = Service.getResourceDir(BoofCv.class); - String nameRgb = "basket_rgb.png"; - String nameDepth = "basket_depth.png"; - String nameCalib = "intrinsic.yaml"; - - CameraPinholeRadial param = CalibrationIO.load(new File(baseDir, nameCalib)); - - GrayU16 depth = UtilImageIO.loadImage(new File(baseDir, nameDepth), false, ImageType.single(GrayU16.class)); - Planar rgb = UtilImageIO.loadImage(new File(baseDir, nameRgb), true, ImageType.pl(3, GrayU8.class)); - - FastQueue cloud = new FastQueue(Point3D_F64.class, true); - FastQueueArray_I32 cloudColor = new FastQueueArray_I32(3); - - VisualDepthOps.depthTo3D(param, rgb, depth, cloud, cloudColor); - - PointCloudViewer viewer = VisualizeData.createPointCloudViewer(); - viewer.setCameraHFov(PerspectiveOps.computeHFov(param)); - viewer.setTranslationStep(10.0); - viewer.getComponent().setPreferredSize(new Dimension(rgb.width, rgb.height)); - - for (int i = 0; i < cloud.size; i++) { - Point3D_F64 p = cloud.get(i); - int[] color = cloudColor.get(i); - int c = (color[0] << 16) | (color[1] << 8) | color[2]; - viewer.addPoint(p.x, p.y, p.z, c); - } - - ShowImages.showWindow(viewer.getComponent(), "Point Cloud", true); - System.out.println("Total points = " + cloud.size); - - // BufferedImage depthOut = VisualizeImageData.disparity(depth, null, 0, - // UtilOpenKinect.FREENECT_DEPTH_MM_MAX_VALUE, 0); - // ShowImages.showWindow(depthOut,"Depth Image", true); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/ExampleDepthPointCloud.java b/src/main/java/org/myrobotlab/boofcv/ExampleDepthPointCloud.java deleted file mode 100644 index a96e8d317a..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/ExampleDepthPointCloud.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.Dimension; -import java.awt.image.BufferedImage; -import java.io.IOException; - -import org.ddogleg.struct.FastQueue; -import org.myrobotlab.framework.Service; -import org.myrobotlab.service.BoofCv; - -import boofcv.alg.depth.VisualDepthOps; -import boofcv.alg.geo.PerspectiveOps; -import boofcv.alg.misc.ImageStatistics; -import boofcv.gui.image.ShowImages; -import boofcv.gui.image.VisualizeImageData; -import boofcv.io.calibration.CalibrationIO; -import boofcv.io.image.ConvertBufferedImage; -import boofcv.io.image.UtilImageIO; -import boofcv.struct.FastQueueArray_I32; -import boofcv.struct.calib.VisualDepthParameters; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; -import boofcv.visualize.PointCloudViewer; -import boofcv.visualize.VisualizeData; -import georegression.struct.point.Point3D_F64; - -/** - * Displays RGB image from the kinect and then pauses after a set period of time. At which point the user can press - * 'y' or 'n' to indicate yes or no for saving the RGB and depth images. Useful when collecting calibration images - * - * @author Peter Abeles - */ -/** - * Example of how to create a point cloud from a RGB-D (Kinect) sensor. Data is - * loaded from two files, one for the visual image and one for the depth image. - * - * @author Peter Abeles - */ -public class ExampleDepthPointCloud { - - public static void main(String args[]) throws IOException { - - // String baseDir = "src/main/resources/resource/BoofCv"; - String nameRgb = Service.getResourceDir(BoofCv.class, "basket_rgb.png"); - String nameDepth = Service.getResourceDir(BoofCv.class, "basket_depth.png"); - String nameCalib = Service.getResourceDir(BoofCv.class, "visualdepth.yaml"); - - VisualDepthParameters param = CalibrationIO.load(nameCalib); - - BufferedImage buffered = UtilImageIO.loadImage(nameRgb); - Planar rgb = ConvertBufferedImage.convertFromPlanar(buffered, null, true, GrayU8.class); - GrayU16 depth = ConvertBufferedImage.convertFrom(UtilImageIO.loadImage(nameDepth), null, GrayU16.class); - - FastQueue cloud = new FastQueue<>(Point3D_F64.class, true); - FastQueueArray_I32 cloudColor = new FastQueueArray_I32(3); - - VisualDepthOps.depthTo3D(param.visualParam, rgb, depth, cloud, cloudColor); - - PointCloudViewer viewer = VisualizeData.createPointCloudViewer(); - viewer.setCameraHFov(PerspectiveOps.computeHFov(param.visualParam)); - viewer.setTranslationStep(15); - - for (int i = 0; i < cloud.size; i++) { - Point3D_F64 p = cloud.get(i); - int[] color = cloudColor.get(i); - int c = (color[0] << 16) | (color[1] << 8) | color[2]; - viewer.addPoint(p.x, p.y, p.z, c); - } - viewer.getComponent().setPreferredSize(new Dimension(rgb.width, rgb.height)); - - // ---------- Display depth image - // use the actual max value in the image to maximize its appearance - int maxValue = ImageStatistics.max(depth); - BufferedImage depthOut = VisualizeImageData.disparity(depth, null, 0, maxValue, 0); - ShowImages.showWindow(depthOut, "Depth Image", true); - - // ---------- Display colorized point cloud - ShowImages.showWindow(viewer.getComponent(), "Point Cloud", true); - System.out.println("Total points = " + cloud.size); - } -} \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/boofcv/ExampleVisualOdometryDepth.java b/src/main/java/org/myrobotlab/boofcv/ExampleVisualOdometryDepth.java deleted file mode 100644 index 45dee26c27..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/ExampleVisualOdometryDepth.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (c) 2011-2017, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * Extended by Mats Önnerby to use then Kinect 360 as input instead of the .mpg files - * used in the bare bones example it's based on - * - */ - -package org.myrobotlab.boofcv; - -import java.awt.image.BufferedImage; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.ejml.data.DMatrixRMaj; -import org.myrobotlab.framework.Service; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.service.BoofCv; -import org.openkinect.freenect.Resolution; -import org.slf4j.Logger; - -import boofcv.abst.feature.detect.interest.ConfigGeneralDetector; -import boofcv.abst.feature.tracker.PointTrackerTwoPass; -import boofcv.abst.sfm.AccessPointTracks3D; -import boofcv.abst.sfm.d3.DepthVisualOdometry; -import boofcv.abst.sfm.d3.VisualOdometry; -import boofcv.alg.sfm.DepthSparse3D; -import boofcv.alg.tracker.klt.PkltConfig; -import boofcv.factory.feature.tracker.FactoryPointTrackerTwoPass; -import boofcv.factory.sfm.FactoryVisualOdometry; -import boofcv.gui.image.ImagePanel; -import boofcv.gui.image.ShowImages; -import boofcv.gui.image.VisualizeImageData; -import boofcv.io.MediaManager; -import boofcv.io.calibration.CalibrationIO; -import boofcv.io.image.ConvertBufferedImage; -import boofcv.io.image.SimpleImageSequence; -import boofcv.io.wrapper.DefaultMediaManager; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.calib.VisualDepthParameters; -import boofcv.struct.distort.DoNothing2Transform2_F32; -import boofcv.struct.image.GrayS16; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.ImageType; -import georegression.struct.point.Vector3D_F64; -import georegression.struct.se.Se3_F64; - -/** - * Bare bones example showing how to estimate the camera's ego-motion using a - * depth camera system, e.g. Kinect. Additional information on the scene can be - * optionally extracted from the algorithm if it implements AccessPointTracks3D. - * - * @author Peter Abeles - */ -public class ExampleVisualOdometryDepth { - - transient public final static Logger log = LoggerFactory.getLogger(ExampleVisualOdometryDepth.class); - - Resolution resolution = Resolution.MEDIUM; - - BufferedImage buffRgb; - BufferedImage buffDepth; - - BufferedImage outRgb; - ImagePanel guiRgb; - - BufferedImage outDepth; - ImagePanel guiDepth; - - Double xTot = 0.0; - Double yTot = 0.0; - Double zTot = 0.0; - - public void process() { - - MediaManager media = DefaultMediaManager.INSTANCE; - - // String directory = UtilIO.pathExample("kinect/straight"); - String directory = Service.getResourceDir(BoofCv.class); - log.info("Using directory {}", directory); - - // load camera description and the video sequence - VisualDepthParameters param = CalibrationIO.load(media.openFile(Service.getResourceDir(BoofCv.class, "visualdepth.yaml"))); - - // specify how the image features are going to be tracked - PkltConfig configKlt = new PkltConfig(); - configKlt.pyramidScaling = new int[] { 1, 2, 4, 8 }; - configKlt.templateRadius = 3; - - PointTrackerTwoPass tracker = FactoryPointTrackerTwoPass.klt(configKlt, new ConfigGeneralDetector(600, 3, 1), GrayU8.class, GrayS16.class); - - DepthSparse3D sparseDepth = new DepthSparse3D.I<>(1e-3); - - // declares the algorithm - DepthVisualOdometry visualOdometry = FactoryVisualOdometry.depthDepthPnP(1.5, 120, 2, 200, 50, true, sparseDepth, tracker, GrayU8.class, GrayU16.class); - - // Pass in intrinsic/extrinsic calibration. This can be changed in the - // future. - visualOdometry.setCalibration(param.visualParam, new DoNothing2Transform2_F32()); - - // Process the video sequence and output the location plus number of inliers - SimpleImageSequence videoVisual = media.openVideo(directory + "/" + "rgb.mjpeg", ImageType.single(GrayU8.class)); - SimpleImageSequence videoDepth = media.openVideo(directory + "/" + "depth.mpng", ImageType.single(GrayU16.class)); - - while (videoVisual.hasNext()) { - - GrayU8 visual = videoVisual.next(); - GrayU16 depth = videoDepth.next(); - - // Handle the Depth stream - if (outDepth == null) { - BufferedImage mode = videoDepth.getGuiImage(); - depth.reshape(mode.getWidth(), mode.getHeight()); - outDepth = new BufferedImage(depth.width, depth.height, BufferedImage.TYPE_INT_RGB); - guiDepth = ShowImages.showWindow(outDepth, "Depth Image"); - } - - if (!visualOdometry.process(visual, depth)) { - throw new RuntimeException("VO Failed!"); - } - - VisualizeImageData.disparity(depth, outDepth, 0, UtilOpenKinect.FREENECT_DEPTH_MM_MAX_VALUE, 0); - guiDepth.repaint(); - - // Handle the video stream - if (outRgb == null) { - BufferedImage mode = videoDepth.getGuiImage(); - visual.reshape(mode.getWidth(), mode.getHeight()); - outRgb = new BufferedImage(visual.width, visual.height, BufferedImage.TYPE_INT_RGB); - guiRgb = ShowImages.showWindow(outRgb, "RGB Image"); - } - - ConvertBufferedImage.convertTo(visual, outRgb, true); - guiRgb.repaint(); - - Se3_F64 leftToWorld = visualOdometry.getCameraToWorld(); - Vector3D_F64 T = leftToWorld.getT(); - DMatrixRMaj R = leftToWorld.getR(); - - System.out.printf("Location %8.2f %8.2f %8.2f inliers %s\n", T.x, T.y, T.z, inlierPercent(visualOdometry)); - int cols = R.getNumCols(); - int rows = R.getNumRows(); - System.out.printf("Rotation matrix \n"); - for (int x = 0; x < cols; x++) { - for (int y = 0; y < rows; y++) { - Double rotation = R.get(x, y); - System.out.printf("%S", rotation); - } - System.out.printf("\n"); - } - System.out.printf("\n"); - xTot = xTot + T.x; - yTot = yTot + T.y; - zTot = zTot + T.z; - // System.out.printf("xT, yT, Zt, %8.2f %8.2f %8.2f \n ", xTot, yTot, - // zTot); - - } - } - - public static String toAbsolutePath(String maybeRelative) { - Path path = Paths.get(maybeRelative); - Path effectivePath = path; - if (!path.isAbsolute()) { - Path base = Paths.get(""); - effectivePath = base.resolve(path).toAbsolutePath(); - } - return effectivePath.normalize().toString(); - } - - /** - * If the algorithm implements AccessPointTracks3D, then count the number of - * inlier features and return a string. - * - * @param alg - * algorithm - * @return a string - */ - public static String inlierPercent(VisualOdometry alg) { - if (!(alg instanceof AccessPointTracks3D)) - return ""; - - AccessPointTracks3D access = (AccessPointTracks3D) alg; - - int count = 0; - int N = access.getAllTracks().size(); - for (int i = 0; i < N; i++) { - if (access.isInlier(i)) - count++; - } - - return String.format("%%%5.3f", 100.0 * count / N); - } - - public static void main(String args[]) { - ExampleVisualOdometryDepth app = new ExampleVisualOdometryDepth(); - app.process(); - } -} \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/boofcv/IntrinsicToDepthParameters.java b/src/main/java/org/myrobotlab/boofcv/IntrinsicToDepthParameters.java deleted file mode 100644 index 7fbbb0a0d8..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/IntrinsicToDepthParameters.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2011-2016, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.io.File; - -import org.myrobotlab.framework.Service; -import org.myrobotlab.service.BoofCv; - -import boofcv.io.calibration.CalibrationIO; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.calib.CameraPinholeRadial; -import boofcv.struct.calib.VisualDepthParameters; - -/** - * Loads an intrinsic parameters file for the RGB camera and creates a - * VisualDepthParameters for the Kinect. - * - * @author Peter Abeles - */ -public class IntrinsicToDepthParameters { - - public static void main(String args[]) { - // String baseDir = UtilIO.pathExample("kinect/basket"); - String baseDir = Service.getResourceDir(BoofCv.class); - - CameraPinholeRadial intrinsic = CalibrationIO.load(new File(baseDir, "intrinsic.yaml")); - - VisualDepthParameters depth = new VisualDepthParameters(); - - depth.setVisualParam(intrinsic); - depth.setMaxDepth(UtilOpenKinect.FREENECT_DEPTH_MM_MAX_VALUE); - depth.setPixelNoDepth(UtilOpenKinect.FREENECT_DEPTH_MM_NO_VALUE); - - CalibrationIO.save(depth, baseDir + "visualdepth.yaml"); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/LogKinectDataApp.java b/src/main/java/org/myrobotlab/boofcv/LogKinectDataApp.java deleted file mode 100644 index efe6a51e35..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/LogKinectDataApp.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.image.BufferedImage; -import java.io.DataOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; - -import org.ddogleg.struct.GrowQueue_I8; -import org.openkinect.freenect.Context; -import org.openkinect.freenect.Device; -import org.openkinect.freenect.Freenect; -import org.openkinect.freenect.Resolution; - -import com.sun.jna.NativeLibrary; - -import boofcv.gui.image.ImagePanel; -import boofcv.gui.image.ShowImages; -import boofcv.io.image.ConvertBufferedImage; -import boofcv.io.image.UtilImageIO; -import boofcv.misc.BoofMiscOps; -import boofcv.openkinect.StreamOpenKinectRgbDepth; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; - -/** - * @author Peter Abeles - */ -public class LogKinectDataApp implements StreamOpenKinectRgbDepth.Listener { - { - // be sure to set OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY to the - // location of your shared library! - NativeLibrary.addSearchPath("freenect", OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY); - } - - int maxImages; - boolean showImage; - Resolution resolution = Resolution.MEDIUM; - - BufferedImage buffRgb; - int frameNumber; - - DataOutputStream logFile; - - GrowQueue_I8 buffer = new GrowQueue_I8(1); - - ImagePanel gui; - - public LogKinectDataApp(int maxImages, boolean showImage) { - this.maxImages = maxImages; - this.showImage = showImage; - } - - public void process() throws IOException { - - logFile = new DataOutputStream(new FileOutputStream("log/timestamps.txt")); - logFile.write("# Time stamps for rgb and depth cameras.\n".getBytes()); - - int w = UtilOpenKinect.getWidth(resolution); - int h = UtilOpenKinect.getHeight(resolution); - - buffRgb = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); - - if (showImage) { - gui = ShowImages.showWindow(buffRgb, "Kinect RGB"); - } - - StreamOpenKinectRgbDepth stream = new StreamOpenKinectRgbDepth(); - Context kinect = Freenect.createContext(); - - if (kinect.numDevices() < 0) - throw new RuntimeException("No kinect found!"); - - Device device = kinect.openDevice(0); - - stream.start(device, resolution, this); - - if (maxImages > 0) { - while (frameNumber < maxImages) { - System.out.printf("Total saved %d\n", frameNumber); - BoofMiscOps.pause(100); - } - stream.stop(); - System.out.println("Exceeded max images"); - System.exit(0); - } - } - - @Override - public void processKinect(Planar rgb, GrayU16 depth, long timeRgb, long timeDepth) { - System.out.println(frameNumber + " " + timeRgb); - try { - logFile.write(String.format("%10d %d %d\n", frameNumber, timeRgb, timeDepth).getBytes()); - logFile.flush(); - UtilImageIO.savePPM(rgb, String.format("log/rgb%07d.ppm", frameNumber), buffer); - UtilOpenKinect.saveDepth(depth, String.format("log/depth%07d.depth", frameNumber), buffer); - frameNumber++; - - if (showImage) { - ConvertBufferedImage.convertTo_U8(rgb, buffRgb, true); - gui.repaint(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - public static void main(String args[]) throws IOException { - LogKinectDataApp app = new LogKinectDataApp(1000000, false); - app.process(); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/OpenKinectExampleParam.java b/src/main/java/org/myrobotlab/boofcv/OpenKinectExampleParam.java deleted file mode 100644 index 448c4d63cc..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/OpenKinectExampleParam.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -/** - * Common parameter used in example code - * - * @author Peter Abeles - */ -public class OpenKinectExampleParam { - - // Modify this link to be where you store your shared library - public static String PATH_TO_SHARED_LIBRARY = "/home/pja/projects/thirdparty/libfreenect/build/lib"; -} diff --git a/src/main/java/org/myrobotlab/boofcv/OpenKinectOdometry.java b/src/main/java/org/myrobotlab/boofcv/OpenKinectOdometry.java deleted file mode 100644 index 5eff21f266..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/OpenKinectOdometry.java +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.Dimension; -import java.io.File; -import java.nio.ByteBuffer; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; - -import org.ddogleg.struct.FastQueue; -import org.ejml.data.DMatrixRMaj; -import org.ejml.equation.Equation; -import org.ejml.equation.Sequence; -import org.myrobotlab.framework.Service; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.service.BoofCv; -import org.openkinect.freenect.Context; -import org.openkinect.freenect.DepthFormat; -import org.openkinect.freenect.DepthHandler; -import org.openkinect.freenect.Device; -import org.openkinect.freenect.FrameMode; -import org.openkinect.freenect.Freenect; -import org.openkinect.freenect.VideoFormat; -import org.openkinect.freenect.VideoHandler; -import org.slf4j.Logger; - -import boofcv.abst.feature.detect.interest.ConfigGeneralDetector; -import boofcv.abst.feature.tracker.PointTrackerTwoPass; -import boofcv.abst.sfm.AccessPointTracks3D; -import boofcv.abst.sfm.d3.DepthVisualOdometry; -import boofcv.abst.sfm.d3.VisualOdometry; -import boofcv.alg.color.ColorRgb; -import boofcv.alg.depth.VisualDepthOps; -import boofcv.alg.geo.PerspectiveOps; -import boofcv.alg.sfm.DepthSparse3D; -import boofcv.alg.tracker.klt.PkltConfig; -import boofcv.factory.feature.tracker.FactoryPointTrackerTwoPass; -import boofcv.factory.sfm.FactoryVisualOdometry; -import boofcv.gui.image.ShowImages; -import boofcv.io.MediaManager; -import boofcv.io.calibration.CalibrationIO; -import boofcv.io.wrapper.DefaultMediaManager; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.FastQueueArray_I32; -import boofcv.struct.calib.CameraPinholeRadial; -import boofcv.struct.calib.VisualDepthParameters; -import boofcv.struct.distort.DoNothing2Transform2_F32; -import boofcv.struct.image.GrayS16; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; -import boofcv.visualize.PointCloudViewer; -import boofcv.visualize.VisualizeData; -import georegression.struct.point.Point3D_F64; -import georegression.struct.point.Vector3D_F64; -import georegression.struct.se.Se3_F64; - -/** - * Example demonstrating how to process and display data from the Kinect. - * - * @author Peter Abeles and Mats Önnerby - * - * This program currently calculates the Odometry ( how much the camera - * has moved as translations and rotations ). Second stage is to - * recalculate the second pointcloud to have the same origin as the - * first. Third stage is to update the original pointcloud with - * information from the new pointcloud to make the world pointcloud - * larger Do we need to keep track of the probability for each point in - * the pointcloud ? Can we transform the pointcloud to larger objects ? - * Like meshes ? - * https://www.mathworks.com/help/vision/examples/3-d-point-cloud-registration-and-stitching.html - * - */ -public class OpenKinectOdometry { - - static final Logger log = LoggerFactory.getLogger(OpenKinectOdometry.class); - - private PointCloudViewer viewer; - private PointCloudViewer viewerFixed; - - private volatile boolean videoAvailable = false; - private volatile boolean depthAvailable = false; - - boolean firstDepth = true; - boolean firstVideo = true; - boolean firstImage = true; - - String baseDir = Service.getResourceDir(BoofCv.class); - String nameCalib = "intrinsic.yaml"; - - Planar rgb = new Planar<>(GrayU8.class, 1, 1, 3); - GrayU16 depth = new GrayU16(1, 1); - - List points = new ArrayList(); - List pointsFixed = new ArrayList(); - int colors[] = new int[1]; - - boolean odometry = true; - - DepthVisualOdometry visualOdometry; - - Double xTot = 0.0; - Double yTot = 0.0; - Double zTot = 0.0; - - // Matrix multiplication initiation - Sequence transform, invert; - DMatrixRMaj pointMatIn = new DMatrixRMaj(4, 1); - DMatrixRMaj pointMatOut = new DMatrixRMaj(4, 1); - DMatrixRMaj transMat = new DMatrixRMaj(4, 4); - Equation eq = new Equation(); - - public void process() { - - Context kinect = Freenect.createContext(); - - if (kinect.numDevices() < 0) - throw new RuntimeException("No kinect found!"); - - Device device = kinect.openDevice(0); - - device.setDepthFormat(DepthFormat.REGISTERED); - device.setVideoFormat(VideoFormat.RGB); - - CameraPinholeRadial param = CalibrationIO.load(new File(baseDir, nameCalib)); - - FastQueue cloud = new FastQueue<>(Point3D_F64.class, true); - FastQueueArray_I32 cloudColor = new FastQueueArray_I32(3); - - if (odometry) { - initOdometry(param); - } - - viewer = VisualizeData.createPointCloudViewer(); - viewer.setCameraHFov(PerspectiveOps.computeHFov(param)); - viewer.setTranslationStep(15); - - device.startDepth(new DepthHandler() { - @Override - public void onFrameReceived(FrameMode mode, ByteBuffer frame, int timestamp) { - processDepth(mode, frame, timestamp); - } - }); - device.startVideo(new VideoHandler() { - @Override - public void onFrameReceived(FrameMode mode, ByteBuffer frame, int timestamp) { - processRgb(mode, frame, timestamp); - } - }); - - long starTime = System.currentTimeMillis(); - while (starTime + 200000 > System.currentTimeMillis()) { - - if (videoAvailable && depthAvailable) { - if (firstImage) { - viewer.getComponent().setPreferredSize(new Dimension(rgb.width, rgb.height)); - if (odometry) { - viewerFixed.getComponent().setPreferredSize(new Dimension(rgb.width, rgb.height)); - } - } - - VisualDepthOps.depthTo3D(param, rgb, depth, cloud, cloudColor); - - if (colors.length != cloud.size()) { - colors = new int[cloud.size()]; - points = new ArrayList(cloud.size()); - } - - points.clear(); - for (int i = 0; i < cloud.size; i++) { - Point3D_F64 p = cloud.get(i); - int[] color = cloudColor.get(i); - int c = (color[0] << 16) | (color[1] << 8) | color[2]; - points.add(p); - colors[i] = c; - } - - log.info("Points size() 1 ", points.size(), "\n"); - viewer.clearPoints(); - viewer.addCloud(points, colors); - - if (firstImage) { - ShowImages.showWindow(viewer.getComponent(), "Point Cloud", true); - if (odometry) { - ShowImages.showWindow(viewerFixed.getComponent(), "Point Cloud Fixed", true); - } - firstImage = false; - } - - // Odometry - if (odometry) { - processOdometry(); - } - - viewer.getComponent().repaint(); - videoAvailable = false; - depthAvailable = false; - } - sleep(10); - } - System.out.println("100 Seconds elapsed"); - - device.stopDepth(); - device.stopVideo(); - device.close(); - } - - private void initOdometry(CameraPinholeRadial param) { - // Initiate and precompile Matrix operations - eq.alias(pointMatIn, "in", transMat, "R", pointMatOut, "out"); - eq.alias(new DMatrixRMaj(1, 1), "in"); - eq.alias(new DMatrixRMaj(1, 1), "R"); - eq.alias(new DMatrixRMaj(1, 1), "out"); - transform = eq.compile("out = R*in"); - invert = eq.compile("R = inv(R)"); - eq.alias(pointMatIn, "in", transMat, "R", pointMatOut, "out"); - - MediaManager media = DefaultMediaManager.INSTANCE; - // String directory = UtilIO.pathExample("kinect/straight"); - String directory = Service.getResourceDir(BoofCv.class) + File.separator; - log.info("Using directory ", directory); - - // load camera description and the video sequence - VisualDepthParameters depthParam = CalibrationIO.load(media.openFile(directory + "visualdepth.yaml")); - - // specify how the image features are going to be tracked - PkltConfig configKlt = new PkltConfig(); - configKlt.pyramidScaling = new int[] { 1, 2, 4, 8 }; - configKlt.templateRadius = 3; - - PointTrackerTwoPass tracker = FactoryPointTrackerTwoPass.klt(configKlt, new ConfigGeneralDetector(600, 3, 1), GrayU8.class, GrayS16.class); - - DepthSparse3D sparseDepth = new DepthSparse3D.I<>(1e-3); - - // declares the algorithm - visualOdometry = FactoryVisualOdometry.depthDepthPnP(1.5, 120, 2, 200, 50, true, sparseDepth, tracker, GrayU8.class, GrayU16.class); - - // Pass in intrinsic/extrinsic calibration. This can be changed in the - // future. - visualOdometry.setCalibration(depthParam.visualParam, new DoNothing2Transform2_F32()); - - viewerFixed = VisualizeData.createPointCloudViewer(); - viewerFixed.setCameraHFov(PerspectiveOps.computeHFov(param)); - viewerFixed.setTranslationStep(15); - } - - private void processOdometry() { - - GrayU8 gray = new GrayU8(rgb.width, rgb.height); - ColorRgb.rgbToGray_Weighted(rgb, gray); - if (!visualOdometry.process(gray, depth)) { - throw new RuntimeException("VO Failed!"); - } - - Se3_F64 leftToWorld = visualOdometry.getCameraToWorld(); - Vector3D_F64 T = leftToWorld.getT(); - DMatrixRMaj R = leftToWorld.getR(); - System.out.printf("Location %8.2f %8.2f %8.2f inliers %s\n", T.x, T.y, T.z, inlierPercent(visualOdometry)); - - int cols = R.getNumCols(); - int rows = R.getNumRows(); - System.out.printf("Rotation matrix \n"); - for (int x = 0; x < cols; x++) { - for (int y = 0; y < rows; y++) { - Double rotation = R.get(x, y); - System.out.printf("%8.2f", rotation); - } - System.out.printf("\n"); - } - System.out.printf("\n"); - pointsFixed.clear(); - // Rotate and transform - - // Load the (4*4) Transformation matrix from Rotations (3*3) and - // Translations (3*1) - // From the Odometry - transMat.set(0, 0, R.get(0, 0)); - transMat.set(1, 0, R.get(1, 0)); - transMat.set(2, 0, R.get(2, 0)); - transMat.set(3, 0, T.getX()); - transMat.set(0, 1, R.get(0, 0)); - transMat.set(1, 1, R.get(1, 1)); - transMat.set(2, 1, R.get(2, 1)); - transMat.set(3, 1, T.getY()); - transMat.set(0, 2, R.get(0, 2)); - transMat.set(1, 2, R.get(1, 2)); - transMat.set(2, 2, R.get(2, 2)); - transMat.set(3, 2, T.getZ()); - transMat.set(0, 3, 0.0); - transMat.set(1, 3, 0.0); - transMat.set(2, 3, 0.0); - transMat.set(3, 3, 1.0); - - // Load the (4*4) Transformation matrix from Rotations (3*3) and - // Translations (3*1) - // No rotation or transformation (unit matrix) - // 1 0 0 0 - // 0 1 0 0 - // 0 0 1 0 - // 0 0 0 1 - transMat.set(0, 0, 1.0); - transMat.set(1, 0, 0.0); - transMat.set(2, 0, 0.0); - transMat.set(3, 0, 0.0); // Translate X - transMat.set(0, 1, 0.0); - transMat.set(1, 1, 1.0); - transMat.set(2, 1, 0.0); - transMat.set(3, 1, 0.0); // Translate Y - transMat.set(0, 2, 0.0); - transMat.set(1, 2, 0.0); - transMat.set(2, 2, 1.0); - transMat.set(3, 2, 0.0); // Translate Z - transMat.set(0, 3, 0.0); - transMat.set(1, 3, 0.0); - transMat.set(2, 3, 0.0); - transMat.set(3, 3, 1.0); - - log.info("Points size() 2", points.size(), "\n"); - for (int i = 0; i < points.size(); i++) { - // Transform from cartesian to homogenous coordinates ( 4 dimensions ) - Point3D_F64 p = points.get(i); - pointMatIn.set(0, 0, p.getX()); - pointMatIn.set(1, 0, p.getY()); - pointMatIn.set(2, 0, p.getZ()); - pointMatIn.set(3, 0, 1.0); - // This statement executes the Matrix multiplication defined in transform - // Log.info("Points in ", pointMatIn.get(0, 0), " ", pointMatIn.get(1, 0), - // " ", pointMatIn.get(2, 0), " ", pointMatIn.get(3, - // 0)); - transform.perform(); - // Log.info("Points out ", pointMatOut.get(0, 0), " ", pointMatOut.get(1, - // 0), " ", pointMatOut.get(2, 0), " ", - // pointMatOut.get(3, 0)); - Point3D_F64 pFixed = new Point3D_F64(pointMatOut.get(0, 0) / pointMatOut.get(3, 0), pointMatOut.get(1, 0) / pointMatOut.get(3, 0), - pointMatOut.get(2, 0) / pointMatOut.get(3, 0)); - pointsFixed.add(pFixed); - } - - viewerFixed.clearPoints(); - viewerFixed.addCloud(pointsFixed, colors); - viewerFixed.getComponent().repaint(); - - } - - private void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - } - - } - - protected void processDepth(FrameMode mode, ByteBuffer frame, int timestamp) { - - // System.out.println("Got depth! "+timestamp); - if (firstDepth) { - depth.reshape(mode.getWidth(), mode.getHeight()); - firstDepth = false; - } - - // Skip frames until the previous depth map has been shown - if (!depthAvailable) { - // Convert the frame to a depth map)) - UtilOpenKinect.bufferDepthToU16(frame, depth); - // Log.info("frame.capacity", frame.capacity()); - // Log.info("depthAvailable", depth.width, depth.height); - depthAvailable = true; - } - } - - protected void processRgb(FrameMode mode, ByteBuffer frame, int timestamp) { - - if (mode.getVideoFormat() != VideoFormat.RGB) { - System.out.println("Bad rgb format!"); - } - - // System.out.println("Got rgb! "+timestamp); - if (firstVideo) { - rgb.reshape(mode.getWidth(), mode.getHeight()); - viewer.getComponent().setPreferredSize(new Dimension(rgb.width, rgb.height)); - firstVideo = false; - } - - // Skip frames until the previous video has been shown - if (!videoAvailable) { - UtilOpenKinect.bufferRgbToMsU8(frame, rgb); - // Log.info("videoAvailable"); - videoAvailable = true; - } - - } - - /** - * If the algorithm implements AccessPointTracks3D, then count the number of - * inlier features and return a string. - * - * @param alg - * algorithm - * @return a string - */ - public static String inlierPercent(VisualOdometry alg) { - if (!(alg instanceof AccessPointTracks3D)) - return ""; - - AccessPointTracks3D access = (AccessPointTracks3D) alg; - - int count = 0; - int N = access.getAllTracks().size(); - for (int i = 0; i < N; i++) { - if (access.isInlier(i)) - count++; - } - - return String.format("%%%5.3f", 100.0 * count / N); - } - - public static String toAbsolutePath(String maybeRelative) { - Path path = Paths.get(maybeRelative); - Path effectivePath = path; - if (!path.isAbsolute()) { - Path base = Paths.get(""); - effectivePath = base.resolve(path).toAbsolutePath(); - } - return effectivePath.normalize().toString(); - } - - public static void main(String args[]) { - OpenKinectOdometry app = new OpenKinectOdometry(); - - app.process(); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/OpenKinectPointCloud.java b/src/main/java/org/myrobotlab/boofcv/OpenKinectPointCloud.java deleted file mode 100644 index 924b57935a..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/OpenKinectPointCloud.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.Dimension; -import java.io.File; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import org.ddogleg.struct.FastQueue; -import org.myrobotlab.framework.Service; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.service.BoofCv; -import org.openkinect.freenect.Context; -import org.openkinect.freenect.DepthFormat; -import org.openkinect.freenect.DepthHandler; -import org.openkinect.freenect.Device; -import org.openkinect.freenect.FrameMode; -import org.openkinect.freenect.Freenect; -import org.openkinect.freenect.VideoFormat; -import org.openkinect.freenect.VideoHandler; -import org.slf4j.Logger; - -import com.sun.jna.NativeLibrary; - -import boofcv.alg.depth.VisualDepthOps; -import boofcv.alg.geo.PerspectiveOps; -import boofcv.gui.image.ShowImages; -import boofcv.io.calibration.CalibrationIO; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.FastQueueArray_I32; -import boofcv.struct.calib.CameraPinholeRadial; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; -import boofcv.visualize.PointCloudViewer; -import boofcv.visualize.VisualizeData; -import georegression.struct.point.Point3D_F64; - -/** - * Example demonstrating how to process and display data from the Kinect. - * - * @author Peter Abeles and Mats Önnerby - */ -public class OpenKinectPointCloud { - - { - // be sure to set OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY to the - // location of your shared library! - NativeLibrary.addSearchPath("freenect", OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY); - } - - static final Logger log = LoggerFactory.getLogger(OpenKinectPointCloud.class); - - private PointCloudViewer viewer; - - private volatile boolean videoAvailable = false; - private volatile boolean depthAvailable = false; - - boolean firstDepth = true; - boolean firstVideo = true; - boolean firstImage = true; - - String baseDir = Service.getResourceDir(BoofCv.class); - String nameCalib = "intrinsic.yaml"; - - Planar rgb = new Planar<>(GrayU8.class, 1, 1, 3); - GrayU16 depth = new GrayU16(1, 1); - - List points = new ArrayList(); - int colors[] = new int[1]; - - public void process() { - - Context kinect = Freenect.createContext(); - - if (kinect.numDevices() < 0) - throw new RuntimeException("No kinect found!"); - - Device device = kinect.openDevice(0); - - device.setDepthFormat(DepthFormat.REGISTERED); - device.setVideoFormat(VideoFormat.RGB); - - CameraPinholeRadial param = CalibrationIO.load(new File(baseDir, nameCalib)); - - FastQueue cloud = new FastQueue<>(Point3D_F64.class, true); - FastQueueArray_I32 cloudColor = new FastQueueArray_I32(3); - - viewer = VisualizeData.createPointCloudViewer(); - viewer.setCameraHFov(PerspectiveOps.computeHFov(param)); - viewer.setTranslationStep(15); - - device.startDepth(new DepthHandler() { - @Override - public void onFrameReceived(FrameMode mode, ByteBuffer frame, int timestamp) { - processDepth(mode, frame, timestamp); - } - }); - device.startVideo(new VideoHandler() { - @Override - public void onFrameReceived(FrameMode mode, ByteBuffer frame, int timestamp) { - processRgb(mode, frame, timestamp); - } - }); - - long starTime = System.currentTimeMillis(); - while (starTime + 100000 > System.currentTimeMillis()) { - - if (videoAvailable && depthAvailable) { - if (firstImage) { - viewer.getComponent().setPreferredSize(new Dimension(rgb.width, rgb.height)); - } - - VisualDepthOps.depthTo3D(param, rgb, depth, cloud, cloudColor); - - if (colors.length != cloud.size()) { - colors = new int[cloud.size()]; - points = new ArrayList(cloud.size()); - } - - points.clear(); - for (int i = 0; i < cloud.size; i++) { - Point3D_F64 p = cloud.get(i); - int[] color = cloudColor.get(i); - int c = (color[0] << 16) | (color[1] << 8) | color[2]; - points.add(p); - colors[i] = c; - } - - viewer.clearPoints(); - if (colors.length != points.size()) { - log.info("WTF", colors.length, points.size(), cloud.size); - } - viewer.addCloud(points, colors); - - if (firstImage) { - ShowImages.showWindow(viewer.getComponent(), "Point Cloud", true); - firstImage = false; - } else { - viewer.getComponent().repaint(); - } - videoAvailable = false; - depthAvailable = false; - } - sleep(10); - } - System.out.println("100 Seconds elapsed"); - - device.stopDepth(); - device.stopVideo(); - device.close(); - } - - private void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - } - - } - - protected void processDepth(FrameMode mode, ByteBuffer frame, int timestamp) { - - // System.out.println("Got depth! "+timestamp); - if (firstDepth) { - depth.reshape(mode.getWidth(), mode.getHeight()); - firstDepth = false; - } - - // Skip frames until the previous depth map has been shown - if (!depthAvailable) { - // Convert the frame to a depth map)) - UtilOpenKinect.bufferDepthToU16(frame, depth); - // Log.info("frame.capacity", frame.capacity()); - // Log.info("depthAvailable", depth.width, depth.height); - depthAvailable = true; - } - } - - protected void processRgb(FrameMode mode, ByteBuffer frame, int timestamp) { - - if (mode.getVideoFormat() != VideoFormat.RGB) { - System.out.println("Bad rgb format!"); - } - - // System.out.println("Got rgb! "+timestamp); - if (firstVideo) { - rgb.reshape(mode.getWidth(), mode.getHeight()); - viewer.getComponent().setPreferredSize(new Dimension(rgb.width, rgb.height)); - firstVideo = false; - } - - // Skip frames until the previous video has been shown - if (!videoAvailable) { - UtilOpenKinect.bufferRgbToMsU8(frame, rgb); - // Log.info("videoAvailable"); - videoAvailable = true; - } - - } - - public static void main(String args[]) { - OpenKinectPointCloud app = new OpenKinectPointCloud(); - - app.process(); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/OpenKinectStreamingTest.java b/src/main/java/org/myrobotlab/boofcv/OpenKinectStreamingTest.java deleted file mode 100644 index 4cb9e28641..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/OpenKinectStreamingTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.image.BufferedImage; -import java.nio.ByteBuffer; - -import org.openkinect.freenect.Context; -import org.openkinect.freenect.DepthFormat; -import org.openkinect.freenect.DepthHandler; -import org.openkinect.freenect.Device; -import org.openkinect.freenect.FrameMode; -import org.openkinect.freenect.Freenect; -import org.openkinect.freenect.VideoFormat; -import org.openkinect.freenect.VideoHandler; - -import com.sun.jna.NativeLibrary; - -import boofcv.gui.image.ImagePanel; -import boofcv.gui.image.ShowImages; -import boofcv.gui.image.VisualizeImageData; -import boofcv.io.image.ConvertBufferedImage; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; - -/** - * Example demonstrating how to process and display data from the Kinect. - * - * @author Peter Abeles - */ -public class OpenKinectStreamingTest { - - { - // be sure to set OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY to the - // location of your shared library! - NativeLibrary.addSearchPath("freenect", OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY); - } - - Planar rgb = new Planar<>(GrayU8.class, 1, 1, 3); - GrayU16 depth = new GrayU16(1, 1); - - BufferedImage outRgb; - ImagePanel guiRgb; - - BufferedImage outDepth; - ImagePanel guiDepth; - - public void process() { - Context kinect = Freenect.createContext(); - - if (kinect.numDevices() < 0) - throw new RuntimeException("No kinect found!"); - - Device device = kinect.openDevice(0); - - device.setDepthFormat(DepthFormat.REGISTERED); - device.setVideoFormat(VideoFormat.RGB); - - device.startDepth(new DepthHandler() { - @Override - public void onFrameReceived(FrameMode mode, ByteBuffer frame, int timestamp) { - processDepth(mode, frame, timestamp); - } - }); - device.startVideo(new VideoHandler() { - @Override - public void onFrameReceived(FrameMode mode, ByteBuffer frame, int timestamp) { - processRgb(mode, frame, timestamp); - } - }); - - long starTime = System.currentTimeMillis(); - while (starTime + 100000 > System.currentTimeMillis()) { - } - System.out.println("100 Seconds elapsed"); - - device.stopDepth(); - device.stopVideo(); - device.close(); - - } - - protected void processDepth(FrameMode mode, ByteBuffer frame, int timestamp) { - System.out.println("Got depth! " + timestamp); - - if (outDepth == null) { - depth.reshape(mode.getWidth(), mode.getHeight()); - outDepth = new BufferedImage(depth.width, depth.height, BufferedImage.TYPE_INT_RGB); - guiDepth = ShowImages.showWindow(outDepth, "Depth Image"); - } - - UtilOpenKinect.bufferDepthToU16(frame, depth); - - // VisualizeImageData.grayUnsigned(depth,outDepth,UtilOpenKinect.FREENECT_DEPTH_MM_MAX_VALUE); - VisualizeImageData.disparity(depth, outDepth, 0, UtilOpenKinect.FREENECT_DEPTH_MM_MAX_VALUE, 0); - guiDepth.repaint(); - } - - protected void processRgb(FrameMode mode, ByteBuffer frame, int timestamp) { - if (mode.getVideoFormat() != VideoFormat.RGB) { - System.out.println("Bad rgb format!"); - } - - System.out.println("Got rgb! " + timestamp); - - if (outRgb == null) { - rgb.reshape(mode.getWidth(), mode.getHeight()); - outRgb = new BufferedImage(rgb.width, rgb.height, BufferedImage.TYPE_INT_RGB); - guiRgb = ShowImages.showWindow(outRgb, "RGB Image"); - } - - UtilOpenKinect.bufferRgbToMsU8(frame, rgb); - ConvertBufferedImage.convertTo_U8(rgb, outRgb, true); - - guiRgb.repaint(); - } - - public static void main(String args[]) { - OpenKinectStreamingTest app = new OpenKinectStreamingTest(); - - app.process(); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/OverlayRgbDepthStreamsApp.java b/src/main/java/org/myrobotlab/boofcv/OverlayRgbDepthStreamsApp.java deleted file mode 100644 index b35c089f78..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/OverlayRgbDepthStreamsApp.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.AlphaComposite; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; - -import org.openkinect.freenect.Context; -import org.openkinect.freenect.Device; -import org.openkinect.freenect.Freenect; -import org.openkinect.freenect.Resolution; - -import com.sun.jna.NativeLibrary; - -import boofcv.gui.image.ImagePanel; -import boofcv.gui.image.ShowImages; -import boofcv.gui.image.VisualizeImageData; -import boofcv.io.image.ConvertBufferedImage; -import boofcv.openkinect.StreamOpenKinectRgbDepth; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; - -/** - * @author Peter Abeles - */ -public class OverlayRgbDepthStreamsApp implements StreamOpenKinectRgbDepth.Listener { - { - // be sure to set OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY to the - // location of your shared library! - NativeLibrary.addSearchPath("freenect", OpenKinectExampleParam.PATH_TO_SHARED_LIBRARY); - } - - Resolution resolution = Resolution.MEDIUM; - - BufferedImage buffRgb; - BufferedImage buffDepth; - - ImagePanel gui; - - public void process() { - - int w = UtilOpenKinect.getWidth(resolution); - int h = UtilOpenKinect.getHeight(resolution); - - buffRgb = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); - buffDepth = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); - - gui = ShowImages.showWindow(buffRgb, "Kinect Overlay"); - - StreamOpenKinectRgbDepth stream = new StreamOpenKinectRgbDepth(); - Context kinect = Freenect.createContext(); - - if (kinect.numDevices() < 0) - throw new RuntimeException("No kinect found!"); - - Device device = kinect.openDevice(0); - stream.start(device, resolution, this); - } - - @Override - public void processKinect(Planar rgb, GrayU16 depth, long timeRgb, long timeDepth) { - VisualizeImageData.disparity(depth, buffDepth, 0, UtilOpenKinect.FREENECT_DEPTH_MM_MAX_VALUE, 0); - ConvertBufferedImage.convertTo_U8(rgb, buffRgb, true); - - Graphics2D g2 = buffRgb.createGraphics(); - float alpha = 0.5f; - int type = AlphaComposite.SRC_OVER; - AlphaComposite composite = AlphaComposite.getInstance(type, alpha); - g2.setComposite(composite); - g2.drawImage(buffDepth, 0, 0, null); - - gui.repaint(); - } - - public static void main(String args[]) { - OverlayRgbDepthStreamsApp app = new OverlayRgbDepthStreamsApp(); - app.process(); - } -} diff --git a/src/main/java/org/myrobotlab/boofcv/PlaybackKinectLogApp.java b/src/main/java/org/myrobotlab/boofcv/PlaybackKinectLogApp.java deleted file mode 100644 index c4a3603a21..0000000000 --- a/src/main/java/org/myrobotlab/boofcv/PlaybackKinectLogApp.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2011-2014, Peter Abeles. All Rights Reserved. - * - * This file is part of BoofCV (http://boofcv.org). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.myrobotlab.boofcv; - -import java.awt.image.BufferedImage; -import java.io.IOException; - -import org.ddogleg.struct.GrowQueue_I8; - -import boofcv.gui.image.ImageGridPanel; -import boofcv.gui.image.ShowImages; -import boofcv.gui.image.VisualizeImageData; -import boofcv.io.image.ConvertBufferedImage; -import boofcv.io.image.UtilImageIO; -import boofcv.misc.BoofMiscOps; -import boofcv.openkinect.UtilOpenKinect; -import boofcv.struct.image.GrayU16; -import boofcv.struct.image.GrayU8; -import boofcv.struct.image.Planar; - -/** - * @author Peter Abeles - */ -public class PlaybackKinectLogApp { - ImageGridPanel gui; - - String directory; - - GrowQueue_I8 data = new GrowQueue_I8(); - boolean depthIsPng = false; - - // image with depth information - private GrayU16 depth = new GrayU16(1, 1); - // image with color information - private Planar rgb = new Planar<>(GrayU8.class, 1, 1, 3); - - BufferedImage outRgb; - BufferedImage outDepth; - - public PlaybackKinectLogApp(String directory) { - this.directory = directory; - } - - public void process() throws IOException { - parseFrame(0); - - outRgb = new BufferedImage(rgb.getWidth(), rgb.getHeight(), BufferedImage.TYPE_INT_RGB); - outDepth = new BufferedImage(depth.getWidth(), depth.getHeight(), BufferedImage.TYPE_INT_RGB); - - gui = new ImageGridPanel(1, 2, outRgb, outDepth); - ShowImages.showWindow(gui, "Kinect Data"); - - int frame = 1; - while (true) { - parseFrame(frame++); - ConvertBufferedImage.convertTo_U8(rgb, outRgb, true); - VisualizeImageData.disparity(depth, outDepth, 0, UtilOpenKinect.FREENECT_DEPTH_MM_MAX_VALUE, 0); - gui.repaint(); - BoofMiscOps.pause(30); - } - } - - private void parseFrame(int frameNumber) throws IOException { - UtilImageIO.loadPPM_U8(String.format("%s/rgb%07d.ppm", directory, frameNumber), rgb, data); - if (depthIsPng) { - BufferedImage image = UtilImageIO.loadImage(String.format("%s/depth%07d.png", directory, frameNumber)); - ConvertBufferedImage.convertFrom(image, depth, true); - } else { - UtilOpenKinect.parseDepth(String.format("%s/depth%07d.depth", directory, frameNumber), depth, data); - } - - } - - public static void main(String args[]) throws IOException { - PlaybackKinectLogApp app = new PlaybackKinectLogApp("log"); - - app.process(); - } -} diff --git a/src/main/java/org/myrobotlab/codec/ApiSwagger.java b/src/main/java/org/myrobotlab/codec/ApiSwagger.java index 1dbf0d4f24..628a0a483e 100644 --- a/src/main/java/org/myrobotlab/codec/ApiSwagger.java +++ b/src/main/java/org/myrobotlab/codec/ApiSwagger.java @@ -1,219 +1,138 @@ package org.myrobotlab.codec; -import java.io.IOException; -import java.io.OutputStream; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Stream; +import java.util.TreeMap; -import org.myrobotlab.framework.Message; -import org.myrobotlab.framework.interfaces.MessageSender; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.Servo; import org.slf4j.Logger; - -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; public class ApiSwagger { public final static Logger log = LoggerFactory.getLogger(Runtime.class); - - public Object process(MessageSender sender, OutputStream out, Message msgFromUri, String data) { - - return null; + + static final Map, String> primitiveMap = new HashMap<>(); + static { + primitiveMap.put(int.class, "integer"); + primitiveMap.put(long.class, "integer"); + primitiveMap.put(float.class, "number"); + primitiveMap.put(double.class, "number"); + primitiveMap.put(short.class, "integer"); + primitiveMap.put(byte.class, "integer"); + primitiveMap.put(boolean.class, "boolean"); + primitiveMap.put(char.class, "string"); + primitiveMap.put(Integer.class, "integer"); + primitiveMap.put(Long.class, "integer"); + primitiveMap.put(Float.class, "number"); + primitiveMap.put(Double.class, "number"); + primitiveMap.put(Short.class, "integer"); + primitiveMap.put(Byte.class, "integer"); + primitiveMap.put(Boolean.class, "boolean"); + primitiveMap.put(Character.class, "string"); } - // edit these to adjust environments - private String[][] serverEndpoints = new String[][] { { "http://170.2.107.118:3333/api", "dev" }, - { "http://qdtmeutelcebl03.azure-qa-eastus.us164.corpintra.net:3333/api", "qaEast" }, - { "http://stnascvdl009.us164.corpintra.net:2500/proxy?target=http://pdtmeutelcebl02.azure-prd-eastus.us164.corpintra.net:3333/api", "prodEast" } }; - - public JsonObject createHeaders() { - JsonObject obj = new JsonObject(); - - // swagger version - obj.addProperty("openapi", "3.0.0"); - - // info - obj.add("info", - new JsonParser().parse("{" + "\"description\": \"Entity Broker Auto-generated documentation\"," + "\"title\": \"Entity Broker API\"," + "\"version\": \"1.0.0\"" + "}")); - - // host - JsonArray servers = new JsonArray(); - for (String[] s : serverEndpoints) { - JsonObject temp = new JsonObject(); - temp.addProperty("url", s[0]); - temp.addProperty("description", s[1]); - servers.add(temp); + + public static String toPrimitive(Class clazz) { + if (primitiveMap.containsKey(clazz)) { + return primitiveMap.get(clazz); + } else { + return "string"; } - obj.add("servers", servers); - - // schemes - // obj.add("schemes", new JsonParser().parse("[\"https\"]")); - - return obj; } - public Map getSwagger() throws IOException { - JsonObject data = createHeaders(); - - /* - * ClassPath cp = - * ClassPath.from(Thread.currentThread().getContextClassLoader()); - * Class[] classes = - * cp.getTopLevelClasses("com.daimler.eb.endpoint").asList().stream().map(( - * c) -> c.load()).toArray(Class[]::new); - */ - Class[] classes = new Class[] { Runtime.class, Servo.class }; - data.add("paths", getPaths(classes)); - - // convert to a Map - return new Gson().fromJson(data.toString(), new HashMap().getClass()); - } - - public JsonObject getPaths(Class[] classes) { - // get all the methods - Method[] methods = Arrays.stream(classes).parallel().map(c -> c.getDeclaredMethods()).reduce(new Method[] {}, - (x, y) -> Stream.concat(Arrays.stream(x), Arrays.stream(y)).toArray(Method[]::new)); - - // filter out all non-inherited and non-public methods and map each - // method to a JSON Object describing its GET / POST - /* - * { "get":{ "path":..., ... }, "post":{ "path":..., ... }, } - */ - JsonObject[] methodDetails = Arrays.stream(methods).parallel() - .filter(m -> Modifier.isPublic(m.getModifiers()) && !m.getName().equals("main") && !Modifier.isStatic(m.getModifiers())).map((m) -> { - JsonObject get = new JsonObject(); - JsonObject post = new JsonObject(); - - // construct the api path - Parameter[] params = m.getParameters(); - String paramNames = ""; - for (Parameter p : params) - paramNames += "/{" + p.getName() + "}"; - - String apiPathGet = "/" + m.getDeclaringClass().getSimpleName() + "/" + m.getName() + paramNames; - String apiPathPost = "/" + m.getDeclaringClass().getSimpleName() + "/" + m.getName(); - - // get properties & map each parameter t a JsonObject - get.addProperty("apiPath", apiPathGet); - get.add("tags", new JsonParser().parse("[" + m.getDeclaringClass().getSimpleName() + "]")); - get.add("responses", new JsonParser().parse("{" + "\"200\": {" + "\"description\": \"successful response\"" + "}" + "}")); - JsonObject[] paramDetails = Arrays.stream(params).parallel().map((p) -> { - JsonObject obj = new JsonObject(); - obj.addProperty("name", p.getName()); - obj.addProperty("in", "path"); - obj.addProperty("required", true); - obj.add("schema", getSchemaFromType(p.getType().getSimpleName())); - return obj; - }).toArray(JsonObject[]::new); - JsonArray parameters = new JsonArray(); - for (JsonObject pd : paramDetails) - parameters.add(pd); - get.add("parameters", parameters); - - // post properties - post.addProperty("apiPath", apiPathPost); - post.add("tags", new JsonParser().parse("[" + m.getDeclaringClass().getSimpleName() + "]")); - post.add("responses", new JsonParser().parse("{" + "\"200\": {" + "\"description\": \"successful response\"" + "}" + "}")); - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("required", true); - JsonObject requestBodyContent = new JsonObject(); - requestBody.add("content", requestBodyContent); - JsonObject rbcAppJson = new JsonObject(); - requestBodyContent.add("application/json", rbcAppJson); - JsonObject postBodySchema = new JsonObject(); - postBodySchema.addProperty("type", "array"); - postBodySchema.addProperty("minItems", params.length); - postBodySchema.addProperty("maxItems", params.length); - JsonObject arrayItems = new JsonObject(); - JsonArray types = new JsonArray(); - for (Parameter p : params) { - types.add(getSchemaFromType(p.getType().getSimpleName())); - } - arrayItems.add("oneOf", types); - postBodySchema.add("items", arrayItems); - rbcAppJson.add("schema", postBodySchema); - post.add("requestBody", requestBody); - - JsonObject res = new JsonObject(); - res.add("get", get); - res.add("post", post); - return res; - }).toArray(JsonObject[]::new); - - // convert each method get/post info to distinct get/post infos - JsonObject res = new JsonObject(); - for (JsonObject m : methodDetails) { - // if both GET/POST have same path, then combine into one object - if (m.get("get").getAsJsonObject().get("apiPath").equals(m.get("post").getAsJsonObject().get("apiPath"))) { - res.add(m.get("get").getAsJsonObject().get("apiPath").getAsString(), m); - } else { // otherwise, separate them into different objects - JsonObject g = new JsonObject(); - g.add("get", m.get("get")); - res.add(m.get("get").getAsJsonObject().get("apiPath").getAsString(), g); - - JsonObject p = new JsonObject(); - p.add("post", m.get("post")); - res.add(m.get("post").getAsJsonObject().get("apiPath").getAsString(), p); + public static String generateSwaggerYaml(Class clazz) { + Map swaggerData = new TreeMap<>(); + Map paths = new TreeMap<>(); + + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (Modifier.isPublic(method.getModifiers())) { + String prefix = "/" + method.getName(); // Method name is used as the path + String path = prefix; + + Class[] parameterTypes = method.getParameterTypes(); + // Parameter names are only available if compiled with -parameters + Parameter[] params = method.getParameters(); + + List> paramArray = new ArrayList<>(); + + + for (int i = 0; i < parameterTypes.length; i++) { + Parameter param = params[i]; + String paramName = "param" + i; // Using param0, param1, etc. as path + // variable names + // only available with compiler -parameters option + paramName = param.getName(); + + Map paramDetail = new TreeMap<>(); + + paramDetail.put("in", "path"); + paramDetail.put("name", paramName); + paramDetail.put("required", true); + MapparamSchema = new TreeMap<>(); + paramDetail.put("schema", paramSchema); + paramSchema.put("type", toPrimitive(param.getType())); + + paramArray.add(paramDetail); + path = path + "/{"+paramName+"}"; + } + + // Create the path entry for the method + Map data = new TreeMap<>(); + + Map get = new TreeMap<>(); + data.put("get", get); + List tags = new ArrayList<>(); + get.put("tags", tags); + tags.add(clazz.getSimpleName()); + get.put("parameters", paramArray); + Map responses = new TreeMap<>(); + get.put("responses", responses); + Map http_200 = new TreeMap<>(); + responses.put("200", http_200); + http_200.put("description", "OK"); + Map content = new TreeMap<>(); + Map json = new TreeMap<>(); + http_200.put("content", content); + content.put("application/json", json); + Map schema = new TreeMap<>(); + json.put("schema", schema); + + + paths.put(path, data); } - - m.get("get").getAsJsonObject().remove("apiPath"); - m.get("post").getAsJsonObject().remove("apiPath"); } - return res; + swaggerData.put("paths", paths); + + // Convert to YAML + Yaml yaml = new Yaml(getDumperOptions()); + return yaml.dump(swaggerData); } - private JsonObject getSchemaFromType(String s) { - JsonObject res = new JsonObject(); - s = s.toLowerCase(); - if (s.equals("String")) - res.addProperty("type", "string"); - else if (s.equals("int")) - res.addProperty("type", "integer"); - else if (s.equals("boolean")) - res.addProperty("type", "boolean"); - else if (s.equals("double") || s.equals("float") || s.equals("long") || s.equals("byte")) - res.addProperty("type", "number"); - else if (s.contains("[]")) { - res.addProperty("type", "array"); - res.add("items", getSchemaFromType(s.substring(0, s.length() - 2))); - } else - res.addProperty("type", "object"); - - return res; + private static DumperOptions getDumperOptions() { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return options; } public static void main(String[] args) { try { - List myList = Arrays.asList("a1", "a2", "b1", "c2", "c1"); - - myList.stream() - // .filter(s -> s.startsWith("c")) - .map(String::toUpperCase).map(s -> s + "blah").map(s -> s + "oink").sorted().forEach(System.out::println); - - ApiSwagger swagger = new ApiSwagger(); - - log.info("{}", CodecUtils.toJson(swagger.getSwagger())); + + // log.info("{}", CodecUtils.toJson(ApiSwagger.generateSwaggerYaml(Servo.class))); + log.info("{}", ApiSwagger.generateSwaggerYaml(Servo.class)); } catch (Exception e) { log.error("main threw", e); } } - public Object process(MessageSender webgui, String apiKey, String uri, String uuid, OutputStream out, String json) throws Exception { - // TODO Auto-generated method stub - return null; - } - } diff --git a/src/main/java/org/myrobotlab/codec/CodecUtils.java b/src/main/java/org/myrobotlab/codec/CodecUtils.java index 6d03fc4985..457f3a8fe5 100644 --- a/src/main/java/org/myrobotlab/codec/CodecUtils.java +++ b/src/main/java/org/myrobotlab/codec/CodecUtils.java @@ -11,11 +11,13 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -26,14 +28,15 @@ import java.util.Set; import java.util.stream.Collectors; -import org.myrobotlab.codec.json.GsonPolymorphicTypeAdapterFactory; import org.myrobotlab.codec.json.JacksonPolymorphicModule; import org.myrobotlab.codec.json.JacksonPrettyPrinter; import org.myrobotlab.codec.json.JsonDeserializationException; import org.myrobotlab.codec.json.JsonSerializationException; +import org.myrobotlab.codec.json.ProxySerializer; import org.myrobotlab.framework.MRLListener; import org.myrobotlab.framework.Message; import org.myrobotlab.framework.MethodCache; +import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.StaticType; import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.logging.Level; @@ -55,1451 +58,1496 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.module.noctordeser.NoCtorDeserModule; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.internal.LinkedTreeMap; /** * handles all encoding and decoding of MRL messages or api(s) assumed context - * services can add an assumed context as a prefix * /api/returnEncoding/inputEncoding/service/method/param1/param2/ ... *

- * xmpp for example assumes (/api/string/gson)/service/method/param1/param2/ ... + * xmpp for example assumes (/api/string/json)/service/method/param1/param2/ ... *

* scheme = alpha *( alpha | digit | "+" | "-" | "." ) Components of all URIs: [ * <scheme>:]<scheme-specific-part>[#<fragment>] *

* branch API test 5 * - * @see Valid characters for URI schemes + * @see Valid + * characters for URI schemes */ public class CodecUtils { - public final static Logger log = LoggerFactory.getLogger(CodecUtils.class); - /** - * The string to be used to specify the API in URIs, - * with leading and trailing slash. - */ - public final static String PARAMETER_API = "/api/"; - /** - * The string to be used in URIs to specify the API - */ - public final static String PREFIX_API = "api"; - /** - * The MIME type used to specify JSON data. - * - * @see MIME types - */ - public final static String MIME_TYPE_JSON = "application/json"; + public final static Logger log = LoggerFactory.getLogger(CodecUtils.class); + /** + * The string to be used to specify the API in URIs, with leading and trailing + * slash. + */ + public final static String PARAMETER_API = "/api/"; + /** + * The string to be used in URIs to specify the API + */ + public final static String PREFIX_API = "api"; + /** + * The MIME type used to specify JSON data. + * + * @see MIME + * types + */ + public final static String MIME_TYPE_JSON = "application/json"; - // mime-types - /** - * Whether we are using the GSON JSON backend or not. If false, - * use Jackson. - * TODO Replace with enum to allow extension for multiple backends - */ - public static final boolean USING_GSON = false; - /** - * The key used to locate type information - * in a JSON dictionary. This is used to serialize - * type information into the JSON and to deserialize - * JSON into the correct type. - */ - public static final String CLASS_META_KEY = "class"; - /** - * Set of all known wrapper types, which are classes that correspond to - * Java primitives (plus {@link Void}). - * - * @see Java Wrapper Classes - */ - public static final Set> WRAPPER_TYPES = new HashSet<>( - Arrays.asList(Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, - Float.class, Double.class, Void.class)); - /** - * Set of the fully-qualified (AKA canonical) names of {@link #WRAPPER_TYPES}. - * - * @see Java Wrapper Classes - */ - public static final Set WRAPPER_TYPES_CANONICAL = - WRAPPER_TYPES.stream().map(Object::getClass).map(Class::getCanonicalName).collect(Collectors.toSet()); - public static final String API_MESSAGES = "messages"; - public static final String API_SERVICE = "service"; + /** + * The key used to locate type information in a JSON dictionary. This is used + * to serialize type information into the JSON and to deserialize JSON into + * the correct type. + */ + public static final String CLASS_META_KEY = "class"; + /** + * Set of all known wrapper types, which are classes that correspond to Java + * primitives (plus {@link Void}). + * + * @see Java + * Wrapper Classes + */ + public static final Set> WRAPPER_TYPES = new HashSet<>( + Arrays.asList(Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Void.class)); + /** + * Set of the fully-qualified (AKA canonical) names of {@link #WRAPPER_TYPES}. + * + * @see Java + * Wrapper Classes + */ + public static final Set WRAPPER_TYPES_CANONICAL = WRAPPER_TYPES.stream().map(Object::getClass).map(Class::getCanonicalName).collect(Collectors.toSet()); + public static final String API_MESSAGES = "messages"; + public static final String API_SERVICE = "service"; - /** - * The path from a top-level URL to the messages API - * endpoint. - *

- * FIXME This should be moved to WebGui, - * CodecUtils should have no knowledge of URLs - */ - public static final String API_MESSAGES_PATH = PARAMETER_API + API_MESSAGES; + /** + * The path from a top-level URL to the messages API endpoint. + *

+ *

+ * FIXME This should be moved to WebGui, CodecUtils should have no knowledge + * of URLs + */ + public static final String API_MESSAGES_PATH = PARAMETER_API + API_MESSAGES; + /** + * The path from a top-level URL to the service API endpoint. + *

+ *

+ * FIXME This should be moved to WebGui, CodecUtils should have no knowledge + * of URLs + */ + public static final String API_SERVICE_PATH = PARAMETER_API + API_SERVICE; + /** + * use {@link MethodCache} + */ + @Deprecated + final static HashMap methodCache = new HashMap(); + /** + * a method signature map based on name and number of methods - the String[] + * will be the keys into the methodCache A method key is generated by input + * from some encoded protocol - the method key is object name + method name + + * parameter number - this returns a full method signature key which is used + * to look up the method in the methodCache + */ + final static HashMap> methodOrdinal = new HashMap>(); + final static HashSet objectsCached = new HashSet(); + /** + * Equivalent to {@link #MIME_TYPE_JSON} + */ + @Deprecated + static final String JSON = "application/javascript"; - /** - * The path from a top-level URL to the service API - * endpoint. - *

- * FIXME This should be moved to WebGui, - * CodecUtils should have no knowledge of URLs - */ - public static final String API_SERVICE_PATH = PARAMETER_API + API_SERVICE; - /** - * use {@link MethodCache} - */ - @Deprecated - final static HashMap methodCache = new HashMap(); - /** - * a method signature map based on name and number of methods - the String[] - * will be the keys into the methodCache A method key is generated by input - * from some encoded protocol - the method key is object name + method name + - * parameter number - this returns a full method signature key which is used - * to look up the method in the methodCache - */ - final static HashMap> methodOrdinal = new HashMap>(); - final static HashSet objectsCached = new HashSet(); - /** - * Equivalent to {@link #MIME_TYPE_JSON} - */ - @Deprecated - static final String JSON = "application/javascript"; - /** - * The type that GSON uses when it attempts to deserialize - * without knowing the target type, e.g. if the target - * is {@link Object}. - */ - private static final Class GSON_DEFAULT_OBJECT_TYPE = LinkedTreeMap.class; - /** - * The type that Jackson uses when it attempts to deserialize - * without knowing the target type, e.g. if the target - * is {@link Object} and no field matching {@link #CLASS_META_KEY} - * is found. - */ - private static final Class JACKSON_DEFAULT_OBJECT_TYPE = LinkedHashMap.class; - /** - * The type that the chosen JSON backend uses when it attempts to deserialize - * without knowing the target type, e.g. if the target - * is {@link Object} and no field matching {@link #CLASS_META_KEY} - * is found. - */ - public static final Class JSON_DEFAULT_OBJECT_TYPE = (USING_GSON) ? GSON_DEFAULT_OBJECT_TYPE : JACKSON_DEFAULT_OBJECT_TYPE; - - /** - * Default type for single parameter fromJson(String json), we initially assume this type - */ - public static final Class DEFAULT_OBJECT_TYPE = LinkedHashMap.class; - - - /** - * The {@link Gson} object used for JSON operations when the selected backend is - * Gson. - * - * @see #USING_GSON - */ - private static final Gson gson = new GsonBuilder().registerTypeAdapterFactory(new GsonPolymorphicTypeAdapterFactory()) - .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").serializeNulls().disableHtmlEscaping().create(); - /** - * The {@link Gson} object used to pretty-print JSON. - */ - private static final Gson prettyGson = new GsonBuilder().registerTypeAdapterFactory(new GsonPolymorphicTypeAdapterFactory()) - .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").setPrettyPrinting().disableHtmlEscaping().create(); - /** - * The Jackson {@link ObjectMapper} used for JSON operations when - * the selected backend is Jackson. - * - * @see #USING_GSON - */ - private static final ObjectMapper mapper = new ObjectMapper(); + /** + * The type that Jackson uses when it attempts to deserialize without knowing + * the target type, e.g. if the target is {@link Object} and no field matching + * {@link #CLASS_META_KEY} is found. + */ + private static final Class JACKSON_DEFAULT_OBJECT_TYPE = LinkedHashMap.class; + /** + * The type that the chosen JSON backend uses when it attempts to deserialize + * without knowing the target type, e.g. if the target is {@link Object} and + * no field matching {@link #CLASS_META_KEY} is found. + */ + public static final Class JSON_DEFAULT_OBJECT_TYPE = JACKSON_DEFAULT_OBJECT_TYPE; + /** + * Default type for single parameter fromJson(String json), we initially + * assume this type + */ + public static final Class DEFAULT_OBJECT_TYPE = LinkedHashMap.class; - /** - * The pretty printer to be used with {@link #mapper} - * when {@link #USING_GSON} equals false, i.e. we're using Jackson. - */ - private static final PrettyPrinter jacksonPrettyPrinter = new JacksonPrettyPrinter(); + /** + * The Jackson {@link ObjectMapper} used for JSON operations when the selected + * backend is Jackson. + * + */ + private static final ObjectMapper mapper = new ObjectMapper(); - /** - * The {@link TypeFactory} used to generate type information for - * {@link #mapper} when the selected backend is Jackson. - *

- * No analogue exists for Gson, as it uses a different mechanism - * to represent types. - * - * @see #USING_GSON - */ - private static final TypeFactory typeFactory = TypeFactory.defaultInstance(); + /** + * The pretty printer to be used with {@link #mapper} + */ + private static final PrettyPrinter jacksonPrettyPrinter = new JacksonPrettyPrinter(); - //Class initializer to setup mapper when the class is loaded - static { - //This allows Jackson to work just like GSON when no default constructor is available - mapper.registerModule(new NoCtorDeserModule()); + /** + * The {@link TypeFactory} used to generate type information for + * {@link #mapper} when the selected backend is Jackson. + *

+ */ + private static final TypeFactory typeFactory = TypeFactory.defaultInstance(); - //Actually add our polymorphic support - mapper.registerModule(JacksonPolymorphicModule.getPolymorphicModule()); + // Class initializer to setup mapper when the class is loaded + static { + // This allows Jackson to when no default constructor is + // available + mapper.registerModule(new NoCtorDeserModule()); - //Disables Jackson's automatic property detection - mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); - mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); -// mapper.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY); - mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + SimpleModule proxySerializerModule = new SimpleModule(); + proxySerializerModule.addSerializer(Proxy.class, new ProxySerializer()); + mapper.registerModule(proxySerializerModule); - //Make jackson behave like gson in that unknown properties are ignored - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } + // Actually add our polymorphic support + mapper.registerModule(JacksonPolymorphicModule.getPolymorphicModule()); - /** - * Ensures a service type name is fully qualified. - * If the type name is short, it will assume the type - * exists in the {@code org.myrobotlab.service} package. - * - * @param type The service type name, either shortened or - * fully qualified. - * @return Null if type is null, otherwise fully qualified name. - */ - public static String makeFullTypeName(String type) { - if (type == null) { - return null; - } - if (!type.contains(".")) { - return String.format("org.myrobotlab.service.%s", type); - } - return type; - } + // Disables Jackson's automatic property detection + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + // mapper.setVisibility(PropertyAccessor.SETTER, + // JsonAutoDetect.Visibility.PUBLIC_ONLY); + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - /** - * Capitalize the first character of the given string - * - * @param line The string to be capitalized - * @return The capitalized version of line. - */ - public static String capitalize(final String line) { - return Character.toUpperCase(line.charAt(0)) + line.substring(1); - } + // Make jackson behave such that unknown properties are ignored + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - /** - * Deserializes a JSON string into the target object - * (or subclass of if {@link #CLASS_META_KEY} exists) - * using the selected JSON backend. - * - * @param json The JSON to be deserialized in String form - * @param clazz The target class. If a class is not supplied the default class returned will be an {@link #DEFAULT_OBJECT_TYPE} - * @param The type of the target class. - * @return An object of the specified class (or a subclass of) with the state - * given by the json. Null is an allowed return object. - * @throws JsonDeserializationException if an error during deserialization occurs. - * @see #USING_GSON - */ - @SuppressWarnings("unchecked") - public static /*@Nullable*/ T fromJson(/*@Nonnull*/ String json, /*@Nonnull*/ Class clazz) { - try { - if (USING_GSON) { - if (clazz == null) { - clazz = (Class)DEFAULT_OBJECT_TYPE; - } - return gson.fromJson(json, clazz); - } else { - if (clazz == null) { - - JsonParser parser = mapper.getFactory().createParser(json); - - // "peek" at the next token to determine its type - JsonToken token = parser.nextToken(); - - if (token == JsonToken.START_OBJECT) { - clazz = (Class)Map.class; - } else if (token == JsonToken.START_ARRAY) { - clazz = (Class)ArrayList.class; - } else if (token.isScalarValue()) { - JsonNode node = mapper.readTree(json); - if (node.isInt()) { - return mapper.readValue(json, (Class)Integer.class); - } else if (node.isBoolean()) { - return mapper.readValue(json, (Class)Boolean.class); - } else if (node.isNumber()) { - return mapper.readValue(json, (Class)Double.class); - } else if (node.isTextual()) { - return mapper.readValue(json, (Class)String.class); - } - } else { - log.error("could not derive type from peeking json {}", json); - } - - parser.close(); - - } - return mapper.readValue(json, clazz); - } - } catch (Exception e) { - throw new JsonDeserializationException(e); - } + } + + /** + * Ensures a service type name is fully qualified. If the type name is short, + * it will assume the type exists in the {@code org.myrobotlab.service} + * package. + * + * @param type + * The service type name, either shortened or fully qualified. + * @return Null if type is null, otherwise fully qualified name. + */ + public static String makeFullTypeName(String type) { + if (type == null) { + return null; + } + if (!type.contains(".")) { + return ("Service".equals(type)) ? "org.myrobotlab.framework.Service" : String.format("org.myrobotlab.service.%s", type); } + return type; + } - /** - * Deserializes a JSON string into the target object - * (or subclass of if {@link #CLASS_META_KEY} exists) - * using the selected JSON backend. - * - * @param json The JSON to be deserialized in String form - * @param genericClass The target class. - * @param parameterized The list of types used as the genericClass type parameters - * of genericClass. - * @param The type of the target class. - * @return An object of the specified class (or a subclass of) with the state - * given by the json. Null is an allowed return object. - * @throws JsonDeserializationException if an error during deserialization occurs. - * @see #USING_GSON - */ - public static /*@Nullable*/ T fromJson(/*@Nonnull*/ String json, /*@Nonnull*/ Class genericClass, - /*@Nonnull*/ Class... parameterized) { - try { - if (USING_GSON) { - return gson.fromJson(json, getType(genericClass, parameterized)); - } - - return mapper.readValue(json, typeFactory.constructParametricType(genericClass, parameterized)); - } catch (Exception e) { - throw new JsonDeserializationException(e); + /** + * Capitalize the first character of the given string + * + * @param line + * The string to be capitalized + * @return The capitalized version of line. + */ + public static String capitalize(final String line) { + return Character.toUpperCase(line.charAt(0)) + line.substring(1); + } + + /** + * Deserializes a JSON string into the target object (or subclass of if + * {@link #CLASS_META_KEY} exists) using the selected JSON backend. + * + * @param json + * The JSON to be deserialized in String form + * @param clazz + * The target class. If a class is not supplied the default class + * returned will be an {@link #DEFAULT_OBJECT_TYPE} + * @param + * The type of the target class. + * @return An object of the specified class (or a subclass of) with the state + * given by the json. Null is an allowed return object. + * @throws JsonDeserializationException + * if an error during deserialization occurs. + */ + @SuppressWarnings("unchecked") + public static /* @Nullable */ T fromJson(/* @Nonnull */ String json, + /* @Nonnull */ Class clazz) { + try { + if (clazz == null) { + + JsonParser parser = mapper.getFactory().createParser(json); + + // "peek" at the next token to determine its type + JsonToken token = parser.nextToken(); + + if (token == JsonToken.START_OBJECT) { + clazz = (Class) Map.class; + } else if (token == JsonToken.START_ARRAY) { + clazz = (Class) ArrayList.class; + } else if (token.isScalarValue()) { + JsonNode node = mapper.readTree(json); + if (node.isInt()) { + return mapper.readValue(json, (Class) Integer.class); + } else if (node.isBoolean()) { + return mapper.readValue(json, (Class) Boolean.class); + } else if (node.isNumber()) { + return mapper.readValue(json, (Class) Double.class); + } else if (node.isTextual()) { + return mapper.readValue(json, (Class) String.class); + } + } else { + log.error("could not derive type from peeking json {}", json); } + + parser.close(); + + } + return mapper.readValue(json, clazz); + } catch (Exception e) { + throw new JsonDeserializationException(e); } + } - /** - * Deserializes a json string into the type represented by - * {@link T}. {@code type} must must match {@link T} exactly, - * otherwise the deserializers may not deserialize into T. - * - * @param json A string encoded in JSON - * @param type Reified type information to pass to the deserializers - * @return An instance of T decoded from the json - * @param The type to deserialize into - * @throws JsonDeserializationException if the selected deserializer throws an exception - */ - public static /*@Nullable*/ T fromJson(/*@NonNull*/ String json, /*@NonNull*/ StaticType type) { - return fromJson(json, type.getType()); + /** + * Deserializes a JSON string into the target object (or subclass of if + * {@link #CLASS_META_KEY} exists) using the selected JSON backend. + * + * @param json + * The JSON to be deserialized in String form + * @param genericClass + * The target class. + * @param parameterized + * The list of types used as the genericClass type parameters of + * genericClass. + * @param + * The type of the target class. + * @return An object of the specified class (or a subclass of) with the state + * given by the json. Null is an allowed return object. + * @throws JsonDeserializationException + * if an error during deserialization occurs. + */ + public static /* @Nullable */ T fromJson(/* @Nonnull */ String json, + /* @Nonnull */ Class genericClass, /* @Nonnull */ Class... parameterized) { + try { + return mapper.readValue(json, typeFactory.constructParametricType(genericClass, parameterized)); + } catch (Exception e) { + throw new JsonDeserializationException(e); } + } - /** - * Deserializes a JSON string into the target object - * (or subclass of if {@link #CLASS_META_KEY} exists) - * using the selected JSON backend. - * - * @param json The JSON to be deserialized in String form - * @param type The target type. - * @param The type of the target class. - * @return An object of the specified class (or a subclass of) with the state - * given by the json. Null is an allowed return object. - * @throws JsonDeserializationException if an error during deserialization occurs. - * @see #USING_GSON - */ - public static /*@Nullable*/ T fromJson(/*@Nonnull*/ String json, /*@Nonnull*/ Type type) { - try { - if (USING_GSON) { - return gson.fromJson(json, type); - } - return mapper.readValue(json, typeFactory.constructType(type)); - } catch (Exception e) { - throw new JsonDeserializationException(e); - } + /** + * Deserializes a json string into the type represented by {@link T}. + * {@code type} must must match {@link T} exactly, otherwise the deserializers + * may not deserialize into T. + * + * @param json + * A string encoded in JSON + * @param type + * Reified type information to pass to the deserializers + * @return An instance of T decoded from the json + * @param + * The type to deserialize into + * @throws JsonDeserializationException + * if the selected deserializer throws an exception + */ + public static /* @Nullable */ T fromJson(/* @NonNull */ String json, + /* @NonNull */ StaticType type) { + return fromJson(json, type.getType()); + } + + /** + * Deserializes a JSON string into the target object (or subclass of if + * {@link #CLASS_META_KEY} exists) using the selected JSON backend. + * + * @param json + * The JSON to be deserialized in String form + * @param type + * The target type. + * @param + * The type of the target class. + * @return An object of the specified class (or a subclass of) with the state + * given by the json. Null is an allowed return object. + * @throws JsonDeserializationException + * if an error during deserialization occurs. + */ + public static /* @Nullable */ T fromJson(/* @Nonnull */ String json, + /* @Nonnull */ Type type) { + try { + return mapper.readValue(json, typeFactory.constructType(type)); + } catch (Exception e) { + throw new JsonDeserializationException(e); } + } - /** - * Convert the given JSON string - * into an equivalent tree map. - * - * @param json The json to be converted - * @return The json in a tree map form - * @throws JsonDeserializationException if deserialization fails - */ - @SuppressWarnings("unchecked") - public static LinkedTreeMap toTree(String json) { - try { - if (USING_GSON) { - return gson.fromJson(json, LinkedTreeMap.class); - } - return (LinkedTreeMap) mapper.readValue(json, LinkedTreeMap.class); - } catch (Exception e) { - throw new JsonDeserializationException(e); - } + /** + * Convert the given JSON string into an equivalent tree map. + * + * @param json + * The json to be converted + * @return The json in a tree map form + * @throws JsonDeserializationException + * if deserialization fails + */ + @SuppressWarnings("unchecked") + public static LinkedHashMap toTree(String json) { + try { + return (LinkedHashMap) mapper.readValue(json, LinkedHashMap.class); + } catch (Exception e) { + throw new JsonDeserializationException(e); } + } + + public static Type getType(final Class rawClass, final Class... parameterClasses) { + return new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return parameterClasses; + } + + @Override + public Type getRawType() { + return rawClass; + } - public static Type getType(final Class rawClass, final Class... parameterClasses) { - return new ParameterizedType() { - @Override - public Type[] getActualTypeArguments() { - return parameterClasses; - } + @Override + public Type getOwnerType() { + return null; + } - @Override - public Type getRawType() { - return rawClass; - } + }; + } - @Override - public Type getOwnerType() { - return null; - } + static public byte[] getBytes(Object o) throws IOException { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(5000); + ObjectOutputStream os = new ObjectOutputStream(new BufferedOutputStream(byteStream)); + os.flush(); + os.writeObject(o); + os.flush(); + return byteStream.toByteArray(); + } - }; + /** + * Gets the short name of a service. A short name is the name of the service + * without any runtime IDs, meaning no '@' signs. + * + * @param name + * The service name to be converted + * @return The simple name of the service. If null, will return null, and if + * already a simple name then will return name + */ + static public String getShortName(String name) { + if (name == null) { + return null; } + if (name.contains("@")) { + return name.substring(0, name.indexOf("@")); + } else { + return name; + } + } - static public byte[] getBytes(Object o) throws IOException { - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(5000); - ObjectOutputStream os = new ObjectOutputStream(new BufferedOutputStream(byteStream)); - os.flush(); - os.writeObject(o); - os.flush(); - return byteStream.toByteArray(); + // TODO + // public static Object encode(Object, encoding) - dispatches appropriately + // should be simple using an enum and map to a new Encoder functional + // interface + + /** + * Gets the instance id from a service name + * + * @param name + * the name of the instance + * @return the name of the instance + */ + static public String getId(String name) { + if (name == null) { + return null; + } + if (name.contains("@")) { + return name.substring(name.lastIndexOf("@") + 1); + } else { + return null; } + } - /** - * Gets the short name of a service. A - * short name is the name of the service without - * any runtime IDs, meaning no '@' signs. - * - * @param name The service name to be converted - * @return The simple name of the service. If null, - * will return null, and if already a simple name - * then will return name - */ - static public String shortName(String name) { - if (name == null) { - return null; - } - if (name.contains("@")) { - return name.substring(0, name.indexOf("@")); - } else { - return name; - } + /** + * Normalizes a service name to its full name, if not already. A full name + * consists of two parts: the short name that identifies the service in the + * context of its own runtime, and the Runtime ID, that identifies the + * service's runtime from others in the network. The two parts are separated + * by an {@code @} symbol. + *

+ * If this method is given a short name, it is assumed to be local to this + * runtime, and it is normalized with the ID of this runtime. If the name is + * already a full name, then it is returned unmodified. + * + * @param name + * The service name to normalize + * @return The normalized (full) name, or null if name is null + */ + public static String getFullName(String name) { + if (name == null) { + return null; } + if (getId(name) == null) { + return name + '@' + Platform.getLocalInstance().getId(); + } else { + return name; + } + } - // TODO - // public static Object encode(Object, encoding) - dispatches appropriately - //should be simple using an enum and map to a new Encoder functional interface + /** + * Checks whether two service names are equal by first normalizing each. If a + * name does not have a runtime ID, it is assumed to be a local service. + * + * @param name1 + * The first service name + * @param name2 + * The second service name + * @return Whether the two names are effectively equal + */ + public static boolean checkServiceNameEquality(String name1, String name2) { + return Objects.equals(getFullName(name1), getFullName(name2)); + } - /** - * Gets the instance id from a service name - * - * @param name the name of the instance - * @return the name of the instance - */ - static public String getId(String name) { - if (name == null) { - return null; - } - if (name.contains("@")) { - return name.substring(name.lastIndexOf("@") + 1); - } else { - return null; - } + + /** + * Converts a topic method name to the name of the method that is used for + * callbacks. Usually this involves prepending the string "on", removing any + * "get" or "publish" prefix, and converting it all to proper camelCase. + * + * @param topicMethod + * The topic method name, such as "publishState" + * @return The name for the callback method, such as "onState" + */ + static public String getCallbackTopicName(String topicMethod) { + // replacements + if (topicMethod.startsWith("publish")) { + return String.format("on%s", capitalize(topicMethod.substring("publish".length()))); + } else if (topicMethod.startsWith("get")) { + return String.format("on%s", capitalize(topicMethod.substring("get".length()))); } - /** - * Converts a topic method name to the name of the method that is - * used for callbacks. Usually this involves prepending the string - * "on", removing any "get" or "publish" prefix, and converting - * it all to proper camelCase. - * - * @param topicMethod The topic method name, such as "publishState" - * @return The name for the callback method, such as "onState" - */ - static public String getCallbackTopicName(String topicMethod) { - // replacements - if (topicMethod.startsWith("publish")) { - return String.format("on%s", capitalize(topicMethod.substring("publish".length()))); - } else if (topicMethod.startsWith("get")) { - return String.format("on%s", capitalize(topicMethod.substring("get".length()))); - } + // no replacement - just pefix and capitalize + // FIXME - subscribe to onMethod --- gets ---> onOnMethod :P + return String.format("on%s", capitalize(topicMethod)); + } - // no replacement - just pefix and capitalize - // FIXME - subscribe to onMethod --- gets ---> onOnMethod :P - return String.format("on%s", capitalize(topicMethod)); + /** + * Gets a String representation of a Message + * + * @param msg + * The message + * @return The String representation of the message + */ + static public String getMsgKey(Message msg) { + if (msg.sendingMethod != null) { + return String.format("%s.%s --> %s.%s(%s) - %d", msg.sender, msg.sendingMethod, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId); + } else { + return String.format("%s --> %s.%s(%s) - %d", msg.sender, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId); } + } - /** - * Gets a String representation of a Message - * - * @param msg The message - * @return The String representation of the message - */ - static public String getMsgKey(Message msg) { - if (msg.sendingMethod != null) { - return String.format("%s.%s --> %s.%s(%s) - %d", msg.sender, msg.sendingMethod, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId); + /** + * Get a String representing the data in method parameter form, i.e. each + * element is separated by a comma. Only {@link #WRAPPER_TYPES} and + * {@link MRLListener} will be directly converted to string form using + * {@link Object#toString()}, all other types will be represented as their + * class's simple name. + * + * @param data + * The list of objects to be represented as a parameter list string. + * @return The string representing the data array + */ + static public String getParameterSignature(final Object[] data) { + if (data == null) { + return ""; + } + + StringBuffer ret = new StringBuffer(); + for (int i = 0; i < data.length; ++i) { + if (data[i] != null) { + Class c = data[i].getClass(); // not all data types are safe + // toString() e.g. + // SerializableImage + // if (c == String.class || c == Integer.class || c == Boolean.class || + // c == Float.class || c == MRLListener.class) { + if (WRAPPER_TYPES.stream().anyMatch(n -> n.equals(c)) || MRLListener.class.equals(c)) { + ret.append(data[i].toString()); } else { - return String.format("%s --> %s.%s(%s) - %d", msg.sender, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId); + String type = data[i].getClass().getCanonicalName(); + String shortTypeName = type.substring(type.lastIndexOf(".") + 1); + ret.append(shortTypeName); } - } - /** - * Get a String representing the data in method parameter form, - * i.e. each element is separated by a comma. Only {@link #WRAPPER_TYPES} - * and {@link MRLListener} will be directly converted to string form using - * {@link Object#toString()}, all other types will be represented as their class's - * simple name. - * - * @param data The list of objects to be represented as a parameter list string. - * @return The string representing the data array - */ - static public String getParameterSignature(final Object[] data) { - if (data == null) { - return ""; + if (data.length != i + 1) { + ret.append(","); } + } else { + ret.append("null"); + } - StringBuffer ret = new StringBuffer(); - for (int i = 0; i < data.length; ++i) { - if (data[i] != null) { - Class c = data[i].getClass(); // not all data types are safe - // toString() e.g. - // SerializableImage - //if (c == String.class || c == Integer.class || c == Boolean.class || c == Float.class || c == MRLListener.class) { - if (WRAPPER_TYPES.stream().anyMatch(n -> n.equals(c)) || MRLListener.class.equals(c)) { - ret.append(data[i].toString()); - } else { - String type = data[i].getClass().getCanonicalName(); - String shortTypeName = type.substring(type.lastIndexOf(".") + 1); - ret.append(shortTypeName); - } - - if (data.length != i + 1) { - ret.append(","); - } - } else { - ret.append("null"); - } + } + return ret.toString(); - } - return ret.toString(); + } + static public String getServiceType(String inType) { + if (inType == null) { + return null; + } + if (inType.contains(".")) { + return inType; } + return String.format("org.myrobotlab.service.%s", inType); + } - static public String getServiceType(String inType) { - if (inType == null) { - return null; - } - if (inType.contains(".")) { - return inType; - } - return String.format("org.myrobotlab.service.%s", inType); + /** + * Deserializes a message and its data from a JSON string representation into + * a fully decoded Message object. This method will first attempt to use the + * method cache to determine what types the data elements should be + * deserialized to, and if the method cache lookup fails it relies on the + * virtual "class" field of the JSON to provide the type information. + * + * @param jsonData + * The serialized Message in JSON form + * @return A completely decoded Message object. Null is allowed if the JSON + * represented null. + * @throws JsonDeserializationException + * if jsonData is malformed + */ + public static /* @Nullable */ Message jsonToMessage(/* @Nonnull */ String jsonData) { + if (log.isDebugEnabled()) { + log.debug("Deserializing message: {}", jsonData); } + Message msg = fromJson(jsonData, Message.class); - /** - * Deserializes a message and its data from a JSON - * string representation into a fully decoded Message - * object. This method will first attempt to use the - * method cache to determine what types the data - * elements should be deserialized to, and if the method - * cache lookup fails it relies on the virtual "class" - * field of the JSON to provide the type information. - * - * @param jsonData The serialized Message in JSON form - * @return A completely decoded Message object. Null is allowed if the JSON - * represented null. - * @throws JsonDeserializationException if jsonData is malformed - */ - public static /*@Nullable*/ Message jsonToMessage(/*@Nonnull*/ String jsonData) { - if (log.isDebugEnabled()) { - log.debug("Deserializing message: {}",jsonData); - } - Message msg = fromJson(jsonData, Message.class); + if (msg == null) { + log.warn("Null message within json, probably shouldn't happen"); + return null; + } + return decodeMessageParams(msg); + } - if (msg == null) { - log.warn("Null message within json, probably shouldn't happen"); - return null; + /** + * Performs the second-stage decoding of a Message with JSON-encoded data + * parameters. This method is meant to be a helper for the top-level Message + * decoding methods to go straight from the various codecs to a completely + * decoded Message. + *

+ * Package visibility to allow alternative codecs to use this method. + *

+ *

+ *

+ *

Implementation Details

There are important caveats to note when + * using this method as a result of the implementation chosen. + *

+ * If the method msg invokes is contained within the {@link MethodCache}, + * there exists type information for the data parameters and they can be + * deserialized into the correct type using this method. + *

+ *

+ * However, if no such method exists within the cache this method falls back + * on using the embedded virtual meta field ({@link #CLASS_META_KEY}). Since + * there is no type information available, there are two main caveats to using + * this fallback method: + *

+ * + *
    + *
  1. Without the type information from the method cache we have no way of + * knowing whether to interpret an array as an array of Objects or as a List + * (or even what implementor of List to use)
  2. + *
+ * + * + * @param msg + * The Message object containing the json-encoded data parameters. + * This object will be modified in-place + * @return A fully-decoded Message + * @throws JsonDeserializationException + * if any of the data parameters are malformed JSON + */ + public static /* @Nonnull */ Message decodeMessageParams(/* @Nonnull */ Message msg) { + String serviceName = msg.getFullName(); + Class clazz = Runtime.getClass(serviceName); + + // Nullability of clazz is checked with this, if null + // falls back to virt class field + boolean useVirtClassField = clazz == null; + if (!useVirtClassField) { + // For blocking send return type checking + ServiceInterface si = Runtime.getService(serviceName); + String blockingKey = String.format("%s.%s", msg.getFullName(), msg.getMethod()); + if (si != null && Message.MSG_TYPE_RETURN.equals(msg.msgType) && si.getInbox().blockingList.containsKey(blockingKey)) { + msg.data[0] = fromJson((String) msg.data[0], si.getInbox().blockingList.get(blockingKey).second); + msg.encoding = null; + return msg; + } + try { + Object[] params = MethodCache.getInstance().getDecodedJsonParameters(clazz, msg.method, msg.data); + if (params == null) + useVirtClassField = true; + else { + msg.data = params; } - return decodeMessageParams(msg); + msg.encoding = null; + } catch (RuntimeException e) { + log.info(String.format("MethodCache lookup fail: %s.%s", serviceName, msg.method)); + // Fallback to virtual class field + useVirtClassField = true; + } } - /** - * Performs the second-stage decoding of a Message - * with JSON-encoded data parameters. This method is meant - * to be a helper for the top-level Message decoding methods - * to go straight from the various codecs to a completely decoded Message. - *

- * Package visibility to allow alternative codecs to use this method. - *

- *

- *

Implementation Details

- * There are important caveats to note when using - * this method as a result of the implementation chosen. - *

- * If the method msg invokes is contained within the - * {@link MethodCache}, there exists type information - * for the data parameters and they can be deserialized - * into the correct type using this method. - *

- *

- * However, if no such method exists within - * the cache this method falls back on using the - * embedded virtual meta field ({@link #CLASS_META_KEY}). - * Since there is no type information available, there are two - * main caveats to using this fallback method: - *

- * - *
    - *
  1. GSON has edge cases for arrays of objects, since - * we don't know what type is contained within the array we are forced - * to deserialize it to an array of Objects, but GSON skips our custom - * deserializer for the Object type. So an array of objects that are not - * primitives nor Strings *will* deserialize incorrectly unless - * we are using Jackson.
  2. - *
  3. Without the type information from the method cache we have - * no way of knowing whether to interpret an array as an array of Objects - * or as a List (or even what implementor of List to use)
  4. - *
- * - * - * @param msg The Message object containing the json-encoded data parameters. - * This object will be modified in-place - * @return A fully-decoded Message - * @throws JsonDeserializationException if any of the data parameters are malformed JSON - */ - static /*@Nonnull*/ Message decodeMessageParams(/*@Nonnull*/ Message msg) { - String serviceName = msg.getFullName(); - Class clazz = Runtime.getClass(serviceName); - ServiceInterface si = Runtime.getService(serviceName); - - //Nullability of clazz is checked with this, if null - //falls back to virt class field - boolean useVirtClassField = clazz == null; - if (!useVirtClassField) { - String blockingKey = String.format("%s.%s", msg.getFullName(), msg.getMethod()); - if (si != null && Message.MSG_TYPE_RETURN.equals(msg.msgType) && si.getInbox().blockingList.containsKey(blockingKey)) { - msg.data[0] = fromJson((String) msg.data[0], si.getInbox().blockingList.get(blockingKey).second); - msg.encoding = null; - return msg; - } - try { - Object[] params = MethodCache.getInstance().getDecodedJsonParameters(clazz, msg.method, msg.data); - if (params == null) - useVirtClassField = true; - else { - msg.data = params; - } - msg.encoding = null; - } catch (RuntimeException e) { - log.info(String.format("MethodCache lookup fail: %s.%s", serviceName, msg.method)); - // Fallback to virtual class field - useVirtClassField = true; - } + // Not an else since useVirtClassField can be set in the above if block + if (useVirtClassField && msg.data != null) { + for (int i = 0; i < msg.data.length; i++) { + if (msg.data[i] instanceof String) { + + if (isBoolean((String) msg.data[i])) { + msg.data[i] = makeBoolean((String) msg.data[i]); + } else if (isInteger((String) msg.data[i])) { + msg.data[i] = makeInteger((String) msg.data[i]); + } else if (isDouble((String) msg.data[i])) { + msg.data[i] = makeDouble((String) msg.data[i]); + } else if (((String) msg.data[i]).startsWith("\"")) { + msg.data[i] = fromJson((String) msg.data[i], String.class); + } else if (((String) msg.data[i]).startsWith("[")) { + // Array, deserialize to ArrayList to maintain compat with jackson + msg.data[i] = fromJson((String) msg.data[i], ArrayList.class); + } else { + // Object + // Serializable should cover everything of interest + + msg.data[i] = fromJson((String) msg.data[i], Serializable.class); + } + + if (msg.data[i] != null && JSON_DEFAULT_OBJECT_TYPE.isAssignableFrom(msg.data[i].getClass())) { + log.warn("Deserialized parameter to default object type. " + "Possibly missing virtual class field: " + msg.data[i]); + } + } else { + log.error("Attempted fallback Message decoding with virtual class field but " + "parameter is not String: %s"); } + } + msg.encoding = null; + } - // Not an else since useVirtClassField can be set in the above if block - if (useVirtClassField) { - for (int i = 0; i < msg.data.length; i++) { - if (msg.data[i] instanceof String) { - // GSON ignores custom deserializers when going to Object - if (!USING_GSON) { - msg.data[i] = fromJson((String) msg.data[i], Object.class); - } else { - // Workaround because GSON won't deserialize a primitive when - // given serializable - if (isBoolean((String) msg.data[i])) { - msg.data[i] = makeBoolean((String) msg.data[i]); - } else if(isInteger((String) msg.data[i])) { - msg.data[i] = makeInteger((String) msg.data[i]); - } else if (isDouble((String) msg.data[i])) { - msg.data[i] = makeDouble((String) msg.data[i]); - } else if (((String) msg.data[i]).startsWith("\"")) { - msg.data[i] = fromJson((String) msg.data[i], String.class); - } else if(((String) msg.data[i]).startsWith("[")) { - // Array, deserialize to ArrayList to maintain compat with jackson - msg.data[i] = fromJson((String) msg.data[i], ArrayList.class); - } else { - // Object - // Serializable should cover everything of interest - - msg.data[i] = fromJson((String) msg.data[i], Serializable.class); - } - - } - - if (msg.data[i] != null && JSON_DEFAULT_OBJECT_TYPE.isAssignableFrom(msg.data[i].getClass())) { - log.warn("Deserialized parameter to default object type. " + - "Possibly missing virtual class field: " + - msg.data[i]); - } - } else { - log.error( - "Attempted fallback Message decoding with virtual class field but " + - "parameter is not String: %s" - ); - } - } - msg.encoding = null; - } + return msg; + } - return msg; - } + /** + * most lossy protocols need conversion of parameters into correctly typed + * elements this method is used to query a candidate method to see if a simple + * conversion is possible + * + * @param clazz + * the class + * @return true/false + */ + public static boolean isSimpleType(Class clazz) { + return WRAPPER_TYPES.contains(clazz) || clazz == String.class; + } - public static Message gsonToMsg(String gsonData) { - return gson.fromJson(gsonData, Message.class); - } + public static boolean isWrapper(Class clazz) { + return WRAPPER_TYPES.contains(clazz); + } - /** - * most lossy protocols need conversion of parameters into correctly typed - * elements this method is used to query a candidate method to see if a simple - * conversion is possible - * - * @param clazz the class - * @return true/false - */ - public static boolean isSimpleType(Class clazz) { - return WRAPPER_TYPES.contains(clazz) || clazz == String.class; + public static boolean isWrapper(String className) { + return WRAPPER_TYPES_CANONICAL.contains(className); + } + + /** + * Converts a snake_case String to a camelCase variant. + * + * @param s + * A String written in snake_case + * @return The same String but converted to camelCase + */ + static public String toCamelCase(String s) { + String[] parts = s.split("_"); + String camelCaseString = ""; + for (String part : parts) { + camelCaseString = camelCaseString + toCCase(part); } + return String.format("%s%s", camelCaseString.substring(0, 1).toLowerCase(), camelCaseString.substring(1)); + } + + /** + * Capitalizes the first character of the string while the rest is set to + * lower case. + * + * @param s + * The string + * @return A String that is all lower case except for the first character + */ + static public String toCCase(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } - public static boolean isWrapper(Class clazz) { - return WRAPPER_TYPES.contains(clazz); + /** + * Convert an Object to its JSON string form using the chosen JSON backend. + * + * @param o + * The object to be converted + * @return The object in String JSON form + * @throws JsonSerializationException + * if serialization fails + */ + public static String toJson(Object o) { + try { + return mapper.writeValueAsString(o); + } catch (Exception e) { + throw new JsonSerializationException(e); } + } - public static boolean isWrapper(String className) { - return WRAPPER_TYPES_CANONICAL.contains(className); + /** + * Convert an object to JSON using the chosen backend and write the result to + * the specified output stream. + * + * @param out + * The OutputStream that the resultant JSON will be written to. + * @param obj + * The object that will be converted to JSON. + * @throws IOException + * if writing to the output stream fails + * @throws JsonSerializationException + * if an exception occurs during serialization. + */ + static public void toJson(OutputStream out, Object obj) throws IOException { + String json; + try { + json = mapper.writeValueAsString(obj); + } catch (Exception jsonProcessingException) { + throw new JsonSerializationException(jsonProcessingException); } + if (json != null) + out.write(json.getBytes()); + } - /** - * Converts a snake_case String to a camelCase variant. - * - * @param s A String written in snake_case - * @return The same String but converted to camelCase - */ - static public String toCamelCase(String s) { - String[] parts = s.split("_"); - String camelCaseString = ""; - for (String part : parts) { - camelCaseString = camelCaseString + toCCase(part); - } - return String.format("%s%s", camelCaseString.substring(0, 1).toLowerCase(), camelCaseString.substring(1)); - } + // === method signatures begin === - /** - * Capitalizes the first character of the string while the rest is - * set to lower case. - * - * @param s The string - * @return A String that is all lower case except for the first character - */ - static public String toCCase(String s) { - return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + /** + * Convert the given object to JSON, as if the object were an instance of the + * given class. + * + * @param o + * The object to be serialized + * @param clazz + * The class to treat the object as + * @return The resultant JSON string + * @throws JsonSerializationException + * if an exception occurs during serialization + */ + public static String toJson(Object o, Class clazz) { + try { + return mapper.writerFor(clazz).writeValueAsString(o); + } catch (Exception e) { + throw new JsonSerializationException(e); } + } - /** - * Convert an Object to its JSON string form using the chosen - * JSON backend. - * - * @param o The object to be converted - * @return The object in String JSON form - * @throws JsonSerializationException if serialization fails - * @see #USING_GSON - */ - public static String toJson(Object o) { - try { - if (USING_GSON) { - return gson.toJson(o); - } - - return mapper.writeValueAsString(o); - } catch (Exception e) { - throw new JsonSerializationException(e); - } + /** + * Serialize the given object to JSON using the selected JSON backend and + * write the result to a file with the given filename. + * + * @param o + * The object to be serialized + * @param filename + * The name of the file to write the JSON to + * @throws IOException + * if writing to the file fails + * @throws JsonSerializationException + * if serialization throws an exception + */ + public static void toJsonFile(Object o, String filename) throws IOException { + byte[] json; + try { + json = mapper.writeValueAsBytes(o); + } catch (Exception e) { + throw new JsonSerializationException(e); } - /** - * Convert an object to JSON using the chosen backend - * and write the result to the specified output stream. - * - * @param out The OutputStream that the resultant JSON will be - * written to. - * @param obj The object that will be converted to JSON. - * @throws IOException if writing to the output stream fails - * @throws JsonSerializationException if an exception occurs during serialization. - * @see #USING_GSON - */ - static public void toJson(OutputStream out, Object obj) throws IOException { - String json; - try { - if (USING_GSON) { - json = gson.toJson(obj); - } else { - json = mapper.writeValueAsString(obj); - } - } catch (Exception jsonProcessingException) { - throw new JsonSerializationException(jsonProcessingException); - } - if (json != null) - out.write(json.getBytes()); + // try-wth-resources, ensures a file is closed even if an exception is + // thrown + try (FileOutputStream fos = new FileOutputStream(filename)) { + fos.write(json); } + } - // === method signatures begin === - - /** - * Convert the given object to JSON, as if the object were - * an instance of the given class. - * - * @param o The object to be serialized - * @param clazz The class to treat the object as - * @return The resultant JSON string - * @throws JsonSerializationException if an exception occurs during serialization - * @see #USING_GSON - */ - public static String toJson(Object o, Class clazz) { - try { - if (USING_GSON) { - return gson.toJson(o, clazz); - } - - return mapper.writerFor(clazz).writeValueAsString(o); - } catch (Exception e) { - throw new JsonSerializationException(e); - } - } + /** + * Converts a given String from camelCase to snake_case, setting the entire + * string to be lowercase. + * + * @param camelCase + * The camelCase string to be converted + * @return The string in snake_case form + */ + static public String toUnderScore(String camelCase) { + return toUnderScore(camelCase, false); + } - /** - * Serialize the given object to JSON using the selected - * JSON backend and write the result to a file with the - * given filename. - * - * @param o The object to be serialized - * @param filename The name of the file to write the JSON to - * @throws IOException if writing to the file fails - * @throws JsonSerializationException if serialization throws an exception - */ - public static void toJsonFile(Object o, String filename) throws IOException { - byte[] json; - try { - if (USING_GSON) { - json = gson.toJson(o).getBytes(); - } else { - json = mapper.writeValueAsBytes(o); - } - } catch (Exception e) { - throw new JsonSerializationException(e); + /** + * Converts a given String from camelCase to snake_case, If toLowerCase is + * true, the entire string will be set to lower case. If false, it will be set + * to uppercase, and if null the casing will not be changed. + * + * @param camelCase + * The camelCase string to be converted + * @param toLowerCase + * Whether the entire string should be lowercase, uppercase, or not + * changed (null) + * @return The string in snake_case form + */ + static public String toUnderScore(String camelCase, Boolean toLowerCase) { + + byte[] a = camelCase.getBytes(); + boolean lastLetterLower = false; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < a.length; ++i) { + boolean currentCaseUpper = Character.isUpperCase(a[i]); + + Character newChar = null; + if (toLowerCase != null) { + if (toLowerCase) { + newChar = (char) Character.toLowerCase(a[i]); + } else { + newChar = (char) Character.toUpperCase(a[i]); } + } else { + newChar = (char) a[i]; + } - //try-wth-resources, ensures a file is closed even if an exception is thrown - try (FileOutputStream fos = new FileOutputStream(filename)) { - fos.write(json); - } + sb.append(String.format("%s%c", (lastLetterLower && currentCaseUpper) ? "_" : "", newChar)); + lastLetterLower = !currentCaseUpper; } - /** - * Converts a given String from camelCase to snake_case, - * setting the entire string to be lowercase. - * - * @param camelCase The camelCase string to be converted - * @return The string in snake_case form - */ - static public String toUnderScore(String camelCase) { - return toUnderScore(camelCase, false); - } + return sb.toString(); - /** - * Converts a given String from camelCase to snake_case, - * If toLowerCase is true, the entire string will be set to - * lower case. If false, it will be set to uppercase, and if - * null the casing will not be changed. - * - * @param camelCase The camelCase string to be converted - * @param toLowerCase Whether the entire string should be lowercase, uppercase, - * or not changed (null) - * @return The string in snake_case form - */ - static public String toUnderScore(String camelCase, Boolean toLowerCase) { - - byte[] a = camelCase.getBytes(); - boolean lastLetterLower = false; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < a.length; ++i) { - boolean currentCaseUpper = Character.isUpperCase(a[i]); - - Character newChar = null; - if (toLowerCase != null) { - if (toLowerCase) { - newChar = (char) Character.toLowerCase(a[i]); - } else { - newChar = (char) Character.toUpperCase(a[i]); - } - } else { - newChar = (char) a[i]; - } - - sb.append(String.format("%s%c", (lastLetterLower && currentCaseUpper) ? "_" : "", newChar)); - lastLetterLower = !currentCaseUpper; - } + } - return sb.toString(); + /** + * Equivalent to {@link #isInteger(String)} + * + * @param string + * The String to be checked + * @return Whether the String can be parsed as an Integer + */ + @Deprecated + public static boolean tryParseInt(String string) { + try { + Integer.parseInt(string); + return true; + } catch (Exception e) { } + return false; + } - /** - * Equivalent to {@link #isInteger(String)} - * - * @param string The String to be checked - * @return Whether the String can be parsed as an Integer - */ - @Deprecated - public static boolean tryParseInt(String string) { - try { - Integer.parseInt(string); - return true; - } catch (Exception e) { - - } - return false; + public static String type(String type) { + int pos0 = type.indexOf("."); + if (pos0 > 0) { + return type; } + return String.format("org.myrobotlab.service.%s", type); + } - public static String type(String type) { - int pos0 = type.indexOf("."); - if (pos0 > 0) { - return type; - } - return String.format("org.myrobotlab.service.%s", type); + /** + * Get the simple name of a service type name. A simple name is the name of + * the service type without any package specifier. + * + * @param serviceType + * The service type in String form + * @return The simple name of the servide type + */ + public static String getSimpleName(String serviceType) { + int pos = serviceType.lastIndexOf("."); + if (pos > -1) { + return serviceType.substring(pos + 1); } + return serviceType; + } - /** - * Get the simple name of a service type name. - * A simple name is the name of the service type - * without any package specifier. - * - * @param serviceType The service type in String form - * @return The simple name of the servide type - */ - public static String getSimpleName(String serviceType) { - int pos = serviceType.lastIndexOf("."); - if (pos > -1) { - return serviceType.substring(pos + 1); - } - return serviceType; - } + public static String getSafeReferenceName(String name) { + return name.replaceAll("[@/ .-]", "_"); + } - public static String getSafeReferenceName(String name) { - return name.replaceAll("[@/ .-]", "_"); + /** + * Serializes the specified object to JSON, using + * {@link #mapper} with {@link #jacksonPrettyPrinter} to pretty-ify the + * result. + * + * @param ret + * The object to be serialized + * @return The object in pretty JSON form + */ + public static String toPrettyJson(Object ret) { + try { + return mapper.writer(jacksonPrettyPrinter).writeValueAsString(ret); + } catch (Exception e) { + throw new JsonSerializationException(e); } - /** - * Serializes the specified object to JSON, using - * {@link #prettyGson} or {@link #mapper} - * with {@link #jacksonPrettyPrinter} - * to pretty-ify the result. Which object is used depends on - * {@link #USING_GSON} - * - * @param ret The object to be serialized - * @return The object in pretty JSON form - */ - public static String toPrettyJson(Object ret) { - try { - if (USING_GSON) { - return prettyGson.toJson(ret); - } else { - - return mapper.writer(jacksonPrettyPrinter).writeValueAsString(ret); - } - } catch (Exception e) { - throw new JsonSerializationException(e); - } + } + /** + * Deserialize a given String into an array of Objects, treating the String as + * JSON and using the selected JSON backend. + * + * @param data + * A String containing a JSON array + * @return An array of Objects created by deserializing the JSON array + * @throws Exception + * If deserialization fails + */ + static public Object[] decodeArray(Object data) throws Exception { + // ITS GOT TO BE STRING - it just has to be !!! :) + String instr = (String) data; + // array of Strings ? - don't want to double encode ! + Object[] ret = null; + synchronized (data) { + ret = mapper.readValue(instr, Object[].class); } + return ret; + } - /** - * Deserialize a given String into an array of Objects, - * treating the String as JSON and using the selected JSON backend. - * - * @param data A String containing a JSON array - * @return An array of Objects created by deserializing the JSON array - * @throws Exception If deserialization fails - */ - static public Object[] decodeArray(Object data) throws Exception { - // ITS GOT TO BE STRING - it just has to be !!! :) - String instr = (String) data; - // array of Strings ? - don't want to double encode ! - Object[] ret = null; - synchronized (data) { - if (USING_GSON) { - ret = gson.fromJson(instr, Object[].class); - } else { - ret = mapper.readValue(instr, Object[].class); - } - } - return ret; - } + /** + * This is a Path to Message decoder - it takes a line of text and generates + * the appropriate msg with json encoded string parameters and either invokes + * (locally) or sendBlockingRemote (remotely) + * + *
+   *
+   * The expectation of this encoding is:
+   *    if "/api/service/" is found - the end of that string is the starting point
+   *    if "/api/service/" is not found - then the starting point of the string should be the service
+   *      e.g "runtime/getUptime"
+   *
+   * Important to remember getRequestURI is NOT decoded and getPathInfo is.
+   *
+   *
+   *
+   * Method              URL-Decoded Result
+   * ----------------------------------------------------
+   * getContextPath()        no      /app
+   * getLocalAddr()                  127.0.0.1
+   * getLocalName()                  30thh.loc
+   * getLocalPort()                  8480
+   * getMethod()                     GET
+   * getPathInfo()           yes     /a?+b
+   * getProtocol()                   HTTP/1.1
+   * getQueryString()        no      p+1=c+dp+2=e+f
+   * getRequestedSessionId() no      S%3F+ID
+   * getRequestURI()         no      /app/test%3F/a%3F+b;jsessionid=S+ID
+   * getRequestURL()         no      http://30thh.loc:8480/app/test%3F/a%3F+b;jsessionid=S+ID
+   * getScheme()                     http
+   * getServerName()                 30thh.loc
+   * getServerPort()                 8480
+   * getServletPath()        yes     /test?
+   * getParameterNames()     yes     [p 2, p 1]
+   * getParameter("p 1")     yes     c d
+   * 
+ * + * @param from + * - sender + * @param path + * - cli encoded msg + * @return - a Message derived from cli + */ + static public Message pathToMsg(String from, String path) { + // Message msg = Message.createMessage(from,"ls", null); + Message msg = new Message(); + msg.name = "runtime"; // default ? + msg.method = "ls"; + + // not required unless asynchronous + msg.sender = from; /** - * This is the Cli encoder - it takes a line of text and generates the - * appropriate msg from it to either invoke (locally) or sendBlockingRemote - * (remotely) - * *
-     *
-     * The expectation of this encoding is:
-     *    if "/api/service/" is found - the end of that string is the starting point
-     *    if "/api/service/" is not found - then the starting point of the string should be the service
-     *      e.g "runtime/getUptime"
-     *
-     * Important to remember getRequestURI is NOT decoded and getPathInfo is.
-     *
+    
+     The key to this interface is leading "/" ...
+     "/" is absolute path - dir or execute
+     without "/" means runtime method - spaces and quotes can be delimiters
+    
+     "/"  -  list services
+     "/{serviceName}" - list data of service
+     "/{serviceName}/" - list methods of service
+     "/{serviceName}/{method}" - invoke method
+     "/{serviceName}/{method}/" - list parameters of method
+     "/{serviceName}/{method}/jsonP0/jsonP1/jsonP2/..." - invoke method with parameters
+    
+     or runtime
+     {method}
+     {method}/
+     {method}/p01
      *
      *
-     * Method              URL-Decoded Result
-     * ----------------------------------------------------
-     * getContextPath()        no      /app
-     * getLocalAddr()                  127.0.0.1
-     * getLocalName()                  30thh.loc
-     * getLocalPort()                  8480
-     * getMethod()                     GET
-     * getPathInfo()           yes     /a?+b
-     * getProtocol()                   HTTP/1.1
-     * getQueryString()        no      p+1=c+dp+2=e+f
-     * getRequestedSessionId() no      S%3F+ID
-     * getRequestURI()         no      /app/test%3F/a%3F+b;jsessionid=S+ID
-     * getRequestURL()         no      http://30thh.loc:8480/app/test%3F/a%3F+b;jsessionid=S+ID
-     * getScheme()                     http
-     * getServerName()                 30thh.loc
-     * getServerPort()                 8480
-     * getServletPath()        yes     /test?
-     * getParameterNames()     yes     [p 2, p 1]
-     * getParameter("p 1")     yes     c d
      * 
- * - * @param contextPath - prefix to be added if supplied - * @param from - sender - * @param to - target service - * @param cmd - cli encoded msg - * @return - a Message derived from cli */ - static public Message cliToMsg(String contextPath, String from, String to, String cmd) { - Message msg = Message.createMessage(from, to, "ls", null); - - /** - *
-
-         The key to this interface is leading "/" ...
-         "/" is absolute path - dir or execute
-         without "/" means runtime method - spaces and quotes can be delimiters
-
-         "/"  -  list services
-         "/{serviceName}" - list data of service
-         "/{serviceName}/" - list methods of service
-         "/{serviceName}/{method}" - invoke method
-         "/{serviceName}/{method}/" - list parameters of method
-         "/{serviceName}/{method}/p0/p1/p2" - invoke method with parameters
-
-         or runtime
-         {method}
-         {method}/
-         {method}/p01
-         *
-         *
-         * 
- */ - - cmd = cmd.trim(); - - // remove uninteresting api prefix - if (cmd.startsWith(API_SERVICE_PATH)) { - cmd = cmd.substring(API_SERVICE_PATH.length()); - } - - if (contextPath != null) { - cmd = contextPath + cmd; - } - // assume runtime as 'default' - if (msg.name == null) { - // FIXME "runtime" really needs to be a constant at the very least - msg.name = "runtime"; - } + path = path.trim(); - // two possibilities - either it begins with "/" or it does not - // if it does begin with "/" its an absolute path to a dir, ls, or invoke - // if not then its a runtime method - - if (cmd.startsWith("/")) { - // ABSOLUTE PATH !!! - String[] parts = cmd.split("/"); - - if (parts.length < 3) { - msg.method = "ls"; - msg.data = new Object[]{cmd}; - return msg; - } - - // fix me diff from 2 & 3 "/" - if (parts.length >= 3) { - // prepare to parse the arguments - - msg.name = parts[1]; - // prepare the method - msg.method = parts[2].trim(); - - // FIXME - to encode or not to encode that is the question ... - // This source comes from the cli - which is "all" strings - // in theory it needs to be decoded from an all strings interface - // json is an all string interface so we will decode from cli strings - // (not json) - // using a json decoder - cuz it will work :P - and string will decode - // to a string - Object[] payload = new Object[parts.length - 3]; - for (int i = 3; i < parts.length; ++i) { - if (isInteger(parts[i])) { - payload[i - 3] = makeInteger(parts[i]); - } else if (isDouble(parts[i])) { - payload[i - 3] = makeDouble(parts[i]); - } else if (parts[i].equals("true") || parts[i].equals("false")) { - payload[i - 3] = makeBoolean(parts[i]); - } else { // String - // sloppy as the cli does not require quotes \" but json does - // humans won't add quotes - but we will - payload[i - 3] = parts[i]; - } - } - - msg.data = payload; - } - return msg; - } else { - // NOT ABOSLUTE PATH - SIMILAR TO EXECUTING IN THE RUNTIME /usr/bin path - // (ie runtime methods!) - // spaces for parameter delimiters ? - String[] spaces = cmd.split(" "); - // FIXME - need to deal with double quotes e.g. func A "B and C" D - p0 = - // "A" p1 = "B and C" p3 = "D" - msg.method = spaces[0]; - Object[] payload = new Object[spaces.length - 1]; - for (int i = 1; i < spaces.length; ++i) { - // webgui will never use this section of code - // currently the codepath is only excercised by InProcessCli - // all of this methods will be "optimized" single commands to runtime (i - // think) - // so we are going to error on the side of String parameters - other - // data types will have problems - payload[i - 1] = spaces[i]; - } - msg.data = payload; - - return msg; - } + // remove uninteresting api prefix + if (path.startsWith(API_SERVICE_PATH)) { + path = path.substring(API_SERVICE_PATH.length()); } - /** - * Parse the specified data as an Integer. If parsing - * fails, returns null - * - * @param data The String to be coerced into an Integer - * @return the data as an Integer, if parsing fails then null instead - */ - static public Integer makeInteger(String data) { - try { - return Integer.parseInt(data); - } catch (Exception e) { - } - return null; - } + // two possibilities - either it begins with "/" or it does not + // if it does begin with "/" its an absolute path to a dir, ls, or invoke + // if not then its a runtime method + + if (path.startsWith("/")) { + // ABSOLUTE PATH !!! + String[] parts = path.split("/"); // <- this breaks things ! e.g. + // /runtime/connect/"http://localhost:8888" + // path parts less than 3 is a dir or ls + if (parts.length < 3) { + // this morphs a path which has less than 3 parts + // into a runtime "ls" method call to do reflection of services or service methods + // e.g. /clock -> /runtime/ls/"/clock" + // e.g. /clock/ -> /runtime/ls/"/clock/" + + msg.method = "ls"; + msg.data = new Object[] { "\"" + path + "\""}; + return msg; + } - /** - * Checks whether the given String can be parsed as - * an Integer - * - * @param data The string to be checked - * @return true if the data can be parsed as an Integer, false otherwise - */ - static public boolean isInteger(String data) { - try { - Integer.parseInt(data); - return true; - } catch (Exception e) { - } - return false; - } + // ["", "runtime", "shutdown"] + if (parts.length == 3) { + msg.name = parts[1]; + msg.method = parts[2]; + return msg; + } - /** - * Checks whether the given String can be parsed as - * a Double - * - * @param data The string to be checked - * @return true if the data can be parsed as a Double, false otherwise - */ - static public boolean isDouble(String data) { - try { - Double.parseDouble(data); - return true; - } catch (Exception e) { - } - return false; + // fix me diff from 2 & 3 "/" + if (parts.length >= 3) { + // prepare to parse the arguments + + msg.name = parts[1]; + // prepare the method + msg.method = parts[2].trim(); + + // remove the first 3 slashes + String data = path.substring(("/" + msg.name + "/" + msg.method + "/").length()); + msg.data = extractJsonParamsFromPath(data); + } + return msg; + } else { + // e.g. ls /webgui/ + // retrieves all webgui methods + String[] spaces = path.split(" "); + // FIXME - need to deal with double quotes e.g. func A "B and C" D - p0 = + // "A" p1 = "B and C" p3 = "D" + msg.method = spaces[0]; + Object[] payload = new Object[spaces.length - 1]; + for (int i = 1; i < spaces.length; ++i) { + // webgui will never use this section of code + // currently the codepath is only excercised by InProcessCli + // all of this methods will be "optimized" single commands to runtime (i + // think) + // so we are going to error on the side of String parameters - other + // data types will have problems + payload[i - 1] = spaces[i]; + } + msg.data = payload; + + return msg; } + } - /** - * Parse the specified data as a Double. If parsing - * fails, returns null - * - * @param data The String to be coerced into a Doubled= - * @return the data as a Double, if parsing fails then null instead - */ - static public Double makeDouble(String data) { - try { - return Double.parseDouble(data); - } catch (Exception e) { + /** + * extractJsonFromPath exects a forwad slash deliminated string + * + *
+   * json1 / json2 / json3
+   * 
+ * + * It will return the json parts in a string array + * + * @param input + * @return + */ + public static Object[] extractJsonParamsFromPath(String input) { + List fromJson = new ArrayList<>(); + StringBuilder currentJson = new StringBuilder(); + boolean insideQuotes = false; + + for (char c : input.toCharArray()) { + if (c == '"' && !insideQuotes) { + insideQuotes = true; + } else if (c == '"' && insideQuotes) { + insideQuotes = false; + } + + if (c == '/' && !insideQuotes) { + if (currentJson.length() > 0) { + fromJson.add(currentJson.toString()); + currentJson = new StringBuilder(); } - return null; + } else { + currentJson.append(c); + } } - /** - * Checks whether the given String can be parsed as - * a Boolean - * - * @param data The string to be checked - * @return true if the data can be parsed as a boolean, false otherwise - */ - @SuppressWarnings("ResultOfMethodCallIgnored") - static public Boolean isBoolean(String data) { - try { - Boolean.parseBoolean(data); - // The above will return false and not throw - // exception for most cases - return "true".equals(data) || "false".equals(data); - } catch (Exception ignored) { - return false; - } + if (currentJson.length() > 0) { + fromJson.add(currentJson.toString()); } - /** - * Parse the specified data as a Boolean. If parsing - * fails, returns null - * - * @param data The String to be coerced into a Boolean - * @return the data as a boolean, if parsing fails then null instead - */ - static public Boolean makeBoolean(String data) { - try { - return Boolean.parseBoolean(data); - } catch (Exception e) { - } - return null; + return fromJson.toArray(new Object[0]); + } + + /** + * Parse the specified data as an Integer. If parsing fails, returns null + * + * @param data + * The String to be coerced into an Integer + * @return the data as an Integer, if parsing fails then null instead + */ + static public Integer makeInteger(String data) { + try { + return Integer.parseInt(data); + } catch (Exception e) { } + return null; + } - /** - * Get a description for each of the supported APIs - * - * @return A list containing a description for each supported API - */ - static public List getApis() { - List ret = new ArrayList<>(); - ret.add(new ApiDescription("message", "{scheme}://{host}:{port}" + API_MESSAGES_PATH, "ws://localhost:8888" + API_MESSAGES_PATH, - "An asynchronous api useful for bi-directional websocket communication, primary messages api for the webgui. URI is " + API_MESSAGES_PATH + " data contains a json encoded Message structure")); - ret.add(new ApiDescription("service", "{scheme}://{host}:{port}" + API_SERVICE_PATH, "http://localhost:8888" + API_SERVICE_PATH +"/runtime/getUptime", - "An synchronous api useful for simple REST responses")); - return ret; + /** + * Checks whether the given String can be parsed as an Integer + * + * @param data + * The string to be checked + * @return true if the data can be parsed as an Integer, false otherwise + */ + static public boolean isInteger(String data) { + try { + Integer.parseInt(data); + return true; + } catch (Exception e) { } + return false; + } - /** - * Creates a properly double encoded Json msg string. Why double encode ? - - * because initial decode should decode router and header information. The - * first decode will leave the payload a array of json strings. The header - * will send it to a another process or it will go to the MethodCache of some - * service. The MethodCache will decode a 2nd time based on a method signature - * key match (key based on parameter types). - * - * @param sender the sender of the message - * @param sendingMethod the method sending it - * @param name dest service - * @param method dest method - * @param params params to pass - * @return the string representation of the json message - */ - public static String createJsonMsg(String sender, String sendingMethod, String name, String method, Object... params) { - Message msg = Message.createMessage(sender, name, method, null); - msg.sendingMethod = sendingMethod; - Object[] d = null; - if (params != null) { - d = new Object[params.length]; - for (int i = 0; i < params.length; ++i) { - d[i] = CodecUtils.toJson(params[i]); - } - msg.setData(d); - } - return CodecUtils.toJson(msg); + /** + * Checks whether the given String can be parsed as a Double + * + * @param data + * The string to be checked + * @return true if the data can be parsed as a Double, false otherwise + */ + static public boolean isDouble(String data) { + try { + Double.parseDouble(data); + return true; + } catch (Exception e) { } + return false; + } - /** - * Encodes a Message as JSON, double-encoding the - * {@link Message#data} if not already double-encoded. - * The selected JSON backend will be used. - * - * @param inMsg The message to be encoded - * @return A String representation of the message and all of its - * members in JSON format. - * @see #USING_GSON - */ - public static String toJsonMsg(Message inMsg) { - if ("json".equals(inMsg.encoding)) { - // msg already has json encoded data parameters - // just encode the msg envelope - return CodecUtils.toJson(inMsg); - } - Message msg = new Message(inMsg); - msg.encoding = "json"; - Object[] params = inMsg.getData(); - Object[] d = null; - if (params != null) { - d = new Object[params.length]; - for (int i = 0; i < params.length; ++i) { - d[i] = CodecUtils.toJson(params[i]); - } - msg.setData(d); - } - return CodecUtils.toJson(msg); + /** + * Parse the specified data as a Double. If parsing fails, returns null + * + * @param data + * The String to be coerced into a Doubled= + * @return the data as a Double, if parsing fails then null instead + */ + static public Double makeDouble(String data) { + try { + return Double.parseDouble(data); + } catch (Exception e) { } + return null; + } - @Deprecated - public static Message toJsonParameters(Message msg) { - Object[] data = msg.getData(); - if (data != null) { - Object[] params = new Object[data.length]; - for (int i = 0; i < params.length; ++i) { - params[i] = toJson(data[i]); - } - msg.setData(params); - } - return msg; + /** + * Checks whether the given String can be parsed as a Boolean + * + * @param data + * The string to be checked + * @return true if the data can be parsed as a boolean, false otherwise + */ + @SuppressWarnings("ResultOfMethodCallIgnored") + static public Boolean isBoolean(String data) { + try { + Boolean.parseBoolean(data); + // The above will return false and not throw + // exception for most cases + return "true".equals(data) || "false".equals(data); + } catch (Exception ignored) { + return false; } + } - /** - * Serialize the given object to YAML. - * - * @param o The object to be serialized - * @return A String formatted as YAML representing the object - */ - public static String toYaml(Object o) { - // not thread safe - so we new here - DumperOptions options = new DumperOptions(); - options.setIndent(2); - options.setPrettyFlow(true); - // options.setBeanAccess(BeanAccess.FIELD); - options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - /** - *
-         *  How to suppress null fields if desired
-         Representer representer = new Representer() {
-         @Override
-         protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) {
-         // if value of property is null, ignore it.
-         if (propertyValue == null) {
-         return null;
-         } else {
-         return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
-         }
-         }
-         };
-         * 
- */ - - Yaml yaml = new Yaml(options); - // yaml.setBeanAccess(BeanAccess.FIELD); - String c = yaml.dump(o); - return c; + /** + * Parse the specified data as a Boolean. If parsing fails, returns null + * + * @param data + * The String to be coerced into a Boolean + * @return the data as a boolean, if parsing fails then null instead + */ + static public Boolean makeBoolean(String data) { + try { + return Boolean.parseBoolean(data); + } catch (Exception e) { } + return null; + } + + /** + * Get a description for each of the supported APIs + * + * @return A list containing a description for each supported API + */ + static public List getApis() { + List ret = new ArrayList<>(); + ret.add(new ApiDescription("message", "{scheme}://{host}:{port}" + API_MESSAGES_PATH, "ws://localhost:8888" + API_MESSAGES_PATH, + "An asynchronous api useful for bi-directional websocket communication, primary messages api for the webgui. URI is " + API_MESSAGES_PATH + + " data contains a json encoded Message structure")); + ret.add(new ApiDescription("service", "{scheme}://{host}:{port}" + API_SERVICE_PATH, "http://localhost:8888" + API_SERVICE_PATH + "/runtime/getUptime", + "An synchronous api useful for simple REST responses")); + return ret; + } - public static String allToYaml(Iterator o) { - // not thread safe - so we new here - DumperOptions options = new DumperOptions(); - options.setIndent(2); - options.setPrettyFlow(true); - options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - - Yaml yaml = new Yaml(options); - // yaml.setBeanAccess(BeanAccess.FIELD); - String c = yaml.dumpAll(o); - return c; + /** + * Creates a properly double encoded Json msg string. Why double encode ? - + * because initial decode should decode router and header information. The + * first decode will leave the payload a array of json strings. The header + * will send it to a another process or it will go to the MethodCache of some + * service. The MethodCache will decode a 2nd time based on a method signature + * key match (key based on parameter types). + * + * @param sender + * the sender of the message + * @param sendingMethod + * the method sending it + * @param name + * dest service + * @param method + * dest method + * @param params + * params to pass + * @return the string representation of the json message + */ + public static String createJsonMsg(String sender, String sendingMethod, String name, String method, Object... params) { + Message msg = Message.createMessage(sender, name, method, null); + msg.sendingMethod = sendingMethod; + Object[] d = null; + if (params != null) { + d = new Object[params.length]; + for (int i = 0; i < params.length; ++i) { + d[i] = CodecUtils.toJson(params[i]); + } + msg.setData(d); } + return CodecUtils.toJson(msg); + } - public static Iterable allFromYaml(InputStream is) { - // Yaml yaml = new Yaml(new Constructor(clazz)); - Yaml yaml = new Yaml(); - // yaml.setBeanAccess(BeanAccess.FIELD); - return yaml.loadAll(is); + /** + * Encodes a Message as JSON, double-encoding the {@link Message#data} if not + * already double-encoded. The selected JSON backend will be used. + * + * @param inMsg + * The message to be encoded + * @return A String representation of the message and all of its members in + * JSON format. + */ + public static String toJsonMsg(Message inMsg) { + if ("json".equals(inMsg.encoding)) { + // msg already has json encoded data parameters + // just encode the msg envelope + return CodecUtils.toJson(inMsg); + } + Message msg = new Message(inMsg); + msg.encoding = "json"; + Object[] params = inMsg.getData(); + Object[] d = null; + if (params != null) { + d = new Object[params.length]; + for (int i = 0; i < params.length; ++i) { + d[i] = CodecUtils.toJson(params[i]); + } + msg.setData(d); } + return CodecUtils.toJson(msg); + } - /** - * Deserialize the given string into the specified class, - * treating the string as YAML. - * - * @param data The YAML to be deserialized - * @param clazz The target class - * @param The type of the target class - * @return An instance of the target class with the state given by the YAML string - */ - public static T fromYaml(String data, Class clazz) { - Yaml yaml = new Yaml(new Constructor(clazz)); - // yaml.setBeanAccess(BeanAccess.FIELD); - return (T) yaml.load(data); + @Deprecated + public static Message toJsonParameters(Message msg) { + Object[] data = msg.getData(); + if (data != null) { + Object[] params = new Object[data.length]; + for (int i = 0; i < params.length; ++i) { + params[i] = toJson(data[i]); + } + msg.setData(params); } + return msg; + } + /** + * Serialize the given object to YAML. + * + * @param o + * The object to be serialized + * @return A String formatted as YAML representing the object + */ + public static String toYaml(Object o) { + // not thread safe - so we new here + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setPrettyFlow(true); + // options.setBeanAccess(BeanAccess.FIELD); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); /** - * Checks if the service name given by name is local, - * i.e. it has no remote ID (has no '@' symbol), or - * if it has a remote ID it matches the ID given. - * - * @param name The service name to be checked - * @param id The runtime ID of the local instance - * @return Whether the service name is local to the given ID + *
+     *  How to suppress null fields if desired
+     Representer representer = new Representer() {
+     @Override
+     protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) {
+     // if value of property is null, ignore it.
+     if (propertyValue == null) {
+     return null;
+     } else {
+     return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
+     }
+     }
+     };
+     * 
*/ - public static boolean isLocal(String name, String id) { - if (!name.contains("@")) { - return true; - } - return name.substring(name.indexOf("@") + 1).equals(id); + + Yaml yaml = new Yaml(options); + // yaml.setBeanAccess(BeanAccess.FIELD); + String c = yaml.dump(o); + return c; + } + + public static String allToYaml(Iterator o) { + // not thread safe - so we new here + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + + Yaml yaml = new Yaml(options); + // yaml.setBeanAccess(BeanAccess.FIELD); + String c = yaml.dumpAll(o); + return c; + } + + public static Iterable allFromYaml(InputStream is) { + // Yaml yaml = new Yaml(new Constructor(clazz)); + Yaml yaml = new Yaml(); + // yaml.setBeanAccess(BeanAccess.FIELD); + return yaml.loadAll(is); + } + + /** + * Deserialize the given string into the specified class, treating the string + * as YAML. + * + * @param data + * The YAML to be deserialized + * @param clazz + * The target class + * @param + * The type of the target class + * @return An instance of the target class with the state given by the YAML + * string + */ + public static T fromYaml(String data, Class clazz) { + Yaml yaml = new Yaml(new Constructor(clazz)); + // yaml.setBeanAccess(BeanAccess.FIELD); + return (T) yaml.load(data); + } + + /** + * Checks if the service name is local to the current process instance + * + * @param name + * The service name to be checked + * @return Whether the service name is local to the given ID + */ + public static boolean isLocal(String name) { + if (!name.contains("@")) { + return true; } + return name.substring(name.indexOf("@") + 1).equals(Platform.getLocalInstance().getId()); + } - /** - * Read a YAML file given by the filename and convert it into - * a ServiceConfig object by deserialization. - * - * @param filename The name of the YAML file - * @return The equivalent ServiceConfig object - * @throws IOException if reading the file fails - */ - public static ServiceConfig readServiceConfig(String filename) throws IOException { - String data = Files.readString(Paths.get(filename)); - Yaml yaml = new Yaml(); - return yaml.load(data); + /** + * Checks if the service name given by name is local, i.e. it has no remote ID + * (has no '@' symbol), or if it has a remote ID it matches the ID given. + * + * @param name + * The service name to be checked + * @param id + * The runtime ID of the local instance + * @return Whether the service name is local to the given ID + */ + public static boolean isLocal(String name, String id) { + if (!name.contains("@")) { + return true; } + return name.substring(name.indexOf("@") + 1).equals(id); + } - /** - * Set a field of the given object identified by the - * given field name to the given value. If the field - * does not exist or the value is of the wrong type, - * this method is a no-op. - * - * @param o The object whose field will be modified - * @param field The name of the field to be modified - * @param value The new value to set the field to - */ - public static void setField(Object o, String field, Object value) { - try { - // TODO - handle all types :P - Field f = o.getClass().getDeclaredField(field); - f.setAccessible(true); - f.set(o, value); - } catch (Exception e) { - /** don't care - if its not there don't set it */ - } + /** + * Read a YAML file given by the filename and convert it into a ServiceConfig + * object by deserialization. + * + * @param filename + * The name of the YAML file + * @return The equivalent ServiceConfig object + * @throws IOException + * if reading the file fails + */ + public static ServiceConfig readServiceConfig(String filename) throws IOException { + String data = Files.readString(Paths.get(filename)); + Yaml yaml = new Yaml(); + return yaml.load(data); + } + + /** + * Set a field of the given object identified by the given field name to the + * given value. If the field does not exist or the value is of the wrong type, + * this method is a no-op. + * + * @param o + * The object whose field will be modified + * @param field + * The name of the field to be modified + * @param value + * The new value to set the field to + */ + public static void setField(Object o, String field, Object value) { + try { + // TODO - handle all types :P + Field f = o.getClass().getDeclaredField(field); + f.setAccessible(true); + f.set(o, value); + } catch (Exception e) { + /** don't care - if its not there don't set it */ } + } - public static void main(String[] args) { - LoggingFactory.init(Level.INFO); + public static void main(String[] args) { + LoggingFactory.init(Level.INFO); - try { - - Object o = readServiceConfig("data/config/InMoov2_FingerStarter/i01.chatBot.yml"); + try { - String json = CodecUtils.fromJson("test", String.class); - log.info("json {}", json); - json = CodecUtils.fromJson("a test", String.class); - log.info("json {}", json); - json = CodecUtils.fromJson("\"a/test\"", String.class); - log.info("json {}", json); - CodecUtils.fromJson("a/test", String.class); + Object o = readServiceConfig("data/config/InMoov2_FingerStarter/i01.chatBot.yml"); - } catch (Exception e) { - log.error("main threw", e); - } + String json = CodecUtils.fromJson("test", String.class); + log.info("json {}", json); + json = CodecUtils.fromJson("a test", String.class); + log.info("json {}", json); + json = CodecUtils.fromJson("\"a/test\"", String.class); + log.info("json {}", json); + CodecUtils.fromJson("a/test", String.class); + + } catch (Exception e) { + log.error("main threw", e); } + } /** * A description of an API type @@ -1562,8 +1610,8 @@ public int hashCode() { } /** - * Single parameter from JSON. Will use default return type, currently LinkedTreeMap - * to return a POJO object that can be easily accessed. + * Single parameter from JSON. Will use default return type, currently + * LinkedTreeMap to return a POJO object that can be easily accessed. * * @param json * @return @@ -1572,4 +1620,12 @@ public static Object fromJson(String json) { return fromJson(json, (Class) null); } + public static String toBase64(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + public static byte[] fromBase64(String input) { + return Base64.getDecoder().decode(input); + } + } diff --git a/src/main/java/org/myrobotlab/codec/ForeignProcessUtils.java b/src/main/java/org/myrobotlab/codec/ForeignProcessUtils.java new file mode 100644 index 0000000000..1c008f3ea2 --- /dev/null +++ b/src/main/java/org/myrobotlab/codec/ForeignProcessUtils.java @@ -0,0 +1,165 @@ +package org.myrobotlab.codec; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * This class contains many utility methods related to foreign + * processes that are written in other programming languages. + *

+ * This class defines a format for describing "classes" + * or types that originate from unknown programming languages. + *

+ * The format consists of two simple parts: the language ID, that + * describes what language the process is using, and the + * language-specific type key. It is expected that each + * language-specific type key maps to a set of runtime-static procedures, + * so in the case of languages where a type's applicable procedures + * can change during runtime, as in Python, it is recommended to + * generate a new type key for every change in the procedure list. + * This is because the set of known procedures is cached and is not regenerated + * to improve performance. + *

+ * Currently, this information is only used to determine when + * to generate a {@link java.lang.reflect.Proxy}, but it would + * enable foreign processes in the future to instantiate the + * correct proxy when dynamic proxies aren't possible such as + * in fully compiled languages. + * + * @author AutonomicPerfectionist + */ +public class ForeignProcessUtils { + + + /** + * The string used to separate the two parts of the + * foreign process type specifier. The language ID + * may not contain this string, but the language-specific + * type key may. It is used in a regular expression + * so escaping might be required. + */ + public static final String LANGUAGE_ID_SEPARATOR = ":"; + + /** + * A pattern that both tests whether a string is a valid foreign + * type key and splits the key on the language id separator. + *

+ * For example, if the separator is a single colon ({@code ':'}), + * then "py:exampleService" would match and the two capture groups would be + * "py" and "exampleService." + *

+ * This pattern does not allow the separator in the language ID at all, + * but does allow it in the language-specific type key (the second capture group). + *

+ * This enables languages that use double-colons for package or module definition + * to work seamlessly. + */ + public static final Pattern FOREIGN_TYPE_KEY_PATTERN = Pattern.compile( + String.format("^([^%s]+)%s(.+)$", LANGUAGE_ID_SEPARATOR, LANGUAGE_ID_SEPARATOR)); + + + /** + * Java identifier pattern, using builtin Java regex "macros." + * This is a string regex pattern that identifies a valid + * Java identifier. + */ + private static final String JAVA_ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; + + /** + * Java fully-qualified class name pattern. This pattern is used + * to determine if a type key is a valid Java class. If it is not + * and the type key also does not match the {@link #FOREIGN_TYPE_KEY_PATTERN}, + * then the type key is malformed and should be rejected. + */ + private static final Pattern JAVA_FQCN_PATTERN = Pattern.compile(JAVA_ID_PATTERN + "(\\." + JAVA_ID_PATTERN + ")*"); + + + /** + * Checks whether the given string is a valid + * fully-qualified class name. + * + * @param name The string to be checked + * @return Whether name is a valid FQCN + */ + public static boolean isValidJavaClassName(String name) { + return JAVA_FQCN_PATTERN.matcher(name).matches(); + } + + + /** + * Checks whether a string is a valid Java class name + * or a valid foreign type key. + * @param typeKey The string to be checked for validity + * @return Whether the string is a valid type keu + */ + public static boolean isValidTypeKey(String typeKey) { + return typeKey != null && ( + FOREIGN_TYPE_KEY_PATTERN.matcher(typeKey).matches() + || isValidJavaClassName(typeKey) + ); + } + + /** + * Checks whether a type key is a Java type key or a foreign + * key. + * @param type The type key to check + * @return Whether the string is a foreign key. If false, then it is a Java type key + * @throws IllegalArgumentException if the string is an invalid type key + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isForeignTypeKey(String type) { + if (!isValidTypeKey(type)) + throw new IllegalArgumentException("Invalid type key: " + type); + return FOREIGN_TYPE_KEY_PATTERN.matcher(type).matches(); + } + + /** + * Gets the language id of a foreign type key. The language ID + * is the first part of the foreign type key, before the {@link #LANGUAGE_ID_SEPARATOR}, + * + * @param typeKey The foreign type key to split + * @return The language ID of the foreign key + * @throws IllegalArgumentException if the string is not a foreign type key + */ + public static String getLanguageId(String typeKey) { + if(!isForeignTypeKey(typeKey)) + throw new IllegalArgumentException("Type key " + typeKey + " is not a foreign key"); + Matcher matcher = FOREIGN_TYPE_KEY_PATTERN.matcher(typeKey); + if (matcher.matches()) + return matcher.group(1); + throw new IllegalStateException("Invalid type key: " + typeKey); + } + + + /** + * Gets the language-specific type key from a foreign type key. + * The language-specific type key is the second part of the foreign type key. + * + * @param typeKey The foreign type key to split + * @return The language-specific type key contained in the foreign type key + * @throws IllegalArgumentException if the string is not a foreign type key + */ + public static String getLanguageSpecificTypeKey(String typeKey) { + if(!isForeignTypeKey(typeKey)) + throw new IllegalArgumentException("Type key " + typeKey + " is not a foreign key"); + Matcher matcher = FOREIGN_TYPE_KEY_PATTERN.matcher(typeKey); + if (matcher.matches()) + return matcher.group(2); + throw new IllegalStateException("Invalid type key: " + typeKey); + } + + public static void main(String[] args) { + String foreignTypeKey = "py:exampleService"; + System.out.println("isValid: " + isValidTypeKey(foreignTypeKey)); + System.out.println("isValidJava: " + isValidJavaClassName(foreignTypeKey)); + System.out.println("isForeign: " + isForeignTypeKey(foreignTypeKey)); + System.out.println("languageId: " + getLanguageId(foreignTypeKey)); + System.out.println("languageSpecificTypeKey: " + getLanguageSpecificTypeKey(foreignTypeKey)); + + String invalidKey = "^abcde"; + + System.out.println("isValid (no): " + isValidTypeKey(invalidKey)); + } + +} diff --git a/src/main/java/org/myrobotlab/codec/MethodCache.java b/src/main/java/org/myrobotlab/codec/MethodCache.java deleted file mode 100644 index 7f2760010b..0000000000 --- a/src/main/java/org/myrobotlab/codec/MethodCache.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.myrobotlab.codec; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.TreeMap; - -@Deprecated /* use new framework MethodCache */ -public class MethodCache { - - static final private HashMap cache = new HashMap(); - - static final public String getSignature(Class clazz, String methodName, int ordinal) { - return String.format("%s/%s-%d", clazz.getSimpleName(), methodName, ordinal); - } - - static final public Class[] getCandidateOnOrdinalSignature(Class clazz, String methodName, int ordinal) throws NoSuchMethodException { - String signature = getSignature(clazz, methodName, ordinal); - if (cache.containsKey(signature)) { - Method m = cache.get(signature); - return m.getParameterTypes(); - } else { - TreeMap methodScore = new TreeMap(); - // changed to getMethods to support inheritance - // if failure - overloading funny re-implementing a vTable in c++ - // Method[] methods = clazz.getDeclaredMethods(); - Method[] methods = clazz.getMethods(); - for (Method method : methods) { - - // FIXME - future Many to one Map - if incoming data can "hint" would be - // an optimization - if (methodName.equals(method.getName())) { - // name matches - lets do more checking - Class[] pTypes = method.getParameterTypes(); - int score = 0; - if (ordinal == pTypes.length) { - // param length matches - boolean interfaceInParamList = false; - for (int i = 0; i < pTypes.length; ++i) { - // we don't support interfaces - // because what will we decode too ? - // we just can't ! :) - Class type = pTypes[i]; - - /* - * BAD ASSUMPTION - SOME CODECs have a default class they - * serialize from for List Map HashSet etc.. - */ - /* - * if (type.isInterface()) { interfaceInParamList = true; break; } - */ - - if (type.isPrimitive() || type.equals(String.class)) { - ++score; - } - - } - - if (!interfaceInParamList) { - // rank / score method - methodScore.put(score, method); - } - } - } - } // we checked all methods - - if (methodScore.size() > 0) { - return methodScore.get(methodScore.lastKey()).getParameterTypes(); - } else { - throw new NoSuchMethodException(String.format("could not find %s.%s(ordinal %d) in declared methods", clazz.getSimpleName(), methodName, ordinal)); - } - } - - } - - final public static void cache(Class clazz, Method method) { - cache.put(getSignature(clazz, method.getName(), method.getParameterTypes().length), method); - } - -} diff --git a/src/main/java/org/myrobotlab/codec/PolymorphicSerializer.java b/src/main/java/org/myrobotlab/codec/PolymorphicSerializer.java deleted file mode 100644 index a4d8342b52..0000000000 --- a/src/main/java/org/myrobotlab/codec/PolymorphicSerializer.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.myrobotlab.codec; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.module.noctordeser.NoCtorDeserModule; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.myrobotlab.codec.json.GsonPolymorphicTypeAdapterFactory; -import org.myrobotlab.codec.json.JacksonPolymorphicModule; -import org.myrobotlab.framework.Message; -import org.myrobotlab.framework.Registration; - -import java.io.Serializable; - - -public class PolymorphicSerializer { - - public static void main(String[] args) throws JsonProcessingException { - //GSON support still works but is kinda wonky - Gson gson = new GsonBuilder().registerTypeAdapterFactory(new GsonPolymorphicTypeAdapterFactory()).create(); - String msgString = gson.toJson( - Message.createMessage("runtime", "runtime", "getId", - new Registration("obsidian", "runtime", "Runtime") - ) - ); - //Have to use `Serializable.class` because `Object.class` uses the default treemap deserializer, no way to override - //So it technically works, but is a little odd to use - Message msgFromString = (Message) gson.fromJson(msgString, Serializable.class); - //Notice that the message data is not preserved, because it is an Object type in Message - //and GSON does not dispatch to custom de/serializers when the type is Object. - System.out.println(msgFromString); - - //Jackson setup - ObjectMapper mapper = new ObjectMapper(); - - //This allows Jackson to work just like GSON when no default constructor is available - mapper.registerModule(new NoCtorDeserModule()); - - //Actually add our polymorphic support - mapper.registerModule(JacksonPolymorphicModule.getPolymorphicModule()); - - //Disables Jackson's automatic property detection - mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); - mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); - mapper.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.ANY); - - //All ready to use now - - msgString = mapper.writeValueAsString( - Message.createMessage("runtime", "runtime", "getId", - new Registration("obsidian", "runtime", "Runtime") - ) - ); - - //Can use Object.class just fine, so generic objects are deserialized correctly - //since type erasure makes everything look like an Object - //Notably, Message works just fine even though it stores an Object array - Message msg = (Message) mapper.readValue(msgString, Object.class); - System.out.println(msg); - - System.out.println(new ObjectMapper().readValue("{\"help\": 10}", Object.class).getClass()); - } -} diff --git a/src/main/java/org/myrobotlab/codec/json/GsonPolymorphicTypeAdapterFactory.java b/src/main/java/org/myrobotlab/codec/json/GsonPolymorphicTypeAdapterFactory.java deleted file mode 100644 index efac1fe8ba..0000000000 --- a/src/main/java/org/myrobotlab/codec/json/GsonPolymorphicTypeAdapterFactory.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.myrobotlab.codec.json; - -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import org.myrobotlab.codec.CodecUtils; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; - -/** - * A {@link TypeAdapterFactory} that enables polymorphic operation. - * The serialization adapter adds a field with the name taken from - * {@link CodecUtils#CLASS_META_KEY} and a value equal - * to the object's fully qualified class name. - * - * The deserialization adapter checks if the JSON has - * {@link CodecUtils#CLASS_META_KEY}, and if so it will use - * the value as the target type. - * - * @author AutonomicPerfectionist - */ -public class GsonPolymorphicTypeAdapterFactory implements TypeAdapterFactory { - - /** - * The TypeAdapter used to create JsonElements - */ - protected TypeAdapter elementAdapter; - protected TypeAdapterFactory taf; - - @SuppressWarnings("unchecked") - public TypeAdapter create(Gson gson, TypeToken type) { - if(!Object.class.isAssignableFrom(type.getRawType()) || String.class.isAssignableFrom(type.getRawType()) - || CodecUtils.WRAPPER_TYPES.contains(type.getRawType()) || Object[].class.isAssignableFrom(type.getRawType()) - || Collection.class.isAssignableFrom(type.getRawType()) || Map.class.isAssignableFrom(type.getRawType())) - return null; - this.taf=this; - TypeAdapter delegate = (TypeAdapter) gson.getDelegateAdapter(this, type); - elementAdapter = gson.getAdapter(JsonElement.class); - TypeAdapter result = (TypeAdapter) new PolymorphicTypeAdapter(type, delegate, gson); - return result.nullSafe(); - - } - - /** - * A type adapter to perform polymorphic deserialization - * and serialization operations. Should only be created with - * {@link GsonPolymorphicTypeAdapterFactory#create(Gson, TypeToken)}. - * - * @author AutonomicPerfectionist - */ - protected class PolymorphicTypeAdapter extends TypeAdapter { - - protected Gson gson; - protected TypeToken type; - protected TypeAdapter delegate; - - public PolymorphicTypeAdapter(TypeToken type, TypeAdapter delegate, Gson gson) { - this.type = type; - this.delegate = delegate; - this.gson = gson; - } - - @Override - public void write(JsonWriter out, Object value) throws IOException { - if(value != null) { - try { - JsonElement element = delegate.toJsonTree(value); - if(element.isJsonObject()) { - JsonObject object = delegate.toJsonTree(value).getAsJsonObject(); - object.addProperty(CodecUtils.CLASS_META_KEY, value.getClass().getName()); - elementAdapter.write(out, object); - } else - delegate.write(out, value); - - } catch (IllegalArgumentException iae) { - delegate.write(out, value); - } - } - else { - delegate.write(out, null); - } - } - - @Override - public Object read(JsonReader in) throws IOException { - JsonElement element = elementAdapter.read(in); - if (element.isJsonObject()) { - JsonObject object = element.getAsJsonObject(); - if (object.has(CodecUtils.CLASS_META_KEY)) { - String className=object.get(CodecUtils.CLASS_META_KEY).getAsString(); - try { - Class clz = Class.forName(className); - if(type.getRawType().isAssignableFrom(clz)) { - TypeAdapter adapter = gson.getDelegateAdapter(taf, TypeToken.get(clz)); - return adapter.fromJsonTree(element); - } - } - catch (Exception ignored) { - } - } - } - - //If element is not a json object, doesn't have the key, - //an exception occurs, or requested class is not a superclass - //of embedded class, will fallthrough to here - return delegate.fromJsonTree(element); - } - } -} diff --git a/src/main/java/org/myrobotlab/codec/json/ProxySerializer.java b/src/main/java/org/myrobotlab/codec/json/ProxySerializer.java new file mode 100644 index 0000000000..8aa383e3ac --- /dev/null +++ b/src/main/java/org/myrobotlab/codec/json/ProxySerializer.java @@ -0,0 +1,51 @@ +package org.myrobotlab.codec.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.myrobotlab.framework.Service; +import org.myrobotlab.framework.TimeoutException; +import org.myrobotlab.framework.interfaces.ServiceInterface; +import org.myrobotlab.service.Runtime; + +import java.io.IOException; +import java.lang.reflect.Proxy; + +/** + * This class serializes proxies made with {@code java.lang.reflect.Proxy}. + * Such proxies are no longer in use, so this class may be modified to + * to support ByteBuddy proxies in the future. + * + * @author AutonomicPerfectionist + */ +public class ProxySerializer extends StdSerializer { + + public ProxySerializer() { + this(Proxy.class); + } + + public ProxySerializer(Class t) { + super(t); + } + + @Override + public void serialize( + Proxy value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + + if (value instanceof ServiceInterface) { + ServiceInterface si = (ServiceInterface) value; + jgen.writeStartObject(); + jgen.writeStringField("name", si.getName()); + jgen.writeStringField("type", si.getTypeKey()); + jgen.writeStringField("id", si.getId()); + try { + jgen.writeStringField("typeKey", (String) Runtime.get().sendBlocking(si.getName(), "getTypeKey")); + } catch (InterruptedException | TimeoutException e) { + throw new RuntimeException(e); + } + jgen.writeEndObject(); + } + } +} diff --git a/src/main/java/org/myrobotlab/cv/CvData.java b/src/main/java/org/myrobotlab/cv/CVData.java similarity index 88% rename from src/main/java/org/myrobotlab/cv/CvData.java rename to src/main/java/org/myrobotlab/cv/CVData.java index 67e75ae479..3db4d9ad96 100644 --- a/src/main/java/org/myrobotlab/cv/CvData.java +++ b/src/main/java/org/myrobotlab/cv/CVData.java @@ -13,7 +13,7 @@ * @author GroG * */ -public abstract class CvData implements Serializable { +public abstract class CVData implements Serializable { private static final long serialVersionUID = 1L; public static final String POINT_CLOUDS = "point.cloud"; diff --git a/src/main/java/org/myrobotlab/cv/CvFilter.java b/src/main/java/org/myrobotlab/cv/CVFilter.java similarity index 80% rename from src/main/java/org/myrobotlab/cv/CvFilter.java rename to src/main/java/org/myrobotlab/cv/CVFilter.java index cb144d4091..5d68375789 100644 --- a/src/main/java/org/myrobotlab/cv/CvFilter.java +++ b/src/main/java/org/myrobotlab/cv/CVFilter.java @@ -1,6 +1,6 @@ package org.myrobotlab.cv; -public interface CvFilter { +public interface CVFilter { public void enable(); diff --git a/src/main/java/org/myrobotlab/service/interfaces/ComputerVision.java b/src/main/java/org/myrobotlab/cv/ComputerVision.java similarity index 72% rename from src/main/java/org/myrobotlab/service/interfaces/ComputerVision.java rename to src/main/java/org/myrobotlab/cv/ComputerVision.java index cee01671db..b6bf9bbc2e 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/ComputerVision.java +++ b/src/main/java/org/myrobotlab/cv/ComputerVision.java @@ -1,6 +1,5 @@ -package org.myrobotlab.service.interfaces; +package org.myrobotlab.cv; -import org.myrobotlab.cv.CvFilter; import org.myrobotlab.framework.interfaces.NameProvider; /** @@ -14,7 +13,7 @@ public interface ComputerVision extends NameProvider { void capture(); - CvFilter addFilter(String name, String filterType); + CVFilter addFilter(String name, String filterType); void removeFilter(String name); diff --git a/src/main/java/org/myrobotlab/deeplearning4j/SolrDataSetIterator.java b/src/main/java/org/myrobotlab/deeplearning4j/SolrDataSetIterator.java index 406d8481e0..3d77e4d9ed 100755 --- a/src/main/java/org/myrobotlab/deeplearning4j/SolrDataSetIterator.java +++ b/src/main/java/org/myrobotlab/deeplearning4j/SolrDataSetIterator.java @@ -16,6 +16,7 @@ import org.apache.solr.common.params.FacetParams; import org.bytedeco.opencv.opencv_core.IplImage; import org.datavec.image.loader.NativeImageLoader; +import org.myrobotlab.image.Util; import org.myrobotlab.opencv.CloseableFrameConverter; import org.myrobotlab.service.Solr; import org.nd4j.linalg.api.ndarray.INDArray; @@ -176,7 +177,7 @@ private DataSet docToDataSet(SolrDocument trainingDoc) { byte[] bytes = (byte[]) trainingDoc.getFirstValue("bytes"); NativeImageLoader loader = new NativeImageLoader(height, width, channels); try { - IplImage iplImage = solr.bytesToImage(bytes); + IplImage iplImage = Util.bytesToImage(bytes); // solr.show(iplImage, label + " " + trainingDoc.getFirstValue("id")); // TODO: just get the buffered image directly. diff --git a/src/main/java/org/myrobotlab/document/Document.java b/src/main/java/org/myrobotlab/document/Document.java index fbdf020c8d..02332691e8 100644 --- a/src/main/java/org/myrobotlab/document/Document.java +++ b/src/main/java/org/myrobotlab/document/Document.java @@ -35,6 +35,9 @@ public ArrayList getField(String fieldName) { } public void setField(String fieldName, ArrayList value) { + if (fieldName == null) { + return; + } if (value == null) { data.remove(fieldName); } else { @@ -43,6 +46,9 @@ public void setField(String fieldName, ArrayList value) { } public void setField(String fieldName, Object value) { + if (fieldName == null) { + return; + } // set field overwrites existing values in the field. if (value == null) { data.remove(fieldName); @@ -77,6 +83,9 @@ public void renameField(String oldField, String newField) { } public void addToField(String fieldName, Object value) { + if (fieldName == null) { + return; + } if (data.containsKey(fieldName) && (data.get(fieldName) != null)) { data.get(fieldName).add(value); } else { diff --git a/src/main/java/org/myrobotlab/document/connector/AbstractConnector.java b/src/main/java/org/myrobotlab/document/connector/AbstractConnector.java index 46c96637b7..d49982478d 100644 --- a/src/main/java/org/myrobotlab/document/connector/AbstractConnector.java +++ b/src/main/java/org/myrobotlab/document/connector/AbstractConnector.java @@ -7,8 +7,8 @@ import org.myrobotlab.document.Document; import org.myrobotlab.document.transformer.ConnectorConfig; import org.myrobotlab.framework.Service; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.DocumentConnector; -import org.myrobotlab.service.interfaces.DocumentListener; import org.myrobotlab.service.interfaces.DocumentPublisher; /** @@ -17,7 +17,7 @@ * service. * */ -public abstract class AbstractConnector extends Service implements DocumentPublisher, DocumentConnector { +public abstract class AbstractConnector extends Service implements DocumentPublisher, DocumentConnector { private static final long serialVersionUID = 1L; protected ConnectorState state = ConnectorState.STOPPED; @@ -126,13 +126,6 @@ public List publishDocuments(List batch) { return batch; } - @Override - public void addDocumentListener(DocumentListener listener) { - addListener("publishDocument", listener.getName(), "onDocument"); - addListener("publishDocuments", listener.getName(), "onDocuments"); - addListener("publishFlush", listener.getName(), "onFlush"); - } - @Override public ConnectorState getConnectorState() { return state; diff --git a/src/main/java/org/myrobotlab/document/transformer/Configuration.java b/src/main/java/org/myrobotlab/document/transformer/Configuration.java index 69d273c448..366266093e 100644 --- a/src/main/java/org/myrobotlab/document/transformer/Configuration.java +++ b/src/main/java/org/myrobotlab/document/transformer/Configuration.java @@ -1,5 +1,6 @@ package org.myrobotlab.document.transformer; +import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -7,7 +8,7 @@ import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.StaxDriver; -public class Configuration { +public abstract class Configuration implements Serializable { // TODO: add a map type. // TODO: push the name/class onto this ? @@ -17,14 +18,13 @@ public class Configuration { // workflow /pipeline config // connector config - protected HashMap config = null; - // private XStream xstream = null; + /** + * + */ + private static final long serialVersionUID = 1L; + public HashMap config = new HashMap(); public Configuration() { - config = new HashMap(); - // figure that we need to be able to serialize / deserialize - // TODO: consider a faster driver / serializer - // xstream = new XStream(new StaxDriver()); } public void setStringParam(String name, String value) { @@ -132,6 +132,12 @@ public List getListParam(String name) { return null; } + public void setMapProperty(String name, Map map) { + // TODO type safety?! + config.put(name, map); + return; + } + public Map getMapProperty(String name) { // TODO type safety?! return (Map) config.get(name); diff --git a/src/main/java/org/myrobotlab/document/transformer/StageConfiguration.java b/src/main/java/org/myrobotlab/document/transformer/StageConfiguration.java index 0359931814..6243f41c88 100644 --- a/src/main/java/org/myrobotlab/document/transformer/StageConfiguration.java +++ b/src/main/java/org/myrobotlab/document/transformer/StageConfiguration.java @@ -4,20 +4,16 @@ public class StageConfiguration extends Configuration { - // private HashMap config = null; - private String stageName = "defaultStage"; private String stageClass = "org.myrobotlab.document.transformer.AbstractStage"; public StageConfiguration(String stageName, String stageClass) { - config = new HashMap(); this.stageName = stageName; this.stageClass = stageClass; } public StageConfiguration() { // depricate this constructor? - config = new HashMap(); } @Override diff --git a/src/main/java/org/myrobotlab/document/transformer/TextExtractor.java b/src/main/java/org/myrobotlab/document/transformer/TextExtractor.java index b030f2b9c8..7427ac9ea6 100644 --- a/src/main/java/org/myrobotlab/document/transformer/TextExtractor.java +++ b/src/main/java/org/myrobotlab/document/transformer/TextExtractor.java @@ -4,10 +4,12 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.StringWriter; import java.util.List; import org.apache.tika.exception.TikaException; +import org.apache.tika.exception.ZeroByteFileException; import org.apache.tika.metadata.Metadata; import org.apache.tika.parser.AutoDetectParser; import org.apache.tika.parser.ParseContext; @@ -16,7 +18,6 @@ import org.myrobotlab.document.Document; import org.myrobotlab.logging.LoggerFactory; import org.slf4j.Logger; -import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; /** @@ -72,39 +73,40 @@ public List processDocument(Document doc) { continue; } - FileInputStream binaryData = null; + if (f.length() == 0) { + // This is a zero byte file, there's no data to try to extract... + log.info("zero byte file {}", f.getAbsolutePath()); + continue; + } + + InputStream binaryData = null; try { binaryData = new FileInputStream(f); - } catch (FileNotFoundException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); + } catch (FileNotFoundException e) { // This should never happen. + log.warn("Document {} not found.", doc.getId(), e); continue; } - // InputStream binaryData = null; Metadata metadata = new Metadata(); StringWriter textData = new StringWriter(); - ContentHandler bch = new BodyContentHandler(textData); + BodyContentHandler bch = new BodyContentHandler(textData); try { parser.parse(binaryData, bch, metadata, parseCtx); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (SAXException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (TikaException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + } catch (TikaException|IOException|SAXException e) { + log.warn("Error processing {} :", doc.getId(), e); + // Accumulate any errors on the document. + doc.addToField("error", e); } doc.addToField(textField, textData.toString()); for (String name : metadata.names()) { // clean the field name first. String cleanName = cleanFieldName(name); - for (String value : metadata.getValues(name)) { - doc.addToField(cleanName, value); + if (cleanName != null) { + for (String value : metadata.getValues(name)) { + doc.addToField(cleanName, value); + } } } } diff --git a/src/main/java/org/myrobotlab/document/transformer/WorkflowConfiguration.java b/src/main/java/org/myrobotlab/document/transformer/WorkflowConfiguration.java index 84f6c8dc21..1a1263a1f1 100644 --- a/src/main/java/org/myrobotlab/document/transformer/WorkflowConfiguration.java +++ b/src/main/java/org/myrobotlab/document/transformer/WorkflowConfiguration.java @@ -7,11 +7,15 @@ public class WorkflowConfiguration extends Configuration { - ArrayList stages; + public ArrayList stages; private String name = "default"; private int numWorkerThreads = 1; private int queueLength = 50; - + + public WorkflowConfiguration() { + // needed for yaml config serialization + } + public WorkflowConfiguration(String name) { this.name = name; stages = new ArrayList(); diff --git a/src/main/java/org/myrobotlab/document/workflow/Workflow.java b/src/main/java/org/myrobotlab/document/workflow/Workflow.java index f7a93411c7..c6ac02ff4d 100644 --- a/src/main/java/org/myrobotlab/document/workflow/Workflow.java +++ b/src/main/java/org/myrobotlab/document/workflow/Workflow.java @@ -5,6 +5,7 @@ import org.myrobotlab.document.Document; import org.myrobotlab.document.transformer.WorkflowConfiguration; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.DocumentPipeline; import org.slf4j.Logger; /** @@ -41,18 +42,18 @@ public Workflow(WorkflowConfiguration workflowConfig) throws ClassNotFoundExcept } // initialize the workflow - public void initialize() { + public void initialize(DocumentPipeline pipeline) { workers = new WorkflowWorker[numWorkerThreads]; for (int i = 0; i < numWorkerThreads; i++) { - initializeWorkerThread(i); + initializeWorkerThread(i, pipeline); } } // init the worker threads - private void initializeWorkerThread(int threadNum) { + private void initializeWorkerThread(int threadNum, DocumentPipeline pipeline) { WorkflowWorker worker = null; try { - worker = new WorkflowWorker(workflowConfig, queue, Integer.toString(threadNum)); + worker = new WorkflowWorker(workflowConfig, queue, Integer.toString(threadNum), pipeline); } catch (ClassNotFoundException e) { // TODO: better handling? log.warn("Error starting the worker thread. {}", e.getLocalizedMessage()); diff --git a/src/main/java/org/myrobotlab/document/workflow/WorkflowServer.java b/src/main/java/org/myrobotlab/document/workflow/WorkflowServer.java index d71e2d0005..c3a0ad05d3 100644 --- a/src/main/java/org/myrobotlab/document/workflow/WorkflowServer.java +++ b/src/main/java/org/myrobotlab/document/workflow/WorkflowServer.java @@ -3,22 +3,25 @@ import java.util.HashMap; import org.myrobotlab.document.transformer.WorkflowConfiguration; +import org.myrobotlab.service.DocumentPipeline; public class WorkflowServer { private static WorkflowServer instance = null; private HashMap workflowMap; + private final DocumentPipeline pipeline; // singleton, the constructor is private. - private WorkflowServer() { + private WorkflowServer(DocumentPipeline pipeline) { workflowMap = new HashMap(); + this.pipeline = pipeline; } // This is a singleton also - public static WorkflowServer getInstance() { + public static WorkflowServer getInstance(DocumentPipeline pipeline) { if (instance == null) { - instance = new WorkflowServer(); + instance = new WorkflowServer(pipeline); return instance; } else { return instance; @@ -31,7 +34,7 @@ public static WorkflowServer getInstance() { public void addWorkflow(WorkflowConfiguration config) throws ClassNotFoundException { Workflow w = new Workflow(config); - w.initialize(); + w.initialize(pipeline); workflowMap.put(w.getName(), w); } @@ -47,10 +50,11 @@ public void processMessage(WorkflowMessage msg) throws InterruptedException { } public void flush(String workflow) { - // TODO Auto-generated method stub + // flush the workflow/pipeline Workflow w = workflowMap.get(workflow); w.flush(); - + // publish that we have flushed (a workflow, pass the flush down the line?) + pipeline.invoke("publishFlush"); } public String[] listWorkflows() { @@ -59,5 +63,4 @@ public String[] listWorkflows() { workflowMap.keySet().toArray(ws); return ws; } - } diff --git a/src/main/java/org/myrobotlab/document/workflow/WorkflowWorker.java b/src/main/java/org/myrobotlab/document/workflow/WorkflowWorker.java index 6ccc170402..fc7a3e48cd 100644 --- a/src/main/java/org/myrobotlab/document/workflow/WorkflowWorker.java +++ b/src/main/java/org/myrobotlab/document/workflow/WorkflowWorker.java @@ -10,6 +10,7 @@ import org.myrobotlab.document.transformer.StageConfiguration; import org.myrobotlab.document.transformer.WorkflowConfiguration; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.DocumentPipeline; import org.slf4j.Logger; /** @@ -24,10 +25,12 @@ public class WorkflowWorker extends Thread { private final LinkedBlockingQueue queue; - WorkflowWorker(WorkflowConfiguration workflowConfig, LinkedBlockingQueue queue, String workerId) throws ClassNotFoundException { + private final DocumentPipeline pipeline; + WorkflowWorker(WorkflowConfiguration workflowConfig, LinkedBlockingQueue queue, String workerId, DocumentPipeline pipeline) throws ClassNotFoundException { // set the thread name this.setName("WorkflowWorker-" + workflowConfig.getName() + "-" + workerId); this.queue = queue; + this.pipeline = pipeline; stages = new ArrayList(); for (StageConfiguration stageConf : workflowConfig.getStages()) { String stageClass = stageConf.getStageClass().trim(); @@ -63,6 +66,7 @@ public void run() { processing = true; // process from the start of the workflow processDocumentInternal(doc, 0); + pipeline.invoke("publishDocument", doc); processing = false; } } catch (Exception e) { diff --git a/src/main/java/org/myrobotlab/framework/CmdConfig.java b/src/main/java/org/myrobotlab/framework/CmdConfig.java index 5ba9870ec7..f807aaeb81 100644 --- a/src/main/java/org/myrobotlab/framework/CmdConfig.java +++ b/src/main/java/org/myrobotlab/framework/CmdConfig.java @@ -27,6 +27,6 @@ public class CmdConfig { /** * if this startup is enabled */ - public boolean enable = true; + public boolean enable = false; } diff --git a/src/main/java/org/myrobotlab/framework/MethodCache.java b/src/main/java/org/myrobotlab/framework/MethodCache.java index f27786fa6f..9732031ff0 100644 --- a/src/main/java/org/myrobotlab/framework/MethodCache.java +++ b/src/main/java/org/myrobotlab/framework/MethodCache.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.StringUtils; import org.myrobotlab.codec.CodecUtils; +import org.myrobotlab.codec.json.JsonDeserializationException; import org.myrobotlab.logging.LoggerFactory; import org.slf4j.Logger; @@ -502,6 +503,7 @@ private String getMethodOrdinalKey(String fullType, String methodName, int param public List getOrdinalMethods(Class object, String methodName, int parameterSize) { if (object == null) { log.error("getOrdinalMethods on a null object "); + return null; } String objectKey = object.getTypeName(); @@ -575,20 +577,17 @@ public List getRemoteOrdinalMethods(Class object, String methodN Class[] paramTypes = methodEntry.getParameterTypes(); try { for (int i = 0; i < encodedParams.length; ++i) { - if (CodecUtils.JSON_DEFAULT_OBJECT_TYPE.equals(encodedParams[i].getClass())) { - // specific gson implementation - // rather than double encode everything - i have chosen - // to re-encode objects back to string since gson will decode them - // all ot linked tree maps - if the json decoder changes from gson - // this will probably need to change too - encodedParams[i] = CodecUtils.toJson(encodedParams[i]); - } - params[i] = CodecUtils.fromJson((String) encodedParams[i], paramTypes[i]); +// try { + params[i] = CodecUtils.fromJson((String) encodedParams[i], paramTypes[i]); +// } catch(JsonDeserializationException e) { +// log.info("could not decode threw {}.{}( ordinal[{}] {} {})- assuming String - missing quotes?", clazz.getSimpleName(), methodName, i, paramTypes[i].getSimpleName(), encodedParams[i]); +// // load raw string on +// params[i] = encodedParams[i]; +// } } // successfully decoded params return params; } catch (Exception e) { - log.info("getDecodedParameters threw clazz {} method {} params {} Message: {}", clazz, methodName, encodedParams.length, e.getMessage()); } } diff --git a/src/main/java/org/myrobotlab/framework/Outbox.java b/src/main/java/org/myrobotlab/framework/Outbox.java index 58ad3b7279..ac1e50b781 100644 --- a/src/main/java/org/myrobotlab/framework/Outbox.java +++ b/src/main/java/org/myrobotlab/framework/Outbox.java @@ -37,13 +37,13 @@ import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.interfaces.MessageListener; -import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.interfaces.Gateway; import org.slf4j.Logger; + /* * Outbox is a message based thread which sends messages based on addListener lists and current * queue status. It is only aware of the Service directory, addListener lists, and operators. @@ -62,18 +62,24 @@ public class Outbox implements Runnable, Serializable { static public final String BROADCAST = "BROADCAST"; static public final String PROCESSANDBROADCAST = "PROCESSANDBROADCAST"; - NameProvider myService = null; - LinkedList msgBox = new LinkedList(); + protected String name = null; + private transient LinkedList msgBox = new LinkedList(); private boolean isRunning = false; private boolean blocking = false; int maxQueue = 1024; int initialThreadCount = 1; transient ArrayList outboxThreadPool = new ArrayList(); + protected Map filters = new HashMap<>(); + + public interface FilterInterface { + public boolean filter(Message msg); + } + /** * pub/sub listeners - HashMap < {topic}, List {listeners} > */ - public Map> notifyList = new HashMap>(); + protected Map> notifyList = new HashMap>(); List listeners = new ArrayList(); @@ -87,8 +93,8 @@ public void setAutoClean(boolean autoClean) { this.autoClean = autoClean; } - public Outbox(NameProvider myService) { - this.myService = myService; + public Outbox(String myService) { + this.name = myService; } public Set getAttached(String publishingPoint) { @@ -99,7 +105,7 @@ public Set getAttached(String publishingPoint, boolean localOnly) { Set unique = new TreeSet<>(); for (List subcribers : notifyList.values()) { for (MRLListener listener : subcribers) { - if (localOnly && listener.callbackName.contains("@")) { + if (localOnly && !CodecUtils.isLocal(listener.callbackName)) { continue; } if (publishingPoint == null) { @@ -135,7 +141,7 @@ public void add(Message msg) { if (msgBox.size() > maxQueue) { // log.warn("{} outbox BUFFER OVERRUN size {} Dropping message to // {}.{}", myService.getName(), msgBox.size(), msg.name, msg.method); - log.warn("{} outbox BUFFER OVERRUN size {} Dropping message to {}", myService.getName(), msgBox.size(), msg); + log.warn("{} outbox BUFFER OVERRUN size {} Dropping message to {}", name, msgBox.size(), msg); } msgBox.addFirst(msg); @@ -209,7 +215,10 @@ public void run() { for (MRLListener listener : subList) { msg.setName(listener.callbackName); msg.method = listener.callbackMethod; - send(msg); + + if (!isFiltered(msg)) { + send(msg); + } // must make new for internal queues // otherwise you'll change the name on @@ -222,9 +231,25 @@ public void run() { } continue; } - } // while (isRunning) } + + public FilterInterface addFilter(String name, String method, FilterInterface filter) { + return filters.put(String.format("%s.%s", CodecUtils.getFullName(name), method), filter); + } + + public FilterInterface removeFilter(String name, String method) { + return filters.remove(String.format("%s.%s", CodecUtils.getFullName(name), method)); + } + + public boolean isFiltered(Message msg) { + String fullname = CodecUtils.getFullName(msg.name); + if (filters.size() == 0 || !filters.containsKey(String.format("%s.%s", fullname, msg.method))) { + return false; + } else { + return filters.get(String.format("%s.%s", fullname, msg.method)).filter(msg); + } + } public int size() { return msgBox.size(); @@ -232,7 +257,7 @@ public int size() { public void start() { for (int i = outboxThreadPool.size(); i < initialThreadCount; ++i) { - Thread t = new Thread(this, myService.getName() + "_outbox_" + i); + Thread t = new Thread(this, name + "_outbox_" + i); outboxThreadPool.add(t); t.start(); } @@ -336,7 +361,8 @@ public void reset() { * the name of the listener to detach * */ - synchronized public void detach(String name) { + synchronized public void detach(String service) { + String name = CodecUtils.getFullName(service); for (String topic : notifyList.keySet()) { List subscribers = notifyList.get(topic); ArrayList smallerList = new ArrayList<>(); @@ -349,4 +375,10 @@ synchronized public void detach(String name) { } } + public Map> getNotifyList() { + return notifyList; + } + + + } diff --git a/src/main/java/org/myrobotlab/framework/ProxyFactory.java b/src/main/java/org/myrobotlab/framework/ProxyFactory.java new file mode 100644 index 0000000000..c8782a20e1 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/ProxyFactory.java @@ -0,0 +1,77 @@ +package org.myrobotlab.framework; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import org.myrobotlab.framework.interfaces.ServiceInterface; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.stream.Collectors; + +import static net.bytebuddy.matcher.ElementMatchers.any; + +/** + * ProxyFactory takes a service description via {@link Registration} + * and uses ByteBuddy to generate + * a new class and instance for it, delegating all method calls to a new instance + * of {@link ProxyInterceptor}. The registration must contain at least the + * fully-qualified name of {@link ServiceInterface} in its {@link Registration#interfaces} + * list. If this name is not present, an {@link IllegalArgumentException} is thrown. + * + * @author AutonomicPerfectionist + */ +public class ProxyFactory { + + /** + * Creates a proxy class and instantiates it using the given registration. + * If the registration's {@link Registration#interfaces} list does not contain + * the fully-qualified name of {@link ServiceInterface}, an {@link IllegalArgumentException} + * is thrown. The generated proxy uses the name, id, and interfaces present in + * the registration to create the new service. The proxy + * delegates to a new instance of {@link ProxyInterceptor}. + * + * @param registration The information required to generate a new proxy + * @return A newly-instantiated proxy + * @throws IllegalArgumentException if registration's interfaces list + * does not contain ServiceInterface + */ + public static ServiceInterface createProxyService(Registration registration) { + // TODO add caching support + if (!registration.interfaces.contains(ServiceInterface.class.getName())) { + throw new IllegalArgumentException( + "Registration must list at least ServiceInterface in the interfaces list." + ); + } + ByteBuddy buddy = new ByteBuddy(); + DynamicType.Builder builder = buddy.subclass(Object.class); + List> interfaceClasses = registration.interfaces.stream().map(i -> { + try { + return Class.forName(i); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load interface " + i + " for registration " + registration, e); + } + }).collect(Collectors.toList()); + + builder = builder.implement(interfaceClasses) + .method(any()) + .intercept(MethodDelegation + .withDefaultConfiguration() + .to(new ProxyInterceptor(registration.name, registration.id))); + + + Class proxyClass = builder.make() + .load(ProxyFactory.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + try { + // We never defined any constructors so the default no-args is available + return (ServiceInterface) proxyClass.getConstructors()[0].newInstance(); + + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + // Really shouldn't happen since we have full control over the + // newly-generated class + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/myrobotlab/framework/ProxyInterceptor.java b/src/main/java/org/myrobotlab/framework/ProxyInterceptor.java new file mode 100644 index 0000000000..f56167d7a8 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/ProxyInterceptor.java @@ -0,0 +1,53 @@ +package org.myrobotlab.framework; + +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.Runtime; +import org.slf4j.Logger; + +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * This class is used internally to intercept + * all method calls being made to a generated ByteBuddy + * proxy. The interceptor forwards all calls to a remote + * instance identified by the id and name via + * {@link Runtime#sendBlocking(String, Integer, String, Object...)}. + * Thus, the service identified by id and name must exist and + * must be reachable, otherwise the interception + * will result in a {@link TimeoutException}. + * + * @author AutonomicPerfectionist + */ +public class ProxyInterceptor { + + protected static Logger log = LoggerFactory.getLogger(ProxyInterceptor.class); + + public static volatile int timeout = 3000; + private final String name; + + private final String id; + + public ProxyInterceptor(String name, String id) { + this.name = name; + this.id = id; + } + + + @RuntimeType + public Object intercept(@Origin Method method, @AllArguments Object... args) throws InterruptedException, TimeoutException { + log.debug( + "Executing proxy method {}@{}.{}({})", + name, + id, + method, + ((args == null) ? "" : Arrays.toString(args)) + ); + // Timeout should be more sophisticated for long blocking methods + return Runtime.getInstance().sendBlocking(name + "@" + id, timeout, method.getName(), + (args != null) ? args : new Object[0]); + } +} diff --git a/src/main/java/org/myrobotlab/framework/ProxyServiceInvocationHandler.java b/src/main/java/org/myrobotlab/framework/ProxyServiceInvocationHandler.java new file mode 100644 index 0000000000..64c688ee01 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/ProxyServiceInvocationHandler.java @@ -0,0 +1,90 @@ +package org.myrobotlab.framework; + +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.Runtime; +import org.slf4j.Logger; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Arrays; + +/** + * This class serves as an alternative interception handler, + * for proxies made with {@code java.lang.reflect.Proxy}. Such a proxy + * is not currently used, it was replaced with ByteBuddy. + * + * @author AutonomicPerfectionist + */ +public class ProxyServiceInvocationHandler implements InvocationHandler { + + protected static Logger log = LoggerFactory.getLogger(ProxyServiceInvocationHandler.class); + + private final String name; + private final String inId; + + public ProxyServiceInvocationHandler(String name, String inId) { + this.name = name; + this.inId = inId; + } + + + /** + * Processes a method invocation on a proxy instance and returns + * the result. This method will be invoked on an invocation handler + * when a method is invoked on a proxy instance that it is + * associated with. + * + * @param proxy the proxy instance that the method was invoked on + * @param method the {@code Method} instance corresponding to + * the interface method invoked on the proxy instance. The declaring + * class of the {@code Method} object will be the interface that + * the method was declared in, which may be a superinterface of the + * proxy interface that the proxy class inherits the method through. + * @param args an array of objects containing the values of the + * arguments passed in the method invocation on the proxy instance, + * or {@code null} if interface method takes no arguments. + * Arguments of primitive types are wrapped in instances of the + * appropriate primitive wrapper class, such as + * {@code java.lang.Integer} or {@code java.lang.Boolean}. + * @return the value to return from the method invocation on the + * proxy instance. If the declared return type of the interface + * method is a primitive type, then the value returned by + * this method must be an instance of the corresponding primitive + * wrapper class; otherwise, it must be a type assignable to the + * declared return type. If the value returned by this method is + * {@code null} and the interface method's return type is + * primitive, then a {@code NullPointerException} will be + * thrown by the method invocation on the proxy instance. If the + * value returned by this method is otherwise not compatible with + * the interface method's declared return type as described above, + * a {@code ClassCastException} will be thrown by the method + * invocation on the proxy instance. + * @throws Throwable the exception to throw from the method + * invocation on the proxy instance. The exception's type must be + * assignable either to any of the exception types declared in the + * {@code throws} clause of the interface method or to the + * unchecked exception types {@code java.lang.RuntimeException} + * or {@code java.lang.Error}. If a checked exception is + * thrown by this method that is not assignable to any of the + * exception types declared in the {@code throws} clause of + * the interface method, then an + * {@link UndeclaredThrowableException} containing the + * exception that was thrown by this method will be thrown by the + * method invocation on the proxy instance. + * @see UndeclaredThrowableException + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + log.debug( + "Executing proxy method {}@{}.{}({})", + name, + inId, + method.getName(), + ((args == null) ? "" : Arrays.toString(args)) + ); + // Timeout should be more sophisticated for long blocking methods + return Runtime.getInstance().sendBlocking(name + "@" + inId, 3000, method.getName(), + (args != null) ? args : new Object[0]); + } +} diff --git a/src/main/java/org/myrobotlab/framework/Registration.java b/src/main/java/org/myrobotlab/framework/Registration.java index b52bbebf3d..4f0b3070dc 100644 --- a/src/main/java/org/myrobotlab/framework/Registration.java +++ b/src/main/java/org/myrobotlab/framework/Registration.java @@ -1,12 +1,15 @@ package org.myrobotlab.framework; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.logging.LoggerFactory; import org.slf4j.Logger; -import java.util.Objects; - /** * * @author GroG @@ -27,6 +30,14 @@ public class Registration { protected String name; protected String typeKey; + /** + * The list of interfaces that a service being registered implements. + * This list must contain the fully qualified names of Java interfaces, + * and is only used for proxy generation. When generating proxies, + * this list must contain at least the fully qualified name of ServiceInterface. + */ + public List interfaces = List.of(); + /** * current serialized state of the service - default encoding is json for all * remote registration @@ -38,18 +49,27 @@ public class Registration { * remote */ transient public ServiceInterface service = null; + + + public Registration(String id, String name, String typeKey) { + this.id = id; + this.name = name; + this.typeKey = typeKey; + } + - public Registration(String id, String name, String typeKey) { + public Registration(String id, String name, String typeKey, ArrayList interfaces) { this.id = id; this.name = name; this.typeKey = typeKey; + this.interfaces = interfaces; } public Registration(ServiceInterface service) { - log.info("creating registration for {}@{} - {}", service.getName(), service.getId(), service.getType()); + log.info("creating registration for {}@{} - {}", service.getName(), service.getId(), service.getTypeKey()); this.id = service.getId(); this.name = service.getName(); - this.typeKey = service.getType(); + this.typeKey = service.getTypeKey(); // when this registration is re-broadcasted to remotes it will use this // serialization to init state this.state = CodecUtils.toJson(service); diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java index 8ffdcc14a2..41e7cdbe85 100644 --- a/src/main/java/org/myrobotlab/framework/Service.java +++ b/src/main/java/org/myrobotlab/framework/Service.java @@ -25,6 +25,7 @@ package org.myrobotlab.framework; +// java or mrl imports only - no dependencies ! import java.io.File; import java.io.IOException; import java.io.PrintWriter; @@ -46,6 +47,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.Timer; @@ -56,6 +58,7 @@ import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.framework.interfaces.Broadcaster; +import org.myrobotlab.framework.interfaces.ConfigurableService; import org.myrobotlab.framework.interfaces.FutureInvoker; import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.framework.interfaces.ServiceInterface; @@ -71,6 +74,7 @@ import org.myrobotlab.service.interfaces.QueueReporter; import org.myrobotlab.service.meta.abstracts.MetaData; import org.myrobotlab.utils.ObjectTypePair; +import org.myrobotlab.string.StringUtil; import org.slf4j.Logger; /** @@ -84,7 +88,7 @@ * messages. * */ -public abstract class Service implements Runnable, Serializable, ServiceInterface, Broadcaster, QueueReporter, FutureInvoker { +public abstract class Service implements Runnable, Serializable, ServiceInterface, Broadcaster, QueueReporter, FutureInvoker, ConfigurableService { // FIXME upgrade to ScheduledExecutorService // http://howtodoinjava.com/2015/03/25/task-scheduling-with-executors-scheduledthreadpoolexecutor-example/ @@ -97,6 +101,12 @@ public abstract class Service implements Runnable, Serializable, ServiceInterfac */ protected MetaData serviceType; + /** + * Config member - configuration of type {ServiceType}Config + * Runtime applys either the default config or a saved config during service creation + */ + protected T config; + private static final long serialVersionUID = 1L; transient public final static Logger log = LoggerFactory.getLogger(Service.class); @@ -126,7 +136,7 @@ public abstract class Service implements Runnable, Serializable, ServiceInterfac /** * full class name used in serialization */ - protected String serviceClass; + protected String typeKey; private boolean isRunning = false; @@ -134,7 +144,7 @@ public abstract class Service implements Runnable, Serializable, ServiceInterfac transient protected Inbox inbox = null; - transient protected Outbox outbox = null; + protected Outbox outbox = null; protected String serviceVersion = null; @@ -143,11 +153,6 @@ public abstract class Service implements Runnable, Serializable, ServiceInterfac */ protected Properties defaultLocalization = null; - /** - * the last config applied to this service - */ - protected ServiceConfig config; - /** * map of keys to localizations - * @@ -186,9 +191,10 @@ public abstract class Service implements Runnable, Serializable, ServiceInterfac /** * This is the map of interfaces - its really "static" information, since its - * a definition. However, since gson will not process statics - we are making + * a definition. However, since serialization will not process statics - we are making * it a member variable */ + // FIXME - this should be a map protected Map interfaceSet; /** @@ -254,12 +260,11 @@ public static Object copyShallowFrom(Object target, Object source) { for (Class sourceClass : ancestry) { - Field fields[] = sourceClass.getDeclaredFields(); - for (int j = 0, m = fields.length; j < m; j++) { + Field[] fields = sourceClass.getDeclaredFields(); + for (Field field : fields) { try { - Field f = fields[j]; - int modifiers = f.getModifiers(); + int modifiers = field.getModifiers(); // if (Modifier.isPublic(mod) // !(Modifier.isPublic(f.getModifiers()) @@ -269,19 +274,23 @@ public static Object copyShallowFrom(Object target, Object source) { // GROG - recent change from this // if ((!Modifier.isPublic(modifiers) // to this - String fname = f.getName(); + String fname = field.getName(); /* * if (fname.equals("desktops") || fname.equals("useLocalResources") * ){ log.info("here"); } */ - if (Modifier.isPrivate(modifiers) || fname.equals("log") || Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) { - log.debug("skipping {}", f.getName()); + if (Modifier.isPrivate(modifiers) + || fname.equals("log") + || Modifier.isTransient(modifiers) + || Modifier.isStatic(modifiers) + || Modifier.isFinal(modifiers)) { + log.debug("skipping {}", field.getName()); continue; } else { - log.debug("copying {}", f.getName()); + log.debug("copying {}", field.getName()); } - Type t = f.getType(); + Type t = field.getType(); // log.info(String.format("setting %s", f.getName())); /* @@ -291,30 +300,30 @@ public static Object copyShallowFrom(Object target, Object source) { // GroG - this is new 1/26/2017 - needed to get webgui data to // load - f.setAccessible(true); - Field targetField = sourceClass.getDeclaredField(f.getName()); + field.setAccessible(true); + Field targetField = sourceClass.getDeclaredField(field.getName()); targetField.setAccessible(true); - if (t.equals(java.lang.Boolean.TYPE)) { - targetField.setBoolean(target, f.getBoolean(source)); - } else if (t.equals(java.lang.Character.TYPE)) { - targetField.setChar(target, f.getChar(source)); - } else if (t.equals(java.lang.Byte.TYPE)) { - targetField.setByte(target, f.getByte(source)); - } else if (t.equals(java.lang.Short.TYPE)) { - targetField.setShort(target, f.getShort(source)); - } else if (t.equals(java.lang.Integer.TYPE)) { - targetField.setInt(target, f.getInt(source)); - } else if (t.equals(java.lang.Long.TYPE)) { - targetField.setLong(target, f.getLong(source)); - } else if (t.equals(java.lang.Float.TYPE)) { - targetField.setFloat(target, f.getFloat(source)); - } else if (t.equals(java.lang.Double.TYPE)) { - targetField.setDouble(target, f.getDouble(source)); + if (t.equals(Boolean.TYPE)) { + targetField.setBoolean(target, field.getBoolean(source)); + } else if (t.equals(Character.TYPE)) { + targetField.setChar(target, field.getChar(source)); + } else if (t.equals(Byte.TYPE)) { + targetField.setByte(target, field.getByte(source)); + } else if (t.equals(Short.TYPE)) { + targetField.setShort(target, field.getShort(source)); + } else if (t.equals(Integer.TYPE)) { + targetField.setInt(target, field.getInt(source)); + } else if (t.equals(Long.TYPE)) { + targetField.setLong(target, field.getLong(source)); + } else if (t.equals(Float.TYPE)) { + targetField.setFloat(target, field.getFloat(source)); + } else if (t.equals(Double.TYPE)) { + targetField.setDouble(target, field.getDouble(source)); } else { // log.debug(String.format("setting reference to remote // object %s", f.getName())); - targetField.set(target, f.get(source)); + targetField.set(target, field.get(source)); } } catch (Exception e) { log.error("copy failed source {} to a {}", source, target, e); @@ -369,7 +378,7 @@ public static void sleep(long millis) { } } - public final static String stackToString(final Throwable e) { + public static String stackToString(final Throwable e) { StringWriter sw; try { sw = new StringWriter(); @@ -393,9 +402,11 @@ static public String getDataDir(String typeName) { String dataDir = Runtime.DATA_DIR + fs + typeName; File f = new File(dataDir); if (!f.exists()) { - f.mkdirs(); + if (!f.mkdirs()) { + log.error("Cannot create data directory: {}", dataDir); + } } - return Runtime.DATA_DIR + fs + typeName; + return dataDir; } public String getDataDir() { @@ -406,9 +417,11 @@ public String getDataInstanceDir() { String dataDir = Runtime.DATA_DIR + fs + getClass().getSimpleName() + fs + getName(); File f = new File(dataDir); if (!f.exists()) { - f.mkdirs(); + if (!f.mkdirs()) { + error("Cannot create data directory: {}", dataDir); + } } - return Runtime.DATA_DIR + fs + getClass().getSimpleName() + fs + getName(); + return dataDir; } // ============== resources begin ====================================== @@ -460,33 +473,29 @@ static public String getResourceDir(Class clazz, String additionalPath) { * to glue together * @return the full resolved path * + * FIXME - DO NOT USE STATIC !!!! + * all instances of services should be able to get the resource directory + * If its static and "configurable" then it needs an instance of Runtime + * which is not available. + * */ + @Deprecated /* this should not be static - remove it */ static public String getResourceDir(String serviceType, String additionalPath) { - // setting resource directory - String resourceDir = "resource" + fs + serviceType; - - // overriden by src - String override = "src" + fs + "main" + fs + "resources" + fs + "resource" + fs + serviceType; - File test = new File(override); - if (test.exists()) { - log.info("found override resource dir {}", override); - resourceDir = override; - } - - override = ".." + fs + serviceType + fs + "resource" + fs + serviceType; - test = new File(override); - if (test.exists()) { - log.info("found override repo dir {}", override); - resourceDir = override; + // setting resource directory + String resourceDir = null; + + // stupid solution to get past static problem + if (!"Runtime".equals(serviceType)) { + resourceDir = Runtime.getInstance().getConfig().resource + fs + serviceType; + } else { + resourceDir = "resource"; } - if (additionalPath != null) { resourceDir = FileIO.gluePaths(resourceDir, additionalPath); } return resourceDir; } - /** * non static get resource path return the path to a resource - since the root * can change depending if in debug or runtime - it gets the appropriate root @@ -512,13 +521,7 @@ public String getResourcePath(String additionalPath) { static public String getResourceRoot() { // setting resource root details - String resourceRootDir = "resource"; - // allow default to be overriden by src if it exists - File src = new File("src"); - if (src.exists()) { - resourceRootDir = "src" + fs + "main" + fs + "resources" + fs + "resource"; - } - return resourceRootDir; + return "resource"; } /** @@ -634,14 +637,12 @@ public Service(String reservedKey, String inId) { log.debug("creating remote proxy service for id {}", id); } - serviceClass = this.getClass().getCanonicalName(); + typeKey = this.getClass().getCanonicalName(); simpleName = this.getClass().getSimpleName(); MethodCache cache = MethodCache.getInstance(); cache.cacheMethodEntries(this.getClass()); - // pull back the overrides - serviceType = MetaData.get(getClass().getSimpleName());// ServiceData.getMetaData(name, - // getClass().getSimpleName()); + serviceType = MetaData.get(getClass().getSimpleName()); // FIXME - this is 'sort-of' static :P if (methodSet == null) { @@ -663,7 +664,7 @@ public Service(String reservedKey, String inId) { loadLocalizations(); this.inbox = new Inbox(getFullName()); - this.outbox = new Outbox(this); + this.outbox = new Outbox(getFullName()); File versionFile = new File(getResourceDir() + fs + "version.txt"); if (versionFile.exists()) { @@ -744,6 +745,7 @@ public void addListener(String topicMethod, String callbackName) { */ @Override public void addListener(String topicMethod, String callbackName, String callbackMethod) { + callbackName = CodecUtils.getFullName(callbackName); MRLListener listener = new MRLListener(topicMethod, callbackName, callbackMethod); if (outbox.notifyList.containsKey(listener.topicMethod)) { // iterate through all looking for duplicate @@ -819,7 +821,7 @@ synchronized public void addTask(String taskName, boolean oneShot, long interval return; } Timer timer = new Timer(String.format("%s.timer", String.format("%s.%s", getName(), taskName))); - Message msg = Message.createMessage(getName(), getName(), method, params); + Message msg = Message.createMessage(getFullName(), getFullName(), method, params); Task task = new Task(this, oneShot, taskName, intervalMs, msg); timer.schedule(task, delayMs); tasks.put(taskName, timer); @@ -981,11 +983,11 @@ public Status getLastError() { // FIXME - use the method cache public Set getMessageSet() { - Set ret = new TreeSet(); + Set ret = new TreeSet<>(); Method[] methods = getMethods(); log.debug("getMessageSet loading {} non-sub-routable methods", methods.length); - for (int i = 0; i < methods.length; ++i) { - ret.add(methods[i].getName()); + for (Method method : methods) { + ret.add(method.getName()); } return ret; } @@ -1013,23 +1015,47 @@ public String[] getMethodNames() { public Method[] getMethods() { return this.getClass().getMethods(); } - + + /** + * Returns a map containing all interface names from the class hierarchy and the interface hierarchy of the + * current class. + * + * @return A map containing all interface names. + */ public Map getInterfaceSet() { - Map ret = new TreeMap(); - Class c = getClass(); - while (c != Object.class) { - - Class[] interfaces = c.getInterfaces(); - for (int i = 0; i < interfaces.length; ++i) { - Class interfaze = interfaces[i]; - // ya silly :P - but gson's default conversion of a HashSet is an - // array - ret.put(interfaze.getName(), interfaze.getName()); + Map ret = new TreeMap<>(); + Set> visitedClasses = new HashSet<>(); + getAllInterfacesHelper(getClass(), ret, visitedClasses); + return ret; + } + + /** + * Recursively traverses the class hierarchy and the interface hierarchy to add all interface names to the + * specified map. + * + * @param c The class to start the traversal from. + * @param ret The map to store the interface names. + * @param visitedClasses A set to keep track of visited classes to avoid infinite loops. + */ + private void getAllInterfacesHelper(Class c, Map ret, Set> visitedClasses) { + if (c != null && !visitedClasses.contains(c)) { + // Add interfaces from the current class + Class[] interfaces = c.getInterfaces(); + for (Class interfaze : interfaces) { + ret.put(interfaze.getName(), interfaze.getName()); + } + + // Add interfaces from interfaces implemented by the current class + for (Class interfaze : interfaces) { + getAllInterfacesHelper(interfaze, ret, visitedClasses); + } + + // Recursively traverse the superclass hierarchy + visitedClasses.add(c); + getAllInterfacesHelper(c.getSuperclass(), ret, visitedClasses); } - c = c.getSuperclass(); - } - return ret; } + public Message getMsg() throws InterruptedException { return inbox.getMsg(); @@ -1060,25 +1086,23 @@ public List getNotifyList(String key) { @Override public ArrayList getNotifyListKeySet() { - ArrayList ret = new ArrayList(); if (getOutbox() == null) { // this is remote system - it has a null outbox, because its // been serialized with a transient outbox // and your in a skeleton // use the runtime to send a message - ArrayList remote = null; try { - remote = Runtime.getInstance().sendBlocking(getName(), "getNotifyListKeySet", new StaticType<>(){}); + return Runtime.getInstance().sendBlocking(getName(), "getNotifyListKeySet", new StaticType<>(){}); + } catch (Exception e) { log.error("remote getNotifyList threw", e); + return null; } - return remote; } else { - ret.addAll(getOutbox().notifyList.keySet()); + return new ArrayList<>(getOutbox().notifyList.keySet()); } - return ret; } @Override @@ -1096,8 +1120,8 @@ public Thread getThisThread() { } @Override - public String getType() { - return getClass().getCanonicalName(); + public String getTypeKey() { + return typeKey; } @Override @@ -1107,10 +1131,10 @@ public boolean hasError() { @Override public Map getPeers() { - if (config == null) { + if (getConfig() == null) { return null; } - return config.getPeers(); + return getConfig().getPeers(); } /** @@ -1135,30 +1159,26 @@ public String getPeerKey(String name) { @Override public Set getPeerKeys() { - if (config == null || config.peers == null) { - return new HashSet(); + if (getConfig() == null || getConfig().peers == null) { + return new HashSet<>(); } - return config.peers.keySet(); + return getConfig().peers.keySet(); } public String help(String format, String level) { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); Method[] methods = this.getClass().getDeclaredMethods(); - TreeMap sorted = new TreeMap(); + TreeMap sorted = new TreeMap<>(); - for (int i = 0; i < methods.length; ++i) { - Method m = methods[i]; + for (Method m : methods) { sorted.put(m.getName(), m); } for (String key : sorted.keySet()) { Method m = sorted.get(key); sb.append("/").append(getName()).append("/").append(m.getName()); Class[] types = m.getParameterTypes(); - if (types != null) { - for (int j = 0; j < types.length; ++j) { - Class c = types[j]; - sb.append("/").append(c.getSimpleName()); - } + for (Class c : types) { + sb.append("/").append(c.getSimpleName()); } sb.append("\n"); } @@ -1294,6 +1314,14 @@ final public R invokeOn(boolean blockLocally, Object obj, String methodName, } retobj = (R) method.invoke(obj, params); if (blockLocally) { + Outbox outbox = null; + if (obj instanceof ServiceInterface) { + outbox = ((ServiceInterface)obj).getOutbox(); + } else { + return retobj; + } + + List subList = outbox.notifyList.get(methodName); // correct? get local (default?) gateway Runtime runtime = Runtime.getInstance(); @@ -1324,7 +1352,7 @@ final public R invokeOn(boolean blockLocally, Object obj, String methodName, // we attempted to invoke this , it blew up. Catch it here, // continue // through the rest of the listeners instead of bombing out. - log.error("Invoke blew up! on: {} calling method {} ", si.getName(), m.toString(), e); + log.error("Invoke blew up! on: {} calling method {} ", si.getName(), m, e); } } } @@ -1373,78 +1401,83 @@ public boolean isRunning() { } /** - * Default load config method, subclasses should override this to support - * service specific configuration in the service yaml files. - * + * getConfig returns current config of the service. This default super method + * will also filter webgui subscriptions out, in addition for any local subscriptions it + * will remove the instance "id" from any service. The reason it removes the webgui + * subscriptions is to avoid overwelming the user when modifying config. UI subscriptions + * tend to be very numerous and not very useful to the user. The reason it removes the + * instance id from local subscriptions is to allow the config to be used with any instance. + * Unless the user is controlling instance id, its random every restart. */ - @Override - public ServiceConfig apply(ServiceConfig inConfig) { - log.info("Default service config loading for service: {} type: {}", getName(), getType()); - /* - * We clone/serialize here because we don't want to use the same reference - * of of config in the plan. If configuration is applied through the plan, - * "or from anywhere else" we make a copy of it here. And the copy is - * applied to the actual service. This keeps the plan safe to modify without - * the worry of modifying a running service config. - */ + public T getConfig() { + return config; + } + + /** + * Super class apply using template type. The default assigns config of the templated type, and also + * add listeners from subscriptions found on the base class ServiceConfig.listeners + */ + public T apply(T c) { + config = c; + addConfigListeners(c); + return config; + } - String yaml = CodecUtils.toYaml(inConfig); - ServiceConfig copyOfConfig = CodecUtils.fromYaml(yaml, inConfig.getClass()); - // TODO - handle subscriptions / listeners - if (copyOfConfig.listeners != null) { - for (Listener listener : copyOfConfig.listeners) { + /** + * The basic ServiceConfig has a list of listeners. These are definitions of + * other subscribers subscribing for data from this service. This method + * processes those listeners and adds them to the outbox notifyList. + */ + public ServiceConfig addConfigListeners(ServiceConfig config) { + if (config != null && config.listeners != null) { + for (Listener listener : config.listeners) { addListener(listener.method, listener.listener, listener.callback); } } - - this.config = copyOfConfig; return config; } /** - * Default getConfig returns name and type with null service specific config - * + * Default filtered config, used when saving, can be overriden by concrete class */ @Override - public ServiceConfig getConfig() { - - boolean filterWeb = true; - + public ServiceConfig getFilteredConfig() { + // Make a copy, because we don't want to modify the original + ServiceConfig sc = CodecUtils.fromYaml(CodecUtils.toYaml(getConfig()), config.getClass()); Map> listeners = getOutbox().notifyList; List newListeners = new ArrayList<>(); // TODO - perhaps a switch for "remote" things ? - if (filterWeb) { - for (String method : listeners.keySet()) { - List list = listeners.get(method); - for (MRLListener listener : list) { - if (!listener.callbackName.endsWith("@webgui-client")) { - - Listener newConfigListener = new Listener(listener.topicMethod, listener.callbackName, listener.callbackMethod); - newListeners.add(newConfigListener); - } + for (String method : listeners.keySet()) { + List list = listeners.get(method); + for (MRLListener listener : list) { + if (!listener.callbackName.endsWith("@webgui-client")) { + // Removes the `@runtime-id` so configs still work with local IDs + // The StringUtils.removeEnd() call is a no-op when the ID is not our + // local ID, + // so doesn't conflict with remote routes + Listener newConfigListener = new Listener(listener.topicMethod, StringUtil.removeEnd(listener.callbackName, '@' + Platform.getLocalInstance().getId()), + listener.callbackMethod); + newListeners.add(newConfigListener); } } } if (newListeners.size() > 0) { - config.listeners = newListeners; + sc.listeners = newListeners; } - return config; - } - - @Override - public ServiceConfig getFilteredConfig() { - ServiceConfig sc = getConfig(); - // deep clone - sc = (ServiceConfig) CodecUtils.fromYaml(CodecUtils.toYaml(sc), sc.getClass()); return sc; } + @Override - public void setConfig(ServiceConfig config) { - this.config = config; + public void setConfigValue(String fieldname, Object value) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + log.info("setting field name fieldname {} to {}", fieldname, value); + + Field field = getConfig().getClass().getDeclaredField(fieldname); + // field.setAccessible(true); should not need this - it "should" be public + field.set(getConfig(), value); } @Override @@ -1540,17 +1573,27 @@ public void removeListener(String topicMethod, String callbackName) { @Override public void removeListener(String outMethod, String serviceName, String inMethod) { + String fullName = CodecUtils.getFullName(serviceName); if (outbox.notifyList.containsKey(outMethod)) { List nel = outbox.notifyList.get(outMethod); - for (int i = 0; i < nel.size(); ++i) { - MRLListener target = nel.get(i); - if (target.callbackName.compareTo(serviceName) == 0) { - nel.remove(i); - log.info("removeListener requested {}.{} to be removed", serviceName, outMethod); + nel.removeIf(listener -> { + if (listener == null) { + log.info("Removing null listener for method {}", outMethod); + return true; } - } + + // Previously we were not checking inMethod, which meant if a service had multiple + // subscriptions to the same topic (one to many mapping), the first in the list would be removed + // instead of the requested one. + if (listener.callbackMethod.equals(inMethod) + && CodecUtils.checkServiceNameEquality(listener.callbackName, fullName)) { + log.info("removeListener requested {}.{} to be removed", fullName, outMethod); + return true; + } + return false; + }); } else { - log.info("removeListener requested {}.{} to be removed - but does not exist", serviceName, outMethod); + log.info("removeListener requested {}.{} to be removed - but does not exist", fullName, outMethod); } } @@ -1648,7 +1691,7 @@ public Object invokePeer(String peerName, String method, Object... data) { public void sendToPeer(String peerName, String method, Object... data) { String name = getPeerName(peerName); - Message msg = Message.createMessage(getName(), name, method, data); + Message msg = Message.createMessage(getFullName(), name, method, data); send(msg); } @@ -1665,14 +1708,13 @@ public void send(String name, String method, Object... data) { // if you know the service is local - use same thread // to call directly ServiceInterface si = Runtime.getService(name); - if (si != null) { + if (si != null && CodecUtils.isLocal(name)) { invokeOn(true, si, method, data); return; } // if unknown assume remote - fire and forget on outbox - Message msg = Message.createMessage(getName(), name, method, data); - msg.sender = this.getFullName(); + Message msg = Message.createMessage(getFullName(), name, method, data); // All methods which are invoked will // get the correct sendingMethod // here its hardcoded @@ -1688,8 +1730,7 @@ public void send(Message msg) { public void sendAsync(String name, String method, Object... data) { // if unknown assume remote - fire and forget on outbox - Message msg = Message.createMessage(getName(), name, method, data); - msg.sender = this.getFullName(); + Message msg = Message.createMessage(getFullName(), name, method, data); // All methods which are invoked will // get the correct sendingMethod // here its hardcoded @@ -1870,7 +1911,7 @@ synchronized public ServiceInterface startPeer(String peerKey) { peerKey = peerKey.trim(); // get current definition of config and peer - Peer peer = config.getPeer(peerKey); + Peer peer = getConfig().getPeer(peerKey); if (peer == null) { error("startPeer could not find peerKey of %s in %s", peerKey, getName()); @@ -1913,8 +1954,8 @@ synchronized public ServiceInterface startPeer(String peerKey) { */ synchronized public void releasePeer(String peerKey) { - if (config != null && config.getPeer(peerKey) != null) { - Peer peer = config.getPeer(peerKey); + if (getConfig() != null && getConfig().getPeer(peerKey) != null) { + Peer peer = getConfig().getPeer(peerKey); ServiceConfig sc = Runtime.getPlan().get(peer.name); // peer recursive if (sc != null && sc.getPeers() != null) { @@ -1944,11 +1985,7 @@ synchronized public void startService() { } thisThread.start(); isRunning = true; - Runtime runtime = Runtime.getInstance(); - if (runtime != null) { - runtime.invoke("started", getName()); // getFullName()); - removed - // fullname - } + send("runtime", "started", getName()); } else { log.debug("startService request: service {} is already running", name); @@ -1975,51 +2012,58 @@ synchronized public void stopService() { @Override public void subscribe(NameProvider topicName, String topicMethod) { String callbackMethod = CodecUtils.getCallbackTopicName(topicMethod); - subscribe(topicName.getName(), topicMethod, getName(), callbackMethod); + subscribe(topicName.getName(), topicMethod, getFullName(), callbackMethod); } @Override public void subscribe(String topicName, String topicMethod) { String callbackMethod = CodecUtils.getCallbackTopicName(topicMethod); - subscribe(topicName, topicMethod, getName(), callbackMethod); + subscribe(topicName, topicMethod, getFullName(), callbackMethod); + } + + @Override + public void subscribe(String service, String method, String callback) { + subscribe(service, method, getFullName(), callback); } public void subscribeTo(String service, String method) { - subscribe(service, method, getName(), CodecUtils.getCallbackTopicName(method)); + subscribe(service, method, getFullName(), CodecUtils.getCallbackTopicName(method)); } public void subscribeToRuntime(String method) { - subscribe(Runtime.getInstance().getName(), method, getName(), CodecUtils.getCallbackTopicName(method)); + subscribe(Runtime.getInstance().getFullName(), method, getFullName(), CodecUtils.getCallbackTopicName(method)); } public void unsubscribeTo(String service, String method) { - unsubscribe(service, method, getName(), CodecUtils.getCallbackTopicName(method)); + unsubscribe(service, method, getFullName(), CodecUtils.getCallbackTopicName(method)); } public void unsubscribeToRuntime(String method) { - unsubscribe(Runtime.getInstance().getName(), method, getName(), CodecUtils.getCallbackTopicName(method)); + unsubscribe(Runtime.getInstance().getFullName(), method, getFullName(), CodecUtils.getCallbackTopicName(method)); } - @Override + // TODO make protected or private public void subscribe(String topicName, String topicMethod, String callbackName, String callbackMethod) { + topicName = CodecUtils.getFullName(topicName); + callbackName = CodecUtils.getFullName(callbackName); log.info("subscribe [{}/{} ---> {}/{}]", topicName, topicMethod, callbackName, callbackMethod); // TODO - do regex matching if (topicName.contains("*")) { // FIXME "any regex expression List tnames = Runtime.getServiceNames(topicName); for (String serviceName : tnames) { MRLListener listener = new MRLListener(topicMethod, callbackName, callbackMethod); - send(Message.createMessage(getName(), serviceName, "addListener", listener)); + send(Message.createMessage(getFullName(), serviceName, "addListener", listener)); } } else { if (topicMethod.contains("*")) { // FIXME "any regex expression Set tnames = Runtime.getMethodMap(topicName).keySet(); for (String method : tnames) { MRLListener listener = new MRLListener(method, callbackName, callbackMethod); - send(Message.createMessage(getName(), topicName, "addListener", listener)); + send(Message.createMessage(getFullName(), topicName, "addListener", listener)); } } else { MRLListener listener = new MRLListener(topicMethod, callbackName, callbackMethod); - send(Message.createMessage(getName(), topicName, "addListener", listener)); + send(Message.createMessage(getFullName(), topicName, "addListener", listener)); } } } @@ -2027,19 +2071,26 @@ public void subscribe(String topicName, String topicMethod, String callbackName, @Override public void unsubscribe(NameProvider topicName, String topicMethod) { String callbackMethod = CodecUtils.getCallbackTopicName(topicMethod); - unsubscribe(topicName.getName(), topicMethod, getName(), callbackMethod); + unsubscribe(topicName.getName(), topicMethod, getFullName(), callbackMethod); } @Override public void unsubscribe(String topicName, String topicMethod) { String callbackMethod = CodecUtils.getCallbackTopicName(topicMethod); - unsubscribe(topicName, topicMethod, getName(), callbackMethod); + unsubscribe(topicName, topicMethod, getFullName(), callbackMethod); } - + @Override + public void unsubscribe(String topicName, String topicMethod, String callback) { + unsubscribe(topicName, topicMethod, getFullName(), callback); + } + + // TODO make protected or private public void unsubscribe(String topicName, String topicMethod, String callbackName, String callbackMethod) { + topicName = CodecUtils.getFullName(topicName); + callbackName = CodecUtils.getFullName(callbackName); log.info("unsubscribe [{}/{} ---> {}/{}]", topicName, topicMethod, callbackName, callbackMethod); - send(Message.createMessage(getName(), topicName, "removeListener", new Object[] { topicMethod, callbackName, callbackMethod })); + send(Message.createMessage(getFullName(), topicName, "removeListener", new Object[] { topicMethod, callbackName, callbackMethod })); } // -------------- Messaging Ends ----------------------- @@ -2056,12 +2107,13 @@ public Status error(Exception e) { @Override public Status error(String format, Object... args) { - Status ret = null; - if (format != null) { - ret = Status.error(String.format(format, args)); - } else { - ret = Status.error(String.format("", args)); - } + Status ret; + ret = Status.error( + String.format( + Objects.requireNonNullElse(format, ""), + args + ) + ); ret.name = getName(); log.error(ret.toString()); lastError = ret; @@ -2199,7 +2251,7 @@ public void attach(String serviceName) throws Exception { */ @Override public boolean isAttached(String serviceName) { - return getAttached().contains(serviceName); + return getAttached().contains(CodecUtils.getFullName(serviceName)); } /** @@ -2526,7 +2578,7 @@ public String localize(String key, Object... args) { if (this != runtime) { // if we are not runtime - we ask runtime prop = runtime.localize(key, args); - } else if (this == runtime) { + } else { // if we are runtime - we try default en prop = runtime.localizeDefault(key); } @@ -2543,6 +2595,7 @@ public String localize(String key, Object... args) { } @Override + @Deprecated /* this system should be removed in favor of a ProgramAB instance with ability to translate */ public void loadLocalizations() { if (defaultLocalization == null) { @@ -2650,7 +2703,7 @@ public boolean isType(String clazz) { if (!clazz.contains(".")) { clazz = String.format("org.myrobotlab.service.%s", clazz); } - return serviceClass.equals(clazz); + return typeKey.equals(clazz); } @Override @@ -2677,10 +2730,10 @@ public int getCreationOrder() { */ public String getPeerName(String peerKey) { - if (config == null) { + if (getConfig() == null) { return null; } - return config.getPeerName(peerKey); + return getConfig().getPeerName(peerKey); } /** @@ -2712,11 +2765,18 @@ public MetaData getMetaData() { */ public void apply() { Runtime runtime = Runtime.getInstance(); - ServiceConfig sc = runtime.readServiceConfig(null, name); + String configName = runtime.getConfigName(); + ServiceConfig sc = runtime.readServiceConfig(configName, name); + + if (sc == null) { + error("config file %s not found", Runtime.getConfigRoot() + fs + configName + fs + name + ".yml"); + return; + } + // updating plan Runtime.getPlan().put(getName(), sc); // applying config to self - apply(sc); + apply((T)sc); } /** @@ -2728,7 +2788,7 @@ public void apply() { * @param fullName */ public void setPeerName(String key, String fullName) { - Peer peer = config.getPeer(key); + Peer peer = getConfig().getPeer(key); String oldName = peer.name; peer.name = fullName; // update plan ? @@ -2737,6 +2797,13 @@ public void setPeerName(String key, String fullName) { // should we also make or update a config file - if the config path is set? info("updated %s name to %s", oldName, peer.name); } + + /** + * get all the subscriptions to this service + */ + public Map> getNotifyList(){ + return getOutbox().getNotifyList(); + } /** * Update a peer's type. First its done in the current Plan, and it will also @@ -2760,7 +2827,7 @@ public void updatePeerType(String key, String peerType) { sc.putPeerType(key, String.format("%s.%s", getName(), key), peerType); } - Peer peer = config.getPeer(key); + Peer peer = getConfig().getPeer(key); peer.type = peerType; // not Needed @@ -2770,17 +2837,17 @@ public void updatePeerType(String key, String peerType) { // FIXME - rename putDefault ServiceConfig.getDefault(Runtime.getPlan(), peer.name, peerType); Runtime runtime = Runtime.getInstance(); - String configPath = runtime.getConfigPath(); + String configName = runtime.getConfigName(); // Seems a bit invasive - but yml file overrides everything // if one exists we need to replace it with the new peer type - if (configPath != null) { - String configFile = configPath + fs + peer.name + ".yml"; + if (configName != null) { + String configFile = configName + fs + peer.name + ".yml"; File staleFile = new File(configFile); if (staleFile.exists()) { log.info("removing old config file {}", configFile); staleFile.delete(); // save new default in its place - runtime.saveDefault(configPath, peer.name, peer.type, false); + runtime.saveDefault(configName, peer.name, peer.type, false); } } info("updated %s to type %s", peer.name, peerType); diff --git a/src/main/java/org/myrobotlab/framework/Status.java b/src/main/java/org/myrobotlab/framework/Status.java index bfd1a0c83c..caba7dfad7 100644 --- a/src/main/java/org/myrobotlab/framework/Status.java +++ b/src/main/java/org/myrobotlab/framework/Status.java @@ -21,7 +21,7 @@ /** * Goal is to have a very simple Pojo with only a few (native Java helper * methods) WARNING !!! - this class used to extend Exception or Throwable - but - * the gson serializer would stack overflow with self reference issue + * the serializer would stack overflow with self reference issue * * TODO - allow radix tree searches for "keys" ??? * diff --git a/src/main/java/org/myrobotlab/framework/Task.java b/src/main/java/org/myrobotlab/framework/Task.java index 68fdd29591..315b62a6f0 100644 --- a/src/main/java/org/myrobotlab/framework/Task.java +++ b/src/main/java/org/myrobotlab/framework/Task.java @@ -64,7 +64,7 @@ public void run() { Task t = new Task(this); // clear history list - becomes "new" message t.msg.historyList.clear(); - Timer timer = myService.tasks.get(taskName); + Timer timer = (Timer) myService.tasks.get(taskName); if (timer != null) { // timer = new Timer(String.format("%s.timer", getName())); try { diff --git a/src/main/java/org/myrobotlab/framework/TypeConverter.java b/src/main/java/org/myrobotlab/framework/TypeConverter.java index c6dad571dd..051ae50e34 100644 --- a/src/main/java/org/myrobotlab/framework/TypeConverter.java +++ b/src/main/java/org/myrobotlab/framework/TypeConverter.java @@ -22,8 +22,6 @@ public class TypeConverter { public final static Logger log = LoggerFactory.getLogger(TypeConverter.class); - // private static Gson gson = new Gson(); - // Possible Optimization -> pointers to known method signatures - // optimization so that once a // method's signature is processed and diff --git a/src/main/java/org/myrobotlab/framework/interfaces/ConfigurableService.java b/src/main/java/org/myrobotlab/framework/interfaces/ConfigurableService.java new file mode 100644 index 0000000000..5e49ecd1fb --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/interfaces/ConfigurableService.java @@ -0,0 +1,8 @@ +package org.myrobotlab.framework.interfaces; + +import org.myrobotlab.service.config.ServiceConfig; + +public interface ConfigurableService { + T getConfig(); + T apply(T c); +} diff --git a/src/main/java/org/myrobotlab/framework/interfaces/MessageSubscriber.java b/src/main/java/org/myrobotlab/framework/interfaces/MessageSubscriber.java index 6e5311f017..231dbf2750 100644 --- a/src/main/java/org/myrobotlab/framework/interfaces/MessageSubscriber.java +++ b/src/main/java/org/myrobotlab/framework/interfaces/MessageSubscriber.java @@ -2,16 +2,54 @@ public interface MessageSubscriber { - public void subscribe(NameProvider topicName, String topicKey); - - public void subscribe(String topicName, String topicKey); - - public void subscribe(String topicName, String topicMethod, String callbackName, String callbackMethod); - - public void unsubscribe(NameProvider topicName, String topicKey); - - public void unsubscribe(String topicName, String topicKey); - - public void unsubscribe(String topicName, String topicMethod, String callbackName, String callbackMethod); + /** + * This will subscribe to a NameProviders method. The callback will be + * automatically generated. Rules are publish{Method} or get{Method} will + * callback with on{Method}. + * + * @param service + * @param method + */ + public void subscribe(NameProvider service, String method); + + /** + * Service name is supplied and method to subscribe to. The callback will be + * automatically generated. Rules are publish{Method} or get{Method} + * will callback with on{Method}. + * + * @param service + * @param method + */ + public void subscribe(String service, String method); + + /** + * Subscribe with callback. The callback is explicitly set. + * @param service + * @param method + * @param callback + */ + public void subscribe(String service, String method, String callback); + + /*** + * Unsubscribe from a NameProviders method. + * @param service + * @param method + */ + public void unsubscribe(NameProvider service, String method); + + /** + * Unsubscribe from a service method. + * @param service + * @param method + */ + public void unsubscribe(String service, String method); + + /** + * Unsubscribe from a service method with an explicit callback. + * @param service + * @param method + * @param callback + */ + public void unsubscribe(String service, String method, String callback); } diff --git a/src/main/java/org/myrobotlab/framework/interfaces/ServiceInterface.java b/src/main/java/org/myrobotlab/framework/interfaces/ServiceInterface.java index 1fb3155156..aad5e3a823 100644 --- a/src/main/java/org/myrobotlab/framework/interfaces/ServiceInterface.java +++ b/src/main/java/org/myrobotlab/framework/interfaces/ServiceInterface.java @@ -8,6 +8,7 @@ import org.myrobotlab.framework.Inbox; import org.myrobotlab.framework.MRLListener; +import org.myrobotlab.framework.MethodCache; import org.myrobotlab.framework.MethodEntry; import org.myrobotlab.framework.Outbox; import org.myrobotlab.framework.Peer; @@ -17,11 +18,11 @@ import org.myrobotlab.service.meta.abstracts.MetaData; import org.slf4j.Logger; -public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypeProvider, MessageSubscriber, MessageSender, StateSaver, Invoker, StatePublisher, StatusPublisher, - ServiceStatus, TaskManager, Attachable, MessageInvoker, Comparable { +public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypeProvider, MessageSubscriber, MessageSender, StateSaver, Invoker, StatePublisher, StatusPublisher, + ServiceStatus, TaskManager, Attachable, MessageInvoker, Comparable/*, ConfigurableService */{ // does this work ? - public final static Logger log = LoggerFactory.getLogger(Service.class); + Logger log = LoggerFactory.getLogger(Service.class); /** * When set service will attempt to provide services with no hardware @@ -33,7 +34,7 @@ public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypePro * @return the value * */ - public boolean setVirtual(boolean b); + boolean setVirtual(boolean b); /** * check to see if the service is running in a virtual mode @@ -41,44 +42,46 @@ public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypePro * @return true if in virtual mode. * */ - public boolean isVirtual(); + boolean isVirtual(); - public String[] getDeclaredMethodNames(); + String[] getDeclaredMethodNames(); - public Method[] getDeclaredMethods(); + Method[] getDeclaredMethods(); - public URI getInstanceId(); + URI getInstanceId(); - public String[] getMethodNames(); + String[] getMethodNames(); - public Method[] getMethods(); + Method[] getMethods(); - public List getNotifyList(String key); + List getNotifyList(String key); - public List getNotifyListKeySet(); + List getNotifyListKeySet(); - public Inbox getInbox(); + Inbox getInbox(); - public Outbox getOutbox(); + Outbox getOutbox(); @Override - public String getSimpleName(); + String getSimpleName(); /** * equivalent to getClass().getCanonicalName() - * + * * @return */ - public String getType(); + String getTypeKey(); + /** + * Does the meta data of this service define peers * Keys to Peers - the keys are string constants the service uses to refer to a * Peer service. The key never changes. However, the Peer's name and type can. * This returns all peers for a service. * * @return */ - public Map getPeers(); + Map getPeers(); /** * Returns peers keys. Peer key is the hardcoded key a composite service @@ -86,7 +89,7 @@ public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypePro * * @return */ - public Set getPeerKeys(); + Set getPeerKeys(); /** * Returns the peer key if a name is supplied and matches a peer name @@ -95,57 +98,54 @@ public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypePro * - service name * @return - coorisponding peer key if it exists */ - public String getPeerKey(String name); + String getPeerKey(String name); /** * Service life-cycle method: releaseService will call stopService, release * its peers, do any derived business logic to release resources, then * un-register itself */ - public void releaseService(); + void releaseService(); /** * called by runtime when system is shutting down a service can use this * method when it has to do some "ordered" cleanup */ - public void preShutdown(); + void preShutdown(); /** * asked by the framework - to determine if the service needs to be secure * * @return true/false */ - public boolean requiresSecurity(); + boolean requiresSecurity(); - public void setInstanceId(URI uri); + void setInstanceId(URI uri); /** * Service life cycle method - calls create, and starts any necessary * resources to function */ - public void startService(); + void startService(); /** * @return get a services current config * */ - public ServiceConfig getConfig(); + ServiceConfig getConfig(); /** - * sets config - just before apply - * - * @param config + * reflectively sets a part of config + * + * @param fieldname + * @param value + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws NoSuchFieldException + * @throws SecurityException */ - public void setConfig(ServiceConfig config); + void setConfigValue(String fieldname, Object value) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException; - /** - * Configure a service by merging in configuration - * - * @param config - * the config to load - * @return the loaded config. - */ - public ServiceConfig apply(ServiceConfig config); /** * Service life-cycle method, stops the inbox and outbox threads - typically @@ -153,42 +153,42 @@ public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypePro * messaging from flowing in or out of this service - which is handled in the * base Service class. Most times this method will not need to be overriden */ - public void stopService(); + void stopService(); - public String clearLastError(); + String clearLastError(); - public boolean hasError(); + boolean hasError(); - public void out(String method, Object retobj); + void out(String method, Object retobj); - public boolean isRuntime(); + boolean isRuntime(); - public String getDescription(); + String getDescription(); - public Map getMethodMap(); + Map getMethodMap(); - public boolean isReady(); + boolean isReady(); - public boolean isRunning(); + boolean isRunning(); /** * @param creationCount * the order this service was created in relation to the other * service */ - public void setOrder(int creationCount); + void setOrder(int creationCount); - public String getId(); + String getId(); - public String getFullName(); + String getFullName(); - public void loadLocalizations(); + void loadLocalizations(); - public void setLocale(String code); + void setLocale(String code); - public int getCreationOrder(); + int getCreationOrder(); - public MetaData getMetaData(); + MetaData getMetaData(); /** * start a peer using a peerKey E.g. inside InMoov service startPeer("head") @@ -196,7 +196,7 @@ public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypePro * @param peerKey * @return */ - public ServiceInterface startPeer(String peerKey); + ServiceInterface startPeer(String peerKey); /** * setting an instance id on the service - this represents the running @@ -204,12 +204,26 @@ public interface ServiceInterface extends ServiceQueue, LoggingSink, NameTypePro * * @param id */ - public void setId(String id); + void setId(String id); /** * Get a clone of config that is filtered based on service preference + * * @return */ - public ServiceConfig getFilteredConfig(); + ServiceConfig getFilteredConfig(); + /** + * Adds the subscribers specified in the Service.listener as listeners to + * this service. + * + * @param config + * @return + */ + public ServiceConfig addConfigListeners(ServiceConfig config); + + /** + * get all the subscriptions to this service + */ + public Map> getNotifyList(); } diff --git a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java index 5e3d4d21d0..5759c4866e 100644 --- a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java +++ b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java @@ -137,7 +137,7 @@ public void createIvyForDependency(String location, ServiceDependency dependency dependency.getVersion() == null ? "latest.integration" : dependency.getVersion())); List excludes = dependency.getExcludes(); - boolean twoTags = dependency.getExt() != null || excludes != null & excludes.size() > 0; + boolean twoTags = dependency.getExt() != null || excludes != null && excludes.size() > 0; if (twoTags) { // more stuffs ! - we have 2 tags - end this one without /> sb.append(">\n"); @@ -152,7 +152,7 @@ public void createIvyForDependency(String location, ServiceDependency dependency } // exclusions begin --- - if (excludes != null & excludes.size() > 0) { + if (excludes != null && excludes.size() > 0) { StringBuilder ex = new StringBuilder(); for (ServiceExclude exclude : excludes) { ex.append(" 0) { - log.error("had errors - repo will not be updated"); + log.error("had errors - repo will not be updated. Errors:\n{}", err); } else { // TODO - promote to Repo.setInstalled diff --git a/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java b/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java index c490e2e6ac..8374f92cdf 100644 --- a/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java +++ b/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java @@ -155,11 +155,11 @@ public void createPom(String location, String[] serviceTypes) throws IOException // If we haven't seen this dependency before, add it to our known // dependencies - if (!allDependencies.containsKey(serviceDependency.getKey())) - allDependencies.put(serviceDependency.getKey(), new ArrayList<>(List.of(serviceDependency))); + if (!allDependencies.containsKey(serviceDependency.getProjectCoordinates())) + allDependencies.put(serviceDependency.getProjectCoordinates(), new ArrayList<>(List.of(serviceDependency))); else { // We have seen it, so loop over all dependencies with matching keys - allDependencies.get(serviceDependency.getKey()).forEach(existingDependency -> { + allDependencies.get(serviceDependency.getProjectCoordinates()).forEach(existingDependency -> { // Check priority, if this dependency is higher priority than // existing, @@ -173,7 +173,7 @@ public void createPom(String location, String[] serviceTypes) throws IOException serviceDependency.setSkipped(true); }); // Add the dependency to the known dependencies - allDependencies.get(serviceDependency.getKey()).add(serviceDependency); + allDependencies.get(serviceDependency.getProjectCoordinates()).add(serviceDependency); } }); diff --git a/src/main/java/org/myrobotlab/framework/repo/ServiceData.java b/src/main/java/org/myrobotlab/framework/repo/ServiceData.java index 39d87b766d..00c4406d4e 100644 --- a/src/main/java/org/myrobotlab/framework/repo/ServiceData.java +++ b/src/main/java/org/myrobotlab/framework/repo/ServiceData.java @@ -39,6 +39,16 @@ */ public class ServiceData implements Serializable { + /** + * the set of all categories + */ + public TreeMap categoryTypes = new TreeMap<>(); + + /** + * all services meta data is contained here + */ + public TreeMap serviceTypes = new TreeMap<>(); + static private ServiceData localInstance = null; static final public String LIBRARIES = "libraries"; @@ -57,7 +67,7 @@ public class ServiceData implements Serializable { private static final long serialVersionUID = 1L; - static private String serviceDataCacheFileName = LIBRARIES + File.separator + "serviceData.json"; + static private final String serviceDataCacheFileName = LIBRARIES + File.separator + "serviceData.json"; /** * clears all overrides. All services shall be using the standard hard co @@ -92,9 +102,8 @@ static public synchronized ServiceData generate() throws IOException { List services = FileIO.getServiceList(); log.info("found {} services", services.size()); - for (int i = 0; i < services.size(); ++i) { + for (String fullClassName : services) { - String fullClassName = services.get(i); log.debug("querying {}", fullClassName); try { @@ -112,7 +121,7 @@ static public synchronized ServiceData generate() throws IOException { sd.add(serviceType); for (String cat : serviceType.categories) { - Category category = null; + Category category; if (serviceType.isAvailable()) { if (sd.categoryTypes.containsKey(cat)) { category = sd.categoryTypes.get(cat); @@ -135,10 +144,10 @@ static public synchronized ServiceData generate() throws IOException { } static public List getDependencyKeys(String fullTypeName) { - List keys = new ArrayList(); + List keys = new ArrayList<>(); ServiceData sd = getLocalInstance(); if (!sd.serviceTypes.containsKey(fullTypeName)) { - log.error("{} not defined in service types"); + log.error("{} not defined in service types", fullTypeName); return keys; } @@ -177,18 +186,17 @@ static public ServiceData getLocalInstance() { // First check the libraries/serviceData.json dir. File jsonFile = new File(serviceDataCacheFileName); if (jsonFile.exists()) { - // load it and return! - String data = null; try { - data = FileIO.toString(jsonFile); - } catch (IOException e) { + // load it and return! + localInstance = CodecUtils.fromJson(FileIO.toString(jsonFile), ServiceData.class); + log.info("returning cached serviceData.json from {}", jsonFile); + + } catch (Exception e) { log.warn("Error reading serviceData.json from location {}", jsonFile.getAbsolutePath()); - } - localInstance = CodecUtils.fromJson(data, ServiceData.class); - log.info("Returning serviceData.json from {}", jsonFile); - return localInstance; - } else { - + } + } + + if (localInstance == null){ // we are running in an IDE and haven't generated/saved the // serviceData.json yet. try { @@ -201,9 +209,10 @@ static public ServiceData getLocalInstance() { log.error("Unable to generate the serivceData.json file!!"); // This is a fatal issue. I think we should exit the jvm here. } - return localInstance; - } + + return localInstance; + } } @@ -278,16 +287,6 @@ static public Map getOverrides() { return planStore; } - /** - * the set of all categories - */ - TreeMap categoryTypes = new TreeMap(); - - /** - * all services meta data is contained here - */ - TreeMap serviceTypes = new TreeMap(); - public ServiceData() { } @@ -397,7 +396,8 @@ public boolean save() { public boolean save(String filename) { try { - + File dirs = new File(filename).getParentFile(); + dirs.mkdirs(); FileOutputStream fos = new FileOutputStream(filename); String json = CodecUtils.toJson(this); fos.write(json.getBytes()); diff --git a/src/main/java/org/myrobotlab/framework/repo/ServiceDependency.java b/src/main/java/org/myrobotlab/framework/repo/ServiceDependency.java index 56322134d9..d8047f90dd 100644 --- a/src/main/java/org/myrobotlab/framework/repo/ServiceDependency.java +++ b/src/main/java/org/myrobotlab/framework/repo/ServiceDependency.java @@ -142,6 +142,27 @@ public String getKey() { return String.format("%s/%s/%s/%s", groupId, artifactId, version, ext); } + /** + * Gives the Maven coordinates for this dependency, + * which are the group ID, artifact ID, and version + * all separated by colons. + * @return The Maven coordinates for this dependency + */ + public String getCoordinates() { + return String.format("%s:%s:%s", groupId, artifactId, version); + } + + /** + * Gives the unique Maven coordinates of this dependency's project. + * This does not give the version, and is mainly used to determine + * if two dependencies are for the same project. + * + * @return The group ID and the artifact ID, separated by a colon + */ + public String getProjectCoordinates() { + return String.format("%s:%s", groupId, artifactId); + } + public void add(ServiceExclude serviceExclude) { excludes.add(serviceExclude); } diff --git a/src/main/java/org/myrobotlab/gpt/ChatCompletions.java b/src/main/java/org/myrobotlab/gpt/ChatCompletions.java new file mode 100644 index 0000000000..b3c94cb90b --- /dev/null +++ b/src/main/java/org/myrobotlab/gpt/ChatCompletions.java @@ -0,0 +1,12 @@ +package org.myrobotlab.gpt; + +import java.util.ArrayList; + +public class ChatCompletions { + public String id; + public String object; + public int created; + public String model; + public ArrayList choices; + public Usage usage; +} diff --git a/src/main/java/org/myrobotlab/gpt/Choice.java b/src/main/java/org/myrobotlab/gpt/Choice.java new file mode 100644 index 0000000000..d497ee7196 --- /dev/null +++ b/src/main/java/org/myrobotlab/gpt/Choice.java @@ -0,0 +1,9 @@ +package org.myrobotlab.gpt; + +public class Choice { + public int index; + public Message message; + public String text; + public Logprobs logprobs; + public String finish_reason; +} diff --git a/src/main/java/org/myrobotlab/gpt/Completions.java b/src/main/java/org/myrobotlab/gpt/Completions.java new file mode 100644 index 0000000000..7084b9ba5a --- /dev/null +++ b/src/main/java/org/myrobotlab/gpt/Completions.java @@ -0,0 +1,12 @@ +package org.myrobotlab.gpt; + +import java.util.ArrayList; + +public class Completions { + public String id; + public String object; + public int created; + public String model; + public ArrayList choices; + public Usage usage; +} diff --git a/src/main/java/org/myrobotlab/gpt/Logprobs.java b/src/main/java/org/myrobotlab/gpt/Logprobs.java new file mode 100644 index 0000000000..c5e8a98826 --- /dev/null +++ b/src/main/java/org/myrobotlab/gpt/Logprobs.java @@ -0,0 +1,11 @@ +package org.myrobotlab.gpt; + +import java.util.ArrayList; + +public class Logprobs { + public ArrayList tokens; + public ArrayList token_logprobs; + public ArrayList top_logprobs; + public ArrayList text_offset; + +} diff --git a/src/main/java/org/myrobotlab/gpt/Message.java b/src/main/java/org/myrobotlab/gpt/Message.java new file mode 100644 index 0000000000..47146a0fda --- /dev/null +++ b/src/main/java/org/myrobotlab/gpt/Message.java @@ -0,0 +1,6 @@ +package org.myrobotlab.gpt; + +public class Message{ + public String role; + public String content; +} diff --git a/src/main/java/org/myrobotlab/gpt/TopLogprob.java b/src/main/java/org/myrobotlab/gpt/TopLogprob.java new file mode 100644 index 0000000000..f78f91cb26 --- /dev/null +++ b/src/main/java/org/myrobotlab/gpt/TopLogprob.java @@ -0,0 +1,5 @@ +package org.myrobotlab.gpt; + +public class TopLogprob { + +} diff --git a/src/main/java/org/myrobotlab/gpt/Usage.java b/src/main/java/org/myrobotlab/gpt/Usage.java new file mode 100644 index 0000000000..3e30146814 --- /dev/null +++ b/src/main/java/org/myrobotlab/gpt/Usage.java @@ -0,0 +1,7 @@ +package org.myrobotlab.gpt; + +public class Usage{ + public int prompt_tokens; + public int completion_tokens; + public int total_tokens; +} \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/i2c/I2CBus.java b/src/main/java/org/myrobotlab/i2c/I2CBus.java index 48c031d89f..b266f368f1 100644 --- a/src/main/java/org/myrobotlab/i2c/I2CBus.java +++ b/src/main/java/org/myrobotlab/i2c/I2CBus.java @@ -21,7 +21,7 @@ public class I2CBus implements Attachable, I2CBusControl { transient public final static Logger log = LoggerFactory.getLogger(I2CBus.class); String name; - // transient too help prevent infinite recursion in gson + // transient too help prevent infinite recursion in serialization transient I2CBusController controller; public I2CBus(String Name) { diff --git a/src/main/java/org/myrobotlab/i2c/I2CFactory.java b/src/main/java/org/myrobotlab/i2c/I2CFactory.java index 7b69481d38..d5b72c8565 100644 --- a/src/main/java/org/myrobotlab/i2c/I2CFactory.java +++ b/src/main/java/org/myrobotlab/i2c/I2CFactory.java @@ -29,20 +29,6 @@ public class I2CFactory { */ public static I2CBus getInstance(int busNumber) throws IOException { - /* - * String architecture = Platform.getArch(); try { String I2CBusType = - * "org.myrobotlab.i2c.I2CBusProxy"; if - * (architecture.equals(Platform.ARCH_ARM)) { // raspi I2CBusType = - * "com.pi4j.io.i2c.impl.I2CBusImpl"; } - * - * Object[] param = new Object[0]; - * - * Class c; c = Class.forName(I2CBusType); Class[] paramTypes = new - * Class[param.length]; for (int i = 0; i < param.length; ++i) { - * paramTypes[i] = param[i].getClass(); } Constructor mc = - * c.getConstructor(paramTypes); return (I2CBus) mc.newInstance(param); } - * catch (Exception e) { Logging.logException(e); return null; } - */ // pi4j's factory calls the implementation directly // which would not be my first choice - but since it does // I will do the same here... otherwise I'll need to invoke @@ -50,12 +36,7 @@ public static I2CBus getInstance(int busNumber) throws IOException { Platform platform = Platform.getLocalInstance(); if (platform.isArm()) { - // raspi - // TODO: fix this!!! - // log.warn("This probable doesn't work for ARM / RasPI now.. update the - // code!"); - // return I2CBusImpl.getBus(busNumber); - // return null; + try { return provider.getBus(busNumber, DEFAULT_LOCKAQUIRE_TIMEOUT, DEFAULT_LOCKAQUIRE_TIMEOUT_UNITS); } catch (UnsupportedBusNumberException e) { @@ -66,13 +47,6 @@ public static I2CBus getInstance(int busNumber) throws IOException { } else { return I2CProxyImpl.getBus(busNumber); } - - // return I2CBusImpl.getBus(busNumber); - } - - public static void main(String[] args) { - // TODO Auto-generated method stub - } } diff --git a/src/main/java/org/myrobotlab/image/Util.java b/src/main/java/org/myrobotlab/image/Util.java index e0252fcda3..ff79c78387 100644 --- a/src/main/java/org/myrobotlab/image/Util.java +++ b/src/main/java/org/myrobotlab/image/Util.java @@ -44,26 +44,32 @@ import java.awt.image.ColorModel; import java.awt.image.PixelGrabber; import java.awt.image.RescaleOp; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; -import java.util.Base64; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import org.bytedeco.javacpp.FloatPointer; import org.bytedeco.javacpp.IntPointer; +import org.bytedeco.javacv.Java2DFrameConverter; +import org.bytedeco.javacv.OpenCVFrameConverter; +import org.bytedeco.javacv.OpenCVFrameConverter.ToIplImage; +import org.bytedeco.opencv.opencv_core.IplImage; import org.bytedeco.opencv.opencv_core.Mat; import org.bytedeco.opencv.opencv_core.Point2f; import org.bytedeco.opencv.opencv_core.RectVector; import org.bytedeco.opencv.opencv_core.RotatedRect; import org.bytedeco.opencv.opencv_core.Size; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.opencv.CloseableFrameConverter; import org.myrobotlab.opencv.DetectedText; import org.slf4j.Logger; @@ -271,21 +277,11 @@ public static ImageIcon getImageIcon(String path) { * * @return current resource directory */ - @Deprecated + @Deprecated /*Resource references do not belong here - the ServiceType and + perhaps even the ServiceName are needed in order to provide context. This + method should be removed, or parameters provided for ServiceType or + ServiceName */ public static String getResourceDir() { - // first try for the resource.dir system property - /* - * THIS CANNOT BE DONE IN TWO PLACES - ONE WILL ALWAYS BE String resourceDir - * = System.getProperty("resource.dir"); if (resourceDir != null) { // - * log.info("Returning {}", resourceDir); return resourceDir; } if - * (!FileIO.isJar()) { // - * log.info("Not in a jar...you're running in an IDE likely."); resourceDir - * = System.getProperty("user.dir") + File.separator + - * "src"+File.separator+"main"+File.separator+"resources"+File.separator+ - * "resource"; } else { resourceDir = System.getProperty("user.dir") + - * File.separator + "resource"; } - */ - // log.info("Returning {}", resourceDir); return Service.getResourceRoot(); } @@ -453,7 +449,7 @@ public final static String getImageAsString(String filename, String type) { BufferedImage img = ImageIO.read(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ImageIO.write(img, "png", bos); - return String.format("data:image/%s;base64,%s", type, Base64.getEncoder().encodeToString(bos.toByteArray())); + return String.format("data:image/%s;base64,%s", type, CodecUtils.toBase64(bos.toByteArray())); } catch (IOException e) { return null; } @@ -607,4 +603,102 @@ public static FloatPointer arrayListToFloatPointer(ArrayList confidences) return confidencesFV; } + + /** + * deserialize from a png byte array to a base64 encoded string + * for display inline in html. + * + * @param bytes + * input bytes + * @return a string + * @throws IOException + * boom + * + */ + public static String bytesToBase64Jpg(byte[] bytes) { + // + // let's assume we're a buffered image .. those are serializable :) + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + BufferedImage bufImage = ImageIO.read(new ByteArrayInputStream(bytes)); + ImageIO.write(bufImage, "jpg", os); + os.close(); + } catch (IOException e) { + // TODO: we should probably just return null and let the caller figure this out. + return "ERROR converting image to jpg base64."; + } + + String data = String.format("data:image/%s;base64,%s", "jpg", CodecUtils.toBase64(os.toByteArray())); + return data; + } + + /** + * Helper method to serialize an IplImage into a byte array. returns the bytes + * of an image in for format specified, png, jpg, bmp,.etc... + * + * @param image + * input iage + * @param format + * defaults to jpg.. + * @return byte array of image + * @throws IOException + * boom + * + */ + public static byte[] imageToBytes(IplImage image, String format) throws IOException { + + // lets make a buffered image + CloseableFrameConverter converter = new CloseableFrameConverter(); + BufferedImage buffImage = converter.toBufferedImage(image); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + ImageIO.write(buffImage, format, stream); + } catch (IOException e) { + // This *shouldn't* happen with a ByteArrayOutputStream, but if it + // somehow does happen, then we don't want to just ignore it + throw new RuntimeException(e); + } + converter.close(); + return stream.toByteArray(); + } + + /** + * Helper method to serialize an IplImage into a byte array. returns a jpg + * version of the original image + * + * @param image + * input iage + * @return byte array of image + * @throws IOException + * boom + * + */ + public static byte[] imageToBytes(IplImage image) throws IOException { + return Util.imageToBytes(image, "jpg"); + } + + /** + * Uses ImageIO to read the byte array into a buffered image. + * It then converts it to an IplImage + * + * @param bytes + * input bytes + * @return an iplimage + * @throws IOException + * boom + * + */ + public static IplImage bytesToImage(byte[] bytes) throws IOException { + // + // let's assume we're a buffered image .. those are serializable :) + BufferedImage bufImage = ImageIO.read(new ByteArrayInputStream(bytes)); + ToIplImage iplConverter = new OpenCVFrameConverter.ToIplImage(); + Java2DFrameConverter java2dConverter = new Java2DFrameConverter(); + IplImage iplImage = iplConverter.convert(java2dConverter.convert(bufImage)); + // now convert the buffered image to ipl image + return iplImage; + // Again this could be try with resources but the original example was in + // Scala + } + } diff --git a/src/main/java/org/myrobotlab/image/WebImage.java b/src/main/java/org/myrobotlab/image/WebImage.java index fe1eb8dd57..fd4ad4e02f 100644 --- a/src/main/java/org/myrobotlab/image/WebImage.java +++ b/src/main/java/org/myrobotlab/image/WebImage.java @@ -2,13 +2,12 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; -import java.util.Base64; - import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.logging.LoggerFactory; import org.slf4j.Logger; @@ -57,7 +56,7 @@ public WebImage(final BufferedImage img, final String source, Integer frameIndex if (quality == null) { ImageIO.write(img, imgType, os); os.close(); - data = String.format("data:image/%s;base64,%s", type, Base64.getEncoder().encodeToString(os.toByteArray())); + data = String.format("data:image/%s;base64,%s", type,CodecUtils.toBase64(os.toByteArray())); } else { // save jpeg image with specific quality. "1f" corresponds to 100% , @@ -76,7 +75,7 @@ public WebImage(final BufferedImage img, final String source, Integer frameIndex writer.dispose(); os.close(); - data = String.format("data:image/jpeg;base64,%s", Base64.getEncoder().encodeToString(os.toByteArray())); + data = String.format("data:image/jpeg;base64,%s", CodecUtils.toBase64(os.toByteArray())); } } catch (Exception e) { log.error("could not create WebImage", e); diff --git a/src/main/java/org/myrobotlab/io/FileIO.java b/src/main/java/org/myrobotlab/io/FileIO.java index 861fc33c17..80d665e101 100644 --- a/src/main/java/org/myrobotlab/io/FileIO.java +++ b/src/main/java/org/myrobotlab/io/FileIO.java @@ -1574,6 +1574,37 @@ public static String getExt(final String filename) { } return null; } + + /** + * validate a directory exists + * @param dir + * @return + */ + public static boolean checkDir(String dir) { + try { + File check = new File(dir); + return check.exists() && check.isDirectory(); + } catch (Exception e) { + log.error("checkDir threw", e); + } + return false; + } + + /** + * validate a file exists + * @param filename + * @return + */ + public static boolean checkFile(String filename) { + try { + File check = new File(filename); + return check.exists() && !check.isDirectory(); + } catch (Exception e) { + log.error("checkDir threw", e); + } + return false; + } + /** * flips all \ to / or / to \ depending on OS @@ -1594,4 +1625,5 @@ public static String normalize(String dirPath) { } } + } diff --git a/src/main/java/org/myrobotlab/io/StreamGobbler.java b/src/main/java/org/myrobotlab/io/StreamGobbler.java index 1c0c565662..711b4c801b 100644 --- a/src/main/java/org/myrobotlab/io/StreamGobbler.java +++ b/src/main/java/org/myrobotlab/io/StreamGobbler.java @@ -6,11 +6,38 @@ import java.io.InputStreamReader; import java.io.OutputStream; +import org.myrobotlab.logging.LoggerFactory; +import org.slf4j.Logger; +/** + * A general stream gobbler, useful when starting processes and handling their streams + * @author GroG + * + */ public class StreamGobbler extends Thread { protected transient InputStream processOut; protected transient OutputStream processIn; protected String name; + public final static Logger log = LoggerFactory.getLogger(StreamGobbler.class); + /** + * When we do not need to redirect the stream to another stream. E.g. + * when we create a process, and are already redirecting std:out and std:err + * + * @param name + * @param processOut + */ + public StreamGobbler(String name, InputStream processOut) { + this(name, processOut, null); + } + + /** + * This is a general stream gobbler that will consume and input stream and move + * the data to an output stream. + * + * @param name + * @param processOut + * @param processIn + */ public StreamGobbler(String name, InputStream processOut, OutputStream processIn) { super(name); this.processOut = processOut; @@ -24,14 +51,14 @@ public void run() { InputStreamReader isr = new InputStreamReader(processOut); BufferedReader br = new BufferedReader(isr); String line = null; - while ((line = br.readLine()) != null) - if (line != null) { + while ((line = br.readLine()) != null) { + log.info("gobbler - {}", line); + if (line != null && processIn != null) { processIn.write(String.format("%s\n", line).getBytes()); processIn.flush(); - } + }} } catch (IOException ioe) { - System.out.println("gobbler leaving"); - ioe.printStackTrace(); + log.info("{} gobbler leaving", name); } } } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/io/Zip.java b/src/main/java/org/myrobotlab/io/Zip.java index 56968ccc83..d41924ebda 100644 --- a/src/main/java/org/myrobotlab/io/Zip.java +++ b/src/main/java/org/myrobotlab/io/Zip.java @@ -299,28 +299,26 @@ static public void zipDirectory(File folder, String parentFolder, ZipOutputStrea } zos.putNextEntry(new ZipEntry(parentFolder + "/" + file.getName())); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); - long bytesRead = 0; byte[] bytesIn = new byte[BUFFER_SIZE]; int read = 0; while ((read = bis.read(bytesIn)) != -1) { zos.write(bytesIn, 0, read); - bytesRead += read; } zos.closeEntry(); + bis.close(); } } static public void zipFile(File file, ZipOutputStream zos) throws FileNotFoundException, IOException { zos.putNextEntry(new ZipEntry(file.getName())); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); - long bytesRead = 0; byte[] bytesIn = new byte[BUFFER_SIZE]; int read = 0; while ((read = bis.read(bytesIn)) != -1) { zos.write(bytesIn, 0, read); - bytesRead += read; } zos.closeEntry(); + bis.close(); } } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/kinematics/Gesture.java b/src/main/java/org/myrobotlab/kinematics/Gesture.java new file mode 100644 index 0000000000..bea0aae688 --- /dev/null +++ b/src/main/java/org/myrobotlab/kinematics/Gesture.java @@ -0,0 +1,34 @@ +package org.myrobotlab.kinematics; + +import java.util.ArrayList; +import java.util.List; + +/** represent a set of servo positions at a given point in time */ +public class Gesture { + + /** + * sequence of poses and offset times + */ + protected List parts = new ArrayList<>(); + + protected boolean repeat = false; + + public List getParts(){ + return parts; + } + + public boolean getRepeat() { + return repeat; + } + + public void setParts(List parts){ + this.parts = parts; + } + + public void setRepeat(boolean repeat) { + this.repeat = repeat; + } + + + +} diff --git a/src/main/java/org/myrobotlab/kinematics/GesturePart.java b/src/main/java/org/myrobotlab/kinematics/GesturePart.java new file mode 100644 index 0000000000..232d0f6b59 --- /dev/null +++ b/src/main/java/org/myrobotlab/kinematics/GesturePart.java @@ -0,0 +1,31 @@ +package org.myrobotlab.kinematics; + +public class GesturePart { + + /** + * name of pose + */ + public String name; + + /** + * type determines how to handle the value + * depending on what is desired ... + */ + public String type; // Pose | Text | Delay | Message + + /** + * delay type when type is Delay, String when type is Text + */ + public Object value; + + /** + * if blocking true will wait until sequence part finished + */ + public boolean blocking = false; + + + @Override + public String toString() { + return String.format("part %s %s %s", name, type, (value != null)?value.toString():null); + } +} diff --git a/src/main/java/org/myrobotlab/kinematics/Pose.java b/src/main/java/org/myrobotlab/kinematics/Pose.java index 185d55852f..1b38479aed 100755 --- a/src/main/java/org/myrobotlab/kinematics/Pose.java +++ b/src/main/java/org/myrobotlab/kinematics/Pose.java @@ -1,89 +1,37 @@ package org.myrobotlab.kinematics; +import java.util.Map; +import java.util.TreeMap; +import java.util.Objects; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.io.FileIO; -import org.myrobotlab.service.interfaces.ServoControl; - -/** represent a set of servo positions at a given point in time */ public class Pose { - public String name; - public HashMap positions = new HashMap(); - public HashMap speeds = new HashMap(); + protected Map moves = new TreeMap<>(); - public Pose(String name, List servos) { - this.name = name; - List servoNames = new ArrayList(); - for (ServoControl sc : servos) { - positions.put(sc.getName(), sc.getCurrentInputPos()); - speeds.put(sc.getName(), sc.getSpeed()); - servoNames.add(sc.getName()); + public Map getMoves() { + return moves; } - } - - public HashMap getSpeeds() { - return speeds; - } - - public HashMap getPositions() { - return positions; - } - - public void savePose(String filename) throws IOException { - String s = CodecUtils.toPrettyJson(this); - FileOutputStream out = new FileOutputStream(new File(filename)); - out.write(s.getBytes()); - out.close(); - } - - public static Pose loadPose(String filename) throws IOException { - String json = FileIO.toString(filename); - Pose pose = CodecUtils.fromJson(json, Pose.class); - return pose; - } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((positions == null) ? 0 : positions.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Pose other = (Pose) obj; + public void setMoves(Map moves) { + this.moves = moves; + } - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (positions == null) { - if (other.positions != null) - return false; - } else if (!positions.equals(other.positions)) - return false; - return true; - } + @Override + public String toString() { + return "Pose{" + + "moves=" + moves + + '}'; + } - @Override - public String toString() { - return "Pose [positions=" + positions + ", speeds=\" + speeds + \", name=" + name + "]"; - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pose pose = (Pose) o; + return Objects.equals(moves, pose.moves); + } + @Override + public int hashCode() { + return Objects.hash(moves); + } } diff --git a/src/main/java/org/myrobotlab/kinematics/PoseMove.java b/src/main/java/org/myrobotlab/kinematics/PoseMove.java new file mode 100644 index 0000000000..503dbd8d2f --- /dev/null +++ b/src/main/java/org/myrobotlab/kinematics/PoseMove.java @@ -0,0 +1,31 @@ +package org.myrobotlab.kinematics; + +/** + * A move to a position at a given speed. If the position of null, its + * still possible to change the speed. If the speed is null, its possible + * to use the "current" speed, whatever that is. + * + * @author GroG + * + */ +public class PoseMove { + + public PoseMove() { + } + + public PoseMove(double position, Double speed) { + this.position = position; + this.speed = speed; + } + + /** + * frame position + */ + public Double position; + + /** + * frame speed + */ + public Double speed; + +} diff --git a/src/main/java/org/myrobotlab/kinematics/PoseSequence.java b/src/main/java/org/myrobotlab/kinematics/PoseSequence.java deleted file mode 100644 index f7b6be7664..0000000000 --- a/src/main/java/org/myrobotlab/kinematics/PoseSequence.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.myrobotlab.kinematics; - -public class PoseSequence { - - public int id; - - /** - * name of pose - */ - public String name; - - /** - * number of ms to wait before starting this pose - */ - public Long waitTimeMs; - - public PoseSequence() { - } - - @Override - public String toString() { - if (waitTimeMs == null) { - return name; - } else { - return String.format("%s %d ms"); - } - } -} diff --git a/src/main/java/org/myrobotlab/kinematics/Sequence.java b/src/main/java/org/myrobotlab/kinematics/Sequence.java deleted file mode 100644 index c20d7f0543..0000000000 --- a/src/main/java/org/myrobotlab/kinematics/Sequence.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.myrobotlab.kinematics; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.io.FileIO; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.service.InMoov2; -import org.slf4j.Logger; - -/** represent a set of servo positions at a given point in time */ -public class Sequence { - - public final static Logger log = LoggerFactory.getLogger(InMoov2.class); - - public String name; - - /** - * sequence of poses and offset times - */ - public List poses = new ArrayList<>(); - - public boolean cycle = false; - - public Sequence() { - } - - public Sequence(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Sequence: " + name; - } - - public static Sequence loadSequence(String filename) throws IOException { - String json = FileIO.toString(filename); - Sequence pose = CodecUtils.fromJson(json, Sequence.class); - return pose; - } - -} diff --git a/src/main/java/org/myrobotlab/net/WsClient.java b/src/main/java/org/myrobotlab/net/WsClient.java index 6653869f94..8ea7c0ea2f 100644 --- a/src/main/java/org/myrobotlab/net/WsClient.java +++ b/src/main/java/org/myrobotlab/net/WsClient.java @@ -27,7 +27,9 @@ public class WsClient extends WebSocketListener { public final static Logger log = LoggerFactory.getLogger(WsClient.class); - transient private OkHttpClient client = null; + private final transient OkHttpClient client = new OkHttpClient.Builder() + .readTimeout(60000, TimeUnit.MILLISECONDS) + .build(); /** * service if it exists */ @@ -38,14 +40,14 @@ public class WsClient extends WebSocketListener { /** * unique identifier for this client */ - protected String uuid = null; + protected String uuid = java.util.UUID.randomUUID().toString(); /** * callback handler if it exists */ transient private RemoteMessageHandler handler = null; public WsClient() { - uuid = java.util.UUID.randomUUID().toString(); + // Best to keep the default constructor for explicitness } /** @@ -87,7 +89,6 @@ public void connect(Object si, String url) { listener = (ConnectionEventListener) si; } - client = new OkHttpClient.Builder().readTimeout(60000, TimeUnit.MILLISECONDS).build(); Request request = new Request.Builder().url(url).build(); socket = client.newWebSocket(request, this); @@ -121,6 +122,8 @@ public void send(ByteString bytes) { socket.send(bytes); } + // FIXME Need to add @NonNull to overriden method params once we standardize on an annotation lib + @Override public void onOpen(WebSocket webSocket, Response response) { log.info("ONOPEN: "); @@ -136,6 +139,11 @@ public void onMessage(WebSocket webSocket, String text) { log.debug(String.format("MESSAGE: %s", text)); } if (handler != null) { + if ("X".equals(text)) { + // ignore Atmosphere does a weird sending of X characters I assume + // to make sure the connection is unbroken + return; + } handler.onRemoteMessage(uuid, text); } } diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVData.java b/src/main/java/org/myrobotlab/opencv/OpenCVData.java index a581a77a9b..3449ec6350 100644 --- a/src/main/java/org/myrobotlab/opencv/OpenCVData.java +++ b/src/main/java/org/myrobotlab/opencv/OpenCVData.java @@ -27,7 +27,7 @@ import org.bytedeco.javacv.FrameGrabber; import org.bytedeco.opencv.opencv_core.IplImage; import org.bytedeco.opencv.opencv_core.Mat; -import org.myrobotlab.cv.CvData; +import org.myrobotlab.cv.CVData; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.math.geometry.Point2df; @@ -81,7 +81,7 @@ * @author GroG * */ -public class OpenCVData extends CvData { +public class OpenCVData extends CVData { public final static Logger log = LoggerFactory.getLogger(OpenCVData.class); private static final long serialVersionUID = 1L; @@ -542,7 +542,7 @@ public void writeAll() { @Override public List getPointCloudList() { - return (List) sources.get(CvData.POINT_CLOUDS); + return (List) sources.get(CVData.POINT_CLOUDS); } @Override @@ -560,12 +560,12 @@ public Set getKeySet() { } public void put(PointCloud pc) { - List pcs = (List) sources.get(CvData.POINT_CLOUDS); + List pcs = (List) sources.get(CVData.POINT_CLOUDS); if (pcs == null) { pcs = new ArrayList(); } pcs.add(pc); - sources.put(CvData.POINT_CLOUDS, pcs); + sources.put(CVData.POINT_CLOUDS, pcs); } public ArrayList getDetectedText() { diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java index b43895d4eb..ac90bae143 100644 --- a/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java +++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java @@ -46,7 +46,7 @@ import org.bytedeco.javacv.CanvasFrame; import org.bytedeco.opencv.opencv_core.IplImage; import org.bytedeco.opencv.opencv_core.Mat; -import org.myrobotlab.cv.CvFilter; +import org.myrobotlab.cv.CVFilter; import org.myrobotlab.document.Classification; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.LoggerFactory; @@ -54,7 +54,7 @@ import org.myrobotlab.service.OpenCV; import org.slf4j.Logger; -public abstract class OpenCVFilter implements Serializable, CvFilter { +public abstract class OpenCVFilter implements Serializable, CVFilter { public final static Logger log = LoggerFactory.getLogger(OpenCVFilter.class.toString()); private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterFaceRecognizer.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterFaceRecognizer.java index b088344287..0af26fc0fd 100644 --- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterFaceRecognizer.java +++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterFaceRecognizer.java @@ -81,6 +81,7 @@ public class OpenCVFilterFaceRecognizer extends OpenCVFilter { transient private FaceRecognizer faceRecognizer; private boolean trained = false; // the directory to store the training images. + // FIXME - this is the wrong place it should be data/OpenCV/training private String trainingDir = "training"; private int modelSizeX = 256; private int modelSizeY = 256; diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterKinectPointCloud.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterKinectPointCloud.java deleted file mode 100644 index c3906f34da..0000000000 --- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterKinectPointCloud.java +++ /dev/null @@ -1,310 +0,0 @@ -/** - * - * @author grog (at) myrobotlab.org - * - * This file is part of MyRobotLab (http://myrobotlab.org). - * - * MyRobotLab is free software: you can redistribute it and/or modify - * it under the terms of the Apache License 2.0 as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version (subject to the "Classpath" exception - * as provided in the LICENSE.txt file that accompanied this code). - * - * MyRobotLab is distributed in the hope that it will be useful or fun, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Apache License 2.0 for more details. - * - * All libraries in thirdParty bundle are subject to their own license - * requirements - please refer to http://myrobotlab.org/libraries for - * details. - * - * Enjoy ! - * - * */ - -package org.myrobotlab.opencv; - -import static org.bytedeco.opencv.global.opencv_core.IPL_DEPTH_8U; - -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.io.File; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.math3.geometry.euclidean.threed.SphericalCoordinates; -import org.bytedeco.javacpp.indexer.UByteIndexer; -import org.bytedeco.javacpp.indexer.UShortRawIndexer; -import org.bytedeco.javacv.Parallel; -import org.bytedeco.opencv.opencv_core.AbstractIplImage; -import org.bytedeco.opencv.opencv_core.IplImage; -import org.myrobotlab.framework.Service; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.math.geometry.Point; -import org.myrobotlab.math.geometry.Point3df; -import org.myrobotlab.math.geometry.PointCloud; -import org.myrobotlab.service.BoofCv; -import org.myrobotlab.service.JMonkeyEngine; -import org.myrobotlab.service.OpenCV; -import org.myrobotlab.service.Runtime; -import org.slf4j.Logger; - -import boofcv.alg.distort.radtan.RemoveRadialPtoN_F64; -import boofcv.io.calibration.CalibrationIO; -import boofcv.struct.calib.CameraPinholeRadial; -import georegression.struct.point.Point2D_F64; - -/** - *
- * 
- * @author GroG
- * 
- * references :
- *  http://blog.elonliu.com/2017/03/18/kinect-coordinate-mapping-summary-and-pitfalls/
- *  https://smeenk.com/kinect-field-of-view-comparison/
- *  https://stackoverflow.com/questions/17832238/kinect-intrinsic-parameters-from-field-of-view/18199938#18199938
- *  https://answers.ros.org/question/195034/coordinates-of-a-specific-pixel-in-depthimage-published-by-kinect/
- * 
- * 
- */ -public class OpenCVFilterKinectPointCloud extends OpenCVFilter { - - // useful data for the kinect is 632 X 480 - 8 pixels on the right edge are - // not good data - // http://groups.google.com/group/openkinect/browse_thread/thread/6539281cf451ae9e?pli=1 - - public final static Logger log = LoggerFactory.getLogger(OpenCVFilterKinectPointCloud.class); - - private static final long serialVersionUID = 1L; - - public boolean clearPoints = false; - - transient IplImage lastDepth = null; - - IplImage returnImage = null; - - /** - * list of samplepoint to return depth - */ - List samplePoints = new ArrayList<>(); - - PointCloud pointCloud = null; - - int colors[] = null; - - SphericalCoordinates transformer = null; - - boolean clearSamplePoints = false; - - Point3df cameraLocation = new Point3df(); - // pitch yaw heading necessary ? - or heading suffice ? - float cameraHeading = 0; - float cameraTilt = 0;// degrees ? - double r, theta, phi; - Point3df[] depthBuffer; - float[] colorBuffer; - IplImage color; - // double focalLength = h / 2 * Math.tan((43 * 0.0174533)/2); - // BoofCv - RemoveRadialPtoN_F64 p2n = null; - - int width, height = 0; - - public OpenCVFilterKinectPointCloud(String name) { - super(name); - - String baseDir = Service.getResourceDir(BoofCv.class); - String nameCalib = "intrinsic.yaml"; - - CameraPinholeRadial param = CalibrationIO.load(new File(baseDir, nameCalib)); - p2n = new RemoveRadialPtoN_F64(); - p2n.setK(param.fx, param.fy, param.skew, param.cx, param.cy).setDistortion(param.radial, param.t1, param.t2); - - // http://myrobotlab.org/content/useful-kinect-info - // vertical focal length - theta = 43 * 0.017453; - phi = 57 * 0.017453; - r = 1; - } - - @Override - public void imageChanged(IplImage image) { - width = image.width(); - height = image.height(); - } - - @Override - public IplImage process(IplImage depth) throws InterruptedException { - - if (depth.depth() != 16 && depth.nChannels() != 1) { - log.error("not valid kinect depth image expecting 1 channel 16 depth got {} channel {} depth", depth.depth(), depth.nChannels()); - return depth; - } - - if (transformer == null) { - transformer = new SphericalCoordinates(r, theta, phi); - } - - lastDepth = depth; - - if (clearSamplePoints) { - samplePoints.clear(); - clearSamplePoints = false; - } - - if (color == null) { - color = AbstractIplImage.create(depth.width(), depth.height(), IPL_DEPTH_8U, 3); - } - - final UShortRawIndexer depthIdx = (UShortRawIndexer) depth.createIndexer(); - final UByteIndexer colorIdx = color.createIndexer(); - - // f = camera focal length - // xv = x viewport - // yv = y viewport - - // w = screen width - // h = screen height - - // xw = x world coordinate - // xy = y world coordinate - // zy = z world coordinate - - // buffer = FloatBuffer.allocate(depth.width() * depth.height()); - depthBuffer = new Point3df[width * height]; - if (colorBuffer == null) { - colorBuffer = new float[width * height * 4]; // RGBA - } - - /** - *
-     * for (int i = 0; i < color.width() * color.height(); ++i) {
-     *   colorBuffer[i] = 0.5f;
-     *   colorBuffer[i + 1] = 0.5f;
-     *   colorBuffer[i + 2] = i % 100 * .999f;
-     *   colorBuffer[i + 3] = 1f;
-     * }
-     * 
- */ - - Point2D_F64 n = new Point2D_F64(); - - Parallel.loop(0, height, new Parallel.Looper() { - @Override - public void loop(int from, int to, int looperID) { - for (int xv = from; xv < to; xv++) { - for (int yv = 0; yv < width; yv++) { - - // FIXME - find correct scale for x & y in mm - - double xw, yw, zw = 0; - float depth = depthIdx.get(xv, yv); - - // zw = D * f / sqrt(xv² + yv² + f²) - // https://hub.jmonkeyengine.org/t/point-cloud-visualization/25838 - - int index = yv * height + xv; - // colorIdx.put(index, 33); - // colorIdx.put(index+1, 33); - - /** - *
-             * BoofCv way p2n.compute(xv,yv,n); Point3D_F64 p = new
-             * Point3D_F64(); p.z = depth; p.x = n.x*p.z; p.y = n.y*p.z;
-             * 
-             * float scaled = depth/1000;
-             * 
-             * yw = n.y*scaled; xw = n.x*scaled; zw = scaled; // really ?
-             */
-
-            zw = -1 * depth / 1000; // we want in 1 meter world unit
-            xw = 2 * (xv - 639 / 2) * Math.tan(57 / 2 * 0.0174533) * (zw / 640);
-            yw = 2 * (479 - yv - 479 / 2) * Math.tan(43 / 2 * 0.0174533) * (zw / 480);
-
-            // points[index] = new Point3df((float)xw,(float)yw,(float)zw);
-            // jmonkey has a flipped y/x
-            depthBuffer[index] = new Point3df((float) yw, (float) xw, (float) zw);
-            if (color != null) {
-
-            }
-          }
-        }
-      }
-    });
-
-    pointCloud = new PointCloud(depthBuffer);
-    pointCloud.setColors(colorBuffer);
-
-    // NO MORE PUBLISHING - just put into OpenCVData !!!
-    // publishPointCloud(pointCloud);
-
-    put(pointCloud);
-
-    depthIdx.release();
-    colorIdx.release();
-
-    return depth;
-  }
-
-  public void publishPlane() {
-    // spin through sample points
-    for (int i = 0; i < samplePoints.size(); ++i) {
-
-    }
-  }
-
-  @Override
-  public BufferedImage processDisplay(Graphics2D graphics, BufferedImage image) {
-    if (lastDepth == null) {
-      return image;
-    }
-    ByteBuffer buffer = lastDepth.getByteBuffer();
-    for (Point point : samplePoints) {
-
-      int depthBytesPerChannel = lastDepth.depth() / 8;
-      int depthIndex = point.y * lastDepth.widthStep() + point.x * lastDepth.nChannels() * depthBytesPerChannel;
-
-      String str = String.format("(%d,%d) %d", point.x, point.y, (buffer.get(depthIndex + 1) & 0xFF) << 8 | (buffer.get(depthIndex) & 0xFF));
-      graphics.drawString(str, point.x + 3, point.y);
-      graphics.drawOval(point.x, point.y, 2, 2);
-    }
-    return image;
-  }
-
-  @Override
-  public void samplePoint(Integer x, Integer y) {
-    samplePoints.add(new Point(x, y));
-  }
-
-  public void clearSamplePoints() {
-    clearSamplePoints = true;
-  }
-
-  public static void main(String[] args) {
-    try {
-      LoggingFactory.init("info");
-      Runtime.start("gui", "SwingGui");
-      JMonkeyEngine jme = (JMonkeyEngine) Runtime.start("jme", "JMonkeyEngine");
-      OpenCV cv = (OpenCV) Runtime.start("cv", "OpenCV");
-
-      jme.attach(cv);
-      // jme.subscribe(cv.getName(), "publishPointCloud");
-      Service.sleep(3000); // FIXME - fix race condition...
-
-      // kinect data
-      // cv.capture("../1543648225287");
-      cv.capture("../00000004.png");
-      // OpenCVFilterKinectPointCloud floor =
-      // (OpenCVFilterKinectPointCloud)cv.addFilter("floor","KinectFloorFinder");
-      OpenCVFilterKinectPointCloud cloud = (OpenCVFilterKinectPointCloud) cv.addFilter("cloud", "KinectPointCloud");
-
-    } catch (Exception e) {
-      log.error("main threw", e);
-    }
-
-  }
-
-}
diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterSetImageROI.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterSetImageROI.java
index fd22d58104..d97788b9ab 100644
--- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterSetImageROI.java
+++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterSetImageROI.java
@@ -38,7 +38,7 @@
 public class OpenCVFilterSetImageROI extends OpenCVFilter {
 
   private static final long serialVersionUID = 1L;
-  CvRect rect = null;
+  private transient CvRect rect = null;
   public final static Logger log = LoggerFactory.getLogger(OpenCVFilterSetImageROI.class);
 
   public OpenCVFilterSetImageROI(String name) {
diff --git a/src/main/java/org/myrobotlab/process/InProcessCli.java b/src/main/java/org/myrobotlab/process/InProcessCli.java
index eeccbf153f..0556c25222 100644
--- a/src/main/java/org/myrobotlab/process/InProcessCli.java
+++ b/src/main/java/org/myrobotlab/process/InProcessCli.java
@@ -154,7 +154,7 @@ public void run() {
           continue;
         }
 
-        log.info("c = {}", c);
+        log.debug("c = {}", c);
         // != 0x04 /* ctrl-d 0x04 ctrl-c 0x03 '\n' */
 
         readLine += (char) c;
@@ -304,18 +304,8 @@ public void process(String srcFullName, String cmd) {
         return;
       }
 
-      // subscribe - setup subscription
-      // MRLListener listener = new MRLListener(cliMsg.method, name + '@' + id,
-      // CodecUtils.getCallbackTopicName(cliMsg.method));
-      // Message subscription = Message.createMessage(name + '@' + id,
-      // cliMsg.getFullName(), "addListener", listener);
-
       String cliFullName = name + '@' + id;
 
-      /*
-       * if (srcFullName == null) { srcFullName = name + '@' + id; }
-       */
-
       // setup cli subscription
       MRLListener listener = new MRLListener(cliMsg.method, cliFullName, CodecUtils.getCallbackTopicName(cliMsg.method));
       Message subscription = Message.createMessage(cliFullName, cliMsg.getFullName(), "addListener", listener);
@@ -348,7 +338,12 @@ public void process(String srcFullName, String cmd) {
    * @return message
    */
   public Message cliToMsg(String data) {
-    return CodecUtils.cliToMsg(contextPath, "runtime@" + id, "runtime@" + remoteId, data);
+
+    if (contextPath != null) {
+      data = contextPath + data;
+    }
+    Message msg = CodecUtils.pathToMsg("runtime@" + id, data);
+    return CodecUtils.decodeMessageParams(msg);
   }
 
   public void writeToJson(Object o) {
diff --git a/src/main/java/org/myrobotlab/programab/Session.java b/src/main/java/org/myrobotlab/programab/Session.java
index f222ea76d1..a234310e57 100644
--- a/src/main/java/org/myrobotlab/programab/Session.java
+++ b/src/main/java/org/myrobotlab/programab/Session.java
@@ -14,6 +14,7 @@
 import org.myrobotlab.io.FileIO;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.ProgramAB;
+import org.myrobotlab.service.config.ProgramABConfig;
 import org.slf4j.Logger;
 
 /**
@@ -82,6 +83,11 @@ private synchronized Chat getChat() {
         predicatesFile = userPredicates;
         chat.predicates.getPredicateDefaults(userPredicates.getAbsolutePath());
       }
+      
+      ProgramABConfig config = (ProgramABConfig)programab.getConfig();
+      if (config.startTopic != null){
+        chat.predicates.put("topic", config.startTopic);
+      }
     }
     predicates = chat.predicates;
     return chat;
@@ -93,10 +99,9 @@ public void savePredicates() {
     sort.addAll(getChat().predicates.keySet());
     for (String predicate : sort) {
       String value = getChat().predicates.get(predicate);
-      if (predicate.equals("test")) {
-        log.info("here");
+      if (!predicate.startsWith("cfg_")) {
+        sb.append(predicate + ":" + value + "\n");
       }
-      sb.append(predicate + ":" + value + "\n");
     }
     File predicates = new File(FileIO.gluePaths(botInfo.path.getAbsolutePath(), String.format("config/%s.predicates.txt", userName)));
     predicates.getParentFile().mkdirs();
diff --git a/src/main/java/org/myrobotlab/service/Adafruit16CServoDriver.java b/src/main/java/org/myrobotlab/service/Adafruit16CServoDriver.java
index 8fc649ea18..290e3fd8b7 100644
--- a/src/main/java/org/myrobotlab/service/Adafruit16CServoDriver.java
+++ b/src/main/java/org/myrobotlab/service/Adafruit16CServoDriver.java
@@ -24,7 +24,6 @@
 import org.myrobotlab.math.MapperLinear;
 import org.myrobotlab.math.interfaces.Mapper;
 import org.myrobotlab.service.config.Adafruit16CServoDriverConfig;
-import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.data.ServoMove;
 import org.myrobotlab.service.data.ServoSpeed;
 import org.myrobotlab.service.interfaces.I2CControl;
@@ -45,7 +44,7 @@
  *         https://learn.adafruit.com/16-channel-pwm-servo-driver
  */
 @Ignore
-public class Adafruit16CServoDriver extends Service implements I2CControl, ServoController,
+public class Adafruit16CServoDriver extends Service implements I2CControl, ServoController,
     MotorController /* , ServoStatusPublisher */ {
 
   /**
@@ -1024,8 +1023,8 @@ public String publishServoStopped(String name) {
   }
   
   @Override
-  public ServiceConfig getConfig() {
-
+  public Adafruit16CServoDriverConfig getConfig() {
+    super.getConfig();
     Adafruit16CServoDriverConfig config = (Adafruit16CServoDriverConfig)super.getConfig();
     // FIXME remove member vars use config directly
     config.controller = controllerName;
@@ -1035,8 +1034,8 @@ public ServiceConfig getConfig() {
   }
   
   @Override
-  public ServiceConfig apply(ServiceConfig c) {
-    Adafruit16CServoDriverConfig config = (Adafruit16CServoDriverConfig)super.apply(c);
+  public Adafruit16CServoDriverConfig apply(Adafruit16CServoDriverConfig c) {
+    super.apply(c);
     if (config.controller != null) {
       try {
         attach(config.controller);
diff --git a/src/main/java/org/myrobotlab/service/AdafruitIna219.java b/src/main/java/org/myrobotlab/service/AdafruitIna219.java
index 43c058d248..328c5e300b 100644
--- a/src/main/java/org/myrobotlab/service/AdafruitIna219.java
+++ b/src/main/java/org/myrobotlab/service/AdafruitIna219.java
@@ -12,6 +12,7 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.I2CControl;
 import org.myrobotlab.service.interfaces.I2CController;
 import org.myrobotlab.service.interfaces.VoltageSensorControl;
@@ -25,7 +26,7 @@
  * 
  *         References : https://www.adafruit.com/products/904
  */
-public class AdafruitIna219 extends Service implements I2CControl, VoltageSensorControl {
+public class AdafruitIna219 extends Service implements I2CControl, VoltageSensorControl {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java b/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
index 4d3c47c955..a089498d4f 100644
--- a/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
+++ b/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
@@ -19,6 +19,7 @@
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.abstracts.AbstractMotorController;
+import org.myrobotlab.service.config.MotorConfig;
 import org.myrobotlab.service.interfaces.I2CControl;
 import org.myrobotlab.service.interfaces.I2CController;
 import org.myrobotlab.service.interfaces.MotorControl;
@@ -33,7 +34,7 @@
  *         https://learn.adafruit.com/adafruit-dc-and-stepper-motor-hat-for-raspberry-pi/overview
  */
 
-public class AdafruitMotorHat4Pi extends AbstractMotorController implements I2CControl {
+public class AdafruitMotorHat4Pi extends AbstractMotorController implements I2CControl {
 
   /** version of the library */
   static public final String VERSION = "0.9";
diff --git a/src/main/java/org/myrobotlab/service/Ads1115.java b/src/main/java/org/myrobotlab/service/Ads1115.java
index 429538e142..1594f287bd 100644
--- a/src/main/java/org/myrobotlab/service/Ads1115.java
+++ b/src/main/java/org/myrobotlab/service/Ads1115.java
@@ -66,7 +66,7 @@
  *         EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-public class Ads1115 extends Service implements I2CControl, PinArrayControl {
+public class Ads1115 extends Service implements I2CControl, PinArrayControl {
   /**
    * Publisher - Publishes pin data at a regular interval
    * 
@@ -1165,8 +1165,8 @@ public String getAddress() {
   }
 
   @Override
-  public ServiceConfig getConfig() {
-    Ads1115Config config = (Ads1115Config)super.getConfig();
+  public Ads1115Config getConfig() {
+    super.getConfig();
     // FIXME remove member variables - use config only
     config.bus = deviceBus;
     config.address = deviceAddress;
@@ -1175,8 +1175,8 @@ public ServiceConfig getConfig() {
   }
 
   @Override
-  public ServiceConfig apply(ServiceConfig c) {
-    Ads1115Config config = (Ads1115Config) super.apply(c);
+  public Ads1115Config apply(Ads1115Config c) {
+    super.apply(c);
     deviceBus = config.bus;
     deviceAddress = config.address;
     if (config.controller != null) {
diff --git a/src/main/java/org/myrobotlab/service/Amt203Encoder.java b/src/main/java/org/myrobotlab/service/Amt203Encoder.java
index c4a22b9d0b..2c76aa21a1 100755
--- a/src/main/java/org/myrobotlab/service/Amt203Encoder.java
+++ b/src/main/java/org/myrobotlab/service/Amt203Encoder.java
@@ -2,6 +2,7 @@
 
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.abstracts.AbstractPinEncoder;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.EncoderControl;
 
 /**
@@ -22,7 +23,7 @@
  * @author kwatters
  *
  */
-public class Amt203Encoder extends AbstractPinEncoder implements EncoderControl {
+public class Amt203Encoder extends AbstractPinEncoder implements EncoderControl {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Android.java b/src/main/java/org/myrobotlab/service/Android.java
index 6ac23de8a3..3334088e75 100644
--- a/src/main/java/org/myrobotlab/service/Android.java
+++ b/src/main/java/org/myrobotlab/service/Android.java
@@ -5,9 +5,10 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
-public class Android extends Service {
+public class Android extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Arduino.java b/src/main/java/org/myrobotlab/service/Arduino.java
index 3040baca3d..89e407c18d 100644
--- a/src/main/java/org/myrobotlab/service/Arduino.java
+++ b/src/main/java/org/myrobotlab/service/Arduino.java
@@ -8,7 +8,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Base64;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -23,6 +22,7 @@
 import org.myrobotlab.arduino.BoardType;
 import org.myrobotlab.arduino.DeviceSummary;
 import org.myrobotlab.arduino.Msg;
+import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.framework.interfaces.NameProvider;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
@@ -38,7 +38,6 @@
 import org.myrobotlab.sensor.EncoderData;
 import org.myrobotlab.service.abstracts.AbstractMicrocontroller;
 import org.myrobotlab.service.config.ArduinoConfig;
-import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.data.DeviceMapping;
 import org.myrobotlab.service.data.PinData;
 import org.myrobotlab.service.data.SerialRelayData;
@@ -71,7 +70,7 @@
 import org.myrobotlab.service.interfaces.UltrasonicSensorController;
 import org.slf4j.Logger;
 
-public class Arduino extends AbstractMicrocontroller implements I2CBusController, I2CController, SerialDataListener, ServoController, MotorController, NeoPixelController,
+public class Arduino extends AbstractMicrocontroller implements I2CBusController, I2CController, SerialDataListener, ServoController, MotorController, NeoPixelController,
     UltrasonicSensorController, PortConnector, RecordControl, PortListener, PortPublisher, EncoderController, PinArrayPublisher, MrlCommPublisher, ServoStatusPublisher {
 
   transient public final static Logger log = LoggerFactory.getLogger(Arduino.class);
@@ -1495,6 +1494,8 @@ public synchronized void onConnect(String portName) {
     info("%s connected to %s", getName(), portName);
     // chained...
     invoke("publishConnect", portName);
+    
+    broadcastState();
   }
 
   public void onCustomMsg(Integer ax, Integer ay, Integer az) {
@@ -1511,7 +1512,7 @@ public void onDisconnect(String portName) {
   }
 
   public String getBase64ZippedMrlComm() {
-    return Base64.getEncoder().encodeToString((getZippedMrlComm()));
+    return CodecUtils.toBase64(getZippedMrlComm());
   }
 
   public byte[] getZippedMrlComm() {
@@ -1968,10 +1969,13 @@ public void onServoMoveTo(ServoMove move) {
   // > servoSetVelocity/deviceId/b16 velocity
   public void onServoSetSpeed(ServoSpeed servoSpeed) {
 
-    // FIXME - FIND OTHER FUNCTIONS THAT CANNOT BE SET WHEN NOT CONNECTED
-    // AND HANDLE THE SAME AS BELOW !!!
+    if (servoSpeed == null) {
+      log.warn("servo speed cannot be null");
+      return;
+    }
+    
     if (!isConnected()) {
-      warn("Arduino cannot set speed when not connected - connected %b msg %b", isConnected());
+      log.info("Arduino cannot set speed of %s when not connected", servoSpeed.name);
       return;
     }
 
@@ -2316,19 +2320,19 @@ public void neoPixelClear(String neopixel) {
   }
 
   @Override
-  public ServiceConfig getConfig() {
-    ArduinoConfig c = (ArduinoConfig) super.getConfig();
+  public ArduinoConfig getConfig() {
+    super.getConfig();
 
     // FIXME "port" shouldn't exist only config.port !
-    c.port = port;
-    c.connect = isConnected();
+    config.port = port;
+    config.connect = isConnected();
 
-    return c;
+    return config;
   }
 
   @Override
-  public ServiceConfig apply(ServiceConfig c) {
-    ArduinoConfig config = (ArduinoConfig) super.apply(c);
+  public ArduinoConfig apply(ArduinoConfig c) {
+    super.apply(c);
 
     if (msg == null) {
       serial = (Serial) startPeer("serial");
@@ -2448,7 +2452,7 @@ public static void main(String[] args) {
       log.info("rest is {}", servo.getRest());
       servo.save();
       // servo.setPin(8);
-      servo.attach(mega, 13);
+      servo.attach(mega);
 
       servo.moveTo(90.0);
 
diff --git a/src/main/java/org/myrobotlab/service/Arm.java b/src/main/java/org/myrobotlab/service/Arm.java
index b86a279557..db695f01ef 100644
--- a/src/main/java/org/myrobotlab/service/Arm.java
+++ b/src/main/java/org/myrobotlab/service/Arm.java
@@ -28,13 +28,14 @@
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 /**
  * Arm
  * 
  */
-public class Arm extends Service {
+public class Arm extends Service {
 
   public transient final static Logger log = LoggerFactory.getLogger(Arm.class.getCanonicalName());
 
diff --git a/src/main/java/org/myrobotlab/service/As5048AEncoder.java b/src/main/java/org/myrobotlab/service/As5048AEncoder.java
index 34ac4ca212..f04b3d7289 100755
--- a/src/main/java/org/myrobotlab/service/As5048AEncoder.java
+++ b/src/main/java/org/myrobotlab/service/As5048AEncoder.java
@@ -2,6 +2,7 @@
 
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.abstracts.AbstractPinEncoder;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.EncoderControl;
 
 /**
@@ -10,7 +11,7 @@
  * @author kwatters
  *
  */
-public class As5048AEncoder extends AbstractPinEncoder implements EncoderControl {
+public class As5048AEncoder extends AbstractPinEncoder implements EncoderControl {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/AudioCapture.java b/src/main/java/org/myrobotlab/service/AudioCapture.java
index c1df1781fd..2ac2da72ee 100644
--- a/src/main/java/org/myrobotlab/service/AudioCapture.java
+++ b/src/main/java/org/myrobotlab/service/AudioCapture.java
@@ -50,13 +50,14 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 /**
  * AudioCapture - a service that can record and playback from a microphone.
  * 
  */
-public class AudioCapture extends Service {
+public class AudioCapture extends Service {
   public final static Logger log = LoggerFactory.getLogger(AudioCapture.class.getCanonicalName());
 
   private static final long serialVersionUID = 1L;
diff --git a/src/main/java/org/myrobotlab/service/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java
index 3738338a69..8b5a3cb828 100644
--- a/src/main/java/org/myrobotlab/service/AudioFile.java
+++ b/src/main/java/org/myrobotlab/service/AudioFile.java
@@ -54,7 +54,7 @@
 import org.myrobotlab.service.interfaces.AudioControl;
 import org.myrobotlab.service.interfaces.AudioPublisher;
 import org.slf4j.Logger;
-
+import java.util.Random;
 /**
  * 
  * AudioFile - This service can be used to play an audio file such as an mp3.
@@ -62,7 +62,7 @@
  * TODO - publishPeak interface
  *
  */
-public class AudioFile extends Service implements AudioPublisher, AudioControl {
+public class AudioFile extends Service implements AudioPublisher, AudioControl {
   static final long serialVersionUID = 1L;
   static final Logger log = LoggerFactory.getLogger(AudioFile.class);
 
@@ -165,6 +165,7 @@ public void stopService() {
   }
 
   public AudioData play(String filename) {
+    log.info("Audio file playing {}", filename);
     return play(filename, false);
   }
 
@@ -174,6 +175,7 @@ public AudioData play(String filename, boolean blocking) {
 
   public AudioData play(String filename, boolean blocking, Integer repeat, String track) {
 
+    log.info("Play called for Filename {}", filename);
     if (track == null || track.isEmpty()) {
       track = currentTrack;
     }
@@ -517,7 +519,7 @@ public void stopPlaylist() {
   }
 
   @Override
-  public ServiceConfig getConfig() {
+  public AudioFileConfig getConfig() {
 
     AudioFileConfig c = (AudioFileConfig) super.getConfig();
     // FIXME - remove members keep data in config !
@@ -535,9 +537,8 @@ public ServiceConfig getConfig() {
     return config;
   }
 
-  @Override
-  public ServiceConfig apply(ServiceConfig c) {
-    AudioFileConfig config = (AudioFileConfig) super.apply(c);
+  public AudioFileConfig apply(AudioFileConfig config) {
+    super.apply(config);
     setMute(config.mute);
     setTrack(config.currentTrack);
     setVolume(config.volume);
@@ -552,14 +553,11 @@ public ServiceConfig apply(ServiceConfig c) {
       }
     }
     
-    // FIXME - THIS IS ALL THATS NEEDED AND IT CAN BE 
-    // DONE IN THE SERVICE LEVEL
-    // if services need "special" handling they can override
-    this.config = c;
-    return c;
+    return config;
   }
 
   public double publishPeak(double peak) {
+    log.info("publishPeak {}", peak);
     return peak;
   }
   
@@ -604,6 +602,28 @@ public static void main(String[] args) {
   public void onPlayAudioFile(String file) {
     play(file);
   }
+  
+  @Override
+  public void onPlayRandomAudioFile(String dir) {
+    File test = new File(dir);
+    if (!test.exists() || !test.isDirectory()) {
+      error("%s is not a valid dir");
+      return;
+    }
+    
+    File[] files = test.listFiles();
+    
+    if (files.length == 0) {
+      error("%s contains no files", dir);
+      return;
+    }
+    
+    Random rand = new Random();
+    File randomFile = files[rand.nextInt(files.length-1)];
+    play(randomFile.getAbsolutePath());
+    
+  }
+
 
   public double getPeakMultiplier() {
     return ((AudioFileConfig)config).peakMultiplier;
diff --git a/src/main/java/org/myrobotlab/service/AzureTranslator.java b/src/main/java/org/myrobotlab/service/AzureTranslator.java
index a4f5c08f39..d9c5aceb68 100644
--- a/src/main/java/org/myrobotlab/service/AzureTranslator.java
+++ b/src/main/java/org/myrobotlab/service/AzureTranslator.java
@@ -18,6 +18,7 @@
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.TextListener;
 import org.myrobotlab.service.interfaces.TextPublisher;
 import org.myrobotlab.service.interfaces.Translator;
@@ -31,7 +32,7 @@
 import okhttp3.RequestBody;
 import okhttp3.Response;
 
-public class AzureTranslator extends Service implements Translator, TextListener, TextPublisher {
+public class AzureTranslator extends Service implements Translator, TextListener, TextPublisher {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/BeagleBoardBlack.java b/src/main/java/org/myrobotlab/service/BeagleBoardBlack.java
index 49600b2b16..fd9513ea1f 100644
--- a/src/main/java/org/myrobotlab/service/BeagleBoardBlack.java
+++ b/src/main/java/org/myrobotlab/service/BeagleBoardBlack.java
@@ -5,6 +5,7 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 /**
@@ -12,7 +13,7 @@
  * service will allow access through Java to the GPIO of the BBB. Needs a Pi4J
  * code to be ported to a BBB4J library.
  */
-public class BeagleBoardBlack extends Service {
+public class BeagleBoardBlack extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Blender.java b/src/main/java/org/myrobotlab/service/Blender.java
index b9dfde34e4..9056dbef23 100644
--- a/src/main/java/org/myrobotlab/service/Blender.java
+++ b/src/main/java/org/myrobotlab/service/Blender.java
@@ -14,9 +14,10 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
-public class Blender extends Service {
+public class Blender extends Service {
 
   /**
    * Control line - JSON over TCP/IP This is the single control communication
@@ -182,7 +183,7 @@ public synchronized String onAttach(String name) {
       Service.sleep(3000);
       // FIXME - more general case determined by "Type"
       ServiceInterface si = Runtime.getService(name);
-      if ("org.myrobotlab.service.Arduino".equals(si.getType())) {
+      if ("org.myrobotlab.service.Arduino".equals(si.getTypeKey())) {
         // FIXME - make more general - "any" Serial device !!!
         Arduino arduino = (Arduino) Runtime.getService(name);
         if (arduino != null) {
diff --git a/src/main/java/org/myrobotlab/service/Blocks.java b/src/main/java/org/myrobotlab/service/Blocks.java
index 49caf8bb28..c0362e2185 100644
--- a/src/main/java/org/myrobotlab/service/Blocks.java
+++ b/src/main/java/org/myrobotlab/service/Blocks.java
@@ -5,9 +5,10 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
-public class Blocks extends Service {
+public class Blocks extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Bno055.java b/src/main/java/org/myrobotlab/service/Bno055.java
index aab854397e..1086f749fc 100644
--- a/src/main/java/org/myrobotlab/service/Bno055.java
+++ b/src/main/java/org/myrobotlab/service/Bno055.java
@@ -12,6 +12,7 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.data.PinData;
 import org.myrobotlab.service.interfaces.I2CControl;
 import org.myrobotlab.service.interfaces.I2CController;
@@ -47,7 +48,7 @@
  * DEALINGS IN THE SOFTWARE. ===============================================
  */
 
-public class Bno055 extends Service implements I2CControl, PinListener {
+public class Bno055 extends Service implements I2CControl, PinListener {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/BodyPart.java b/src/main/java/org/myrobotlab/service/BodyPart.java
deleted file mode 100644
index 64cc4817d4..0000000000
--- a/src/main/java/org/myrobotlab/service/BodyPart.java
+++ /dev/null
@@ -1,323 +0,0 @@
-package org.myrobotlab.service;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.commons.lang3.StringUtils;
-import org.myrobotlab.framework.interfaces.Attachable;
-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.abstracts.AbstractBodyPart;
-import org.myrobotlab.service.interfaces.IKJointAngleListener;
-import org.myrobotlab.service.interfaces.ServoControl;
-import org.slf4j.Logger;
-
-/**
- * Body spare parts for universal ServoControl gestures Inspired by
- * InMoov.java...
- * 
- * TODO : IK moveTo(x,y,z) ?
- * 
- * Syntax to declare body part Runtime.start("nodeToAttach.name", "BodyPart");
- * 
- * Syntax to declare an actuator Runtime.start("nodeToAttach.name", "Servo");
- * 
- * 
- * Do not declare the whole path i01.rightarm.righthand.thumb... as name , just
- * the node to attach : The root will learn every nodes attached for a complete
- * linkage
- * 
- */
-public class BodyPart extends AbstractBodyPart implements IKJointAngleListener {
-
-  private static final long serialVersionUID = 1L;
-
-  public final static Logger log = LoggerFactory.getLogger(BodyPart.class);
-
-  public static void main(String[] args) {
-    LoggingFactory.init(Level.INFO);
-    BodyPart rightArm = (BodyPart) Runtime.start("rightArm", "BodyPart");
-    BodyPart rightHand = (BodyPart) Runtime.start("rightHand", "BodyPart");
-    Runtime.start("gui", "SwingGui");
-    VirtualArduino virtualArduino = (VirtualArduino) Runtime.start("virtualArduino", "VirtualArduino");
-    try {
-      virtualArduino.connect("COM42");
-    } catch (IOException e) {
-      // TODO Auto-generated catch block
-      e.printStackTrace();
-    }
-
-    Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino");
-    arduino.connect("COM42");
-
-    // virtual arduino can't simulate velocity at this time
-    // i2c service connected onto virtual arduino will do the job
-    // https://github.com/MyRobotLab/myrobotlab/issues/99
-    Adafruit16CServoDriver adafruit16CServoDriver = (Adafruit16CServoDriver) Runtime.start("adafruit16CServoDriver", "Adafruit16CServoDriver");
-    adafruit16CServoDriver.attach(arduino, "0", "0x40");
-
-    Servo servo = (Servo) Runtime.start("rightArm.bicep", "Servo");
-    Servo servo2 = (Servo) Runtime.start("rightArm.rotate", "Servo");
-    Servo servo3 = (Servo) Runtime.start("rightHand.majeure", "Servo");
-    Servo servo4 = (Servo) Runtime.start("rightHand.thumb", "Servo");
-
-    try {
-      servo.attach(adafruit16CServoDriver, 1);
-      servo2.attach(adafruit16CServoDriver, 2);
-      servo3.attach(adafruit16CServoDriver, 3);
-      servo4.attach(adafruit16CServoDriver, 4);
-      rightArm.attach(servo, servo2);
-      rightHand.attach(servo4, servo3);
-      rightArm.attach(rightHand);
-    } catch (Exception e) {
-      // TODO Auto-generated catch block
-      e.printStackTrace();
-    }
-
-    rightArm.moveTo(10.0);
-
-    rightArm.moveTo(90.0, 90.0, 90.0, 90.0, 90.0);
-    // log.info(rightArm.childs.toString() + "childNodes");
-    // log.info(rightArm.childs.crawlForDataStartingWith("") + "");
-
-    // inMoovTorso.servos =
-    // inMoovTorso.childs.crawlForDataStartingWith("rightArm");
-    rightArm.moveTo("rightHand", 63.0);
-    rightArm.getBodyParts();
-  }
-
-  public BodyPart(String n, String id) {
-    super(n, id);
-    // optional standardised servo names for priority order
-    servoOrder.put("thumb", 0);
-    servoOrder.put("index", 1);
-    servoOrder.put("majeure", 2);
-    servoOrder.put("ringFinger", 3);
-    servoOrder.put("pinky", 4);
-    servoOrder.put("wrist", 5);
-    servoOrder.put("bicep", 6);
-    servoOrder.put("rotate", 7);
-    servoOrder.put("shoulder", 8);
-    servoOrder.put("omoplate", 9);
-    servoOrder.put("neck", 10);
-    servoOrder.put("rothead", 11);
-    servoOrder.put("rollNeck", 12);
-    servoOrder.put("eyeX", 13);
-    servoOrder.put("eyeY", 14);
-    servoOrder.put("jaw", 15);
-    servoOrder.put("topStom", 16);
-    servoOrder.put("midStom", 17);
-    servoOrder.put("lowStom", 18);
-  }
-
-  @Override
-  public void attach(Attachable attachable) {
-
-    // attach the child to this node
-    if (attachable instanceof BodyPart) {
-      // store bodypart service inside the tree
-      thisNode.put(this.getName() + "." + attachable.getName(), attachable);
-      // store bodypart nodes
-      ArrayList nodes = ((BodyPart) attachable).thisNode.flatten();
-      for (Attachable service : nodes) {
-        thisNode.put(this.getName() + "." + service.getName(), service);
-      }
-
-      // or ServoControl ( as leaf ) to this
-    } else if (attachable instanceof ServoControl) {
-      // detect if syntax name is correct : parent.child
-      if (StringUtils.countMatches(attachable.getName(), ".") == 1) {
-        thisNode.put(attachable.getName(), attachable);
-      } else {
-        error("Can't attach %s, we need {parent.child} format .", attachable.getName());
-      }
-    }
-    broadcastState();
-  }
-
-  public void attach(ServoControl... attachable) {
-    for (int i = 0; i < attachable.length; i++) {
-      attach(attachable[i]);
-    }
-  }
-
-  public void moveTo(Double... servoPos) {
-    moveTo(this.getIntanceName(), servoPos);
-  }
-
-  public void moveToBlocking(Double... servoPos) {
-    moveToBlocking(this.getIntanceName(), servoPos);
-  }
-
-  public void waitTargetPos() {
-    waitTargetPos(this.getIntanceName());
-  }
-
-  /**
-   * Electrize servos group
-   */
-  public void enable() {
-    for (int i = 0; i < getAcuators(this.getIntanceName()).size(); i++) {
-      getAcuators(this.getIntanceName()).get(i).enable();
-    }
-  }
-
-  /**
-   * Shutdown power of servos group
-   */
-  public void disable() {
-    for (int i = 0; i < getAcuators(this.getIntanceName()).size(); i++) {
-      getAcuators(this.getIntanceName()).get(i).disable();
-    }
-  }
-
-  public void setVelocity(Double... servoVelocity) {
-    for (int i = 0; i < servoVelocity.length && i < getAcuators(this.getIntanceName()).size(); i++) {
-      getAcuators(this.getIntanceName()).get(i).setSpeed(servoVelocity[i]);
-    }
-  }
-
-  public void setRest(Double... rest) {
-    for (int i = 0; i < rest.length && i < getAcuators(this.getIntanceName()).size(); i++) {
-      getAcuators(this.getIntanceName()).get(i).setRest(rest[i]);
-    }
-  }
-
-  public void setAutoDisable(Boolean... param) {
-    for (int i = 0; i < param.length && i < getAcuators(this.getIntanceName()).size(); i++) {
-      getAcuators(this.getIntanceName()).get(i).setAutoDisable(param[i]);
-    }
-  }
-
-  public void setOverrideAutoDisable(Boolean... param) {
-    for (int i = 0; i < param.length && i < getAcuators(this.getIntanceName()).size(); i++) {
-      // getAcuators(this.getIntanceName()).get(i).setOverrideAutoDisable(param[i]);
-    }
-  }
-
-  public void rest() {
-    for (int i = 0; i < getAcuators(this.getIntanceName()).size(); i++) {
-      getAcuators(this.getIntanceName()).get(i).rest();
-    }
-  }
-
-  // pasted from inmoov service, for compatibility
-  @Override
-  public void onJointAngles(Map angleMap) {
-    // todo implement other body parts
-    if (this.getIntanceName().toLowerCase().contains("arm")) {
-      // We should walk though our list of servos and see if
-      // the map has it.. if so .. move to it!
-      // Peers p = InMoovArm.getPeers(getName()).getPeers("Servo");
-      // TODO: look up the mapping for all the servos in the arm.
-
-      // we map the servo 90 degrees to be 0 degrees.
-      HashMap phaseShiftMap = new HashMap();
-      // phaseShiftMap.put("omoplate", 90);
-      // Harry's omoplate is +90 degrees from Gaels InMoov..
-      // These are for the encoder offsets.
-      // these map between the reference frames of the dh model & the actual
-      // arm.
-      // (calibration)
-      phaseShiftMap.put("omoplate", 90.0);
-      phaseShiftMap.put("shoulder", 90.0);
-      phaseShiftMap.put("rotate", -450.0);
-      phaseShiftMap.put("bicep", 90.0);
-
-      HashMap gainMap = new HashMap();
-      gainMap.put("omoplate", 1.0);
-      gainMap.put("shoulder", -1.0);
-      gainMap.put("rotate", -1.0);
-      gainMap.put("bicep", -1.0);
-
-      ArrayList servos = new ArrayList();
-      servos.add("omoplate");
-      servos.add("shoulder");
-      servos.add("rotate");
-      servos.add("bicep");
-      for (String s : servos) {
-        if (angleMap.containsKey(s)) {
-          if ("omoplate".equals(s)) {
-            Double angle = (gainMap.get(s) * angleMap.get(s) + phaseShiftMap.get(s)) % 360.0;
-            if (angle < 0) {
-              angle += 360;
-            }
-            getServo("omoplate").moveTo(angle);
-          }
-          if ("shoulder".equals(s)) {
-            Double angle = (gainMap.get(s) * angleMap.get(s) + phaseShiftMap.get(s)) % 360.0;
-            if (angle < 0) {
-              angle += 360;
-            }
-            getServo("shoulder").moveTo(angle);
-          }
-          if ("rotate".equals(s)) {
-            Double angle = (gainMap.get(s) * angleMap.get(s) + phaseShiftMap.get(s)) % 360.0;
-            if (angle < 0) {
-              angle += 360;
-            }
-            getServo("rotate").moveTo(angle);
-          }
-          if ("bicep".equals(s)) {
-            Double angle = (gainMap.get(s) * angleMap.get(s) + phaseShiftMap.get(s)) % 360.0;
-            getServo("bicep").moveTo(angle);
-            if (angle < 0) {
-              angle += 360;
-            }
-          }
-        }
-      }
-    } else {
-      log.warn("Kinematics class not yet implemented for this body part");
-    }
-  }
-
-  // pasted from inmoov service, for compatibility
-  public DHRobotArm getDHRobotArm() {
-    if (this.getIntanceName().toLowerCase().contains("arm")) {
-      // TODO: specify this correctly and document the reference frames!
-      DHRobotArm arm = new DHRobotArm();
-      // d , r, theta , alpha
-
-      // TODO: the DH links should take into account the encoder offsets and
-      // calibration maps
-      DHLink link1 = new DHLink("omoplate", 0, 40, MathUtils.degToRad(-90), MathUtils.degToRad(-90));
-      // dh model + 90 degrees = real
-      link1.setMin(MathUtils.degToRad(-90));
-      link1.setMax(MathUtils.degToRad(0));
-
-      // -80 vs +80 difference between left/right arm.
-      DHLink link2 = new DHLink("shoulder", -80, 0, MathUtils.degToRad(90), MathUtils.degToRad(90));
-      // TODO: this is actually 90 to -90 ? validate if inverted.
-      // this link is inverted :-/
-      link2.setMin(MathUtils.degToRad(-90));
-      link2.setMax(MathUtils.degToRad(90));
-
-      DHLink link3 = new DHLink("rotate", 280, 0, MathUtils.degToRad(0), MathUtils.degToRad(90));
-      // TODO: check if this is inverted. i think it is.
-      link3.setMin(MathUtils.degToRad(0));
-      link3.setMax(MathUtils.degToRad(180));
-
-      DHLink link4 = new DHLink("bicep", 0, 280, MathUtils.degToRad(90), MathUtils.degToRad(0));
-      // TODO: this is probably inverted? should be 90 to 0...
-      link4.setMin(MathUtils.degToRad(90));
-      link4.setMax(MathUtils.degToRad(180));
-
-      arm.addLink(link1);
-      arm.addLink(link2);
-      arm.addLink(link3);
-      arm.addLink(link4);
-
-      return arm;
-    } else {
-      log.warn("Kinematics class not yet implemented for this body part");
-      return null;
-    }
-  }
-}
diff --git a/src/main/java/org/myrobotlab/service/BoofCV.java b/src/main/java/org/myrobotlab/service/BoofCV.java
new file mode 100644
index 0000000000..3da1e0a523
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/BoofCV.java
@@ -0,0 +1,431 @@
+package org.myrobotlab.service;
+
+import java.awt.Dimension;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.myrobotlab.boofcv.BoofCVFilter;
+import org.myrobotlab.boofcv.BoofCVFilterTrackerObjectQuad;
+import org.myrobotlab.cv.ComputerVision;
+import org.myrobotlab.cv.CVFilter;
+import org.myrobotlab.framework.Instantiator;
+import org.myrobotlab.framework.Service;
+import org.myrobotlab.image.WebImage;
+import org.myrobotlab.logging.Level;
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.logging.Logging;
+import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.BoofCVConfig;
+import org.slf4j.Logger;
+
+import com.github.sarxos.webcam.Webcam;
+
+//import boofcv.abst.video.VideoDisplay;
+//import boofcv.abst.video.VideoDisplayProcessing;
+import boofcv.gui.image.ImagePanel;
+import boofcv.gui.image.ShowImages;
+import boofcv.io.MediaManager;
+import boofcv.io.image.ConvertBufferedImage;
+import boofcv.io.image.SimpleImageSequence;
+import boofcv.io.webcamcapture.UtilWebcamCapture;
+import boofcv.io.wrapper.DefaultMediaManager;
+import boofcv.struct.image.GrayF32;
+import boofcv.struct.image.GrayU8;
+import boofcv.struct.image.ImageBase;
+import boofcv.struct.image.ImageType;
+
+public class BoofCV extends Service
+    implements ComputerVision /* Point2DfPublisher, Point2DfListener */ {
+
+  // FIXME - reconcile and make a real serializable enum with OpenCV definitions
+  public final String INPUT_SOURCE_CAMERA = "camera";
+  public final String INPUT_SOURCE_FILE = "imagefile";
+  
+  public class VideoProcessor implements Runnable {
+
+    volatile boolean running = false;
+
+    transient Thread worker = null;
+
+    @Override
+    public void run() {
+      capturing = true;
+      running = true;
+      try {
+        while (running) {
+          process();
+          frameIndex++;
+        }
+      } catch (Exception e) {
+        error(e);
+      }
+      capturing = false;
+      broadcastState();
+      worker = null;
+    }
+
+    synchronized void start() {
+      if (worker == null) {
+        worker = new Thread(this, String.format("%s-VideoProcessor", getName()));
+        worker.start();
+      } else {
+        log.info("{} video processor already running", getName());
+      }
+    }
+
+    synchronized void stop() {
+      running = false;
+    }
+
+  }
+
+  public final static Logger log = LoggerFactory.getLogger(BoofCV.class);
+
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * capturing state
+   */
+  protected boolean capturing = false;
+
+  /**
+   * last buffered image to be processed
+   */
+  transient BufferedImage bimage = null;
+
+  /**
+   * list of cameras
+   */
+  List cameras = new ArrayList<>();
+
+  // FIXME - put in config
+  String cameraDevice = null;
+
+  /**
+   * list of named filters to process the video stream
+   */
+  transient Map filters = new LinkedHashMap<>();
+
+  /**
+   * video stream frame index
+   */
+  protected int frameIndex = 0;
+
+  /**
+   * native swing display
+   */
+  transient private ImagePanel gui = null;
+
+  protected String inputSource = "camera";
+
+  protected boolean loop = true;
+
+  transient MediaManager media = DefaultMediaManager.INSTANCE;
+
+  boolean nativeViewer = true;
+
+  protected long ts = 0;
+
+  transient SimpleImageSequence video = null;
+
+  transient protected com.github.sarxos.webcam.Webcam webcam = null;
+
+  boolean webViewer = true;
+
+  final VideoProcessor worker = new VideoProcessor();
+  private ImageBase lastFrame;
+  private BufferedImage lastImage;
+
+  public BoofCV(String n, String id) {
+    super(n, id);
+  }
+
+  @Override
+  public CVFilter addFilter(String name, String filterType) {
+    String type = String.format("org.myrobotlab.boofcv.BoofCVFilter%s", filterType);
+    BoofCVFilter filter = (BoofCVFilter) Instantiator.getNewInstance(type, name);
+    if (filter == null) {
+      error("cannot create filter %s of type %s", name, type);
+      return null;
+    }
+    addFilter(filter);
+    return filter;
+  }
+
+  public BoofCVFilter addFilter(BoofCVFilter filter) {
+    filter.setBoofCV(this);
+
+    // guard against putting same name filter in
+    if (filters.containsKey(filter.getName())) {
+      warn("trying to add same named filter - %s - choose a different name", filter);
+      return filters.get(filter.getName());
+    }
+
+    // heh - protecting against concurrency the way Scala does it ;
+    Map newFilters = new LinkedHashMap<>();
+    newFilters.putAll(filters);
+    // add new filter
+    newFilters.put(filter.getName(), filter);
+    // switch to new references
+    filters = newFilters;
+    setDisplayFilter(filter.getName());
+    broadcastState();
+    return filter;
+
+  }
+
+  @Override
+  public void capture() {
+    worker.start();
+    capturing = true;
+    broadcastState();
+  }
+
+  @Override
+  public void disableAll() {
+    for (BoofCVFilter filter : filters.values()) {
+      filter.disable();
+    }
+    broadcastState();
+  }
+
+  @Override
+  public void disableFilter(String name) {
+    BoofCVFilter f = filters.get(name);
+    if (f != null && f.isEnabled()) {
+      f.disable();
+      broadcastState();
+    }
+  }
+
+  @Override
+  public void enableFilter(String name) {
+    BoofCVFilter f = filters.get(name);
+    if (f != null && !f.isEnabled()) {
+      f.enable();
+      broadcastState();
+    }
+  }
+
+  public List getCameras() {
+    // Get a list of available camera names
+    List webcams = Webcam.getWebcams();
+    cameras = new ArrayList<>();
+    for (Webcam cam : webcams) {
+      cameras.add(cam.getName());
+    }
+    return cameras;
+  }
+
+  private ImageBase getFrame() {
+
+    ImageBase frame = null;
+
+    // shutdown video source if loop and has no next
+    // FIXME - second param image type info
+    if (video != null && !video.hasNext() && loop) {
+      video.close();
+      video = null;
+    }
+
+    // create a video source if one is specified and null reference
+    
+      if (config.inputSource == INPUT_SOURCE_FILE) {
+        if (video == null) {
+          // file sequence needs a video
+        ImageType imageType = ImageType.pl(3, GrayU8.class);
+        imageType = ImageType.single(GrayF32.class);
+        // video = media.openVideo(config.inputFile, imageType);
+        video = media.openVideo(config.inputFile, ImageType.single(GrayU8.class));
+        }
+      } else if (config.inputSource == INPUT_SOURCE_CAMERA) {
+        // camera needs a webcam
+        // Configure webcam capture
+//        UtilWebcamCapture.CaptureInfo info = UtilWebcamCapture.selectSize(null,640,480);
+//        UtilWebcamCapture capture = UtilWebcamCapture.create(info);
+        if (webcam == null) {
+        // webcam = UtilWebcamCapture.openDevice(cameraDevice, 640,480);
+          webcam = UtilWebcamCapture.openDefault(640,480);
+        }
+      }
+    
+      // return a frame based on video source
+      if (config.inputSource == INPUT_SOURCE_FILE) {
+        frame = video.next();
+        if (frame != null) {
+          lastImage = video.getGuiImage();
+        }
+      } else if (config.inputSource == INPUT_SOURCE_CAMERA) {
+        BufferedImage image = webcam.getImage();
+        if (image != null) {
+          lastImage = image;
+        }
+        frame = ConvertBufferedImage.convertFrom(image, (GrayU8) null);
+      }
+      
+      if (frame != null) {
+        lastFrame = frame;
+      }
+
+    return frame;
+  }
+
+  private void process() {
+    // process the video stream
+
+    // get timestamp
+    ts = System.currentTimeMillis();
+
+    // get an image boofcv? frame
+    ImageBase frame = getFrame();
+
+    try {
+      // iterate through filters
+      for (BoofCVFilter filter : filters.values()) {
+        frame = filter.process(frame);
+      }
+    } catch (Exception e) {
+      error(e);
+    }
+
+    // convert to Buffered Image ?
+    bimage = ConvertBufferedImage.convertTo(frame, bimage, true);
+
+    // display the image natively
+    if (nativeViewer) {
+      if (gui == null) {
+        gui = new ImagePanel();
+        // gui.setPreferredSize(webcam.getViewSize());
+        gui.setPreferredSize(new Dimension(frame.getWidth(), frame.getHeight()));
+        // gui.setPreferredSize(bimage.getViewSize());
+        ShowImages.showWindow(gui, getName(), true);
+      }
+
+      gui.setImageRepaint(bimage);
+      sleep(100);
+
+    } else {
+      if (gui != null) {
+        gui.setVisible(false);
+        gui = null;
+      }
+    }
+
+    // display the image via web
+    if (webViewer) {
+      WebImage webImage = new WebImage(bimage, getName(), frameIndex);
+      webImage.ts = ts;
+      // broadcast does not queue the image and operates on the same thread
+      broadcast("publishWebDisplay", webImage);
+    }
+
+    // publish results
+    // publishCvData
+
+  }
+
+  // FIXME put in an interface
+  public WebImage publishWebDisplay(WebImage data) {
+    return data;
+  }
+
+  @Override
+  public void removeFilter(String name) {
+    if (filters.containsKey(name)) {
+      Map newFilters = new LinkedHashMap<>();
+      newFilters.putAll(filters);
+      BoofCVFilter removed = newFilters.remove(name);
+      removed.release();
+      filters = newFilters;
+      broadcastState();
+    }
+  }
+
+  @Override
+  public void removeFilters() {
+    for (BoofCVFilter filter : filters.values()) {
+      filter.release();
+    }
+    filters = new LinkedHashMap<>();
+    broadcastState();
+  }
+
+  @Override
+  public Integer setCameraIndex(Integer index) {
+    getCameras();
+    if (cameras.size() > index) {
+      // Webcam.
+      // cameraDevice = UtilWebcamCapture.openDevice(cameraDevice, frameIndex,
+      // frameIndex)
+
+    }
+    return null;
+  }
+
+  @Override
+  public void setDisplayFilter(String name) {
+    // TODO Auto-generated method stub
+
+  }
+
+  @Override
+  public void stopCapture() {
+    worker.stop();
+    capturing = false;
+    broadcastState();
+  }
+  
+  public void capture(String filename) {
+    config.inputFile = filename;
+    // FIXME make a communal enum shared with opencv (and serializable)
+    config.inputSource = INPUT_SOURCE_FILE;        
+    capture();
+  }
+  
+  public void capture(int cameraIndex) {
+    config.cameraIndex = cameraIndex;
+    // FIXME make a communal enum shared with opencv (and serializable)
+    config.inputSource = INPUT_SOURCE_CAMERA;
+    capture();
+  }
+
+  public static void main(String[] args) {
+    try {
+
+      LoggingFactory.init(Level.INFO);
+
+      Runtime.start("webgui", "WebGui");
+
+      BoofCV boofcv = (BoofCV) Runtime.start("boofcv", "BoofCV");
+
+      List cameras = boofcv.getCameras();
+
+      BoofCVFilterTrackerObjectQuad tracker = new BoofCVFilterTrackerObjectQuad("tracker");
+      tracker.setLocation(211.0, 162.0, 326.0, 153.0, 335.0, 258.0, 215.0, 249.0);
+      boofcv.addFilter(tracker);
+      // boofcv.addFilter("tracker", "TrackerObjectQuad");
+
+      boofcv.capture(0);
+      
+      // boofcv.capture("wildcat_robot.mjpeg");
+      // boofcv.capture("zoom.mjpeg");
+
+      // boofcv.capture();
+      log.info("here");
+      // Runtime.start("gui", "SwingGui");
+    } catch (Exception e) {
+      Logging.logError(e);
+    }
+  }
+
+  public BufferedImage getGuiImage() {  
+    if (lastImage != null) {
+      return lastImage;  
+    }
+    return null;
+  }
+
+}
diff --git a/src/main/java/org/myrobotlab/service/BoofCv.java b/src/main/java/org/myrobotlab/service/BoofCv.java
deleted file mode 100644
index 21bdad5ee4..0000000000
--- a/src/main/java/org/myrobotlab/service/BoofCv.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.myrobotlab.service;
-
-import org.myrobotlab.framework.Service;
-import org.myrobotlab.logging.Level;
-import org.myrobotlab.logging.LoggerFactory;
-import org.myrobotlab.logging.Logging;
-import org.myrobotlab.logging.LoggingFactory;
-import org.myrobotlab.math.geometry.Point2df;
-import org.myrobotlab.service.interfaces.Point2DfListener;
-import org.myrobotlab.service.interfaces.Point2DfPublisher;
-import org.slf4j.Logger;
-
-public class BoofCv extends Service implements Point2DfPublisher, Point2DfListener {
-
-  private static final long serialVersionUID = 1L;
-
-  public final static Logger log = LoggerFactory.getLogger(BoofCv.class);
-
-  public BoofCv(String n, String id) {
-    super(n, id);
-  }
-
-  @Override
-  public Point2df publishPoint2Df(Point2df point) {
-    return point;
-  }
-
-  @Override
-  public Point2df onPoint2Df(Point2df point) {
-    // System.out.println("Receinvig");
-    return point;
-  }
-
-  public static void main(String[] args) {
-    try {
-
-      LoggingFactory.init(Level.INFO);
-
-      // ImageType> colorType = ImageType.pl(3,GrayU8.class);
-      BoofCv boofcv = (BoofCv) Runtime.start("boofcv", "BoofCv");
-
-      // BoofCV template = (BoofCV) Runtime.start("template", "BoofCV");
-      // Runtime.start("gui", "SwingGui");
-    } catch (Exception e) {
-      Logging.logError(e);
-    }
-  }
-
-}
diff --git a/src/main/java/org/myrobotlab/service/Chassis.java b/src/main/java/org/myrobotlab/service/Chassis.java
index c0b5da0549..6f2c5ba082 100644
--- a/src/main/java/org/myrobotlab/service/Chassis.java
+++ b/src/main/java/org/myrobotlab/service/Chassis.java
@@ -4,11 +4,12 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.MotorControl;
 import org.myrobotlab.service.interfaces.MotorController;
 import org.slf4j.Logger;
 
-public class Chassis extends Service {
+public class Chassis extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/ChessGame.java b/src/main/java/org/myrobotlab/service/ChessGame.java
index f9be32e27f..adb0c7dc22 100644
--- a/src/main/java/org/myrobotlab/service/ChessGame.java
+++ b/src/main/java/org/myrobotlab/service/ChessGame.java
@@ -31,9 +31,10 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
-public class ChessGame extends Service {
+public class ChessGame extends Service {
 
   public final static Logger log = LoggerFactory.getLogger(ChessGame.class.getCanonicalName());
 
diff --git a/src/main/java/org/myrobotlab/service/Clock.java b/src/main/java/org/myrobotlab/service/Clock.java
index aefe9fdf03..14fd02b2f3 100644
--- a/src/main/java/org/myrobotlab/service/Clock.java
+++ b/src/main/java/org/myrobotlab/service/Clock.java
@@ -14,7 +14,6 @@
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.config.ClockConfig;
-import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 /**
@@ -22,7 +21,7 @@
  * generates a pulse with a timestamp on a regular interval defined by the
  * setInterval(Integer) method. Interval is in milliseconds.
  */
-public class Clock extends Service {
+public class Clock extends Service {
 
   public class ClockThread implements Runnable {
 
@@ -31,14 +30,13 @@ public class ClockThread implements Runnable {
 
     @Override
     public void run() {
-      ClockConfig c = (ClockConfig) config;
 
       try {
 
-        c.running = true;
+        config.running = true;
         invoke("publishClockStarted");
-        while (c.running) {
-          Thread.sleep(c.interval);
+        while (config.running) {
+          Thread.sleep(config.interval);
           Date now = new Date();
           for (Message msg : events) {
             send(msg);
@@ -50,7 +48,7 @@ public void run() {
       } catch (InterruptedException e) {
         log.info("ClockThread interrupt");
       }
-      c.running = false;
+      config.running = false;
       thread = null;
     }
 
@@ -68,19 +66,18 @@ synchronized public void start() {
     }
 
     synchronized public void stop() {
-      ClockConfig c = (ClockConfig) config;
 
       if (thread != null) {
         thread.interrupt();
         broadcastState();
       } else {
-        log.info("{} already stopped");
+        log.info("{} already stopped", getName());
       }
-      c.running = false;
+      config.running = false;
       Service.sleep(20);
     }
   }
-
+  
   private static final long serialVersionUID = 1L;
 
   final public static Logger log = LoggerFactory.getLogger(Clock.class);
@@ -159,8 +156,7 @@ public long publishEpoch(Date time) {
    * @param milliseconds
    */
   public void setInterval(Integer milliseconds) {
-    ClockConfig c = (ClockConfig) config;
-    c.interval = milliseconds;
+    config.interval = milliseconds;
     broadcastState();
   }
 
@@ -181,8 +177,7 @@ public void startClock() {
    * @return
    */
   public boolean isClockRunning() {
-    ClockConfig c = (ClockConfig) config;
-    return c.running;
+    return config.running;
   }
 
   /**
@@ -203,20 +198,19 @@ public void stopService() {
    * @return
    */
   public Integer getInterval() {
-    return ((ClockConfig) config).interval;
+    return config.interval;
   }
 
-  @Override
-  public ServiceConfig apply(ServiceConfig c) {    
-    ClockConfig config = (ClockConfig) super.apply(c);
-    if (config.running != null) {
-      if (config.running) {
+  public ClockConfig apply(ClockConfig c) {    
+    super.apply(c);
+    if (c.running != null) {
+      if (c.running) {
         startClock();
       } else {
         stopClock();
       }
     }
-    return config;
+    return c;
   }
 
   public void restartClock() {
@@ -227,13 +221,20 @@ public void restartClock() {
   public static void main(String[] args) throws Exception {
     try {
 
-      WebGui webgui = (WebGui)Runtime.create("webgui", "WebGui");
-      webgui.autoStartBrowser(false);
-      webgui.setPort(8887);
-      webgui.startService();
+//      WebGui webgui = (WebGui)Runtime.create("webgui", "WebGui");
+//      webgui.autoStartBrowser(false);
+//      webgui.setPort(8887);
+//      webgui.startService();
 
       Clock c1 = (Clock) Runtime.start("c1", "Clock");
-      c1.startClock();
+      Runtime.setLogLevel("ERROR");
+      // c1.startClock();
+      Runtime.getInstance().connect("ws://localhost:8888");
+      
+      boolean done = true;
+      if (done) return;
+      
+      
       Runtime.getInstance().connect("ws://localhost:8888");
       c1.stopClock();
 
@@ -243,4 +244,6 @@ public static void main(String[] args) throws Exception {
     }
   }
 
+
+
 }
\ No newline at end of file
diff --git a/src/main/java/org/myrobotlab/service/Cron.java b/src/main/java/org/myrobotlab/service/Cron.java
index fd2ca9d064..9faaabe0d0 100644
--- a/src/main/java/org/myrobotlab/service/Cron.java
+++ b/src/main/java/org/myrobotlab/service/Cron.java
@@ -2,43 +2,81 @@
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
-import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.CronConfig;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 import it.sauronsoftware.cron4j.Scheduler;
 
 /**
- * Cron - This is a cron based service that can execute a "task" at some point
- * in the future such as "invoke this method on that service"
+ * Cron - This is a cron based service that can execute a "task".
+ * It does not need an operating system in order to run.  It is a
+ * pure java implementation of a cron service.  It accepts cron 
+ * patterns and will execute a task based on the pattern.  The
+ * task is a message that is sent to a service.  The message
+ * can be any message that the service accepts. 
  * 
- * FIXME - the common cron notation is kind of nice - but this thing doesn't do
- * more than Service.addTask
- * 
- * FIXME - make a purge & delete DUH !
- *
  */
-public class Cron extends Service {
-
+public class Cron extends Service {
+  
   public static class Task implements Serializable, Runnable {
+
     private static final long serialVersionUID = 1L;
-    transient Cron myService;
+    /**
+     * reference to service
+     */
+    transient Cron cron;
+
+    /**
+     * cron pattern for this task
+     */
     public String cronPattern;
-    public String name;
-    public String method;
+
+    /**
+     * data parameters to invoke
+     */
     public Object[] data;
 
-    public Task(Cron myService, String cronPattern, String name, String method) {
-      this(myService, cronPattern, name, method, (Object[]) null);
+    /**
+     * unique hash the scheduler uses (only)
+     */
+    transient public String hash;
+
+    /**
+     * id for the user to use
+     */
+    public String id;
+
+    /**
+     * method to invoke
+     */
+    public String method;
+
+    /**
+     * name of the target service
+     */
+    public String name;
+
+    public Task() {
     }
 
-    public Task(Cron myService, String cronPattern, String name, String method, Object... data) {
-      this.myService = myService;
+    public Task(Cron cron, String id, String cronPattern, String name, String method) {
+      this(cron, id, cronPattern, name, method, (Object[]) null);
+    }
+
+    public Task(Cron cron, String id, String cronPattern, String name, String method, Object... data) {
+      this.cron = cron;
+      this.id = id;
       this.cronPattern = cronPattern;
       this.name = name;
       this.method = method;
@@ -47,128 +85,228 @@ public Task(Cron myService, String cronPattern, String name, String method, Obje
 
     @Override
     public void run() {
-      log.info("{} Cron firing message {}->{}.{}", myService.getName(), name, method, data);
-      myService.send(name, method, data);
+        log.info("{} Cron firing message {}->{}.{}", cron.getName(), name, method, data);
+        cron.send(name, method, data);
+        cron.history.add(new TaskHistory(id, new Date()));
+        if (cron.history.size() > cron.HISTORY_SIZE) {
+          cron.history.remove(0);
+        }
+        cron.broadcastState();
+    }
+
+    @Override
+    public String toString() {
+      return String.format("%s, %s, %s, %s", id, cronPattern, name, method);
     }
   }
+  
+  public static class TaskHistory {
+    public String id;
+    public Date processedTime;
+    
+    public TaskHistory(String id, Date now) {
+      this.id = id;
+      this.processedTime = now;
+    }    
+  }
+
+  public final static Logger log = LoggerFactory.getLogger(Cron.class);
 
   private static final long serialVersionUID = 1L;
 
-  public final static Logger log = LoggerFactory.getLogger(Cron.class.getCanonicalName());
+  /**
+   * history buffer of tasks that have been executed
+   */
+  final protected List history = new ArrayList<>();
 
+  /**
+   * max size of history buffer
+   */
+  final int HISTORY_SIZE = 30;
+  
+  /**
+   * the thing that translates all the cron pattern values and implements actual tasks
+   */
   transient private Scheduler scheduler = new Scheduler();
 
-  // Schedule a once-a-week task at 8am on Sunday.
-  // 0 8 * * 7
-  // Schedule a twice a day task at 7am and 6pm on weekdays
-  // 0 7 * * 1-5 |0 18 * * 1-5
-
-  public final static String EVERY_MINUTE = "* * * * *";
-
-  public ArrayList tasks = new ArrayList();
-
-  public static void main(String[] args) {
-    LoggingFactory.init(Level.INFO);
-
-    try {
-      Cron cron = (Cron) Runtime.start("cron", "Cron");// new
-      // Cron("cron");
-      cron.startService();
+  /**
+   * map of tasks organized by id
+   */
+  protected Map tasks = new LinkedHashMap<>();
 
-      /*
-       * cron.addScheduledEvent("0 6 * * 1,3,5","arduino","digitalWrite", 13,
-       * 1); cron.addScheduledEvent("0 7 * * 1,3,5","arduino","digitalWrite",
-       * 12, 1); cron.addScheduledEvent("0 8 * * 1,3,5"
-       * ,"arduino","digitalWrite", 11, 1);
-       * 
-       * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 13, 0);
-       * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 12, 0);
-       * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 11, 0);
-       */
-      cron.addTask("* * * * *", "cron", "test", 7);
+  public Cron(String n, String id) {
+    super(n, id);
+  }
 
-      // cron.addScheduledEvent(EVERY_MINUTE, "log", "log");
-      // west wall | back | east wall
+  /**
+   * Add a named task with out parameters
+   * 
+   * @param id
+   * @param cron
+   * @param serviceName
+   * @param method
+   * @return
+   */
+  public String addTask(String id, String cron, String serviceName, String method) {
+    return addTask(id, cron, serviceName, method, (Object[]) null);
+  }
 
-      String json = CodecUtils.toJson(cron.getTasks());
+  /**
+   * Add a named task with parameters
+   * 
+   * @param id
+   * @param cronPattern
+   * @param serviceName
+   * @param method
+   * @param data
+   * @return
+   */
+  public String addTask(String id, String cronPattern, String serviceName, String method, Object... data) {    
+    Task task = new Task(this, id, cronPattern, serviceName, method, data);
+    addTask(task);
+    return id;
+  }
 
-      log.info("here {}", json);
 
-      // Runtime.createAndStart("webgui", "WebGui");
+  /**
+   * 
+   * @param task
+   * @return
+   */
+  public String addTask(Task task) {
+    if (tasks.containsKey(task.id)) {
+      log.info("descheduling prexisting task {} hash {}", task.id, task.hash);
+      scheduler.deschedule(task.id);
+    }
+    log.info("scheduling task {}", task.id);
+    task.hash = scheduler.schedule(task.cronPattern, task);
+    task.cron = this;
+    tasks.put(task.id, task);
+    broadcastState();
+    return task.id;
+  }
 
-      // 1. doug - find location where checked in ----
-      // 2. take out security token from DL broker's response
-      // 3. Tony - status ? and generated xml responses - "update" looks
-      // ok
+  @Override
+  public CronConfig apply(CronConfig c) {
+    super.apply(c);
+    
+    // deschedule current tasks
+    removeAllTasks();
+    
+    // add new tasks
+    for (Task task : c.tasks) {
+      addTask(task);
+    }
+    return c;
+  }
 
-      // Runtime.createAndStart("gui", "SwingGui");
-      /*
-       * SwingGui gui = new SwingGui("gui"); gui.startService();
-       */
-    } catch (Exception e) {
-      Logging.logError(e);
+  @Override
+  public CronConfig getConfig() {
+    super.getConfig();
+    config.tasks = new ArrayList<>();
+    for (Task task: tasks.values()) {
+      config.tasks.add(task);
     }
+    return config;
   }
 
-  public Cron(String n, String id) {
-    super(n, id);
+  public Map getCronTasks() {
+    return tasks;
   }
 
-  /*
-   * addTask - Add a task to the cron service to invoke a method on a service on
-   * some schedule.
-   * 
-   * @param cron - The cron string to define the schedule
-   * 
-   * @param serviceName - The name of the service to invoke
-   * 
-   * @param method - the method on the service to invoke when the task starts.
+  /**
+   * get a task from id
+   * @param id
+   * @return
    */
-  public String addTask(String cron, String serviceName, String method) {
-    return addTask(cron, serviceName, method, (Object[]) null);
+  public Task getTask(String id) {
+    return tasks.get(id);
   }
 
-  /*
-   * addTask - Add a task to the cron service to invoke a method on a service on
-   * some schedule.
-   * 
-   * @param cron - The cron string to define the schedule
-   * 
-   * @param serviceName - The name of the service to invoke
-   * 
-   * @param method - the method on the service to invoke when the task starts.
-   * 
-   * @param data - additional objects/varags to pass to the method
+  /**
+   * removes all the tasks without stopping the scheduler
    */
-  public String addTask(String cron, String serviceName, String method, Object... data) {
-    Task task = new Task(this, cron, serviceName, method, data);
-    tasks.add(task);
-    return scheduler.schedule(cron, task);
+  public void removeAllTasks() {
+    for (Task t : tasks.values()) {
+      scheduler.deschedule(t.hash);
+    }
+    tasks.clear();
   }
 
-  public ArrayList getCronTasks() {
-    return tasks;
+  /**
+   * removes task by id
+   * @param id - id of the task to remove
+   * @return the removed task if it exists
+   */
+  public Task removeTask(String id) {
+    Task t = tasks.remove(id);
+    if (t != null) {
+      scheduler.deschedule(t.hash);
+    } else {
+      log.error("%s could not find task %s to remove", getName(), id);
+    }
+    broadcastState();
+    return t;
   }
-
-  @Override
-  public void startService() {
-    super.startService();
+  
+  /**
+   * start the schedular and all associated tasks
+   */
+  public void start() {
     if (!scheduler.isStarted()) {
       scheduler.start();
     }
   }
-
+  
   @Override
-  public void stopService() {
-    super.stopService();
+  public void startService() {
+    super.startService();
+    start();
+  }
+  
+  /**
+   * stop the schedular ad all associated tasks
+   */
+  public void stop() {
+    removeAllTasks();
     if (scheduler.isStarted()) {
       scheduler.stop();
     }
   }
+  
 
-  public int test(Integer data) {
-    log.info("data {}", data);
-    return data;
+  @Override
+  public void stopService() {
+    super.stopService();
+    stop();
   }
+  
+  public static void main(String[] args) {
+    LoggingFactory.init(Level.INFO);
+
+    try {
+      Cron cron = (Cron) Runtime.start("cron", "Cron");
+      Arduino mega = (Arduino) Runtime.start("mega", "Arduino");
+      mega.connect("/dev/ttyACM2");
+
+      Runtime.start("webgui", "WebGui");
 
+      /*
+       * 
+       * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 13, 0);
+       * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 12, 0);
+       * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 11, 0);
+       */
+      // every odd minute
+      String id = cron.addTask("led on", "1-59/2 * * * *", "mega", "digitalWrite", 13, 1);
+      // every event minute
+      String id2 = cron.addTask("led off", "*/2 * * * *", "mega", "digitalWrite", 13, 0);
+
+      // Runtime.createAndStart("webgui", "WebGui");
+
+    } catch (Exception e) {
+      Logging.logError(e);
+    }
+  }
+  
 }
diff --git a/src/main/java/org/myrobotlab/service/Database.java b/src/main/java/org/myrobotlab/service/Database.java
index ddcd521989..a90006dcd4 100644
--- a/src/main/java/org/myrobotlab/service/Database.java
+++ b/src/main/java/org/myrobotlab/service/Database.java
@@ -9,9 +9,10 @@
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
-public class Database extends Service {
+public class Database extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Deeplearning4j.java b/src/main/java/org/myrobotlab/service/Deeplearning4j.java
index 0381ced24e..892e33cea6 100644
--- a/src/main/java/org/myrobotlab/service/Deeplearning4j.java
+++ b/src/main/java/org/myrobotlab/service/Deeplearning4j.java
@@ -85,6 +85,7 @@
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.opencv.CloseableFrameConverter;
 import org.myrobotlab.opencv.YoloDetectedObject;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.nd4j.linalg.activations.Activation;
 import org.nd4j.linalg.api.ndarray.INDArray;
 import org.nd4j.linalg.dataset.api.iterator.DataSetIterator;
@@ -112,7 +113,7 @@
  * @author kwatters
  *
  */
-public class Deeplearning4j extends Service {
+public class Deeplearning4j extends Service {
 
   private static final long serialVersionUID = 1L;
   transient public final static Logger log = LoggerFactory.getLogger(Deeplearning4j.class);
diff --git a/src/main/java/org/myrobotlab/service/DiscordBot.java b/src/main/java/org/myrobotlab/service/DiscordBot.java
index 19c9ec3171..f3aa3560cf 100644
--- a/src/main/java/org/myrobotlab/service/DiscordBot.java
+++ b/src/main/java/org/myrobotlab/service/DiscordBot.java
@@ -29,7 +29,7 @@
  * channels. The bot user also needs a token that is used to authenticate and
  * identify the bot.
  */
-public class DiscordBot extends Service implements UtterancePublisher, UtteranceListener, ImageListener {
+public class DiscordBot extends Service implements UtterancePublisher, UtteranceListener, ImageListener {
 
   transient public final static Logger log = LoggerFactory.getLogger(DiscordBot.class);
 
@@ -54,27 +54,20 @@ public DiscordBot(String reservedKey, String inId) {
   }
 
   @Override
-  public ServiceConfig apply(ServiceConfig c) {
-    DiscordBotConfig config = (DiscordBotConfig) super.apply(c);
+  public DiscordBotConfig apply(DiscordBotConfig c) {
+    super.apply(c);
 
-    if (config.token != null) {
-      setToken(config.token);
+    if (c.token != null) {
+      setToken(c.token);
     }
 
-    // REMOVED - OVERLAP WITH SUBSCRIPTIONS
-//    if (config.utteranceListeners != null) {
-//      for (String name : config.utteranceListeners) {
-//        attachUtteranceListener(name);
-//      }
-//    }
-
-    if (config.connect && config.token != null && !config.token.isEmpty()) {
+    if (c.connect && c.token != null && !c.token.isEmpty()) {
       connect();
-    } else if (config.token == null || config.token.isEmpty()) {
+    } else if (c.token == null || c.token.isEmpty()) {
       error("requires valid token to connect");
     }
 
-    return config;
+    return c;
   }
 
   public String getBotName() {
@@ -103,20 +96,9 @@ public void attach(Attachable attachable) {
   }
 
   @Override
-  public ServiceConfig getConfig() {
-    // TODO: this is also an ugly pattern. you can't really call super get
-    // config here!
-    // ServiceConfig c = super.getConfig();
-    // TODO: is this unsafe?
-    // TODO: what sets the type of this config?
-    /// TODO: this isn't good OO programming to have to do it this way.
-    DiscordBotConfig c = (DiscordBotConfig) super.getConfig();
-    c.token = token;
-
-    // REMOVED BECAUSE OVERLAP WITH SUBSCRIPTION
-//    Set listeners = getAttached("publishUtterance");
-//    c.utteranceListeners = listeners.toArray(new String[listeners.size()]);
-
+  public DiscordBotConfig getConfig() {
+    super.getConfig();
+    config.token = token;
     return config;
   }
 
diff --git a/src/main/java/org/myrobotlab/service/DiyServo.java b/src/main/java/org/myrobotlab/service/DiyServo.java
index 670cce1a59..2e125d8509 100644
--- a/src/main/java/org/myrobotlab/service/DiyServo.java
+++ b/src/main/java/org/myrobotlab/service/DiyServo.java
@@ -38,6 +38,7 @@
 import org.myrobotlab.math.MathUtils;
 import org.myrobotlab.math.interfaces.Mapper;
 import org.myrobotlab.service.abstracts.AbstractServo;
+import org.myrobotlab.service.config.ServoConfig;
 import org.myrobotlab.service.data.PinData;
 import org.myrobotlab.service.interfaces.EncoderControl;
 import org.myrobotlab.service.interfaces.MotorControl;
@@ -75,7 +76,7 @@
  *         TODO : move is not accurate ( 1° step seem not possible )
  */
 
-public class DiyServo extends AbstractServo implements PinListener, ServiceLifeCycleListener {
+public class DiyServo extends AbstractServo implements PinListener, ServiceLifeCycleListener {
 
   double lastOutput = 0.0;
   /**
@@ -261,11 +262,6 @@ public long getLastActivityTime() {
     return lastActivityTimeTs;
   }
 
-  // FIXME - change to enabled()
-  public Boolean isAttached() {
-    return isAttached;
-  }
-
   public boolean isControllerSet() {
     return isControllerSet;
   }
@@ -560,13 +556,11 @@ public void attach(EncoderControl encoder) {
     this.encoderControl = encoder;
   }
 
-  @Override
-  public void attach(String pinArrayControlName, Integer pin) {
-    // myServo = (DiyServo) Runtime.getService(boundServiceName);
-    attach((PinArrayControl) Runtime.getService(pinArrayControlName), (int) pin);
-  }
-
-  public void attach(PinArrayControl pinArrayControl, int pin) {
+  public void attach(PinArrayControl pinArrayControl, int pin) throws Exception {
+    if (pinArrayControl == null) {
+      error("pinArrayCOntrol cannot be null");
+      return;
+    }
     this.pinArrayControl = pinArrayControl;
     if (pinArrayControl != null) {
       pinControlName = pinArrayControl.getName();
@@ -586,8 +580,8 @@ public void attach(PinArrayControl pinArrayControl, int pin) {
     // %s",pinArrayControl.getClass(), pinArrayControl.getName(),resolution));
 
     int rate = 1000 / sampleTime;
-    pinArrayControl.attachPinListener(this, pin);
-    pinArrayControl.enablePin(pin, rate);
+    pinArrayControl.attach(getName());
+    pinArrayControl.enablePin(this.pin, rate);
     broadcastState();
   }
 
diff --git a/src/main/java/org/myrobotlab/service/Docker.java b/src/main/java/org/myrobotlab/service/Docker.java
index d42947e4c7..4f36b0c456 100644
--- a/src/main/java/org/myrobotlab/service/Docker.java
+++ b/src/main/java/org/myrobotlab/service/Docker.java
@@ -6,19 +6,15 @@
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.config.DockerConfig;
-import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 import com.github.dockerjava.api.DockerClient;
-import com.github.dockerjava.api.command.CreateContainerResponse;
-import com.github.dockerjava.api.model.Bind;
 import com.github.dockerjava.api.model.Container;
-import com.github.dockerjava.api.model.PortBinding;
 import com.github.dockerjava.core.DefaultDockerClientConfig;
 import com.github.dockerjava.core.DockerClientBuilder;
 import com.github.dockerjava.core.DockerClientConfig;
 
-public class Docker extends Service {
+public class Docker extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/DocumentPipeline.java b/src/main/java/org/myrobotlab/service/DocumentPipeline.java
index dd25011b85..0d511776c4 100644
--- a/src/main/java/org/myrobotlab/service/DocumentPipeline.java
+++ b/src/main/java/org/myrobotlab/service/DocumentPipeline.java
@@ -1,6 +1,8 @@
 package org.myrobotlab.service;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.myrobotlab.document.Document;
 import org.myrobotlab.document.ProcessingStatus;
@@ -10,10 +12,13 @@
 import org.myrobotlab.document.workflow.WorkflowMessage;
 import org.myrobotlab.document.workflow.WorkflowServer;
 import org.myrobotlab.framework.Service;
+import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.DocumentPipelineConfig;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.DocumentListener;
 import org.myrobotlab.service.interfaces.DocumentPublisher;
 
-public class DocumentPipeline extends Service implements DocumentListener, DocumentPublisher {
+public class DocumentPipeline extends Service implements DocumentListener, DocumentPublisher {
 
   private static final long serialVersionUID = 1L;
 
@@ -40,14 +45,6 @@ public Document publishDocument(Document doc) {
     return doc;
   }
 
-  @Override
-  public void addDocumentListener(DocumentListener listener) {
-    // TODO Auto-generated method stub
-    // ??
-    // subscribe("publishDocument", topicMethod, callbackName, callbackMethod);
-
-  }
-
   @Override
   public ProcessingStatus onDocument(Document doc) {
     // TODO Auto-generated method stub
@@ -101,32 +98,65 @@ public void flush() {
 
   public static void main(String[] args) throws Exception {
 
-    // create the pipeline service in MRL
+    LoggingFactory.init("info");
+    Python python = (Python)Runtime.start("python", "Python");
+    WebGui webgui = (WebGui)Runtime.start("webgui", "WebGui");
+    // Solr 
+    Solr solr = (Solr)Runtime.start("solr", "Solr");
+    // the audio file service to play music
+    AudioFile audiofile = (AudioFile)Runtime.start("audiofile", "AudioFile");
+    // document pipeline to get metadata from the mp3s and other files.
     DocumentPipeline pipeline = (DocumentPipeline) Runtime.start("docproc", "DocumentPipeline");
-
-    // pipeline.workflowName = "default";
-    // create a workflow to load into that pipeline service
     WorkflowConfiguration workflowConfig = new WorkflowConfiguration("default");
     workflowConfig.setName("default");
+    // number of threads to extract metadata from the documents.
+    workflowConfig.setNumWorkerThreads(8);
+    // Some stages to get / stamp metadata on the documents being indexed.
     StageConfiguration stage1Config = new StageConfiguration();
     stage1Config.setStageClass("org.myrobotlab.document.transformer.SetStaticFieldValue");
-    stage1Config.setStageName("SetTableField");
-    stage1Config.setStringParam("table", "MRL");
+    stage1Config.setStageName("SetTypeField");
+    stage1Config.setStringParam("type", "file");
     workflowConfig.addStage(stage1Config);
-
+    // perform text extraction from the file using Apache Tika
     StageConfiguration stage2Config = new StageConfiguration();
-    stage2Config.setStageClass("org.myrobotlab.document.transformer.SendToSolr");
-    stage2Config.setStageName("SendToSolr");
-    stage2Config.setStringParam("solrUrl", "http://phobos:8983/solr/graph");
+    stage2Config.setStageClass("org.myrobotlab.document.transformer.TextExtractor");
+    stage2Config.setStageName("TextExtractor");
     workflowConfig.addStage(stage2Config);
-
+    // rename some fields to be more human readable and to match the solr schema.
+    StageConfiguration stage3Config = new StageConfiguration();
+    stage3Config.setStageClass("org.myrobotlab.document.transformer.RenameFields");
+    stage3Config.setStageName("RenameFields");
+    Map fieldNameMap = new HashMap();
+    fieldNameMap.put("xmpdm_tracknumber", "tracknumber");
+    fieldNameMap.put("xmpdm_releasedate", "year");
+    fieldNameMap.put("xmpdm_duration", "duration");
+    fieldNameMap.put("xmpdm_genre", "genre");
+    fieldNameMap.put("xmpdm_artist", "artist");
+    fieldNameMap.put("dc_title", "title");
+    fieldNameMap.put("xmpdm_album", "album");
+    stage3Config.setMapProperty("fieldNameMap", fieldNameMap);
+    workflowConfig.addStage(stage3Config);;
+    // TODO: rename more fields..
+    // TODO: delete unnecessary fields.
     pipeline.setConfig(workflowConfig);
     pipeline.initalize();
-
-    RSSConnector connector = (RSSConnector) Runtime.start("rss", "RSSConnector");
-    connector.addDocumentListener(pipeline);
-    connector.startCrawling();
-
+    // attach the pipeline to solr.
+    // 
+    pipeline.attachDocumentListener(solr.getName());
+    // start the file connector to scan the file system.
+    // RSSConnector connector = (RSSConnector) Runtime.start("rss", "RSSConnector");
+    FileConnector connector = (FileConnector) Runtime.start("fileconnector", "FileConnector");
+    connector.setDirectory("Z:\\Music");
+
+    // connector to pipeline connection
+    connector.attachDocumentListener(pipeline.getName());
+
+    Runtime.saveConfig("mediasearch");
+    // start the crawl!
+    boolean doCrawl = false;
+    if (doCrawl) {
+      connector.startCrawling();
+    }
     // TODO: make sure we flush the pending batches!
     // connector.flush();
     // poll to make sure the connector is still running./
@@ -137,14 +167,14 @@ public static void main(String[] args) throws Exception {
     // when the connector is done, tell the pipeline to flush/
     pipeline.flush();
 
-    // wee! news!
+    // 
 
   }
 
   public void initalize() throws ClassNotFoundException {
     // init the workflow server and load the pipeline config.
     if (workflowServer == null) {
-      workflowServer = WorkflowServer.getInstance();
+      workflowServer = WorkflowServer.getInstance(this);
     }
     workflowServer.addWorkflow(workFlowConfig);
     workflowName = workFlowConfig.getName();
@@ -175,4 +205,30 @@ public boolean onFlush() {
     return true;
   }
 
+  @Override
+  public void publishFlush() {
+    // publish the flush event..
+  }
+  
+  @Override
+  public DocumentPipelineConfig apply(DocumentPipelineConfig c) {
+    super.apply(c);
+
+    this.workFlowConfig = c.workFlowConfig;
+    try {
+      initalize();
+    } catch (ClassNotFoundException e) {
+      log.error("Error initializing the document pipeline.", e);
+      // TODO: shoiuld we throw some runtime here?
+    }
+    return c;
+  }
+
+  @Override
+  public DocumentPipelineConfig getConfig() {
+    super.getConfig();
+    config.workFlowConfig = this.workFlowConfig;
+    return config;
+  }
+
 }
diff --git a/src/main/java/org/myrobotlab/service/DruppNeck.java b/src/main/java/org/myrobotlab/service/DruppNeck.java
index a4f608bd74..1c7aa42e39 100755
--- a/src/main/java/org/myrobotlab/service/DruppNeck.java
+++ b/src/main/java/org/myrobotlab/service/DruppNeck.java
@@ -4,6 +4,7 @@
 import org.myrobotlab.kinematics.DruppIKSolver;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.math.MathUtils;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.ServoControl;
 
 /**
@@ -17,7 +18,7 @@
  * @author kwatters
  *
  */
-public class DruppNeck extends Service {
+public class DruppNeck extends Service {
 
   private static final long serialVersionUID = 1L;
   // 3 servos for the drupp neck
diff --git a/src/main/java/org/myrobotlab/service/EddieControlBoard.java b/src/main/java/org/myrobotlab/service/EddieControlBoard.java
index 3ee28c11b9..7ffe8a822f 100644
--- a/src/main/java/org/myrobotlab/service/EddieControlBoard.java
+++ b/src/main/java/org/myrobotlab/service/EddieControlBoard.java
@@ -9,6 +9,7 @@
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.math.MapperLinear;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.data.JoystickData;
 import org.myrobotlab.service.interfaces.JoystickListener;
 import org.myrobotlab.service.interfaces.KeyListener;
@@ -21,7 +22,7 @@
  * EddieControlBoard It can publish sensor data , control motors and more!
  *
  */
-public class EddieControlBoard extends Service implements KeyListener, SerialDataListener, JoystickListener {
+public class EddieControlBoard extends Service implements KeyListener, SerialDataListener, JoystickListener {
 
   class SensorPoller extends Thread {
 
diff --git a/src/main/java/org/myrobotlab/service/Elasticsearch.java b/src/main/java/org/myrobotlab/service/Elasticsearch.java
index 52aa7c0d94..e3d5e41076 100644
--- a/src/main/java/org/myrobotlab/service/Elasticsearch.java
+++ b/src/main/java/org/myrobotlab/service/Elasticsearch.java
@@ -6,12 +6,13 @@
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 import pl.allegro.tech.embeddedelasticsearch.EmbeddedElastic;
 import pl.allegro.tech.embeddedelasticsearch.PopularProperties;
 
-public class Elasticsearch extends Service {
+public class Elasticsearch extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Email.java b/src/main/java/org/myrobotlab/service/Email.java
index c7d3eb1ed6..24a1414ed0 100644
--- a/src/main/java/org/myrobotlab/service/Email.java
+++ b/src/main/java/org/myrobotlab/service/Email.java
@@ -25,7 +25,6 @@
 import org.myrobotlab.service.data.ImageData;
 import org.slf4j.Logger;
 
-
 /**
  * 
  * Basic smtp at the moment. It can send a email with image through gmail.
@@ -36,7 +35,7 @@
  * @author grog
  *
  */
-public class Email extends Service {
+public class Email extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Emoji.java b/src/main/java/org/myrobotlab/service/Emoji.java
index bb8ff1ee30..64930f2923 100644
--- a/src/main/java/org/myrobotlab/service/Emoji.java
+++ b/src/main/java/org/myrobotlab/service/Emoji.java
@@ -28,7 +28,7 @@
 // emotionListener
 // Links
 // - http://googleemotionalindex.com/
-public class Emoji extends Service implements TextListener, EventHandler, ImagePublisher {
+public class Emoji extends Service implements TextListener, EventHandler, ImagePublisher {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Esp8266.java b/src/main/java/org/myrobotlab/service/Esp8266.java
index adaf5cb424..afdd32a9ae 100644
--- a/src/main/java/org/myrobotlab/service/Esp8266.java
+++ b/src/main/java/org/myrobotlab/service/Esp8266.java
@@ -5,9 +5,10 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
-public class Esp8266 extends Service {
+public class Esp8266 extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Esp8266_01.java b/src/main/java/org/myrobotlab/service/Esp8266_01.java
index 232ff6a8da..6630ea434f 100644
--- a/src/main/java/org/myrobotlab/service/Esp8266_01.java
+++ b/src/main/java/org/myrobotlab/service/Esp8266_01.java
@@ -20,6 +20,7 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.I2CControl;
 import org.myrobotlab.service.interfaces.I2CController;
 import org.slf4j.Logger;
@@ -32,7 +33,7 @@
  * 
  */
 // TODO Ensure that only one instance of RasPi can execute on each RaspBerry PI
-public class Esp8266_01 extends Service implements I2CController {
+public class Esp8266_01 extends Service implements I2CController {
 
   public static class I2CDeviceMap {
     public transient I2CControl control;
diff --git a/src/main/java/org/myrobotlab/service/FileConnector.java b/src/main/java/org/myrobotlab/service/FileConnector.java
index 7e755ff027..3b9d9c95d6 100644
--- a/src/main/java/org/myrobotlab/service/FileConnector.java
+++ b/src/main/java/org/myrobotlab/service/FileConnector.java
@@ -7,12 +7,15 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Date;
 
 import org.myrobotlab.document.Document;
 import org.myrobotlab.document.connector.AbstractConnector;
 import org.myrobotlab.document.connector.ConnectorState;
 import org.myrobotlab.document.transformer.ConnectorConfig;
 import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.config.FileConnectorConfig;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.interfaces.DocumentPublisher;
 import org.slf4j.Logger;
 
@@ -20,10 +23,11 @@ public class FileConnector extends AbstractConnector implements DocumentPublishe
 
   public final static Logger log = LoggerFactory.getLogger(FileConnector.class.getCanonicalName());
   private static final long serialVersionUID = 1L;
-  private String directory;
+  // private String directory;
+  private FileConnectorConfig config = new FileConnectorConfig();
   // TODO: add wildcard includes/excludes
   // TODO: add file path includes/excludes
-  private boolean interrupted = false;
+  private volatile boolean interrupted = false;
 
   public FileConnector(String name, String id) {
     super(name, id);
@@ -38,22 +42,28 @@ public void setConfig(ConnectorConfig config) {
   @Override
   public void startCrawling() {
     state = ConnectorState.RUNNING;
-    Path startPath = Paths.get(directory);
+    Path startPath = Paths.get(((FileConnectorConfig)config).directory);
+    log.info("Started Crawling {}", startPath);
     try {
       Files.walkFileTree(startPath, this);
     } catch (IOException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
     }
+    // we're done.. publish a flush so other down stream components know to flush any partial batches they might have.
+    invoke("publishFlush");
     log.info("File Connector finished walking the tree.");
+    
     // TODO: should we flush here immediately?
     state = ConnectorState.STOPPED;
   }
 
   @Override
   public void stopCrawling() {
+    log.info("Stop crawling requested...");    
     interrupted = true;
     state = ConnectorState.INTERRUPTED;
+//    notify();
   }
 
   @Override
@@ -69,10 +79,11 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
     }
     String docId = getDocIdPrefix() + file.toFile().getAbsolutePath();
     Document doc = new Document(docId);
-    doc.setField("last_modified", attrs.lastModifiedTime());
-    doc.setField("created_date", attrs.creationTime());
-    doc.setField("filename", file.toFile().getAbsolutePath());
+    doc.setField("last_modified", new Date(attrs.lastModifiedTime().toMillis()));
+    doc.setField("created_date", new Date(attrs.creationTime().toMillis()));
+    doc.setField("filepath", file.toFile().getAbsolutePath());
     doc.setField("size", attrs.size());
+    doc.setField("type", "file");
     // TODO: potentially add a byte array of the file
     // or maybe an input stream or other handle to the file.
     feed(doc);
@@ -81,7 +92,20 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
 
   @Override
   public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
-    throw exc;
+    if (interrupted) {
+      state = ConnectorState.INTERRUPTED;
+      return FileVisitResult.TERMINATE;
+    }
+    String docId = getDocIdPrefix() + file.toFile().getAbsolutePath();
+    Document doc = new Document(docId);
+    doc.setField("type", "file");
+    // TODO: how does this serialize?
+    doc.setField("error", exc);
+    // doc.setField("timestamp", new Date());
+    feed(doc);
+    log.warn("Exception processing {}", file, exc);
+    // Keep going!!!
+    return FileVisitResult.CONTINUE;
   }
 
   @Override
@@ -90,15 +114,30 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx
       throw exc;
     }
     return FileVisitResult.CONTINUE;
-
   }
 
   public String getDirectory() {
-    return directory;
+    return config.directory;
   }
 
   public void setDirectory(String directory) {
-    this.directory = directory;
+    config.directory = directory;
   }
 
+  @Override
+  public ServiceConfig apply(ServiceConfig c) {
+    super.apply(c);
+    // anything else?
+    return c;
+  }
+
+  @Override
+  public ServiceConfig getConfig() {
+    // return the config
+    // we need the super stuff here.
+    FileConnectorConfig config = (FileConnectorConfig)super.getConfig();
+    // this is goofy..
+    config.directory = this.config.directory;
+    return config;
+  }
 }
diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
index 83f23d36cf..a093f61565 100644
--- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
+++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
@@ -32,7 +32,7 @@
  * 
  * @author GroG
  */
-public class FiniteStateMachine extends Service {
+public class FiniteStateMachine extends Service {
 
   public final static Logger log = LoggerFactory.getLogger(FiniteStateMachine.class);
 
@@ -227,17 +227,17 @@ public String publishNewState(String state) {
   }
 
   @Override
-  public ServiceConfig getConfig() {
-    FiniteStateMachineConfig c = (FiniteStateMachineConfig) super.getConfig();
-    c.current = getCurrent();
-    c.messageListeners = new ArrayList<>();
-    c.messageListeners.addAll(messageListeners);
-    return c;
+  public FiniteStateMachineConfig getConfig() {
+    super.getConfig();
+    config.current = getCurrent();
+    config.messageListeners = new ArrayList<>();
+    config.messageListeners.addAll(messageListeners);
+    return config;
   }
 
   @Override
-  public ServiceConfig apply(ServiceConfig c) {
-    FiniteStateMachineConfig config = (FiniteStateMachineConfig) super.apply(c);
+  public FiniteStateMachineConfig apply(FiniteStateMachineConfig c) {
+    super.apply(c);
 
     if (config.transitions != null) {
 
@@ -245,20 +245,20 @@ public ServiceConfig apply(ServiceConfig c) {
       // when config is "applied" we need to copy out and
       // re-apply the config using addTransition
       List newTransistions = new ArrayList<>();
-      newTransistions.addAll(config.transitions);
+      newTransistions.addAll(c.transitions);
       clear();
       for (Transition t : newTransistions) {
         addTransition(t.from, t.event, t.to);
       }
 
       messageListeners = new HashSet<>();
-      messageListeners.addAll(config.messageListeners);
+      messageListeners.addAll(c.messageListeners);
       broadcastState();
     }
 
     // setCurrent
-    if (config.current != null) {
-      setCurrent(config.current);
+    if (c.current != null) {
+      setCurrent(c.current);
     }
 
     return c;
diff --git a/src/main/java/org/myrobotlab/service/Git.java b/src/main/java/org/myrobotlab/service/Git.java
index 5378f3f53c..1d2d3766f5 100644
--- a/src/main/java/org/myrobotlab/service/Git.java
+++ b/src/main/java/org/myrobotlab/service/Git.java
@@ -36,9 +36,10 @@
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
-public class Git extends Service {
+public class Git extends Service {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/GoPro.java b/src/main/java/org/myrobotlab/service/GoPro.java
index 776230fdbe..c8fcd079fa 100644
--- a/src/main/java/org/myrobotlab/service/GoPro.java
+++ b/src/main/java/org/myrobotlab/service/GoPro.java
@@ -5,9 +5,10 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.GoProConfig;
 import org.slf4j.Logger;
 
-public class GoPro extends Service {
+public class GoPro extends Service {
 
   transient public HttpClient http;
 
diff --git a/src/main/java/org/myrobotlab/service/GoogleCloud.java b/src/main/java/org/myrobotlab/service/GoogleCloud.java
index 07ae425095..feb64b0999 100644
--- a/src/main/java/org/myrobotlab/service/GoogleCloud.java
+++ b/src/main/java/org/myrobotlab/service/GoogleCloud.java
@@ -8,6 +8,7 @@
 
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 import com.google.cloud.vision.v1.AnnotateImageRequest;
@@ -40,7 +41,7 @@
 
 //[END import_libraries]
 
-public class GoogleCloud extends Service {
+public class GoogleCloud extends Service {
 
   private static final long serialVersionUID = 1L;
   final static Logger log = LoggerFactory.getLogger(GoogleCloud.class);
diff --git a/src/main/java/org/myrobotlab/service/GoogleSearch.java b/src/main/java/org/myrobotlab/service/GoogleSearch.java
index 749adf3263..4eceb24eed 100644
--- a/src/main/java/org/myrobotlab/service/GoogleSearch.java
+++ b/src/main/java/org/myrobotlab/service/GoogleSearch.java
@@ -20,7 +20,6 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.config.GoogleSearchConfig;
-import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.data.ImageData;
 import org.myrobotlab.service.data.Locale;
 import org.myrobotlab.service.data.SearchResults;
@@ -31,7 +30,7 @@
 import org.myrobotlab.service.interfaces.TextPublisher;
 import org.slf4j.Logger;
 
-public class GoogleSearch extends Service implements ImagePublisher, TextPublisher, SearchPublisher, LocaleProvider {
+public class GoogleSearch extends Service implements ImagePublisher, TextPublisher, SearchPublisher, LocaleProvider {
 
   private static final long serialVersionUID = 1L;
 
@@ -190,7 +189,6 @@ public byte[] saveFile(String filename, byte[] data) throws IOException {
     return data;
   }
 
-  // FIXME - use gson not simpl json
   @Override
   public List imageSearch(String searchText) {
 
diff --git a/src/main/java/org/myrobotlab/service/GoogleTranslate.java b/src/main/java/org/myrobotlab/service/GoogleTranslate.java
index 8e1899f8c0..ce892a709e 100644
--- a/src/main/java/org/myrobotlab/service/GoogleTranslate.java
+++ b/src/main/java/org/myrobotlab/service/GoogleTranslate.java
@@ -8,6 +8,7 @@
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.config.ServiceConfig;
 import org.slf4j.Logger;
 
 import com.google.auth.oauth2.GoogleCredentials;
@@ -15,7 +16,7 @@
 import com.google.cloud.translate.TranslateOptions;
 import com.google.cloud.translate.Translation;
 
-public class GoogleTranslate extends Service {
+public class GoogleTranslate extends Service {
 
   private static final long serialVersionUID = 1L;
 
@@ -57,25 +58,6 @@ public String translate(String text) throws FileNotFoundException, IOException {
   }
   
 
-  /**
-   * The methods apply and getConfig can be used, if more complex configuration handling is needed.
-   * By default, the framework takes care of most of it, including subscription handling.
-   * 
-  @Override
-  public ServiceConfig apply(ServiceConfig c) {
-    // _TemplateServiceConfig config = (_TemplateService)super.apply(c);
-    // if more complex config handling is needed
-    return c;
-  }
-
-  @Override
-  public ServiceConfig getConfig() {
-    // _TemplateServiceConfig config = (_TemplateService)super.getConfig();
-    return config;
-  }
-  
- **/ - public static void main(String[] args) { try { diff --git a/src/main/java/org/myrobotlab/service/Gps.java b/src/main/java/org/myrobotlab/service/Gps.java index 704e675853..4dce48a0ca 100644 --- a/src/main/java/org/myrobotlab/service/Gps.java +++ b/src/main/java/org/myrobotlab/service/Gps.java @@ -13,6 +13,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.GpsConfig; import org.myrobotlab.service.interfaces.SerialDataListener; import org.myrobotlab.service.interfaces.SerialDevice; import org.slf4j.Logger; @@ -37,7 +38,7 @@ * wandered into our out of a fenced area. * */ -public class Gps extends Service implements SerialDataListener { +public class Gps extends Service implements SerialDataListener { /*********************************************************************************** * This block of methods will be used to GeoFencing This code is based on the diff --git a/src/main/java/org/myrobotlab/service/Gpt3.java b/src/main/java/org/myrobotlab/service/Gpt3.java index 5326427b56..19da929f49 100644 --- a/src/main/java/org/myrobotlab/service/Gpt3.java +++ b/src/main/java/org/myrobotlab/service/Gpt3.java @@ -1,12 +1,13 @@ package org.myrobotlab.service; import java.io.IOException; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Service; +import org.myrobotlab.framework.StaticType; +import org.myrobotlab.framework.Status; import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; @@ -47,7 +48,7 @@ * @author GroG * */ -public class Gpt3 extends Service implements TextListener, TextPublisher, UtterancePublisher, UtteranceListener, ResponsePublisher { +public class Gpt3 extends Service implements TextListener, TextPublisher, UtterancePublisher, UtteranceListener, ResponsePublisher { private static final long serialVersionUID = 1L; @@ -87,24 +88,48 @@ public Response getResponse(String text) { if (!c.sleeping) { - String json = + // chat completions + String json = + "{\r\n" + + " \"model\": \""+ c.engine +"\",\r\n" + + " \"messages\": [{\"role\": \"user\", \"content\": \""+ text +"\"}],\r\n" + + " \"temperature\": 0.7\r\n" + + " }"; + +// completions +// String json = +// "{\r\n" + " \"model\": \"" + c.engine + "\",\r\n" + " \"prompt\": \"" + text + "\",\r\n" + " \"temperature\": " + c.temperature + ",\r\n" + " \"max_tokens\": " +// + c.maxTokens + ",\r\n" + " \"top_p\": 1,\r\n" + " \"frequency_penalty\": 0,\r\n" + " \"presence_penalty\": 0\r\n" + "}"; - "{\r\n" + " \"model\": \"" + c.engine + "\",\r\n" + " \"prompt\": \"" + text + "\",\r\n" + " \"temperature\": " + c.temperature + ",\r\n" + " \"max_tokens\": " - + c.maxTokens + ",\r\n" + " \"top_p\": 1,\r\n" + " \"frequency_penalty\": 0,\r\n" + " \"presence_penalty\": 0\r\n" + "}"; HttpClient http = (HttpClient) startPeer("http"); String msg = http.postJson(c.token, c.url, json); + Map payload = CodecUtils.fromJson(msg, new StaticType<>() {}); + @SuppressWarnings({ "unchecked", "rawtypes" }) - Map payload = (Map) CodecUtils.fromJson(msg, LinkedHashMap.class); + Map errors = (Map)payload.get("error"); + if (errors != null) { + error((String)errors.get("message")); + } @SuppressWarnings({ "unchecked", "rawtypes" }) List choices = (List) payload.get("choices"); if (choices != null && choices.size() > 0) { @SuppressWarnings({ "unchecked", "rawtypes" }) Map textObject = (Map) choices.get(0); responseText = (String) textObject.get("text"); - invoke("publishText", responseText); + if (responseText != null) { + // /completions + invoke("publishText", responseText); + } else { + // /chat/completions + @SuppressWarnings({ "unchecked", "rawtypes" }) + Map content = (Map)textObject.get("message"); + // role=assistant + responseText = (String)content.get("content"); + } + } else { warn("no response for %s", text); @@ -141,6 +166,23 @@ public Response getResponse(String text) { return null; } + /** + * Overridden error to also publish the errors + * probably would be a better solution to self subscribe to errors and + * have the subscriptions publish utterances/responses/text + */ + @Override + public Status error(String error) { + Status status = super.error(error); + invoke("publishText", error); + Response response = new Response("friend", getName(), error, null); + Utterance utterance = new Utterance(); + utterance.text = error; + invoke("publishUtterance", utterance); + invoke("publishResponse", response); + return status; + } + public String publishRequest(String text) { return text; } diff --git a/src/main/java/org/myrobotlab/service/Hd44780.java b/src/main/java/org/myrobotlab/service/Hd44780.java index da1750d5c2..9294fee992 100644 --- a/src/main/java/org/myrobotlab/service/Hd44780.java +++ b/src/main/java/org/myrobotlab/service/Hd44780.java @@ -11,8 +11,8 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.Hd44780Config; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.I2CControl; +import org.myrobotlab.service.interfaces.TextListener; import org.slf4j.Logger; /** @@ -24,7 +24,7 @@ * @author Moz4r modified by Ray Edgley. * */ -public class Hd44780 extends Service { +public class Hd44780 extends Service implements TextListener { public final static Logger log = LoggerFactory.getLogger(Hd44780.class); @@ -122,6 +122,10 @@ public Hd44780(String n, String id) { @Override public void attach(String name) { ServiceInterface si = Runtime.getService(name); + if (si == null) { + error("could not attach to %s not found", name); + return; + } if (si instanceof Pcf8574) { attachPcf8574((Pcf8574) si); return; @@ -163,7 +167,7 @@ public void attachPcf8574(Pcf8574 pcf8574) { pcf = pcf8574; isAttached = true; pcfName = pcf.getName(); - pcf.writeRegister((byte) 0b11110000); // Make sure we initilise the PCF8574 + pcf.writeRegister((byte) 0b11110000); // Make sure we initialize the PCF8574 // output state broadcastState(); } @@ -176,29 +180,32 @@ public void attachPcf8574(Pcf8574 pcf8574) { * display, only the first 16 characters will be used On the 4 line x * 20 display, when writing to Line 0, the 21st character will appear * on line 2 When writing to line 1, the 21st character will appear - * on line 3 + * on line 3. There is a gap in line 2, the setDramAddress method will + * check for valid addresses * @param line * l * */ public void display(String string, int line) { + log.info("display({},{})", string, line); if (!initialized) { init(); } screenContent.put(line, string); + // FIXME a bit sloppy .. should publishText broadcastState(); switch (line) { case 0: setDdramAddress((byte) 0x00); break; case 1: - setDdramAddress((byte) 0x28); + setDdramAddress((byte) 0x40); break; case 2: setDdramAddress((byte) 0x14); break; case 3: - setDdramAddress((byte) 0x3C); + setDdramAddress((byte) 0x54); break; default: error("line %d is invalid, valid line values are 0 - 3"); @@ -206,6 +213,31 @@ public void display(String string, int line) { lcdWriteDataString(string); } + /** + * Write text to the address at preferred location. Remember the line wrap is + * strange for this device. + * + * @param address + * - ddram address position + * @param text + * - the text to write there + */ + public void displayAt(int address, String text) { + setDdramAddress(address); + lcdWriteDataString(text); + } + + /** + * display the text FIXME - should by default scroll if text is larger than + * the width of the hd + * + * @param text + */ + public void display(String text) { + // FIXME - lame, but going to default this way + display(text, 0); + } + /** * Clear lcd and set to home. */ @@ -259,7 +291,8 @@ public void init() { } else { log.info("Init I2C Display"); - setInterface(); // this commands ensures we are in 4 bit mode and our commands are in sync. + setInterface(); // this commands ensures we are in 4 bit mode and our + // commands are in sync. setFunction(true, false); // Set the function Control. clearDisplay(); // Clear the Display and set DDRAM address 0. returnHome(); // Set DDRAM address 0 and return display home. @@ -379,9 +412,9 @@ public boolean getBlinkEnable() { } /** - * Display on/off control. When the display os off, no charaters are displayed - * at all. The cursor is the line under the charater and may be on or off. - * Blink when set to on blinks the entire charater box where the cursor is. + * Display on/off control. When the display os off, no character are displayed + * at all. The cursor is the line under the character and may be on or off. + * Blink when set to on blinks the entire character box where the cursor is. * * @param display * true the display is on. false the display is off. @@ -459,28 +492,29 @@ public void clearDisplay() { * memory. Each memory location corresponds to a position on the display For * both 2 x 16 displays and the 4 x 20 displays: Address 0 is the left most * character on the first line. Address 40 is the left most character on the - * second line. For the 4 x 20 dispalys: Address 20 is the left most character + * second line. For the 4 x 20 display: Address 20 is the left most character * on the third line. Address 60 is the left most character on the fourth - * line. A continous series of writes to the DDRAM will wrap from the end of - * the first line to the start of the thrid line, then back to the second line + * line. A continuous series of writes to the DDRAM will wrap from the end of + * the first line to the start of the third line, then back to the second line * and finally the fourth line. * * @param address */ public void setDdramAddress(int address) { - if (address < 80) { // Make sure the address is in a valid range - lcdWriteCmd((byte) (address | 0b10000000)); - } else { - error("%d Outside allowed DDRAM Address range 0 - 79", address); + if (address < 0 || (address > 40 && address < 63) || address > 108) { + error("%d Outside allowed DDRAM Address range must valid range is 0-40 and 63-108", address); + return; } + lcdWriteCmd((byte) (address | 0b10000000)); } - + + /** - * Set the address to read or write data to the Caracter Generator RAM. The - * HD44780 has a built in 205 charater generator rom as well as a 8 charater + * Set the address to read or write data to the character Generator RAM. The + * HD44780 has a built in 205 character generator from as well as a 8 character * generator RAM. The only the lower 5 bits are used with 8 bytes allocated to * each of the 8 characters that may be used. Not that the last by of each - * charater is not used as this is where the cursor sits. + * character is not used as this is where the cursor sits. * * @param address */ @@ -533,7 +567,7 @@ private void lcdWriteData(byte data) { * Set or clear the test busy flag in the HD44780. There are two ways of * ensuring the HD44780 is ready for the next instruction. You can either read * the instruction register until the MSB D7 is clear. Or you can wait a - * minimum time peiod that ensure the device is ready. The longest busy period + * minimum time period that ensure the device is ready. The longest busy period * is 10mS after a power reset. * * @param setFlag @@ -553,29 +587,37 @@ public boolean getVerifyBusyFlag() { } /** - * This method will first make sure the HD44780 is in a known state - * and that we are syncrnised to the state. - * It does this by setting the module into 8 bit mode - * then setting it back to 4 bit mode. + * This method will first make sure the HD44780 is in a known state and that + * we are synchronized to the state. It does this by setting the module into 8 + * bit mode then setting it back to 4 bit mode. */ private void setInterface() { - byte Blight = 0b00001000; // The backlight bit is not used by the HD44780 chip. + byte Blight = 0b00001000; // The backlight bit is not used by the HD44780 + // chip. if (!backLight) { Blight = 0; } pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Set to 8 bit mode - pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command + pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command sleep(10); - pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 bit command - pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command + pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 + // bit command + pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command sleep(10); - pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 bit command - pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command . We should now be in 8 bit mode and in sync + pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 + // bit command + pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command . We + // should now be in 8 bit + // mode and in sync sleep(10); - pcf.writeRegister((byte) (0b00100000 | En | Blight)); // Now set for 4 bit mode. - pcf.writeRegister((byte) (0b00100000 | Blight)); // Strobe in command. In theory, we should now be in 4 bit mode. + pcf.writeRegister((byte) (0b00100000 | En | Blight)); // Now set for 4 bit + // mode. + pcf.writeRegister((byte) (0b00100000 | Blight)); // Strobe in command. In + // theory, we should now be + // in 4 bit mode. sleep(10); } + /** * This method will write the cmd value to the instruction register then wait * until it is ready for the next instruction. Most commands are pretty quick @@ -681,8 +723,8 @@ public static void main(String[] args) { * Load the config into memory */ @Override - public ServiceConfig getConfig() { - Hd44780Config config = (Hd44780Config)super.getConfig(); + public Hd44780Config getConfig() { + super.getConfig(); if (pcfName != null) { config.controller = pcfName; } @@ -694,20 +736,26 @@ public ServiceConfig getConfig() { * Applies the config to the service attaching to the PCF8574 if it exists. */ @Override - public ServiceConfig apply(ServiceConfig c) { - Hd44780Config config = (Hd44780Config) super.apply(c); + public Hd44780Config apply(Hd44780Config c) { + super.apply(c); - if (config.controller != null) { + if (c.controller != null) { try { - attach(config.controller); + attach(c.controller); } catch (Exception e) { error(e); } } - if (pcf != null && config.backlight != null && config.backlight) { - setBackLight(config.backlight); + if (pcf != null && c.backlight != null && c.backlight) { + setBackLight(c.backlight); } return c; } + + @Override + public void onText(String text) throws Exception { + display(text); + } + } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/service/HtmlFilter.java b/src/main/java/org/myrobotlab/service/HtmlFilter.java index ec3142f3a0..ff0a4b4fb1 100644 --- a/src/main/java/org/myrobotlab/service/HtmlFilter.java +++ b/src/main/java/org/myrobotlab/service/HtmlFilter.java @@ -21,7 +21,7 @@ * @author kwatters * */ -public class HtmlFilter extends Service implements TextListener, TextPublisher, TextFilter { +public class HtmlFilter extends Service implements TextListener, TextPublisher, TextFilter { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/HtmlParser.java b/src/main/java/org/myrobotlab/service/HtmlParser.java index f790e3235a..cb4ace1bba 100644 --- a/src/main/java/org/myrobotlab/service/HtmlParser.java +++ b/src/main/java/org/myrobotlab/service/HtmlParser.java @@ -5,9 +5,10 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; -public class HtmlParser extends Service { +public class HtmlParser extends Service { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(HtmlParser.class); diff --git a/src/main/java/org/myrobotlab/service/HttpClient.java b/src/main/java/org/myrobotlab/service/HttpClient.java index 5da460bbf8..c7caaa2982 100644 --- a/src/main/java/org/myrobotlab/service/HttpClient.java +++ b/src/main/java/org/myrobotlab/service/HttpClient.java @@ -54,6 +54,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.net.InstallCert; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.HttpData; import org.myrobotlab.service.interfaces.HttpDataListener; import org.myrobotlab.service.interfaces.HttpResponseListener; @@ -73,7 +74,7 @@ * - Proxies proxies proxies ! - * https://memorynotfound.com/configure-http-proxy-settings-java/ */ -public class HttpClient extends Service implements TextPublisher { +public class HttpClient extends Service implements TextPublisher { public final static Logger log = LoggerFactory.getLogger(HttpClient.class); diff --git a/src/main/java/org/myrobotlab/service/I2cMux.java b/src/main/java/org/myrobotlab/service/I2cMux.java index cccb01b7c1..c3403a1eb4 100644 --- a/src/main/java/org/myrobotlab/service/I2cMux.java +++ b/src/main/java/org/myrobotlab/service/I2cMux.java @@ -31,7 +31,7 @@ * More Info : https://www.adafruit.com/product/2717 * */ -public class I2cMux extends Service implements I2CControl, I2CController { +public class I2cMux extends Service implements I2CControl, I2CController { private static final long serialVersionUID = 1L; @@ -362,8 +362,8 @@ public String getAddress() { } @Override - public ServiceConfig getConfig() { - I2cMuxConfig config = (I2cMuxConfig)super.getConfig(); + public I2cMuxConfig getConfig() { + super.getConfig(); // FIXME this should only be in config, no need for local fields config.bus = deviceBus; config.address = deviceAddress; @@ -373,14 +373,14 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - I2cMuxConfig config = (I2cMuxConfig) super.apply(c); + public I2cMuxConfig apply(I2cMuxConfig c) { + super.apply(c); // FIXME - remove all this, it should "only" be in config - deviceBus = config.bus; - deviceAddress = config.address; - i2cDevices = config.i2cDevices; - if (config.controller != null) { - controllerName = config.controller; + deviceBus = c.bus; + deviceAddress = c.address; + i2cDevices = c.i2cDevices; + if (c.controller != null) { + controllerName = c.controller; } return c; } diff --git a/src/main/java/org/myrobotlab/service/IBus.java b/src/main/java/org/myrobotlab/service/IBus.java index e0c62fb281..0ea0d04b4e 100644 --- a/src/main/java/org/myrobotlab/service/IBus.java +++ b/src/main/java/org/myrobotlab/service/IBus.java @@ -4,6 +4,7 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.SerialDataListener; import org.myrobotlab.service.interfaces.SerialDevice; import org.slf4j.Logger; @@ -14,7 +15,7 @@ * @author aanon4 * */ -public class IBus extends Service implements SerialDataListener { +public class IBus extends Service implements SerialDataListener { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/ImageDisplay.java b/src/main/java/org/myrobotlab/service/ImageDisplay.java index e89f866566..d115227158 100644 --- a/src/main/java/org/myrobotlab/service/ImageDisplay.java +++ b/src/main/java/org/myrobotlab/service/ImageDisplay.java @@ -34,13 +34,12 @@ import org.myrobotlab.net.Http; import org.myrobotlab.service.config.ImageDisplayConfig; import org.myrobotlab.service.config.ImageDisplayConfig.Display; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.ImageData; import org.myrobotlab.service.interfaces.ImageListener; import org.myrobotlab.service.interfaces.ImagePublisher; import org.slf4j.Logger; -public class ImageDisplay extends Service implements ImageListener, MouseListener, ActionListener, MouseMotionListener { +public class ImageDisplay extends Service implements ImageListener, MouseListener, ActionListener, MouseMotionListener { final static Logger log = LoggerFactory.getLogger(ImageDisplay.class); @@ -80,15 +79,15 @@ public void actionPerformed(ActionEvent arg0) { } @Override - public ServiceConfig apply(ServiceConfig c) { - ImageDisplayConfig config = (ImageDisplayConfig) super.apply(c); - if (config.displays != null) { - for (String displayName : config.displays.keySet()) { + public ImageDisplayConfig apply(ImageDisplayConfig c) { + super.apply(c); + if (c.displays != null) { + for (String displayName : c.displays.keySet()) { close(displayName); displayInternal(displayName); } } - return config; + return c; } @Override diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 95a34a3bb6..51dac5ae44 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -44,7 +44,7 @@ import org.myrobotlab.service.interfaces.TextPublisher; import org.slf4j.Logger; -public class InMoov2 extends Service implements ServiceLifeCycleListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener { +public class InMoov2 extends Service implements ServiceLifeCycleListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener { public final static Logger log = LoggerFactory.getLogger(InMoov2.class); @@ -93,159 +93,56 @@ public static boolean loadFile(String file) { return true; } - public static void main(String[] args) { - try { - - LoggingFactory.init(Level.ERROR); - // Platform.setVirtual(true); - // Runtime.start("s01", "Servo"); - // Runtime.start("intro", "Intro"); - - // Runtime.startConfig("pr-1213-1"); - - Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); - - boolean done = true; - if (done) { - return; - } - - - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - // webgui.setSsl(true); - webgui.autoStartBrowser(false); - // webgui.setPort(8888); - webgui.startService(); - - Runtime.start("python", "Python"); - // Runtime.start("ros", "Ros"); - Runtime.start("intro", "Intro"); - // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - // i01.startPeer("simulator"); - // Runtime.startConfig("i01-05"); - // Runtime.startConfig("pir-01"); - - // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly"); - // i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - - - // polly.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); - // i01.startPeer("mouth"); - // i01.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); - - Runtime.start("python", "Python"); - - // i01.startSimulator(); - Plan plan = Runtime.load("webgui", "WebGui"); - // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui"); - // webgui.autoStartBrowser = false; - Runtime.startConfig("webgui"); - Runtime.start("webgui", "WebGui"); - - Random random = (Random) Runtime.start("random", "Random"); - - random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - - random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); - random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); - - random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - - random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); - random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0); - - random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0); - random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0); - - random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0); - random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0); - - random.save(); - -// i01.startChatBot(); -// -// i01.startAll("COM3", "COM4"); - Runtime.start("python", "Python"); - - } catch (Exception e) { - log.error("main threw", e); - } - } - - transient ProgramAB chatBot; + protected transient ProgramAB chatBot; protected List configList; - String currentConfigurationName = "default"; + /** + * Configuration from runtime has started. This is when runtime starts + * processing a configuration set for the first time since inmoov was started + */ + protected boolean configStarted = false; - transient SpeechRecognizer ear; + String currentConfigurationName = "default"; - // - // public void onStartConfig(String configName) { - // log.info("onStartConfig"); - // - // } + protected transient SpeechRecognizer ear; - transient Tracking eyesTracking; // waiting controable threaded gestures we warn user - boolean gestureAlreadyStarted = false; + protected boolean gestureAlreadyStarted = false; - Set gestures = new TreeSet(); + protected Set gestures = new TreeSet(); - transient Tracking headTracking; + protected transient HtmlFilter htmlFilter; - transient HtmlFilter htmlFilter; + protected transient ImageDisplay imageDisplay; - transient ImageDisplay imageDisplay; + protected String lastGestureExecuted; - String lastGestureExecuted; + protected Long lastPirActivityTime; - Long lastPirActivityTime; - - LedDisplayData led = new LedDisplayData(); + protected LedDisplayData led = new LedDisplayData(); /** * supported locales */ - Map locales = null; - - int maxInactivityTimeSeconds = 120; + protected Map locales = null; - transient SpeechSynthesis mouth; + protected int maxInactivityTimeSeconds = 120; - // FIXME ugh - new MouthControl service that uses AudioFile output - transient public MouthControl mouthControl; + protected transient SpeechSynthesis mouth; - boolean mute = false; + protected boolean mute = false; - // transient JMonkeyEngine simulator; + protected transient OpenCV opencv; - transient OpenCV opencv; - - transient Pir pir; - - transient Python python; - - transient ServoMixer servoMixer; - - transient UltrasonicSensor ultrasonicLeft; - - transient UltrasonicSensor ultrasonicRight; + protected transient Python python; protected String voiceSelected; - transient WebGui webgui; public InMoov2(String n, String id) { super(n, id); - Runtime.getInstance().attachServiceLifeCycleListener(getName()); } public void addTextListener(TextListener service) { @@ -254,25 +151,25 @@ public void addTextListener(TextListener service) { } @Override - public ServiceConfig apply(ServiceConfig c) { - InMoov2Config config = (InMoov2Config) super.apply(c); + public InMoov2Config apply(InMoov2Config c) { + super.apply(c); try { locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR"); - if (config.locale != null) { - setLocale(config.locale); + if (c.locale != null) { + setLocale(c.locale); } else { setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString())); } - init(); + loadInitScripts(); - if (config.loadGestures) { + if (c.loadGestures) { loadGestures(); } - if (config.heartbeat) { + if (c.heartbeat) { startHeartbeat(); } else { stopHeartbeat(); @@ -284,13 +181,7 @@ public ServiceConfig apply(ServiceConfig c) { return c; } - // FIXME FIXME !!! THIS IS A MESS !! - public void applyConfig() { - super.apply(); - log.error("applyConfig()"); - // always getResponse ! - speak("InMoov apply config"); - } + @Override public void attachTextListener(String name) { @@ -509,13 +400,16 @@ public boolean exec(String pythonCode) { */ public String execGesture(String gesture) { + // FIXME PUB SUB - THIS SHOULD JUST PUBLISH TO publishPython + // although its problematic if this call is to be synchronous ... + subscribe("python", "publishStatus", this.getName(), "onGestureStatus"); + startedGesture(gesture); lastGestureExecuted = gesture; + Python python = (Python)Runtime.getService("python"); if (python == null) { - log.warn("execGesture : No jython engine..."); + error("python service not started"); return null; } - subscribe(python.getName(), "publishStatus", this.getName(), "onGestureStatus"); - startedGesture(lastGestureExecuted); return python.evalAndWait(gesture); } @@ -588,10 +482,6 @@ public InMoov2Arm getArm(String side) { return (InMoov2Arm) getPeer(side + "Arm"); } - public Tracking getEyesTracking() { - return eyesTracking; - } - public InMoov2Hand getHand(String side) { if (!"left".equals(side) && !"right".equals(side)) { error("side must be left or right - instead of %s", side); @@ -604,10 +494,6 @@ public InMoov2Head getHead() { return (InMoov2Head) getPeer("head"); } - public Tracking getHeadTracking() { - return headTracking; - } - /** * finds most recent activity * @@ -686,10 +572,6 @@ public InMoov2Hand getRightHand() { return (InMoov2Hand) getPeer("rightHand"); } - public Simulator getSimulator() { - return (Simulator) getPeer("simulator"); - } - /** * matches on language only not variant expands language match to full InMoov2 * bot locale @@ -738,8 +620,7 @@ public void halfSpeed() { * * @throws IOException */ - public void init() throws IOException { - invoke("publishEvent", "INIT"); + public void loadInitScripts() throws IOException { loadScripts(getResourceDir() + fs + "init"); } @@ -880,21 +761,24 @@ public void moveHead(Integer neck, Integer rothead, Integer rollNeck) { moveHead((double) neck, (double) rothead, null, null, null, (double) rollNeck); } - public void moveHeadBlocking(Double neck, Double rothead) { + public void moveHeadBlocking(Double neck, Double rothead) { moveHeadBlocking(neck, rothead, null); } - public void moveHeadBlocking(Double neck, Double rothead, Double rollNeck) { + public void moveHeadBlocking(Double neck, Double rothead, Double rollNeck) { moveHeadBlocking(neck, rothead, null, null, null, rollNeck); } - public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw) { + public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw) { moveHeadBlocking(neck, rothead, eyeX, eyeY, jaw, null); } - public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { - // the "right" way - sendToPeer("head", "moveToBlocking", neck, rothead, eyeX, eyeY, jaw, rollNeck); + public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { + try { + sendBlocking(getPeerName("head"), "moveToBlocking", neck, rothead, eyeX, eyeY, jaw, rollNeck); + } catch (Exception e) { + error(e); + } } public void moveLeftArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { @@ -962,7 +846,7 @@ public void onCreated(String fullname) { public void onFinishedConfig(String configName) { log.info("onFinishedConfig"); // invoke("publishEvent", "configFinished"); - invoke("publishEvent", "CONFIG LOADED"); + invoke("publishFinishedConfig", configName); } public void onGestureStatus(Status status) { @@ -970,7 +854,8 @@ public void onGestureStatus(Status status) { error("I cannot execute %s, please check logs", lastGestureExecuted); } finishedGesture(lastGestureExecuted); - unsubscribe(python.getName(), "publishStatus", this.getName(), "onGestureStatus"); + + unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } @Override @@ -988,8 +873,9 @@ public void onJointAngles(Map angleMap) { @Override public void onJoystickInput(JoystickData input) throws Exception { - // TODO Auto-generated method stub - + // TODO timer ? to test and not send an event + // switches to manual control ? + invoke("publishEvent", "joystick"); } public String onNewState(String state) { @@ -1010,6 +896,7 @@ public String onNewState(String state) { } public OpenCVData onOpenCVData(OpenCVData data) { + // FIXME - publish event with or without data ? String file reference return data; } @@ -1024,7 +911,7 @@ public void onPirOn() { led.blue = 150; led.count = 5; led.interval = 500; - + // FIXME flash on config.flashOnBoot invoke("publishFlash"); // pirOn event vs wake event invoke("publishEvent", "WAKE"); @@ -1053,8 +940,6 @@ public void onRegistered(Registration registration) { @Override public void onReleased(String name) { - // TODO Auto-generated method stub - } // I THINK THIS IS GOOD (good simple one)- need more info though @@ -1070,6 +955,16 @@ public boolean onSense(boolean b) { return b; } + /** + * runtime re-publish relay + * + * @param configName + */ + public void onStartConfig(String configName) { + log.info("onStartConfig"); + invoke("publishStartConfig", configName); + } + /** * Part of service life cycle - a new servo has been started * @@ -1079,10 +974,11 @@ public boolean onSense(boolean b) { */ @Override public void onStarted(String name) { - log.info("{} started", name); + InMoov2Config c = (InMoov2Config) config; + + log.info("onStarted {}", name); try { - InMoov2Config c = (InMoov2Config) config; Runtime runtime = Runtime.getInstance(); log.info("onStarted {}", name); @@ -1092,83 +988,113 @@ public void onStarted(String name) { // } String peerKey = getPeerKey(name); - if (peerKey != null) { - invoke("publishEvent", "STARTED " + peerKey); - } - - String actualName = getPeerName("ear"); - if (actualName.equals(name)) { - AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) Runtime.getService(actualName); - ear.attachTextListener(getPeerName("chatBot")); - } - - actualName = getPeerName("mouth"); - if (actualName.equals(name)) { - AbstractSpeechSynthesis mouth = (AbstractSpeechSynthesis) Runtime.getService(actualName); - mouth.attachSpeechListener(getPeerName("ear")); - } - - actualName = getPeerName("chatBot"); - if (actualName.equals(name)) { - ProgramAB chatBot = (ProgramAB) Runtime.getService(actualName); - chatBot.attachTextListener(getPeerName("htmlFilter")); - startPeer("htmlFilter"); - } - - actualName = getPeerName("htmlFilter"); - if (actualName.equals(name)) { - TextPublisher htmlFilter = (TextPublisher) Runtime.getService(actualName); - htmlFilter.attachTextListener(getPeerName("mouth")); - } - - // FIXME - this is a much better way to do it than the code above - // the types are not exposed, its not using a local reference of the - // service - // its remote compatible, and it uses the peers actual name - it does not - // break easily, - // allows user modification and subscribing to topics for extensibility, - // no NPEs uses messaging, and follows mrl pub/sub conventions - above - // code should be - // refactored accordingly. - actualName = getPeerName("head"); - if (actualName.equals(name)) { - addListener("publishMoveHead", actualName); - } - - actualName = getPeerName("torso"); - if (actualName.equals(name)) { - addListener("publishMoveTorso", actualName); - } - - // mapping a channel to a single end point depending on peer - actualName = getPeerName("leftHand"); - if (actualName.equals(name)) { - addListener("publishMoveLeftHand", actualName, "onMoveHand"); - } - - // mapping a channel to a single end point depending on peer - actualName = getPeerName("rightHand"); - if (actualName.equals(name)) { - addListener("publishMoveRightHand", actualName, "onMoveHand"); + if (peerKey == null) { + // service not a peer + return; } - // mapping a channel to a single end point depending on peer - actualName = getPeerName("leftArm"); - if (actualName.equals(name)) { - addListener("publishMoveLeftArm", actualName, "onMoveArm"); + if (runtime.isProcessingConfig() && !configStarted) { + invoke("publishEvent", "CONFIG STARTED " + runtime.getConfigName()); + configStarted = true; } - // mapping a channel to a single end point depending on peer - actualName = getPeerName("rightArm"); - if (actualName.equals(name)) { - addListener("publishMoveRightArm", actualName, "onMoveArm"); + invoke("publishEvent", "STARTED " + peerKey); + + switch (peerKey) { + case "audioPlayer": + break; + case "chatBot": + ProgramAB chatBot = (ProgramAB) Runtime.getService(name); + chatBot.attachTextListener(getPeerName("htmlFilter")); + startPeer("htmlFilter"); + break; + case "controller3": + break; + case "controller4": + break; + case "ear": + AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) Runtime.getService(name); + ear.attachTextListener(getPeerName("chatBot")); + break; + case "eyeTracking": + break; + case "fsm": + break; + case "gpt3": + break; + case "head": + addListener("publishMoveHead", name); + break; + case "headTracking": + break; + case "htmlFilter": + TextPublisher htmlFilter = (TextPublisher) Runtime.getService(name); + htmlFilter.attachTextListener(getPeerName("mouth")); + break; + case "imageDisplay": + break; + case "leap": + break; + case "left": + break; + case "leftArm": + addListener("publishMoveLeftArm", name, "onMoveArm"); + break; + case "leftHand": + addListener("publishMoveLeftHand", name, "onMoveHand"); + break; + case "mouth": + mouth = (AbstractSpeechSynthesis) Runtime.getService(name); + mouth.attachSpeechListener(getPeerName("ear")); + break; + case "mouthControl": + break; + case "neoPixel": + break; + case "opencv": + subscribeTo(name, "publishOpenCVData"); + break; + case "openni": + break; + case "openWeatherMap": + break; + case "pid": + break; + case "pir": + break; + case "random": + break; + case "right": + break; + case "rightArm": + addListener("publishMoveRightArm", name, "onMoveArm"); + break; + case "rightHand": + addListener("publishMoveRightHand", name, "onMoveHand"); + break; + case "servoMixer": + break; + case "simulator": + break; + case "torso": + addListener("publishMoveTorso", name); + break; + case "ultrasonicRight": + break; + case "ultrasonicLeft": + break; + default: + log.warn("unknown peer %s not hanled in onStarted", peerKey); + break; } + // type processing for Servo ServiceInterface si = Runtime.getService(name); if ("Servo".equals(si.getSimpleName())) { log.info("sending setAutoDisable true to {}", name); - send(name, "setAutoDisable", true); - // ServoControl sc = (ServoControl)Runtime.getService(name); + // send(name, "setAutoDisable", true); + Servo servo = (Servo) Runtime.getService(name); + servo.setAutoDisable(true); } } catch (Exception e) { log.error("onStarted threw", e); @@ -1178,6 +1104,7 @@ public void onStarted(String name) { @Override public void onStopped(String name) { // using release peer for peer releasing + // FIXME - auto remove subscriptions of peers? } @Override @@ -1234,6 +1161,19 @@ public void publish(String name, String method, Object... data) { invoke("publishMessage", msg); } + public String publishStartConfig(String configName) { + info("config %s started", configName); + invoke("publishEvent", "CONFIG STARTED " + configName); + return configName; + } + + public String publishFinishedConfig(String configName) { + info("config %s finished", configName); + invoke("publishEvent", "CONFIG LOADED " + configName); + + return configName; + } + /** * "re"-publishing runtime config list, because I don't want to fix the js * subscribeTo :P @@ -1278,6 +1218,7 @@ public String publishHeartbeat() { /** * A more extensible interface point than publishEvent + * FIXME - create interface for this * * @param msg * @return @@ -1388,7 +1329,7 @@ public String publishText(String text) { public void releasePeer(String peerKey) { super.releasePeer(peerKey); if (peerKey != null) { - invoke("publishEvent","STOPPED " + peerKey); + invoke("publishEvent", "STOPPED " + peerKey); } } @@ -1729,11 +1670,7 @@ public ProgramAB startChatBot() { } } chatBot.setPredicate("parameterHowDoYouDo", ""); - try { - chatBot.savePredicates(); - } catch (IOException e) { - log.error("saving predicates threw", e); - } + chatBot.savePredicates(); htmlFilter = (HtmlFilter) startPeer("htmlFilter");// Runtime.start("htmlFilter", // "HtmlFilter"); chatBot.attachTextListener(htmlFilter); @@ -1828,16 +1765,6 @@ public SpeechSynthesis startMouth() { return mouth; } - public void startMouthControl() { - speakBlocking(get("STARTINGMOUTHCONTROL")); - mouthControl = (MouthControl) startPeer("mouthControl"); - InMoov2Head head = getHead(); - if (head != null) { - mouthControl.attach(head.getPeer("jaw")); - } - mouthControl.attach(getPeer("mouth")); - } - // FIXME - universal (good) way of handling all exceptions - ie - reporting // back to the user the problem in a short concise way but have // expandable detail in appropriate places @@ -1857,19 +1784,13 @@ public ServiceInterface startPeer(String peer) { @Override public void startService() { super.startService(); + InMoov2Config c = (InMoov2Config) config; Runtime runtime = Runtime.getInstance(); - // InMoov2 has a huge amount of peers + // get service start and release life cycle events + runtime.attachServiceLifeCycleListener(getName()); - // by default all servos will auto-disable - // Servo.setAutoDisableDefault(true); //until peer servo services for - // InMoov2 have the auto disable behavior, we should keep this - - // same as created in runtime - send asyc message to all - // registered services, this service has started - // find all servos - set them all to autoDisable(true) - // onStarted(name) will handle all future created servos List services = Runtime.getServices(); for (ServiceInterface si : services) { if ("Servo".equals(si.getSimpleName())) { @@ -1877,34 +1798,21 @@ public void startService() { } } - // REALLY NEEDS TO BE CLEANED UP - no direct references - // "publish" scripts which should be executed :( - // python = (Python) startPeer("python"); - // python = (Python) Runtime.start("python", "Python"); <- BAD !!!! - // load(locale.getTag()); WTH ? - // get events of new services and shutdown - Runtime r = Runtime.getInstance(); - subscribe(r.getName(), "shutdown"); - subscribe(r.getName(), "publishConfigList"); - - // FIXME - Framework should auto-magically auto-start peers AFTER - // construction - unless explicitly told not to - // peers to start on construction - // imageDisplay = (ImageDisplay) startPeer("imageDisplay"); - + subscribe("runtime", "shutdown"); + // power up loopback subscription + addListener(getName(), "powerUp"); + + + subscribe("runtime", "publishConfigList"); if (runtime.isProcessingConfig()) { invoke("publishEvent", "configStarted"); } + subscribe("runtime", "publishStartConfig"); + subscribe("runtime", "publishFinishedConfig"); - // power up loopback subscription - addListener(getName(), "powerUp"); - + // chatbot getresponse attached to publishEvent addListener("publishEvent", getPeerName("chatBot"), "getResponse"); - - // for begin and end of processing config ? - // subscribe("runtime", "publishStartConfig"); - subscribe("runtime", "publishFinishedConfig"); try { // copy config if it doesn't already exist @@ -1924,27 +1832,10 @@ public void startService() { } } } - - // copy (if they don't already exist) the chatbots which came with InMoov2 - resourceBotDir = FileIO.gluePaths(getResourceDir(), "chatbot/bots"); - files = FileIO.getFileList(resourceBotDir); - for (File f : files) { - String botDir = "data/ProgramAB/" + f.getName(); - if (new File(botDir).exists()) { - log.info("found data/ProgramAB/{} not copying", botDir); - } else { - log.info("will copy new data/ProgramAB/{}", botDir); - try { - FileIO.copy(f.getAbsolutePath(), botDir); - } catch (Exception e) { - error(e); - } - } - } - } catch (Exception e) { error(e); } + runtime.invoke("publishConfigList"); } @@ -2020,4 +1911,90 @@ public void waitTargetPos() { sendToPeer("torso", "waitTargetPos"); } + + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.ERROR); + // Platform.setVirtual(true); + // Runtime.start("s01", "Servo"); + // Runtime.start("intro", "Intro"); + + // Runtime.startConfig("pr-1213-1"); + + Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); + + boolean done = true; + if (done) { + return; + } + + + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + // webgui.setSsl(true); + webgui.autoStartBrowser(false); + // webgui.setPort(8888); + webgui.startService(); + + Runtime.start("python", "Python"); + // Runtime.start("ros", "Ros"); + Runtime.start("intro", "Intro"); + // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2"); + // i01.startPeer("simulator"); + // Runtime.startConfig("i01-05"); + // Runtime.startConfig("pir-01"); + + // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly"); + // i01 = (InMoov2) Runtime.start("i01", "InMoov2"); + + + // polly.speakBlocking("Hi, to be or not to be that is the question, + // wheather to take arms against a see of trouble, and by aposing them end + // them, to sleep, to die"); + // i01.startPeer("mouth"); + // i01.speakBlocking("Hi, to be or not to be that is the question, + // wheather to take arms against a see of trouble, and by aposing them end + // them, to sleep, to die"); + + Runtime.start("python", "Python"); + + // i01.startSimulator(); + Plan plan = Runtime.load("webgui", "WebGui"); + // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui"); + // webgui.autoStartBrowser = false; + Runtime.startConfig("webgui"); + Runtime.start("webgui", "WebGui"); + + Random random = (Random) Runtime.start("random", "Random"); + + random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + + random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); + random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); + + random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + + random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); + random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0); + + random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0); + random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0); + + random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0); + random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0); + + random.save(); + +// i01.startChatBot(); +// +// i01.startAll("COM3", "COM4"); + Runtime.start("python", "Python"); + + } catch (Exception e) { + log.error("main threw", e); + } + } + } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java index c15f73bf49..a73f5527b5 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Arm.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Arm.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import org.myrobotlab.framework.Service; @@ -13,6 +12,7 @@ import org.myrobotlab.kinematics.DHRobotArm; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.math.MathUtils; +import org.myrobotlab.service.config.InMoov2ArmConfig; import org.myrobotlab.service.interfaces.IKJointAngleListener; import org.myrobotlab.service.interfaces.ServoControl; import org.slf4j.Logger; @@ -30,7 +30,7 @@ * startPeers() does * */ -public class InMoov2Arm extends Service implements IKJointAngleListener { +public class InMoov2Arm extends Service implements IKJointAngleListener { public final static Logger log = LoggerFactory.getLogger(InMoov2Arm.class); diff --git a/src/main/java/org/myrobotlab/service/InMoov2Hand.java b/src/main/java/org/myrobotlab/service/InMoov2Hand.java index 3c97ccb997..b1744ad03e 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java @@ -13,6 +13,7 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.InMoov2HandConfig; import org.myrobotlab.service.data.LeapData; import org.myrobotlab.service.data.LeapHand; import org.myrobotlab.service.data.PinData; @@ -29,7 +30,7 @@ * * There is also leap motion support. */ -public class InMoov2Hand extends Service implements LeapDataListener, PinArrayListener { +public class InMoov2Hand extends Service implements LeapDataListener, PinArrayListener { public final static Logger log = LoggerFactory.getLogger(InMoov2Hand.class); diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java index 7a7f9db801..be1a72f793 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Head.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java @@ -3,7 +3,6 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; -import java.util.Locale; import java.util.concurrent.ThreadLocalRandom; import org.myrobotlab.framework.Service; @@ -11,6 +10,7 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.InMoov2HeadConfig; import org.myrobotlab.service.interfaces.ServoControl; import org.slf4j.Logger; @@ -19,7 +19,7 @@ * servos for the following: jaw, eyeX, eyeY, rothead and neck. * */ -public class InMoov2Head extends Service { +public class InMoov2Head extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/InMoov2Torso.java b/src/main/java/org/myrobotlab/service/InMoov2Torso.java index d003e7f4c8..d2607a1033 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java @@ -3,13 +3,13 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; -import java.util.Locale; import org.myrobotlab.framework.Service; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.InMoov2TorsoConfig; import org.myrobotlab.service.interfaces.ServoControl; import org.slf4j.Logger; @@ -18,7 +18,7 @@ * midStom, and lowStom servos. * */ -public class InMoov2Torso extends Service { +public class InMoov2Torso extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/IndianTts.java b/src/main/java/org/myrobotlab/service/IndianTts.java index 636b668b36..0ca15d1cc6 100644 --- a/src/main/java/org/myrobotlab/service/IndianTts.java +++ b/src/main/java/org/myrobotlab/service/IndianTts.java @@ -8,6 +8,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; +import org.myrobotlab.service.config.IndianTtsConfig; import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.data.HttpData; import org.slf4j.Logger; @@ -18,7 +19,7 @@ * * http://indiantts.com/ */ -public class IndianTts extends AbstractSpeechSynthesis { +public class IndianTts extends AbstractSpeechSynthesis { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/IntegratedMovement.java b/src/main/java/org/myrobotlab/service/IntegratedMovement.java index 98b55b3fde..cdbb59ef36 100644 --- a/src/main/java/org/myrobotlab/service/IntegratedMovement.java +++ b/src/main/java/org/myrobotlab/service/IntegratedMovement.java @@ -29,6 +29,7 @@ import org.myrobotlab.math.MathUtils; import org.myrobotlab.math.interfaces.Mapper; import org.myrobotlab.openni.OpenNiData; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.IKJointAnglePublisher; import org.myrobotlab.service.interfaces.ServoControl; import org.myrobotlab.service.interfaces.ServoEvent; @@ -51,7 +52,7 @@ * @author Christian/Calamity * */ -public class IntegratedMovement extends Service implements IKJointAnglePublisher { +public class IntegratedMovement extends Service implements IKJointAnglePublisher { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(IntegratedMovement.class); @@ -223,7 +224,9 @@ public static void main(String[] args) throws Exception { // they need to go so that their part they where attach to // move by the input degree Servo mtorso = (Servo) Runtime.start("mtorso", "Servo"); - mtorso.attach(arduino, 26, 90.0); + mtorso.setPin(26); + mtorso.setPosition(90); + mtorso.attach(arduino); mtorso.map(15.0, 165.0, 148.0, 38.0); // mtorso.map(89.9,90.1,93.1,92.9); mtorso.setRest(90.0); @@ -231,69 +234,91 @@ public static void main(String[] args) throws Exception { mtorso.setVelocity(13.0); mtorso.moveTo(90.0); Servo ttorso = (Servo) Runtime.start("ttorso", "Servo"); - ttorso.attach(arduino, 7, 90.0); + ttorso.setPin(7); + ttorso.setPosition(90); + ttorso.attach(arduino); ttorso.map(80.0, 100.0, 92.0, 118.0); // ttorso.setInverted(False) // #ttorso.setMinMax(85,125) ttorso.setVelocity(13.0); ttorso.moveTo(90.0); Servo omoplate = (Servo) Runtime.start("omoplate", "Servo"); - omoplate.attach(arduino, 11, 10.0); + omoplate.setPin(11); + omoplate.setPosition(10); + omoplate.attach(arduino); omoplate.map(10.0, 70.0, 10.0, 70.0); omoplate.setVelocity(15.0); // #omoplate.setMinMax(10,70) omoplate.moveTo(10.0); Servo Romoplate = (Servo) Runtime.start("Romoplate", "Servo"); - Romoplate.attach(arduino, 31, 10.0); + Romoplate.setPin(31); + Romoplate.setPosition(10); + Romoplate.attach(arduino); Romoplate.map(10.0, 70.0, 10.0, 70.0); Romoplate.setVelocity(15.0); // #omoplate.setMinMax(10,70) Romoplate.moveTo(10.0); Servo shoulder = (Servo) Runtime.start("shoulder", "Servo"); - shoulder.attach(arduino, 26, 30.0); + shoulder.setPin(26); + shoulder.setPosition(30); + shoulder.attach(arduino); shoulder.map(0.0, 180.0, 0.0, 180.0); // #shoulder.setMinMax(0,180) shoulder.setVelocity(14.0); shoulder.moveTo(30.0); Servo Rshoulder = (Servo) Runtime.start("Rshoulder", "Servo"); - Rshoulder.attach(arduino, 6, 30.0); + Rshoulder.setPin(6); + Rshoulder.setPosition(30); + Rshoulder.attach(arduino); Rshoulder.map(0.0, 180.0, 0.0, 180.0); // #shoulder.setMinMax(0,180) Rshoulder.setVelocity(14.0); Rshoulder.moveTo(30.0); Servo rotate = (Servo) Runtime.start("rotate", "Servo"); - rotate.attach(arduino, 9, 90.0); + rotate.setPin(9); + rotate.setPosition(90); + rotate.attach(arduino); rotate.map(46.0, 160.0, 46.0, 160.0); // #rotate.setMinMax(46,180) rotate.setVelocity(18.0); rotate.moveTo(90.0); Servo Rrotate = (Servo) Runtime.start("Rrotate", "Servo"); - Rrotate.attach(arduino, 29, 90.0); + Rrotate.setPin(29); + Rrotate.setPosition(90); + Rrotate.attach(arduino); Rrotate.map(46.0, 160.0, 46.0, 160.0); // #rotate.setMinMax(46,180) Rrotate.setVelocity(18.0); Rrotate.moveTo(90.0); Servo bicep = (Servo) Runtime.start("bicep", "Servo"); - bicep.attach(arduino, 8, 10.0); + bicep.setPin(8); + bicep.setPosition(10); + bicep.attach(arduino); bicep.map(5.0, 60.0, 5.0, 80.0); bicep.setVelocity(26.0); // #bicep.setMinMax(5,90) bicep.moveTo(10.0); Servo Rbicep = (Servo) Runtime.start("Rbicep", "Servo"); - Rbicep.attach(arduino, 28, 10.0); + Rbicep.setPin(28); + Rbicep.setPosition(10); + Rbicep.attach(arduino); Rbicep.map(5.0, 60.0, 5.0, 80.0); Rbicep.setVelocity(26.0); // #bicep.setMinMax(5,90) Rbicep.moveTo(10.0); Servo wrist = (Servo) Runtime.start("wrist", "Servo"); - wrist.attach(arduino, 7, 90.0); + wrist.setPin(7); + wrist.setPosition(90); + wrist.attach(arduino); // #wrist.map(45,135,45,135) wrist.map(0.0, 180.0, 0.0, 180.0); wrist.setVelocity(26.0); // #bicep.setMinMax(5,90) wrist.moveTo(90.0); Servo Rwrist = (Servo) Runtime.start("Rwrist", "Servo"); - Rwrist.attach(arduino, 27, 90.0); + Rwrist.setPin(27); + Rwrist.setPosition(90); + Rwrist.attach(arduino); // #wrist.map(45,135,45,135) wrist.map(0.0, 180.0, 0.0, 180.0); Rwrist.setVelocity(26.0); diff --git a/src/main/java/org/myrobotlab/service/Intro.java b/src/main/java/org/myrobotlab/service/Intro.java index d70ab16702..ad64524e3c 100644 --- a/src/main/java/org/myrobotlab/service/Intro.java +++ b/src/main/java/org/myrobotlab/service/Intro.java @@ -13,9 +13,10 @@ import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; -public class Intro extends Service { +public class Intro extends Service { private static final long serialVersionUID = 1L; @@ -36,7 +37,7 @@ public String registered(Registration registration) { } public String released(String fullName) { - String name = CodecUtils.shortName(fullName); + String name = CodecUtils.getShortName(fullName); set(name + "IsActive", false); return name; } diff --git a/src/main/java/org/myrobotlab/service/InverseKinematics.java b/src/main/java/org/myrobotlab/service/InverseKinematics.java index c9afaa6982..9c86b01004 100644 --- a/src/main/java/org/myrobotlab/service/InverseKinematics.java +++ b/src/main/java/org/myrobotlab/service/InverseKinematics.java @@ -5,6 +5,7 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** @@ -13,7 +14,7 @@ * will be replaced with the DH parameter based InverseKinematics3D service. * */ -public class InverseKinematics extends Service { +public class InverseKinematics extends Service { protected IKEngine ikEngine; diff --git a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java index 8d2d2a7fca..05524a2630 100644 --- a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java +++ b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java @@ -14,6 +14,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.math.MathUtils; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.interfaces.IKJointAngleListener; import org.myrobotlab.service.interfaces.IKJointAnglePublisher; @@ -34,7 +35,7 @@ * @author kwatters * */ -public class InverseKinematics3D extends Service implements IKJointAnglePublisher, PointsListener { +public class InverseKinematics3D extends Service implements IKJointAnglePublisher, PointsListener { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(InverseKinematics3D.class); diff --git a/src/main/java/org/myrobotlab/service/IpCamera.java b/src/main/java/org/myrobotlab/service/IpCamera.java index 5a4c7592a0..295668ba62 100644 --- a/src/main/java/org/myrobotlab/service/IpCamera.java +++ b/src/main/java/org/myrobotlab/service/IpCamera.java @@ -14,6 +14,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** @@ -23,7 +24,7 @@ * Android bitmap to buffered image * Bitmap to BufferedImage - conversion once Bitmap class is serialized */ -public class IpCamera extends Service { +public class IpCamera extends Service { public class VideoProcess implements Runnable { @Override diff --git a/src/main/java/org/myrobotlab/service/JFugue.java b/src/main/java/org/myrobotlab/service/JFugue.java index ffdb99c0b1..f30468c6cb 100644 --- a/src/main/java/org/myrobotlab/service/JFugue.java +++ b/src/main/java/org/myrobotlab/service/JFugue.java @@ -31,6 +31,7 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** @@ -38,7 +39,7 @@ * some sounds and music based on string patterns that define the beat. * */ -public class JFugue extends Service { +public class JFugue extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java index c1d038c9ef..fbc5ef5e99 100644 --- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java +++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java @@ -7,21 +7,24 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.FloatBuffer; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.cv.CvData; +import org.myrobotlab.cv.CVData; import org.myrobotlab.framework.Instantiator; import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Platform; @@ -54,6 +57,7 @@ import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.Gateway; import org.myrobotlab.service.interfaces.IKJointAngleListener; +import org.myrobotlab.service.interfaces.SelectListener; import org.myrobotlab.service.interfaces.ServoControl; import org.myrobotlab.service.interfaces.ServoControlListener; import org.myrobotlab.service.interfaces.ServoStatusListener; @@ -70,7 +74,7 @@ import com.jme3.export.binary.BinaryExporter; import com.jme3.font.BitmapFont; import com.jme3.font.BitmapText; -import com.jme3.input.FlyByCamera; +import com.jme3.input.ChaseCamera; import com.jme3.input.InputManager; import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; @@ -86,6 +90,7 @@ import com.jme3.math.FastMath; import com.jme3.math.Quaternion; import com.jme3.math.Ray; +import com.jme3.math.Transform; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; @@ -119,133 +124,139 @@ * @author GroG, calamity, kwatters, moz4r and many others ... * */ -public class JMonkeyEngine extends Service implements Gateway, ActionListener, Simulator, EncoderListener, IKJointAngleListener, ServoStatusListener, ServoControlListener { +public class JMonkeyEngine extends Service implements Gateway, ActionListener, Simulator, EncoderListener, IKJointAngleListener, ServoStatusListener, ServoControlListener { final static String CAMERA = "camera"; public final static Logger log = LoggerFactory.getLogger(JMonkeyEngine.class); - final static String ROOT = "root"; + protected final static String ROOT = "root"; private static final long serialVersionUID = 1L; - boolean altLeftPressed = false; + protected boolean altLeft = false; - transient AnalogListener analog = null; + protected transient AnalogListener analog = null; - transient Jme3App app; + protected transient Jme3App app; - transient AssetManager assetManager; + protected transient AssetManager assetManager; - String assetsDir = getDataDir() + File.separator + "assets"; + protected String assetsDir = getResourceDir() + File.separator + "assets"; - boolean autoAttach = true; + protected String modelsDir = assetsDir + File.separator + "Models"; - transient Node camera = new Node(CAMERA); + protected boolean autoAttach = true; - transient Camera cameraSettings; + protected transient Node camera = new Node(CAMERA); - transient CameraNode camNode; + protected transient Camera cam; - boolean ctrlLeftPressed = false; + protected transient CameraNode camNode; - String defaultAppType = "Jme3App"; + protected boolean ctrlLeftPressed = false; - double defaultServoSpeed = 500; + protected String defaultAppType = "Jme3App"; - long deltaMs; + protected double defaultServoSpeed = 500; - transient DisplayMode displayMode = null; + protected long deltaMs; - transient FlyByCamera flyCam; - - String fontColor = "#66ff66"; // green - - int fontSize = 14; + protected transient DisplayMode displayMode = null; + + protected ChaseCamera chaseCamera; - boolean fullscreen = false; + protected String fontColor = "#66ff66"; // green - private String guiId; + protected int fontSize = 14; - transient Node guiNode; + protected boolean fullscreen = false; - transient Map guiText = new TreeMap<>(); + protected transient Node guiNode; - int height = 768; + protected transient Map guiText = new TreeMap<>(); - transient List history = new ArrayList(); + protected int height = 768; - transient AtomicInteger id = new AtomicInteger(); + protected transient List history = new ArrayList(); - transient InputManager inputManager; + protected transient InputManager inputManager; - transient Interpolator interpolator; + protected transient Interpolator interpolator; - transient protected Queue jme3MsgQueue = new ConcurrentLinkedQueue(); + protected transient Queue jme3MsgQueue = new ConcurrentLinkedQueue(); + + /** + * currently loaded models, if JMonkey is asked to reload a model, it will explode + */ + final protected Set loadedModels = new TreeSet<>(); final public String KEY_SEPERATOR = "/"; - transient DisplayMode lastDisplayMode = null; + protected boolean mouseLeft = false; - String modelsDir = assetsDir + File.separator + "Models"; + protected boolean mouseRightPressed = false; - boolean mouseLeftPressed = false; - - boolean mouseRightPressed = false; - - Map multiMapped = new TreeMap<>(); + protected Map multiMapped = new TreeMap<>(); // https://stackoverflow.com/questions/16861727/jmonkey-engine-3-0-drawing-points - transient FloatBuffer pointCloudBuffer = null; - - transient Material pointCloudMat = null; + protected transient FloatBuffer pointCloudBuffer = null; - transient Mesh pointCloudMesh = new Mesh(); + protected transient Material pointCloudMat = null; - transient Node rootNode; + protected transient Mesh pointCloudMesh = new Mesh(); - boolean saveHistory = false; + protected transient Node rootNode; - transient Spatial selectedForMovement = null; + protected boolean saveHistory = false; - transient Spatial selectedForView = null; + protected transient Spatial selectedForMovement = null; - int selectIndex = 0; + protected transient Spatial selectedForView = null; - @Deprecated /* came from jme3ServoController... */ - transient Map servos = new TreeMap<>(); + protected int selectIndex = 0; - transient AppSettings settings; + protected transient AppSettings settings; - boolean shiftLeftPressed = false; + protected boolean shiftLeft = false; - long sleepMs; + protected long sleepMs; - long startUpdateTs; + protected long startUpdateTs; - transient AppStateManager stateManager; + protected transient AppStateManager stateManager; - transient Jme3Util util; + protected transient Jme3Util util; - transient ViewPort viewPort; + protected transient ViewPort viewPort; - int width = 1024; + protected int width = 1024; + + protected float orbitRadius = 10f; + + protected float orbitSpeed = 0.5f; + + protected float mouseX = 0f; + + protected float mouseY = 0f; // protected Set modelPaths = new LinkedHashSet<>(); protected Map nodes = new LinkedHashMap<>(); + /** + * current selected path + */ + protected String selectedPath = null; + + protected boolean mouseMiddle = false; public JMonkeyEngine(String n, String id) { super(n, id); - File d = new File(modelsDir); - d.mkdirs(); util = new Jme3Util(this); analog = new AnalogHandler(this); interpolator = new Interpolator(this, util); - guiId = "jme-" + getName() + "-" + getId(); - // setup the virtual reflection // this will "connect" to our mrl instance // and part of the connection is the mrl instance @@ -380,18 +391,26 @@ public void attach(Attachable attachable) throws Exception { log.error("{} not found in registry", name); return; } + + if (service instanceof SelectListener) { + addListener("getSelectedPath", service.getName(), "onSelected"); + } + // FIXME 2023-06-21 GroG: interested services SHOULD NOT evaluate by type, they + // should evaluate how to attach by INTERFACE - the following should be refactored + // to subscribe based on interface not type + // We do type evaluation and routing based on string values vs instance // values // this is to support future (non-Java) classes that cannot be instantiated // and // are subclassed in a proxy class with getType() overloaded for to identify - if (service.getType().equals("org.myrobotlab.service.OpenCV")) { + if (service.getTypeKey().equals("org.myrobotlab.service.OpenCV")) { AbstractComputerVision cv = (AbstractComputerVision) service; subscribe(service.getName(), "publishCvData"); } - if (service.getType().equals("org.myrobotlab.service.Servo")) { + if (service.getTypeKey().equals("org.myrobotlab.service.Servo")) { // non-batched - "instantaneous" move data subscription subscribe(service.getName(), "publishEncoderData", getName(), "onEncoderData"); } @@ -470,6 +489,14 @@ public Map buildTree(Map tree, String path, } return tree; } + + public void resetView() { + // cam.setLocation(new Vector3f(0, 1, 2)); + camera.setLocalTransform(new Transform(new Vector3f(0, 3, 5))); +// camera.setLocalTransform(null); +// camera.move(0, 1, 2);; + cameraLookAt("root"); + } public void cameraLookAt(Spatial spatial) { @@ -485,7 +512,7 @@ public void cameraLookAt(Spatial spatial) { } public void cameraLookAt(String name) { - JMonkeyEngineConfig c = (JMonkeyEngineConfig)config; + JMonkeyEngineConfig c = (JMonkeyEngineConfig) config; Spatial s = get(name); if (s == null) { log.error("cameraLookAt - cannot find {}", name); @@ -506,8 +533,8 @@ public Geometry checkCollision() { CollisionResults results = new CollisionResults(); // Convert screen click to 3d position Vector2f click2d = inputManager.getCursorPosition(); - Vector3f click3d = cameraSettings.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); - Vector3f dir = cameraSettings.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal(); + Vector3f click3d = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone(); + Vector3f dir = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal(); // Aim the ray from the clicked spot forwards. Ray ray = new Ray(click3d, dir); // Collect intersections between ray and all nodes in results list. @@ -572,7 +599,7 @@ public void cycle() { List siblings = parent.getChildren(); - if (shiftLeftPressed) { + if (shiftLeft) { --selectIndex; } else { ++selectIndex; @@ -697,10 +724,7 @@ public void enableBoundingBox(String name, boolean b) { enableBoundingBox(get(name), b, null); } - public void enableFlyCam(boolean b) { - flyCam.setEnabled(b); - } - + // FIXME - use ctrl space like blender ... public void enableFullScreen(boolean fullscreen) { this.fullscreen = fullscreen; @@ -879,12 +903,6 @@ public String getCoorAxesName(Spatial spatial) { return String.format("_axis-%s-%s", getType(spatial), spatial.getName()); } - @Override - public Message getDescribeMsg(String connId) { - // TODO Auto-generated method stub - return null; - } - private String getExt(String name) { int pos = name.lastIndexOf("."); String ext = null; @@ -993,6 +1011,34 @@ public Node getRootNode() { public Spatial getSelected() { return selectedForView; } + + /** + * Set selected path updates the current selectedPath to from a ray + * collision in the scene graph. The collision is currently implemented + * as a mouse click. The point at where the mouse is clicked a "path" to + * an object collision is created and set an published through getSelectedPath. + * This publication can be picked up by other services if they need such + * events. + * + * @param path + * @return + */ + public String setSelectedPath(String path) { + selectedPath = path; + if (path != null) { + invoke("getSelectedPath"); + } + return path; + } + + /** + * selected path is the ORIGINAL_PATH of the selected node + * @return + */ + public String getSelectedPath() { + return selectedPath; + } + public AppSettings getSettings() { return settings; @@ -1189,60 +1235,51 @@ public void loadFile(String inFileName) { } } + /** + * Load a specific model file + * @param assetPath + * @return + */ public Spatial loadModel(String assetPath) { - return assetManager.loadModel(assetPath); - } - - public void loadModels() { - // load the root data dir - loadModels(modelsDir); - loadNodes(modelsDir); - } - - public void loadModels(String dirPath) { - JMonkeyEngineConfig c = (JMonkeyEngineConfig)config; - // FIXME - must be unique AND AND ... only loaded once ! -// if (c.modelPaths.contains(dirPath)) { -// info("already loaded %s", dirPath); -// return; -// } - c.addModelPath(dirPath); - traverseLoadModels(dirPath); - } - - public void traverseLoadModels(String dirPath) { - dirPath = FileIO.normalize(dirPath); - log.info("loading models from {}", dirPath); - File dir = new File(dirPath); - if (!dir.exists()) { - dir.mkdirs(); - } - if (!dir.isDirectory()) { - error("%s is not a directory", dirPath); - return; - } - assetManager.registerLocator(dirPath, FileLocator.class); - // get list of files in dir .. - File[] files = dir.listFiles(); - - // scan for all non json files first ... - // initially set them invisible ... - for (File f : files) { - if (!f.isDirectory()) { // && !"json".equals(getExt(f.getName()))) { - loadResource(f.getAbsolutePath()); + JMonkeyEngineConfig c = (JMonkeyEngineConfig) config; + Spatial model = null; + try { + if (loadedModels.contains(assetPath)) { + log.info("model {} already loaded"); + return null; } - } - - // process structure json files .. - - // breadth first search ... - for (File f : files) { - if (f.isDirectory()) { - traverseLoadModels(f.getAbsolutePath()); + + if (FileIO.checkDir(modelsDir + fs + assetPath)) { + log.info("skipping directory {}"); + return null; + } + + if (assetPath.toLowerCase().endsWith(".md") || assetPath.toLowerCase().endsWith(".txt") || assetPath.toLowerCase().endsWith(".bin")) { + log.info("skipping {} not and valid model type"); + return null; } + + log.info("loading {}", assetPath); + model = assetManager.loadModel(assetPath); + log.info("loaded {}", assetPath); + if (model != null) { + getRootNode().attachChild(model); + } else { + error("%s model null"); + } + + if (c.models == null) { + c.models = new ArrayList<>(); + } + + c.models.add(assetPath); + } catch(Exception e) { + error(e); } + return model; } + /** * load a node with all potential children * @@ -1335,14 +1372,10 @@ public void moveTo(String name, double x, double y, double z) { @Override public void onAction(String name, boolean keyPressed, float tpf) { - log.info("onAction {} {} {}", name, keyPressed, tpf); + log.debug("onAction {} {} {}", name, keyPressed, tpf); if (name.equals("mouse-click-right")) { mouseRightPressed = keyPressed; - if (mouseRightPressed) { - Geometry target = checkCollision(); - setSelected(target); - } } if ("full-screen".equals(name)) { @@ -1356,16 +1389,31 @@ public void onAction(String name, boolean keyPressed, float tpf) { } else if ("cycle".equals(name) && keyPressed) { cycle(); } else if (name.equals("shift-left")) { - shiftLeftPressed = keyPressed; + shiftLeft = keyPressed; } else if (name.equals("ctrl-left")) { ctrlLeftPressed = keyPressed; } else if (name.equals("alt-left")) { - altLeftPressed = keyPressed; + altLeft = keyPressed; } else if ("export".equals(name) && keyPressed) { saveSpatial(selectedForView.getName()); } else if ("mouse-click-left".equals(name)) { - mouseLeftPressed = keyPressed; - } else { + mouseLeft = keyPressed; + if (mouseLeft) { + Geometry target = checkCollision(); + setSelected(target); + } + } + + else if ("mouse-click-middle".equals(name)) { + mouseMiddle = keyPressed; + // USEFUL - but need a different key combo +// if (mouseMiddle && selectedForView != null) { +// cameraLookAt(selectedForView.getName()); +// } + } + + + else { warn("%s - key %b %f not found", name, keyPressed, tpf); } } @@ -1382,27 +1430,17 @@ public void onAction(String name, boolean keyPressed, float tpf) { * */ public void onAnalog(String name, float keyPressed, float tpf) { - // log.info("onAnalog [{} {} {}]", name, keyPressed, tpf); + log.debug("onAnalog [{} {} {}]", name, keyPressed, tpf); + // selectedForMovement invariably is the camera if (selectedForMovement == null) { selectedForMovement = camera;// FIXME "new" selectedMove vs selected } - // wheelmouse zoom (done) - // alt+ctrl+lmb - zoom
(done) - // alt+lmb - rotate
(done) - // alt+shft+lmb - pan (done) - // rotate around selection - - // https://www.youtube.com/watch?v=IVZPm9HAMD4&feature=youtu.be - // wrap text of breadcrumbs - // draggable - resize for menu - what you set is how it stays - // when menu active - inputs(hotkey when non-menu) should be deactive - - // FIXME - do jme.rotateTo or "new" jme.rotate for all these input driven - // controls - - // ROTATE - if (mouseLeftPressed && altLeftPressed && !shiftLeftPressed) { + // ROTATE ORBIT (should be middle button / mouse wheel button) + // currently wrong :P its rotating in place - you want to orbit on a selection at 10 pts out + if (mouseMiddle && !shiftLeft) { + switch (name) { case "mouse-axis-x": selectedForMovement.rotate(0, -keyPressed, 0); @@ -1417,16 +1455,45 @@ public void onAnalog(String name, float keyPressed, float tpf) { selectedForMovement.rotate(keyPressed, 0, 0); break; } + + + if (name.equals("mouse-axis-x")) { + mouseX = inputManager.getCursorPosition().x; + } else if (name.equals("mouse-axis-y")) { + mouseY = inputManager.getCursorPosition().y; + } + + + } - // PAN - if (mouseLeftPressed && altLeftPressed && shiftLeftPressed) { + // PAN -- works(ish) + if (mouseMiddle && shiftLeft) { + log.info("PAN !!!!"); switch (name) { case "mouse-axis-x": - selectedForMovement.move(keyPressed * 3, 0, 0); - break; case "mouse-axis-x-negative": - selectedForMovement.move(-keyPressed * 3, 0, 0); + + // Get the local rotation of the camera + Quaternion rotation = selectedForMovement.getLocalRotation(); + + // Extract the X-axis rotation column from the quaternion + Vector3f rotationAxis = rotation.getRotationColumn(0); + + // Define the direction and distance to pan + float direction = name.equals("mouse-axis-x") ? -0.13f : 0.13f; + float distance = 0.3f; + + // Calculate the translation vector by multiplying the rotation axis with the direction and distance + Vector3f translation = rotationAxis.mult(direction).mult(distance); + + // Move the camera by the translation vector + // camera.setLocation(camera.getLocation().add(translation)); + // selectedForMovement.move(translation); + + // needs to be on the normal + // selectedForMovement.move(direction, 0, direction); + selectedForMovement.move(translation); break; case "mouse-axis-y": selectedForMovement.move(0, keyPressed * 3, 0); @@ -1438,24 +1505,13 @@ public void onAnalog(String name, float keyPressed, float tpf) { } // ZOOM - if (mouseLeftPressed && altLeftPressed && ctrlLeftPressed) { - - // FIXME - zoom where cursor is :P - it becomes a rotate and zoom - log.info("zoom - cursor is currently {}", inputManager.getCursorPosition()); + if (name.equals("mouse-wheel-up") || name.equals("mouse-wheel-down")) { - if (name.equals("mouse-axis-y")) { - selectedForMovement.move(0, 0, keyPressed * 10); - } else if (name.equals("mouse-axis-y-negative")) { - selectedForMovement.move(0, 0, -keyPressed * 10); - } - } - - if (name.equals("mouse-wheel-up") || name.equals("forward")) { - // selected.setLocalScale(selected.getLocalScale().mult(1.0f)); - selectedForMovement.move(0, 0, keyPressed * -1); - } else if (name.equals("mouse-wheel-down") || name.equals("backward")) { - // selected.setLocalScale(selected.getLocalScale().mult(1.0f)); - selectedForMovement.move(0, 0, keyPressed * 1); + Quaternion normal = camera.getLocalRotation(); + Vector3f rotationAxis = normal.getRotationColumn(2); + float direction = name.equals("mouse-wheel-up")?0.3f:-0.3f; + Vector3f translation = rotationAxis.mult(direction); + camera.move(translation); } } @@ -1466,7 +1522,7 @@ public void onAnalog(String name, float keyPressed, float tpf) { * @param data * cv data */ - public void onCvData(CvData data) { + public void onCvData(CVData data) { // onPointCloud(data.getPointCloud()); FIXME - brittle and not correct // FIXME - do something interesting ... :) } @@ -1911,12 +1967,21 @@ public void setRotation(String name, String axis) { o.rotationMask = axis; } + @Deprecated + public String publishSelected(String data) { + return data; + } + + // xxx public void setSelected(Spatial newSelected) { // turn off old if (selectedForView != null) { - enableBoundingBox(selectedForView, false); - enableAxes(selectedForView, false); + // enableBoundingBox(selectedForView, false); + // enableAxes(selectedForView, false); + + // try to publish "quality" data + // String[] parts = newSelected.getUserData("ORIGINAL_PATH"); } // set selected @@ -1930,9 +1995,54 @@ public void setSelected(Spatial newSelected) { // turn on new if (newSelected != null) { - enableBoundingBox(newSelected, true); - enableAxes(newSelected, true); + // enableBoundingBox(newSelected, true); + // enableAxes(newSelected, true); + + String originalPath = newSelected.getUserData("ORIGINAL_PATH"); + // invoke("publishSelected", originalPath); + + // invoke("getSelected"); + if (originalPath != null) { + selectedPath = originalPath; + + // Kludge ... this should be structured and set directly on the data + // when building the inmoov model + // but in an attempt to improve data quality we got to do this matching + // thing .. + String normalizedPath = findCommonPrefix(originalPath); + + if (normalizedPath != null) { + invoke("setSelectedPath", normalizedPath); + } + } + } + + } + + /** + * horrific function to calculate hits on path parts :/ to improve path + * selection + * + * @param path + * @return + */ + public String findCommonPrefix(String path) { + // found in nodes + ArrayList pathParts = new ArrayList<>(Arrays.asList(path.split("/"))); + Collections.reverse(pathParts); + for (String part : pathParts) { + if (nodes.containsKey(part)) { + + // i01.leftHand.index3 + if (Character.isDigit(part.charAt(part.length() - 1))) { + part = part.substring(0, part.length() - 1); + } + + return part; + } + } + return null; } public void setSelected(String name) { @@ -2003,15 +2113,8 @@ public void simpleInitApp() { inputManager = app.getInputManager(); guiNode = app.getGuiNode(); - - // disable flycam we are going to use our - // own camera - flyCam = app.getFlyByCamera(); - if (flyCam != null) { - flyCam.setEnabled(false); - } - - cameraSettings = app.getCamera(); + + cam = app.getCamera(); rootNode = app.getRootNode(); rootNode.setName(ROOT); rootNode.attachChild(camera); @@ -2019,7 +2122,7 @@ public void simpleInitApp() { viewPort = app.getViewPort(); // Setting the direction to Spatial to camera, this means the camera will // copy the movements of the Node - camNode = new CameraNode("cam", cameraSettings); + camNode = new CameraNode("cam", cam); camNode.setControlDir(ControlDirection.SpatialToCamera); // camNode.setControlDir(ControlDirection.CameraToSpatial); // rootNode.attachChild(camNode); @@ -2054,15 +2157,11 @@ public void simpleInitApp() { new File(getDataDir()).mkdirs(); new File(getResourceDir()).mkdirs(); - assetManager.registerLocator("./", FileLocator.class); + // assetManager.registerLocator("./", FileLocator.class); assetManager.registerLocator(getDataDir(), FileLocator.class); assetManager.registerLocator(assetsDir, FileLocator.class); + assetManager.registerLocator(modelsDir, FileLocator.class); assetManager.registerLocator(getResourceDir(), FileLocator.class); - assetManager.registerLocator(getResourceDir() + "/Interface/Logo", FileLocator.class); // /Interface/Logo/Monkey.jpg - - // FIXME - should be moved under ./data/JMonkeyEngine/ - // assetManager.registerLocator("InMoov/jm3/assets", FileLocator.class); - assetManager.registerLocator(getDataDir(), FileLocator.class); assetManager.registerLoader(BlenderLoader.class, "blend"); /** @@ -2101,6 +2200,9 @@ public void simpleInitApp() { inputManager.addMapping("mouse-click-right", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); inputManager.addListener(this, "mouse-click-right"); + + inputManager.addMapping("mouse-click-middle", new MouseButtonTrigger(MouseInput.BUTTON_MIDDLE)); + inputManager.addListener(this, "mouse-click-middle"); inputManager.addMapping("mouse-wheel-up", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false)); inputManager.addListener(analog, "mouse-wheel-up"); @@ -2165,9 +2267,6 @@ public void simpleInitApp() { PhysicsTestHelper.createBallShooter(app, rootNode, bulletAppState.getPhysicsSpace()); } - // load models in the default directory - // loadModels(); - } public void simpleUpdate(float tpf) { @@ -2364,11 +2463,20 @@ public static void main(String[] args) { // reservedRotations from different controllers // FIXME - make "load" work .. + LoggingFactory.init("WARN"); WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); webgui.autoStartBrowser(false); webgui.startService(); + + + boolean done = true; + if (done) { + return; + } + + Runtime.start("sim", "JMonkeyEngine"); boolean worky = false; if (worky) { @@ -2382,11 +2490,6 @@ public static void main(String[] args) { i01.startPeer("simulator"); } - boolean done = true; - if (done) { - return; - } - Platform.setVirtual(true); // Runtime.main(new String[] { "--interactive", "--id", "admin" }); JMonkeyEngine jme = (JMonkeyEngine) Runtime.start("simulator", "JMonkeyEngine"); @@ -2562,44 +2665,67 @@ public void onServoMoveTo(ServoControl servo) { rotateOnAxis(name, null, servo.getTargetPos(), velocity); } } - + public UserDataConfig toUserDataConfig(UserData userData) { UserDataConfig udc = new UserDataConfig(userData.mapper, userData.rotationMask); return udc; } - + @Override - public ServiceConfig getConfig() { - JMonkeyEngineConfig config = (JMonkeyEngineConfig)super.getConfig(); + public JMonkeyEngineConfig getConfig() { + super.getConfig(); - if (config.modelPaths != null) { - Collections.sort(config.modelPaths); + if (config.models != null) { + Collections.sort(config.models); } - + // WARNING - getConfig is "used" before the delayed apply is processed - // so if you detroy things here - ie clear nodes, you will be unable to load them appropriately + // so if you detroy things here - ie clear nodes, you will be unable to load + // them appropriately // you need to guard with null checking for (String key : nodes.keySet()) { config.nodes.put(key, toUserDataConfig(nodes.get(key))); } - + if (multiMapped != null && multiMapped.size() > 0) { - // FIXME - FIXED ! config.multiMapped = multiMapped; <- MUST DO NON DESTRUCTIVE ADDITION + // FIXME - FIXED ! config.multiMapped = multiMapped; <- MUST DO NON + // DESTRUCTIVE ADDITION config.multiMapped.putAll(multiMapped); } // generate defaults end --------- return config; } + + /** + * Scans and loads the default resource location and loads any models not already loaded + */ + public void loadDefaultModels() { + loadModels(modelsDir); + } + + /** + * Scans and loads all files from a modelPath directory + * @param modelPath + */ + public void loadModels(String modelPath) { + List models = scanForModels(modelPath); + for (String path : models) { + loadModel(path); + } + } public ServiceConfig loadDelayed(ServiceConfig c) { JMonkeyEngineConfig config = (JMonkeyEngineConfig) c; - if (config.modelPaths != null) { - List tempList = new ArrayList<>(config.modelPaths); + if (config.models != null && config.models.size() > 0) { + List tempList = new ArrayList<>(config.models); for (String modelPath : tempList) { - loadModels(modelPath); + loadModel(modelPath); } + } else { + // scan resource dir + loadDefaultModels(); } if (config.nodes != null) { @@ -2609,16 +2735,16 @@ public ServiceConfig loadDelayed(ServiceConfig c) { UserData ud = getUserData(path); UserDataConfig udc = config.nodes.get(path); // UserData ud = new UserData(config.nodes.get(path)); -// if (ud == null) { -// addNode(path); -// ud = nodes.get(path); // new UserData(config.nodes.get(path)); -// } - + // if (ud == null) { + // addNode(path); + // ud = nodes.get(path); // new UserData(config.nodes.get(path)); + // } + if (ud == null) { log.error("could not find node for {}", path); continue; } - + if (udc.mapper != null) { MapperLinear m = udc.mapper; setMapper(path, m.minX, m.maxX, m.minY, m.maxY); @@ -2642,17 +2768,49 @@ public ServiceConfig loadDelayed(ServiceConfig c) { return c; } - - -// @Override -// public ServiceConfig apply(ServiceConfig c) { -// JMonkeyEngineConfig config = (JMonkeyEngineConfig) super.apply(c); -// if (app != null) { -// // if there is an app we can load immediately -// loadDelayed(config); -// } -// return config; -// } + /** + * Scan a directory for models, perhaps filtering should be done, + * but I don't know all the possible 3d model files JMonkeyEngine is + * currently capable of rendering and don't want to prematurely limit + * it. + * @param modelDir + * @return + */ + public List scanForModels(String modelDir) { + List models = new ArrayList<>(); + + if (modelDir == null) { + error("models directory cannot be null"); + return models; + } + + File dir = new File(modelDir); + if (!dir.exists() || !dir.isDirectory()) { + error("%s models directory is not valid"); + return models; + } + + for(File file : dir.listFiles()) { + +// Path pathAbsolute = Paths.get(file.getAbsolutePath()); +// Path pathBase = Paths.get(System.getProperty("user.dir")); +// Path pathRelative = pathBase.relativize(pathAbsolute); +// models.add(pathRelative.toString()); + models.add(file.getName()); + } + + return models; + } + + // @Override + // public ServiceConfig apply(ServiceConfig c) { + // JMonkeyEngineConfig config = (JMonkeyEngineConfig) super.apply(c); + // if (app != null) { + // // if there is an app we can load immediately + // loadDelayed(config); + // } + // return config; + // } public void multiMap(String name, String... nodeNames) { if (nodeNames != null) { diff --git a/src/main/java/org/myrobotlab/service/JavaScript.java b/src/main/java/org/myrobotlab/service/JavaScript.java index 20816ae83e..533a43badc 100644 --- a/src/main/java/org/myrobotlab/service/JavaScript.java +++ b/src/main/java/org/myrobotlab/service/JavaScript.java @@ -9,9 +9,10 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; -public class JavaScript extends Service { +public class JavaScript extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Joystick.java b/src/main/java/org/myrobotlab/service/Joystick.java index 414224bfec..0e8bee108a 100644 --- a/src/main/java/org/myrobotlab/service/Joystick.java +++ b/src/main/java/org/myrobotlab/service/Joystick.java @@ -67,7 +67,7 @@ The buttons values (booleans) can be accessed individually, or * To Test java -Djava.library.path="./" -cp "./*" * net.java.games.input.test.ControllerReadTest */ -public class Joystick extends Service implements AnalogPublisher { +public class Joystick extends Service implements AnalogPublisher { public final static Logger log = LoggerFactory.getLogger(Joystick.class); private static final long serialVersionUID = 1L; @@ -695,8 +695,8 @@ public void moveTo(String axisName, double value) { } @Override - public ServiceConfig getConfig() { - JoystickConfig config = (JoystickConfig)super.getConfig(); + public JoystickConfig getConfig() { + super.getConfig(); config.controller = controller; if (analogListeners.size() > 0) { @@ -716,9 +716,10 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - + public JoystickConfig apply(JoystickConfig c) { + super.apply(c); // "special" needs native libs + // FIXME - should be done in startService initNativeLibs(); // scan for hardware controllers @@ -727,16 +728,15 @@ public ServiceConfig apply(ServiceConfig c) { getControllers(); // get controller request from config - JoystickConfig config = (JoystickConfig)super.apply(c); - if (config.controller != null) { + if (c.controller != null) { setController(config.controller); } // stupid transform from array to set - yaml wants array, set prevents // duplicates :( - if (config.analogListeners != null) { - for (String id : config.analogListeners.keySet()) { - ArrayList list = config.analogListeners.get(id); + if (c.analogListeners != null) { + for (String id : c.analogListeners.keySet()) { + ArrayList list = c.analogListeners.get(id); Set s = analogListeners.get(id); if (s == null) { s = new HashSet<>(); diff --git a/src/main/java/org/myrobotlab/service/KafkaConnector.java b/src/main/java/org/myrobotlab/service/KafkaConnector.java index d3dc3b7d3d..55c0687d74 100755 --- a/src/main/java/org/myrobotlab/service/KafkaConnector.java +++ b/src/main/java/org/myrobotlab/service/KafkaConnector.java @@ -7,6 +7,7 @@ import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.myrobotlab.framework.Service; +import org.myrobotlab.service.config.ServiceConfig; /** * A kafka connector that can subscribe to a string/string kafka stopic and @@ -19,7 +20,7 @@ * @author kwatters * */ -public class KafkaConnector extends Service { +public class KafkaConnector extends Service { public String bootstrapServers = "localhost:9092"; public String groupId = "test"; diff --git a/src/main/java/org/myrobotlab/service/Keyboard.java b/src/main/java/org/myrobotlab/service/Keyboard.java index 4d76c8d028..9b53cfbe39 100644 --- a/src/main/java/org/myrobotlab/service/Keyboard.java +++ b/src/main/java/org/myrobotlab/service/Keyboard.java @@ -14,6 +14,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.math.geometry.Point2df; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** @@ -22,7 +23,7 @@ * * */ -public class Keyboard extends Service { +public class Keyboard extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/KeyboardSim.java b/src/main/java/org/myrobotlab/service/KeyboardSim.java index f2ea917eeb..b9052a0541 100644 --- a/src/main/java/org/myrobotlab/service/KeyboardSim.java +++ b/src/main/java/org/myrobotlab/service/KeyboardSim.java @@ -8,13 +8,14 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** * @author MaVo (MyRobotLab) / LunDev (GitHub) */ -public class KeyboardSim extends Service { +public class KeyboardSim extends Service { static final long serialVersionUID = 1L; static final Logger log = LoggerFactory.getLogger(KeyboardSim.class); diff --git a/src/main/java/org/myrobotlab/service/LeapMotion.java b/src/main/java/org/myrobotlab/service/LeapMotion.java index b63cc1b904..ed1387d4b3 100644 --- a/src/main/java/org/myrobotlab/service/LeapMotion.java +++ b/src/main/java/org/myrobotlab/service/LeapMotion.java @@ -26,7 +26,7 @@ import com.leapmotion.leap.Hand; import com.leapmotion.leap.Vector; -public class LeapMotion extends Service implements LeapDataListener, LeapDataPublisher, PointPublisher { +public class LeapMotion extends Service implements LeapDataListener, LeapDataPublisher, PointPublisher { private static final long serialVersionUID = 1L; @@ -192,7 +192,6 @@ private double computeAngleDegrees(Finger f, Vector palmNormal) { } private LeapHand mapLeapHandData(Hand lh) { - LeapMotionConfig c = (LeapMotionConfig) config; LeapHand mrlHand = new LeapHand(); // process the normal Vector palmNormal = lh.palmNormal(); @@ -368,19 +367,19 @@ public void stop() { MapperLinear rightThumb = new MapperLinear(); @Override - public ServiceConfig apply(ServiceConfig config) { - LeapMotionConfig c = (LeapMotionConfig) super.apply(config); - leftIndex = new MapperLinear(c.leftIndex.minIn, c.leftIndex.maxIn, c.leftIndex.minOut, c.leftIndex.maxOut); - leftMiddle = new MapperLinear(c.leftMiddle.minIn, c.leftMiddle.maxIn, c.leftMiddle.minOut, c.leftMiddle.maxOut); - leftRing = new MapperLinear(c.leftRing.minIn, c.leftRing.maxIn, c.leftRing.minOut, c.leftRing.maxOut); - leftPinky = new MapperLinear(c.leftPinky.minIn, c.leftPinky.maxIn, c.leftPinky.minOut, c.leftPinky.maxOut); - leftThumb = new MapperLinear(c.leftThumb.minIn, c.leftThumb.maxIn, c.leftThumb.minOut, c.leftThumb.maxOut); - - rightIndex = new MapperLinear(c.rightIndex.minIn, c.rightIndex.maxIn, c.rightIndex.minOut, c.rightIndex.maxOut); - rightMiddle = new MapperLinear(c.rightMiddle.minIn, c.rightMiddle.maxIn, c.rightMiddle.minOut, c.rightMiddle.maxOut); - rightRing = new MapperLinear(c.rightRing.minIn, c.rightRing.maxIn, c.rightRing.minOut, c.rightRing.maxOut); - rightPinky = new MapperLinear(c.rightPinky.minIn, c.rightPinky.maxIn, c.rightPinky.minOut, c.rightPinky.maxOut); - rightThumb = new MapperLinear(c.rightThumb.minIn, c.rightThumb.maxIn, c.rightThumb.minOut, c.rightThumb.maxOut); + public LeapMotionConfig apply(LeapMotionConfig config) { + super.apply(config); + leftIndex = new MapperLinear(config.leftIndex.minIn, config.leftIndex.maxIn, config.leftIndex.minOut, config.leftIndex.maxOut); + leftMiddle = new MapperLinear(config.leftMiddle.minIn, config.leftMiddle.maxIn, config.leftMiddle.minOut, config.leftMiddle.maxOut); + leftRing = new MapperLinear(config.leftRing.minIn, config.leftRing.maxIn, config.leftRing.minOut, config.leftRing.maxOut); + leftPinky = new MapperLinear(config.leftPinky.minIn, config.leftPinky.maxIn, config.leftPinky.minOut, config.leftPinky.maxOut); + leftThumb = new MapperLinear(config.leftThumb.minIn, config.leftThumb.maxIn, config.leftThumb.minOut, config.leftThumb.maxOut); + + rightIndex = new MapperLinear(config.rightIndex.minIn, config.rightIndex.maxIn, config.rightIndex.minOut, config.rightIndex.maxOut); + rightMiddle = new MapperLinear(config.rightMiddle.minIn, config.rightMiddle.maxIn, config.rightMiddle.minOut, config.rightMiddle.maxOut); + rightRing = new MapperLinear(config.rightRing.minIn, config.rightRing.maxIn, config.rightRing.minOut, config.rightRing.maxOut); + rightPinky = new MapperLinear(config.rightPinky.minIn, config.rightPinky.maxIn, config.rightPinky.minOut, config.rightPinky.maxOut); + rightThumb = new MapperLinear(config.rightThumb.minIn, config.rightThumb.maxIn, config.rightThumb.minOut, config.rightThumb.maxOut); return config; } diff --git a/src/main/java/org/myrobotlab/service/LeapMotion2.java b/src/main/java/org/myrobotlab/service/LeapMotion2.java index e918f95b54..a48a1c7cbd 100644 --- a/src/main/java/org/myrobotlab/service/LeapMotion2.java +++ b/src/main/java/org/myrobotlab/service/LeapMotion2.java @@ -20,7 +20,7 @@ import okhttp3.Response; import okhttp3.WebSocket; -public class LeapMotion2 extends Service implements LeapDataListener, LeapDataPublisher, PointPublisher, RemoteMessageHandler { +public class LeapMotion2 extends Service implements LeapDataListener, LeapDataPublisher, PointPublisher, RemoteMessageHandler { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Lidar.java b/src/main/java/org/myrobotlab/service/Lidar.java index fb76b5b29f..db3447ace7 100644 --- a/src/main/java/org/myrobotlab/service/Lidar.java +++ b/src/main/java/org/myrobotlab/service/Lidar.java @@ -9,10 +9,11 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.LidarConfig; import org.myrobotlab.service.interfaces.SerialDataListener; import org.slf4j.Logger; -public class Lidar extends Service implements SerialDataListener { +public class Lidar extends Service implements SerialDataListener { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/LidarVlp16.java b/src/main/java/org/myrobotlab/service/LidarVlp16.java index 6cbc349957..52b0531901 100644 --- a/src/main/java/org/myrobotlab/service/LidarVlp16.java +++ b/src/main/java/org/myrobotlab/service/LidarVlp16.java @@ -12,9 +12,10 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.LidarConfig; import org.slf4j.Logger; -public class LidarVlp16 extends Service { +public class LidarVlp16 extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Lloyd.java b/src/main/java/org/myrobotlab/service/Lloyd.java index 0ea682cc68..ca1dc561ff 100755 --- a/src/main/java/org/myrobotlab/service/Lloyd.java +++ b/src/main/java/org/myrobotlab/service/Lloyd.java @@ -24,6 +24,7 @@ import org.myrobotlab.opencv.OpenCVFilterDL4JTransfer; import org.myrobotlab.opencv.OpenCVFilterLloyd; import org.myrobotlab.programab.OOBPayload; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.SpeechRecognizer; import org.myrobotlab.service.interfaces.SpeechSynthesis; import org.nd4j.linalg.dataset.api.iterator.DataSetIterator; @@ -38,7 +39,7 @@ * @author kwatters * */ -public class Lloyd extends Service { +public class Lloyd extends Service { public final static Logger log = LoggerFactory.getLogger(Lloyd.class); private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Lm75a.java b/src/main/java/org/myrobotlab/service/Lm75a.java index d0029a9942..d0c05fc7dd 100644 --- a/src/main/java/org/myrobotlab/service/Lm75a.java +++ b/src/main/java/org/myrobotlab/service/Lm75a.java @@ -12,6 +12,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.I2CControl; import org.myrobotlab.service.interfaces.I2CController; import org.slf4j.Logger; @@ -23,7 +24,7 @@ * * References : https://www.nxp.com/documents/data_sheet/LM75A.pdf */ -public class Lm75a extends Service implements I2CControl { +public class Lm75a extends Service implements I2CControl { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/LocalSpeech.java b/src/main/java/org/myrobotlab/service/LocalSpeech.java index ee95463632..292198f3f6 100644 --- a/src/main/java/org/myrobotlab/service/LocalSpeech.java +++ b/src/main/java/org/myrobotlab/service/LocalSpeech.java @@ -18,7 +18,6 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.LocalSpeechConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.data.Locale; import org.slf4j.Logger; @@ -53,7 +52,7 @@ * https://github.com/espeak-ng/espeak-ng/blob/master/docs/mbrola.md#linux-installation * */ -public class LocalSpeech extends AbstractSpeechSynthesis { +public class LocalSpeech extends AbstractSpeechSynthesis { public final static Logger log = LoggerFactory.getLogger(LocalSpeech.class); @@ -133,7 +132,7 @@ public AudioData generateAudioData(AudioData audioData, String toSpeak) throws I args.add("$speak.SelectVoice('" + getVoice().getVoiceProvider().toString() + "');"); args.add("$speak.SetOutputToWaveFile('" + localFileName + "');"); args.add("$speak.speak('" + toSpeak + "')"); - String ret = Runtime.execute("powershell.exe", args, null, null, null); + String ret = Runtime.execute("powershell.exe", args, null, null, true); log.info("powershell returned : {}", ret); @@ -207,8 +206,10 @@ public void loadVoices() { String voicesText = null; + // FIXME this is not right - it should be based on speechType not OS + // speechType should be "set" based on OS and user preference if (platform.isWindows()) { - + try { List args = new ArrayList<>(); @@ -220,7 +221,7 @@ public void loadVoices() { args.add("Select-Object -Property * | "); // args.add("Select-Object -Property Culture, Name, Gender, Age"); args.add("ConvertTo-Json "); - voicesText = Runtime.execute("powershell.exe", args, null, null, null); + voicesText = Runtime.execute("powershell.exe", args, null, null, true); // voicesText = Runtime.execute("cmd.exe", "/c", "\"\"" + ttsPath + "\"" // + " -V" + "\""); @@ -270,9 +271,11 @@ public void loadVoices() { addVoice(matcher.group(1).toLowerCase(), "male", matcher.group(2), matcher.group(1).toLowerCase()); } } - } else if (platform.isLinux()) { - addVoice("Linus", "male", "en-US", "festival"); } + // let apply config add and set the voices +// else if (platform.isLinux()) { +// addVoice("Linus", "male", "en-US", "festival"); +// } } public void removeExt(boolean b) { @@ -283,6 +286,11 @@ public void removeExt(boolean b) { * @return setEspeak sets the Linux tts to espeak template */ public boolean setEspeak() { + if (!Runtime.getPlatform().isLinux()) { + error("espeak only supported on Linux"); + return false; + } + LocalSpeechConfig c = (LocalSpeechConfig) config; c.speechType = "Espeak"; voices.clear(); @@ -297,6 +305,11 @@ public boolean setEspeak() { * @return setFestival sets the Linux tts to festival template */ public boolean setFestival() { + if (!Runtime.getPlatform().isLinux()) { + error("festival only supported on Linux"); + return false; + } + LocalSpeechConfig c = (LocalSpeechConfig) config; voices.clear(); addVoice("Linus", "male", "en-US", "festival"); @@ -304,10 +317,6 @@ public boolean setFestival() { removeExt(false); setTtsHack(false); setTtsCommand("echo \"{text}\" | text2wave -o {filename}"); - if (!Runtime.getPlatform().isLinux()) { - error("festival only supported on Linux"); - return false; - } return true; } @@ -317,6 +326,11 @@ public boolean setFestival() { * @return true if successfully switched */ public boolean setPico2Wav() { + if (!Runtime.getPlatform().isLinux()) { + error("pico2wave only supported on Linux"); + return false; + } + LocalSpeechConfig c = (LocalSpeechConfig) config; c.speechType = "Pico2Wav"; removeExt(false); @@ -329,12 +343,13 @@ public boolean setPico2Wav() { addVoice("es-ES", "female", "es-ES", "pico2wav"); addVoice("fr-FR", "female", "fr-FR", "pico2wav"); addVoice("it-IT", "female", "it-IT", "pico2wav"); + + if (voice == null) { + setVoice(getLocale().getTag()); + } setTtsCommand("pico2wave -l {voice_name} -w {filename} \"{text}\" "); - if (!Runtime.getPlatform().isLinux()) { - error("pico2wave only supported on Linux"); - return false; - } + broadcastState(); return true; } @@ -478,31 +493,54 @@ public void setTtsHack(boolean b) { public void setTtsPath(String ttsPath) { this.ttsPath = ttsPath; } + + public boolean isExecutableAvailable(String executableName) { + ProcessBuilder processBuilder = new ProcessBuilder(); + String command = ""; + boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + if (isWindows) { + command = "where " + executableName; + } else { + command = "which " + executableName; + } + processBuilder.command("sh", "-c", command); + try { + Process process = processBuilder.start(); + process.waitFor(); + return process.exitValue() == 0; + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + return false; + } +} - @Override - public ServiceConfig apply(ServiceConfig config) { - LocalSpeechConfig c = (LocalSpeechConfig) super.apply(config); + public LocalSpeechConfig apply(LocalSpeechConfig config) { + super.apply(config); // setup the default tts per os Platform platform = Runtime.getPlatform(); - if (c.speechType == null) { + if (config.speechType == null) { if (platform.isWindows()) { setTts(); } else if (platform.isMac()) { setSay(); } else if (platform.isLinux()) { - setFestival(); + if (isExecutableAvailable("pico2wave")) { + setPico2Wav(); + } else { + setFestival(); + } } else { error("%s unknown platform %s", getName(), platform.getOS()); } } else { - setSpeechType(c.speechType); + setSpeechType(config.speechType); } - if (c.voice != null) { - setVoice(c.voice); + if (config.voice != null) { + setVoice(config.voice); } - return c; + return config; } public static void main(String[] args) { @@ -539,7 +577,7 @@ public static void main(String[] args) { arguments.add("Add-Type -AssemblyName System.Speech;"); arguments.add("$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer;"); arguments.add("$speak.speak('HELLO !!!!');"); - Runtime.execute("powershell.exe", arguments, null, null, null); + Runtime.execute("powershell.exe", arguments, null, null, true); // log.info(ret); mouth.speakBlocking("hello my name is sam, sam i am yet again, how \"are you? do you 'live in a zoo too? "); diff --git a/src/main/java/org/myrobotlab/service/Log.java b/src/main/java/org/myrobotlab/service/Log.java index 1e8508e487..fa88732014 100644 --- a/src/main/java/org/myrobotlab/service/Log.java +++ b/src/main/java/org/myrobotlab/service/Log.java @@ -34,6 +34,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -43,7 +44,7 @@ import ch.qos.logback.core.spi.FilterReply; import ch.qos.logback.core.status.Status; -public class Log extends Service implements Appender { +public class Log extends Service implements Appender { public static class LogEntry { public long ts; diff --git a/src/main/java/org/myrobotlab/service/Mail.java b/src/main/java/org/myrobotlab/service/Mail.java index 5b8d0f47ed..3a0e482a42 100644 --- a/src/main/java/org/myrobotlab/service/Mail.java +++ b/src/main/java/org/myrobotlab/service/Mail.java @@ -15,6 +15,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** @@ -27,7 +28,7 @@ * -gmail-smtp-example/ * */ -public class Mail extends Service { +public class Mail extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/MarySpeech.java b/src/main/java/org/myrobotlab/service/MarySpeech.java index 1fb17de3cb..f1686253b1 100644 --- a/src/main/java/org/myrobotlab/service/MarySpeech.java +++ b/src/main/java/org/myrobotlab/service/MarySpeech.java @@ -15,6 +15,7 @@ import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; +import org.myrobotlab.service.config.MarySpeechConfig; import org.myrobotlab.service.data.AudioData; import org.slf4j.Logger; import org.xml.sax.SAXException; @@ -35,7 +36,7 @@ * More info at : http://mary.dfki.de/ * */ -public class MarySpeech extends AbstractSpeechSynthesis { +public class MarySpeech extends AbstractSpeechSynthesis { public final static Logger log = LoggerFactory.getLogger(MarySpeech.class); diff --git a/src/main/java/org/myrobotlab/service/Maven.java b/src/main/java/org/myrobotlab/service/Maven.java index 6b100cdf6e..c9afdb4a52 100644 --- a/src/main/java/org/myrobotlab/service/Maven.java +++ b/src/main/java/org/myrobotlab/service/Maven.java @@ -12,11 +12,12 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import picocli.CommandLine.Option; -public class Maven extends Service { +public class Maven extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/MobilePlatform.java b/src/main/java/org/myrobotlab/service/MobilePlatform.java index 14809daeaa..60d4e36fdd 100644 --- a/src/main/java/org/myrobotlab/service/MobilePlatform.java +++ b/src/main/java/org/myrobotlab/service/MobilePlatform.java @@ -30,6 +30,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.MotorControl; import org.slf4j.Logger; @@ -52,7 +53,7 @@ * Differential_steer_drive_dead_reckoning */ @Deprecated // use Chassis -public class MobilePlatform extends Service { +public class MobilePlatform extends Service { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(MobilePlatform.class); diff --git a/src/main/java/org/myrobotlab/service/Motor.java b/src/main/java/org/myrobotlab/service/Motor.java index f57df11ab7..6b410e1f7d 100644 --- a/src/main/java/org/myrobotlab/service/Motor.java +++ b/src/main/java/org/myrobotlab/service/Motor.java @@ -13,7 +13,7 @@ * */ -public class Motor extends AbstractMotor { +public class Motor extends AbstractMotor { private static final long serialVersionUID = 1L; @@ -70,7 +70,7 @@ public void setPwmFreq(Integer pwmfreq) { } @Override - public ServiceConfig getConfig() { + public MotorConfig getConfig() { MotorConfig config = (MotorConfig)super.getConfig(); config.dirPin = getDirPin(); config.pwrPin = getPwrPin(); @@ -78,9 +78,8 @@ public ServiceConfig getConfig() { return config; } - @Override - public ServiceConfig apply(ServiceConfig c) { - MotorConfig config = (MotorConfig)super.apply(c); + public MotorConfig apply(MotorConfig config) { + super.apply(config); if (config.pwrPin != null) { setPwrPin(pwrPin); @@ -91,7 +90,7 @@ public ServiceConfig apply(ServiceConfig c) { if (config.pwmFreq != null) { setPwmFreq(pwmFreq); } - return c; + return config; } public static void main(String[] args) { diff --git a/src/main/java/org/myrobotlab/service/MotorDualPwm.java b/src/main/java/org/myrobotlab/service/MotorDualPwm.java index 9a6b872d71..fe4764ae6d 100644 --- a/src/main/java/org/myrobotlab/service/MotorDualPwm.java +++ b/src/main/java/org/myrobotlab/service/MotorDualPwm.java @@ -10,7 +10,7 @@ import org.myrobotlab.service.config.MotorDualPwmConfig; import org.myrobotlab.service.config.ServiceConfig; -public class MotorDualPwm extends AbstractMotor { +public class MotorDualPwm extends AbstractMotor { private static final long serialVersionUID = 1L; protected String leftPwmPin; @@ -69,26 +69,25 @@ public void setPwmFreq(Integer pwmfreq) { } @Override - public ServiceConfig getConfig() { + public MotorDualPwmConfig getConfig() { // FIXME - may need to do call super.config for config that has parent :( - MotorDualPwmConfig config = (MotorDualPwmConfig)super.getConfig(); + super.getConfig(); config.leftPwmPin = leftPwmPin; config.rightPwmPin = rightPwmPin; config.pwmFreq = pwmFreq; return config; } - @Override - public ServiceConfig apply(ServiceConfig c) { - MotorDualPwmConfig config = (MotorDualPwmConfig)super.apply(c); - if (config.leftPwmPin != null) { - setLeftPwmPin(config.leftPwmPin); + public MotorDualPwmConfig apply(MotorDualPwmConfig c) { + super.apply(c); + if (c.leftPwmPin != null) { + setLeftPwmPin(c.leftPwmPin); } - if (config.rightPwmPin != null) { - setRightPwmPin(config.rightPwmPin); + if (c.rightPwmPin != null) { + setRightPwmPin(c.rightPwmPin); } - if (config.pwmFreq != null) { - setPwmFreq(config.pwmFreq); + if (c.pwmFreq != null) { + setPwmFreq(c.pwmFreq); } return c; } diff --git a/src/main/java/org/myrobotlab/service/MotorHat4Pi.java b/src/main/java/org/myrobotlab/service/MotorHat4Pi.java index 927d2a98e8..1bc2bb6c1d 100644 --- a/src/main/java/org/myrobotlab/service/MotorHat4Pi.java +++ b/src/main/java/org/myrobotlab/service/MotorHat4Pi.java @@ -8,9 +8,8 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.abstracts.AbstractMotor; import org.myrobotlab.service.config.MotorHat4PiConfig; -import org.myrobotlab.service.config.ServiceConfig; -public class MotorHat4Pi extends AbstractMotor { +public class MotorHat4Pi extends AbstractMotor { private static final long serialVersionUID = 1L; Integer leftDirPin; @@ -92,17 +91,15 @@ public String getMotorId() { } @Override - public ServiceConfig getConfig() { + public MotorHat4PiConfig getConfig() { // FIXME - may need to do call super.config for config that has parent :( - MotorHat4PiConfig config = (MotorHat4PiConfig) super.getConfig(); config.motorId = motorId; return config; } - @Override - public ServiceConfig apply(ServiceConfig c) { - MotorHat4PiConfig config = (MotorHat4PiConfig) super.apply(c); - setMotor(config.motorId); + public MotorHat4PiConfig apply(MotorHat4PiConfig c) { + super.apply(c); + setMotor(c.motorId); return c; } diff --git a/src/main/java/org/myrobotlab/service/MotorPort.java b/src/main/java/org/myrobotlab/service/MotorPort.java index 00abe31899..d908d79e37 100644 --- a/src/main/java/org/myrobotlab/service/MotorPort.java +++ b/src/main/java/org/myrobotlab/service/MotorPort.java @@ -15,7 +15,7 @@ * string value can handle either we use a String port. * */ -public class MotorPort extends AbstractMotor { +public class MotorPort extends AbstractMotor { private static final long serialVersionUID = 1L; public MotorPort(String n, String id) { diff --git a/src/main/java/org/myrobotlab/service/MouseSim.java b/src/main/java/org/myrobotlab/service/MouseSim.java index 33f67420c8..050aa2c7c9 100644 --- a/src/main/java/org/myrobotlab/service/MouseSim.java +++ b/src/main/java/org/myrobotlab/service/MouseSim.java @@ -8,13 +8,14 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** * @author MaVo (MyRobotLab) / LunDev (GitHub) */ -public class MouseSim extends Service { +public class MouseSim extends Service { static final long serialVersionUID = 1L; static final Logger log = LoggerFactory.getLogger(MouseSim.class); diff --git a/src/main/java/org/myrobotlab/service/MouthControl.java b/src/main/java/org/myrobotlab/service/MouthControl.java index 5653c97a62..9d07a7c55e 100644 --- a/src/main/java/org/myrobotlab/service/MouthControl.java +++ b/src/main/java/org/myrobotlab/service/MouthControl.java @@ -19,7 +19,7 @@ * It's peers are the jaw servo, speech service and an arduino. * */ -public class MouthControl extends Service implements SpeechListener { +public class MouthControl extends Service implements SpeechListener { private static final long serialVersionUID = 1L; @@ -229,9 +229,9 @@ public void setmouth(Integer closed, Integer opened) { } @Override - public ServiceConfig getConfig() { + public MouthControlConfig getConfig() { - MouthControlConfig config = (MouthControlConfig)super.getConfig(); + super.getConfig(); // FIXME - remove local fields, use config only config.jaw = jaw; config.mouth = mouth; @@ -246,23 +246,23 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - MouthControlConfig config = (MouthControlConfig) super.apply(c); + public MouthControlConfig apply(MouthControlConfig c) { + super.apply(c); // FIXME - remove local fields, use config only - mouthClosedPos = config.mouthClosedPos; - mouthOpenedPos = config.mouthOpenedPos; - delaytime = config.delaytime; - delaytimestop = config.delaytimestop; - delaytimeletter = config.delaytimeletter; - jaw = config.jaw; - neoPixel = config.neoPixel; + mouthClosedPos = c.mouthClosedPos; + mouthOpenedPos = c.mouthOpenedPos; + delaytime = c.delaytime; + delaytimestop = c.delaytimestop; + delaytimeletter = c.delaytimeletter; + jaw = c.jaw; + neoPixel = c.neoPixel; // mouth needs to attach to us // it needs to create notify entries // so we fire a message to attach to us - mouth = config.mouth; - if (config.mouth != null) { - mouth = config.mouth; + mouth = c.mouth; + if (c.mouth != null) { + mouth = c.mouth; send(mouth, "attach", getName()); } diff --git a/src/main/java/org/myrobotlab/service/Mpr121.java b/src/main/java/org/myrobotlab/service/Mpr121.java index e0d4bced9c..d71ca4da60 100644 --- a/src/main/java/org/myrobotlab/service/Mpr121.java +++ b/src/main/java/org/myrobotlab/service/Mpr121.java @@ -33,7 +33,7 @@ * https://www.sparkfun.com/datasheets/Components/MPR121.pdf * */ -public class Mpr121 extends Service implements I2CControl, PinArrayControl { +public class Mpr121 extends Service implements I2CControl, PinArrayControl { /** * Publisher - Publishes pin data at a regular interval */ @@ -926,25 +926,19 @@ public void setDeviceAddress(String deviceAddress) { } @Override - public ServiceConfig getConfig() { - Mpr121Config config = (Mpr121Config) super.getConfig(); - return config; - } - - @Override - public ServiceConfig apply(ServiceConfig c) { - Mpr121Config config = (Mpr121Config) super.apply(c); + public Mpr121Config apply(Mpr121Config c) { + super.apply(c); // FIXME remove local fields in favor of config only - if (config.address != null) { - setAddress(config.address); + if (c.address != null) { + setAddress(c.address); } - if (config.bus != null) { - setBus(config.bus); + if (c.bus != null) { + setBus(c.bus); } - if (config.controller != null) { + if (c.controller != null) { try { - attach(config.controller); + attach(c.controller); } catch (Exception e) { error(e); } diff --git a/src/main/java/org/myrobotlab/service/Mpu6050.java b/src/main/java/org/myrobotlab/service/Mpu6050.java index cd4d6956b4..ac33ec9907 100644 --- a/src/main/java/org/myrobotlab/service/Mpu6050.java +++ b/src/main/java/org/myrobotlab/service/Mpu6050.java @@ -39,7 +39,7 @@ * */ -public class Mpu6050 extends Service implements I2CControl, OrientationPublisher { +public class Mpu6050 extends Service implements I2CControl, OrientationPublisher { private static final long serialVersionUID = 1L; @@ -4435,8 +4435,8 @@ public boolean isAttached(Attachable instance) { } @Override - public ServiceConfig getConfig() { - Mpu6050Config config = (Mpu6050Config)super.getConfig(); + public Mpu6050Config getConfig() { + super.getConfig(); // FIXME remove local fields in favor or config config.start = publisher.isRunning; config.sampleRate = sampleRateHz; @@ -4447,7 +4447,7 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { + public Mpu6050Config apply(Mpu6050Config c) { Mpu6050Config config = (Mpu6050Config) super.apply(c); // FIXME remove local fields in favor or config if (config.start) { diff --git a/src/main/java/org/myrobotlab/service/Mqtt.java b/src/main/java/org/myrobotlab/service/Mqtt.java index 716293866a..8466016039 100644 --- a/src/main/java/org/myrobotlab/service/Mqtt.java +++ b/src/main/java/org/myrobotlab/service/Mqtt.java @@ -32,6 +32,7 @@ import org.myrobotlab.mqtt.MqttMsg; import org.myrobotlab.net.Connection; import org.myrobotlab.net.SslUtil; +import org.myrobotlab.service.config.MqttConfig; import org.myrobotlab.service.interfaces.Gateway; import org.myrobotlab.service.interfaces.KeyConsumer; import org.slf4j.Logger; @@ -69,7 +70,7 @@ * @author kmcgerald and GroG * */ -public class Mqtt extends Service implements MqttCallback, IMqttActionListener, Gateway, KeyConsumer { +public class Mqtt extends Service implements MqttCallback, IMqttActionListener, Gateway, KeyConsumer { public final static Logger log = LoggerFactory.getLogger(Mqtt.class); @@ -385,11 +386,6 @@ public Map getClients() { return Runtime.getInstance().getConnections(getName()); } - @Override - public Message getDescribeMsg(String connId) { - return Runtime.getInstance().getDescribeMsg(connId); - } - public String getPassword() { return password; } @@ -572,8 +568,9 @@ public void messageArrived(String topic, MqttMessage message) throws MqttExcepti sendRemote(subscribe); // 4. describe new instance for me + // FIXME why isn't this using Gateway.getDescribeMessage()? Message describe = Message.createMessage(String.format("%s@%s", getName(), getId()), "runtime@" + remoteId, "describe", - new Object[] { "fill-uuid", new DescribeQuery(Platform.getLocalInstance().getId(), uuid) }); + new Object[] { Gateway.FILL_UUID_MAGIC_VAL, new DescribeQuery(Platform.getLocalInstance().getId(), uuid) }); describe.sendingMethod = "onConnect"; sendRemote(describe); @@ -834,7 +831,7 @@ public void subscribe(String topic, int qos) throws MqttException { } String tokenToString(IMqttToken token) { - // FIXME - just gson encode it.. + StringBuffer sb = new StringBuffer(); sb.append(" MessageId:").append(token.getMessageId()); sb.append(" Response:").append(token.getResponse()); diff --git a/src/main/java/org/myrobotlab/service/MqttBroker.java b/src/main/java/org/myrobotlab/service/MqttBroker.java index eb56541a1c..2a80bd9edd 100644 --- a/src/main/java/org/myrobotlab/service/MqttBroker.java +++ b/src/main/java/org/myrobotlab/service/MqttBroker.java @@ -60,7 +60,7 @@ * whole array is decoded - then each parameter is, similar to http form values * */ -public class MqttBroker extends Service implements InterceptHandler, Gateway, KeyConsumer { +public class MqttBroker extends Service implements InterceptHandler, Gateway, KeyConsumer { public final static Logger log = LoggerFactory.getLogger(MqttBroker.class); @@ -169,10 +169,11 @@ public MqttBroker(String n, String id) { password = security.getKey(getName() + ".password"); } + // FIXME - more than one type of gateway ... client gateway and server gateway @Override public void connect(String uri) throws Exception { - // TODO Auto-generated method stub - + // Mqtt Brokers do not "connect" to other instances + // NOOP } public String getAddress() { @@ -189,11 +190,6 @@ public Map getClients() { return Runtime.getInstance().getConnections(getName()); } - @Override - public Message getDescribeMsg(String connId) { - return Runtime.getInstance().getDescribeMsg(connId); - } - @Override public String getID() { return getName(); @@ -344,8 +340,12 @@ public void onPublish(InterceptPublishMessage im) { // don't let broker process messages if (processApiMessages && topic.startsWith(serviceTopic)) { String mrlUri = "/" + topic.substring((serviceTopic).length()); - Message msg = CodecUtils.cliToMsg(null, getFullName(), null, mrlUri); + // FIXME - should they all be full name ? + // This will parse a topic into a json parameter message + // the message params can then be decoded with getDecodedJsonParameters + Message msg = CodecUtils.pathToMsg(getFullName(), mrlUri); String payload = m.getPayload(); + // payload takes precedence over path if (payload != null && payload.length() > 0) { msg.data = CodecUtils.decodeArray(payload); } @@ -588,6 +588,16 @@ public void unsubscribe(String mqttTopic, String callbackName, String callbackMe public static void main(String[] args) { try { LoggingFactory.init("info"); + + + Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); + + + boolean done = true; + if (done) { + return; + } + Runtime.main(new String[] { "--id", "c2"}); Python python = (Python) Runtime.start("python", "Python"); @@ -601,9 +611,13 @@ public static void main(String[] args) { Clock clock01 = (Clock) Runtime.start("clock01", "Clock"); // clock01.startClock(); + + + Mqtt mqtt = (Mqtt) Runtime.start("mqtt02", "Mqtt"); mqtt.setAutoConnect(false); + mqtt.connect("mqtt://localhost:1883"); // mqtt.connect("mqtt://test.mosquitto.org:1883"); // mqtt.publish("mrl/"); @@ -659,20 +673,20 @@ public void setKey(String keyName, String keyValue) { } @Override - public ServiceConfig getConfig() { - MqttBrokerConfig c = (MqttBrokerConfig)super.getConfig(); + public MqttBrokerConfig getConfig() { + super.getConfig(); // FIXME - remove local fields in favor of just config - c.address = address; - c.mqttPort = mqttPort; - c.wsPort = wsPort; - c.username = username; - c.password = password; - return c; + config.address = address; + config.mqttPort = mqttPort; + config.wsPort = wsPort; + config.username = username; + config.password = password; + return config; } @Override - public ServiceConfig apply(ServiceConfig c) { - MqttBrokerConfig config = (MqttBrokerConfig) super.apply(c); + public MqttBrokerConfig apply(MqttBrokerConfig c) { + super.apply(c); // FIXME - remove local fields in favor of just config address = config.address; mqttPort = config.mqttPort; diff --git a/src/main/java/org/myrobotlab/service/MultiWii.java b/src/main/java/org/myrobotlab/service/MultiWii.java index 457015cfd5..d91cdb8d58 100644 --- a/src/main/java/org/myrobotlab/service/MultiWii.java +++ b/src/main/java/org/myrobotlab/service/MultiWii.java @@ -5,6 +5,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.SerialDevice; import org.slf4j.Logger; @@ -16,7 +17,7 @@ * MultiWii is a general purpose software to control a multirotor RC model. * http://www.multiwii.com/ */ -public class MultiWii extends Service { +public class MultiWii extends Service { transient public SerialDevice serial; diff --git a/src/main/java/org/myrobotlab/service/MyoThalmic.java b/src/main/java/org/myrobotlab/service/MyoThalmic.java index 496442ccf7..f658c9e1bf 100644 --- a/src/main/java/org/myrobotlab/service/MyoThalmic.java +++ b/src/main/java/org/myrobotlab/service/MyoThalmic.java @@ -5,6 +5,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.MyoData; import org.myrobotlab.service.interfaces.MyoDataListener; import org.myrobotlab.service.interfaces.MyoDataPublisher; @@ -44,7 +45,7 @@ * https://github.com/NicholasAStuart/myo-java-JNI-Library * */ -public class MyoThalmic extends Service implements DeviceListener, MyoDataListener, MyoDataPublisher { +public class MyoThalmic extends Service implements DeviceListener, MyoDataListener, MyoDataPublisher { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 47fa43eff2..e13daf273d 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -44,7 +44,7 @@ import org.myrobotlab.service.interfaces.NeoPixelController; import org.slf4j.Logger; -public class NeoPixel extends Service implements NeoPixelControl { +public class NeoPixel extends Service implements NeoPixelControl { /** * Thread to do animations Java side and push the changing of pixels to the diff --git a/src/main/java/org/myrobotlab/service/OakD.java b/src/main/java/org/myrobotlab/service/OakD.java index 902cf34bbd..99e9fdbfea 100644 --- a/src/main/java/org/myrobotlab/service/OakD.java +++ b/src/main/java/org/myrobotlab/service/OakD.java @@ -5,7 +5,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.ServiceConfig; -import org.myrobotlab.service.config.ServoConfig; import org.slf4j.Logger; /** * @@ -15,7 +14,7 @@ * @author GroG * */ -public class OakD extends Service { +public class OakD extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/OculusDiy.java b/src/main/java/org/myrobotlab/service/OculusDiy.java index a4097da3ab..a848cb2989 100644 --- a/src/main/java/org/myrobotlab/service/OculusDiy.java +++ b/src/main/java/org/myrobotlab/service/OculusDiy.java @@ -7,6 +7,7 @@ import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.math.MapperLinear; +import org.myrobotlab.service.config.OculusDiyConfig; import org.myrobotlab.service.data.Orientation; import org.myrobotlab.service.interfaces.OrientationListener; import org.myrobotlab.service.interfaces.PinArrayControl; @@ -18,7 +19,7 @@ * build of MRLComm to work. Check with \@Alessandruino for questions. * */ -public class OculusDiy extends Service implements OrientationListener { +public class OculusDiy extends Service implements OrientationListener { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/OculusRift.java b/src/main/java/org/myrobotlab/service/OculusRift.java index 7bea8f6e63..fadc3f2bef 100644 --- a/src/main/java/org/myrobotlab/service/OculusRift.java +++ b/src/main/java/org/myrobotlab/service/OculusRift.java @@ -13,6 +13,7 @@ import org.myrobotlab.opencv.OpenCVFilterTranspose; import org.myrobotlab.opencv.OpenCVFilterUndistort; import org.myrobotlab.opencv.OpenCVFilterYolo; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.Orientation; import org.myrobotlab.service.interfaces.PointPublisher; import org.slf4j.Logger; @@ -36,7 +37,7 @@ * */ // TODO: implement publishOculusRiftData ... -public class OculusRift extends Service implements PointPublisher { +public class OculusRift extends Service implements PointPublisher { public static final int ABS_TIME_MS = 0; public static final boolean LATENCY_MARKER = false; diff --git a/src/main/java/org/myrobotlab/service/OledSsd1306.java b/src/main/java/org/myrobotlab/service/OledSsd1306.java index aed9aa7b4d..eb75bbeca4 100644 --- a/src/main/java/org/myrobotlab/service/OledSsd1306.java +++ b/src/main/java/org/myrobotlab/service/OledSsd1306.java @@ -19,6 +19,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.I2CControl; import org.myrobotlab.service.interfaces.I2CController; import org.slf4j.Logger; @@ -49,7 +50,7 @@ * pin and VCC * */ -public class OledSsd1306 extends Service implements I2CControl { +public class OledSsd1306 extends Service implements I2CControl { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java index 0060bc2e5b..7abc45de8b 100644 --- a/src/main/java/org/myrobotlab/service/OpenCV.java +++ b/src/main/java/org/myrobotlab/service/OpenCV.java @@ -41,7 +41,6 @@ import java.lang.reflect.InvocationTargetException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -96,8 +95,8 @@ import org.bytedeco.opencv.opencv_core.Rect; import org.bytedeco.opencv.opencv_imgproc.CvFont; import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.cv.CvData; -import org.myrobotlab.cv.CvFilter; +import org.myrobotlab.cv.CVData; +import org.myrobotlab.cv.CVFilter; import org.myrobotlab.document.Classification; import org.myrobotlab.document.Classifications; import org.myrobotlab.framework.Instantiator; @@ -126,7 +125,6 @@ import org.myrobotlab.reflection.Reflector; import org.myrobotlab.service.abstracts.AbstractComputerVision; import org.myrobotlab.service.config.OpenCVConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.ImageData; import org.myrobotlab.service.interfaces.ImageListener; import org.myrobotlab.service.interfaces.ImagePublisher; @@ -145,7 +143,7 @@ * Audet : https://github.com/bytedeco/javacv * */ -public class OpenCV extends AbstractComputerVision implements ImagePublisher { +public class OpenCV extends AbstractComputerVision implements ImagePublisher { int vpId = 0; @@ -551,7 +549,7 @@ public static IplImage cropImage(IplImage img, CvRect rect) { boolean undockDisplay = false; - final private VideoProcessor vp = new VideoProcessor(); + final transient private VideoProcessor vp = new VideoProcessor(); Integer width = null; @@ -653,15 +651,19 @@ synchronized public OpenCVFilter addFilter(OpenCVFilter filter) { * - name of filter * @return the filter */ - public CvFilter addFilter(String filterName) { + public CVFilter addFilter(String filterName) { String filterType = filterName.substring(0, 1).toUpperCase() + filterName.substring(1); return addFilter(filterName, filterType); } @Override - public CvFilter addFilter(String name, String filterType) { + public CVFilter addFilter(String name, String filterType) { String type = String.format("org.myrobotlab.opencv.OpenCVFilter%s", filterType); OpenCVFilter filter = (OpenCVFilter) Instantiator.getNewInstance(type, name); + if (filter == null) { + error("cannot create filter %s of type %s", name, type); + return null; + } addFilter(filter); return filter; } @@ -833,7 +835,10 @@ public List getFaces(int timeout) { // fd.enable(); long startTs = System.currentTimeMillis(); while (!ret.keySet().contains("face") && System.currentTimeMillis() - startTs < timeout) { - ret.putAll(blockingClassification.poll(timeout, TimeUnit.MILLISECONDS)); + Map> faces = blockingClassification.poll(timeout, TimeUnit.MILLISECONDS); + if (faces != null) { + ret.putAll(faces); + } } } catch (InterruptedException e) { } @@ -1155,7 +1160,7 @@ public String toBase64Jpg(BufferedImage img) { final ByteArrayOutputStream os = new ByteArrayOutputStream(); ImageIO.write(img, "jpg", os); os.close(); - String ret = Base64.getEncoder().encodeToString(os.toByteArray()); + String ret = CodecUtils.toBase64(os.toByteArray()); return ret; } catch (Exception e) { log.error("toBase64Jpg threw", e); @@ -1438,7 +1443,7 @@ public final OpenCVData publishOpenCVData(OpenCVData data) { return data; } - public final CvData publishCvData(CvData data) { + public final CVData publishCvData(CVData data) { return data; } @@ -2004,7 +2009,7 @@ public void saveFile(String filename, String data) { try { String path = FileIO.gluePaths(getDataDir(), filename); fos = new FileOutputStream(path); - byte[] decoded = Base64.getDecoder().decode(data); + byte[] decoded = CodecUtils.fromBase64(data); fos.write(decoded); fos.close(); setInputFileName(path); @@ -2021,8 +2026,8 @@ public void saveFile(String filename, String data) { } @Override - public ServiceConfig getConfig() { - OpenCVConfig config = (OpenCVConfig) super.getConfig(); + public OpenCVConfig getConfig() { + super.getConfig(); // FIXME - remove member vars use config only config.capturing = capturing; config.cameraIndex = cameraIndex; @@ -2039,26 +2044,26 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - OpenCVConfig config = (OpenCVConfig) super.apply(c); - setCameraIndex(config.cameraIndex); - setGrabberType(config.grabberType); - setInputFileName(config.inputFile); - setInputSource(config.inputSource); + public OpenCVConfig apply(OpenCVConfig c) { + super.apply(c); + setCameraIndex(c.cameraIndex); + setGrabberType(c.grabberType); + setInputFileName(c.inputFile); + setInputSource(c.inputSource); - setNativeViewer(config.nativeViewer); + setNativeViewer(c.nativeViewer); - setWebViewer(config.webViewer); + setWebViewer(c.webViewer); filters.clear(); - if (config.filters != null) { - for (OpenCVFilter f : config.filters.values()) { + if (c.filters != null) { + for (OpenCVFilter f : c.filters.values()) { addFilter(f); // TODO: better configuration of the filter when it's added. } } - if (config.capturing) { + if (c.capturing) { capture(); } diff --git a/src/main/java/org/myrobotlab/service/OpenNi.java b/src/main/java/org/myrobotlab/service/OpenNi.java index ed7f0f65a1..fc3d278cf9 100644 --- a/src/main/java/org/myrobotlab/service/OpenNi.java +++ b/src/main/java/org/myrobotlab/service/OpenNi.java @@ -19,6 +19,7 @@ import org.myrobotlab.openni.PImage; import org.myrobotlab.openni.PVector; import org.myrobotlab.openni.Skeleton; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.VideoSink; import org.slf4j.Logger; @@ -48,7 +49,7 @@ * * */ -public class OpenNi extends Service // implements +public class OpenNi extends Service // implements // UserTracker.NewFrameListener, // HandTracker.NewFrameListener { diff --git a/src/main/java/org/myrobotlab/service/OpenWeatherMap.java b/src/main/java/org/myrobotlab/service/OpenWeatherMap.java index c2a494502a..2a9fcb045e 100644 --- a/src/main/java/org/myrobotlab/service/OpenWeatherMap.java +++ b/src/main/java/org/myrobotlab/service/OpenWeatherMap.java @@ -7,7 +7,6 @@ import org.json.JSONObject; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.config.OpenWeatherMapConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** @@ -18,7 +17,7 @@ * ( 3 hours TO 5 days forecast ) * */ -public class OpenWeatherMap extends HttpClient { + public class OpenWeatherMap extends HttpClient { private static final long serialVersionUID = 1L; private String apiForecast = "http://api.openweathermap.org/data/2.5/forecast/?q="; @@ -252,8 +251,8 @@ public String getApiKey() { } @Override - public ServiceConfig getConfig() { - OpenWeatherMapConfig config = (OpenWeatherMapConfig)super.getConfig(); + public OpenWeatherMapConfig getConfig() { + super.getConfig(); // FIXME - remove local fields in favor of only config config.currentUnits = units; config.currentTown = location; @@ -261,14 +260,14 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - OpenWeatherMapConfig config = (OpenWeatherMapConfig) super.apply(c); + public OpenWeatherMapConfig apply(OpenWeatherMapConfig c) { + super.apply(c); // FIXME - remove local fields in favor of only config - if (config.currentUnits != null) { - setUnits(config.currentUnits); + if (c.currentUnits != null) { + setUnits(c.currentUnits); } - if (config.currentTown != null) { - setLocation(config.currentTown); + if (c.currentTown != null) { + setLocation(c.currentTown); } return c; } diff --git a/src/main/java/org/myrobotlab/service/Osc.java b/src/main/java/org/myrobotlab/service/Osc.java index 63bfe69efd..5f4dc3e929 100644 --- a/src/main/java/org/myrobotlab/service/Osc.java +++ b/src/main/java/org/myrobotlab/service/Osc.java @@ -14,6 +14,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import com.illposed.osc.OSCListener; @@ -21,7 +22,7 @@ import com.illposed.osc.OSCPortIn; import com.illposed.osc.OSCPortOut; -public class Osc extends Service implements OSCListener { +public class Osc extends Service implements OSCListener { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Pcf8574.java b/src/main/java/org/myrobotlab/service/Pcf8574.java index b66373f92e..4e7410d0d2 100644 --- a/src/main/java/org/myrobotlab/service/Pcf8574.java +++ b/src/main/java/org/myrobotlab/service/Pcf8574.java @@ -16,7 +16,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.Pcf8574Config; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.I2CControl; import org.myrobotlab.service.interfaces.I2CController; @@ -45,7 +44,7 @@ * */ -public class Pcf8574 extends Service +public class Pcf8574 extends Service implements I2CControl, /* FIXME - add I2CController */ PinArrayControl { /** * Publisher - Publishes pin data at a regular interval @@ -156,9 +155,9 @@ public void run() { public Pcf8574(String n, String id) { super(n, id); - registerForInterfaceChange(I2CController.class); + // registerForInterfaceChange(I2CController.class); createPinList(); - refreshControllers(); + // refreshControllers(); for (int i = 0; i < pinDataCnt; ++i) { int value = (writeRegister >> i) & 1; getPin(i).setValue(value); @@ -726,25 +725,25 @@ public void stopService() { } @Override - public ServiceConfig getConfig() { - Pcf8574Config config = (Pcf8574Config) super.getConfig(); + public Pcf8574Config getConfig() { + super.getConfig(); return config; } @Override - public ServiceConfig apply(ServiceConfig c) { - Pcf8574Config config = (Pcf8574Config) super.apply(c); + public Pcf8574Config apply(Pcf8574Config c) { + super.apply(c); // FIXME remove local fields in favor of config only - if (config.address != null) { - setAddress(config.address); + if (c.address != null) { + setAddress(c.address); } - if (config.bus != null) { - setBus(config.bus); + if (c.bus != null) { + setBus(c.bus); } - if (config.controller != null) { + if (c.controller != null) { try { - attach(config.controller); + attach(c.controller); } catch (Exception e) { error(e); } diff --git a/src/main/java/org/myrobotlab/service/Pid.java b/src/main/java/org/myrobotlab/service/Pid.java index b8894435f8..d2a630eb2c 100644 --- a/src/main/java/org/myrobotlab/service/Pid.java +++ b/src/main/java/org/myrobotlab/service/Pid.java @@ -62,7 +62,7 @@ * https://en.wikipedia.org/wiki/PID_controller#Integral_windup * */ -public class Pid extends Service implements PidControl { +public class Pid extends Service implements PidControl { public static class PidOutput { public long ts; @@ -519,8 +519,8 @@ private double constrain(double value, Double min, Double max) { } @Override - public ServiceConfig getConfig() { - PidConfig config = (PidConfig)super.getConfig(); + public PidConfig getConfig() { + super.getConfig(); config.data = data; return config; } @@ -533,8 +533,8 @@ public void reset(String key) { } @Override - public ServiceConfig apply(ServiceConfig c) { - PidConfig config = (PidConfig) super.apply(c); + public PidConfig apply(PidConfig c) { + super.apply(c); if (config.data != null) { data = config.data; for (String key : config.data.keySet()) { diff --git a/src/main/java/org/myrobotlab/service/Pingdar.java b/src/main/java/org/myrobotlab/service/Pingdar.java index 94d540b3cc..fe931b945c 100644 --- a/src/main/java/org/myrobotlab/service/Pingdar.java +++ b/src/main/java/org/myrobotlab/service/Pingdar.java @@ -6,6 +6,7 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.sensor.EncoderData; import org.myrobotlab.sensor.EncoderListener; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.RangeListener; import org.myrobotlab.service.interfaces.RangingControl; import org.slf4j.Logger; @@ -16,7 +17,7 @@ * module. The result is a sonar style range finding. * */ -public class Pingdar extends Service implements RangingControl, RangeListener, EncoderListener { +public class Pingdar extends Service implements RangingControl, RangeListener, EncoderListener { public static class Point { @@ -178,7 +179,7 @@ public static void main(String[] args) { Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino"); arduino.connect("COM5"); sr04.attach(arduino, 12, 11); - servo.attach(arduino, 2); + servo.attach(arduino); Pingdar pingdar = (Pingdar) Runtime.start("pingdar", "Pingdar"); sleep(1000); diff --git a/src/main/java/org/myrobotlab/service/Pir.java b/src/main/java/org/myrobotlab/service/Pir.java index 21269b2dbd..5a2f0d750c 100644 --- a/src/main/java/org/myrobotlab/service/Pir.java +++ b/src/main/java/org/myrobotlab/service/Pir.java @@ -1,17 +1,22 @@ package org.myrobotlab.service; +import java.util.ArrayList; +import java.util.List; + +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Service; -import org.myrobotlab.framework.interfaces.ServiceInterface; +import org.myrobotlab.framework.TimeoutException; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.PirConfig; import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.PinArrayControl; +import org.myrobotlab.service.interfaces.PinDefinition; import org.myrobotlab.service.interfaces.PinListener; import org.slf4j.Logger; -public class Pir extends Service implements PinListener { +public class Pir extends Service implements PinListener { public final static Logger log = LoggerFactory.getLogger(Pir.class); @@ -24,103 +29,96 @@ public class Pir extends Service implements PinListener { */ Boolean active = null; - /** - * The pin to be used as a string. Example "D4" or "A0". - */ - // String pin; - - transient PinArrayControl pinControl; - /** * Timestamp of the last poll. */ Long lastChangeTs = null; - boolean attached = false; + protected boolean isAttached = false; public Pir(String n, String id) { super(n, id); - registerForInterfaceChange(PinArrayControl.class); } @Deprecated /* use attach(String) or attachPinArrayControl(PinArrayControl) */ public void attach(PinArrayControl control, String pin) { setPin(pin); - attachPinArrayControl(control); + attachPinArrayControl(control.getName()); } @Override public void attach(String name) { - ServiceInterface si = Runtime.getService(name); - if (si instanceof PinArrayControl) { - attachPinArrayControl((PinArrayControl) si); - } else { - error("do not know how to attach to %s of type %s", name, si.getSimpleName()); - } + attachPinArrayControl(name); } - public void attachPinArrayControl(PinArrayControl control) { + public void setPinArrayControl(String control) { PirConfig c = (PirConfig) config; - try { - this.pinControl = control; - c.controller = control.getName(); - - if (c.pin == null) { - error("pin should be set before attaching"); - } - pinControl.attach(getName()); - attached = true; - broadcastState(); - } catch (Exception e) { - error(e); - } + c.controller = control; } - @Override - public void detach(String name) { + public void attachPinArrayControl(String control) { PirConfig c = (PirConfig) config; + + if (control == null) { + error("controller cannot be null"); + return; + } + + if (c.pin == null) { + error("pin should be set before attaching"); + return; + } + + c.controller = CodecUtils.getShortName(control); + + // fire and forget + send(c.controller, "attach", getName()); + // assume worky + isAttached = true; - ServiceInterface si = Runtime.getService(name); - if (si instanceof PinArrayControl && c.pin != null) { - // FIXME - problem - what if someone else is using this pin ? - // FIXME - should disable in the context of this service's name - ((PinArrayControl) si).disablePin(c.pin); - detachPinArrayControl((PinArrayControl) si); - } + // enable if configured + if (c.enable) { + send(c.controller, "enablePin", c.pin, c.rate); + } - active = null; - c.enable = false; - attached = false; broadcastState(); } - public void detachPinArrayControl(PinArrayControl control) { + @Override + public void detach(String name) { + detachPinArrayControl(name); + } + + /** + * FIXME - use interface of service names not direct references + * + * @param control + */ + public void detachPinArrayControl(String control) { PirConfig c = (PirConfig) config; - try { if (control == null) { log.info("detaching null"); return; } if (c.controller != null) { - if (!c.controller.equals(control.getName())) { - log.warn("attempting to detach {} but this pir is attached to {}", control.getName(), c.controller); + if (!c.controller.equals(control)) { + log.warn("attempting to detach {} but this pir is attached to {}", control, c.controller); return; } } - // FYI - we could detach like this without a reference - good for remote - // send(controllerName, "detach", getName()); - pinControl.detach(getName()); + // disable + disable(); + + send(c.controller, "detach", getName()); + // c.controller = null; left as configuration .. "last controller" - this.pinControl = null; - c.controller = null; + // detached + isAttached = false; broadcastState(); - } catch (Exception e) { - error(e); - } } /** @@ -132,8 +130,8 @@ public void detachPinArrayControl(PinArrayControl control) { public void disable() { PirConfig c = (PirConfig) config; - if (pinControl != null && c.pin != null) { - pinControl.disablePin(c.pin); + if (c.controller != null && c.pin != null) { + send(c.controller, "disablePin", c.pin); } c.enable = false; @@ -158,7 +156,7 @@ public void enable() { public void enable(int rateHz) { PirConfig c = (PirConfig) config; - if (pinControl == null) { + if (c.controller == null) { error("pin control not set"); return; } @@ -168,13 +166,14 @@ public void enable(int rateHz) { return; } - if (rateHz < 1) { + if (rateHz < 0) { error("invalid poll rate - default is 1 Hz valid value is > 0"); return; } c.rate = rateHz; - pinControl.enablePin(c.pin, rateHz); + /* PinArrayControl.enablePin */ + send(c.controller, "enablePin", c.pin, rateHz); c.enable = true; broadcastState(); } @@ -217,10 +216,14 @@ public boolean isEnabled() { } @Override - public ServiceConfig apply(ServiceConfig c) { - PirConfig config = (PirConfig) super.apply(c); + public PirConfig apply(PirConfig c) { + super.apply(c); + + if (config.controller != null) { + attach(config.controller);; + } - if (config.enable) { + if (config.enable) { enable(config.rate); } else { disable(); @@ -228,16 +231,29 @@ public ServiceConfig apply(ServiceConfig c) { return c; } + + @Override + public PirConfig getConfig() { + super.getConfig(); + if (config.controller != null) { + // it makes sense that the controller should always be local for a PIR + // but in general this is bad practice on 2 levels + // 1. in some other context it might make sense not to be local + // 2. it should just be another listener on ServiceConfig.listener + config.controller=CodecUtils.getShortName(config.controller); + } + return config; + } @Override public void onPin(PinData pindata) { - + PirConfig c = (PirConfig) config; log.debug("onPin {}", pindata); boolean sense = (pindata.value != 0); - // sparse publishing only on state change - if (active == null || active != sense) { + // sparse publishing only on state change + if (active == null || active != sense && c.enable) { // state change invoke("publishSense", sense); active = sense; @@ -255,11 +271,11 @@ public Boolean publishSense(Boolean b) { } public void publishPirOn() { - log.info("publishPirOn"); + log.debug("publishPirOn"); } public void publishPirOff() { - log.info("publishPirOff"); + log.debug("publishPirOff"); } /** @@ -277,7 +293,6 @@ public void setPin(String pin) { @Deprecated /* use attach(String) */ public void setPinArrayControl(PinArrayControl pinControl) { PirConfig c = (PirConfig) config; - this.pinControl = pinControl; c.controller = pinControl.getName(); } @@ -304,23 +319,53 @@ public Long getLastChangeTs() { return lastChangeTs; } + /** + * This returns the pin list of the selected PinArrayControl. This allows + * dynamic selection of a pin based on a query to a PinArrayControl. It would + * be advisable that other services manage pins in the same way. Where + * "selecting" the controller's name, returns the possible list of pins to + * attach. + * + * @return + * @throws InterruptedException + * @throws TimeoutException + */ + @SuppressWarnings("unchecked") + public List getPinList(String pinArrayControl) { + List pinList = new ArrayList<>(); + try { + if (pinArrayControl != null) { + pinList = (List) sendBlocking(pinArrayControl, "getPinList"); + } + } catch (Exception e) { + error(e); + } + return pinList; + } + public static void main(String[] args) { try { LoggingFactory.init("info"); - Pir pir = (Pir) Runtime.start("pir", "Pir"); - pir.setPin("D6"); - Runtime.start("webgui", "WebGui"); - Arduino mega = (Arduino) Runtime.start("mega", "Arduino"); - mega.connect("/dev/ttyACM2"); + // Runtime.start("webgui", "WebGui"); + + // standard install - develop and debug using config + Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); + boolean done = true; if (done) { return; } + Pir pir = (Pir) Runtime.start("pir", "Pir"); + pir.setPin("D23"); + + Arduino mega = (Arduino) Runtime.start("mega", "Arduino"); + mega.connect("/dev/ttyACM71"); + mega.attach(pir); // Runtime.setAllVirtual(true); diff --git a/src/main/java/org/myrobotlab/service/Polly.java b/src/main/java/org/myrobotlab/service/Polly.java index e2acb2c260..5a447902d4 100644 --- a/src/main/java/org/myrobotlab/service/Polly.java +++ b/src/main/java/org/myrobotlab/service/Polly.java @@ -45,7 +45,7 @@ * @author GroG * */ -public class Polly extends AbstractSpeechSynthesis { +public class Polly extends AbstractSpeechSynthesis { private static final long serialVersionUID = 1L; @@ -253,8 +253,7 @@ public boolean isReady() { return ready; } - @Override - public ServiceConfig apply(ServiceConfig c) { + public PollyConfig apply(PollyConfig c) { super.apply(c); getVoices(); return c; diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java index 90aa90216e..29b8653ad8 100644 --- a/src/main/java/org/myrobotlab/service/ProgramAB.java +++ b/src/main/java/org/myrobotlab/service/ProgramAB.java @@ -46,6 +46,7 @@ import org.myrobotlab.service.interfaces.UtteranceListener; import org.myrobotlab.service.interfaces.UtterancePublisher; import org.slf4j.Logger; +import org.yaml.snakeyaml.Yaml; /** * Program AB service for MyRobotLab Uses AIML 2.0 to create a ChatBot This is a @@ -60,7 +61,7 @@ * @author kwatters * */ -public class ProgramAB extends Service +public class ProgramAB extends Service implements TextListener, TextPublisher, LocaleProvider, LogPublisher, ProgramABListener, UtterancePublisher, UtteranceListener, ResponsePublisher { /** @@ -76,13 +77,6 @@ public class ProgramAB extends Service */ protected boolean useGlobalSession = false; - /** - * 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 - */ - protected boolean sleep = false; - transient public final static Logger log = LoggerFactory.getLogger(ProgramAB.class); /** @@ -95,17 +89,6 @@ public class ProgramAB extends Service */ Map sessions = new TreeMap<>(); - /** - * initial bot name - this bot comes with ProgramAB this will be the result of - * whatever is scanned in the constructor - */ - String currentBotName = null; - - /** - * default user name chatting with the bot - */ - String currentUserName = "human"; - /** * start GoogleSearch (a peer) instead of sraix web service which is down or * problematic much of the time @@ -125,46 +108,6 @@ public class ProgramAB extends Service */ public ProgramAB(String n, String id) { super(n, id); - - // TODO - allow lazy selection of bot - even if it currently doesn't exist - // in the bot map - move scanning to start - - // 1. scan resources .. either "resource/ProgramAB" or - // ../ProgramAB/resource/ProgramAB (for dev) for valid bot directories - - // List resourceBots = scanForBots(getResourceDir()); - // - // if (isDev()) { - // // 2. dev loading "only" dev bots - from dev location - // for (File file : resourceBots) { - // addBotPath(file.getAbsolutePath()); - // } - // } else { - // // 2. runtime loading - // // copy any bot in "resource/ProgramAB/{botName}" not found in - // // "data/ProgramAB/{botName}" - // for (File file : resourceBots) { - // String botName = getBotName(file); - // File dataBotDir = new File(FileIO.gluePaths("data/ProgramAB", botName)); - // if (dataBotDir.exists()) { - // log.info("found data/ProgramAB/{} not copying", botName); - // } else { - // log.info("will copy new data/ProgramAB/{}", botName); - // try { - // FileIO.copy(file, dataBotDir); - // } catch (Exception e) { - // error(e); - // } - // } - // } - // - // // 3. addPath for all bots found in "data/ProgramAB/" - // List dataBots = scanForBots("data/ProgramAB"); - // for (File file : dataBots) { - // addBotPath(file.getAbsolutePath()); - // } - // } - } public String getBotName(File file) { @@ -197,6 +140,7 @@ public List scanForBots(String path) { if (checkIfValid(file)) { info("found %s bot directory", file.getName()); botDirs.add(file); + addBotPath(file.getAbsolutePath()); } } return botDirs; @@ -615,7 +559,7 @@ public void reloadSession(String userName, String botName) throws IOException { * @return */ public Map getPredicates() { - return getPredicates(currentUserName, currentBotName); + return getPredicates(config.currentUserName, config.currentBotName); } /** @@ -633,11 +577,8 @@ public Map getPredicates(String userName, String botName) { /** * Save all the predicates for all known sessions. - * - * @throws IOException - * boom */ - public void savePredicates() throws IOException { + public void savePredicates() { for (Session session : sessions.values()) { session.savePredicates(); } @@ -696,7 +637,7 @@ public void removeBotProperty(String botName, String name) { } public Session startSession() throws IOException { - return startSession(currentUserName); + return startSession(config.currentUserName); } // FIXME - it should just set the current userName only @@ -817,25 +758,7 @@ public void addCategory(String pattern, String template, String that) { public void addCategory(String pattern, String template) { addCategory(pattern, template, "*"); } - - /** - * writeAndQuit will write brain to disk For learn.aiml is concerned - */ - public void writeAndQuit() { - // write out all bots aiml & save all predicates for all sessions? - for (BotInfo bot : bots.values()) { - if (bot.isActive()) { - try { - savePredicates(); - // important to save learnf.aiml - // bot.writeQuit(); - } catch (IOException e1) { - log.error("saving predicates threw", e1); - } - } - } - } - + /** * Verifies and adds a new path to the search directories for bots * @@ -867,7 +790,6 @@ public String addBotPath(String path) { bots.put(botInfo.name, botInfo); botInfo.img = getBotImage(botInfo.name); - setCurrentBotName(botInfo.name); broadcastState(); } else { error("invalid bot path - a bot must be a directory with a subdirectory named \"aiml\""); @@ -887,13 +809,13 @@ public String setPath(String path) { } public void setCurrentBotName(String botName) { - this.currentBotName = botName; + config.currentBotName = botName; invoke("getBotImage", botName); broadcastState(); } public void setCurrentUserName(String currentUserName) { - this.currentUserName = currentUserName; + config.currentUserName = currentUserName; broadcastState(); } @@ -906,11 +828,11 @@ public String getSessionKey(String userName, String botName) { } public String getCurrentUserName() { - return currentUserName; + return config.currentUserName; } public String getCurrentBotName() { - return currentBotName; + return config.currentBotName; } /** @@ -956,29 +878,6 @@ public Set initBotPaths() { } } } - - // check for 'local' bots in /data/ProgramAB dir - - // check for dev bots - if (getResourceDir().startsWith("src")) { - log.info("in dev mode resourceDir starts with src"); - // automatically look in ../ProgramAB for the cloned repo - // look for dev paths in ../ProgramAB - File devRepoCheck = new File("../ProgramAB/resource/ProgramAB/bots"); - if (devRepoCheck.exists() && devRepoCheck.isDirectory()) { - log.info("found repo {} adding bot paths", devRepoCheck.getAbsoluteFile()); - File[] listOfFiles = devRepoCheck.listFiles(); - for (int i = 0; i < listOfFiles.length; i++) { - if (listOfFiles[i].isFile()) { - } else if (listOfFiles[i].isDirectory()) { - paths.add(listOfFiles[i].getAbsolutePath()); - } - } - } else { - log.error("ProgramAB is a service module clone it at the same level as myrobotlab"); - } - } - return paths; } @@ -1021,7 +920,7 @@ public void attach(Attachable attachable) { @Override public void stopService() { super.stopService(); - writeAndQuit(); + savePredicates(); } public boolean setPeerSearch(boolean b) { @@ -1041,6 +940,8 @@ public void startService() { logging.setLevel("class org.myrobotlab.programab.MrlSraixHandler", "DEBUG"); logPublisher.start(); + scanForBots(getResourceDir()); + } @Override /* FIXME - just do this once in abstract */ @@ -1091,7 +992,7 @@ public String publishLog(String msg) { } public BotInfo getBotInfo() { - return getBotInfo(currentBotName); + return getBotInfo(config.currentBotName); } /** @@ -1170,15 +1071,8 @@ public void saveAimlFile(String botName, String filename, String data) { } @Override - public ServiceConfig getConfig() { - ProgramABConfig config = (ProgramABConfig) super.getConfig(); - // REMOVED from overlap with subscriptions - // Set listeners = getAttached("publishText"); - // config.textListeners = listeners.toArray(new String[listeners.size()]); - - // listeners = getAttached("publishUtterance"); - // config.utteranceListeners = listeners.toArray(new - // String[listeners.size()]); + public ProgramABConfig getConfig() { + super.getConfig(); if (config.bots == null) { config.bots = new ArrayList<>(); } @@ -1193,57 +1087,42 @@ public ServiceConfig getConfig() { } - config.currentBotName = currentBotName; - config.currentUserName = currentUserName; - return config; } @Override - public ServiceConfig apply(ServiceConfig c) { - ProgramABConfig config = (ProgramABConfig) super.apply(c); - if (config.bots != null && config.bots.size() > 0) { - bots.clear(); - for (String botPath : config.bots) { + public ProgramABConfig apply(ProgramABConfig c) { + super.apply(c); + if (c.bots != null && c.bots.size() > 0) { + // bots.clear(); + for (String botPath : c.bots) { addBotPath(botPath); } } - - if (config.botDir != null) { - List botsFromScanning = scanForBots(config.botDir); - for (File file : botsFromScanning) { - addBotPath(file.getAbsolutePath()); - } + + if (c.botDir == null) { + c.botDir = getResourceDir(); } - if (config.currentBotName != null) { - setCurrentBotName(config.currentBotName); + List botsFromScanning = scanForBots(c.botDir); + for (File file : botsFromScanning) { + addBotPath(file.getAbsolutePath()); } - if (config.currentUserName != null) { - setCurrentUserName(config.currentUserName); + if (c.currentUserName != null) { + setCurrentUserName(c.currentUserName); } + + if (c.currentBotName != null) { + setCurrentBotName(c.currentBotName); + } + + if (c.startTopic != null) { + setTopic(c.startTopic); + } + - // useGlobalSession = config.useGlobalSession; - - sleep = config.sleep; - - setCurrentSession(currentUserName, currentBotName); - - // REMOVED because of overlap with subscriptions - // if (config.textListeners != null) { - // for (String local : config.textListeners) { - // attachTextListener(local); - // } - // } - // - // if (config.utteranceListeners != null) { - // for (String local : config.utteranceListeners) { - // attachUtteranceListener(local); - // } - // } - - return config; + return c; } public static void main(String args[]) { @@ -1330,11 +1209,16 @@ synchronized public void onChangePredicate(Chat chat, String predicateName, Stri } /** - * Predicate updates are published here. Topic (one of the most important predicate change) is also published - * when it changes. Session is needed to extract current user and bot this is relevant to. - * @param session - session where the predicate change occurred - * @param name - name of predicate - * @param value - new value of predicate + * Predicate updates are published here. Topic (one of the most important + * predicate change) is also published when it changes. Session is needed to + * extract current user and bot this is relevant to. + * + * @param session + * - session where the predicate change occurred + * @param name + * - name of predicate + * @param value + * - new value of predicate * @return */ public PredicateEvent publishPredicate(Session session, String name, String value) { @@ -1344,12 +1228,12 @@ public PredicateEvent publishPredicate(Session session, String name, String valu event.botName = session.botInfo.name; event.name = name; event.value = value; - + if ("topic".equals(name) && value != null && !value.equals(session.currentTopic)) { invoke("publishTopic", new TopicChange(session.userName, session.botInfo.name, value, session.currentTopic)); session.currentTopic = value; } - + return event; } @@ -1406,19 +1290,19 @@ synchronized public void addCategoryToFile(Bot bot, Category c) { * wakes the global session up */ public void wake() { - sleep = false; + config.sleep = false; } /** * sleeps the global session */ public void sleep() { - sleep = true; + config.sleep = true; } @Override public void onUtterance(Utterance utterance) throws Exception { - + log.info("Utterance Received " + utterance); boolean talkToBots = false; @@ -1448,8 +1332,8 @@ public void onUtterance(Utterance utterance) throws Exception { // TODO: don't talk to bots.. it won't go well.. // TODO: the discord api can provide use the list of mentioned users. // for now.. we'll just see if we see Mr. Turing as a substring. - sleep = (sleep || utterance.text.contains("@")) && !utterance.text.contains(botName); - if (!sleep) { + config.sleep = (config.sleep || utterance.text.contains("@")) && !utterance.text.contains(botName); + if (!config.sleep) { shouldIRespond = true; } } @@ -1483,17 +1367,51 @@ public void onUtterance(Utterance utterance) throws Exception { } } } + + /** + * This receiver can take a config published by another service and sync + * predicates from it + * @param cfg + */ + public void onConfig(ServiceConfig cfg) { + Yaml yaml = new Yaml(); + String yml = yaml.dumpAsMap(cfg); + Map cfgMap = yaml.load(yml); + + for (Map.Entry entry : cfgMap.entrySet()) { + if (entry.getValue() == null) { + setPredicate("cfg_" + entry.getKey(), null); + } else { + setPredicate("cfg_" + entry.getKey(), entry.getValue().toString()); + } + } + + invoke("getPredicates"); + } @Override public Utterance publishUtterance(Utterance utterance) { return utterance; } - - + public TopicChange publishTopic(TopicChange topicChange) { return topicChange; } + + public String getTopic() { + return getPredicate(getCurrentUserName(), "topic"); + } - + public String getTopic(String username) { + return getPredicate(username, "topic"); + } + + public void setTopic(String username, String topic) { + setPredicate(username, "topic", topic); + } + + public void setTopic(String topic) { + setPredicate(getCurrentUserName(), "topic", topic); + } } diff --git a/src/main/java/org/myrobotlab/service/Py4j.java b/src/main/java/org/myrobotlab/service/Py4j.java index 44071f96ce..fd2755a733 100644 --- a/src/main/java/org/myrobotlab/service/Py4j.java +++ b/src/main/java/org/myrobotlab/service/Py4j.java @@ -1,148 +1,367 @@ package org.myrobotlab.service; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.bytedeco.javacpp.Loader; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Message; +import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.Service; -import org.myrobotlab.framework.interfaces.Invoker; +import org.myrobotlab.io.FileIO; +import org.myrobotlab.io.StreamGobbler; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.Py4jConfig; import org.myrobotlab.service.data.Script; +import org.myrobotlab.service.interfaces.Executor; import org.slf4j.Logger; + +import py4j.GatewayServer; import py4j.GatewayServerListener; import py4j.Py4JServerConnection; -import py4j.GatewayServer; -public class Py4j extends Service implements GatewayServerListener { +/** + * + * + * A bridge between a native proces of Python running and MRL. + * Should support any version of Python. + *
+ *  requirements: 
+ * 
+ *  1.  some version of python is installed and the python
+ *      executable is available in the PATH.  Py4j service will start the
+ *      default python and run a small script resource/Py4j/Py4j.py
+ *      
+ *  2.  pip install py4j.
+ *  
+ *  TODO:
+ *  1.  Support multiple instances of Py4j running - this requires more management 
+ *  of the service ports
+ *  2. Perhaps asynchronous calling of handler ?
+ *
+ * 
+ * + * @author GroG + */ +public class Py4j extends Service implements GatewayServerListener { - private static final long serialVersionUID = 1L; + /** + * POJO class to tie all the data elements of a external python process + * together. Including the process handler, the std out, std err streams and + * termination signal thread. + * + * @author GroG + * + */ + class Py4jClient { + // py4j connection + public transient Py4JServerConnection connection; + public transient StreamGobbler gobbler; + public transient Process process; + public transient Py4j py4j; + public transient Thread waitFor; + + /* TODO figure out a way to connect the process to the connection */ + public Py4jClient() { + } + + public Py4jClient(Py4j py4j, Process process) { + this.process = process; + this.py4j = py4j; + this.gobbler = new StreamGobbler(String.format("%s-gobbler", getName()), process.getInputStream()); + this.gobbler.start(); + this.waitFor = new WaitForProcess(py4j, process); + this.waitFor.start(); + + log.info("process started {}", process); + } + } + + /** + * A class to wait on a process signal and notify client is disconnected + * + * @author GroG + * + */ + public class WaitForProcess extends Thread { + public Integer exitCode; + public Process process; + public Py4j py4j; + + public WaitForProcess(Py4j py4j, Process process) { + super(String.format("%s-process-signal", py4j.getName())); + this.process = process; + this.py4j = py4j; + } + + @Override + public void run() { + try { + exitCode = process.waitFor(); + } catch (InterruptedException e) { + } + warn("process %s terminated with exit code %d", process.toString(), exitCode); + } + } public final static Logger log = LoggerFactory.getLogger(Py4j.class); + private static final long serialVersionUID = 1L; + + /** + * py4j clients currently attached to this service + */ + protected Map clients = new HashMap<>(); + + /** + * Java server side gateway for the python process to attach default port + * 25333 + */ private transient GatewayServer gateway = null; - private transient Invoker handler = null; - + /** + * the all important interface to the Python MessageHandler This defines what + * Java can call that will rpc'd into Python's MessageHandler + */ + private transient Executor handler = null; + + /** + * Opened scripts are scripts opened in memory, from there they can be + * executed or saved to the file system, or updatd in memory which the js + * client does + */ protected HashMap openedScripts = new HashMap(); - protected String activeScript = null; + /** + * client process and connectivity reference + */ + protected Py4jClient pythonProcess = null; + /** + * The base command to launch the Python interpreter without any arguments. + */ + protected transient String pythonCommand = "python"; public Py4j(String n, String id) { super(n, id); } /** - * start the gateway service listening on port + * Add a new script to Py4j default location will be in + * data/Py4j/{serviceName} + * + * @param scriptName + * - name of the script + * @param code + * - code block */ - public void start() { - if (gateway == null) { - gateway = new GatewayServer(this); - gateway.addListener(this); - gateway.start(); - info("server started listening on %s:%d", gateway.getAddress(), gateway.getListeningPort()); - handler = (Invoker) gateway.getPythonServerEntryPoint(new Class[] { Invoker.class }); - } else { - log.info("Py4j gateway server already started"); - } - } - - public void newScript() { - newScript("script.py"); - } + public void addScript(String scriptName, String code) { + Py4jConfig c = (Py4jConfig)config; + File script = new File(c.scriptRootDir + fs + scriptName); - public void newScript(String scriptName) { - if (!openedScripts.containsKey(scriptName)) { - openScript(scriptName, ""); + if (script.exists()) { + error("script %s already exists", scriptName); + return; } - } - public void openScript(String scriptName, String code) { - activeScript = scriptName; openedScripts.put(scriptName, new Script(scriptName, code)); broadcastState(); } + /** + * removes script from memory of openScripts + * + * @param scriptName The name of the script to close. + */ public void closeScript(String scriptName) { openedScripts.remove(scriptName); broadcastState(); } - @Override - public boolean preProcessHook(Message msg) { - // let the messages for this service - // get processed normally - if (methodSet.contains(msg.method)) { - return true; + public void connectionError(Exception e) { + error(e); + } + + @Override /* TODO add a one shot addTask to call handler.setName(name) */ + public void connectionStarted(Py4JServerConnection gatewayConnection) { + try { + log.info("connectionStarted {}", gatewayConnection.toString()); + clients.put(getClientKey(gatewayConnection), new Py4jClient()); + + info("connection started"); + invoke("getClients"); + } catch (Exception e) { + error(e); } + } - // will probably need to queu this - if (handler != null) { - // TODO - determine clients are connected .. how many clients etc.. - try { - handler.invoke(msg.method, msg.data); - } catch(Exception e) { - error(e); + @Override + public void connectionStopped(Py4JServerConnection gatewayConnection) { + info("connection stopped"); + clients.remove(getClientKey(gatewayConnection)); + invoke("getClients"); + } + + /** + * One of 3 methods supported on the MessageHandler() callbacks + * + * @param code The Python code to execute in the interpreter. + */ + public void exec(String code) { + try { + if (handler != null) { + handler.exec(code); + } else { + error("handler is null"); } + } catch (Exception e) { + error(e); } - return false; + } + + private String getClientKey(Py4JServerConnection gatewayConnection) { + return String.format("%s:%d", gatewayConnection.getSocket().getInetAddress(), gatewayConnection.getSocket().getPort()); } /** - * stop the gateway service + * return a set of client connections - probably could be deprecated to a + * single client, but was not sure + * + * @return */ - public void stop() { - if (gateway != null) { - gateway.shutdown(); - gateway = null; - } else { - log.info("Py4j gateway server already stopped"); + public Set getClients() { + return clients.keySet(); + } + + /** + * get listing of filesystem files location will be data/Py4j/{serviceName} + * + * @return + * @throws IOException + */ + public List getScriptList() throws IOException { + List sorted = new ArrayList<>(); + System.out.println(CodecUtils.toJson(config)); + Py4jConfig c = (Py4jConfig)config; + List files = FileIO.getFileList(c.scriptRootDir, true); + for (File file : files) { + if (file.toString().endsWith(".py")) { + sorted.add(file.toString().substring(c.scriptRootDir.length() + 1)); + } } + Collections.sort(sorted); + return sorted; } - // https://stackoverflow.com/questions/23157424/py4j-how-would-i-go-about-on-calling-a-python-method-in-java - public interface PythonInterface { - public Message onMsg(Message msg); + /** + * Sink for standard output from Py4j-related subprocesses. + * This method immediately publishes the output on {@link #publishStdOut(String)}. + * + * @param msg The output from a py4j related subprocess. + */ + public void handleStdOut(String msg) { + invoke("publishStdOut", msg); } - // TODO - now just need to set a reference of callbacks - public void PythonCall(PythonInterface callback, Message msg) { - callback.onMsg(msg); - // return numbers; + /** + * Potential entry point for python message + * + * @param code + */ + public void onPython(String code) { + log.info("onPython {}", code); + exec(code); } - public static void main(String[] args) { + /** + * Opens an example "service" script maintained in myrobotlab + * + * @param serviceType + * the type of service + * @throws IOException + */ + public void openExampleScript(String serviceType) throws IOException { + String filename = getResourceRoot() + fs + serviceType + fs + String.format("%s.py", serviceType); + String serviceScript = null; try { + serviceScript = FileIO.toString(filename); + } catch (Exception e) { + error("%s.py not found", serviceType); + log.error("getting service file script example threw", e); + } + addScript(serviceType + ".py", serviceScript); + } - LoggingFactory.init(Level.INFO); - - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); - Runtime.start("servo", "Servo"); - Py4j py4j = (Py4j) Runtime.start("py4j", "Py4j"); - py4j.start(); + /** + * Opens an existing script. All file operations will be relative to the + * data/Py4j/{serviceName} directory. + * + * @param scriptName + * - name of the script file relatie to scriptRootDir + * data/Py4j/{serviceName}/ + * @throws IOException + */ + public void openScript(String scriptName) throws IOException { + Py4jConfig c = (Py4jConfig)config; + File script = new File(c.scriptRootDir + fs + scriptName); - } catch (Exception e) { - log.error("main threw", e); + if (!script.exists()) { + error("file %s not found", script.getAbsolutePath()); + return; } + + openedScripts.put(scriptName, new Script(scriptName, FileIO.toString(script.getAbsoluteFile()))); + broadcastState(); } @Override - public void connectionError(Exception e) { - error(e); + public boolean preProcessHook(Message msg) { + // let the messages for this service + // get processed normally + if (methodSet.contains(msg.method)) { + return true; + } + + // TODO - determine clients are connected .. how many clients etc.. + try { + if (handler != null) { + handler.invoke(msg.method, msg.data); + } else { + error("preProcessHook handler is null"); + } + } catch (Exception e) { + error(e); + } + return false; } - @Override - public void connectionStarted(Py4JServerConnection gatewayConnection) { - info("connection started"); + public String publishStdOut(String data) { + return data; } - @Override - public void connectionStopped(Py4JServerConnection gatewayConnection) { - info("connection stopped"); + /** + * Saves a script to the file system default will be in + * data/Py4j/{serviceName}/{scriptName} + * + * @param scriptName + * @param code + * @throws IOException + */ + public void saveScript(String scriptName, String code) throws IOException { + Py4jConfig c = (Py4jConfig)config; + FileIO.toFile(c.scriptRootDir + fs + scriptName, code); + info("saved file %s", scriptName); } @Override @@ -168,16 +387,206 @@ public void serverStarted() { @Override public void serverStopped() { - info("%s stopped", getName()); + info("%s stopped", getName()); } - - public void handleStdOut(String msg) { - invoke("publishStdOut", msg); + + /** + * start the gateway service listening on port + */ + public void start() { + try { + if (gateway == null) { + gateway = new GatewayServer(this); + gateway.addListener(this); + gateway.start(); + info("server started listening on %s:%d", gateway.getAddress(), gateway.getListeningPort()); + handler = (Executor) gateway.getPythonServerEntryPoint(new Class[] { Executor.class }); + } else { + log.info("Py4j gateway server already started"); + } + } catch (Exception e) { + error(e); + } } - public String publishStdOut(String data) { - return data; + /** + * function which start the python process and begins the client + * MessageHandler and setup for runtime references to work + */ + public void startPythonProcess() { + try { + + // Specify the Python script path and arguments + String pythonScript = new File(getResourceDir() + fs + "Py4j.py").getAbsolutePath(); + + // Script requires full name as first command line argument + String[] pythonArgs = {getFullName()}; + + // Build the command to start the Python process + ProcessBuilder processBuilder; + if (((Py4jConfig) config).useBundledPython) { + String venv = getDataDir() + fs + "venv"; + pythonCommand = (Platform.getLocalInstance().isWindows()) ? venv + fs + "Scripts" + fs + "python.exe" : venv + fs + "bin" + fs + "python"; + if (!FileIO.checkDir(venv)) { + // We don't have an initialized virtual environment, so lets make one + // and install our required packages + String python = Loader.load(org.bytedeco.cpython.python.class); + String venvLib = new File(python).getParent() + fs + "lib" + fs + "venv" + fs + "scripts" + fs + "nt"; + if (Platform.getLocalInstance().isWindows()) { + // Super hacky workaround, venv works differently on Windows and requires these two + // files, but they are not distributed in bare-bones Python or in any pip packages. + // So we copy them where it expects, and it seems to work now + FileIO.copy(getResourceDir() + fs + "python.exe", venvLib + fs + "python.exe"); + FileIO.copy(getResourceDir() + fs + "pythonw.exe", venvLib + fs + "pythonw.exe"); + } + ProcessBuilder installProcess = new ProcessBuilder(python, "-m", "venv", venv); + int ret = installProcess.inheritIO().start().waitFor(); + if (ret != 0) { + error("Could not create virtual environment, subprocess returned {}. If on Windows, make sure there is a python.exe file in {}", ret, venvLib); + return; + } + + installProcess = new ProcessBuilder(pythonCommand, "-m", "pip", "install", "py4j"); + ret = installProcess.inheritIO().start().waitFor(); + if (ret != 0) { + error("Could not install package, subprocess returned " + ret); + return; + } + + } + + // Virtual environment should exist, so lets use that python + } else { + // Just use the system python + pythonCommand = "python"; + } + processBuilder = new ProcessBuilder(pythonCommand, pythonScript); + processBuilder.redirectErrorStream(true); + processBuilder.command().addAll(List.of(pythonArgs)); + + // Start the Python process + pythonProcess = new Py4jClient(this, processBuilder.start()); + + } catch (Exception e) { + error(e); + } + } + + /** + * Install a list of packages into the environment Py4j is running in. + * Py4j does not need to be running/connected to call this method as it + * spawns a new subprocess to invoke Pip. Output from pip is echoed + * via {@link #handleStdOut(String)}. + * + * @param packages The list of packages to install. Must be findable by Pip + * @throws IOException If an I/O error occurs running Pip. + */ + public void installPipPackages(List packages) throws IOException { + List commandArgs = new ArrayList<>(List.of("-m", "pip", "install")); + commandArgs.addAll(packages); + ProcessBuilder pipProcess = new ProcessBuilder(pythonCommand); + pipProcess.command().addAll(commandArgs); + Process proc = pipProcess.redirectErrorStream(true).start(); + new Thread(() -> { + BufferedReader stdOutput = new BufferedReader(new + InputStreamReader(proc.getInputStream())); + String s; + try { + while ((s = stdOutput.readLine()) != null) { + handleStdOut(s + '\n'); + } + } catch (IOException e) { + error(e); + } + }).start(); + int ret = 0; + try { + ret = proc.waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (ret != 0) { + error("Could not install packages, subprocess returned " + ret); + } } + @Override + public void startService() { + super.startService(); + Py4jConfig c = (Py4jConfig)config; + if (c.scriptRootDir == null) { + c.scriptRootDir = new File(getDataInstanceDir()).getAbsolutePath(); + } + File dataDir = new File(c.scriptRootDir); + dataDir.mkdirs(); + // start the py4j socket server + start(); + sleep(300); + // start the python process which starts the Py4j.py MessageHandler + startPythonProcess(); + } + /** + * stop the gateway service and teardown of the python process + */ + public void stop() { + if (gateway != null) { + log.info("stopping py4j gateway"); + gateway.shutdown(); + gateway = null; + } else { + log.info("Py4j gateway server already stopped"); + } + + handler = null; + + if (pythonProcess != null) { + log.info("shutting down python process"); + pythonProcess.process.destroy(); + } + } + + /** + * shutdown cleanly + */ + @Override + public void stopService() { + super.stopService(); + stop(); + } + + /** + * updates a script in memory + * + * @param scriptName + * @param code + * @return + */ + public void updateScript(String scriptName, String code) { + if (openedScripts.containsKey(scriptName)) { + Script script = openedScripts.get(scriptName); + script.code = code; + } else { + error("cannot find script %s to update", scriptName); + } + } + + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); + // Runtime.start("servo", "Servo"); + Py4j py4j = (Py4j) Runtime.start("py4j", "Py4j"); + + } catch (Exception e) { + log.error("main threw", e); + } + } + + } + diff --git a/src/main/java/org/myrobotlab/service/Python.java b/src/main/java/org/myrobotlab/service/Python.java index 5c6a2718d3..4153d0513f 100644 --- a/src/main/java/org/myrobotlab/service/Python.java +++ b/src/main/java/org/myrobotlab/service/Python.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,8 +50,8 @@ * @author GroG * */ -public class Python extends Service implements ServiceLifeCycleListener, MessageListener { - +public class Python extends Service implements ServiceLifeCycleListener, MessageListener { + /** * this thread handles all callbacks to Python process all input and sets msg * handles @@ -124,6 +125,8 @@ public void run() { } log.info("shutting down python queue"); } + + synchronized public void stop() { if (myThread != null) { @@ -174,7 +177,7 @@ public void run() { } } } - + public final static transient Logger log = LoggerFactory.getLogger(Python.class); // TODO this needs to be moved into an actual cache if it is to be used // Cache of compile python code @@ -269,10 +272,10 @@ public Object get(String pythonRefName) { */ Map exampleFiles = new TreeMap(); - transient LinkedBlockingQueue inputQueue = new LinkedBlockingQueue(); - final transient InputQueue inputQueueThread; + final transient LinkedBlockingQueue inputQueue = new LinkedBlockingQueue(); + final transient InputQueue inputQueueThread = new InputQueue(this); transient PythonInterpreter interp = null; - transient Map interpThreads = new HashMap(); + final transient Map interpThreads = new HashMap(); int interpreterThreadCount = 0; @@ -296,69 +299,37 @@ public Object get(String pythonRefName) { public Python(String n, String id) { super(n, id); - - log.info("created python {}", getName()); - - log.info("creating module directory pythonModules"); - new File("pythonModules").mkdir(); - - createPythonInterpreter(); - sleep(250); - - inputQueueThread = new InputQueue(this); - - // I love ServiceData ! - ServiceData sd = ServiceData.getLocalInstance(); - List sdt = sd.getAvailableServiceTypes(); - for (int i = 0; i < sdt.size(); ++i) { - MetaData st = sdt.get(i); - // FIXME - cache in "data" dir Or perhaps it should be pulled into - // resource directory during build time and packaged with jar - String file = String.format("%s/%s.py", st.getSimpleName(), st.getSimpleName()); - exampleFiles.put(st.getSimpleName(), file); - } - - localPythonFiles = getFileListing(); - - attachPythonConsole(); - - String selfReferenceScript = "from time import sleep\nfrom org.myrobotlab.framework import Platform\n" + "from org.myrobotlab.service import Runtime\n" - + "from org.myrobotlab.framework import Service\n" + "from org.myrobotlab.service import Python\n" - + String.format("%s = Runtime.getService(\"%s\")\n\n", CodecUtils.getSafeReferenceName(getName()), getName()) + "Runtime = Runtime.getInstance()\n\n" - + String.format("runtime = Runtime.getInstance()\n") + String.format("myService = Runtime.getService(\"%s\")\n", getName()); - // FIXME !!! myService is SO WRONG it will collide on more than 1 python - // service :( - PyObject compiled = getCompiledMethod("initializePython", selfReferenceScript, interp); - interp.exec(compiled); - - // initialize all the pre-existing service before python was created - Map services = Runtime.getLocalServices(); - for (ServiceInterface service : services.values()) { - if (service.isRunning()) { - onStarted(service.getName()); - } - } - - log.info("starting python {}", getName()); - inputQueueThread.start(); - log.info("started python {}", getName()); + // for scripts saved or opened by the user + new File(getDataDir()).mkdirs(); } - public void newScript() { - if (!openedScripts.containsKey("script.py")) { - openScript("script.py", ""); + /** + * Opens an existing script. All file operations will be relative to the + * data/Py4j/{serviceName} directory. + * + * @param scriptName + * - name of the script file relatie to scriptRootDir + * data/Py4j/{serviceName}/ + * @throws IOException + */ + public void openScript(String scriptName) throws IOException { + File script = new File(config.scriptRootDir + fs + scriptName); + + if (!script.exists()) { + error("file %s not found", script.getAbsolutePath()); + return; } - } - public void openScript(String scriptName, String code) { - activeScript = scriptName; - openedScripts.put(scriptName, new Script(scriptName, code)); + openedScripts.put(scriptName, new Script(scriptName, FileIO.toString(script.getAbsoluteFile()))); broadcastState(); } + - public void closeScript(String scriptName) { - openedScripts.remove(scriptName); - broadcastState(); + public void closeScript(String file) { + if (openedScripts.containsKey(file)) { + openedScripts.remove(file); + broadcastState(); + } } /** @@ -427,20 +398,23 @@ synchronized public void createPythonInterpreter() { Properties preprops = System.getProperties(); PythonInterpreter.initialize(preprops, props, new String[0]); - + interp = new PythonInterpreter(); + + addModulePath(getResourceDir() + fs + "modules"); + } public void addModulePath(String path) { - PythonConfig c = (PythonConfig) config; - if (c.modulePaths != null) { - c.modulePaths.add(path); - if (interp != null) { - PySystemState sys = Py.getSystemState(); - sys.path.append(new PyString(path)); - log.info("Python System Path: {}", sys.path); - } + if (config.modulePaths != null) { + config.modulePaths.add(path); } + + if (interp != null) { + PySystemState sys = Py.getSystemState(); + sys.path.append(new PyString(path)); + log.info("Python System Path: {}", sys.path); + } } public String eval(String method) { @@ -567,7 +541,7 @@ public boolean execFile(String filename) throws IOException { public boolean execFile(String filename, boolean block) throws IOException { String script = FileIO.toString(filename); if (openOnExecute) { - openScript(filename, script); + addScript(filename, script); } return exec(script); } @@ -647,6 +621,7 @@ public List getFileListing() { * the type of service */ public void loadServiceScript(String serviceType) { + try { String filename = getResourceRoot() + fs + serviceType + fs + String.format("%s.py", serviceType); String serviceScript = null; try { @@ -655,7 +630,10 @@ public void loadServiceScript(String serviceType) { error("%s.py not found", serviceType); log.error("getting service file script example threw {}", e); } - openScript(filename, serviceScript); + addScript(filename, serviceScript); + } catch(Exception e) { + error(e); + } } @Deprecated @@ -674,7 +652,7 @@ public void loadPyRobotLabServiceScript(String serviceType) { public void openScriptFromFile(String filename) throws IOException { log.info("loadScriptFromFile {}", filename); String data = FileIO.toString(filename); - openScript(filename, data); + addScript(filename, data); } @Override @@ -762,25 +740,36 @@ public void setLocalScriptDir(String path) { } /** - * Save a script + * Saves a script to the file system default will be in + * data/Py4j/{serviceName}/{scriptName} * * @param scriptName - * - path and name of script * @param code - * - content - * @return true if successful + * @throws IOException */ - public boolean saveScript(String scriptName, String code) { - try { - FileIO.toFile(scriptName, code.getBytes()); - info("saved script %s", scriptName); - return true; - } catch (Exception e) { - error("%s could not save script %s", getName(), scriptName); - } - return false; + public void saveScript(String scriptName, String code) throws IOException { + FileIO.toFile(config.scriptRootDir + fs + scriptName, code); + info("saved file %s", scriptName); } + + /** + * upserts a script in memory + * @param file + * @param code + * @return + */ + public void updateScript(String file, String code) { + if (openedScripts.containsKey(file)) { + Script script = openedScripts.get(file); + script.code = code; + } else { + openedScripts.put(file, new Script(file, code)); + broadcastState(); + } + } + + // @Override /* FIXME - make interface for it */ public void defaultInvokeMethod(String method, Object... params) { if (interp == null) { @@ -797,17 +786,23 @@ public void defaultInvokeMethod(String method, Object... params) { @Override synchronized public void startService() { super.startService(); + + if (config.scriptRootDir == null) { + config.scriptRootDir = new File(getDataInstanceDir()).getAbsolutePath(); + } + File dataDir = new File(config.scriptRootDir); + dataDir.mkdirs(); + Map services = Runtime.getLocalServices(); for (ServiceInterface s : services.values()) { onStarted(s.getName()); } // register runtime life cycle events for other services Runtime.getInstance().attachServiceLifeCycleListener(getName()); - - PythonConfig c = (PythonConfig) config; + // run start scripts if there are any - if (c.startScripts != null) { - for (String script : c.startScripts) { + if (config.startScripts != null) { + for (String script : config.startScripts) { // i think in this context its safer to block try { execFile(script, true); @@ -856,9 +851,7 @@ public boolean stop() { @Override public void stopService() { // run any stop scripts - PythonConfig c = (PythonConfig) config; - - for (String script : c.stopScripts) { + for (String script : config.stopScripts) { // i think in this context its safer to block try { execFile(script, true); @@ -871,58 +864,96 @@ public void stopService() { // release the interpeter stop(); } + + /** + * Initialize the Jython interpreter including all the jython/python which needs to + * run in order to interface correctly with mrl. + */ + public void init() { + + log.info("created python {}", getName()); + createPythonInterpreter(); + sleep(250); + + // I love ServiceData ! + ServiceData sd = ServiceData.getLocalInstance(); + List sdt = sd.getAvailableServiceTypes(); + for (int i = 0; i < sdt.size(); ++i) { + MetaData st = sdt.get(i); + // FIXME - cache in "data" dir Or perhaps it should be pulled into + // resource directory during build time and packaged with jar + String file = String.format("%s/%s.py", st.getSimpleName(), st.getSimpleName()); + exampleFiles.put(st.getSimpleName(), file); + } - public boolean isOpenOnExecute() { - return openOnExecute; - } + localPythonFiles = getFileListing(); - public void setOpenOnExecute(boolean openOnExecute) { - this.openOnExecute = openOnExecute; - } + attachPythonConsole(); - public static void main(String[] args) { - try { - LoggingFactory.init("INFO"); + String selfReferenceScript = "from time import sleep\nfrom org.myrobotlab.framework import Platform\n" + "from org.myrobotlab.service import Runtime\n" + + "from org.myrobotlab.framework import Service\n" + "from org.myrobotlab.service import Python\n" + + String.format("%s = Runtime.getService(\"%s\")\n\n", CodecUtils.getSafeReferenceName(getName()), getName()) + "Runtime = Runtime.getInstance()\n\n" + + String.format("runtime = Runtime.getInstance()\n") + String.format("myService = Runtime.getService(\"%s\")\n", getName()); + // FIXME !!! myService is SO WRONG it will collide on more than 1 python + // service :( + PyObject compiled = getCompiledMethod("initializePython", selfReferenceScript, interp); + interp.exec(compiled); - // Runtime.start("i01.head.rothead", "Servo"); - // Runtime.start("i01.head.neck", "Servo"); - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); - Python python = (Python) Runtime.start("python", "Python"); - // python.execFile("data/adafruit.py"); + // initialize all the pre-existing service before python was created + Map services = Runtime.getLocalServices(); + for (ServiceInterface service : services.values()) { + if (service.isRunning()) { + onStarted(service.getName()); + } + } - // Runtime.start("i01", "InMoov2"); + log.info("starting python {}", getName()); + inputQueueThread.start(); + log.info("started python {}", getName()); + } - } catch (Exception e) { - log.error("main threw", e); - } + public boolean isOpenOnExecute() { + return openOnExecute; + } + public void setOpenOnExecute(boolean openOnExecute) { + this.openOnExecute = openOnExecute; } @Override public void onCreated(String name) { - + log.info("onCreated {}", name); + } + + public void onPython(String code) { + log.info("onPython {}", code); + exec(code); } + @Override public void onRegistered(Registration registration) { - + log.info("onCreated {}", registration); } @Override public void onStopped(String fullname) { - + log.info("onCreated {}", fullname); } - @Override - public ServiceConfig apply(ServiceConfig c) { - PythonConfig config = (PythonConfig) super.apply(c); - if (config.startScripts != null && config.startScripts.size() > 0) { + public PythonConfig apply(PythonConfig c) { + super.apply(c); + + // apply is the first method called after construction, + // since we offer the capability of executing scripts specified in config + // the interpreter must be configured and created here + init(); + + if (c.startScripts != null && c.startScripts.size() > 0) { if (isRunning()) { - for (String script : config.startScripts) { + for (String script : c.startScripts) { try { execFile(script); } catch (Exception e) { @@ -934,8 +965,8 @@ public ServiceConfig apply(ServiceConfig c) { PySystemState sys = Py.getSystemState(); - if (config.modulePaths != null) { - for (String path : config.modulePaths) { + if (c.modulePaths != null) { + for (String path : c.modulePaths) { sys.path.append(new PyString(path)); } } @@ -944,6 +975,60 @@ public ServiceConfig apply(ServiceConfig c) { return c; } + + /** + * get listing of filesystem files location will be data/Py4j/{serviceName} + * + * @return + * @throws IOException + */ + public List getScriptList() throws IOException { + List sorted = new ArrayList<>(); + List files = FileIO.getFileList(config.scriptRootDir, true); + for (File file : files) { + if (file.toString().endsWith(".py")) { + sorted.add(file.toString().substring(config.scriptRootDir.length() + 1)); + } + } + Collections.sort(sorted); + return sorted; + } + + + /** + * Add a new script to Py4j default location will be in + * data/Py4j/{serviceName} + * + * @param scriptName + * - name of the script + * @param code + * - code block + * @throws IOException + */ + public void addScript(String scriptName, String code) throws IOException { + openedScripts.put(scriptName, new Script(scriptName, code)); + broadcastState(); + } + + /** + * Opens an example "service" script maintained in myrobotlab + * + * @param serviceType + * the type of service + * @throws IOException + */ + public void openExampleScript(String serviceType) throws IOException { + String filename = getResourceRoot() + fs + serviceType + fs + String.format("%s.py", serviceType); + String serviceScript = null; + try { + serviceScript = FileIO.toString(filename); + } catch (Exception e) { + error("%s.py not found", serviceType); + log.error("getting service file script example threw {}", e); + } + addScript(serviceType + ".py", serviceScript); + } + @Override public void onMessage(Message msg) { @@ -951,4 +1036,31 @@ public void onMessage(Message msg) { } + public static void main(String[] args) { + try { + LoggingFactory.init("INFO"); + + // Runtime.start("i01.head.rothead", "Servo"); + // Runtime.start("i01.head.neck", "Servo"); + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); + Python python = (Python) Runtime.start("python", "Python"); + + // Py4j py4j = (Py4j) Runtime.start("py4j", "Py4j"); + // python.execFile("data/adafruit.py"); + + // Runtime.start("i01", "InMoov2"); + + } catch (Exception e) { + log.error("main threw", e); + } + + } + + @Override + public PythonConfig getConfig() { + return config; + } + } diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java index faed6d4364..93b8178fd2 100644 --- a/src/main/java/org/myrobotlab/service/Random.java +++ b/src/main/java/org/myrobotlab/service/Random.java @@ -18,7 +18,6 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.RandomConfig; import org.myrobotlab.service.config.RandomConfig.RandomMessageConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; /** @@ -27,7 +26,7 @@ * @author GroG * */ -public class Random extends Service { +public class Random extends Service { private static final long serialVersionUID = 1L; @@ -236,9 +235,8 @@ public void process(String key) { } @Override - public ServiceConfig getConfig() { - - RandomConfig config = (RandomConfig)super.getConfig(); + public RandomConfig getConfig() { + super.getConfig(); config.enabled = enabled; @@ -256,13 +254,13 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - RandomConfig config = (RandomConfig) c; - enabled = config.enabled; + public RandomConfig apply(RandomConfig c) { + super.apply(c); + enabled = c.enabled; try { - for (String key : config.randomMessages.keySet()) { - RandomMessageConfig msgc = config.randomMessages.get(key); + for (String key : c.randomMessages.keySet()) { + RandomMessageConfig msgc = c.randomMessages.get(key); addRandom(msgc.minIntervalMs, msgc.maxIntervalMs, key.substring(0, key.lastIndexOf(".")), key.substring(key.lastIndexOf(".") + 1), msgc.data); if (!msgc.enabled) { disable(key); diff --git a/src/main/java/org/myrobotlab/service/RasPi.java b/src/main/java/org/myrobotlab/service/RasPi.java index a6f8fd5d6e..ea6e33444f 100644 --- a/src/main/java/org/myrobotlab/service/RasPi.java +++ b/src/main/java/org/myrobotlab/service/RasPi.java @@ -8,18 +8,18 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import org.myrobotlab.arduino.BoardInfo; import org.myrobotlab.arduino.BoardType; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.i2c.I2CFactory; import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.abstracts.AbstractMicrocontroller; import org.myrobotlab.service.config.RasPiConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.I2CControl; import org.myrobotlab.service.interfaces.I2CController; @@ -31,7 +31,7 @@ import com.pi4j.io.gpio.GpioPinDigitalMultipurpose; import com.pi4j.io.gpio.Pin; import com.pi4j.io.gpio.PinMode; -import com.pi4j.io.gpio.PinPullResistance; +import com.pi4j.io.gpio.PinState; import com.pi4j.io.gpio.RaspiPin; import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent; import com.pi4j.io.gpio.event.GpioPinListenerDigital; @@ -49,25 +49,20 @@ * More Info : http://pi4j.com/ * */ -public class RasPi extends AbstractMicrocontroller implements I2CController, GpioPinListenerDigital { - - @Override - public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) { - // display pin state on console - log.info(" --> GPIO PIN STATE CHANGE: {} = {}", event.getPin(), event.getState()); - } +public class RasPi extends AbstractMicrocontroller implements I2CController, GpioPinListenerDigital { public static class I2CDeviceMap { transient public I2CBus bus; transient public I2CDevice device; public int deviceHandle; public String serviceName; + + public String toString() { + return String.format("bus: %d deviceHandle: %d service: %s", bus.getBusNumber(), deviceHandle, serviceName); + } } - /** - * default bus current bus of raspi service - */ - String bus = "1"; + public final static Map bcmToWiring = new HashMap<>(); public static final int INPUT = 0x0; @@ -77,54 +72,94 @@ public static class I2CDeviceMap { private static final long serialVersionUID = 1L; - transient GpioController gpio; + public final static Map wiringToBcm = new HashMap<>(); + + static { + + bcmToWiring.put("GPIO 0", "GPIO 27"); + bcmToWiring.put("GPIO 1", "GPIO 31"); + bcmToWiring.put("GPIO 2", "GPIO 8"); + bcmToWiring.put("GPIO 3", "GPIO 9"); + bcmToWiring.put("GPIO 4", "GPIO 7"); + bcmToWiring.put("GPIO 5", "GPIO 21"); + bcmToWiring.put("GPIO 6", "GPIO 22"); + bcmToWiring.put("GPIO 7", "GPIO 11"); + bcmToWiring.put("GPIO 8", "GPIO 10"); + bcmToWiring.put("GPIO 9", "GPIO 13"); + bcmToWiring.put("GPIO 10", "GPIO 12"); + bcmToWiring.put("GPIO 11", "GPIO 14"); + bcmToWiring.put("GPIO 12", "GPIO 27"); + bcmToWiring.put("GPIO 13", "GPIO 26"); + bcmToWiring.put("GPIO 14", "GPIO 15"); + bcmToWiring.put("GPIO 15", "GPIO 16"); + bcmToWiring.put("GPIO 16", "GPIO 25"); + bcmToWiring.put("GPIO 17", "GPIO 0"); + bcmToWiring.put("GPIO 18", "GPIO 1"); + bcmToWiring.put("GPIO 19", "GPIO 23"); + bcmToWiring.put("GPIO 20", "GPIO 28"); + bcmToWiring.put("GPIO 21", "GPIO 29"); + bcmToWiring.put("GPIO 22", "GPIO 3"); + bcmToWiring.put("GPIO 23", "GPIO 4"); + bcmToWiring.put("GPIO 24", "GPIO 5"); + bcmToWiring.put("GPIO 25", "GPIO 6"); + bcmToWiring.put("GPIO 26", "GPIO 24"); + bcmToWiring.put("GPIO 27", "GPIO 2"); + + for (String pin : bcmToWiring.keySet()) { + String wiring = bcmToWiring.get(pin); + wiringToBcm.put(wiring, pin); + } + } + + public static void main(String[] args) { + LoggingFactory.init("info"); + + /* + * RasPi.displayString(1, 70, "1"); + * + * RasPi.displayString(1, 70, "abcd"); + * + * RasPi.displayString(1, 70, "1234"); + * + * + * //RasPi raspi = new RasPi("raspi"); + */ + + // raspi.writeDisplay(busAddress, deviceAddress, data) - protected Map> validAddresses = new HashMap<>(); + int i = 0; + + Runtime.start("servo01", "Servo"); + Runtime.start("ada16", "Adafruit16CServoDriver"); + Runtime.start(String.format("rasPi%d", i), "RasPi"); + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); + + } + + protected String boardType = null; /** - * for attached devices + * default bus current bus of raspi service */ - HashMap i2cDevices = new HashMap(); + protected String bus = "1"; + + protected transient GpioController gpio; /** - * "quick fix" - no subscriptions nor listeners are made with other services, - * so I created this to show the references to i2c device services + * for attached devices */ - protected Set attachedServices = new HashSet<>(); + protected Map i2cDevices = new HashMap(); - protected String boardType = null; + protected Map> validI2CAddresses = new HashMap<>(); + + protected String wrongPlatformError = null; public RasPi(String n, String id) { super(n, id); - - Platform platform = Platform.getLocalInstance(); - log.info("platform is {}", platform); - log.info("architecture is {}", platform.getArch()); - - try { - boardType = SystemInfo.getBoardType().toString(); - gpio = GpioFactory.getInstance(); - log.info("Executing on Raspberry PI"); - getPinList(); - } catch (Exception e) { - // an error in the constructor won't get broadcast - so we need Runtime to - // do it - Runtime.getInstance().error("raspi service requires arm %s is not arm - %s", getName(), e.getMessage()); - } } - /* - * @Override public void attach(String name) { ServiceInterface si = - * Runtime.getService(name); if - * (I2CControl.class.isAssignableFrom(si.getClass())) { - * attachI2CControl((I2CControl) si); return; } } - * - * @Override public void detach(String name) { ServiceInterface si = - * Runtime.getService(name); if - * (I2CControl.class.isAssignableFrom(si.getClass())) { - * detachI2CControl((I2CControl) si); return; } } - */ - @Override public void attach(Attachable service) throws Exception { if (I2CControl.class.isAssignableFrom(service.getClass())) { @@ -133,18 +168,9 @@ public void attach(Attachable service) throws Exception { } } - @Override - public void detach(Attachable service) { - if (I2CControl.class.isAssignableFrom(service.getClass())) { - detachI2CControl((I2CControl) service); - return; - } - } - @Override public void attachI2CControl(I2CControl control) { - attachedServices.add(control.getName()); // This part adds the service to the mapping between // busAddress||DeviceAddress // and the service name to be able to send data back to the invoker @@ -179,6 +205,14 @@ void createI2cDevice(int bus, int address, String serviceName) { } } + @Override + public void detach(Attachable service) { + if (I2CControl.class.isAssignableFrom(service.getClass())) { + detachI2CControl((I2CControl) service); + return; + } + } + @Override public void detachI2CControl(I2CControl control) { // This method should delete the i2c device entry from the list of @@ -191,85 +225,205 @@ public void detachI2CControl(I2CControl control) { i2cDevices.remove(key); control.detachI2CController(this); } - attachedServices.remove(control.getName()); } - public void digitalWrite(int pin, int value) { - log.info("digitalWrite {} {}", pin, value); - // msg.digitalWrite(pin, value); - PinDefinition pinDef = addressIndex.get(pin); - GpioPinDigitalMultipurpose gpio = ((GpioPinDigitalMultipurpose) pinDef.getPinImpl()); - if (value == 0) { - gpio.low(); - } else { - gpio.high(); - } - invoke("publishPinDefinition", pinDef); + @Override + @Deprecated /* use disablePin(String) */ + public void disablePin(int address) { + error("disablePin(int) not supported use disablePin(String)"); } @Override - public void disablePin(int address) { - PinDefinition pin = addressIndex.get(address); - pin.setEnabled(false); - ((GpioPinDigitalMultipurpose) pin.getPinImpl()).removeListener(); - PinDefinition pinDef = addressIndex.get(address); + public void disablePin(String pin) { + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } + PinDefinition pinDef = pinIndex.get(pin); + pinDef.setEnabled(false); + getGPIO(pin).removeListener(this); invoke("publishPinDefinition", pinDef); } @Override + @Deprecated /* use enablePin(String pin) */ public void enablePin(int address) { - enablePin(address, 0); + error("enablePin(int address) not supoprted use enablePin(String pin)"); } @Override + @Deprecated /* use enablePin(String, int) */ public void enablePin(int address, int rate) { - PinDefinition pinDef = addressIndex.get(address); - GpioPinDigitalMultipurpose gpio = ((GpioPinDigitalMultipurpose) pinDef.getPinImpl()); - gpio.addListener(this); + error("use enablePin(String, int)"); + } + + @Override + public void enablePin(String pin) { + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } + RasPiConfig c = (RasPiConfig) config; + enablePin(pin, c.pollRateHz); + } + + @Override + public void enablePin(String pin, int rate) { + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } + + PinDefinition pinDef = pinIndex.get(pin); + pinMode(pin, "INPUT"); + getGPIO(pin).addListener(this); pinDef.setEnabled(true); invoke("publishPinDefinition", pinDef); // broadcast pin change } + // - add more pin mappings if desired ... + @Override + public Integer getAddress(String pin) { + return Integer.parseInt(pin); + } + @Override /* services attached - not i2c devices */ public Set getAttached() { - // return i2cDevices.keySet(); - return attachedServices; + Set ret = new TreeSet<>(); + for (I2CDeviceMap i2c : i2cDevices.values()) { + ret.add(CodecUtils.getFullName(i2c.serviceName)); + } + return ret; + } + + @Override + public BoardInfo getBoardInfo() { + + BoardInfo boardInfo = new BoardInfo(); + + try { + + // Get the board revision + String revision = SystemInfo.getRevision(); + log.info("Board Revision: " + revision); + + // Get the board type + boardInfo.boardTypeName = SystemInfo.getModelName(); + log.info("Board Model: " + boardInfo.boardTypeName); + + // Get the board's memory information + log.info("Memory Info: " + SystemInfo.getMemoryTotal()); + + // Get the board's operating system info + log.info("OS Name: " + SystemInfo.getOsName()); + + } catch (Exception e) { + error(e); + } + + return boardInfo; + } + + @Override + public List getBoardTypes() { + // TODO Auto-generated method stub + // FIXME - this need work + return null; + } + + /** + * Gets the multipurpose implementation of a pin, if it doesn't currently + * exists, it will provision it. + * + * @param pin + * @return + */ + private GpioPinDigitalMultipurpose getGPIO(String pin) { + log.debug("getGPIO {}", pin); + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return null; + } + + PinDefinition pindef = getPin(pin); + if (pindef == null) { + error("No pin definition exists for %s", pin); + return null; + } + + GpioPinDigitalMultipurpose gpioPin = (GpioPinDigitalMultipurpose) pindef.getPinImpl(); + if (gpioPin == null) { + log.info("provisioning gpio {}", pin); + gpioPin = gpio.provisionDigitalMultipurposePin(RaspiPin.getPinByName(bcmToWiring.get(pin)), PinMode.DIGITAL_OUTPUT); + pindef.setPinImpl(gpioPin); + } + + return gpioPin; } @Override public List getPinList() { + List pinList = new ArrayList<>(); - for (Pin pin : RaspiPin.allPins()) { - - // pin.getSupportedPinModes() - PinDefinition pindef = new PinDefinition(getName(), pin.getAddress()); - pindef.setPinName(pin.getName()); - EnumSet modes = pin.getSupportedPinModes(); - // FIXME - the raspi definitions are "better" they have input & ouput - // FIXME - reconcile rxtx - // FIXME - get pull up resistance - if (modes.contains(PinMode.DIGITAL_OUTPUT)) { - pindef.setDigital(true); - } - if (modes.contains(PinMode.ANALOG_OUTPUT)) { - pindef.setAnalog(true); - } - if (modes.contains(PinMode.PWM_OUTPUT)) { - pindef.setAnalog(true); + if (!pinIndex.isEmpty()) { + pinList.addAll(pinIndex.values()); + return pinList; + } + + for (Pin wiringPin : RaspiPin.allPins()) { + + // RaspiPin.allPins() RETURNS WIRING NUMBERS !!!! + + // if (wiringPin.getName().equals("GPIO 2") || + // wiringPin.getName().equals("GPIO 3") || + // wiringPin.getName().equals("GPIO 8") || + // wiringPin.getName().equals("GPIO 9")) { + // log.info("filtering out pin {} from gpio provisioning", wiringPin); + // continue; + // } + + String wPinName = wiringPin.getName(); + + if (!wiringToBcm.containsKey(wPinName)) { + log.info("skipping wiring pin {} - no gpio definition", wPinName); + continue; } - addressIndex.put(pin.getAddress(), pindef); - pinIndex.put(pin.getName(), pindef); + String bcmPinName = wiringToBcm.get(wPinName); - // GpioPinDigitalInput provisionedPin = gpio.provisionDigitalInputPin(pin, - // pull); - // provisionedPin.setShutdownOptions(true); // unexport pin on program - // shutdown - // provisionedPins.add(provisionedPin); // add provisioned pin to - // collection + PinDefinition pindef = new PinDefinition(); + // set to output for starting + pindef.setMode("OUTPUT"); + pindef.setPinName(bcmPinName); + EnumSet modes = wiringPin.getSupportedPinModes(); + + pindef.setDigital(modes.contains(PinMode.DIGITAL_OUTPUT)); + pindef.setAnalog(modes.contains(PinMode.ANALOG_OUTPUT)); + pindef.setPwm(modes.contains(PinMode.PWM_OUTPUT)); + + // FIXME - remove this, do not support address only pin + String lastPart = bcmPinName.trim().split(" ")[1]; + pindef.setAddress(Integer.parseInt(lastPart)); + + pinIndex.put(bcmPinName, pindef); + pinList.add(pindef); + } + + return pinList; + } + + @Override + public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) { + // display pin state on console + log.info(" --> GPIO PIN STATE CHANGE: {} = {}", event.getPin(), event.getState()); + PinDefinition pindef = pinIndex.get(wiringToBcm.get(event.getPin().getName())); + if (pindef == null) { + log.error("pindef is null for pin {}", event.getPin().getName()); + } else { + pindef.setValue(event.getState().getValue()); + invoke("publishPinDefinition", pindef); } - return new ArrayList(addressIndex.values()); } @Override // FIXME - I2CControl has bus why is it supplied here as a @@ -332,7 +486,7 @@ public int i2cWriteRead(I2CControl control, int busAddress, int deviceAddress, b try { devicedata.device.read(writeBuffer, 0, writeBuffer.length, readBuffer, 0, readBuffer.length); } catch (IOException e) { - Logging.logError(e); + error(e); } return readBuffer.length; } @@ -346,25 +500,35 @@ public int i2cWriteRead(I2CControl control, int busAddress, int deviceAddress, b * @param mode * INPUT = 0x0. Output = 0x1. */ - public void pinMode(int pin, int mode) { + public void pinMode(String pin, String mode) { + log.info("pinMode {}, mode {}", pin, mode); - PinDefinition pinDef = addressIndex.get(pin); - if (mode == INPUT) { - pinDef.setPinImpl(gpio.provisionDigitalMultipurposePin(RaspiPin.getPinByAddress(pin), PinMode.DIGITAL_INPUT)); - } else { - pinDef.setPinImpl(gpio.provisionDigitalMultipurposePin(RaspiPin.getPinByAddress(pin), PinMode.DIGITAL_OUTPUT)); + if (mode == null) { + error("Pin mode cannot be null"); + return; } - invoke("publishPinDefinition", pinDef); - } - @Override - public void pinMode(int address, String mode) { + mode = mode.trim().toUpperCase(); + + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } - if (mode != null && mode.equalsIgnoreCase("INPUT")) { - pinMode(address, INPUT); + PinDefinition pinDef = pinIndex.get(pin); + // this will provision the pin if it is not already provisioned + GpioPinDigitalMultipurpose gpio = getGPIO(pin); + if (mode.equals("INPUT")) { + pinDef.setMode("INPUT"); + gpio.setMode(PinMode.DIGITAL_INPUT); + } else if (mode.equals("OUTPUT")) { + pinDef.setMode("OUTPUT"); + gpio.setMode(PinMode.DIGITAL_OUTPUT); } else { - pinMode(address, OUTPUT); + error("mode %s is not valid", mode); } + log.info("pinDef {}", pinDef); + invoke("publishPinDefinition", pinDef); } @Override @@ -377,6 +541,110 @@ public PinData publishPin(PinData pinData) { return pinData; } + public Map> publishScan() { + return validI2CAddresses; + } + + public void read() { + log.debug("read task invoked"); + List pinArray = new ArrayList<>(); + // load pin array + for (String pin : pinIndex.keySet()) { + PinDefinition pindef = pinIndex.get(pin); + if (pindef.isEnabled()) { + log.debug("pin {} enabled {}", pin, pindef.isEnabled()); + int value = read(pin); + pindef.setValue(value); + PinData pd = new PinData(pin, value); + log.debug("pin data {}", pd); + pinArray.add(pd); + } + } + + if (pinArray.size() > 0) { + PinData[] array = pinArray.toArray(new PinData[0]); + invoke("publishPinArray", new Object[] { array }); + } + } + + @Override + public int read(String pin) { + + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return -1; + } + PinDefinition pindef = pinIndex.get(pin); + GpioPinDigitalMultipurpose gpioPin = getGPIO(pin); + if (!gpioPin.isMode(PinMode.DIGITAL_INPUT)) { + pinMode(pin, "INPUT"); + } + if (gpioPin.isLow()) { + pindef.setValue(0); + return 0; + } else { + pindef.setValue(1); + return 1; + } + } + + @Override + public void reset() { + // TODO Auto-generated method stub + // reset pins/i2c devices/gpio pins + } + + public void scan() { + scan(null); + } + + public void scan(Integer busNumber) { + + if (busNumber == null) { + busNumber = Integer.parseInt(bus); + } + + try { + + I2CBus bus = I2CFactory.getInstance(busNumber); + + if (!validI2CAddresses.containsKey(busNumber)) { + validI2CAddresses.put(busNumber, new HashSet<>()); + } + + Set addresses = validI2CAddresses.get(busNumber); + + for (int i = 1; i < 128; i++) { + String hex = Integer.toHexString(i); + try { + I2CDevice device = bus.getDevice(i); + device.read(); + if (!addresses.contains(hex)) { + addresses.add(hex); + info("found new i2c device %s", hex); + } + } catch (Exception ignore) { + if (addresses.contains(hex)) { + info("removing i2c device %s", hex); + addresses.remove(hex); + } + } + } + + log.debug("scanning bus {} found: ---", busNumber); + for (String a : addresses) { + log.debug("address: " + a); + } + log.debug("----------"); + + } catch (Exception e) { + error("cannot access i2c bus %d", busNumber); + log.error("scan threw", e); + } + + invoke("publishScan"); + } + // FIXME - return array /** * Starts a scan of the I2C specified and returns a list of addresses that @@ -418,7 +686,7 @@ public Integer[] scanI2CDevices(int busAddress) { } } } catch (Exception e) { - Logging.logError(e); + error(e); } Integer[] ret = list.toArray(new Integer[list.size()]); @@ -429,18 +697,72 @@ public Integer[] scanI2CDevices(int busAddress) { public void startService() { super.startService(); try { + + Platform platform = Platform.getLocalInstance(); + log.info("platform is {}", platform); + log.info("architecture is {}", platform.getArch()); + + boardType = SystemInfo.getBoardType().toString(); + gpio = GpioFactory.getInstance(); + log.info("Executing on Raspberry PI"); + getPinList(); + log.info("Initiating i2c"); I2CFactory.getInstance(Integer.parseInt(bus)); log.info("i2c initiated on bus {}", bus); - // scan(); takes too long + addTask(1000, "scan"); + + log.info("read task initialized"); + addTask(1000, "read"); + } catch (IOException e) { log.error("i2c initiation failed", e); + } catch (Exception e) { + // an error in the constructor won't get broadcast - so we need Runtime to + // do it + Runtime.getInstance().error("raspi service requires arm %s is not arm - %s", getName(), e.getMessage()); + log.error("RasPi init failed", e); + wrongPlatformError = "The RasPi service requires raspberry pi hardware"; + broadcastState(); } + } - public void testGPIOOutput() { - GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin(RaspiPin.GPIO_02, PinMode.DIGITAL_INPUT, PinPullResistance.PULL_DOWN); - log.info("Pin: {}", pin); + // FIXME - remove + public void test() { + // Create GPIO controller instance + GpioController gpio = GpioFactory.getInstance(); + + // Provision GPIO pin 0 as a digital input/output pin + // GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin( + // RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT, PullUpResistance); + GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin(RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT); + + // Set the pin mode to output + pin.setMode(PinMode.DIGITAL_OUTPUT); + + // Write a value of 1 (HIGH) to GPIO pin 0 + pin.high(); + + // Delay for 2 seconds + sleep(2000); + + // Write a value of 0 (LOW) to GPIO pin 0 + pin.low(); + + // Delay for 2 seconds + sleep(2000); + + // Set the pin mode to input + pin.setMode(PinMode.DIGITAL_INPUT); + + // Add a listener to monitor pin state changes + pin.addListener((GpioPinListenerDigital) (GpioPinDigitalStateChangeEvent event) -> { + log.info("Pin state changed to: " + event.getState()); + }); + + // Shutdown GPIO controller and release resources + // gpio.shutdown(); } public void testPWM() { @@ -467,116 +789,36 @@ public void testPWM() { } } } catch (Exception e) { - + error(e); } } @Override - public void write(int address, int value) { - - PinDefinition pinDef = addressIndex.get(address); - pinMode(address, Arduino.OUTPUT); - digitalWrite(address, value); - // cache value - pinDef.setValue(value); - } - - public static void main(String[] args) { - LoggingFactory.init("info"); - - /* - * RasPi.displayString(1, 70, "1"); - * - * RasPi.displayString(1, 70, "abcd"); - * - * RasPi.displayString(1, 70, "1234"); - * - * - * //RasPi raspi = new RasPi("raspi"); - */ - - // raspi.writeDisplay(busAddress, deviceAddress, data) - - int i = 0; - - Runtime.start("servo01", "Servo"); - Runtime.start("ada16", "Adafruit16CServoDriver"); - Runtime.start(String.format("rasPi%d", i), "RasPi"); - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); + public void write(String pin, int value) { + log.info("write {} {}", pin, value); + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } - } + PinDefinition pinDef = pinIndex.get(pin); + pinMode(pin, "OUTPUT"); - @Override - public void reset() { - // TODO Auto-generated method stub - // reset pins/i2c devices/gpio pins - } + GpioPinDigitalMultipurpose gpio = getGPIO(pin); + gpio.setState(value == 0 ? PinState.LOW : PinState.HIGH); + pinDef.setValue(value); - @Override - public BoardInfo getBoardInfo() { - RaspiPin.allPins(); - // FIXME - this needs more work .. BoardInfo needs to be an interface where - // RasPiInfo is derived - return null; + invoke("publishPinDefinition", pinDef); } @Override - public List getBoardTypes() { - // TODO Auto-generated method stub - // FIXME - this need work - return null; + public void pinMode(int address, String mode) { + pinMode(String.format("%d", address), mode); } - // - add more pin mappings if desired ... @Override - public Integer getAddress(String pin) { - return Integer.parseInt(pin); - } - - public void scan() { - scan(null); - } - - public void scan(Integer busNumber) { - - if (busNumber == null) { - busNumber = Integer.parseInt(bus); - } - - try { - - I2CBus bus = I2CFactory.getInstance(busNumber); - - validAddresses = new HashMap<>(); - - if (!validAddresses.containsKey(busNumber)) { - validAddresses.put(busNumber, new HashSet<>()); - } - - Set addresses = validAddresses.get(busNumber); - - for (int i = 1; i < 128; i++) { - try { - I2CDevice device = bus.getDevice(i); - device.write((byte) 0); - addresses.add(Integer.toHexString(i)); - } catch (Exception ignore) { - } - } - - log.info("scanning bus {} found: ---", busNumber); - for (String a : addresses) { - log.info("address: " + a); - } - log.info("----------"); - } catch (Exception e) { - error("cannot access i2c bus %d", busNumber); - log.error("scan threw", e); - } - - broadcastState(); + public void write(int address, int value) { + write(String.format("%d", address), value); } } diff --git a/src/main/java/org/myrobotlab/service/Rekognition.java b/src/main/java/org/myrobotlab/service/Rekognition.java index dd7b2bfed5..70f9f550ba 100644 --- a/src/main/java/org/myrobotlab/service/Rekognition.java +++ b/src/main/java/org/myrobotlab/service/Rekognition.java @@ -12,6 +12,7 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import com.amazonaws.auth.AWSCredentials; @@ -33,7 +34,7 @@ import com.amazonaws.services.rekognition.model.Label; import com.amazonaws.util.IOUtils; -public class Rekognition extends Service { +public class Rekognition extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Relay.java b/src/main/java/org/myrobotlab/service/Relay.java index bd7c40edb5..c56999f2d0 100644 --- a/src/main/java/org/myrobotlab/service/Relay.java +++ b/src/main/java/org/myrobotlab/service/Relay.java @@ -6,9 +6,10 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; -public class Relay extends Service { +public class Relay extends Service { public Arduino arduino; public Integer pin; diff --git a/src/main/java/org/myrobotlab/service/RoboClaw.java b/src/main/java/org/myrobotlab/service/RoboClaw.java index a26d3dfbe8..83203b99f8 100644 --- a/src/main/java/org/myrobotlab/service/RoboClaw.java +++ b/src/main/java/org/myrobotlab/service/RoboClaw.java @@ -16,6 +16,7 @@ import org.myrobotlab.serial.CRC; import org.myrobotlab.service.Pid.PidData; import org.myrobotlab.service.abstracts.AbstractMotorController; +import org.myrobotlab.service.config.MotorConfig; import org.myrobotlab.service.interfaces.MotorControl; import org.myrobotlab.service.interfaces.MotorController; import org.myrobotlab.service.interfaces.PortConnector; @@ -54,7 +55,7 @@ * this value IS correct * */ -public class RoboClaw extends AbstractMotorController implements EncoderPublisher, PortConnector, MotorController, SerialDataListener { +public class RoboClaw extends AbstractMotorController implements EncoderPublisher, PortConnector, MotorController, SerialDataListener { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Roomba.java b/src/main/java/org/myrobotlab/service/Roomba.java index 504b9184f8..66b8e9bffb 100644 --- a/src/main/java/org/myrobotlab/service/Roomba.java +++ b/src/main/java/org/myrobotlab/service/Roomba.java @@ -32,6 +32,7 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.roomba.RoombaComm; import org.myrobotlab.roomba.RoombaCommPort; +import org.myrobotlab.service.config.RoombaConfig; import org.slf4j.Logger; /** @@ -41,7 +42,7 @@ * * More Info: RoombaComm */ -public class Roomba extends Service { +public class Roomba extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Ros.java b/src/main/java/org/myrobotlab/service/Ros.java index 0c16a500aa..bea1ad4bc2 100644 --- a/src/main/java/org/myrobotlab/service/Ros.java +++ b/src/main/java/org/myrobotlab/service/Ros.java @@ -36,7 +36,7 @@ * @author GroG * */ -public class Ros extends Service implements RemoteMessageHandler, ConnectionEventListener { +public class Ros extends Service implements RemoteMessageHandler, ConnectionEventListener { /** * @see https://github.com/biobotus/rosbridge_suite/blob/master/ROSBRIDGE_PROTOCOL.md @@ -148,12 +148,12 @@ public Ros(String n, String id) { } @Override - public ServiceConfig apply(ServiceConfig c) { - RosConfig config = (RosConfig) super.apply(c); - if (config.connect) { - connect(config.bridgeUrl); - if (config.subscriptions != null) { - for (String topic : config.subscriptions) { + public RosConfig apply(RosConfig c) { + super.apply(c); + if (c.connect) { + connect(c.bridgeUrl); + if (c.subscriptions != null) { + for (String topic : c.subscriptions) { rosSubscribe(topic); } } @@ -185,8 +185,8 @@ public void disconnect() { } @Override - public ServiceConfig getConfig() { - RosConfig config = (RosConfig) super.getConfig(); + public RosConfig getConfig() { + super.getConfig(); config.connect = connected; return config; } diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index 8b00325a44..f6000f2a2f 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -31,15 +31,19 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Queue; import java.util.Random; import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; +import java.util.stream.Collectors; import org.myrobotlab.codec.ClassUtil; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.codec.CodecUtils.ApiDescription; +import org.myrobotlab.codec.ForeignProcessUtils; import org.myrobotlab.framework.CmdConfig; import org.myrobotlab.framework.CmdOptions; import org.myrobotlab.framework.DescribeQuery; @@ -53,11 +57,14 @@ import org.myrobotlab.framework.Peer; import org.myrobotlab.framework.Plan; import org.myrobotlab.framework.Platform; +import org.myrobotlab.framework.ProxyFactory; import org.myrobotlab.framework.Registration; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.ServiceReservation; import org.myrobotlab.framework.Status; +import org.myrobotlab.framework.interfaces.ConfigurableService; import org.myrobotlab.framework.interfaces.MessageListener; +import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.framework.repo.IvyWrapper; import org.myrobotlab.framework.repo.Repo; @@ -79,7 +86,6 @@ import org.myrobotlab.process.Launcher; import org.myrobotlab.service.config.RuntimeConfig; import org.myrobotlab.service.config.ServiceConfig; -import org.myrobotlab.service.config.ServiceConfig.Listener; import org.myrobotlab.service.data.Locale; import org.myrobotlab.service.data.ServiceTypeNameResults; import org.myrobotlab.service.interfaces.ConnectionManager; @@ -90,6 +96,7 @@ import org.myrobotlab.service.meta.abstracts.MetaData; import org.myrobotlab.string.StringUtil; import org.slf4j.Logger; +import org.yaml.snakeyaml.constructor.ConstructorException; import picocli.CommandLine; @@ -117,7 +124,7 @@ * VAR OF RUNTIME ! * */ -public class Runtime extends Service implements MessageListener, ServiceLifeCyclePublisher, RemoteMessageHandler, ConnectionManager, Gateway, LocaleProvider { +public class Runtime extends Service implements MessageListener, ServiceLifeCyclePublisher, RemoteMessageHandler, ConnectionManager, Gateway, LocaleProvider { final static private long serialVersionUID = 1L; // FIXME - AVOID STATIC FIELDS !!! use .getInstance() to get the singleton @@ -133,7 +140,8 @@ public class Runtime extends Service implements MessageListener, ServiceLifeCycl * to start and configure new services. The master plan is an accumulation of * all these requests. */ - final Plan masterPlan = new Plan("runtime"); + @Deprecated /* use the filesystem only no memory plan */ + transient final Plan masterPlan = new Plan("runtime"); /** * thread for non-blocking install of services @@ -143,7 +151,7 @@ public class Runtime extends Service implements MessageListener, ServiceLifeCycl /** * services which want to know if another service with an interface they are * interested in registers or is released - * + * * requestor type > interface > set of applicable service names */ protected final Map> interfaceToNames = new HashMap<>(); @@ -169,7 +177,7 @@ public class Runtime extends Service implements MessageListener, ServiceLifeCycl "org.myrobotlab.service.interfaces.ServiceLifeCycleListener", "org.myrobotlab.framework.interfaces.StatePublisher")); protected final Set serviceTypes = new HashSet<>(); - + /** * The directory name currently being used for config. This is NOT full path * name. It cannot be null, it cannot have "/" or "\" in the name - it has to @@ -205,7 +213,7 @@ public class Runtime extends Service implements MessageListener, ServiceLifeCycl * which connection a client is from) is in the Map <String, Object> information. * Since different connections have different requirements, and details regarding * clients the only "fixed" required info to add a client is : - * + * * uuid - key unique identifier for the client * connection - name of the connection currently managing the clients connection * state - state of the client and/or connection @@ -232,7 +240,8 @@ public class Runtime extends Service implements MessageListener, ServiceLifeCycl protected Integer creationCount = 0; /** - * the local repo.json manifest of this machine, which is a list of all libraries ivy installed + * the local repo.json manifest of this machine, which is a list of all + * libraries ivy installed */ transient private IvyWrapper repo = null; // was transient abstract Repo @@ -312,7 +321,7 @@ public class Runtime extends Service implements MessageListener, ServiceLifeCycl * * @return the number of processors available to the Java virtual machine. * @see java.lang.Runtime#availableProcessors() - * + * */ public static final int availableProcessors() { return java.lang.Runtime.getRuntime().availableProcessors(); @@ -359,13 +368,13 @@ static public ServiceInterface create(String name) { /** * Create create(name, type) goes through the full service lifecycle of: - * + * *
    * clear - clearing the plan for construction of service(s) needed 
    * load  - loading the plan for desired services 
    * check - checking all planned service have met appropriate licensing and dependency checks create -
    * 
- * + * * @param name * - Required, cannot be null * @param type @@ -401,7 +410,7 @@ static public synchronized ServiceInterface create(String name, String type) { * should be defined in the plan. * * FIXME - should Plan be passed in as param ? - * + * * @param name * @return */ @@ -414,7 +423,7 @@ synchronized private static Map createServicesFromPlan // Plan's config RuntimeConfig plansRtConfig = (RuntimeConfig) plan.get("runtime"); // current Runtime config - RuntimeConfig currentConfig = (RuntimeConfig) Runtime.getInstance().config; + RuntimeConfig currentConfig = Runtime.getInstance().config; for (String service : plansRtConfig.getRegistry()) { // FIXME - determine if you want to return a complete merge of activated @@ -430,10 +439,12 @@ synchronized private static Map createServicesFromPlan sc.state = "CREATING"; ServiceInterface si = createService(service, sc.type, null); sc.state = "CREATED"; - si.setConfig(sc); - si.apply(sc); + // process the base listeners/subscription of ServiceConfig + si.addConfigListeners(sc); + if (si instanceof ConfigurableService) { + ((ConfigurableService)si).apply(sc); + } createdServices.put(service, si); - // si.startService(); bad idea currentConfig.add(service); } @@ -463,7 +474,7 @@ public static String getPeerName(String peerKey, ServiceConfig config, Map services) { */ @Override public boolean setVirtual(boolean b) { + boolean changed = isVirtual != b; setAllVirtual(b); + if (changed) { + broadcastState(); + } return b; } @@ -594,15 +608,16 @@ static public boolean setAllVirtual(boolean b) { Runtime.getInstance().broadcastState(); return b; } - + /** - * Sets the enable value in start.yml. start.yml is a file which can control the automatic - * loading of config. In general when its on, and a config is selected and saved, the next - * time Runtime starts it will attempt to load the last saved config and get the user back to - * their last state. + * Sets the enable value in start.yml. start.yml is a file which can control + * the automatic loading of config. In general when its on, and a config is + * selected and saved, the next time Runtime starts it will attempt to load + * the last saved config and get the user back to their last state. * * @param autoStart - * @throws IOException - thrown if cannot write file to filesystem + * @throws IOException + * - thrown if cannot write file to filesystem */ public void setAutoStart(boolean autoStart) throws IOException { log.debug("setAutoStart {}", autoStart); @@ -679,17 +694,12 @@ static private synchronized ServiceInterface createService(String name, String t return null; } - String fullTypeName; - if (type.contains(".")) { - fullTypeName = type; - } else { - fullTypeName = String.format("org.myrobotlab.service.%s", type); - } + String fullTypeName = CodecUtils.makeFullTypeName(type); ServiceInterface si = Runtime.getService(fullName); if (si != null) { - if (!si.getType().equals(fullTypeName)) { - runtime.error("Service with name {} already exists but is of type {} while requested type is ", name, si.getType(), type); + if (!si.getTypeKey().equals(fullTypeName)) { + runtime.error("Service with name {} already exists but is of type {} while requested type is ", name, si.getTypeKey(), type); return null; } return si; @@ -852,8 +862,8 @@ public static String getExternalIp() throws Exception { * @return the amount of free memory in the Java Virtual Machine. Calling the * gc method may result in increasing the value returned by * freeMemory. - * - * + * + * */ public static final long getFreeMemory() { return java.lang.Runtime.getRuntime().freeMemory(); @@ -881,10 +891,13 @@ public static Runtime getInstance() { // a bit backwards - it loads after it been created // but its necessary because you need an runtime instance before you // load + + File cfgRoot = new File(CONFIG_ROOT); + cfgRoot.mkdirs(); if (startYml.enable) { Runtime.load("runtime", "Runtime"); } - ((RuntimeConfig) runtime.config).add("runtime"); + runtime.config.add("runtime"); runtime.startService(); // platform virtual is higher priority than service virtual @@ -924,7 +937,7 @@ public static Runtime getInstance() { /** * The jvm args which started this process - * + * * @return all jvm args in a list */ static public List getJvmArgs() { @@ -934,7 +947,7 @@ static public List getJvmArgs() { /** * gets all non-loopback, active, non-virtual ip addresses - * + * * @return list of local ipv4 IP addresses */ static public List getIpAddresses() { @@ -995,7 +1008,7 @@ static public Set getNetworkPeers() throws UnknownHostException { networkPeers = new TreeSet<>(); // String myip = InetAddress.getLocalHost().getHostAddress(); List myips = getIpAddresses(); // TODO - if nothing else - - // 127.0.0.1 + // 127.0.0.1 for (String myip : myips) { if (myip.equals("127.0.0.1")) { log.info("This PC is not connected to any network!"); @@ -1089,7 +1102,7 @@ public static Map getLocalServices() { /** * FIXME - return - * + * * @return filtering/query requests */ public static Map getLocalServicesForExport() { @@ -1099,7 +1112,7 @@ public static Map getLocalServicesForExport() { /* * FIXME - DEPRECATE - THIS IS NOT "instance" specific info - its Class * definition info - Runtime should return based on ClassName - * + * * FIXME - INPUT PARAMETER SHOULD BE TYPE NOT INSTANCE NAME !!!! */ public static Map getMethodMap(String inName) { @@ -1127,18 +1140,11 @@ public static Map getMethodMap(String inName) { * TODO - future work would be to supply a query to the getServiceList(query) * such that interfaces, types, or processes ids, can selectively be queried * out of it - * + * * @return list of registrations */ synchronized public List getServiceList() { - List ret = new ArrayList<>(); - for (ServiceInterface si : registry.values()) { - // problem with - // ret.add(new NameAndType(si.getId(), si.getName(), si.getType(), - // CodecUtils.toJson(si))); - ret.add(new Registration(si.getId(), si.getName(), si.getType())); - } - return ret; + return registry.values().stream().map(si -> new Registration(si.getId(), si.getName(), si.getTypeKey())).collect(Collectors.toList()); } // FIXME - scary function - returns private data @@ -1171,7 +1177,7 @@ public static ServiceInterface getService(String inName) { /** * @return all service names in an array form * - * + * */ static public String[] getServiceNames() { List si = getServices(); @@ -1179,7 +1185,7 @@ static public String[] getServiceNames() { for (int i = 0; i < ret.length; ++i) { ServiceInterface s = si.get(i); - if (s.getId().contentEquals(Platform.getLocalInstance().getId())) { + if (isLocal(s.getFullName())) { ret[i] = s.getName(); } else { ret[i] = s.getFullName(); @@ -1199,17 +1205,7 @@ public static boolean match(String text, String pattern) { } public static List getServiceNames(String pattern) { - List sis = getServices(); - List ret = new ArrayList(); - for (ServiceInterface si : sis) { - String serviceName = si.getName(); - - if (match(serviceName, pattern)) { - ret.add(serviceName); - } - - } - return ret; + return getServices().stream().map(NameProvider::getName).filter(serviceName -> match(serviceName, pattern)).collect(Collectors.toList()); } /** @@ -1218,7 +1214,7 @@ public static List getServiceNames(String pattern) { * @return a list of service names that implement the interface * @throws ClassNotFoundException * if the class for the requested interface is not found. - * + * */ public static List getServiceNamesFromInterface(String interfaze) throws ClassNotFoundException { if (!interfaze.contains(".")) { @@ -1233,14 +1229,9 @@ public static List getServiceNamesFromInterface(String interfaze) throws * interface * @return list of service names * - */ // FIXME !!! NOT RETURNING FULL NAMES !!! + */ public static List getServiceNamesFromInterface(Class interfaze) { - List ret = new ArrayList(); - List services = getServicesFromInterface(interfaze); - for (int i = 0; i < services.size(); ++i) { - ret.add(services.get(i).getName()); - } - return ret; + return getServicesFromInterface(interfaze).stream().map(ServiceInterface::getFullName).collect(Collectors.toList()); } /** @@ -1280,7 +1271,7 @@ public static List getServices(String id) { * @param interfaze * interface * @return results - * + * */ public ServiceTypeNameResults getServiceTypeNamesFromInterface(String interfaze) { ServiceTypeNameResults results = new ServiceTypeNameResults(interfaze); @@ -1296,7 +1287,7 @@ public ServiceTypeNameResults getServiceTypeNamesFromInterface(String interfaze) for (MetaData st : sts) { - Set> ancestry = new HashSet>(); + Set> ancestry = new HashSet<>(); Class targetClass = Class.forName(st.getType()); // this.getClass(); while (targetClass.getCanonicalName().startsWith("org.myrobotlab") && !targetClass.getCanonicalName().startsWith("org.myrobotlab.framework")) { @@ -1326,11 +1317,11 @@ public ServiceTypeNameResults getServiceTypeNamesFromInterface(String interfaze) /** * return a list of services which are currently running and implement a * specific interface - * + * * @param interfaze * class * @return list of service interfaces - * + * */ // FIXME !!! - use single implementation that gets parents @Deprecated /* @@ -1354,11 +1345,12 @@ public static synchronized List getServicesFromInterface(Class } return ret; } - + /** - * Because startYml is required to be a static variable, since it's needed "before" - * a runtime instance exists it will be null in json serialization. This method is needed - * so we can serialize the data appropriately. + * Because startYml is required to be a static variable, since it's needed + * "before" a runtime instance exists it will be null in json serialization. + * This method is needed so we can serialize the data appropriately. + * * @return */ static public CmdConfig getStartYml() { @@ -1453,9 +1445,9 @@ public static String getDiffTime(long diff) { * Get version returns the current version of mrl. It must be done this way, * because the version may be queried on the command line without the desire * to start a "Runtime" - * + * * @return the version of the running platform instance - * + * */ public static String getVersion() { return Platform.getLocalInstance().getVersion(); @@ -1515,14 +1507,14 @@ static public void install(String serviceType) { * During typically runtime install of services - non blocking is desired, * otherwise status info from the install is blocked until installation is * completed. For command line installation "blocking" mode would be desired - * + * * FIXME - problematic in that Runtime.create calls this directly, and this * should be stepped through, because: If we need to install new components, a * restart is likely needed ... we don't do custom dynamic classloaders .... * yet - * + * * License - should be appropriately accepted or rejected by user - * + * * @param serviceType * the service tyype to install * @param blocking @@ -1597,7 +1589,7 @@ static public void invokeCommands(String[] invoke) { */ public static boolean isLocal(String serviceName) { ServiceInterface sw = getService(serviceName); - return sw.isLocal(); + return Objects.equals(sw.getId(), Platform.getLocalInstance().getId()); } /* @@ -1694,28 +1686,34 @@ public void onState(ServiceInterface updatedService) { registry.put(String.format("%s@%s", updatedService.getName(), updatedService.getId()), updatedService); } + public static synchronized Registration register(String id, String name, String typeKey, ArrayList interfaces) { + Registration proxy = new Registration(id, name, typeKey, interfaces); + register(proxy); + return proxy; + } + /** * Registration is the process where a remote system sends detailed info * related to its services. It will have details on each service type, state, * id, and other info. The registration is serializable, with state * information in a serialized for so that stateless processes or other * non-Java instances can register or be registered. - * + * * Registration might setup subscriptions to support a UI. - * + * * Additional info which will be added in the future is a method map (a * swagger concept) and a list of supported interfaces - * + * * TODO - have rules on what registrations to accept - dependent on security, * desire, re-broadcasting configuration etc. TODO - determine rules on * re-broadcasting based on configuration - * + * * @param registration * registration * @return registration - * + * */ - public final static synchronized Registration register(Registration registration) { + public static synchronized Registration register(Registration registration) { try { @@ -1728,22 +1726,64 @@ public final static synchronized Registration register(Registration registration return registration; } - // FIXME - make work for non-Java classes.. - String fullTypeName; - if (registration.getTypeKey().contains(".")) { - fullTypeName = registration.getTypeKey(); - } else { - fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey()); + if (!ForeignProcessUtils.isValidTypeKey(registration.getTypeKey())) { + log.error("Invalid type key being registered: " + registration.getTypeKey()); + return null; } log.info("{}@{} registering at {} of type {}", registration.getName(), registration.getId(), Platform.getLocalInstance().getId(), registration.getTypeKey()); if (!registration.isLocal(Platform.getLocalInstance().getId())) { - // de-serialize - registration.service = Runtime.createService(registration.getName(), registration.getTypeKey(), registration.getId()); - if (registration.getState() != null) { - copyShallowFrom(registration.service, CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName))); + // Check if we're registering a java service + if (ForeignProcessUtils.isValidJavaClassName(registration.getTypeKey())) { + + String fullTypeName; + if (registration.getTypeKey().contains(".")) { + fullTypeName = registration.getTypeKey(); + } else { + fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey()); + } + + try { + // de-serialize, class exists + registration.service = Runtime.createService(registration.getName(), fullTypeName, registration.getId()); + if (registration.getState() != null) { + copyShallowFrom(registration.service, CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName))); + } + } catch (ClassNotFoundException classNotFoundException) { + log.error(String.format("Unknown service class for %s@%s: %s", registration.getName(), registration.getId(), registration.getTypeKey()), classNotFoundException); + return null; + } + } else { + // We're registering a foreign process service. We don't need to check + // ForeignProcessUtils.isForeignTypeKey() because the type key is + // valid + // but is not a java class name + + // Class does not exist, check if registration has empty interfaces + // Interfaces should always include ServiceInterface if coming from + // remote client + if (registration.interfaces == null || registration.interfaces.isEmpty()) { + log.error("Unknown service type being registered, registration does not contain any " + "interfaces for proxy generation: " + registration.getTypeKey()); + return null; + } + + // Class[] interfaces = registration.interfaces.stream().map(i -> { + // try { + // return Class.forName(i); + // } catch (ClassNotFoundException e) { + // throw new RuntimeException("Unable to load interface " + i + " + // defined in remote registration " + registration, e); + // } + // }).toArray(Class[]::new); + + // registration.service = (ServiceInterface) + // Proxy.newProxyInstance(Runtime.class.getClassLoader(), interfaces, + // new ProxyServiceInvocationHandler(registration.getName(), + // registration.getId())); + registration.service = ProxyFactory.createProxyService(registration); + log.info("Created proxy: " + registration.service); } } @@ -1752,11 +1792,10 @@ public final static synchronized Registration register(Registration registration if (runtime != null) { String type = registration.getTypeKey(); - Set names = runtime.typeToNames.get(type); - if (names == null) { - names = new HashSet<>(); - runtime.typeToNames.put(type, names); - } + + // If type does not exist in typeToNames, make it an empty hash set and + // return it + Set names = runtime.typeToNames.computeIfAbsent(type, k -> new HashSet<>()); names.add(fullname); // FIXME - most of this could be static as it represents meta data of @@ -1775,9 +1814,7 @@ public final static synchronized Registration register(Registration registration if (!runtime.serviceTypes.contains(type)) { // CHECK IF "CAN FULFILL" // add the interfaces of the new service type - - // FIXME - make work for non Java - Set interfaces = ClassUtil.getInterfaces(fullTypeName, FILTERED_INTERFACES); + Set interfaces = ClassUtil.getInterfaces(registration.service.getClass(), FILTERED_INTERFACES); for (String interfaze : interfaces) { Set types = runtime.interfaceToType.get(interfaze); if (types == null) { @@ -1811,7 +1848,7 @@ public final static synchronized Registration register(Registration registration // TODO - remove ? already get state from registration if (!registration.isLocal(Platform.getLocalInstance().getId())) { - runtime.subscribe(registration.getName(), "publishState"); + runtime.subscribe(registration.getFullName(), "publishState"); } } catch (Exception e) { @@ -1827,11 +1864,11 @@ public final static synchronized Registration register(Registration registration * resources, and removes registry entries * * FIXME - clean up subscriptions from released - * + * * @param inName * name to release * @return true/false - * + * */ public synchronized static boolean releaseService(String inName) { if (inName == null) { @@ -1931,7 +1968,7 @@ synchronized public static void unregister(String inName) { // !! // it should be FULLNAME ! // runtime.broadcast("released", inName); - String type = sw.getType(); + String type = sw.getTypeKey(); boolean updatedServiceLists = false; @@ -1957,9 +1994,9 @@ synchronized public static void unregister(String inName) { // last step - remove from registry registry.remove(name); // and config - RuntimeConfig c = (RuntimeConfig)Runtime.getInstance().config; - c.registry.remove(CodecUtils.shortName(name)); - + RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config; + c.registry.remove(CodecUtils.getShortName(name)); + log.info("released {}", name); } @@ -2015,7 +2052,7 @@ public static void releaseAll() { * FIXME - send SHUTDOWN event to all running services with a timeout period - * end with System.exit() FIXME normalize with releaseAllLocal and * releaseAllExcept - * + * * local only? YES !!! LOCAL ONLY !! * * @param releaseRuntime @@ -2054,7 +2091,7 @@ static private void processRelease(boolean releaseRuntime) { // reverse release to order of creation Collection local = getLocalServices().values(); List ordered = new ArrayList<>(local); - + ordered.removeIf(Objects::isNull); Collections.sort(ordered); Collections.reverse(ordered); @@ -2069,12 +2106,19 @@ static private void processRelease(boolean releaseRuntime) { log.info("releasing service {}", sw.getName()); try { - if (sw != null) { - sw.releaseService(); - } + sw.releaseService(); } catch (Exception e) { runtime.error("%s threw while releasing", e); - log.error("rease", e); + log.error("release", e); + } + } + + // clean up remote ... the contract should + // probably be just remove their references - do not + // ask for them to be released remotely .. + for (String remoteService : registry.keySet()) { + if (!remoteService.equals(runtime.getFullName())) { + registry.remove(remoteService); } } @@ -2143,7 +2187,7 @@ public Integer publishShutdown(Integer seconds) { /** * publish the folders of the parent directory of configPath if the configPath * is null then publish directory names of data/config - * + * * @return list of configs */ public List publishConfigList() { @@ -2156,6 +2200,11 @@ public List publishConfigList() { } File[] files = configDirFile.listFiles(); + if (files == null) { + // We checked for if directory earlier, so can only be null for IO error + error("IO error occurred while listing config directory files"); + return configList; + } for (File file : files) { String n = file.getName(); @@ -2180,11 +2229,9 @@ public List publishConfigList() { public static void releaseAllServicesExcept(HashSet saveMe) { log.info("releaseAllServicesExcept"); List list = Runtime.getServices(); - for (int i = 0; i < list.size(); ++i) { - ServiceInterface si = list.get(i); + for (ServiceInterface si : list) { if (saveMe != null && saveMe.contains(si.getName())) { log.info("leaving {}", si.getName()); - continue; } else { si.releaseService(); } @@ -2246,11 +2293,11 @@ public void disconnect() throws IOException { /** * FIXME - can this be renamed back to attach ? jump to another process using * the cli - * + * * @param id * instance id. * @return string - * + * */ // FIXME - remove - the way to 'jump' is just to change // context to the correct mrl id e.g. cd /runtime@remote07 @@ -2395,11 +2442,11 @@ public void connect(String url) { // runtime@{id} // subscribe to "describe" MRLListener listener = new MRLListener("describe", getFullName(), "onDescribe"); - Message msg = Message.createMessage(getFullName(), "runtime", "addListener", listener); - client2.send(CodecUtils.toJson(msg)); + Message msg = Message.createMessage(getFullName(), "runtime", "addListener", listener); + client2.send(CodecUtils.toJsonMsg(msg)); // send describe - client2.send(CodecUtils.toJson(getDescribeMsg(null))); + client2.send(CodecUtils.toJsonMsg(getDescribeMsg(null))); } catch (Exception e) { log.error("connect to {} giving up {}", url, e.getMessage()); @@ -2600,9 +2647,7 @@ public String publishFinishedConfig(String configName) { */ synchronized static public ServiceInterface start(String name, String type) { try { - if (name.equals("proxy")) { - log.info("herex"); - } + ServiceInterface requestedService = Runtime.getService(name); if (requestedService != null) { @@ -2639,7 +2684,8 @@ synchronized static public ServiceInterface start(String name, String type) { } if (requestedService == null) { - log.error("here"); + Runtime.getInstance().error("could not start %s of type %s", name, type); + return null; } // getConfig() was problematic here for JMonkeyEngine @@ -2720,9 +2766,9 @@ public Runtime(String n, String id) { // fist and only time.... runtime = this; repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper"); - + // resolve serviceData MetaTypes for the repo - + for (MetaData metaData : serviceData.getServiceTypes()) { if (metaData.getSimpleName().equals("OpenCV")) { log.warn("here"); @@ -2734,7 +2780,7 @@ public Runtime(String n, String id) { log.warn("{} not installed", metaData.getSimpleName()); } } - + } } @@ -2920,7 +2966,7 @@ static public String getInputAsString(InputStream is) { /** * list the contents of the current working directory - * + * * @return object */ public Object ls() { @@ -2940,18 +2986,19 @@ public Object ls(String path) { /** * list the contents of a specific path - * + *

+ *

* TODO It looks like this only returns Object because it wants to return * either a String array or a method entry list. It would probably be best to * just convert the method entry list to a string array using streams and * change the signature to match. - * + * * @param contextPath * c * @param path * p * @return object - * + * */ public Object ls(String contextPath, String path) { String absPath = null; @@ -2990,7 +3037,7 @@ public Object ls(String contextPath, String path) { } else if (parts.length == 3) { ServiceInterface si = Runtime.getService(parts[1]); MethodCache cache = MethodCache.getInstance(); - List me = cache.query(si.getType(), parts[2]); + List me = cache.query(si.getTypeKey(), parts[2]); return me; // si.getMethodMap().get(parts[2]); } return ret; @@ -2998,9 +3045,9 @@ public Object ls(String contextPath, String path) { /** * serviceName at id - * + * * @return runtime name with instance id. - * + * */ public String whoami() { return "runtime@" + getId(); @@ -3054,7 +3101,7 @@ public Repo getRepo() { *

* The serviceData.json lists all service types, dependencies, categories and * other relevant information regarding service creation - * + * * @return list of all service type names */ public String[] getServiceTypeNames() { @@ -3064,11 +3111,11 @@ public String[] getServiceTypeNames() { /** * getServiceTypeNames will publish service names based on some filter * criteria - * + * * @param filter * f * @return array of service types - * + * */ public String[] getServiceTypeNames(String filter) { return serviceData.getServiceTypeNames(filter); @@ -3125,10 +3172,10 @@ public void onMessage(Message msg) { /** * Publishing point when a service was successfully registered locally - * regardless if the service is local or not. - * + * * TODO - more business logic can be created here to limit broadcasting or * re-broadcasting published registrations - * + * * @param registration * - contains all the information need for a registration to process */ @@ -3140,7 +3187,7 @@ public Registration registered(Registration registration) { /** * released event - when a service is successfully released from the registry * this event is triggered - * + * */ @Override public String released(String name) { @@ -3150,13 +3197,13 @@ public String released(String name) { /** * A function for runtime to "save" a service - or if the service does not * exists save the "default" config of that type of service - * + * * @param name * name of service to export * @return true/false * @throws IOException * boom - * + * */ @Deprecated /* use save(name) */ public boolean export(String name /* , String type */) throws IOException { @@ -3261,7 +3308,7 @@ static public Map getManifest() { /** * Runtime's setLogLevel will set the root log level if its called from a * service - it will only set that Service type's log level - * + * * @param level * - DEBUG | INFO | WARN | ERROR * @return the level which was set @@ -3380,7 +3427,7 @@ static public void removeAllSubscriptions() { for (ServiceInterface si : getLocalServices().values()) { List nlks = si.getNotifyListKeySet(); for (int i = 0; i < nlks.size(); ++i) { - si.getOutbox().notifyList.clear(); + si.getNotifyList().clear(); } } } @@ -3451,7 +3498,7 @@ static public String execute(String... args) { } } - return execute(program, list, null, null, null); + return execute(program, list, null, null, true); } /** @@ -3473,79 +3520,69 @@ static public String execute(String... args) { * Whether this method blocks for the program to execute * @return The programs stderr and stdout output */ - static public String execute(String program, List args, String workingDir, Map additionalEnv, Boolean block) { - + static public String execute(String program, List args, String workingDir, Map additionalEnv, boolean block) { log.info("execToString(\"{} {}\")", program, args); - ArrayList command = new ArrayList(); + List command = new ArrayList<>(); command.add(program); if (args != null) { - for (String arg : args) { - command.add(arg); - } + command.addAll(args); } - Integer exitValue = null; - ProcessBuilder builder = new ProcessBuilder(command); + if (workingDir != null) { + builder.directory(new File(workingDir)); + } Map environment = builder.environment(); if (additionalEnv != null) { - environment.putAll(additionalEnv); + environment.putAll(additionalEnv); } - StringBuilder outputBuilder; - - try { - Process handle = builder.start(); - - InputStream stdErr = handle.getErrorStream(); - InputStream stdOut = handle.getInputStream(); - - // TODO: we likely don't need this - // OutputStream stdIn = handle.getOutputStream(); - - outputBuilder = new StringBuilder(); - byte[] buff = new byte[32768]; - // TODO: should we read both of these streams? - // if we break out of the first loop is the process terminated? + StringBuilder outputBuilder = new StringBuilder(); - // read stdout - for (int n; (n = stdOut.read(buff)) != -1;) { - outputBuilder.append(new String(buff, 0, n)); - } - - // read stderr - for (int n; (n = stdErr.read(buff)) != -1;) { - outputBuilder.append(new String(buff, 0, n)); - } - - stdOut.close(); - stdErr.close(); + try { + Process handle = builder.start(); - // TODO: stdin if we use it. - // stdIn.close(); + InputStream stdErr = handle.getErrorStream(); + InputStream stdOut = handle.getInputStream(); - // the process should be closed by now? + // Read the output streams in separate threads to avoid potential blocking + Thread stdErrThread = new Thread(() -> readStream(stdErr, outputBuilder)); + stdErrThread.start(); - handle.waitFor(); + Thread stdOutThread = new Thread(() -> readStream(stdOut, outputBuilder)); + stdOutThread.start(); - handle.destroy(); + if (block) { + int exitValue = handle.waitFor(); + outputBuilder.append("Exit Value: ").append(exitValue); + log.info("Command exited with exit value: {}", exitValue); + } else { + log.info("Command started"); + } - exitValue = handle.exitValue(); - // print the output from the command - // TODO replace with logging calls - System.out.println(outputBuilder.toString()); - System.out.println("Exit Value : " + exitValue); - outputBuilder.append("Exit Value : " + exitValue); + return outputBuilder.toString(); + } catch (IOException e) { + log.error("Error executing command", e); + return e.getMessage(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Command execution interrupted", e); + return e.getMessage(); + } +} - return outputBuilder.toString(); - } catch (Exception e) { - log.error("execute threw", e); - exitValue = 5; - return e.getMessage(); +private static void readStream(InputStream inputStream, StringBuilder outputBuilder) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + outputBuilder.append(line).append(System.lineSeparator()); + } + } catch (IOException e) { + log.error("Error reading process output", e); } - } +} /** * Get the current battery level of the computer this MRL instance is running @@ -3629,7 +3666,7 @@ public ServiceData getServiceData() { /** * Return supported system languages - * + * * @return map of languages to locales */ public Map getLanguages() { @@ -3660,8 +3697,8 @@ public Map setLocales(String... codes) { /** * @return get the Security singleton - * - * + * + * */ static public Security getSecurity() { return Security.getInstance(); @@ -3726,11 +3763,11 @@ public ServiceData setServiceTypes(ServiceData sd) { * service data * service methods * details of a service method * help/javadoc * of a service method * list of other known instances * levels of detail, or * lists of fields to display * meaningful default - * + * * FIXME - input parameters will need to change - at some point, a subscribe * to describe, and appropriate input parameters should replace the current * onRegistered system - * + * * @param type * t * @param id @@ -3738,7 +3775,7 @@ public ServiceData setServiceTypes(ServiceData sd) { * @param remoteUuid * remote id * @return describe results - * + * */ public DescribeResults describe(String type, String id, String remoteUuid) { DescribeQuery query = new DescribeQuery(type, remoteUuid); @@ -3764,15 +3801,15 @@ public DescribeResults describe() { * state, etc *

* FIXME uuid and query are unused - * + * * @param uuid * u * @param query * q * @return describe results - * - * - * + * + * + * */ public DescribeResults describe(String uuid, DescribeQuery query) { @@ -3812,11 +3849,11 @@ public DescribeResults describe(String uuid, DescribeQuery query) { /** * Describe results from remote query to describe - * + * * @param results * describe results - * - * + * + * */ public void onDescribe(DescribeResults results) { List reservations = results.getReservations(); @@ -3841,13 +3878,13 @@ public void onDescribe(DescribeResults results) { * registrations to the newly connected remote process. If the "registered" * event is subscribed, any newly created service will be broadcasted thorough * this publishing point as well. - * + * * TODO - write filtering, configuration, or security which affects what can * be registered - * + * * Primarily, this is where new services are registered from remote systems - * - * + * + * */ public void onRegistered(Registration registration) { try { @@ -3899,10 +3936,10 @@ public List getServiceTypes() { if (metaData.isAvailable()) { filteredTypes.add(metaData); } - } + } return filteredTypes; } - + /** * Register a connection route from one instance to this one. * @@ -3929,33 +3966,21 @@ public void addConnection(String uuid, String id, Connection connection) { addRoute(id, uuid, 10); } - @Override - public Message getDescribeMsg(String connId) { - // TODO support queries - // FIXME !!! - msg.name is wrong with only "runtime" it should be - // "runtime@id" - // TODO - lots of options for a default "describe" - Message msg = Message.createMessage(String.format("%s@%s", getName(), getId()), "runtime", "describe", - new Object[] { "fill-uuid", CodecUtils.toJson(new DescribeQuery(Platform.getLocalInstance().getId(), connId)) }); - - return msg; - } - - @Override - public ServiceConfig getFilteredConfig() { - RuntimeConfig sc = (RuntimeConfig) super.getFilteredConfig(); - Set removeList = new HashSet<>(); - for (Listener listener : sc.listeners) { - if (listener.callback.equals("onReleased") || listener.callback.equals("onStarted") || listener.callback.equals("onRegistered") || listener.callback.equals("onStopped") - || listener.callback.equals("onCreated")) { - removeList.add(listener); - } - } - for (Listener remove : removeList) { - sc.listeners.remove(remove); - } - return sc; - } +// @Override +// public ServiceConfig getFilteredConfig() { +// RuntimeConfig sc = (RuntimeConfig) super.getFilteredConfig(); +// Set removeList = new HashSet<>(); +// for (Listener listener : sc.listeners) { +// if (listener.callback.equals("onReleased") || listener.callback.equals("onStarted") || listener.callback.equals("onRegistered") || listener.callback.equals("onStopped") +// || listener.callback.equals("onCreated")) { +// removeList.add(listener); +// } +// } +// for (Listener remove : removeList) { +// sc.listeners.remove(remove); +// } +// return sc; +// } /** * Unregister all connections that a specified client has made. @@ -4006,7 +4031,7 @@ public Connection publishConnect(Connection attributes) { /** * globally get all client - * + * * @return connection map */ public Map getConnections() { @@ -4016,11 +4041,11 @@ public Map getConnections() { /** * separated by connection - send connection name and get filter results back * for a specific connections connected clients - * + * * @param gatwayName * name * @return map of connections - * + * */ public Map getConnections(String gatwayName) { Map ret = new HashMap<>(); @@ -4036,7 +4061,7 @@ public Map getConnections(String gatwayName) { /** * @return list connections - current connection names to this mrl runtime - * + * */ public Map lc() { return getConnections(); @@ -4044,11 +4069,11 @@ public Map lc() { /** * get a specific clients data - * + * * @param uuid * uuid to get * @return connection for uuid - * + * */ public Connection getConnection(String uuid) { return connections.get(uuid); @@ -4056,7 +4081,7 @@ public Connection getConnection(String uuid) { /** * @return Globally get all connection uuids - * + * */ public List getConnectionUuids() { return getConnectionUuids(null); @@ -4076,11 +4101,11 @@ boolean connectionExists(String uuid) { /** * Get connection ids that belong to a specific gateway - * + * * @param name * n * @return list of uuids - * + * */ public List getConnectionUuids(String name) { List ret = new ArrayList<>(); @@ -4113,11 +4138,11 @@ public static Class getClass(String inName) { /** * takes an id returns a connection uuid - * + * * @param id * id * @return the connection - * + * */ public Connection getRoute(String id) { return connections.get(routeTable.getRoute(id)); @@ -4129,11 +4154,11 @@ public RouteTable getRouteTable() { /** * get gateway based on remote address of a msg e.g. msg.getRemoteId() - * + * * @param remoteId * remote * @return the gateway - * + * */ public Gateway getGatway(String remoteId) { // get a connection from the route @@ -4175,10 +4200,7 @@ public Gateway getGatway(String remoteId) { * name */ static public String getFullName(String shortname) { - if (shortname == null) { - return null; - } - if (shortname.contains("@")) { + if (shortname == null || shortname.contains("@")) { // already long form return shortname; } @@ -4257,10 +4279,10 @@ public Object publishCli(Message msg) { /** * DONT MODIFY NAME - JUST work on is Local - and InvokeOn should handle it - * + * * if the incoming Message's remote Id is the (same as ours) OR (it can't be * found it our route table) - peel it off and treat it as local. - * + * * if we have an @{id/connection} but do not have the connection - we'll peel * off the @{id/connection} and treat it as local if id is ours - peel it off * ! @@ -4361,9 +4383,9 @@ public static boolean exists() { /** * Attempt to get the most likely valid address priority would be a lan * address - possibly the smallest class - * + * * @return string address - * + * */ public String getAddress() { List addresses = getIpAddresses(); @@ -4488,10 +4510,10 @@ public void python() { * Main entry point for the MyRobotLab Runtime Check CmdOptions for list of * options -h help -v version -list jvm args -Dhttp.proxyHost=webproxy * f-Dhttp.proxyPort=80 -Dhttps.proxyHost=webproxy -Dhttps.proxyPort=80 - * + * * @param args * cmd line args from agent spawn - * + * */ public static void main(String[] args) { @@ -4638,12 +4660,12 @@ public Connection getConnectionFromId(String remoteId) { * "Connection". This key should be retrievable, when a msg arrives at the * service which needs to be sent remotely. This key is used to get the * "Connection" to send the msg remotely - * + * * @param string * s * @param uuid * u - * + * */ public void addLocalGatewayKey(String string, String uuid) { routeTable.addLocalGatewayKey(string, uuid); @@ -4659,7 +4681,7 @@ public String getConnectionUuidFromGatewayKey(String gatewayKey) { /** * This helper method will create, load then start a service - * + * * @param name * - name of instance * @param type @@ -4716,10 +4738,6 @@ synchronized private Plan loadService(Plan plan, String name, String type, boole log.info("loading - {} {} {}", name, type, level); - if (name.equals("i01.controller3")) { - log.info("here"); - } - if (plan == null) { log.error("plan required to load a system"); return null; @@ -4789,7 +4807,7 @@ synchronized private Plan loadService(Plan plan, String name, String type, boole } /** - * + * * @param configName * - filename or dir of config set * @param name @@ -4814,6 +4832,8 @@ public ServiceConfig readServiceConfig(String configName, String name) { if (check.exists()) { try { sc = CodecUtils.readServiceConfig(filename); + } catch (ConstructorException e) { + error("%s invalid %s %s. Please remove it from the file.", name, filename, e.getCause().getMessage()); } catch (IOException e) { error(e); } @@ -4847,9 +4867,12 @@ public String setAllIds(String id) { return id; } + @Override - public ServiceConfig apply(ServiceConfig c) { - RuntimeConfig config = (RuntimeConfig) super.apply(c); + public RuntimeConfig apply(RuntimeConfig c) { + super.apply(c); + config = c; + setLocale(config.locale); if (config.id != null) { @@ -4905,7 +4928,7 @@ static public void releaseConfig(String configName) { * * @param configPath * config set to release - * + * */ static public void releaseConfigPath(String configPath) { try { @@ -4951,7 +4974,7 @@ static public boolean saveConfig(String configName) { * Saves the current runtime, all services and all configuration for each * service in the current "config path", if the config path does not exist * will error - * + * * @param configName * - config set name if null defaults to default * @param serviceName @@ -4979,7 +5002,7 @@ public boolean saveService(String configName, String serviceName, String filenam Set servicesToSave = new HashSet<>(); // conditional boolean to flip and save a config name to start.yml ? - if (startYml.enable) { + if (startYml.enable) { startYml.id = getId(); startYml.config = configName; startYml.configRoot = CONFIG_ROOT; @@ -4995,15 +5018,12 @@ public boolean saveService(String configName, String serviceName, String filenam } for (String s : servicesToSave) { - if (CodecUtils.shortName(s).equals("i01")) { - log.info("here"); - } ServiceInterface si = getService(s); // TODO - switch to save "NON FILTERED" config !!!! // get filtered clone of config for saving ServiceConfig config = si.getFilteredConfig(); String data = CodecUtils.toYaml(config); - String ymlFileName = configPath + fs + CodecUtils.shortName(s) + ".yml"; + String ymlFileName = configPath + fs + CodecUtils.getShortName(s) + ".yml"; FileIO.toFile(ymlFileName, data.getBytes()); info("saved %s", ymlFileName); } @@ -5039,10 +5059,12 @@ public boolean isProcessingConfig() { /** * Sets the directory for the current config. This will be under configRoot + - * fs + configName Equivalent to setConfigName except its a static wrapper. - * + * fs + configName. Static wrapper around setConfigName - so it can be used in + * the same way as all the other common static service methods + * * @param configName - * @return + * - config dir name under data/config/{config} + * @return configName */ public static String setConfig(String configName) { Runtime runtime = Runtime.getInstance(); @@ -5077,13 +5099,13 @@ public void registerForInterfaceChange(String requestor, Class interestedInte /** * Builds the requestedAttachMatrix which is a mapping between new types and * their requested interfaces - interfaces they are interested in. - * + * * This data should be published whenever new "Type" definitions are found - * + * * @param targetedInterface * - interface this add new interface to requested interfaces - add * current names of services which fulfill that interface "IS ASKING" - * + * */ public void registerForInterfaceChange(String targetedInterface) { // boolean changed @@ -5144,7 +5166,7 @@ private void savePlanInternal(String configName) { /** * Published whenever a new service type definition if found - * + * * @return */ public Map> publishInterfaceTypeMatrix() { @@ -5167,7 +5189,7 @@ static public Plan saveDefault(String className) { /** * Helper method - returns if a service is started - * + * * @param name * - name of service * @return - true if started @@ -5213,7 +5235,6 @@ public static void loadConfigPath(String configPath) { for (File f : configFiles) { if (!f.getName().toLowerCase().endsWith(".yml")) { log.info("{} - none yml file found in config set", f.getAbsolutePath()); - continue; } else { runtime.loadFile(f.getAbsolutePath()); } @@ -5226,9 +5247,6 @@ public static void loadConfigPath(String configPath) { * @param path * The full path of the file to load - this DOES NOT set the * configPath - * @param overwrite - * loading the file should overwrite any current service in the plan - * - but not change the global configPath */ public void loadFile(String path) { try { @@ -5247,31 +5265,22 @@ public void loadFile(String path) { } final public Plan getDefault(String name, String type) { - return ServiceConfig.getDefault(Runtime.getPlan(), name, type); + return ServiceConfig.getDefault(new Plan("runtime"), name, type); } final public Plan saveDefault(String name, String type) { - return saveDefault(CONFIG_ROOT + fs + name, name, type, false); + return saveDefault(name, name, type, false); } final public Plan saveDefault(String name, String type, boolean fullPlan) { - return saveDefault(CONFIG_ROOT + fs + name, name, type, fullPlan); - } - - final public Plan saveDefault(String configPath, String name, String type, boolean fullPlan) { - // Runtime.getPlan() - // File resourceDir = new File(configPath); - // try { - //// for (File file : resourceDir.listFiles()) { - //// if (file.getName().endsWith(".yml")) { - //// file.delete(); - //// } - //// } - // } catch (Exception e) { - // log.error("here", e); - // } + return saveDefault(name, name, type, fullPlan); + } + + final public Plan saveDefault(String configName, String name, String type, boolean fullPlan) { + Plan plan = ServiceConfig.getDefault(new Plan(name), name, type); - // for (String service : plan.getConfig().keySet()) { + String configPath = CONFIG_ROOT + fs + configName; + if (!fullPlan) { try { String filename = configPath + fs + name + ".yml"; @@ -5293,11 +5302,8 @@ final public Plan saveDefault(String configPath, String name, String type, boole } catch (IOException e) { error(e); } - } - } - // } return plan; } @@ -5324,4 +5330,10 @@ public String getConfigPath() { return CONFIG_ROOT + fs + configName; } + @Override + public RuntimeConfig getConfig() { + config = super.getConfig(); + return config; + } + } diff --git a/src/main/java/org/myrobotlab/service/Sabertooth.java b/src/main/java/org/myrobotlab/service/Sabertooth.java index 3173c767af..a022a347eb 100644 --- a/src/main/java/org/myrobotlab/service/Sabertooth.java +++ b/src/main/java/org/myrobotlab/service/Sabertooth.java @@ -9,7 +9,6 @@ import org.myrobotlab.logging.Logging; import org.myrobotlab.service.abstracts.AbstractMotorController; import org.myrobotlab.service.config.SabertoothConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.MotorControl; import org.myrobotlab.service.interfaces.PortConnector; import org.myrobotlab.service.interfaces.SerialDevice; @@ -30,7 +29,7 @@ * @author GroG * */ -public class Sabertooth extends AbstractMotorController implements PortConnector { +public class Sabertooth extends AbstractMotorController implements PortConnector { private static final long serialVersionUID = 1L; @@ -300,8 +299,8 @@ public void detach() { } @Override - public ServiceConfig getConfig() { - SabertoothConfig config = (SabertoothConfig)super.getConfig(); + public SabertoothConfig getConfig() { + super.getConfig(); // FIXME - remove fields and use config only config.port = getSerialPort(); config.connect = isConnected; @@ -309,11 +308,11 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - SabertoothConfig config = (SabertoothConfig) super.apply(c); - if (config.connect) { + public SabertoothConfig apply(SabertoothConfig c) { + super.apply(c); + if (c.connect) { try { - connect(config.port); + connect(c.port); } catch (Exception e) { error(e); } diff --git a/src/main/java/org/myrobotlab/service/Security.java b/src/main/java/org/myrobotlab/service/Security.java index 811ee0d51a..0219c7ca53 100644 --- a/src/main/java/org/myrobotlab/service/Security.java +++ b/src/main/java/org/myrobotlab/service/Security.java @@ -47,13 +47,14 @@ import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.AuthorizationProvider; import org.myrobotlab.service.interfaces.KeyConsumer; import org.slf4j.Logger; // controlling export is "nice" but its control messages are the most important to mediate -public class Security extends Service implements AuthorizationProvider { +public class Security extends Service implements AuthorizationProvider { protected Set serviceKeyNames = new HashSet<>(); @@ -772,4 +773,16 @@ public void addServiceKeyNames(String[] keyNamesIn) { } } + @Override + public ServiceConfig apply(ServiceConfig c) { + super.apply(c); + config = c; + return config; + } + + @Override + public ServiceConfig getConfig() { + return config; + } + } diff --git a/src/main/java/org/myrobotlab/service/SegmentDisplay.java b/src/main/java/org/myrobotlab/service/SegmentDisplay.java index 2934846d96..9bad17635e 100644 --- a/src/main/java/org/myrobotlab/service/SegmentDisplay.java +++ b/src/main/java/org/myrobotlab/service/SegmentDisplay.java @@ -5,9 +5,10 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; -public class SegmentDisplay extends Service { +public class SegmentDisplay extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/SensorMonitor.java b/src/main/java/org/myrobotlab/service/SensorMonitor.java index f2dfd3b02c..ed8e79b25b 100644 --- a/src/main/java/org/myrobotlab/service/SensorMonitor.java +++ b/src/main/java/org/myrobotlab/service/SensorMonitor.java @@ -36,6 +36,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.Pin; import org.myrobotlab.service.data.Trigger; import org.slf4j.Logger; @@ -47,7 +48,7 @@ * would be triggered if a sensor goes above or below some threshold. * */ -public class SensorMonitor extends Service { +public class SensorMonitor extends Service { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Serial.java b/src/main/java/org/myrobotlab/service/Serial.java index bf93bea2d9..af16f3522a 100644 --- a/src/main/java/org/myrobotlab/service/Serial.java +++ b/src/main/java/org/myrobotlab/service/Serial.java @@ -33,7 +33,6 @@ import org.myrobotlab.serial.PortStream; import org.myrobotlab.serial.SerialControl; import org.myrobotlab.service.config.SerialConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.PortConnector; import org.myrobotlab.service.interfaces.PortPublisher; import org.myrobotlab.service.interfaces.QueueSource; @@ -47,7 +46,7 @@ * Serial - a service that allows reading and writing to a serial port device. * */ -public class Serial extends Service implements SerialControl, QueueSource, SerialDataListener, RecordControl, SerialDevice, PortPublisher, PortConnector { +public class Serial extends Service implements SerialControl, QueueSource, SerialDataListener, RecordControl, SerialDevice, PortPublisher, PortConnector { /** * general read timeout - 0 is infinite > 0 is number of milliseconds to @@ -1294,21 +1293,21 @@ public void stopTcpServer() throws IOException { } @Override - public ServiceConfig getConfig() { - SerialConfig config = (SerialConfig) super.getConfig(); + public SerialConfig getConfig() { + super.getConfig(); // FIXME remove fields and use config only config.port = lastPortName; return config; } @Override - public ServiceConfig apply(ServiceConfig c) { - SerialConfig config = (SerialConfig) super.apply(c); + public SerialConfig apply(SerialConfig c) { + super.apply(c); - if (config.port != null) { + if (c.port != null) { try { if (isConnected()) { - connect(config.port); + connect(c.port); } } catch (Exception e) { log.error("load connecting threw", e); diff --git a/src/main/java/org/myrobotlab/service/SerialRelay.java b/src/main/java/org/myrobotlab/service/SerialRelay.java index 7f0f371e44..6c2d97a7fd 100644 --- a/src/main/java/org/myrobotlab/service/SerialRelay.java +++ b/src/main/java/org/myrobotlab/service/SerialRelay.java @@ -11,13 +11,14 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.SerialRelayData; import org.myrobotlab.service.interfaces.SerialDataListener; import org.myrobotlab.service.interfaces.SerialDevice; import org.myrobotlab.service.interfaces.SerialRelayListener; import org.slf4j.Logger; -public class SerialRelay extends Service implements SerialDevice, Attachable { +public class SerialRelay extends Service implements SerialDevice, Attachable { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Servo.java b/src/main/java/org/myrobotlab/service/Servo.java index 7969d62b8f..3e0b46ce95 100644 --- a/src/main/java/org/myrobotlab/service/Servo.java +++ b/src/main/java/org/myrobotlab/service/Servo.java @@ -25,18 +25,14 @@ package org.myrobotlab.service; -import java.util.HashSet; import java.util.Set; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.math.MapperLinear; -import org.myrobotlab.sensor.EncoderData; import org.myrobotlab.sensor.TimeEncoder; import org.myrobotlab.service.abstracts.AbstractServo; import org.myrobotlab.service.config.ServiceConfig; -import org.myrobotlab.service.config.ServiceConfig.Listener; import org.myrobotlab.service.config.ServoConfig; import org.myrobotlab.service.data.ServoMove; import org.myrobotlab.service.interfaces.ServiceLifeCycleListener; @@ -65,7 +61,7 @@ * */ -public class Servo extends AbstractServo implements ServiceLifeCycleListener { +public class Servo extends AbstractServo implements ServiceLifeCycleListener { private static final long serialVersionUID = 1L; @@ -87,6 +83,17 @@ public Servo(String n, String id) { */ @Override protected boolean processMove(Double newPos, boolean blocking, Long timeoutMs) { + if (newPos == null) { + log.info("servo processMove(null) not valid position"); + return false; + } + + double minLimit = Math.min(mapper.minX, mapper.maxX); + double maxLimit = Math.max(mapper.minX, mapper.maxX); + newPos = (newPos < minLimit) ? minLimit : newPos; + newPos = (newPos > maxLimit) ? maxLimit : newPos; + + log.debug("{} processMove {}", getName(), newPos); // This is to allow attaching disabled // then delay enabling until the first moveTo command @@ -96,7 +103,7 @@ protected boolean processMove(Double newPos, boolean blocking, Long timeoutMs) { firstMove = false; } - if (autoDisable && !enabled) { + if (config.autoDisable && !enabled) { // if the servo was disable with a timer - re-enable it enable(); } @@ -174,22 +181,22 @@ protected boolean processMove(Double newPos, boolean blocking, Long timeoutMs) { // movement // usually knowing about encoder type is "bad" but the timer encoder is the // default native encoder - Long blockingTimeMs = null; + long blockingTimeMs = 0; if (encoder != null && encoder instanceof TimeEncoder) { TimeEncoder timeEncoder = (TimeEncoder) encoder; // calculate trajectory calculates and processes this move - // blockingTimeMs = timeEncoder.calculateTrajectory(getCurrentOutputPos(), - // getTargetOutput(), getSpeed()); blockingTimeMs = timeEncoder.calculateTrajectory(getCurrentInputPos(), getTargetPos(), getSpeed()); } if (isBlocking) { // our thread did a blocking call - we will wait until encoder notifies us - // to continue or timeout (if supplied) has been reached + // to continue or timeout (if supplied) has been reached - "cheesy" need + // to + // re-work for real monitor callbacks from real encoders sleep(blockingTimeMs); isBlocking = false; isMoving = false; - if (autoDisable) { + if (config.autoDisable) { // and start our countdown addTaskOneShot(idleTimeout, "disable"); } @@ -202,115 +209,12 @@ public void enableAutoDisable(boolean value) { setAutoDisable(value); } - @Override - public ServiceConfig getConfig() { - - ServoConfig config = (ServoConfig) super.getConfig(); - - config.autoDisable = autoDisable; - config.enabled = enabled; - - if (mapper != null) { - config.clip = mapper.isClip(); - config.maxIn = mapper.getMaxX(); - config.maxOut = mapper.getMaxY(); - config.minIn = mapper.getMinX(); - config.minOut = mapper.getMinY(); - config.inverted = mapper.isInverted(); - } - - // FIXME remove members and use config only - config.idleTimeout = idleTimeout; - config.pin = pin; - config.rest = rest; - config.speed = speed; - config.sweepMax = sweepMax; - config.sweepMin = sweepMin; - - config.controller = this.controller; - - if (syncedServos.size() > 0) { - config.synced = new String[syncedServos.size()]; - int i = 0; - for (String s : syncedServos) { - config.synced[i] = s; - ++i; - } - } - - return config; - } - - @Override - public ServiceConfig apply(ServiceConfig c) { - ServoConfig config = (ServoConfig) super.apply(c); - - autoDisable = config.autoDisable; - - // important - if starting up - // and autoDisable - then the assumption at this point - // is it is currently disabled, otherwise it will take - // a move to disable - if (config.autoDisable) { - disable(); - } - if (config.minIn != null && config.maxIn != null && config.minOut != null && config.maxOut != null) { - mapper = new MapperLinear(config.minIn, config.maxIn, config.minOut, config.maxOut); - } - mapper.setInverted(config.inverted); - mapper.setClip(config.clip); - enabled = config.enabled; - if (config.idleTimeout != null) { - idleTimeout = config.idleTimeout; - } - pin = config.pin; - - speed = config.speed; - sweepMax = config.sweepMax; - sweepMin = config.sweepMin; - - if (config.synced != null) { - syncedServos.clear(); - for (String s : config.synced) { - syncedServos.add(s); - } - } - - // rest = config.rest; - if (config.rest != null) { - rest = config.rest; - targetPos = config.rest; - // currentInputP = mapper.calcOutput(config.rest); - currentInputPos = config.rest; - broadcast("publishEncoderData", new EncoderData(getName(), pin, config.rest, config.rest)); - } - - if (config.controller != null) { - try { - attach(config.controller); - } catch (Exception e) { - error(e); - } - } - - return c; - } - @Override public ServiceConfig getFilteredConfig() { ServoConfig sc = (ServoConfig) super.getFilteredConfig(); - Set removeList = new HashSet<>(); + Set removeList = Set.of("onServoEnable", "onServoDisable", "onEncoderData", "onServoSetSpeed", "onServoWriteMicroseconds", "onServoMoveTo", "onServoStop"); if (sc.listeners != null) { - for (Listener listener : sc.listeners) { - if (listener.callback.equals("onServoEnable") || listener.callback.equals("onServoDisable") || listener.callback.equals("onEncoderData") - || listener.callback.equals("onServoSetSpeed") || listener.callback.equals("onServoWriteMicroseconds") || listener.callback.equals("onServoMoveTo") - || listener.callback.equals("onServoStop")) { - removeList.add(listener); - } - } - for (Listener remove : removeList) { - sc.listeners.remove(remove); - } + sc.listeners.removeIf(listener -> removeList.contains(listener.callback)); } return sc; } @@ -445,4 +349,5 @@ public void onStopped(String name) { public void onReleased(String name) { } + } diff --git a/src/main/java/org/myrobotlab/service/ServoMixer.java b/src/main/java/org/myrobotlab/service/ServoMixer.java index af75df320f..dc6c3f7127 100755 --- a/src/main/java/org/myrobotlab/service/ServoMixer.java +++ b/src/main/java/org/myrobotlab/service/ServoMixer.java @@ -7,77 +7,202 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.myrobotlab.codec.CodecUtils; +import org.myrobotlab.framework.Registration; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.framework.interfaces.ServiceInterface; +import org.myrobotlab.io.FileIO; +import org.myrobotlab.kinematics.Gesture; +import org.myrobotlab.kinematics.GesturePart; import org.myrobotlab.kinematics.Pose; -import org.myrobotlab.kinematics.PoseSequence; -import org.myrobotlab.kinematics.Sequence; +import org.myrobotlab.kinematics.PoseMove; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServoMixerConfig; +import org.myrobotlab.service.interfaces.SelectListener; +import org.myrobotlab.service.interfaces.ServiceLifeCycleListener; import org.myrobotlab.service.interfaces.ServoControl; import org.slf4j.Logger; /** * ServoMixer - a service which can control multiple servos. The position of the * servos can be saved into multiple poses, and the poses in turn can be saved - * in a sequence file to play back later. + * in a gesture file to play back later. + * + * @author GroG, kwatters, others... + * */ -public class ServoMixer extends Service { +public class ServoMixer extends Service implements ServiceLifeCycleListener, SelectListener { - public final static Logger log = LoggerFactory.getLogger(InMoov2.class); + /** + * The Player plays a requested gesture, which is a sequence of Poses. + * Poses can be positions, delays, or speech. It publishes when it + * starts a gesture and when its finished with a gesture. All + * poses within the gesture are published as well. This potentially will + * provide some measure of safety if servos should not accept other input + * when doing a gesture. + * + */ + public class Player implements Runnable { + protected String gestureName = null; + protected int poseIndex = 0; + protected boolean running = false; + protected Gesture runningGesture = null; + transient private ExecutorService executor; - private static final long serialVersionUID = 1L; + private void play() { + if (runningGesture.getParts() != null) { + invoke("publishGestureStarted", gestureName); + for (int i = 0; i < runningGesture.getParts().size(); ++i) { + if (!running) { + break; + } + GesturePart sp = runningGesture.getParts().get(i); + invoke("publishPlayingGesturePart", sp); + invoke("publishPlayingGesturePartIndex", i); + switch(sp.type) { + case "Pose":{ + Pose pose = getPose(sp.name); + if (pose == null) { + warn("Pose %s not found", sp.name); + continue; + } + // move to positions + moveToPose(sp.name, pose, sp.blocking); + // TODO if stopped, get and stop all servos + + } + break; + case "Delay":{ + sleep((Integer)sp.value); + } + break; + case "Speech":{ + invoke("publishText", (String)sp.value); + } + break; + default:{ + error("do not know how to handle gesture part of type %s", sp.type); + } + } + } // poses + + invoke("publishGestureStopped", gestureName); - protected String servoMixerDirectory = getDataDir() + fs + "poses"; + } + running = false; + } - /** - * set autoDisable on "all" servos .. true - will make all servos autoDisable - * false - will make all servos autoDisable false null - will make no changes - */ - protected Boolean autoDisable = null;; + @Override + public void run() { + try { + running = true; + if (runningGesture.getRepeat()) { + while (running) { + play(); + } + } else { + play(); + } + } catch (Exception e) { + error(e); + } + running = false; + } + + public void start(String name, Gesture seq) { + gestureName = name; + runningGesture = seq; + poseIndex++; + executor = Executors.newSingleThreadExecutor(); + executor.execute(this::run); + } + + public void stop() { + if (executor != null) { + executor.shutdownNow(); + } + running = false; + invoke("publishGestureStopped", gestureName); + } + } + + public final static Logger log = LoggerFactory.getLogger(InMoov2.class); + + + private static final long serialVersionUID = 1L; + /** - * sequence player + * Set of servo names kept in sync with current registry */ - protected transient Player player = new Player(); + protected Set allServos = new TreeSet<>(); + /** - * Set of name kept in sync with current registry + * gesture player */ - protected TreeSet allServos = new TreeSet<>(); + final protected transient Player player = new Player(); + public ServoMixer(String n, String id) { super(n, id); } + /** + * Explicitly saving a new gesture file. This will error if the + * file already exists. The gesture part moves will be empty. + * @param filename + * @return + */ + public String addNewGestureFile(String filename) { + if (filename == null) { + error("filename cannot be null"); + return null; + } + if (!filename.toLowerCase().endsWith(".yml")) { + filename += ".yml"; + } + if (FileIO.checkFile(filename)) { + error("file %s already exists", filename); + return null; + } + saveGesture(filename, new Gesture()); + return filename; + } + + /** + * general interface attach + */ + @Override + public void attach(Attachable attachable) { + if (attachable instanceof Servo) { + attachServo((Servo) attachable); + } + }; + /** * name attach "the best" */ @Override public void attach(String name) { + ServoMixerConfig c = (ServoMixerConfig)config; // FIXME - check type in registry, describe, or query ... make sure Servo // type.. // else return error - should be type checking ServiceInterface si = Runtime.getService(name); if (si != null & "Servo".equals(si.getSimpleName())) { Servo servo = (Servo) Runtime.getService(name); - if (autoDisable != null) { - servo.setAutoDisable(autoDisable); + if (c.autoDisable) { + servo.setAutoDisable(true); } allServos.add(name); - } - } - - /** - * general interface attach - */ - @Override - public void attach(Attachable attachable) { - if (attachable instanceof Servo) { - attachServo((Servo) attachable); + } else { + log.info("do not know how to attach {}", name); } } @@ -92,77 +217,45 @@ public void attachServo(Servo servo) { attach(servo.getName()); } - /** - * Part of service life cycle - a new servo has been started - */ - public void onStarted(String name) { + public Gesture getGesture(String name) { + ServoMixerConfig c = (ServoMixerConfig)config; + Gesture gesture = null; try { - attach(name); + if (!FileIO.checkDir(c.gesturesDir)) { + error("invalid poses directory %s", c.gesturesDir); + return null; + } + String filename = new File(c.gesturesDir).getAbsolutePath() + File.separator + name + ".yml"; + log.info("Loading Pose name {}", filename); + gesture = CodecUtils.fromYaml(FileIO.toString(filename), Gesture.class); + return gesture; } catch (Exception e) { - log.error("onStarted threw", e); + error(e); } + return gesture; } - /** - * Part of service life cycle - a servo has been removed from the system - */ - public void onReleased(String name) { - allServos.remove(name); - } + public List getGestureFiles() { + ServoMixerConfig c = (ServoMixerConfig)config; - public List listAllServos() { - ArrayList servos = new ArrayList(); - // TODO: get a list of all servos - for (ServiceInterface service : Runtime.getServices()) { - if (service instanceof ServoControl) { - servos.add((ServoControl) service); - } + List files = new ArrayList<>(); + if (!FileIO.checkDir(c.gesturesDir)) { + error("gestures %s directory does not exist", c.gesturesDir); + return files; } - return servos; - } - /** - * Save a {name}.pose file to the current poses directory. - * - * @param name - * name to save pose as - * @throws IOException - * boom - * - */ - public void savePose(String name) throws IOException { - savePose(name, null); - } - - public void savePose(String name, List servos) throws IOException { - - if (servos == null) { - servos = listAllServos(); + File dir = new File(c.gesturesDir); + File[] all = dir.listFiles(); + Set sorted = new TreeSet<>(); + for (File f : all) { + if (f.getName().toLowerCase().endsWith(".yml")) { + sorted.add(f.getName().substring(0, f.getName().lastIndexOf("."))); + } } - - File poseDirectory = new File(servoMixerDirectory); - poseDirectory.mkdirs(); - - log.info("Saving pose name {}", name); - Pose p = new Pose(name, servos); - String filename = poseDirectory.getAbsolutePath() + File.separator + name + ".pose"; - p.savePose(filename); - broadcast("getPoseFiles"); - } - - private boolean checkDir(String dir) { - try { - File check = new File(dir); - return check.exists(); - } catch (Exception e) { - error(e); + for (String s : sorted) { + files.add(s); } - return false; - } - - @Deprecated /* use getPose(name) */ - public Pose loadPose(String name) { - return getPose(name); + return files; } /** @@ -175,305 +268,346 @@ public Pose loadPose(String name) { * @return the loaded pose object */ public Pose getPose(String name) { - Pose pose = null; + try { - if (!checkDir(servoMixerDirectory)) { - error("invalid poses directory %s", servoMixerDirectory); - return null; - } - String filename = new File(servoMixerDirectory).getAbsolutePath() + File.separator + name + ".pose"; - log.info("Loading Pose name {}", filename); - pose = Pose.loadPose(filename); + ServoMixerConfig c = (ServoMixerConfig)config; + + String filename = new File(c.posesDir).getAbsolutePath() + File.separator + name + ".yml"; + log.info("loading pose name {}", filename); + String yml = FileIO.toString(filename); + return CodecUtils.fromYaml(yml, Pose.class); + // pose = Pose.loadPose(filename); // broadcastState(); "maybe too chatty" } catch (Exception e) { error(e); } - return pose; + return null; } - public void removePose(String name) { - try { - if (!checkDir(servoMixerDirectory)) { - error("invalid poses directory %s", servoMixerDirectory); - return; + public List getPoseFiles() { + ServoMixerConfig c = (ServoMixerConfig)config; + + List files = new ArrayList<>(); + if (!FileIO.checkDir(c.posesDir)) { + return files; + } + + File dir = new File(c.posesDir); + + if (!dir.exists() || !dir.isDirectory()) { + error("%s not a valid directory", c.posesDir); + return files; + } + File[] all = dir.listFiles(); + Set sorted = new TreeSet<>(); + for (File f : all) { + if (f.getName().toLowerCase().endsWith(".yml")) { + sorted.add(f.getName().substring(0, f.getName().lastIndexOf("."))); } - String filename = new File(servoMixerDirectory).getAbsolutePath() + File.separator + name + ".pose"; - File del = new File(filename); - if (del.exists()) { - del.delete(); + } + for (String s : sorted) { + files.add(s); + } + return files; + } + + public String getPosesDirectory() { + ServoMixerConfig c = (ServoMixerConfig)config; + return c.posesDir; + } + + public List listAllServos() { + ArrayList servos = new ArrayList(); + for (ServiceInterface service : Runtime.getServices()) { + if (service instanceof ServoControl) { + servos.add((ServoControl) service); } - invoke("getPoseFiles"); - } catch (Exception e) { - error(e); } + return servos; + } + + public void moveToPose(String name) throws IOException { + Pose p = getPose(name); + if (p == null) { + error("cannot find pose %s", name); + } + moveToPose(name, p, false); } - public void removeSequence(String name) { + public void moveToPose(String name, Pose p, boolean blocking) { try { - if (!checkDir(servoMixerDirectory)) { - error("invalid poses directory %s", servoMixerDirectory); + + if (p.getMoves() == null) { + error("no moves within pose file %s", name); return; } - String filename = new File(servoMixerDirectory).getAbsolutePath() + File.separator + name + ".seq"; - File del = new File(filename); - if (del.exists()) { - del.delete(); + + for (String sc : p.getMoves().keySet()) { + PoseMove pm = p.getMoves().get(sc); + ServoControl servo = (ServoControl) Runtime.getService(sc); + if (servo == null) { + warn("servo (%s) cannot move to pose because it does not exist", sc); + continue; + } + Double speed = pm.speed; + Double position = pm.position; + servo.setSpeed(speed); + if (blocking) { + servo.moveToBlocking(position); + } else { + servo.moveTo(position); + } } - invoke("getSequenceFiles"); + invoke("publishStopPose", name); } catch (Exception e) { error(e); } } - public Sequence getSequence(String name) { - Sequence pose = null; - try { - if (!checkDir(servoMixerDirectory)) { - error("invalid poses directory %s", servoMixerDirectory); - return null; - } - String filename = new File(servoMixerDirectory).getAbsolutePath() + File.separator + name + ".seq"; - log.info("Loading Pose name {}", filename); - pose = Sequence.loadSequence(filename); - broadcastState(); - } catch (Exception e) { - error(e); - } - return pose; + @Override + public void onCreated(String name) { + } + + @Override + public void onRegistered(Registration registration) { } /** - * Export Python representation of a sequence - * - * @param name - * does nothing.. - * @return null - * + * Part of service life cycle - a servo has been removed from the system */ - public String exportSequence(String name) { - return null; + @Override + public void onReleased(String name) { + allServos.remove(name); } - public class Player implements Runnable { - Thread thread = null; - Sequence runningSeq = null; - int seqCnt = 0; - boolean running = false; - - @Override - public void run() { - try { - running = true; - if (runningSeq.cycle) { - while (running) { - play(); - } - } else { - play(); - } - } catch (Exception e) { - } - running = false; - } + /** + * handles incoming selected events and publishes + * search events + * @param selected + */ + @Override + public void onSelected(String selected) { + invoke("search", selected); + } - public void stop() { - running = false; + /** + * Part of service life cycle - a new servo has been started + */ + @Override + public void onStarted(String name) { + try { + attach(name); + } catch (Exception e) { + log.error("onStarted threw", e); } + } - private void play() { - if (runningSeq.poses != null) { - for (PoseSequence ps : runningSeq.poses) { - Pose pose = getPose(ps.name); - if (pose == null) { - warn("Pose %s not found", ps.name); - continue; - } - if (ps.waitTimeMs != null) { - sleep(ps.waitTimeMs); - } - // move to positions - Pose p = getPose(ps.name); - moveToPose(p); - } // poses - } - } + @Override + public void onStopped(String name) { + } - public void start(Sequence seq) { - runningSeq = seq; - seqCnt++; - thread = new Thread(this, String.format("%s-player-%d", getName(), seqCnt)); - thread.start(); - } + public void playGesture(String name) { + // Gesture gesture = (Gesture) broadcast("getGesture", name); + invoke("getGesture", name); + Gesture gesture = getGesture(name); + player.start(name, gesture); } - public String publishPlayingPose(String name) { + /** + * When a gesture starts its name will be published + * @param name + * @return + */ + public String publishGestureStarted(String name) { return name; } - public String publishStopPose(String name) { + /** + * When a gesture stops its name will be published + * @param name + * @return + */ + public String publishGestureStopped(String name) { return name; } - public void playSequence(String name) { - Sequence seq = (Sequence) broadcast("getSequence", name); - player.start(seq); + /** + * Current gesture part being processed by player + * @param sp + * @return + */ + public GesturePart publishPlayingGesturePart(GesturePart sp) { + return sp; } - public void moveToPose(Pose p) { - try { - invoke("publishPlayingPose", p.name); - for (String sc : p.getPositions().keySet()) { - ServoControl servo = (ServoControl) Runtime.getService(sc); - if (servo == null) { - warn("servo (%s) cannot move to pose because it does not exist", sc); - continue; - } - Double speed = p.getSpeeds().get(sc); - Double position = p.getPositions().get(sc); - servo.setSpeed(speed); - // servo.broadcastState(); WAY TOO CHATTY - // servo.moveToBlocking(position); // WOAH - sequential movements :P - servo.moveTo(position); - } - invoke("publishStopPose", p.name); - } catch (Exception e) { - error(e); - } + /** + * Current index of gesture being played + * @param i + * @return + */ + public int publishPlayingGesturePartIndex(int i) { + return i; } - public void moveToPose(String name) throws IOException { - Pose p = loadPose(name); - moveToPose(p); + public String publishPlayingPose(String name) { + return name; } - public String getPosesDirectory() { - return servoMixerDirectory; + public String publishStopPose(String name) { + return name; } - public void setPosesDirectory(String posesDirectory) { - File dir = new File(posesDirectory); - if (!dir.exists()) { - dir.mkdirs(); - } - this.servoMixerDirectory = posesDirectory; - invoke("getPoseFiles"); - broadcastState(); + /** + * Speech publishes here - a SpeechSynthesis service could subscribe, + * or ProgramAB + * @param text + * @return + */ + public String publishText(String text) { + return text; } - public List getPoseFiles() { + public void removeGesture(String name) { + ServoMixerConfig c = (ServoMixerConfig)config; - List files = new ArrayList<>(); - if (!checkDir(servoMixerDirectory)) { - return files; - } - - File dir = new File(servoMixerDirectory); - - if (!dir.exists() || !dir.isDirectory()) { - error("%s not a valid directory", servoMixerDirectory); - return files; - } - File[] all = dir.listFiles(); - Set sorted = new TreeSet<>(); - for (File f : all) { - if (f.getName().toLowerCase().endsWith(".pose")) { - sorted.add(f.getName().substring(0, f.getName().lastIndexOf("."))); + try { + if (!FileIO.checkDir(c.gesturesDir)) { + error("invalid poses directory %s", c.gesturesDir); + return; } + String filename = new File(c.gesturesDir).getAbsolutePath() + File.separator + name + ".yml"; + File del = new File(filename); + if (del.exists()) { + del.delete(); + } + invoke("getGestureFiles"); + } catch (Exception e) { + error(e); } - for (String s : sorted) { - files.add(s); - } - return files; } - public List getSequenceFiles() { - - List files = new ArrayList<>(); - if (!checkDir(servoMixerDirectory)) { - return files; - } - - File dir = new File(servoMixerDirectory); + public void removePose(String name) { + try { + ServoMixerConfig c = (ServoMixerConfig)config; - if (!dir.exists() || !dir.isDirectory()) { - error("%s not a valid directory", servoMixerDirectory); - return files; - } - File[] all = dir.listFiles(); - Set sorted = new TreeSet<>(); - for (File f : all) { - if (f.getName().toLowerCase().endsWith(".seq")) { - sorted.add(f.getName().substring(0, f.getName().lastIndexOf("."))); + if (!FileIO.checkDir(c.posesDir)) { + error("invalid poses directory %s", c.posesDir); + return; + } + String filename = new File(c.posesDir).getAbsolutePath() + File.separator + name + ".yml"; + File del = new File(filename); + if (del.exists()) { + del.delete(); + } else { + error("could not delete file %s", filename); } + invoke("getPoseFiles"); + } catch (Exception e) { + error(e); } - for (String s : sorted) { - files.add(s); + } + + public void rest() { + for(String servo : allServos) { + Servo s = (Servo)Runtime.getService(servo); + s.rest(); } - return files; } /** - * Takes name of a file and a json encoded string of a sequence, saves it to - * file and sets the "current" sequence to the data + * Takes name of a file and a json encoded string of a gesture, saves it to + * file and sets the "current" gesture to the data * * @param filename - * the filename to save the sequence as + * the filename to save the gesture as * @param json * the json to save - * */ - public void saveSequence(String filename, String json) { + public void saveGesture(String filename, Gesture gesture) { try { + ServoMixerConfig c = (ServoMixerConfig)config; + if (filename == null) { - error("save sequence file name cannot be null"); + error("save gesture file name cannot be null"); return; } - if (json == null) { - error("sequence json cannot be null"); + if (gesture == null) { + error("gesture json cannot be null"); return; } - if (!filename.toLowerCase().endsWith(".seq")) { - filename += ".seq"; + if (!filename.toLowerCase().endsWith(".yml")) { + filename += ".yml"; } - Sequence seq = CodecUtils.fromJson(json, Sequence.class); - if (seq != null) { - String path = servoMixerDirectory + fs + filename; + // Gesture seq = CodecUtils.fromJson(json, Gesture.class); + + if (gesture != null) { + String path = c.gesturesDir + fs + filename; FileOutputStream fos = new FileOutputStream(path); - fos.write(CodecUtils.toPrettyJson(seq).getBytes()); + fos.write(CodecUtils.toYaml(gesture).getBytes()); fos.close(); - } - invoke("getSequenceFiles"); + } + invoke("getGestureFiles"); } catch (Exception e) { error(e); } } + + /** + * Save a {name}.yml file to the current poses directory. + * + * @param name + * name to save pose as + * @throws IOException + * boom + * + */ + public void savePose(String name) throws IOException { + savePose(name, allServos); + } + + public void savePose(String name, Set servos) throws IOException { + ServoMixerConfig c = (ServoMixerConfig)config; - @Override - public void startService() { - try { - List all = Runtime.getServiceNamesFromInterface(ServoControl.class); - for (String sc : all) { - attach(sc); - } - - File poseDirectory = new File(servoMixerDirectory); - if (!poseDirectory.exists()) { - poseDirectory.mkdirs(); + if (servos == null) { + log.error("cannot save %s null servos"); + return; + } + + Pose pose = new Pose(); + + for (String servo : servos) { + ServoControl sc = (ServoControl)Runtime.getService(servo); + if (sc == null) { + error("servo %s null", name); + continue; + } else { + pose.getMoves().put(CodecUtils.getShortName(servo), new PoseMove(sc.getCurrentInputPos(), sc.getSpeed())); } - super.startService(); - } catch (Exception e) { - error(e); } + + log.info("saving pose name {}", name); + + String filename = c.posesDir + File.separator + name + ".yml"; + FileIO.toFile(filename, CodecUtils.toYaml(pose).getBytes()); + invoke("getPoseFiles"); + } + + /** + * publishing a search to allow UI searching based on other + * service selections + * @param servos + * @return + */ + public String search(String servos) { + return servos; } - public void setAutoDisable(Boolean b) { - this.autoDisable = b; - if (b == null) { - return; - } + public void setAutoDisable(boolean b) { + ServoMixerConfig c = (ServoMixerConfig)config; + c.autoDisable = b; if (b) { List servos = Runtime.getServiceNamesFromInterface(Servo.class); for (String name : servos) { @@ -489,8 +623,54 @@ public void setAutoDisable(Boolean b) { } } - public Boolean getAudoDisable() { - return autoDisable; + public void setPosesDirectory(String posesDirectory) { + ServoMixerConfig c = (ServoMixerConfig)config; + + File dir = new File(posesDirectory); + if (!dir.exists()) { + dir.mkdirs(); + } + c.posesDir = posesDirectory; + invoke("getPoseFiles"); + broadcastState(); + } + + @Override + public void startService() { + try { + ServoMixerConfig c = (ServoMixerConfig)config; + + new File(c.posesDir).mkdirs(); + new File(c.gesturesDir).mkdirs(); + + Runtime.getInstance().attachServiceLifeCycleListener(getName()); + + List all = Runtime.getServiceNamesFromInterface(ServoControl.class); + for (String sc : all) { + attach(sc); + } + + File poseDirectory = new File(c.posesDir); + if (!poseDirectory.exists()) { + poseDirectory.mkdirs(); + } + super.startService(); + } catch (Exception e) { + error(e); + } + } + + /** + * stop the current running gesture + */ + public void stop() { + player.stop(); + } + + @Override + public void stopService() { + super.stopService(); + player.stop(); } public static void main(String[] args) throws Exception { @@ -510,4 +690,5 @@ public static void main(String[] args) throws Exception { log.error("main threw", e); } } + } diff --git a/src/main/java/org/myrobotlab/service/Shoutbox.java b/src/main/java/org/myrobotlab/service/Shoutbox.java index 0376e96665..a567ae073e 100644 --- a/src/main/java/org/myrobotlab/service/Shoutbox.java +++ b/src/main/java/org/myrobotlab/service/Shoutbox.java @@ -28,10 +28,11 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.programab.Response; import org.myrobotlab.service.Xmpp.XmppMsg; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; // FIXME - use Peers ! -public class Shoutbox extends Service { +public class Shoutbox extends Service { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(Shoutbox.class); diff --git a/src/main/java/org/myrobotlab/service/SlackBot.java b/src/main/java/org/myrobotlab/service/SlackBot.java index 7d61214f9d..990fc21855 100755 --- a/src/main/java/org/myrobotlab/service/SlackBot.java +++ b/src/main/java/org/myrobotlab/service/SlackBot.java @@ -26,7 +26,7 @@ * A slack bot gateway for utterance publishers and listeners. * */ -public class SlackBot extends Service implements UtteranceListener, UtterancePublisher { +public class SlackBot extends Service implements UtteranceListener, UtterancePublisher { public final static Logger log = LoggerFactory.getLogger(SlackBot.class); @@ -43,8 +43,8 @@ public SlackBot(String reservedKey, String inId) { } @Override - public ServiceConfig getConfig() { - SlackBotConfig config = (SlackBotConfig)super.getConfig(); + public SlackBotConfig getConfig() { + super.getConfig(); // FIXME remove members and use config only config.appToken = appToken; config.botToken = botToken; @@ -52,7 +52,7 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { + public SlackBotConfig apply(SlackBotConfig c) { SlackBotConfig config = (SlackBotConfig) super.apply(c); appToken = config.appToken; botToken = config.botToken; diff --git a/src/main/java/org/myrobotlab/service/Solr.java b/src/main/java/org/myrobotlab/service/Solr.java index 753c6eb0ed..a388b034a8 100644 --- a/src/main/java/org/myrobotlab/service/Solr.java +++ b/src/main/java/org/myrobotlab/service/Solr.java @@ -1,8 +1,5 @@ package org.myrobotlab.service; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -10,15 +7,13 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import javax.imageio.ImageIO; - -import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; @@ -31,10 +26,8 @@ import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.core.CoreContainer; -import org.bytedeco.javacv.Java2DFrameConverter; -import org.bytedeco.javacv.OpenCVFrameConverter; -import org.bytedeco.javacv.OpenCVFrameConverter.ToIplImage; import org.bytedeco.opencv.opencv_core.IplImage; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.document.Document; import org.myrobotlab.document.ProcessingStatus; import org.myrobotlab.framework.Inbox; @@ -43,15 +36,17 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.MessageListener; import org.myrobotlab.framework.interfaces.ServiceInterface; +import org.myrobotlab.image.Util; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.opencv.CloseableFrameConverter; import org.myrobotlab.opencv.OpenCVData; import org.myrobotlab.opencv.YoloDetectedObject; import org.myrobotlab.programab.Response; +import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.service.config.SolrConfig; import org.myrobotlab.service.interfaces.DocumentListener; import org.myrobotlab.service.interfaces.SpeechRecognizer; import org.myrobotlab.service.interfaces.TextListener; @@ -71,7 +66,8 @@ * @author kwatters * */ -public class Solr extends Service implements DocumentListener, TextListener, MessageListener { +public class Solr extends Service implements DocumentListener, TextListener, MessageListener { + private static final String CORE_NAME = "core1"; public final static Logger log = LoggerFactory.getLogger(Solr.class); @@ -83,6 +79,10 @@ public class Solr extends Service implements DocumentListener, TextListener, Mes public String solrHome = "Solr"; // EmbeddedSolrServer embeddedSolrServer = null; transient private EmbeddedSolrServer embeddedSolrServer = null; + + Collection documentBatch = Collections.synchronizedCollection(new ArrayList<>()); + // The batch size of documents to accumulate before flushing the batch to solr. + public transient int batchSize = 100; // TODO: consider moving this tagging logic into opencv.. // for now, we'll just set a counter that will count down how many opencv // frames @@ -133,13 +133,17 @@ public void deleteEmbeddedIndex() throws SolrServerException, IOException { * @throws IOException * boom */ - public void startEmbedded(String path) throws SolrServerException, IOException { + public synchronized void startEmbedded(String path) throws SolrServerException, IOException { // let's extract our default configs into the directory/ // FileIO.extract(Util.getResourceDir() , "Solr/core1", path); // FileIO.extract(Util.getResourceDir() , "Solr/solr.xml", path + // File.separator + "solr.xml"); // load up the solr core container and start solr - + if (embeddedSolrServer != null) { + log.info("Embedded solr already running."); + return; + } + // FIXME - a bit unsatisfactory File f = new File(getDataInstanceDir()); f.mkdirs(); @@ -151,12 +155,13 @@ public void startEmbedded(String path) throws SolrServerException, IOException { Path solrHome = Paths.get(path); log.info(solrHome.toFile().getAbsolutePath()); Path solrXml = solrHome.resolve("solr.xml"); - + String absolueHome = solrHome.toFile().getAbsolutePath(); CoreContainer cores = CoreContainer.createAndLoad(Paths.get(absolueHome), solrXml); for (String coreName : cores.getAllCoreNames()) { log.info("Found core core {}", coreName); } + // create the actual solr instance with core1 embeddedSolrServer = new EmbeddedSolrServer(cores, CORE_NAME); // TODO: verify when the embedded solr server has fully started. @@ -166,43 +171,49 @@ public void startEmbedded(String path) throws SolrServerException, IOException { * Add a single document at a time to the solr server. * * @param doc - * the input doc to send to solr - * + * the input doc to send to solr (prefer to send batches with addDocuments instead) + * */ public void addDocument(SolrInputDocument doc) { - try { - if (embeddedSolrServer != null) { - embeddedSolrServer.add(doc); - } else { - solrServer.add(doc); - } - } catch (SolrServerException e) { - // TODO : retry? - log.warn("An exception occurred when trying to add document to the index.", e); - } catch (IOException e) { - // TODO : maybe retry? - log.warn("A network exception occurred when trying to add document to the index.", e); - } + // Always batch! + addDocuments(List.of(doc)); } /** - * Add a batch of documents (this is more effecient than adding one at a time. + * Add a batch of documents (this is more efficient than adding one at a time.) * * @param docs * a collection of solr input docs to add to solr. */ public void addDocuments(Collection docs) { - try { - if (embeddedSolrServer != null) { - embeddedSolrServer.add(docs); - } else { - solrServer.add(docs); + // TODO: setup a thread to flush this batch + // performance optimization.. always batch updates to solr. + documentBatch.addAll(docs); + flushDocumentBatch(false); + } + + private ProcessingStatus flushDocumentBatch(boolean forceFlush) { + synchronized (documentBatch) { + if (documentBatch.size() >= batchSize || forceFlush) { + try { + if (embeddedSolrServer != null) { + embeddedSolrServer.add(documentBatch); + } else { + solrServer.add(documentBatch); + } + // we sent the batch, so let's clear it up. + documentBatch.clear(); + } catch (SolrServerException e) { + log.warn("An exception occurred when trying to add documents to the index.", e); + // ?? + return ProcessingStatus.ERROR; + } catch (IOException e) { + log.warn("A network exception occurred when trying to add documents to the index.", e); + return ProcessingStatus.ERROR; + } } - } catch (SolrServerException e) { - log.warn("An exception occurred when trying to add documents to the index.", e); - } catch (IOException e) { - log.warn("A network exception occurred when trying to add documents to the index.", e); } + return ProcessingStatus.OK; } /** @@ -212,6 +223,9 @@ public void addDocuments(Collection docs) { * */ public void commit() { + // if we are explicitly calling a commit.. first flush any partial batch + // followed by the commit. + flushDocumentBatch(true); try { if (embeddedSolrServer != null) { embeddedSolrServer.commit(); @@ -343,7 +357,7 @@ public IplImage fetchImage(String queryString) throws IOException { // TODO: this is a byte array or is it base64? // byte[] decoded = Base64.decodeBase64((byte[])result); // read these bytes as an image. - IplImage image = bytesToImage((byte[]) result); + IplImage image = Util.bytesToImage((byte[]) result); String docId = qr.getResults().get(0).getFirstValue("id").toString(); // show(image, docId); return image; @@ -412,57 +426,6 @@ public void createTrainingDataDir(SolrQuery query, String directory) throws IOEx } } - /** - * Helper method to serialize an IplImage into a byte array. returns a png - * version of the original image - * - * @param image - * input iage - * @return byte array of image - * @throws IOException - * boom - * - */ - public byte[] imageToBytes(IplImage image) throws IOException { - - // lets make a buffered image - CloseableFrameConverter converter = new CloseableFrameConverter(); - BufferedImage buffImage = converter.toBufferedImage(image); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - try { - ImageIO.write(buffImage, "png", stream); - } catch (IOException e) { - // This *shouldn't* happen with a ByteArrayOutputStream, but if it - // somehow does happen, then we don't want to just ignore it - throw new RuntimeException(e); - } - converter.close(); - return stream.toByteArray(); - } - - /** - * deserialize from a png byte array to an IplImage - * - * @param bytes - * input bytes - * @return an iplimage - * @throws IOException - * boom - * - */ - public IplImage bytesToImage(byte[] bytes) throws IOException { - // - // let's assume we're a buffered image .. those are serializable :) - BufferedImage bufImage = ImageIO.read(new ByteArrayInputStream(bytes)); - ToIplImage iplConverter = new OpenCVFrameConverter.ToIplImage(); - Java2DFrameConverter java2dConverter = new Java2DFrameConverter(); - IplImage iplImage = iplConverter.convert(java2dConverter.convert(bufImage)); - // now convert the buffered image to ipl image - return iplImage; - // Again this could be try with resources but the original example was in - // Scala - } - /** * Helper search function that runs a search and returns a specified field * from the first result @@ -512,6 +475,7 @@ public String fetchFirstResultSentence(String queryString, String fieldName) { */ public QueryResponse search(String queryString) { // default to 10 hits returned. + // System.err.println("Here..."); return search(queryString, 10, 0, true); } @@ -551,8 +515,98 @@ public QueryResponse search(String queryString, int rows, int start, boolean mos return resp; } - public QueryResponse publishResults(QueryResponse resp) { + + /** + * Default query to fetch the top 10 documents that match the query request. + * + * @param queryString + * query string + * @param rows + * number of rows to return + * @param start + * offset into the restult + * @param facetFields + * a list of fields in which to return facets for + * @return the response + */ + public QueryResponse searchWithFacets(String queryString, int rows, int start, String[] facetFields, String[] filters) { + log.info("Searching for (with facets): {}", queryString); + int numFacetBuckets = 50; + SolrQuery query = new SolrQuery(); + query.set("q", queryString); + query.setRows(rows); + query.setStart(start); + query.setFacet(true); + query.setFacetLimit(numFacetBuckets); + query.setFacetMinCount(1); + // default search fields. + query.add("qf", "title^10"); + query.add("qf", "artist^5"); + query.add("qf", "album^2"); + query.add("qf", "genre"); + query.add("qf", "year"); + + query.setParam("defType", "edismax"); + query.setParam("q.op", "AND"); + // TODO: expose sorting in a fancier search method signature + // Alternatively, pass the list of parameters and their values into a generic search method instead. + query.setSort("index_date", ORDER.desc); + for (String field : facetFields) { + query.addFacetField(field); + } + for (String filter : (String[])filters) { + query.addFilterQuery(filter); + } + + QueryResponse resp = null; + try { + if (embeddedSolrServer != null) { + resp = embeddedSolrServer.query(query); + } else { + resp = solrServer.query(query); + } + } catch (SolrServerException | IOException e) { + log.warn("Search failed with exception", e); + } + invoke("publishResults", resp); + // invoke("publishResults"); return resp; + } + + public String publishResults(QueryResponse resp) { + // The QueryResponse object doesn't properly serialize some useful info + // from the results ( SolrDocumentList ) object, like the number found, the start offset + // So we manually copy that info to top level items in the response so the webgui + // can get at it. + // TODO: why are the facet buckets not ordered!!! + long numFound = resp.getResults().getNumFound(); + long start = resp.getResults().getStart(); + Float maxScore = resp.getResults().getMaxScore(); + Boolean numFoundExact = resp.getResults().getNumFoundExact(); + // this is lame but we have to copy some metadata around on the response + // because it doesn't serialize properly. + resp.getResponse().add("numFound", numFound); + resp.getResponse().add("start", start); + resp.getResponse().add("maxScore", maxScore); + resp.getResponse().add("numFoundExact", numFoundExact); + resp.getResponse().add("size", resp.getResults().size()); + // Now, let's convert the byte arrays to base64 image strings. + for (SolrDocument d : resp.getResults()) { + // TODO: decide what to do based on the mime type... + if (d.containsKey("bytes")) { + // encode this as base 64 image data. + // TODO: support multiple byte arrays. + byte[] bytes = (byte[])(d.getFirstValue("bytes")); + String base64jpg = Util.bytesToBase64Jpg(bytes); + d.setField("bytes", base64jpg); + } + } + + String jsonResponse = resp.jsonStr(); + // publish the json string of the response. + // System.err.println(jsonResponse); + + return jsonResponse; }; /** @@ -579,6 +633,7 @@ public void startService() { } @Override + // TODO: why do we return ProcessingStatus here?! public ProcessingStatus onDocuments(List docs) { // Convert the input document to a solr input docs and send it! if (docs.size() == 0) { @@ -587,19 +642,9 @@ public ProcessingStatus onDocuments(List docs) { } ArrayList docsToSend = new ArrayList(); for (Document d : docs) { - docsToSend.add(convertDocument(d)); - } - try { - if (embeddedSolrServer != null) { - embeddedSolrServer.add(docsToSend); - } else { - solrServer.add(docsToSend); - } - return ProcessingStatus.OK; - } catch (Exception e) { - log.warn("Exception in Solr onDocuments.", e); - return ProcessingStatus.DROP; + documentBatch.add(convertDocument(d)); } + return flushDocumentBatch(false); } private SolrInputDocument convertDocument(Document doc) { @@ -622,18 +667,15 @@ private SolrInputDocument convertDocument(Document doc) { @Override public ProcessingStatus onDocument(Document doc) { // always be batching when sending docs. - ArrayList docs = new ArrayList(); - docs.add(doc); - return onDocuments(docs); + // TODO: we want to add to the current batch to send.. + // and make sure we have a thread flushing the batch if it gets too old. + return onDocuments(List.of(doc)); } @Override public boolean onFlush() { - // NoOp currently, but at some point if we change how this service batches - // it's - // add messages to solr, we could revisit this. - // or maybe issue a commit here? I hate committing the index so frequently, - // but maybe it's ok. + // if we got a flush call, let's flush any partial batch. + flushDocumentBatch(true); if (commitOnFlush) { commit(); } @@ -650,9 +692,9 @@ public void setCommitOnFlush(boolean commitOnFlush) { // Attach Pattern stuff! public void attach(OpenCV opencv) { - opencv.addListener("publishOpenCVData", getName(), "onOpenCVData"); - opencv.addListener("publishClassification", getName(), "onClassification"); - opencv.addListener("publishYoloClassification", getName(), "onYoloClassification"); + opencv.addListener("publishOpenCVData", getName()); + opencv.addListener("publishClassification", getName()); + opencv.addListener("publishYoloClassification", getName()); } // to make it easier to call from aiml @@ -703,8 +745,8 @@ public ArrayList onYoloClassification(ArrayList { class SpeechProcessor extends Thread { Sphinx myService = null; @@ -90,29 +91,28 @@ public void run() { String newPath = FileIO.getCfgDir() + File.separator + myService.getName() + ".xml"; File localGramFile = new File(newPath); + warn("CONFIG NOT IMPLEMENTED, ONLY SUPPORTS BASE EN-US"); + info("loading grammar file"); if (localGramFile.exists()) { info(String.format("grammar config %s", newPath)); - cm = new ConfigurationManager(newPath); } else { // resource in jar default info(String.format("grammar /resource/Sphinx/simple.xml")); - cm = new ConfigurationManager(this.getClass().getResource(FileIO.gluePaths(getResourceDir(), "/Sphinx/simple.xml"))); } info("starting recognizer"); // start the word recognizer - recognizer = (Recognizer) cm.lookup("recognizer"); - recognizer.allocate(); - - info("starting microphone"); - microphone = (Microphone) cm.lookup("microphone"); - if (!microphone.startRecording()) { - log.error("Cannot start microphone."); - recognizer.deallocate(); - } +// recognizer = cm.lookup("recognizer"); + Configuration configuration = new Configuration(); + + configuration.setAcousticModelPath("resource:/edu/cmu/sphinx/models/en-us/en-us"); + configuration.setDictionaryPath("resource:/edu/cmu/sphinx/models/en-us/cmudict-en-us.dict"); + configuration.setLanguageModelPath("resource:/edu/cmu/sphinx/models/en-us/en-us.lm.bin"); + recognizer = new LiveSpeechRecognizer(configuration); + recognizer.startRecognition(true); - SpeechRecognizerConfig c = (SpeechRecognizerConfig)config; + SpeechRecognizerConfig c = config; // loop the recognition until the program exits. @@ -121,7 +121,7 @@ public void run() { info("listening: %b", c.listening); invoke("listeningEvent", true); - Result result = recognizer.recognize(); + Result result = recognizer.getResult().getResult(); if (!c.listening) { // we could have stopped listening @@ -220,7 +220,7 @@ public void run() { public final static Logger log = LoggerFactory.getLogger(Sphinx.class.getCanonicalName()); transient Microphone microphone = null; transient ConfigurationManager cm = null; - transient Recognizer recognizer = null; + transient LiveSpeechRecognizer recognizer = null; transient SpeechProcessor speechProcessor = null; @@ -434,7 +434,7 @@ public boolean isRecording() { * */ public synchronized boolean onIsSpeaking(Boolean talking) { - SpeechRecognizerConfig c = (SpeechRecognizerConfig)config; + SpeechRecognizerConfig c = config; if (talking) { c.listening = false; @@ -501,7 +501,7 @@ public void lockOutAllGrammarExcept(String lockPhrase) { @Override public synchronized void pauseListening() { log.info("Pausing Listening"); - SpeechRecognizerConfig c = (SpeechRecognizerConfig)config; + SpeechRecognizerConfig c = config; c.listening = false; if (microphone != null && recognizer != null) { @@ -539,11 +539,7 @@ public void resumeListening() { SpeechRecognizerConfig c = (SpeechRecognizerConfig)config; c.listening = true; - if (microphone != null) { - // TODO: no idea if this does anything useful. - microphone.clear(); - microphone.startRecording(); - } + recognizer.startRecognition(true); } // FYI - grammar must be created BEFORE we start to listen @@ -587,8 +583,7 @@ private String cleanGrammar(String grammar) { } public void startRecordingx() { - microphone.clear(); - microphone.startRecording(); + recognizer.startRecognition(true); } /** @@ -598,13 +593,12 @@ public void startRecordingx() { */ @Override public void stopListening() { - if (microphone != null) { - microphone.stopRecording(); - microphone.clear(); - } - SpeechRecognizerConfig c = (SpeechRecognizerConfig)config; + SpeechRecognizerConfig c = config; c.listening = false; + if (recognizer != null) { + recognizer.stopRecognition(); + } if (speechProcessor != null) { speechProcessor.isRunning = false; } @@ -615,14 +609,6 @@ public void stopListening() { public void stopService() { super.stopService(); stopListening(); - if (recognizer != null) { - recognizer.deallocate(); - recognizer = null; - } - if (microphone != null) { - microphone.stopRecording(); - microphone = null; - } } @Override diff --git a/src/main/java/org/myrobotlab/service/SpotMicro.java b/src/main/java/org/myrobotlab/service/SpotMicro.java index 7a61a7065a..5c3f84f515 100644 --- a/src/main/java/org/myrobotlab/service/SpotMicro.java +++ b/src/main/java/org/myrobotlab/service/SpotMicro.java @@ -5,10 +5,9 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.ServiceConfig; -import org.myrobotlab.service.config.ServoConfig; import org.slf4j.Logger; -public class SpotMicro extends Service { +public class SpotMicro extends Service { private static final long serialVersionUID = 1L; @@ -18,16 +17,6 @@ public SpotMicro(String n, String id) { super(n, id); } - @Override - public ServiceConfig apply(ServiceConfig c) { - return c; - } - - @Override - public ServiceConfig getConfig() { - return config; - } - public static void main(String[] args) { try { diff --git a/src/main/java/org/myrobotlab/service/Test.java b/src/main/java/org/myrobotlab/service/Test.java index d3bf958054..50460a63eb 100644 --- a/src/main/java/org/myrobotlab/service/Test.java +++ b/src/main/java/org/myrobotlab/service/Test.java @@ -33,6 +33,7 @@ import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.process.GitHub; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.StatusListener; import org.myrobotlab.service.meta.abstracts.MetaData; import org.slf4j.Logger; @@ -55,7 +56,7 @@ * method appended is a callback * */ -public class Test extends Service implements StatusListener { +public class Test extends Service implements StatusListener { /** * filter services by availabilities diff --git a/src/main/java/org/myrobotlab/service/Tracking.java b/src/main/java/org/myrobotlab/service/Tracking.java index 3d05493de1..205d0df3db 100644 --- a/src/main/java/org/myrobotlab/service/Tracking.java +++ b/src/main/java/org/myrobotlab/service/Tracking.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; +import org.myrobotlab.cv.ComputerVision; import org.myrobotlab.document.Classification; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.ServiceInterface; @@ -11,9 +12,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.Pid.PidOutput; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.config.TrackingConfig; -import org.myrobotlab.service.interfaces.ComputerVision; import org.myrobotlab.service.interfaces.PidControl; import org.myrobotlab.service.interfaces.ServoControl; import org.slf4j.Logger; @@ -27,7 +26,7 @@ * @author GroG * */ -public class Tracking extends Service { +public class Tracking extends Service { private static final long serialVersionUID = 1L; @@ -274,24 +273,24 @@ public void attachCv(String cv) { } @Override - public ServiceConfig getConfig() { - TrackingConfig config = (TrackingConfig) super.getConfig(); + public TrackingConfig getConfig() { + super.getConfig(); config.enabled = (state == TrackingState.IDLE) ? false : true; return config; } @Override - public ServiceConfig apply(ServiceConfig c) { - TrackingConfig config = (TrackingConfig) super.apply(c); + public TrackingConfig apply(TrackingConfig c) { + super.apply(c); - config.lostTrackingDelayMs = lostTrackingDelayMs; + c.lostTrackingDelayMs = lostTrackingDelayMs; - if (config.enabled) { + if (c.enabled) { // enable(); } else { disable(); } - return config; + return c; } public boolean isIdle() { diff --git a/src/main/java/org/myrobotlab/service/UltrasonicSensor.java b/src/main/java/org/myrobotlab/service/UltrasonicSensor.java index eaa57a4576..c5df12d252 100644 --- a/src/main/java/org/myrobotlab/service/UltrasonicSensor.java +++ b/src/main/java/org/myrobotlab/service/UltrasonicSensor.java @@ -12,7 +12,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.config.UltrasonicSensorConfig; import org.myrobotlab.service.data.RangeData; import org.myrobotlab.service.interfaces.RangeListener; @@ -30,7 +29,7 @@ * UltrasonicSensor implements RangeListener just for testing purposes * */ -public class UltrasonicSensor extends Service implements RangeListener, RangePublisher, UltrasonicSensorControl { +public class UltrasonicSensor extends Service implements RangeListener, RangePublisher, UltrasonicSensorControl { private final static Logger log = LoggerFactory.getLogger(UltrasonicSensor.class); @@ -162,7 +161,7 @@ public Set getAttached() { @Override public UltrasonicSensorConfig getConfig() { - UltrasonicSensorConfig config = (UltrasonicSensorConfig)super.getConfig(); + super.getConfig(); // FIXME - remove member variables use config directly config.controller = controllerName; config.triggerPin = trigPin; @@ -234,21 +233,21 @@ protected boolean isAttached(UltrasonicSensorController controller) { } @Override - public ServiceConfig apply(ServiceConfig c) { - UltrasonicSensorConfig config = (UltrasonicSensorConfig) super.apply(c); + public UltrasonicSensorConfig apply(UltrasonicSensorConfig c) { + super.apply(c); - if (config.triggerPin != null) - setTriggerPin(config.triggerPin); + if (c.triggerPin != null) + setTriggerPin(c.triggerPin); - if (config.echoPin != null) - setEchoPin(config.echoPin); + if (c.echoPin != null) + setEchoPin(c.echoPin); - if (config.timeout != null) - timeout = config.timeout; + if (c.timeout != null) + timeout = c.timeout; - if (config.controller != null) { + if (c.controller != null) { try { - attach(config.controller); + attach(c.controller); } catch (Exception e) { error(e); } diff --git a/src/main/java/org/myrobotlab/service/VideoStreamer.java b/src/main/java/org/myrobotlab/service/VideoStreamer.java index f21acbebb6..b47256e5ca 100644 --- a/src/main/java/org/myrobotlab/service/VideoStreamer.java +++ b/src/main/java/org/myrobotlab/service/VideoStreamer.java @@ -11,6 +11,7 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.net.MjpegServer; import org.myrobotlab.service.abstracts.AbstractVideoSink; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.VideoSource; import org.slf4j.Logger; @@ -28,7 +29,7 @@ */ public class VideoStreamer - extends AbstractVideoSink /* extends Service implements VideoSink */ { + extends AbstractVideoSink /* extends Service implements VideoSink */ { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/VirtualArduino.java b/src/main/java/org/myrobotlab/service/VirtualArduino.java index 4fb23d38ec..8d53922761 100644 --- a/src/main/java/org/myrobotlab/service/VirtualArduino.java +++ b/src/main/java/org/myrobotlab/service/VirtualArduino.java @@ -13,6 +13,7 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.PinDefinition; import org.myrobotlab.service.interfaces.PortConnector; import org.myrobotlab.service.interfaces.PortListener; @@ -28,7 +29,7 @@ * @author GroG * */ -public class VirtualArduino extends Service implements PortPublisher, PortListener, PortConnector, SerialDataListener { +public class VirtualArduino extends Service implements PortPublisher, PortListener, PortConnector, SerialDataListener { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(VirtualArduino.class); diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java index 8d5b67f6a8..dd2178bf8c 100644 --- a/src/main/java/org/myrobotlab/service/WebGui.java +++ b/src/main/java/org/myrobotlab/service/WebGui.java @@ -34,7 +34,6 @@ import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.MRLListener; import org.myrobotlab.framework.Message; -import org.myrobotlab.framework.MethodCache; import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.Registration; import org.myrobotlab.framework.Service; @@ -45,7 +44,6 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.net.BareBonesBrowserLaunch; import org.myrobotlab.net.Connection; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.config.WebGuiConfig; import org.myrobotlab.service.interfaces.AuthorizationProvider; import org.myrobotlab.service.interfaces.Gateway; @@ -64,7 +62,7 @@ * services are already APIs - perhaps a data API - same as service without the * message wrapper */ -public class WebGui extends Service implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener { +public class WebGui extends Service implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener { public static class LiveVideoStreamHandler implements Handler { @@ -91,7 +89,7 @@ public void handle(AtmosphereResource r) { } } } - + private final transient IncomingMsgQueue inMsgQueue = new IncomingMsgQueue(); public static class Panel { @@ -123,7 +121,6 @@ public Panel(String name, int x, int y, int z) { private static final long serialVersionUID = 1L; - transient protected JmDNS jmdns = null; /** @@ -175,6 +172,7 @@ static public String getApiKey(String uri) { // just marking as transient to remove some of the data load 10240 max frame transient Map panels = new HashMap(); + // FIXME - add as a config member public Integer port; public String root = "root"; @@ -215,9 +213,6 @@ static public String getApiKey(String uri) { public WebGui(String n, String id) { super(n, id); - // adding initial route - // Runtime.getInstance().addRoute(".*", getName(), 10); - if (desktops == null) { desktops = new HashMap>(); } @@ -228,9 +223,6 @@ public WebGui(String n, String id) { panels = desktops.get(currentDesktop); } - // subscribe("runtime", "registered"); - // FIXME - "unregistered" / "released" - onDisconnect = new AtmosphereResourceEventListenerAdapter() { @Override @@ -317,16 +309,14 @@ public Config.Builder getNettosphereConfig() { Config.Builder configBuilder = new Config.Builder(); try { if (isSsl) { -// SelfSignedCertificate cert = new SelfSignedCertificate(); -// SslContext context = SslContextBuilder.forServer(cert.certificate(), cert.privateKey()).build(); - - + // SelfSignedCertificate cert = new SelfSignedCertificate(); + // SslContext context = SslContextBuilder.forServer(cert.certificate(), + // cert.privateKey()).build(); + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); - SslContext context = SslContextBuilder - .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()) - .sslProvider(SslProvider.JDK).clientAuth(ClientAuth.NONE).build(); - - + SslContext context = SslContextBuilder.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()).sslProvider(SslProvider.JDK) + .clientAuth(ClientAuth.NONE).build(); + configBuilder.sslContext(context); } } catch (Exception e) { @@ -334,34 +324,11 @@ public Config.Builder getNettosphereConfig() { } configBuilder.resource("/stream", stream); - // .resource("/video/ffmpeg.1443989700495.mp4", test) - - // FIRST DEFINED HAS HIGHER PRIORITY !! no virtual mapping of resources - // for access after extracting :( - - // configBuilder.resource("./src/main/resources/resource/InMoov2/resource/WebGui/app"); - // clone InMoov2 at the same level as myrobotlab - - // TODO - spin through dirs ? - look for any exact match for service file - // and add it as a resource ? - configBuilder.resource("../InMoov2/resource/WebGui/app"); - - // for debugging - has higher priority - // v- this makes http://localhost:8888/#/main worky - configBuilder.resource("./src/main/resources/resource/WebGui/app"); - // allow sub components to be served - // v- this makes http://localhost:8888/react/index.html worky - configBuilder.resource("./src/main/resources/resource/WebGui"); - // v- this makes http://localhost:8888/Runtime.png worky - configBuilder.resource("./src/main/resources/resource"); - // for future references of resource - keep the html/js reference to - // "resource/x" not "/resource/x" which breaks moving the app - // FUTURE !!! - configBuilder.resource("./src/main/resources"); - - configBuilder.resource("./resource/WebGui/app"); - configBuilder.resource("./resource"); + // add all webgui resource directories + for (String resource : config.resources) { + configBuilder.resource(resource); + } // can't seem to make this work .mappingPath("resource/") @@ -455,10 +422,12 @@ protected void setBroadcaster(AtmosphereResource r) { /** * This method handles all http:// and ws:// requests. Depending on apiKey * which is part of initial GET - *

+ *

+ *

* messages api attempts to promote the connection to websocket and suspends * the connection for a 2 way channel - *

+ *

+ *

* id and session_id authentication should be required * */ @@ -476,6 +445,7 @@ public void handle(AtmosphereResource r) { if (!CodecUtils.API_SERVICE.equals(apiKey) && !CodecUtils.API_MESSAGES.equals(apiKey)) { // NOT A VALID API - send what we support - we're done... OutputStream out = r.getResponse().getOutputStream(); + r.getResponse().addHeader("Content-Type", CodecUtils.MIME_TYPE_JSON); out.write(CodecUtils.toJson(CodecUtils.getApis()).getBytes()); return; } @@ -526,8 +496,6 @@ public void handle(AtmosphereResource r) { log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(), request.getRequestURI(), logData, uuid); } - MethodCache cache = MethodCache.getInstance(); - // important persistent connections will have associated routes ... // http/api/service requests (not persistent connections) will not // (neither will udp) @@ -563,11 +531,10 @@ public void handle(AtmosphereResource r) { } else if (apiKey.equals(CodecUtils.API_SERVICE)) { - Message msg = CodecUtils.cliToMsg( - null, - getName(), - null, - URLDecoder.decode(r.getRequest().getPathInfo(), StandardCharsets.UTF_8)); + String path = URLDecoder.decode(r.getRequest().getPathInfo(), StandardCharsets.UTF_8); + Message msg = CodecUtils.pathToMsg(getFullName(), path); + msg = CodecUtils.decodeMessageParams(msg); + // if body exists it overrides if (bodyData != null) { msg.data = CodecUtils.fromJson(bodyData, Object[].class); } @@ -575,7 +542,8 @@ public void handle(AtmosphereResource r) { if (isLocal(msg)) { // String serviceName = msg.getFullName();// getName(); // Class clazz = Runtime.getClass(serviceName); - // Object[] params = cache.getDecodedJsonParameters(clazz, msg.method, msg.data); + // Object[] params = cache.getDecodedJsonParameters(clazz, msg.method, + // msg.data); // msg.data = params; Object ret = invoke(msg); OutputStream out = r.getResponse().getOutputStream(); @@ -628,7 +596,8 @@ public void handle(AtmosphereResource r) { } // do not decode unless needed - // Object[] params = cache.getDecodedJsonParameters(clazz, msg.method, msg.data); + // Object[] params = cache.getDecodedJsonParameters(clazz, + // msg.method, msg.data); ServiceInterface si = Runtime.getService(serviceName); @@ -1138,11 +1107,6 @@ public void useLocalResources(boolean useLocalResources) { this.useLocalResources = useLocalResources; } - @Override - public Message getDescribeMsg(String connId) { - return Runtime.getInstance().getDescribeMsg(connId); - } - public void display(String image) { // FIXME // http/https can be proxied if necessary or even fetched, @@ -1187,26 +1151,25 @@ public void stopMdns() { } @Override - public ServiceConfig getConfig() { - WebGuiConfig config = (WebGuiConfig)super.getConfig(); - // FIXME - remove member variables use config only + // FIXME port and autoStartBrowser should just be part of config + // then this override can be removed + public WebGuiConfig getConfig() { config.port = port; config.autoStartBrowser = autoStartBrowser; return config; } - @Override - public ServiceConfig apply(ServiceConfig c) { - WebGuiConfig config = (WebGuiConfig) super.apply(c); - - if (config.port != null && (port != null && config.port.intValue() != port.intValue())) { - setPort(config.port); + public WebGuiConfig apply(WebGuiConfig c) { + super.apply(c); + + if (c.port != null && (port != null && c.port.intValue() != port.intValue())) { + setPort(c.port); } - autoStartBrowser(config.autoStartBrowser); - if (config.enableMdns) { + autoStartBrowser(c.autoStartBrowser); + if (c.enableMdns) { startMdns(); } - return config; + return c; } public static void main(String[] args) { @@ -1215,6 +1178,14 @@ public static void main(String[] args) { try { // Platform.setVirtual(true); + + Runtime.startConfig("default"); + + boolean done = true; + if (done) { + return; + } + // Runtime.start("python", "Python"); // Arduino arduino = (Arduino)Runtime.start("arduino", "Arduino"); @@ -1229,24 +1200,17 @@ public static void main(String[] args) { // Runtime.start("intro", "Intro"); // Runtime.start("i01", "InMoov2"); - boolean done = true; - if (done) { - return; - } - - - + // Runtime.start("i01", "InMoov2"); // Runtime.start("python", "Python"); // Runtime.start("i01", "InMoov2"); - + // Runtime.start("i01", "InMoov2"); Runtime.start("track", "Tracking"); // Runtime.startConfig("worky"); // Runtime.startConfig("InMoov2Head"); // Runtime.startConfig("Tracking"); - // Runtime.start("i01", "InMoov2"); // Runtime.start("python", "Python"); // Runtime.start("i01", "InMoov2"); @@ -1338,4 +1302,6 @@ public void onStopped(String name) { @Override public void onReleased(String name) { } + + } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/service/WebSocketConnector.java b/src/main/java/org/myrobotlab/service/WebSocketConnector.java index 995ebc7cf1..dacf200686 100644 --- a/src/main/java/org/myrobotlab/service/WebSocketConnector.java +++ b/src/main/java/org/myrobotlab/service/WebSocketConnector.java @@ -9,6 +9,7 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.net.Connection; import org.myrobotlab.net.WsClient; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.ConnectionManager; import org.myrobotlab.service.interfaces.RemoteMessageHandler; import org.myrobotlab.service.interfaces.TextPublisher; @@ -18,7 +19,7 @@ * @author MaVo (MyRobotLab) / LunDev (GitHub) */ -public class WebSocketConnector extends Service implements RemoteMessageHandler, ConnectionManager, TextPublisher { +public class WebSocketConnector extends Service implements RemoteMessageHandler, ConnectionManager, TextPublisher { static final long serialVersionUID = 1L; static final Logger log = LoggerFactory.getLogger(WebSocketConnector.class); diff --git a/src/main/java/org/myrobotlab/service/Webcam.java b/src/main/java/org/myrobotlab/service/Webcam.java index f56c1bf8bb..d20faf8804 100644 --- a/src/main/java/org/myrobotlab/service/Webcam.java +++ b/src/main/java/org/myrobotlab/service/Webcam.java @@ -12,6 +12,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.math.geometry.Point; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.string.StringUtil; import org.slf4j.Logger; @@ -20,7 +21,7 @@ import com.github.sarxos.webcam.WebcamStreamer; import com.github.sarxos.webcam.ds.v4l4j.V4l4jDriver; -public class Webcam extends Service implements WebcamListener { +public class Webcam extends Service implements WebcamListener { protected class VideoProcessor implements Runnable { diff --git a/src/main/java/org/myrobotlab/service/WebkitSpeechSynthesis.java b/src/main/java/org/myrobotlab/service/WebkitSpeechSynthesis.java index b5306a69d7..d13a23468e 100644 --- a/src/main/java/org/myrobotlab/service/WebkitSpeechSynthesis.java +++ b/src/main/java/org/myrobotlab/service/WebkitSpeechSynthesis.java @@ -9,6 +9,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; +import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.AudioData; import org.slf4j.Logger; @@ -20,7 +21,7 @@ * @author GroG * */ -public class WebkitSpeechSynthesis extends AbstractSpeechSynthesis { +public class WebkitSpeechSynthesis extends AbstractSpeechSynthesis { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Wii.java b/src/main/java/org/myrobotlab/service/Wii.java index ebba625bcf..7447b38d06 100644 --- a/src/main/java/org/myrobotlab/service/Wii.java +++ b/src/main/java/org/myrobotlab/service/Wii.java @@ -34,6 +34,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import jssc.SerialPortEvent; @@ -64,7 +65,7 @@ * http://www.bot-thoughts.com/2010/12/connecting-mbed-to-wiimote-ir-camera.html * */ -public class Wii extends Service implements WiimoteListener, SerialPortEventListener { +public class Wii extends Service implements WiimoteListener, SerialPortEventListener { public static class IRData implements Serializable { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/Wikipedia.java b/src/main/java/org/myrobotlab/service/Wikipedia.java index 4842d48a60..414fbef7ca 100644 --- a/src/main/java/org/myrobotlab/service/Wikipedia.java +++ b/src/main/java/org/myrobotlab/service/Wikipedia.java @@ -5,7 +5,6 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Map; import java.util.List; import java.util.Map; @@ -59,7 +58,7 @@ * @author GroG * */ -public class Wikipedia extends Service implements SearchPublisher, ImagePublisher, TextPublisher { +public class Wikipedia extends Service implements SearchPublisher, ImagePublisher, TextPublisher { public final static Logger log = LoggerFactory.getLogger(Wikipedia.class); @@ -195,7 +194,7 @@ private SearchResults searchWikipedia(String searchText, Boolean publishText, Bo } } else { - log.info("no response for %s", searchText); + log.info("no response for {}", searchText); } invoke("publishResults", results); diff --git a/src/main/java/org/myrobotlab/service/WorkE.java b/src/main/java/org/myrobotlab/service/WorkE.java index 489ddf6eab..9bede4d64b 100644 --- a/src/main/java/org/myrobotlab/service/WorkE.java +++ b/src/main/java/org/myrobotlab/service/WorkE.java @@ -13,6 +13,7 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.opencv.OpenCVData; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis.WordFilter; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.interfaces.JoystickListener; import org.myrobotlab.service.interfaces.MotorControl; @@ -25,7 +26,7 @@ import org.myrobotlab.service.interfaces.TextPublisher; import org.slf4j.Logger; -public class WorkE extends Service implements StatusListener, TextPublisher, SpeechSynthesisControl, SpeechSynthesisControlPublisher, JoystickListener { +public class WorkE extends Service implements StatusListener, TextPublisher, SpeechSynthesisControl, SpeechSynthesisControlPublisher, JoystickListener { public final static Logger log = LoggerFactory.getLogger(WorkE.class); diff --git a/src/main/java/org/myrobotlab/service/Xmpp.java b/src/main/java/org/myrobotlab/service/Xmpp.java index 12c5e13e08..46f6b6b029 100644 --- a/src/main/java/org/myrobotlab/service/Xmpp.java +++ b/src/main/java/org/myrobotlab/service/Xmpp.java @@ -42,6 +42,7 @@ import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.net.Connection; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.Gateway; import org.slf4j.Logger; @@ -54,7 +55,7 @@ * @author GROG * */ -public class Xmpp extends Service implements Gateway, ChatManagerListener, ChatMessageListener, MessageListener, RosterListener, ConnectionListener {// , +public class Xmpp extends Service implements Gateway, ChatManagerListener, ChatMessageListener, MessageListener, RosterListener, ConnectionListener {// , public static class Contact { public String user; @@ -99,9 +100,11 @@ public XmppMsg(Chat chat, Message msg) { String serviceName = "myrobotlab.org"; // xmpp.myrobotlab.org int port = 5222; - transient XMPPTCPConnectionConfiguration config; + transient XMPPTCPConnectionConfiguration configx; transient XMPPTCPConnection connection; transient ChatManager chatManager; + + protected ServiceConfig config; transient Roster roster = null; @@ -291,7 +294,7 @@ public void processMessage(Chat chat, Message message) { try { // org.myrobotlab.framework.Message msg = // CodecUri.decodePathInfo(pathInfo); - org.myrobotlab.framework.Message msg = CodecUtils.cliToMsg(null, getName(), null, pathInfo); + org.myrobotlab.framework.Message msg = CodecUtils.pathToMsg(getName(), pathInfo); // FIXME - do the same as InProcessCli & WebGui Object ret = null; @@ -527,9 +530,4 @@ public boolean isLocal(org.myrobotlab.framework.Message msg) { return Runtime.getInstance().isLocal(msg); } - @Override - public org.myrobotlab.framework.Message getDescribeMsg(String connId) { - return Runtime.getInstance().getDescribeMsg(connId); - } - } diff --git a/src/main/java/org/myrobotlab/service/_TemplateService.java b/src/main/java/org/myrobotlab/service/_TemplateService.java index 0c90dd27a6..61798b307e 100644 --- a/src/main/java/org/myrobotlab/service/_TemplateService.java +++ b/src/main/java/org/myrobotlab/service/_TemplateService.java @@ -7,7 +7,7 @@ import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; -public class _TemplateService extends Service { +public class _TemplateService extends Service { private static final long serialVersionUID = 1L; @@ -23,14 +23,13 @@ public _TemplateService(String n, String id) { *
   @Override
   public ServiceConfig apply(ServiceConfig c) {
-    // _TemplateServiceConfig config = (_TemplateService)super.apply(c);
-    // if more complex config handling is needed
+    super.apply(c)
     return c;
   }
 
   @Override
   public ServiceConfig getConfig() {
-    // _TemplateServiceConfig config = (_TemplateService)super.getConfig();
+    super.getConfig()
     return config;
   }
   
diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractBodyPart.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractBodyPart.java deleted file mode 100644 index c09001031f..0000000000 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractBodyPart.java +++ /dev/null @@ -1,170 +0,0 @@ -package org.myrobotlab.service.abstracts; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.TreeMap; - -import org.myrobotlab.framework.Index; -import org.myrobotlab.framework.Service; -import org.myrobotlab.framework.interfaces.Attachable; -import org.myrobotlab.service.BodyPart; -import org.myrobotlab.service.interfaces.ServoControl; - -/** - * Shared some methods between bodyPart and root controller ( like inmoov - * service ) - */ -public abstract class AbstractBodyPart extends Service { - - private static final long serialVersionUID = 1L; - - public transient Index thisNode = new Index(); - /** - * System will sort servo order based first on conventional name ( if it find - * any off it ), then by attach order. - */ - protected HashMap servoOrder = new HashMap(); - - public AbstractBodyPart(String reservedKey, String id) { - super(reservedKey, id); - } - - /** - * @return get attached body parts ( parents ) - */ - public ArrayList getBodyParts() { - - ArrayList nodes = thisNode.flatten(); - ArrayList bodyParts = new ArrayList(); - for (Attachable service : nodes) { - if (service instanceof BodyPart) { - bodyParts.add((BodyPart) service); - log.info(service + " found by getChilds"); - } - } - return bodyParts; - } - - /** - * get a ServoControl element inside the branches by name - * - * @param identifier - * the id - * @return the servo control - */ - public ServoControl getServo(String identifier) { - return (ServoControl) thisNode.getNode(thisNode.findNode(identifier)).getValue(); - } - - /** - * get a BodyPart element inside the branches by name - * - * @param identifier - * the name/id - * @return the body part - */ - public BodyPart getBodyPart(String identifier) { - return (BodyPart) thisNode.getNode(thisNode.findNode(identifier)).getValue(); - } - - /** - * move a group of servo moveTo order is based on attach order ! But ... If - * you use some dedicated conventional names for your servo, like - * rightHand.thumb ... This standardized order will be respected - * - * Please note syntax order for information : HAND thumb, index, majeure, - * ringFinger, pinky, wrist ARM bicep, rotate, shoulder, omoplate HEAD neck, - * rothead, rollNeck, eyeX, eyeY, jaw - * - * @param node - * the node - * @param servoPos - * the positions varargs - */ - public void moveTo(String node, Double... servoPos) { - checkParameters(node, servoPos.length); - info("moveTo %s servo from %s node that contain %s servo", servoPos.length, node, getAcuators(node).size()); - for (int i = 0; i < servoPos.length && i < getAcuators(node).size(); i++) { - getAcuators(node).get(i).moveTo(servoPos[i]); - info("Moving %s servo", getAcuators(node).get(i)); - } - } - - /** - * move a group of servo And wait for every servo of the group reached the - * asked position - * - * @param node - * the node - * @param servoPos - * the positions - */ - public void moveToBlocking(String node, Double... servoPos) { - checkParameters(node, servoPos.length); - log.info("init {} moveToBlocking ", node); - for (int i = 0; i < servoPos.length && i < getAcuators(node).size(); i++) { - getAcuators(node).get(i).moveTo(servoPos[i]); - } - waitTargetPos(node); - log.info("end {} moveToBlocking ", node); - } - - public void waitTargetPos(String node) { - for (int i = 0; i < getAcuators(node).size(); i++) { - getAcuators(node).get(i).waitTargetPos(); - } - } - - public boolean checkParameters(String node, Integer parameters) { - if (parameters > getAcuators(node).size()) { - warn("Too many input parameters for " + node + " ! not enough elements, don't worry will move what is availabe ..."); - return false; - } - return true; - } - - /** - * @param bodyPart - * the bod=y part name - * @return childsServo : the servos attached to the bodyPart - */ - public ArrayList getAcuators(String bodyPart) { - - ArrayList leafs = thisNode.getLeafs(thisNode.findNode(bodyPart)); - - // Iterate over the desired node to get only ServoControl elements - - /** - * temporary Servo list collection to get priority by conventional names - */ - ArrayList childsServoTmp = new ArrayList(); - ArrayList childsServo = new ArrayList(); - HashMap childsServoHash = new HashMap(); - - for (String leaf : leafs) { - Attachable service = thisNode.getNode(leaf).getValue(); - if (service instanceof ServoControl) { - String[] leafSingleNameSplited = service.getName().split("\\."); - String leafSingleName = leafSingleNameSplited[leafSingleNameSplited.length - 1].toLowerCase(); - // we found a standardized servo name ! - // we need a dedicated position for it ... - if (servoOrder.containsKey(leafSingleName)) { - - childsServoHash.put(servoOrder.get(leafSingleName), (ServoControl) service); - - log.debug("Standardized servo {} found ! Set into dedicated position {}...", leafSingleName, servoOrder.get(leafSingleName)); - } else { - childsServoTmp.add((ServoControl) service); - } - - } - } - // sort standardized servo list first - Map sortedServo = new TreeMap(childsServoHash); - childsServo.addAll(sortedServo.values()); - // add unknown type - childsServo.addAll(childsServoTmp); - return childsServo; - } -} \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractComputerVision.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractComputerVision.java index d410874c8b..b229a8ccdc 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractComputerVision.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractComputerVision.java @@ -1,8 +1,9 @@ package org.myrobotlab.service.abstracts; -import org.myrobotlab.service.interfaces.ComputerVision; +import org.myrobotlab.cv.ComputerVision; +import org.myrobotlab.service.config.ServiceConfig; -public abstract class AbstractComputerVision extends AbstractVideoSource implements ComputerVision { +public abstract class AbstractComputerVision extends AbstractVideoSource implements ComputerVision { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractMicrocontroller.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractMicrocontroller.java index f564ffb25d..2beafb2958 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractMicrocontroller.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractMicrocontroller.java @@ -9,13 +9,14 @@ import org.myrobotlab.arduino.BoardInfo; import org.myrobotlab.arduino.BoardType; import org.myrobotlab.framework.Service; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.Microcontroller; import org.myrobotlab.service.interfaces.PinArrayListener; import org.myrobotlab.service.interfaces.PinDefinition; import org.myrobotlab.service.interfaces.PinListener; -public abstract class AbstractMicrocontroller extends Service implements Microcontroller { +public abstract class AbstractMicrocontroller extends Service implements Microcontroller { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotor.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotor.java index f2e2faa0ff..eb496e0d35 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotor.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotor.java @@ -59,7 +59,7 @@ * */ -abstract public class AbstractMotor extends Service implements MotorControl, EncoderListener { +abstract public class AbstractMotor extends Service implements MotorControl, EncoderListener { private static final long serialVersionUID = 1L; @@ -135,7 +135,7 @@ public Set refreshControllers() { } public MotorController getController() { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; return (MotorController) Runtime.getService(c.controller); } @@ -147,13 +147,13 @@ public double getPowerLevel() { @Override public boolean isAttached(MotorController controller) { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; return controller.getName().equals(c.controller); } @Override public boolean isInverted() { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; return c.mapper.maxIn < c.mapper.minOut; } @@ -169,10 +169,10 @@ public void move(double powerInput) { info("%s is locked - will not move"); return; } - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; // FIXME make mapper.isInInputRange(x) - double min = (c.mapper.minIn < c.mapper.maxIn) ? c.mapper.minIn : c.mapper.maxIn; - double max = (c.mapper.minIn < c.mapper.maxIn) ? c.mapper.maxIn : c.mapper.minIn; + double min = Math.min(c.mapper.minIn, c.mapper.maxIn); + double max = Math.max(c.mapper.minIn, c.mapper.maxIn); if (powerInput < min) { warn("requested power %.2f is under minimum %.2f", powerInput, c.mapper.minIn); @@ -211,7 +211,7 @@ public double publishPowerOutputChange(double output) { @Override public void setInverted(boolean invert) { log.warn("setting {} inverted = {}", getName(), invert); - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; double temp = c.mapper.minIn; c.mapper.minIn = c.mapper.maxIn; c.mapper.maxIn = temp; @@ -220,7 +220,7 @@ public void setInverted(boolean invert) { @Override public void setMinMax(double min, double max) { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; c.mapper.minIn = min; c.mapper.maxIn = max; info("updated min %.2f max %.2f", min, max); @@ -228,7 +228,7 @@ public void setMinMax(double min, double max) { } public void map(double minX, double maxX, double minY, double maxY) { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; c.mapper.map(minX, maxX, minY, maxY); broadcastState(); } @@ -304,7 +304,7 @@ public void setEncoder(EncoderPublisher encoder) { @Override public void detachMotorController(MotorController controller) { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; controller.detach(this); controller = null; c.controller = null; @@ -338,10 +338,6 @@ public void detachAnalogPublisher(AnalogPublisher publisher) { publisher.detachAnalogListener(this); } - public void attach(ButtonDefinition buttondef) { - subscribe(buttondef.getName(), "publishButton", getName(), "move"); - } - @Override public void attachMotorController(MotorController controller) throws Exception { if (controller == null) { @@ -354,7 +350,7 @@ public void attachMotorController(MotorController controller) throws Exception { } log.info("attachMotorController {}", controller.getName()); - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; c.controller = controller.getName(); motorPorts = controller.getPorts(); // TODO: KW: set a reasonable mapper. for pwm motor it's probable -1 to 1 to @@ -389,7 +385,7 @@ public boolean isAttached() { @Override public void detach() { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; c.controller = null; // MAKE NOTE!: don't want to do this anymore for fear of infinit detach loop // just detach this service @@ -402,7 +398,7 @@ public void detach() { // TODO - this could be Java 8 default interface implementation @Override public void detach(String name) { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; MotorController controller = getController(); if (controller == null || !name.equals(controller.getName())) { @@ -426,7 +422,7 @@ public boolean isAttached(String name) { @Override public Set getAttached() { - HashSet ret = new HashSet(); + HashSet ret = new HashSet<>(); MotorController controller = getController(); if (controller != null) { ret.add(controller.getName()); @@ -436,33 +432,33 @@ public Set getAttached() { // FIXME promote to interface public Mapper getMapper() { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; return c.mapper; } // FIXME promote to interface public void setMapper(MapperSimple mapper) { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; c.mapper = mapper; } // FIXME promote to interface @Override public double calcControllerOutput() { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; return c.mapper.calcOutput(getPowerLevel()); } @Override public void setAxis(String name) { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; c.axis = name; broadcastState(); } @Override public String getAxis() { - GeneralMotorConfig c = (GeneralMotorConfig) config; + GeneralMotorConfig c = config; return c.axis; } @@ -472,8 +468,8 @@ public void onAnalog(AnalogData data) { } @Override - public ServiceConfig apply(ServiceConfig c) { - GeneralMotorConfig config = (GeneralMotorConfig) super.apply(c); + public C apply(C c) { + GeneralMotorConfig config = super.apply(c); // config.mapper = new MapperLinear(config.minIn, config.maxIn, // config.minOut, config.maxOut); @@ -481,9 +477,9 @@ public ServiceConfig apply(ServiceConfig c) { // mapper.setClip(config.clip); // FIXME ?? future use only ServiceConfig.listeners ? - if (config.controller != null) { + if (c.controller != null) { try { - attach(config.controller); + attach(c.controller); } catch (Exception e) { error(e); } diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java index 62b4ea5aca..5f01ebc74f 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java @@ -8,10 +8,11 @@ import org.myrobotlab.math.MapperLinear; import org.myrobotlab.math.interfaces.Mapper; import org.myrobotlab.service.Runtime; +import org.myrobotlab.service.config.MotorConfig; import org.myrobotlab.service.interfaces.MotorControl; import org.myrobotlab.service.interfaces.MotorController; -public abstract class AbstractMotorController extends Service implements MotorController { +public abstract class AbstractMotorController extends Service implements MotorController { /** * currently attached motors to this controller diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractPinEncoder.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractPinEncoder.java index b04a551f16..7ebc91a687 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractPinEncoder.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractPinEncoder.java @@ -2,10 +2,11 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.sensor.EncoderData; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.EncoderControl; import org.myrobotlab.service.interfaces.EncoderController; -public abstract class AbstractPinEncoder extends Service implements EncoderControl { +public abstract class AbstractPinEncoder extends Service implements EncoderControl { private static final long serialVersionUID = 1L; public String pin; diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java index 9956a77b72..5c57cef155 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java @@ -1,9 +1,11 @@ package org.myrobotlab.service.abstracts; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Config; import org.myrobotlab.framework.Registration; import org.myrobotlab.framework.Service; @@ -15,6 +17,7 @@ import org.myrobotlab.sensor.EncoderPublisher; import org.myrobotlab.sensor.TimeEncoder; import org.myrobotlab.service.Runtime; +import org.myrobotlab.service.config.ServoConfig; import org.myrobotlab.service.data.AngleData; import org.myrobotlab.service.data.ServoMove; import org.myrobotlab.service.data.ServoSpeed; @@ -48,29 +51,21 @@ * The mapper accepts inputs, the controller needs mapper outputs. * Nothing outside of the servo controller should need the mapper * outputs. - * - * TODO - make a publishing interface which publishes "CONTROL" angles - * vs status of angles * */ -public abstract class AbstractServo extends Service implements ServoControl, ServoControlPublisher, ServoStatusPublisher, EncoderPublisher { +public abstract class AbstractServo extends Service implements ServoControl, ServoControlPublisher, ServoStatusPublisher, EncoderPublisher { public final static Logger log = LoggerFactory.getLogger(AbstractServo.class); private static final long serialVersionUID = 1L; /** - * The automatic disabling of the servo in idleTimeout ms This de-energizes - * the servo. By default this is disabled. - * + * The current servo controller that this servo is attached to. Although most + * of the control events from ServoControl publish as desired, there is an + * "optimization" of having a controller field. It represents a single + * controller, which in turn become a set of notifyEntries. */ - protected boolean autoDisable = false; - - /** - * The current servo controller that this servo is attached to. TODO: move - * this to Servo.java , DiyServo doesn't care about this detail. - */ - protected String controller; + // protected String controller; /** * This allows the servo to attach disabled, and only energize after the first @@ -116,6 +111,11 @@ public abstract class AbstractServo extends Service implements ServoControl, Ser */ protected int idleTimeout = 3000; + /** + * status field if the currently set controller is attached + */ + protected boolean isAttached = false; + /** * if the servo is doing a blocking call - it will block other blocking calls * until the move it complete or a timeout has been reached. A "moveTo" @@ -232,9 +232,6 @@ public AbstractServo(String n, String id) { // we have no "historical" info - assume we are @ rest targetPos = rest; - // TODO: this value is default already. - // mapper.setMinMax(0, 180); - // create our default TimeEncoder if (encoder == null) { encoder = new TimeEncoder(this); // if the encoder has a current value - we initialize the @@ -242,12 +239,9 @@ public AbstractServo(String n, String id) { Double savedPos = encoder.getPos(); if (savedPos != null && loadSavedPositions) { log.info("found previous values for {} setting initial position to {}", getName(), savedPos); - // TODO: kw: output position shouldn't be set to the targetPos.. currentInputPos = targetPos = savedPos; } } - // currentInputP = mapper.calcOutput(targetPos); FIXME - FIXED - encoder now - // publishing input not output } /** @@ -267,10 +261,12 @@ public void onStarted(String name) { @Override public void attach(Attachable service) throws Exception { if (ServoController.class.isAssignableFrom(service.getClass())) { - attach((ServoController) service, null, null, null); - } else if (EncoderControl.class.isAssignableFrom(service.getClass())) { + attachServoController(service.getName()); + } + if (EncoderControl.class.isAssignableFrom(service.getClass())) { attach((EncoderControl) service); - } else { + } + if ((!EncoderControl.class.isAssignableFrom(service.getClass())) && (!ServoController.class.isAssignableFrom(service.getClass()))) { warn(String.format("%s.attach does not know how to attach to a %s", this.getClass().getSimpleName(), service.getClass().getSimpleName())); } } @@ -297,42 +293,23 @@ public void attach(EncoderControl enc) throws Exception { broadcastState(); } - @Override - public void attach(ServoController sc) { - attach(sc, null, null, null); - } - - @Deprecated /* setPin then attach(String) */ - public void attach(ServoController sc, Integer pin) { - attachServoController(sc.getName(), pin, null, null); - } - - public void attach(ServoController sc, Integer pin, Double pos) { - attachServoController(sc.getName(), pin, pos, null); - } - - @Deprecated /* setPin setPos setSpeed then attach(String) */ - public void attach(ServoController sc, Integer pin, Double pos, Double speed) { - attachServoController(sc.getName(), pin, pos, speed); + public void setController(String name) { + config.controller = name; + broadcastState(); } @Override - public void attach(String sc) throws Exception { - attachServoController(sc, null, null, null); - } - - @Deprecated /* setPin then attach(String) */ - public void attach(String controllerName, Integer pin) { - attach(controllerName, pin, null); + public void attach(ServoController sc) { + attach(sc.getName()); } - @Deprecated /* setPin setPos then attach(String) */ - public void attach(String controllerName, Integer pin, Double pos) { - attach(controllerName, pin, pos, null); + @Override + public void attach(String sc) { + attachServoController(sc); } @Deprecated - /* + /** * Servos Do Not publish Joint Angles - they only publish their position ! */ public AngleData publishJointAngle(AngleData angle) { @@ -340,67 +317,58 @@ public AngleData publishJointAngle(AngleData angle) { return angle; } - // @Override - // FIXME - decide how attach will work or wont with extra parameters - public void attach(String controllerName, Integer pin, Double pos, Double speed) { - try { - setPin(pin); - setPosition(pos); - setSpeed(speed); - attach(controllerName); - } catch (Exception e) { - error(e); - } - } - /** - * maximum complexity attach with reference to controller FIXME - max - * complexity service should use NAME not a direct reference to - * ServoController !!!! + * maximum complexity attach with reference to controller */ @Override - public void attachServoController(String sc, Integer pin, Double pos, Double speed) { - if (controller != null && controller.equals(sc)) { - log.info("{} already attached", sc); + public void attachServoController(String service) { + if (service == null) { + error("attachServoController null"); return; } - // update pin if non-null value supplied - if (pin != null) { - setPin(pin); - } - // update pos if non-null value supplied - if (pos != null) { - targetPos = pos; + + if (getPin() == null) { + error("cannot attach servo if pin is null"); + return; } - // update speed if non-null value supplied - if (speed != null) { - setSpeed(speed); + + if (isAttached && !CodecUtils.getFullName(service).equals(CodecUtils.getFullName(config.controller))) { + warn("%s already attached to %s detach first", getName(), service); + return; + } else if (isAttached) { + log.info("is attached"); + return; } + // the subscribes .... or addListeners in this case ... - addListener("publishServoMoveTo", sc); - addListener("publishServoStop", sc); - addListener("publishServoWriteMicroseconds", sc); - addListener("publishServoSetSpeed", sc); - addListener("publishServoEnable", sc); - addListener("publishServoDisable", sc); - controller = sc; - - ServoController servoController = (ServoController) Runtime.getService(sc); - if (servoController != null) { - servoController.attachServoControl(this); + addListener("publishServoMoveTo", service); + addListener("publishServoStop", service); + addListener("publishServoWriteMicroseconds", service); + addListener("publishServoSetSpeed", service); + addListener("publishServoEnable", service); + addListener("publishServoDisable", service); + if (CodecUtils.isLocal(service)) { + service = CodecUtils.getShortName(service); + } + config.controller = service; + + // "guessing" its ok if it exists ... + if (Runtime.getService(service) != null) { + isAttached = true; + } else { + // for at least arduino it must be started to attach a servo + warn("%s servo could not attach to controller %s not available", getName(), service); + isAttached = false; + } + + // asynchronous - did we successfully attach ¯\_(ツ)_/¯ ! + send(service, "attach", getName()); + log.info("{} attached to {} on pin {}", getName(), service, pin); + + if (config.autoDisable) { + disable(); + addTaskOneShot(idleTimeout, "disable"); } - // FIXME - remove !!! - // FIXME change to broadcast ? - // TODO: there is a race condition here.. we need to know that - // the servo control ackowledged this. - // try { - // sendBlocking(sc, "attachServoControl", this); // <-- change to broadcast - // ? - // } catch (Exception e) { - // log.error("sendBlocking attachServoControl threw", e); - // } - // TOOD: we need to wait here for the servo controller to acknowledge that - // it was attached. broadcastState(); } @@ -410,7 +378,7 @@ public void attachServoController(String sc, Integer pin, Double pos, Double spe */ @Override public void detach() { - detach(controller); + detach(config.controller); } @Override @@ -423,19 +391,22 @@ public void detach(ServoController sc) { detach(sc.getName()); } - // AbstractServo - + /** + * detach this servo from the controller named controllerName + */ @Override public void detach(String controllerName) { - if (controller == null) { + if (!isAttached) { log.info("already detached"); return; } - if (controller != null && !controller.equals(controllerName)) { + if (config.controller != null && !config.controller.equals(controllerName)) { log.warn("{} not attached to {}", getName(), controllerName); return; } + // disable servo before detaching controller disable(); // the subscribes .... or addListeners in this case ... @@ -445,13 +416,18 @@ public void detach(String controllerName) { removeListener("publishServoSetSpeed", controllerName); removeListener("publishServoEnable", controllerName); removeListener("publishServoDisable", controllerName); - controller = null; - // 20210703 - grog I don't know why a sleep was put here - // junit ServoTest will fail without this :P - // sleep(500); + // no need to nullify controller its useful data + // the servo can keep .. like "pin" + // controller = null; firstMove = true; + // assume successful + isAttached = false; + + // fire and forget send(controllerName, "detach", getName()); + + log.info("{} detached from {}", getName(), controllerName); broadcastState(); } @@ -464,8 +440,7 @@ public void disable() { @Override public void enable() { - - if (autoDisable) { + if (config.autoDisable) { if (!isMoving) { // not moving - safe & expected to put in a disable purgeTask("disable"); @@ -475,7 +450,6 @@ public void enable() { enabled = true; broadcast("publishServoEnable", this); - // broadcastState(); } @Override @@ -486,12 +460,12 @@ public void fullSpeed() { @Override public boolean isAutoDisable() { - return autoDisable; + return config.autoDisable; } @Override public String getController() { - return controller; + return config.controller; } @Override @@ -530,7 +504,15 @@ public String getPin() { */ @Override public double getCurrentInputPos() { - // return mapper.calcInput(currentInputP); + return currentInputPos; + } + + /** + * for backward compatibility + * + * @return + */ + public double getPos() { return currentInputPos; } @@ -564,14 +546,18 @@ public Double getVelocity() { return speed; } + public boolean isAttached() { + return isAttached; + } + @Override public boolean isAttached(Attachable attachable) { - return controller != null && controller.equals(attachable.getName()); + return isAttached && config.controller.equals(attachable.getName()); } @Override public boolean isAttached(String name) { - return controller != null && controller.equals(name); + return isAttached && CodecUtils.getFullName(config.controller).equals(CodecUtils.getFullName(name)); } @Override @@ -624,9 +610,8 @@ public Double moveTo(Double newPos) { * weather a move request was successful. The cases it would be false is no * controller or calling moveTo when blocking is in process */ - if (newPos == null) { - log.info("will not move to null position - not moving"); + log.info("{} will not move to null position - not moving", getName()); return newPos; } @@ -756,10 +741,7 @@ public String publishServoDisable(ServoControl sc) { return sc.getName(); } - @Override /* - * FIXME these should be returning null - the event itself is enough - * info - sending whole servo is excessive - */ + @Override public String publishServoEnable(ServoControl sc) { log.debug("{}.publishServoEnable()", getName()); return sc.getName(); @@ -833,8 +815,8 @@ public void setAutoDisable(boolean autoDisable) { } else { purgeTask("disable"); } - boolean valueChanged = this.autoDisable != autoDisable; - this.autoDisable = autoDisable; + boolean valueChanged = config.autoDisable != autoDisable; + config.autoDisable = autoDisable; if (valueChanged) { broadcastState(); } @@ -1031,17 +1013,16 @@ public void sync(String name) { public void unsync(ServoControl sc) { if (sc == null) { log.error("{}.unsync(null)", getName()); + return; } unsync(sc.getName()); } @Override public void waitTargetPos() { - // - // while (this.pos != this.targetPos) { - // Some sleep perhaps? - // TODO: - // } + while (this.getCurrentInputPos() != this.targetPos) { + sleep(30); + } } @Override @@ -1073,12 +1054,11 @@ public ServoEvent publishServoStarted(String name, Double position) { */ @Override public ServoEvent publishServoStopped(String name, Double position) { - log.debug("publishServoStopped({}, {})", name, position); - + log.debug("{} publishServoStopped({}, {})", System.currentTimeMillis(), name, position); // log.info("TIME-ENCODER SERVO_STOPPED - {}", name); // if currently configured to autoDisable - the timer starts now // if we are "stopping" going from moving to not moving - if (autoDisable && isMoving) { + if (config.autoDisable && isMoving) { // we cancel any pre-existing timer if it exists purgeTask("disable"); // and start our countdown @@ -1152,4 +1132,97 @@ public void attachServoControlListener(String name) { } + @Override + public C apply(C c) { + super.apply(c); + + // important - if starting up + // and autoDisable - then the assumption at this point + // is it is currently disabled, otherwise it will take + // a move to disable + if (c.autoDisable) { + disable(); + } + if (c.minIn != null && c.maxIn != null && c.minOut != null && c.maxOut != null) { + mapper = new MapperLinear(c.minIn, c.maxIn, c.minOut, c.maxOut); + } + mapper.setInverted(c.inverted); + mapper.setClip(c.clip); + enabled = c.enabled; + if (c.idleTimeout != null) { + idleTimeout = c.idleTimeout; + } + pin = c.pin; + + speed = c.speed; + sweepMax = c.sweepMax; + sweepMin = c.sweepMin; + + if (c.synced != null) { + syncedServos.clear(); + Collections.addAll(syncedServos, c.synced); + } + + // rest = c.rest; + if (c.rest != null) { + rest = c.rest; + targetPos = c.rest; + // currentInputP = mapper.calcOutput(c.rest); + currentInputPos = c.rest; + broadcast("publishEncoderData", new EncoderData(getName(), pin, c.rest, c.rest)); + } + + if (c.controller != null) { + try { + attach(c.controller); + } catch (Exception e) { + error(e); + } + } + + // connect and attach on an arduino can take considerable time + // so we'll add our id + if (c.autoDisable) { + disable(); + addTaskOneShot(idleTimeout, "disable"); + } + + return c; + } + + @Override + public C getConfig() { + + super.getConfig(); + + config.enabled = enabled; + + if (mapper != null) { + config.clip = mapper.isClip(); + config.maxIn = mapper.getMaxX(); + config.maxOut = mapper.getMaxY(); + config.minIn = mapper.getMinX(); + config.minOut = mapper.getMinY(); + config.inverted = mapper.isInverted(); + } + + // FIXME remove members and use config only + config.idleTimeout = idleTimeout; + config.pin = pin; + config.rest = rest; + config.speed = speed; + config.sweepMax = sweepMax; + config.sweepMin = sweepMin; + + if (!syncedServos.isEmpty()) { + config.synced = new String[syncedServos.size()]; + int i = 0; + for (String s : syncedServos) { + config.synced[i] = s; + ++i; + } + } + + return config; + } } diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechRecognizer.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechRecognizer.java index b87bd7daae..fa54857772 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechRecognizer.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechRecognizer.java @@ -8,7 +8,6 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.service.Runtime; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.config.SpeechRecognizerConfig; import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.data.Locale; @@ -16,7 +15,7 @@ import org.myrobotlab.service.interfaces.SpeechSynthesis; import org.myrobotlab.service.interfaces.TextListener; -public abstract class AbstractSpeechRecognizer extends Service implements SpeechRecognizer { +public abstract class AbstractSpeechRecognizer extends Service implements SpeechRecognizer { /** * text and confidence (and any additional meta data) to be published @@ -575,33 +574,33 @@ public void unsetWakeWord() { } @Override - public ServiceConfig getConfig() { - SpeechRecognizerConfig c = (SpeechRecognizerConfig) super.getConfig(); + public C getConfig() { + C c = super.getConfig(); c.listening = isListening(); c.wakeWord = getWakeWord(); Set listeners = getAttached("publishText"); - c.textListeners = listeners.toArray(new String[listeners.size()]); + c.textListeners = listeners.toArray(new String[0]); return c; } @Override - public ServiceConfig apply(ServiceConfig c) { - SpeechRecognizerConfig config = (SpeechRecognizerConfig)super.apply(c);; - setWakeWord(config.wakeWord); - if (config.listening) { + public C apply(C c) { + super.apply(c); + setWakeWord(c.wakeWord); + if (c.listening) { startListening(); } else { stopListening(); } - if (config.recording) { + if (c.recording) { startRecording(); } else { stopRecording(); } - if (config.textListeners != null) { - for (String listener : config.textListeners) { + if (c.textListeners != null) { + for (String listener : c.textListeners) { addListener("publishText", listener); } } diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java index d70210df21..50bd64ccd0 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java @@ -19,7 +19,6 @@ import org.myrobotlab.service.AudioFile; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.Security; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.data.Locale; @@ -33,7 +32,7 @@ import org.myrobotlab.service.interfaces.TextPublisher; import org.slf4j.Logger; -public abstract class AbstractSpeechSynthesis extends Service implements SpeechSynthesis, TextListener, KeyConsumer, AudioListener { +public abstract class AbstractSpeechSynthesis extends Service implements SpeechSynthesis, TextListener, KeyConsumer, AudioListener { private static final long serialVersionUID = 1L; @@ -1112,27 +1111,27 @@ public boolean isMute() { } @Override - public ServiceConfig apply(ServiceConfig c) { - SpeechSynthesisConfig config = (SpeechSynthesisConfig) super.apply(c); + public C apply(C c) { + super.apply(c); - setMute(config.mute); + setMute(c.mute); - setBlocking(config.blocking); + setBlocking(c.blocking); - if (config.substitutions != null) { - for (String n : config.substitutions.keySet()) { - replaceWord(n, config.substitutions.get(n)); + 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 (config.voice != null) { - setVoice(config.voice); + if (c.voice != null) { + setVoice(c.voice); } - if (config.speechRecognizers != null) { - for (String name : config.speechRecognizers) { + if (c.speechRecognizers != null) { + for (String name : c.speechRecognizers) { try { attachSpeechListener(name); } catch (Exception e) { @@ -1154,11 +1153,11 @@ public void attachSpeechControl(SpeechSynthesisControl control) { } @Override - public ServiceConfig getConfig() { - SpeechSynthesisConfig c = (SpeechSynthesisConfig) super.getConfig(); + public C getConfig() { + C c = super.getConfig(); c.mute = mute; c.blocking = blocking; - if (substitutions != null && substitutions.size() > 0) { + if (substitutions != null && !substitutions.isEmpty()) { c.substitutions = new HashMap<>(); c.substitutions.putAll(substitutions); } @@ -1166,7 +1165,7 @@ public ServiceConfig getConfig() { c.voice = voice.name; } Set listeners = getAttached("publishStartSpeaking"); - c.speechRecognizers = listeners.toArray(new String[listeners.size()]); + c.speechRecognizers = listeners.toArray(new String[0]); return c; } diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSink.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSink.java index 47246ae947..9850d58630 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSink.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSink.java @@ -3,10 +3,11 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.image.SerializableImage; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.VideoSink; import org.myrobotlab.service.interfaces.VideoSource; -public abstract class AbstractVideoSink extends Service implements VideoSink { +public abstract class AbstractVideoSink extends Service implements VideoSink { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSource.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSource.java index a1a236bc0d..18e359d1b6 100644 --- a/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSource.java +++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractVideoSource.java @@ -1,10 +1,11 @@ package org.myrobotlab.service.abstracts; import org.myrobotlab.framework.Service; +import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.VideoSink; import org.myrobotlab.service.interfaces.VideoSource; -public abstract class AbstractVideoSource extends Service implements VideoSource { +public abstract class AbstractVideoSource extends Service implements VideoSource { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/myrobotlab/service/config/BoofCVConfig.java b/src/main/java/org/myrobotlab/service/config/BoofCVConfig.java new file mode 100644 index 0000000000..f90c9ba81b --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/BoofCVConfig.java @@ -0,0 +1,12 @@ +package org.myrobotlab.service.config; + +public class BoofCVConfig extends ServiceConfig { + + public Integer cameraIndex = 0; + public String inputSource = "camera"; + public String inputFile = null; + public boolean nativeViewer = true; + public boolean webViewer = false; + public boolean capturing = false; + +} diff --git a/src/main/java/org/myrobotlab/service/config/CronConfig.java b/src/main/java/org/myrobotlab/service/config/CronConfig.java new file mode 100644 index 0000000000..974526c1f2 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/CronConfig.java @@ -0,0 +1,12 @@ +package org.myrobotlab.service.config; + +import java.util.ArrayList; +import java.util.List; + +import org.myrobotlab.service.Cron.Task; + +public class CronConfig extends ServiceConfig { + + public List tasks = new ArrayList<>(); + +} diff --git a/src/main/java/org/myrobotlab/service/config/DocumentPipelineConfig.java b/src/main/java/org/myrobotlab/service/config/DocumentPipelineConfig.java new file mode 100755 index 0000000000..17a42ae982 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/DocumentPipelineConfig.java @@ -0,0 +1,9 @@ +package org.myrobotlab.service.config; + +import org.myrobotlab.document.transformer.WorkflowConfiguration; + +public class DocumentPipelineConfig extends ServiceConfig { + + public WorkflowConfiguration workFlowConfig; + +} diff --git a/src/main/java/org/myrobotlab/service/config/EmojiConfig.java b/src/main/java/org/myrobotlab/service/config/EmojiConfig.java index ff8dd2d3f2..e2d6cbbdcc 100644 --- a/src/main/java/org/myrobotlab/service/config/EmojiConfig.java +++ b/src/main/java/org/myrobotlab/service/config/EmojiConfig.java @@ -3,7 +3,6 @@ import java.util.HashMap; import java.util.Map; -import org.myrobotlab.framework.Peer; import org.myrobotlab.framework.Plan; public class EmojiConfig extends ServiceConfig { diff --git a/src/main/java/org/myrobotlab/service/config/FileConnectorConfig.java b/src/main/java/org/myrobotlab/service/config/FileConnectorConfig.java new file mode 100755 index 0000000000..68b71fad77 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/FileConnectorConfig.java @@ -0,0 +1,7 @@ +package org.myrobotlab.service.config; + +public class FileConnectorConfig extends ServiceConfig { + + public String directory; + +} diff --git a/src/main/java/org/myrobotlab/service/config/Gpt3Config.java b/src/main/java/org/myrobotlab/service/config/Gpt3Config.java index a1029b81f7..62a6627a1d 100644 --- a/src/main/java/org/myrobotlab/service/config/Gpt3Config.java +++ b/src/main/java/org/myrobotlab/service/config/Gpt3Config.java @@ -13,9 +13,9 @@ public class Gpt3Config extends ServiceConfig { public int maxTokens = 256; public float temperature = 0.7f; - public String url = "https://api.openai.com/v1/completions"; + public String url = "https://api.openai.com/v1/chat/completions"; public String token = null; - public String engine = "text-davinci-003"; + public String engine = "gpt-3.5-turbo"; // "text-davinci-003" public String wakeWord = "wake"; public String sleepWord = "sleep"; diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index a7a4dc8e33..ecb2d59c4e 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -3,10 +3,8 @@ import java.util.ArrayList; import org.myrobotlab.framework.Plan; -import org.myrobotlab.framework.Service; import org.myrobotlab.jme3.UserDataConfig; import org.myrobotlab.math.MapperLinear; -import org.myrobotlab.service.InMoov2; import org.myrobotlab.service.Pid.PidData; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.config.FiniteStateMachineConfig.Transition; @@ -81,10 +79,6 @@ public class InMoov2Config extends ServiceConfig { * Sleep 5 minutes after last presence detected */ public int sleepTimeoutMs=300000; - - public boolean startBrainOnBoot = true; - - public boolean startMouthOnBoot = true; public boolean startupSound = true; @@ -103,7 +97,7 @@ public Plan getDefault(Plan plan, String name) { // peers FIXME global opencv addDefaultPeerConfig(plan, name, "audioPlayer", "AudioFile", true); - addDefaultPeerConfig(plan, name, "chatBot", "ProgramAB", false); + addDefaultPeerConfig(plan, name, "chatBot", "ProgramAB", true); addDefaultPeerConfig(plan, name, "controller3", "Arduino", false); addDefaultPeerConfig(plan, name, "controller4", "Arduino", false); addDefaultPeerConfig(plan, name, "ear", "WebkitSpeechRecognition", false); @@ -112,7 +106,7 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "gpt3", "Gpt3", false); addDefaultPeerConfig(plan, name, "head", "InMoov2Head", false); addDefaultPeerConfig(plan, name, "headTracking", "Tracking", false); - addDefaultPeerConfig(plan, name, "htmlFilter", "HtmlFilter", false); + addDefaultPeerConfig(plan, name, "htmlFilter", "HtmlFilter", true); addDefaultPeerConfig(plan, name, "imageDisplay", "ImageDisplay", false); addDefaultPeerConfig(plan, name, "leap", "LeapMotion", false); addDefaultPeerConfig(plan, name, "left", "Arduino", false); @@ -148,10 +142,6 @@ public Plan getDefault(Plan plan, String name) { mouthControl.mouth = i01Name + ".mouth"; - // FIXME ! - look at this !!! I've made austartPeers = false ! - // by just sending a runtime that starts only i01 - RuntimeConfig rtConfig = (RuntimeConfig) plan.get("runtime"); - ProgramABConfig chatBot = (ProgramABConfig) plan.get(getPeerName("chatBot")); Runtime runtime = Runtime.getInstance(); String[] bots = new String[] { "cn-ZH", "en-US", "fi-FI", "hi-IN", "nl-NL", "ru-RU", "de-DE", "es-ES", "fr-FR", "it-IT", "pt-PT", "tr-TR" }; @@ -166,12 +156,14 @@ public Plan getDefault(Plan plan, String name) { } } } + + chatBot.currentUserName = "human"; + // chatBot.textListeners = new String[] { name + ".htmlFilter" }; if (chatBot.listeners == null) { chatBot.listeners = new ArrayList<>(); } chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); - chatBot.botDir = "data/ProgramAB"; HtmlFilterConfig htmlFilter = (HtmlFilterConfig) plan.get(getPeerName("htmlFilter")); // htmlFilter.textListeners = new String[] { name + ".mouth" }; @@ -183,17 +175,19 @@ public Plan getDefault(Plan plan, String name) { // == Peer - mouth ============================= // setup name references to different services MarySpeechConfig mouth = (MarySpeechConfig) plan.get(getPeerName("mouth")); + mouth.voice = "Mark"; mouth.speechRecognizers = new String[] { name + ".ear" }; // == Peer - ear ============================= // setup name references to different services WebkitSpeechRecognitionConfig ear = (WebkitSpeechRecognitionConfig) plan.get(getPeerName("ear")); - ear.textListeners = new String[] { name + ".chatBot" }; + ear.listeners = new ArrayList<>(); + ear.listeners.add(new Listener("publishText", name + ".chatBot", "onText")); + ear.listening = true; + // remove, should only need ServiceConfig.listeners + ear.textListeners = new String[]{name + ".chatBot"}; JMonkeyEngineConfig simulator = (JMonkeyEngineConfig) plan.get(getPeerName("simulator")); - // FIXME - SHOULD USE RESOURCE DIR ! - String assestsDir = Service.getResourceDir(InMoov2.class) + "/JMonkeyEngine"; - simulator.addModelPath(assestsDir); simulator.multiMapped.put(name + ".leftHand.index", new String[] { name + ".leftHand.index", name + ".leftHand.index2", name + ".leftHand.index3" }); simulator.multiMapped.put(name + ".leftHand.majeure", new String[] { name + ".leftHand.majeure", name + ".leftHand.majeure2", name + ".leftHand.majeure3" }); diff --git a/src/main/java/org/myrobotlab/service/config/JMonkeyEngineConfig.java b/src/main/java/org/myrobotlab/service/config/JMonkeyEngineConfig.java index 6b48f198d4..27d8a24060 100644 --- a/src/main/java/org/myrobotlab/service/config/JMonkeyEngineConfig.java +++ b/src/main/java/org/myrobotlab/service/config/JMonkeyEngineConfig.java @@ -1,40 +1,28 @@ package org.myrobotlab.service.config; import java.util.ArrayList; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.myrobotlab.jme3.UserDataConfig; public class JMonkeyEngineConfig extends ServiceConfig { /** - * must be unique entries - use addModelPath(path) helper + * Models for JMonkeyEngine to load - can be of format + */ + public List models = new ArrayList<>(); + + /** + * A spatial associated with some part of the scene graph */ - public List modelPaths = new ArrayList<>(); public Map nodes = new LinkedHashMap<>(); public Map multiMapped = new LinkedHashMap<>(); - public String cameraLookAt; - public Set test = new HashSet<>(); - + /** - * JMonkeyEngine requires model paths to be unique - they are not idempotent - * when adding to the graphtree - use this function to keep them unique, yet - * the yaml will still be a simple array. This is to avoid Yaml's !!set - * definition - * - * @param path + * The name of the node which the camera should look at */ - public void addModelPath(String path) { - for (String p : modelPaths) { - if (p.equals(path)) { - return; - } - } - modelPaths.add(path); - } + public String cameraLookAt; } diff --git a/src/main/java/org/myrobotlab/service/config/PirConfig.java b/src/main/java/org/myrobotlab/service/config/PirConfig.java index c1413bb9f1..3d67430959 100644 --- a/src/main/java/org/myrobotlab/service/config/PirConfig.java +++ b/src/main/java/org/myrobotlab/service/config/PirConfig.java @@ -14,6 +14,7 @@ public class PirConfig extends ServiceConfig { /** * poll rate in Hz + * FIXME change to double or float to support 0.01 Hz */ public int rate = 1; diff --git a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java index b0bf8975c0..ce9ae14033 100644 --- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java +++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java @@ -6,19 +6,46 @@ import org.myrobotlab.framework.Plan; public class ProgramABConfig extends ServiceConfig { - - public String currentBotName = "Alice"; - public String currentUserName; + + @Deprecated /* unused text filters */ public String[] textFilters; - // public String[] textListeners; - // public String[] utteranceListeners; + + /** + * a directory ProgramAB will scan for new bots + */ public String botDir; + + /** + * explicit bot directories + */ public List bots = new ArrayList<>(); + + /** + * current sessions bot name, it must match a botname that was scanned + * currently with ProgramAB Alice, Dr.Who, Mr. Turing and Ency + */ + public String currentBotName = "Alice"; /** + * 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 */ public boolean sleep = false; + + /** + * topic to start with, if null then topic will be loaded from predicates of + * a new session if available, this means a config/{username}.predicates.txt + * will need to exist with a topic field + */ + public String startTopic = "unknown"; @Override public Plan getDefault(Plan plan, String name) { diff --git a/src/main/java/org/myrobotlab/service/config/Py4jConfig.java b/src/main/java/org/myrobotlab/service/config/Py4jConfig.java new file mode 100644 index 0000000000..cafb6d790f --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/Py4jConfig.java @@ -0,0 +1,17 @@ +package org.myrobotlab.service.config; + +public class Py4jConfig extends ServiceConfig { + + /** + * root of python scripts - if not specified by user it will be + * /data/Py4j/{serviceName} + */ + public String scriptRootDir; + + /** + * Whether to use the bundled Python executable + * or invoke the system Python. + */ + public boolean useBundledPython = true; + +} diff --git a/src/main/java/org/myrobotlab/service/config/PythonConfig.java b/src/main/java/org/myrobotlab/service/config/PythonConfig.java index 90e433d221..1aab8f14c7 100644 --- a/src/main/java/org/myrobotlab/service/config/PythonConfig.java +++ b/src/main/java/org/myrobotlab/service/config/PythonConfig.java @@ -4,6 +4,12 @@ import java.util.List; public class PythonConfig extends ServiceConfig { + + /** + * root of python scripts - if not specified by user it will be + * /data/Python/{serviceName} + */ + public String scriptRootDir; /** * scripts to execute when python is started @@ -19,5 +25,6 @@ public class PythonConfig extends ServiceConfig { * dist or site paths for pure python 2.7 modules */ public List modulePaths = new ArrayList<>(); + } diff --git a/src/main/java/org/myrobotlab/service/config/RasPiConfig.java b/src/main/java/org/myrobotlab/service/config/RasPiConfig.java index ef0c4769db..f10c2a1fc3 100644 --- a/src/main/java/org/myrobotlab/service/config/RasPiConfig.java +++ b/src/main/java/org/myrobotlab/service/config/RasPiConfig.java @@ -1,5 +1,13 @@ package org.myrobotlab.service.config; public class RasPiConfig extends ServiceConfig { + /** + * reading poll rate for all enabled GPIO pins + * this "should" not be an int but a float, but at this time + * its better to follow the PinDefinition pollRateHz type + */ + public int pollRateHz = 1; + + // TODO - config which starts pins in a mode (read/write) and if write a value 0/1 } diff --git a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java index e9e9ab27a0..87b9e4a1d1 100644 --- a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java +++ b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java @@ -21,6 +21,11 @@ public class RuntimeConfig extends ServiceConfig { // NEED THIS PRIVATE BUT CANNOT BE public List registry = new ArrayList<>(); + + /** + * Root of resource location + */ + public String resource = "resource"; /** * add and remove a service using these methods and the uniqueness will be diff --git a/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java b/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java index 9bd54ce192..42db50689b 100644 --- a/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java +++ b/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java @@ -2,7 +2,7 @@ import org.myrobotlab.framework.Plan; -public class SabertoothConfig extends ServiceConfig { +public class SabertoothConfig extends MotorConfig { public String port; public boolean connect = false; diff --git a/src/main/java/org/myrobotlab/service/config/ServiceConfig.java b/src/main/java/org/myrobotlab/service/config/ServiceConfig.java index c85d43d947..233e0c29b8 100755 --- a/src/main/java/org/myrobotlab/service/config/ServiceConfig.java +++ b/src/main/java/org/myrobotlab/service/config/ServiceConfig.java @@ -1,13 +1,19 @@ package org.myrobotlab.service.config; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.TreeMap; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Peer; import org.myrobotlab.framework.Plan; +import org.myrobotlab.framework.Service; +import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.Runtime; import org.slf4j.Logger; @@ -79,12 +85,6 @@ public String toString() { // heh non transient makes it easy to debug ! transient public String state = "INIT"; // INIT | LOADED | CREATED | STARTED | // STOPPED | RELEASED - // FIXME - SO IMPORTANT ! - - public String getx(String key) { - // FIXME - return reflected value - return null; - } public String getPath(String name, String peerKey) { if (name == null) { @@ -170,7 +170,7 @@ public ServiceConfig addDefaultGlobalConfig(Plan plan, String key, String global * @param plan * @param key * @param globalName - * @param peer + * @param peerType * @return */ public ServiceConfig addDefaultGlobalConfig(Plan plan, String key, String globalName, String peerType, boolean autoStart) { @@ -217,11 +217,6 @@ public Peer putPeerType(String peerKey, String fullName, String peerType) { public static Plan getDefault(Plan plan, String name, String inType) { try { - // if (type == null) { - // log.error("getDefault(null)"); - // return null; - // } - // FIXME - at some point setting, examining and changing // peer keys to actual names will need to be worky String fullType = getConfigType(inType); @@ -233,11 +228,37 @@ public static Plan getDefault(Plan plan, String name, String inType) { // plan.merge(); config.getDefault(plan, name); - } catch (ClassNotFoundException e) { - log.info("could not find {} loading generalized ServiceConfig", inType); - ServiceConfig sc = new ServiceConfig(); - sc.type = inType; - plan.put(name, sc); + } catch (ClassNotFoundException cnfe) { + // We could not find the config type with the simple {serviceType}Config pattern + // So now we look at its superclasses and try to find a config class in + // the generic type parameters + // FIXME should also perform the simple pattern check on superclasses + try { + @SuppressWarnings("rawtypes") + Class serviceClass = Class.forName(CodecUtils.makeFullTypeName(inType)).asSubclass(Service.class); + Type superClass = serviceClass.getGenericSuperclass(); + if (superClass instanceof ParameterizedType) { + ParameterizedType genericSuperClass = (ParameterizedType) superClass; + log.debug("Got generic superclass: " + genericSuperClass + " for service class " + serviceClass); + Class configClass = ((Class) genericSuperClass.getActualTypeArguments()[0]).asSubclass(ServiceConfig.class); + ServiceConfig newConfig = configClass.getConstructor().newInstance(); + newConfig.type = inType; + newConfig.getDefault(plan, name); + } else { + throw new NoSuchElementException("Superclass is not generic"); + } + + } catch (NoClassDefFoundError | ClassNotFoundException | NoSuchElementException | NoSuchMethodException | InstantiationException | + IllegalAccessException | InvocationTargetException | ClassCastException ignored) { + // Many ways for the generic inspection code to fail. NoClassDefFound is thrown when the service isn't installed + // We should probably only attempt to load configs for installed services + // NoSuchElementException is manually thrown when we can't find a generic superclass to inspect + // All others are checked exceptions thrown by the reflection utilities being used + log.info("could not find config class for {}, loading generalized ServiceConfig", inType); + ServiceConfig sc = new ServiceConfig(); + sc.type = inType; + plan.put(name, sc); + } } catch (Exception e) { Runtime.getInstance().error(e); } diff --git a/src/main/java/org/myrobotlab/service/config/ServoConfig.java b/src/main/java/org/myrobotlab/service/config/ServoConfig.java index 2e21ac76d8..8b6dcc7186 100644 --- a/src/main/java/org/myrobotlab/service/config/ServoConfig.java +++ b/src/main/java/org/myrobotlab/service/config/ServoConfig.java @@ -2,8 +2,13 @@ public class ServoConfig extends ServiceConfig { + /** + * The automatic disabling of the servo in idleTimeout ms This de-energizes + * the servo. By default this is disabled. + * + */ public boolean autoDisable = false; - // public String controller; + public boolean enabled = true; public Integer idleTimeout; diff --git a/src/main/java/org/myrobotlab/service/config/ServoMixerConfig.java b/src/main/java/org/myrobotlab/service/config/ServoMixerConfig.java new file mode 100644 index 0000000000..923a01d78c --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/ServoMixerConfig.java @@ -0,0 +1,22 @@ +package org.myrobotlab.service.config; + +public class ServoMixerConfig extends ServiceConfig { + + /** + * set autoDisable on "all" servos .. true - will make all servos autoDisable + * false - will make all servos autoDisable false null - will make no changes + */ + public boolean autoDisable = true; + + /** + * where gesture files are stored + */ + public String gesturesDir = "data/ServoMixer/gestures"; + + /** + * where pose files are stored + */ + public String posesDir = "data/ServoMixer/poses"; + + +} diff --git a/src/main/java/org/myrobotlab/service/config/SolrConfig.java b/src/main/java/org/myrobotlab/service/config/SolrConfig.java new file mode 100755 index 0000000000..33e574eb90 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/SolrConfig.java @@ -0,0 +1,11 @@ +package org.myrobotlab.service.config; + +public class SolrConfig extends ServiceConfig { + + // if you use embedded, the solrUrl is ignored + public boolean embedded = true; + // If embedded = false, then the following url will be used to search a solr cluster. + // This will/should be replaced with the zkHost and a SolrCloud client. + public String solrUrl = "http://localhost:8983/solr/collection1"; + +} diff --git a/src/main/java/org/myrobotlab/service/config/SpeechRecognizerConfig.java b/src/main/java/org/myrobotlab/service/config/SpeechRecognizerConfig.java index cfd28e1a71..53812ced82 100644 --- a/src/main/java/org/myrobotlab/service/config/SpeechRecognizerConfig.java +++ b/src/main/java/org/myrobotlab/service/config/SpeechRecognizerConfig.java @@ -13,6 +13,7 @@ public class SpeechRecognizerConfig extends ServiceConfig { public boolean recording = false; // probably should be removed as listeners[] already has this info + @Deprecated /* use ServiceConfig.listeners */ public String[] textListeners; /** diff --git a/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java b/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java index 86134e5245..f48ec59e49 100644 --- a/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java +++ b/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java @@ -2,9 +2,24 @@ public class UltrasonicSensorConfig extends ServiceConfig { + /** + * controller for the sensor + */ public String controller; + + /** + * pulse pin + */ public Integer triggerPin; + + /** + * listening pin + */ public Integer echoPin; - public Long timeout; + + /** + * 500 ms timeout default + */ + public Long timeout = 500L; } diff --git a/src/main/java/org/myrobotlab/service/config/WebGuiConfig.java b/src/main/java/org/myrobotlab/service/config/WebGuiConfig.java index 5f0674679c..4a1f389447 100644 --- a/src/main/java/org/myrobotlab/service/config/WebGuiConfig.java +++ b/src/main/java/org/myrobotlab/service/config/WebGuiConfig.java @@ -1,9 +1,18 @@ package org.myrobotlab.service.config; +import java.util.ArrayList; +import java.util.List; + public class WebGuiConfig extends ServiceConfig { public Integer port = 8888; public boolean autoStartBrowser = true; public boolean enableMdns = false; + public List resources = new ArrayList<>(); + + public WebGuiConfig() { + resources.add("./resource/WebGui/app"); + resources.add("./resource"); + } } diff --git a/src/main/java/org/myrobotlab/service/data/Locale.java b/src/main/java/org/myrobotlab/service/data/Locale.java index 9a6c35667c..dc5009c449 100644 --- a/src/main/java/org/myrobotlab/service/data/Locale.java +++ b/src/main/java/org/myrobotlab/service/data/Locale.java @@ -143,15 +143,31 @@ public String getDisplayCountry() { } public static Map getDefaults() { - +// Pulls all languages available from the OS, not useful to us +// Map locales = new TreeMap<>(); +// java.util.Locale[] ls = java.util.Locale.getAvailableLocales(); +// for (java.util.Locale l : ls) { +// Locale newLocale = new Locale(l.toString()); +// if (l.toString() != null && l.toString().length() != 0) { +// locales.put(newLocale.tag, newLocale); +// } +// } + // We really only support a few Locales dictated by ProgramAB, Polly, + // WebkitSpeechRecognition, & WebKitSpeechSynthesis Map locales = new TreeMap<>(); - java.util.Locale[] ls = java.util.Locale.getAvailableLocales(); - for (java.util.Locale l : ls) { - Locale newLocale = new Locale(l.toString()); - if (l.toString() != null && l.toString().length() != 0) { - locales.put(newLocale.tag, newLocale); - } - } + locales.put("cn-ZH", new Locale("cn-ZH")); + locales.put("de-DE", new Locale("de-DE")); + locales.put("en-US", new Locale("en-US")); + locales.put("es-ES", new Locale("es-ES")); + // locales.put("en-GB", new Locale("en-GB")); + locales.put("fi-FI", new Locale("fi-FI")); + locales.put("fr-FR", new Locale("fr-FR")); + locales.put("hi-IN", new Locale("hi-IN")); + locales.put("it-IT", new Locale("it-IT")); + locales.put("nl-NL", new Locale("nl-NL")); + locales.put("pt-PT", new Locale("pt-PT")); + locales.put("ru-RU", new Locale("ru-RU")); + locales.put("tr-TR", new Locale("tr-TR")); return locales; } diff --git a/src/main/java/org/myrobotlab/service/data/Script.java b/src/main/java/org/myrobotlab/service/data/Script.java index c47c64fc93..6652c74fa4 100644 --- a/src/main/java/org/myrobotlab/service/data/Script.java +++ b/src/main/java/org/myrobotlab/service/data/Script.java @@ -1,58 +1,46 @@ package org.myrobotlab.service.data; -import java.io.File; import java.io.Serializable; - +import java.util.Objects; + +/** + * A very basic POJO to transport scripts. Used in Python and Py4j and their + * front ends, and for saving or updating scripts. + * + * @author GroG + * + */ public class Script implements Serializable { + static final long serialVersionUID = 1L; /** * unique location & key of the script e.g. /mrl/scripts/myScript.py */ - File file; + public String file; /** * actual code/contents of the script */ - String code; + public String code; - public Script(String name, String script) { - this.file = new File(name); - // DOS2UNIX line endings. - // This seems to get triggered when people use editors that don't do - // the cr/lf thing very well.. - // TODO:This will break python quoted text with the """ syntax in - // python. - if (script != null) { - script = script.replaceAll("(\r)+\n", "\n"); - } + /** + * Convenient constructor + * + * @param file + * @param script + */ + public Script(String file, String script) { + this.file = file; this.code = script; } - public String getCode() { - return code; + @Override + public int hashCode() { + return Objects.hash(file, code); } - public String getName() { - if (file == null) { - return null; - } - // FIXME - display name - return file.getName(); + @Override + public String toString() { + return "Script{" + "file='" + file + '\'' + ", code='" + code + '\'' + '}'; } - public String getDisplayName() { - if (file == null) { - return null; - } - // FIXME - display name - return file.getName(); - } - - public void setCode(String code) { - this.code = code; - } - - public void setName(String name) { - // FIXME - logic for setting file ? - this.file = new File(name); - } } diff --git a/src/main/java/org/myrobotlab/service/data/Script2.java b/src/main/java/org/myrobotlab/service/data/Script2.java new file mode 100644 index 0000000000..ff5cbf1144 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/data/Script2.java @@ -0,0 +1,21 @@ +package org.myrobotlab.service.data; + +import java.io.Serializable; + +public class Script2 implements Serializable { + static final long serialVersionUID = 1L; + /** + * unique location & key of the script e.g. /mrl/scripts/myScript.py + */ + public String file; + /** + * actual code/contents of the script + */ + public String code; + + public Script2(String file, String script) { + this.file = file; + this.code = script; + } + +} diff --git a/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java b/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java index 52e15685bd..003b956f34 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java +++ b/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java @@ -6,7 +6,17 @@ public interface AudioControl { public double getVolume(); + /** + * plays an audiofile - is a listener function for publishAudioFile + * @param file + */ public void onPlayAudioFile(String file); + + /** + * must be a directory, will play one of the audio files within that directory + * @param dir + */ + public void onPlayRandomAudioFile(String dir); // pause // resume diff --git a/src/main/java/org/myrobotlab/service/interfaces/ButtonDefinition.java b/src/main/java/org/myrobotlab/service/interfaces/ButtonDefinition.java index fe8f0faae5..94e6074335 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/ButtonDefinition.java +++ b/src/main/java/org/myrobotlab/service/interfaces/ButtonDefinition.java @@ -2,14 +2,13 @@ import java.io.Serializable; -public class ButtonDefinition extends SensorDefinition implements Serializable { +public class ButtonDefinition implements Serializable { private static final long serialVersionUID = 1L; String axisName; Double value; public ButtonDefinition(String serviceName, String axisName) { - super(serviceName); this.axisName = axisName; } } diff --git a/src/main/java/org/myrobotlab/service/interfaces/DocumentListener.java b/src/main/java/org/myrobotlab/service/interfaces/DocumentListener.java index 54570ad011..7c0c9f46b0 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/DocumentListener.java +++ b/src/main/java/org/myrobotlab/service/interfaces/DocumentListener.java @@ -13,6 +13,7 @@ public interface DocumentListener { public ProcessingStatus onDocuments(List docs); + // TODO: maybe remove this from the interface. public boolean onFlush(); } diff --git a/src/main/java/org/myrobotlab/service/interfaces/DocumentPublisher.java b/src/main/java/org/myrobotlab/service/interfaces/DocumentPublisher.java index 9550e29a75..479dd6dfef 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/DocumentPublisher.java +++ b/src/main/java/org/myrobotlab/service/interfaces/DocumentPublisher.java @@ -4,10 +4,21 @@ public interface DocumentPublisher { + public static String[] publishMethods = new String[] { "publishDocument" , "publishFlush"}; + public String getName(); - + public Document publishDocument(Document doc); - - public void addDocumentListener(DocumentListener listener); + + public void publishFlush(); + + default public void attachDocumentListener(String name) { + for (String publishMethod : DocumentPublisher.publishMethods) { + addListener(publishMethod, name); + } + } + + // Add the addListener method to the interface all services implement this. + public void addListener(String topicMethod, String callbackName); } diff --git a/src/main/java/org/myrobotlab/service/interfaces/Executor.java b/src/main/java/org/myrobotlab/service/interfaces/Executor.java new file mode 100644 index 0000000000..e1566c015a --- /dev/null +++ b/src/main/java/org/myrobotlab/service/interfaces/Executor.java @@ -0,0 +1,32 @@ +package org.myrobotlab.service.interfaces; + +import org.myrobotlab.framework.interfaces.Invoker; + +/** + * Interface to a Executor - currently only utilized by Py4j to + * represent an interface for python from java-land, implemented + * by MessageHandler in /resource/Py4j/Py4j.py + * + * @author GroG + * + */ +public interface Executor extends Invoker { + + /** + * exec in Python - executes arbitrary code + * @param code + * @return + */ + public Object exec(String code); + + /** + * To use the service framework appropriately, you only need + * a reference to runtime and the service name. This method + * is required in Java-land to set the MessageHandler's name in + * Python-land + * @param name + * @return + */ + public String setName(String name); + +} diff --git a/src/main/java/org/myrobotlab/service/interfaces/Gateway.java b/src/main/java/org/myrobotlab/service/interfaces/Gateway.java index c113ee8dc0..783ec951ff 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/Gateway.java +++ b/src/main/java/org/myrobotlab/service/interfaces/Gateway.java @@ -28,12 +28,21 @@ import java.util.List; import java.util.Map; +import org.myrobotlab.framework.DescribeQuery; import org.myrobotlab.framework.Message; +import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.net.Connection; +import org.myrobotlab.service.Runtime; public interface Gateway extends NameProvider { + /** + * A constant used to determine whether a call to {@link Runtime#describe(String, DescribeQuery)} + * should fill the UUID field or not. + */ + String FILL_UUID_MAGIC_VAL = "fill-uuid"; + public void connect(String uri) throws Exception; // <-- FIXME invalid I // assume ? @@ -52,6 +61,31 @@ public interface Gateway extends NameProvider { public boolean isLocal(Message msg); - public Message getDescribeMsg(String connId); + /** + * Generates a message to be sent to the runtime + * instance at the given MRL instance. This message + * invokes the runtime's {@link org.myrobotlab.service.Runtime#describe(String, DescribeQuery)} + * method with a "fill-uuid" uuid parameter and a {@link DescribeQuery} + * with the given connId. + * + * @param connId The UUID that is being connected to + * @return A generated message that calls the remote runtime's {@code describe()} method + */ + default Message getDescribeMsg(String connId) { + // TODO support queries + // FIXME !!! - msg.name is wrong with only "runtime" it should be + // "runtime@id" + // TODO - lots of options for a default "describe" + + return Message.createMessage( + String.format("%s@%s", getName(), Runtime.get().getId()), + "runtime", + "describe", + new Object[] { + FILL_UUID_MAGIC_VAL, + new DescribeQuery(Platform.getLocalInstance().getId(), connId) + } + ); + } } diff --git a/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java b/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java index 71e4d40336..bdbeb9383c 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java +++ b/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java @@ -2,12 +2,12 @@ import java.io.Serializable; -public class PinDefinition extends SensorDefinition implements Serializable { +public class PinDefinition implements Serializable { private static final long serialVersionUID = 1L; /** - * label or name of the pin e.g. P0 D1 D2 etc... + * label or name of the pin e.g. P0, A5, D1, D2, GPIO 2, etc... */ String pin; @@ -25,6 +25,8 @@ public class PinDefinition extends SensorDefinition implements Serializable { * pin mode INPUT or OUTPUT, other... */ String mode; + + public String serviceName; /** * statistics @@ -90,9 +92,13 @@ public void setScl(boolean isScl) { * rate in Hz for which the pin will be polled 0 == no rate imposed */ int pollRateHz = 0; + + public PinDefinition() { + } + public PinDefinition(String serviceName, int address, String pin) { - super(serviceName); + this.serviceName = serviceName; this.address = address; this.pin = pin; } diff --git a/src/main/java/org/myrobotlab/service/interfaces/SelectListener.java b/src/main/java/org/myrobotlab/service/interfaces/SelectListener.java new file mode 100644 index 0000000000..3a6415a95c --- /dev/null +++ b/src/main/java/org/myrobotlab/service/interfaces/SelectListener.java @@ -0,0 +1,17 @@ +package org.myrobotlab.service.interfaces; + +/** + * A very generalized listener that listens to a "selected" method, where + * the selected is identified with a string and is of some interest. + * + * @author grog + * + */ +public interface SelectListener extends Listener { + + /** + * event and id of the "selected" + * @param selected + */ + public void onSelected(String selected); +} diff --git a/src/main/java/org/myrobotlab/service/interfaces/SensorDefinition.java b/src/main/java/org/myrobotlab/service/interfaces/SensorDefinition.java deleted file mode 100644 index 372e9b721f..0000000000 --- a/src/main/java/org/myrobotlab/service/interfaces/SensorDefinition.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.myrobotlab.service.interfaces; - -import java.io.Serializable; - -import org.myrobotlab.framework.interfaces.NameProvider; -import org.myrobotlab.math.interfaces.Mapper; - -public abstract class SensorDefinition implements NameProvider, Serializable { - private static final long serialVersionUID = 1L; - Mapper outputMapper; - String serviceName; - - public SensorDefinition(String SensorDefinition) { - this.serviceName = SensorDefinition; - } - - @Override - public String getName() { - return serviceName; - } - -} diff --git a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java index ca0b82ac9a..1a38dcaaf0 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java +++ b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java @@ -401,7 +401,7 @@ public interface ServoControl extends AbsolutePositionControl, EncoderListener, void writeMicroseconds(int uS); // for instance attachment - void attachServoController(String sc, Integer pin, Double pos, Double speed); + void attachServoController(String sc); /** * disable speed control and move the servos at full speed. diff --git a/src/main/java/org/myrobotlab/service/meta/BoofCVMeta.java b/src/main/java/org/myrobotlab/service/meta/BoofCVMeta.java new file mode 100644 index 0000000000..4312731854 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/meta/BoofCVMeta.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 BoofCVMeta extends MetaData { + private static final long serialVersionUID = 1L; + public final static Logger log = LoggerFactory.getLogger(BoofCVMeta.class); + + /** + * This static method returns all the details of the class without it having + * to be constructed. It has description, categories, dependencies, and peer + * definitions. + */ + public BoofCVMeta() { + + addDependency("org.boofcv", "boofcv-all", "0.40.1"); + addDescription("BoofCV computer vision service"); + addCategory("vision"); + } + +} diff --git a/src/main/java/org/myrobotlab/service/meta/BoofCvMeta.java b/src/main/java/org/myrobotlab/service/meta/BoofCvMeta.java deleted file mode 100644 index 476516ad43..0000000000 --- a/src/main/java/org/myrobotlab/service/meta/BoofCvMeta.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.myrobotlab.service.meta; - -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.service.meta.abstracts.MetaData; -import org.slf4j.Logger; - -public class BoofCvMeta extends MetaData { - private static final long serialVersionUID = 1L; - public final static Logger log = LoggerFactory.getLogger(BoofCvMeta.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 BoofCvMeta() { - - addDescription("a very portable vision library using pure Java"); - setAvailable(true); - // add dependency if necessary - addDependency("org.boofcv", "boofcv-core", "0.31"); - addDependency("org.boofcv", "boofcv-swing", "0.31"); - addDependency("org.boofcv", "boofcv-openkinect", "0.31"); - addCategory("vision", "video"); - } - -} diff --git a/src/main/java/org/myrobotlab/service/meta/DocumentPipelineMeta.java b/src/main/java/org/myrobotlab/service/meta/DocumentPipelineMeta.java index 7a1cdfc6bb..3a179e93be 100644 --- a/src/main/java/org/myrobotlab/service/meta/DocumentPipelineMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/DocumentPipelineMeta.java @@ -16,7 +16,21 @@ public DocumentPipelineMeta() { addDescription("This service will pass a document through a document processing pipeline made up of transformers"); addCategory("ingest"); - addDependency("org.apache.tika", "tika-core", "1.22"); + addDependency("org.apache.tika", "tika-core", "2.8.0"); + exclude("org.slf4j", "*"); + exclude("log4j", "*"); + exclude("org.apache.logging.log4j", "*"); + exclude("com.fasterxml.jackson.core", "*"); + exclude("io.netty", "*"); + + addDependency("org.apache.tika", "tika-parser-audiovideo-module", "2.8.0"); + exclude("org.slf4j", "*"); + exclude("log4j", "*"); + exclude("org.apache.logging.log4j", "*"); + exclude("com.fasterxml.jackson.core", "*"); + exclude("io.netty", "*"); + + addDependency("org.apache.opennlp", "opennlp-tools", "1.6.0"); addDependency("net.objecthunter", "exp4j", "0.4.8"); // for parsing wikitext diff --git a/src/main/java/org/myrobotlab/service/meta/JMonkeyEngineMeta.java b/src/main/java/org/myrobotlab/service/meta/JMonkeyEngineMeta.java index 1fd93a98b7..c089fda836 100644 --- a/src/main/java/org/myrobotlab/service/meta/JMonkeyEngineMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/JMonkeyEngineMeta.java @@ -26,6 +26,10 @@ public JMonkeyEngineMeta() { addDependency("org.jmonkeyengine", "jme3-bullet", jmeVersion); addDependency("org.jmonkeyengine", "jme3-bullet-native", jmeVersion); + + + + addDependency("org.jmonkeyengine", "jme3-plugins", jmeVersion); // addDependency("jme3utilities", "Minie", "0.6.2"); // "new" physics - ik forward kinematics ... diff --git a/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java b/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java index 03342dee4a..9a8d6107a8 100644 --- a/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java @@ -22,10 +22,10 @@ public JoystickMeta() { log.info("Joystick.getMetaData {} isArm() {}", platform, platform.isArm()); if (platform.isArm()) { - log.info("loading arm binaries"); + log.info("adding armv7 native dependencies"); addDependency("jinput-natives", "jinput-natives-armv7.hfp", "2.0.7", "zip"); } else { - log.info("loading non-arm binaries"); + log.info("adding jinput native dependencies"); addDependency("jinput-natives", "jinput-natives", "2.0.7", "zip"); } // addDependency("net.java.jinput", "jinput-platform", "2.0.7"); diff --git a/src/main/java/org/myrobotlab/service/meta/Py4jMeta.java b/src/main/java/org/myrobotlab/service/meta/Py4jMeta.java index 9e16837072..def9039a53 100644 --- a/src/main/java/org/myrobotlab/service/meta/Py4jMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/Py4jMeta.java @@ -1,5 +1,6 @@ package org.myrobotlab.service.meta; +import org.myrobotlab.framework.Platform; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.meta.abstracts.MetaData; import org.slf4j.Logger; @@ -18,6 +19,11 @@ public Py4jMeta() { addCategory("programming"); setSponsor("GroG"); addDependency("net.sf.py4j", "py4j", "0.10.9.7"); + + // Used just as a Python exe redistributable. + // ABSOLUTELY NO JNI/JNA IS USED + addDependency("org.bytedeco", "cpython-platform", "3.10.8-1.5.8"); + addDependency("org.bytedeco", "cpython", "3.10.8-1.5.8"); } } diff --git a/src/main/java/org/myrobotlab/service/meta/RuntimeMeta.java b/src/main/java/org/myrobotlab/service/meta/RuntimeMeta.java index 56ba7f31b5..f21e872171 100644 --- a/src/main/java/org/myrobotlab/service/meta/RuntimeMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/RuntimeMeta.java @@ -23,9 +23,11 @@ public RuntimeMeta() { addDependency("ch.qos.logback", "logback-classic", "1.2.3"); includeServiceInOneJar(true); - // apache 2.0 license - addDependency("com.google.code.gson", "gson", "2.8.5"); + + // for proxy generation + addDependency("net.bytebuddy", "byte-buddy", "1.12.16"); + // apache 2.0 license addDependency("com.fasterxml.jackson.core", "jackson-core", "2.14.0"); addDependency("com.fasterxml.jackson.core", "jackson-annotations", "2.14.0"); addDependency("com.fasterxml.jackson.core", "jackson-databind", "2.14.0"); diff --git a/src/main/java/org/myrobotlab/string/StringUtil.java b/src/main/java/org/myrobotlab/string/StringUtil.java index 6ea3e26531..411e8a9322 100644 --- a/src/main/java/org/myrobotlab/string/StringUtil.java +++ b/src/main/java/org/myrobotlab/string/StringUtil.java @@ -155,10 +155,7 @@ public static String StringToMethodName(String english) { public static boolean isEmpty(String v) { // return true if the string is null or empty. - if (v == null || "".equals(v)) { - return true; - } - return false; + return v == null || v.isEmpty(); } // split a string into sub strings that are a maxlength @@ -220,11 +217,11 @@ public static String removeAccents(String text) { * list of the integer values of the bytes. This ensures that the human * readable string values for the bytes are considered unsigned. (range 0-255) * not (-128 to 127) - * + * * @param bytes * input array to convert * @return string representing the bytes as integers - * + * */ public static String byteArrayToIntString(byte[] bytes) { if (bytes.length == 0) { @@ -251,4 +248,23 @@ public static String intArrayToString(int[] ints) { return builder.toString(); } + + /** + * Removes a trailing substring from the given + * string if it exists as the last component + * of the given string. + * + * @param fullString The string to remove the end from + * @param toRemove The string to be removed if {@code fullString.endsWith(toRemove) == true} + * @return fullString with toRemove stripped from only the end. + */ + public static String removeEnd(String fullString, String toRemove) { + if (isEmpty(fullString) || isEmpty(toRemove) || !fullString.endsWith(toRemove)) { + return fullString; + } + + + return fullString.substring(0, fullString.length() - toRemove.length()); + } + } diff --git a/src/main/resources/resource/Arduino/compile.activated.png b/src/main/resources/resource/Arduino/compile.activated.png deleted file mode 100644 index d8e1d6bf81..0000000000 Binary files a/src/main/resources/resource/Arduino/compile.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/compile.png b/src/main/resources/resource/Arduino/compile.png deleted file mode 100644 index 56e455f2fc..0000000000 Binary files a/src/main/resources/resource/Arduino/compile.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/compile.rollover.png b/src/main/resources/resource/Arduino/compile.rollover.png deleted file mode 100644 index 333833d455..0000000000 Binary files a/src/main/resources/resource/Arduino/compile.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/connect.activated.png b/src/main/resources/resource/Arduino/connect.activated.png deleted file mode 100644 index fa21979fb0..0000000000 Binary files a/src/main/resources/resource/Arduino/connect.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/connect.png b/src/main/resources/resource/Arduino/connect.png deleted file mode 100644 index 1e9b631354..0000000000 Binary files a/src/main/resources/resource/Arduino/connect.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/connect.rollover.png b/src/main/resources/resource/Arduino/connect.rollover.png deleted file mode 100644 index 7c0d8f6741..0000000000 Binary files a/src/main/resources/resource/Arduino/connect.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/fullscreen.activated.png b/src/main/resources/resource/Arduino/fullscreen.activated.png deleted file mode 100644 index 008c35cc5e..0000000000 Binary files a/src/main/resources/resource/Arduino/fullscreen.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/fullscreen.png b/src/main/resources/resource/Arduino/fullscreen.png deleted file mode 100644 index ad2f8e17f9..0000000000 Binary files a/src/main/resources/resource/Arduino/fullscreen.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/fullscreen.rollover.png b/src/main/resources/resource/Arduino/fullscreen.rollover.png deleted file mode 100644 index d7ebf9ceeb..0000000000 Binary files a/src/main/resources/resource/Arduino/fullscreen.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/monitor.activated.png b/src/main/resources/resource/Arduino/monitor.activated.png deleted file mode 100644 index 94b0423a8b..0000000000 Binary files a/src/main/resources/resource/Arduino/monitor.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/monitor.png b/src/main/resources/resource/Arduino/monitor.png deleted file mode 100644 index 5e77107eb1..0000000000 Binary files a/src/main/resources/resource/Arduino/monitor.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/monitor.rollover.png b/src/main/resources/resource/Arduino/monitor.rollover.png deleted file mode 100644 index 4d8cc95e86..0000000000 Binary files a/src/main/resources/resource/Arduino/monitor.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/new.activated.png b/src/main/resources/resource/Arduino/new.activated.png deleted file mode 100644 index 3086dd64a9..0000000000 Binary files a/src/main/resources/resource/Arduino/new.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/new.png b/src/main/resources/resource/Arduino/new.png deleted file mode 100644 index cef2658c0c..0000000000 Binary files a/src/main/resources/resource/Arduino/new.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/new.rollover.png b/src/main/resources/resource/Arduino/new.rollover.png deleted file mode 100644 index ae7635d284..0000000000 Binary files a/src/main/resources/resource/Arduino/new.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/open.activated.png b/src/main/resources/resource/Arduino/open.activated.png deleted file mode 100644 index 9f4bbc7485..0000000000 Binary files a/src/main/resources/resource/Arduino/open.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/open.png b/src/main/resources/resource/Arduino/open.png deleted file mode 100644 index 9cfbdc277b..0000000000 Binary files a/src/main/resources/resource/Arduino/open.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/open.rollover.png b/src/main/resources/resource/Arduino/open.rollover.png deleted file mode 100644 index c6707827fe..0000000000 Binary files a/src/main/resources/resource/Arduino/open.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/save.activated.png b/src/main/resources/resource/Arduino/save.activated.png deleted file mode 100644 index 3fa63005ee..0000000000 Binary files a/src/main/resources/resource/Arduino/save.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/save.png b/src/main/resources/resource/Arduino/save.png deleted file mode 100644 index 335a2ff1d1..0000000000 Binary files a/src/main/resources/resource/Arduino/save.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/save.rollover.png b/src/main/resources/resource/Arduino/save.rollover.png deleted file mode 100644 index 0f09dad3d4..0000000000 Binary files a/src/main/resources/resource/Arduino/save.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/upload.activated.png b/src/main/resources/resource/Arduino/upload.activated.png deleted file mode 100644 index 63e441618a..0000000000 Binary files a/src/main/resources/resource/Arduino/upload.activated.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/upload.png b/src/main/resources/resource/Arduino/upload.png deleted file mode 100644 index b6bda5a1fc..0000000000 Binary files a/src/main/resources/resource/Arduino/upload.png and /dev/null differ diff --git a/src/main/resources/resource/Arduino/upload.rollover.png b/src/main/resources/resource/Arduino/upload.rollover.png deleted file mode 100644 index 35a45be76f..0000000000 Binary files a/src/main/resources/resource/Arduino/upload.rollover.png and /dev/null differ diff --git a/src/main/resources/resource/AudioFile/audiofile.yml b/src/main/resources/resource/AudioFile/audiofile.yml index c4c3904321..df93f71509 100644 --- a/src/main/resources/resource/AudioFile/audiofile.yml +++ b/src/main/resources/resource/AudioFile/audiofile.yml @@ -5,7 +5,7 @@ currentTrack: default listeners: null mute: false peakDelayMs: null -peakMultiplier: 1.0 +peakMultiplier: 100.0 peakSampleInterval: 15.0 peers: null playlists: null diff --git a/src/main/resources/resource/BoofCv.png b/src/main/resources/resource/BoofCV.png similarity index 100% rename from src/main/resources/resource/BoofCv.png rename to src/main/resources/resource/BoofCV.png diff --git a/src/main/resources/resource/BoofCv/BoofCv.py b/src/main/resources/resource/BoofCV/BoofCV.py similarity index 100% rename from src/main/resources/resource/BoofCv/BoofCv.py rename to src/main/resources/resource/BoofCV/BoofCV.py diff --git a/src/main/resources/resource/BoofCv/basket_depth.png b/src/main/resources/resource/BoofCv/basket_depth.png deleted file mode 100644 index 1477305355..0000000000 Binary files a/src/main/resources/resource/BoofCv/basket_depth.png and /dev/null differ diff --git a/src/main/resources/resource/BoofCv/basket_rgb.png b/src/main/resources/resource/BoofCv/basket_rgb.png deleted file mode 100644 index 0bf3d57021..0000000000 Binary files a/src/main/resources/resource/BoofCv/basket_rgb.png and /dev/null differ diff --git a/src/main/resources/resource/BoofCv/intrinsic.yaml b/src/main/resources/resource/BoofCv/intrinsic.yaml deleted file mode 100644 index db7c7f9ca9..0000000000 --- a/src/main/resources/resource/BoofCv/intrinsic.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Pinhole camera model with radial and tangential distortion -# (fx,fy) = focal length, (cx,cy) = principle point, (width,height) = image shape -# radial = radial distortion, (t1,t2) = tangential distortion - -pinhole: - fx: 529.74137370586 - fy: 529.5715453060717 - cx: 312.57382117058427 - cy: 257.05061008728114 - width: 640 - height: 480 - skew: 0.0 -model: pinhole_radial_tangential -radial_tangential: - radial: - - 0.17889353480851655 - - -0.32301207366192053 - t1: 0.0 - t2: 0.0 diff --git a/src/main/resources/resource/BoofCv/visualdepth.yaml b/src/main/resources/resource/BoofCv/visualdepth.yaml deleted file mode 100644 index b911c9a497..0000000000 --- a/src/main/resources/resource/BoofCv/visualdepth.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# RGB Depth Camera Calibration -model: visual_depth -max_depth: 10000 -no_depth: 0 -intrinsic: - pinhole: - fx: 529.74137370586 - fy: 529.5715453060717 - cx: 312.57382117058427 - cy: 257.05061008728114 - width: 640 - height: 480 - skew: 0.0 - model: pinhole_radial_tangential - radial_tangential: - radial: - - 0.17889353480851655 - - -0.32301207366192053 - t1: 0.0 - t2: 0.0 \ No newline at end of file diff --git a/src/main/resources/resource/Docker/docker.yml b/src/main/resources/resource/Docker/docker.yml index 7febfdc2a0..1b6f69275a 100644 --- a/src/main/resources/resource/Docker/docker.yml +++ b/src/main/resources/resource/Docker/docker.yml @@ -1,19 +1,4 @@ !!org.myrobotlab.service.config.DockerConfig -autoDisable: false -clip: true -controller: null -enabled: true -idleTimeout: null -inverted: false listeners: null -maxIn: 180.0 -maxOut: 180.0 -minIn: 0.0 -minOut: 0.0 peers: null -pin: null -rest: 90.0 -speed: null -sweepMax: null -sweepMin: null type: Docker diff --git a/src/main/resources/resource/Email/email.yml b/src/main/resources/resource/Email/email.yml new file mode 100644 index 0000000000..f75bae4e8e --- /dev/null +++ b/src/main/resources/resource/Email/email.yml @@ -0,0 +1,13 @@ +!!org.myrobotlab.service.config.EmailConfig +format: text/html +from: null +host: null +listeners: null +pass: null +peers: null +port: 25 +properties: { + } +to: null +type: Email +user: null diff --git a/src/main/resources/resource/Gpt3/gpt3.yml b/src/main/resources/resource/Gpt3/gpt3.yml new file mode 100644 index 0000000000..e716fbde16 --- /dev/null +++ b/src/main/resources/resource/Gpt3/gpt3.yml @@ -0,0 +1,17 @@ +!!org.myrobotlab.service.config.Gpt3Config +currentUserName: null +engine: gpt-3.5-turbo +listeners: null +maxTokens: 256 +peers: + http: + autoStart: true + name: gpt3.http + type: HttpClient +sleepWord: sleep +sleeping: false +temperature: 0.7 +token: null +type: Gpt3 +url: https://api.openai.com/v1/chat/completions +wakeWord: wake diff --git a/src/main/resources/resource/InMoov2/inmoov2.yml b/src/main/resources/resource/InMoov2/inmoov2.yml index b301c86bc1..ce01362503 100644 --- a/src/main/resources/resource/InMoov2/inmoov2.yml +++ b/src/main/resources/resource/InMoov2/inmoov2.yml @@ -1,143 +1 @@ -!!org.myrobotlab.service.config.InMoov2Config -data: { - } -heartbeat: true -idleTimer: true -listeners: null -loadGestures: true -locale: en-US -peers: - audioPlayer: - autoStart: true - name: inmoov2.audioPlayer - type: AudioFile - chatBot: - autoStart: false - name: inmoov2.chatBot - type: ProgramAB - controller3: - autoStart: false - name: inmoov2.controller3 - type: Arduino - controller4: - autoStart: false - name: inmoov2.controller4 - type: Arduino - ear: - autoStart: false - name: inmoov2.ear - type: WebkitSpeechRecognition - eyeTracking: - autoStart: false - name: inmoov2.eyeTracking - type: Tracking - fsm: - autoStart: false - name: inmoov2.fsm - type: FiniteStateMachine - head: - autoStart: false - name: inmoov2.head - type: InMoov2Head - headTracking: - autoStart: false - name: inmoov2.headTracking - type: Tracking - htmlFilter: - autoStart: false - name: inmoov2.htmlFilter - type: HtmlFilter - imageDisplay: - autoStart: false - name: inmoov2.imageDisplay - type: ImageDisplay - leap: - autoStart: false - name: inmoov2.leap - type: LeapMotion - left: - autoStart: false - name: inmoov2.left - type: Arduino - leftArm: - autoStart: false - name: inmoov2.leftArm - type: InMoov2Arm - leftHand: - autoStart: false - name: inmoov2.leftHand - type: InMoov2Hand - mouth: - autoStart: false - name: inmoov2.mouth - type: MarySpeech - mouthControl: - autoStart: false - name: inmoov2.mouthControl - type: MouthControl - neoPixel: - autoStart: false - name: inmoov2.neoPixel - type: NeoPixel - openWeatherMap: - autoStart: false - name: inmoov2.openWeatherMap - type: OpenWeatherMap - opencv: - autoStart: false - name: inmoov2.opencv - type: OpenCV - openni: - autoStart: false - name: inmoov2.openni - type: OpenNi - pid: - autoStart: false - name: inmoov2.pid - type: Pid - pir: - autoStart: false - name: inmoov2.pir - type: Pir - random: - autoStart: false - name: inmoov2.random - type: Random - right: - autoStart: false - name: inmoov2.right - type: Arduino - rightArm: - autoStart: false - name: inmoov2.rightArm - type: InMoov2Arm - rightHand: - autoStart: false - name: inmoov2.rightHand - type: InMoov2Hand - servoMixer: - autoStart: false - name: inmoov2.servoMixer - type: ServoMixer - simulator: - autoStart: false - name: inmoov2.simulator - type: JMonkeyEngine - torso: - autoStart: false - name: inmoov2.torso - type: InMoov2Torso - ultrasonicLeft: - autoStart: false - name: inmoov2.ultrasonicLeft - type: UltrasonicSensor - ultrasonicRight: - autoStart: false - name: inmoov2.ultrasonicRight - type: UltrasonicSensor -pirEnableTracking: false -pirPlaySounds: true -pirWakeUp: false -shutdownStartupSpeed: 50.0 -type: InMoov2 -virtual: false +hello diff --git a/src/main/resources/resource/IntegratedMovement/IntegratedMovement.py b/src/main/resources/resource/IntegratedMovement/IntegratedMovement.py index e5688a512f..786f6e82e7 100644 --- a/src/main/resources/resource/IntegratedMovement/IntegratedMovement.py +++ b/src/main/resources/resource/IntegratedMovement/IntegratedMovement.py @@ -24,7 +24,7 @@ i01 = runtime.start("i01","InMoov2") -i01.startAll(leftPort,rightPort) +# i01.startAll(leftPort,rightPort) # no longer a method #configure servo diff --git a/src/main/resources/resource/Intro/InMoov01_start.py b/src/main/resources/resource/Intro/InMoov01_start.py deleted file mode 100644 index f03e5038bd..0000000000 --- a/src/main/resources/resource/Intro/InMoov01_start.py +++ /dev/null @@ -1,8 +0,0 @@ -######################################### -# InMoov01_start.py -# categories: inmoov -# more info @: http://myrobotlab.org/service/InMoov -######################################### -# uncomment for virtual hardware -# Platform.setVirtual(True) -i01 = runtime.start('i01', 'InMoov2') \ No newline at end of file diff --git a/src/main/resources/resource/JMonkeyEngine/assets/Models/README.md b/src/main/resources/resource/JMonkeyEngine/assets/Models/README.md new file mode 100644 index 0000000000..5a270c4598 --- /dev/null +++ b/src/main/resources/resource/JMonkeyEngine/assets/Models/README.md @@ -0,0 +1,3 @@ +==JMonkeyEngine Model Directory== +Models can be dropped in this directory and when JMonkeyEngine service starts +it will automatically scan and load them. diff --git a/src/main/resources/resource/JMonkeyEngine/jmonkeyengine.yml b/src/main/resources/resource/JMonkeyEngine/jmonkeyengine.yml index af8b15e8b5..da8288c40d 100644 --- a/src/main/resources/resource/JMonkeyEngine/jmonkeyengine.yml +++ b/src/main/resources/resource/JMonkeyEngine/jmonkeyengine.yml @@ -8,4 +8,6 @@ multiMapped: { nodes: { } peers: null +test: !!set { + } type: JMonkeyEngine diff --git a/src/main/resources/resource/LeapMotion/leapmotion.yml b/src/main/resources/resource/LeapMotion/leapmotion.yml index bf030ebc9b..e7f2bfa93f 100644 --- a/src/main/resources/resource/LeapMotion/leapmotion.yml +++ b/src/main/resources/resource/LeapMotion/leapmotion.yml @@ -1,5 +1,4 @@ !!org.myrobotlab.service.config.LeapMotionConfig -# mappings for all fingers leftIndex: maxIn: 180 maxOut: 180 @@ -25,29 +24,31 @@ leftThumb: maxOut: 180 minIn: 0 minOut: 0 -rightIndex: # minIn 10 maxIn 100 minOut 180 maxOut 0 - maxIn: 100 - maxOut: 0 - minIn: 10 - minOut: 180 +listeners: null +peers: null +rightIndex: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 rightMiddle: - maxIn: 100 - maxOut: 0 - minIn: 10 - minOut: 180 + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 rightPinky: - maxIn: 100 - maxOut: 0 - minIn: 10 - minOut: 180 + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 rightRing: - maxIn: 100 - maxOut: 0 - minIn: 10 - minOut: 180 + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 rightThumb: - maxIn: 100 - maxOut: 0 - minIn: 10 - minOut: 180 + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 type: LeapMotion diff --git a/src/main/resources/resource/LeapMotion2/leapmotion2.yml b/src/main/resources/resource/LeapMotion2/leapmotion2.yml new file mode 100644 index 0000000000..5841a8de45 --- /dev/null +++ b/src/main/resources/resource/LeapMotion2/leapmotion2.yml @@ -0,0 +1,56 @@ +!!org.myrobotlab.service.config.LeapMotion2Config +leftIndex: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +leftMiddle: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +leftPinky: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +leftRing: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +leftThumb: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +listeners: null +peers: null +rightIndex: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +rightMiddle: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +rightPinky: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +rightRing: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +rightThumb: + maxIn: 180 + maxOut: 180 + minIn: 0 + minOut: 0 +tracking: true +type: LeapMotion2 +websocketUrl: ws://127.0.0.1:6437 diff --git a/src/main/resources/resource/LocalSpeech/localspeech.yml b/src/main/resources/resource/LocalSpeech/localspeech.yml index e8e64cd244..bdb6967076 100644 --- a/src/main/resources/resource/LocalSpeech/localspeech.yml +++ b/src/main/resources/resource/LocalSpeech/localspeech.yml @@ -7,6 +7,7 @@ peers: autoStart: true name: localspeech.audioFile type: AudioFile +replaceChars: null speechRecognizers: null speechType: null substitutions: null diff --git a/src/main/resources/resource/Motor/motor.yml b/src/main/resources/resource/Motor/motor.yml index 1e07942f69..f6101c3861 100644 --- a/src/main/resources/resource/Motor/motor.yml +++ b/src/main/resources/resource/Motor/motor.yml @@ -1,13 +1,16 @@ !!org.myrobotlab.service.config.MotorConfig -clip: false +axis: null +controller: null dirPin: null -inverted: false listeners: null locked: false -maxIn: null -maxOut: null -minIn: null -minOut: null +mapper: + clip: true + inverted: false + maxIn: 100.0 + maxOut: 100.0 + minIn: -100.0 + minOut: -100.0 peers: null pwmFreq: null pwrPin: null diff --git a/src/main/resources/resource/MotorDualPwm/motordualpwm.yml b/src/main/resources/resource/MotorDualPwm/motordualpwm.yml index 4cd5077442..5ca7e4276f 100644 --- a/src/main/resources/resource/MotorDualPwm/motordualpwm.yml +++ b/src/main/resources/resource/MotorDualPwm/motordualpwm.yml @@ -1,13 +1,16 @@ !!org.myrobotlab.service.config.MotorDualPwmConfig -clip: false -inverted: false +axis: null +controller: null leftPwmPin: null listeners: null locked: false -maxIn: null -maxOut: null -minIn: null -minOut: null +mapper: + clip: true + inverted: false + maxIn: 100.0 + maxOut: 100.0 + minIn: -100.0 + minOut: -100.0 peers: null pwmFreq: null rightPwmPin: null diff --git a/src/main/resources/resource/MotorHat4Pi/motorhat4pi.yml b/src/main/resources/resource/MotorHat4Pi/motorhat4pi.yml index 2d8f057011..699880fa60 100644 --- a/src/main/resources/resource/MotorHat4Pi/motorhat4pi.yml +++ b/src/main/resources/resource/MotorHat4Pi/motorhat4pi.yml @@ -1,12 +1,15 @@ !!org.myrobotlab.service.config.MotorHat4PiConfig -clip: false -inverted: false +axis: null +controller: null listeners: null locked: false -maxIn: null -maxOut: null -minIn: null -minOut: null +mapper: + clip: true + inverted: false + maxIn: 100.0 + maxOut: 100.0 + minIn: -100.0 + minOut: -100.0 motorId: null peers: null type: MotorHat4Pi diff --git a/src/main/resources/resource/MotorPort/motorport.yml b/src/main/resources/resource/MotorPort/motorport.yml index f378133578..77838cce2c 100644 --- a/src/main/resources/resource/MotorPort/motorport.yml +++ b/src/main/resources/resource/MotorPort/motorport.yml @@ -1,12 +1,15 @@ !!org.myrobotlab.service.config.MotorPortConfig -clip: false -inverted: false +axis: null +controller: null listeners: null locked: false -maxIn: null -maxOut: null -minIn: null -minOut: null +mapper: + clip: true + inverted: false + maxIn: 100.0 + maxOut: 100.0 + minIn: -100.0 + minOut: -100.0 peers: null port: null type: MotorPort diff --git a/src/main/resources/resource/Mpr121/mpr121.yml b/src/main/resources/resource/Mpr121/mpr121.yml new file mode 100644 index 0000000000..0b517ff04e --- /dev/null +++ b/src/main/resources/resource/Mpr121/mpr121.yml @@ -0,0 +1,8 @@ +!!org.myrobotlab.service.config.Mpr121Config +address: '0x5A' +bus: '1' +controller: null +listeners: null +peers: null +rateHz: 1.0 +type: Mpr121 diff --git a/src/main/resources/resource/NeoPixel/neopixel.yml b/src/main/resources/resource/NeoPixel/neopixel.yml index ee04834797..9bd351414e 100644 --- a/src/main/resources/resource/NeoPixel/neopixel.yml +++ b/src/main/resources/resource/NeoPixel/neopixel.yml @@ -1,10 +1,12 @@ !!org.myrobotlab.service.config.NeoPixelConfig +autoClear: true blue: 0 brightness: 255 controller: null currentAnimation: null fill: false green: 0 +idleTimeout: 1000 listeners: null peers: null pin: null diff --git a/src/main/resources/resource/Pcf8574/pcf8574.yml b/src/main/resources/resource/Pcf8574/pcf8574.yml index 1d73647e92..1bf13972fc 100644 --- a/src/main/resources/resource/Pcf8574/pcf8574.yml +++ b/src/main/resources/resource/Pcf8574/pcf8574.yml @@ -1,7 +1,8 @@ !!org.myrobotlab.service.config.Pcf8574Config -address: '0x38' +address: '0x20' bus: '1' controller: null listeners: null peers: null +rateHz: 1.0 type: Pcf8574 diff --git a/src/main/resources/resource/Pir/pir.yml b/src/main/resources/resource/Pir/pir.yml index 9e0851fc7c..ca6c21883a 100644 --- a/src/main/resources/resource/Pir/pir.yml +++ b/src/main/resources/resource/Pir/pir.yml @@ -1,8 +1,8 @@ !!org.myrobotlab.service.config.PirConfig controller: null -enable: false +enable: true listeners: null peers: null pin: null -rate: null +rate: 1 type: Pir diff --git a/src/main/resources/resource/ProgramAB/programab.yml b/src/main/resources/resource/ProgramAB/programab.yml deleted file mode 100644 index c96b3555e6..0000000000 --- a/src/main/resources/resource/ProgramAB/programab.yml +++ /dev/null @@ -1,15 +0,0 @@ -!!org.myrobotlab.service.config.ProgramABConfig -botDir: null -bots: [ - ] -currentBotName: Alice -currentUserName: null -listeners: null -peers: - search: - autoStart: true - name: programab.search - type: Wikipedia -sleep: false -textFilters: null -type: ProgramAB diff --git a/src/main/resources/resource/Py4j/Py4j.py b/src/main/resources/resource/Py4j/Py4j.py index 75fd6779cd..0cdb5b3bce 100644 --- a/src/main/resources/resource/Py4j/Py4j.py +++ b/src/main/resources/resource/Py4j/Py4j.py @@ -5,60 +5,126 @@ # Java objects in a Java Virtual Machine. # Methods are called as if the Java objects resided in the Python interpreter and # Java collections can be accessed through standard Python collection methods. -# Py4J also enables Java programs to call back Python objects. Py4J is distributed under the BSD +# Py4J also enables Java programs to call back Python objects. Py4J is distributed under the BSD license # Python 2.7 -to- 3.x is supported -# to use you will need the py4j lib - -# run in mrl instance in Jython or start manually -# py4j = runtime.start("py4j","Py4j") - -# start the listening socket on the gateway -# py4j.start() - -######################################## # In your python 3.x project # pip install py4j # you have full access to mrl instance that's running # the gateway -# import the gateway import sys -from time import sleep -from py4j.clientserver import ClientServer, JavaParameters, PythonParameters -from py4j.java_collections import JavaList -from py4j.java_gateway import JavaGateway, CallbackServerParameters -from py4j.java_collections import JavaArray, JavaObject, JavaClass +from py4j.java_collections import JavaObject, JavaClass +from py4j.java_gateway import JavaGateway, CallbackServerParameters, GatewayParameters + +runtime = None + class MessageHandler(object): + """ + The class responsible for receiving and processing Py4j messages, + including handling `invoke()` and `exec()` requests. Class + must be initialized and then the `setName()` method must be invoked before + the Java and Python sides can talk correctly. + """ def __init__(self): + global runtime # initializing stdout and stderr + print("initializing") + self.name = None self.stdout = sys.stdout self.stderr = sys.stderr sys.stdout = self sys.stderr = self + self.gateway = JavaGateway(callback_server_parameters=CallbackServerParameters(), + python_server_entry_point=self, + gateway_parameters=GatewayParameters(auto_convert=True)) + self.runtime = self.gateway.jvm.org.myrobotlab.service.Runtime.getInstance() + runtime = self.runtime + self.py4j = None # need to wait until name is set + print("initialized ... waiting for name to be set") + + # Define the callback function + def handle_connection_break(self): + # Add your custom logic here to handle the connection break + print("Connection with Java gateway was lost or terminated.") + print("goodbye.") + sys.exit(1) def write(self,string): - # TODO find out how to do service binding with name - global py4j, runtime - py4j = runtime.getService('py4j') - # py4j.invoke('publishStdOut', string) - py4j.handleStdOut(string) - # py4j.info(string) - # runtime.send('py4j', 'publishStdOut', string) - # runtime.info(string) - - - def invoke(self, method, data=None): + if (self.py4j): + self.py4j.handleStdOut(string) + + def flush(self): + pass + + def setName(self, name): + """ + Called after initialization completed in order + for this Python side handler to know how to contact the Java-side + service. + + :param name: Name of the Java-side Py4j service this script is linked to, preferably as a full name. + :type name: str + """ + print("name set to", name) + self.name = name + self.py4j = self.runtime.getService(name) + print(self.runtime.getUptime()) + + print("python started", sys.version) + print("runtime attached", self.runtime.getVersion()) + print("reference to runtime") + # TODO print env vars PYTHONPATH etc + return name + + def exec(self, code): + """ + Executes Python code in the global namespace. + All exceptions are caught and printed so that the Python subprocess doesn't crash. + + :param code: The Python code to execute. + :type code: str + """ + try: + # Restricts the exec() to the global namespace, + # so the code is executed as if it were the top level of a module + exec(code, globals()) + except Exception as e: + print(e) + + def invoke(self, method, data=()): + """ + Invoke a function from the global namespace with the given parameters. + + :param method: The name of the function to invoke. + :type method: str + :param data: The parameters to pass to the function, defaulting to no parameters. + :type data: Iterable + """ + # convert to list params = list(data) - eval(method)(*params) - def flush(self): - pass + # Lookup the method in the global namespace + # Much much faster than using eval() + globals()[method](*params) + + def shutdown(self): + """ + Shutdown the Py4j gateway + :return: + """ + self.gateway.shutdown() def convert_array(self, array): + """ + Utility method used by Py4j to convert arrays of Java objects + into equivalent Python lists. + :param array: The array to convert + :return: The converted array + """ result = [] for item in array: if isinstance(item, JavaObject): @@ -88,21 +154,9 @@ def convert_array(self, array): class Java: implements = ['org.myrobotlab.framework.interfaces.Invoker'] -handler = MessageHandler() -gateway = JavaGateway(callback_server_parameters=CallbackServerParameters(), python_server_entry_point=handler) - -runtime = gateway.jvm.org.myrobotlab.service.Runtime.getInstance() -py4j = runtime.start("py4j","Py4j") - -print(runtime.getUptime()) -# example callback methods -# def test(text): -# print('onTest', text) - -# def test2(text, value): -# print('onTest2', text, value) - -# TODO spin - -sleep(1000) \ No newline at end of file +handler = MessageHandler() +if len(sys.argv) > 1: + handler.setName(sys.argv[1]) +else: + raise RuntimeError("This script requires the full name of the Py4j service as its first command-line argument") diff --git a/src/main/resources/resource/Py4j/py4j.yml b/src/main/resources/resource/Py4j/py4j.yml index 924dbc4b49..59a5767286 100644 --- a/src/main/resources/resource/Py4j/py4j.yml +++ b/src/main/resources/resource/Py4j/py4j.yml @@ -1,4 +1,6 @@ -!!org.myrobotlab.service.config.ServiceConfig +!!org.myrobotlab.service.config.Py4jConfig listeners: null peers: null +scriptRootDir: null +useBundledPython: true type: Py4j diff --git a/src/main/resources/resource/Py4j/python.exe b/src/main/resources/resource/Py4j/python.exe new file mode 100644 index 0000000000..0c68b4c6a7 Binary files /dev/null and b/src/main/resources/resource/Py4j/python.exe differ diff --git a/src/main/resources/resource/Py4j/pythonw.exe b/src/main/resources/resource/Py4j/pythonw.exe new file mode 100644 index 0000000000..01861b4353 Binary files /dev/null and b/src/main/resources/resource/Py4j/pythonw.exe differ diff --git a/src/main/resources/resource/Python/python.yml b/src/main/resources/resource/Python/python.yml index 221745e2db..3f3561be22 100644 --- a/src/main/resources/resource/Python/python.yml +++ b/src/main/resources/resource/Python/python.yml @@ -3,6 +3,7 @@ listeners: null modulePaths: [ ] peers: null +scriptRootDir: null startScripts: [ ] stopScripts: [ diff --git a/src/main/resources/resource/PythonProxy/PythonProxy.py b/src/main/resources/resource/PythonProxy/PythonProxy.py deleted file mode 100755 index 727fd2b842..0000000000 --- a/src/main/resources/resource/PythonProxy/PythonProxy.py +++ /dev/null @@ -1,2 +0,0 @@ -# TODO: Implement this script fpr -pythonproxy = runtime.start("pythonproxy","PythonProxy")... \ No newline at end of file diff --git a/src/main/resources/resource/RasPi/diagram.png b/src/main/resources/resource/RasPi/diagram.png new file mode 100644 index 0000000000..c37c17c776 Binary files /dev/null and b/src/main/resources/resource/RasPi/diagram.png differ diff --git a/src/main/resources/resource/RasPi/i2c-scan.png b/src/main/resources/resource/RasPi/i2c-scan.png new file mode 100644 index 0000000000..4648fe46dc Binary files /dev/null and b/src/main/resources/resource/RasPi/i2c-scan.png differ diff --git a/src/main/resources/resource/RasPi/raspi.yml b/src/main/resources/resource/RasPi/raspi.yml index 9a416b66b5..5ad257bb7d 100644 --- a/src/main/resources/resource/RasPi/raspi.yml +++ b/src/main/resources/resource/RasPi/raspi.yml @@ -1,4 +1,5 @@ !!org.myrobotlab.service.config.RasPiConfig listeners: null peers: null +pollRateHz: 1 type: RasPi diff --git a/src/main/resources/resource/Runtime/runtime.yml b/src/main/resources/resource/Runtime/runtime.yml index 132b3cceea..640274b38f 100644 --- a/src/main/resources/resource/Runtime/runtime.yml +++ b/src/main/resources/resource/Runtime/runtime.yml @@ -1,10 +1,11 @@ !!org.myrobotlab.service.config.RuntimeConfig enableCli: true +id: null listeners: null locale: null -logLevel: null +logLevel: info peers: null registry: [ ] type: Runtime -virtual: null +virtual: false diff --git a/src/main/resources/resource/Solr/core1/conf/managed-schema b/src/main/resources/resource/Solr/core1/conf/managed-schema index 4a1f758e75..5c5a46c87c 100755 --- a/src/main/resources/resource/Solr/core1/conf/managed-schema +++ b/src/main/resources/resource/Solr/core1/conf/managed-schema @@ -2,6 +2,131 @@ id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -418,88 +543,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/resource/Solr/core1/conf/solrconfig.xml b/src/main/resources/resource/Solr/core1/conf/solrconfig.xml index c5f7134dbc..c0bacba5bc 100755 --- a/src/main/resources/resource/Solr/core1/conf/solrconfig.xml +++ b/src/main/resources/resource/Solr/core1/conf/solrconfig.xml @@ -290,8 +290,9 @@ have some sort of hard autoCommit to limit the log size. --> - ${solr.autoCommit.maxTime:15000} - false + + ${solr.autoCommit.maxTime:1000} + true + autowarmCount="32"/> + autowarmCount="32"/> + autowarmCount="32"/> got describe: and set jsRuntimeMethodCallbackMap') - let hello = JSON.parse(request.data[1]) + hello = request.data[1] remotePlatform = hello.platform // FIXME - remove this - there aren't 1 remoteId there are many ! @@ -603,7 +603,7 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { } this.getService = function(name) { - + id = _self.remoteId if (registry[_self.getFullName(name)] == null) { return null } @@ -872,7 +872,7 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { let properties = [] - let exclude = ['serviceType', 'id', 'simpleName', 'interfaceSet', 'serviceClass', 'statusBroadcastLimitMs', 'isRunning', 'name', 'creationOrder', 'serviceType'] + let exclude = ['serviceType', 'id', 'simpleName', 'interfaceSet', 'typeKey', 'statusBroadcastLimitMs', 'isRunning', 'name', 'creationOrder', 'serviceType'] // FIXME - extract from javadoc ! let info = { @@ -943,7 +943,7 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { lastPosY += 40 zIndex++ //construct panel & add it to dictionary - panels[fullname] = createPanel(fullname, service.serviceClass, 15, lastPosY, 800, 0, zIndex) + panels[fullname] = createPanel(fullname, service.typeKey, 15, lastPosY, 800, 0, zIndex) return panels[fullname] } @@ -959,10 +959,12 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { let createPanel = function(fullname, type, x, y, width, height, zIndex, data) { + let displayName = fullname.endsWith(_self.remoteId)?_self.getShortName(fullname):fullname + console.error('createPanel', _self.remoteId, displayName) let panel = { simpleName: _self.getSimpleName(type), name: fullname, - displayName: _self.getShortName(fullname), + displayName: displayName, //the state the loading of the template is in (loading, loaded, notfound) - probably can be removed templatestatus: null, @@ -1107,7 +1109,7 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { } panels[newPanel.name].name = newPanel.name - panels[newPanel.name].displayName = _self.getShortName(newPanel.name) + panels[newPanel.name].displayName = _self.getShortName(newPanel.name) if (newPanel.simpleName) { panels[newPanel.name].simpleName = newPanel.simpleName } @@ -1369,11 +1371,11 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { // console.log("here") // expected 'framework' level subscriptions - we should at a minimum // be interested in state and status changes of the services - _self.sendTo(name, "addListener", "publishStatus", 'runtime@' + _self.id) - _self.sendTo(name, "addListener", "publishState", 'runtime@' + _self.id) - _self.sendTo(name, "addListener", "getMethodMap", 'runtime@' + _self.id) + _self.sendTo(_self.getFullName(name), "addListener", "publishStatus", 'runtime@' + _self.id) + _self.sendTo(_self.getFullName(name), "addListener", "publishState", 'runtime@' + _self.id) + _self.sendTo(_self.getFullName(name), "addListener", "getMethodMap", 'runtime@' + _self.id) - _self.sendTo(name, "broadcastState") + _self.sendTo(_self.getFullName(name), "broadcastState") // below we subscribe to the Angular callbacks - where anything sent // back from the webgui with our service's name on the message - send // it to our onMsg method @@ -1463,41 +1465,40 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { return arrayOfServices }, - controllerscope: _self.controllerscope, - setSearchFunction: _self.setSearchFunction, - setNavCtrl: _self.setNavCtrl, - setTabsViewCtrl: _self.setTabsViewCtrl, - error: _self.error, changeTab: _self.changeTab, - goBack: _self.goBack, - search: _self.search, + controllerscope: _self.controllerscope, createMessage: _self.createMessage, display: _self.display, + error: _self.error, + getDisplayName: _self.getDisplayName, getDisplayImages: getDisplayImages, - setDisplayCallback: setDisplayCallback, - subscribeToUpdates: _self.subscribeToUpdates, - subscribeToRegistered: _self.subscribeToRegistered, - subscribeToReleased: _self.subscribeToReleased, - getPanelList: _self.getPanelList, + getFullName: _self.getFullName, getPanel: _self.getPanel, - sendTo: _self.sendTo, + getPanelList: _self.getPanelList, + getProperties: _self.getProperties, getShortName: _self.getShortName, getSimpleName: _self.getSimpleName, getStyle: _self.getStyle, - subscribe: _self.subscribe, - unsubscribe: _self.unsubscribe, + goBack: _self.goBack, isPeerStarted: _self.isPeerStarted, - subscribeToService: _self.subscribeToService, - getFullName: _self.getFullName, + search: _self.search, + sendMessage: _self.sendMessage, sendBlockingMessage: _self.sendBlockingMessage, + sendTo: _self.sendTo, + setNavCtrl: _self.setNavCtrl, + setDisplayCallback: setDisplayCallback, + setSearchFunction: _self.setSearchFunction, + setTabsViewCtrl: _self.setTabsViewCtrl, + subscribe: _self.subscribe, subscribeConnected: _self.subscribeConnected, + subscribeTo: _self.subscribeTo, subscribeToMethod: _self.subscribeToMethod, + subscribeToReleased: _self.subscribeToReleased, + subscribeToRegistered: _self.subscribeToRegistered, + subscribeToService: _self.subscribeToService, subscribeToServiceMethod: _self.subscribeToServiceMethod, - subscribeTo: _self.subscribeTo, - // better name - getProperties: _self.getProperties, - sendMessage: _self.sendMessage, - // setViewType: _self.setViewType, + subscribeToUpdates: _self.subscribeToUpdates, + unsubscribe: _self.unsubscribe, interfaceToPossibleServices: _self.interfaceToPossibleServices } @@ -1570,7 +1571,7 @@ angular.module('mrlapp.mrl', []).provider('mrl', [function() { "name": "runtime", "id": "webgui-client-1234-5678", "simpleName": "Runtime", - "serviceClass": "org.myrobotlab.service.Runtime", + "typeKey": "org.myrobotlab.service.Runtime", "isRunning": true, "interfaceSet": { "org.myrobotlab.client.Client$RemoteMessageHandler": "org.myrobotlab.client.Client$RemoteMessageHandler", diff --git a/src/main/resources/resource/WebGui/app/service/css/SpotGui.css b/src/main/resources/resource/WebGui/app/service/css/SpotGui.css index 5bb2ebf47d..8782ff4b91 100644 --- a/src/main/resources/resource/WebGui/app/service/css/SpotGui.css +++ b/src/main/resources/resource/WebGui/app/service/css/SpotGui.css @@ -272,7 +272,7 @@ #startButton{ position:absolute; width:83px; - left: 216px; + left: 152px; top: 528px; } @@ -299,7 +299,7 @@ #gesturesButton{ position:absolute; width:122px; - left: 314px; + left: 246px; top: 528px; } @@ -320,3 +320,30 @@ box-shadow: 0 0px 5px 2px rgb(218, 94, 94); cursor:default; } + +/* ------------------- gestures position --------------------*/ + +#simulatorButton{ + position:absolute; + width:122px; + left: 380px; + top: 528px; +} + +.rectangleSimulator{ + position:absolute; + display: table-cell; + border-radius: 5px; + background: #73AD21; + font-family: helvetica,sans-serif; + font-size: 18px; + font-weight: normal; + color: #feb331; + line-height: 40px; + background-image: linear-gradient(#001720, #01424c); + text-align: center; + vertical-align: middle; + text-shadow: 0 0 3px #3e2e2e, 0 0 20px #fff, 0 0 30px #00a5cb, 0 0 40px #74ace6, 0 0 50px #0042a4, 0 0 60px #3582f5, 0 0 70px #4954ff; + box-shadow: 0 0px 5px 2px rgb(218, 94, 94); + cursor:default; +} \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js b/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js index ef97b14ff0..1edc44bbf8 100644 --- a/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js @@ -34,6 +34,16 @@ angular.module('mrlapp.service.AudioFileGui', []).controller('AudioFileGuiCtrl', } } + $scope.stopPlaylist = function() { + if ($scope.selectedPlaylist) { + msg.send('stopPlaylist', $scope.selectedPlaylist[0]) + msg.send('stop') + } else { + msg.send('stopPlaylist') + msg.send('stop') + } + } + // GOOD TEMPLATE TO FOLLOW this.updateState = function(service) { $scope.service = service diff --git a/src/main/resources/resource/WebGui/app/service/js/BoofCVGui.js b/src/main/resources/resource/WebGui/app/service/js/BoofCVGui.js new file mode 100644 index 0000000000..9746f5d564 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/BoofCVGui.js @@ -0,0 +1,243 @@ +angular.module('mrlapp.service.BoofCVGui', []).controller('BoofCVGuiCtrl', ['$scope', 'mrl', '$uibModal', function($scope, mrl, $uibModal) { + console.info('BoofCVGuiCtrl') + // grab a reference + var _self = this + // grab the message + var msg = this.msg + + var addFilterDialog = null + + $scope.fps = 0 + + $scope.myFile = null + + $scope.lastFrameTs = null + + $scope.stats = { + latency: 0, + fps: 0 + } + + $scope.samplePoint = { + x: 0, + y: 0 + } + + var avgSampleCnt = 30 + + var latencyDeltaAccumulator = 0 + + var lastFrameIndex = 0 + + var lastFrameTs = 0 + + /** + * Filter "Types" - this is the meta data type information necessary to build + * filter dialogs which get and set the filter attributes. + * in BoofCV.html - you can find were these are used when getFilterType() is called + */ + $scope.filterMetaData = { + 'AdaptiveThreshold': { + algorithm: 'mean' + }, + 'Affine': { + }, + + 'Canny': { + }, + 'LKOpticalTrack': { + }// LKOpticalTrack + + } + + $scope.diplayImage = null + + // first in list + $scope.selectedFilterType = 'AdaptiveThreshold' + // $scope.displayFilter = null + + // local scope variables + // necessary because service.cameraIndex is an int but ng-option only handles strings + // $scope.cameraIndex = "0" + $scope.camera = { + index: "0" + } + + $scope.possibleFilters = null + + // initial state of service. + + if ($scope.service.capturing) { + $scope.startCaptureLabel = "Stop Capture" + // $sce.trustAsResourceUrl ? + $scope.imgSource = "http://localhost:9090/input" + } else { + $scope.startCaptureLabel = "Start Capture" + $scope.imgSource = "service/img/BoofCV.png" + } + + // Handle an update state call from BoofCV service. + this.updateState = function(service) { + $scope.service = service + console.info("Open CV State had been updated") + console.info(service) + // int to string conversion + $scope.camera.index = service.cameraIndex.toString() + if ($scope.service.capturing) { + console.info("Started capturing") + $scope.startCaptureLabel = "Stop Capture" + $scope.imgSource = "http://localhost:9090/input" + } else { + console.info("Stopped capturing.") + $scope.startCaptureLabel = "Start Capture" + $scope.imgSource = "service/img/BoofCV.png" + } + + } + + $scope.addFilter = function(size) { + + addFilterDialog = $uibModal.open({ + templateUrl: "addFilterDialog.html", + scope: $scope, + controller: function($scope) { + $scope.cancel = function() { + addFilterDialog.dismiss() + } + } + }) + } + + $scope.addNamedFilter = function(name) { + console.info('addNamedFilter', name, $scope.selectedFilterType) + msg.send('addFilter', name, $scope.selectedFilterType) + if (addFilterDialog) { + addFilterDialog.dismiss() + } + } + + $scope.setDisplayFilter = function(name) { + console.info('setDisplayFilter', name) + msg.send('setDisplayFilter', name) + } + + this.onMsg = function(inMsg) { + let data = inMsg.data[0] + switch (inMsg.method) { + case 'onState': + _self.updateState(data) + $scope.$apply() + break + case 'onPossibleFilters': + $scope.possibleFilters = data + $scope.$apply() + break + case 'onWebDisplay': + // $scope.diplayImage = 'data:image/jpeg;base64,' + data + $scope.diplayImage = data.data + if (data.frameIndex % avgSampleCnt == 0) { + $scope.stats.latency = Math.round(latencyDeltaAccumulator / avgSampleCnt) + latencyDeltaAccumulator = 0 + $scope.stats.fps = Math.round((data.frameIndex - lastFrameIndex) * 1000 / (data.ts - lastFrameTs)) + lastFrameIndex = data.frameIndex + lastFrameTs = data.ts + } + + latencyDeltaAccumulator += new Date().getTime() - data.ts + + $scope.$apply() + break + default: + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } + } + + // FIXME - rename isFitlerType('Canny') + $scope.isFilterType = function(type) { + if ($scope.service.filters[$scope.service.displayFilter]) { + return $scope.service.filters[$scope.service.displayFilter].type == type + } + return null + } + + // get currently selected filter + $scope.getFilter = function() { + if ($scope.service.filters[$scope.service.displayFilter]) { + return $scope.service.filters[$scope.service.displayFilter] + } + return null + } + + $scope.getDisplayImage = function() { + return $scope.diplayImage + } + + $scope.setFilterState = function() { + let filter = $scope.service.filters[$scope.service.displayFilter] + // let meta = $scope.getFilterType().apertureSize.options + // let x = $scope.service.filters[$scope.service.displayFilter].apertureSize + msg.send('setFilterState', filter.name, JSON.stringify(filter)) + console.info(filter) + } + + $scope.getFilterType = function(typeName) { + if (!typeName) { + typeName = $scope.service.displayFilter + } + if ($scope.service.filters[typeName]) { + return $scope.filterMetaData[$scope.service.filters[$scope.service.displayFilter].type] + } + return null + } + + $scope.meta = function() { + let type = $scope.isFilterType() + } + + $scope.onSamplePoint = function($event) { + console.info('samplePoint ' + $event) + $scope.samplePoint.x = $event.offsetX + $scope.samplePoint.y = $event.offsetY + msg.send('samplePoint', $scope.samplePoint.x, $scope.samplePoint.y) + } + + $scope.uploadFile = function() { + + var f = $scope.myFile; + var r = new FileReader(); + + r.onloadend = function(e) { + var data = e.target.result; + console.info('onloadend') + msg.send('saveFile', f.name, btoa(data)) + $scope.loadFile = false + // close dialog + } + + r.readAsBinaryString(f); + console.info('readAsBinaryString') + } + + msg.subscribe('getPossibleFilters') + msg.subscribe('publishWebDisplay') + msg.subscribe('publishState') + msg.send('getPossibleFilters') + msg.subscribe(this) + +}]).directive('fileModel', ['$parse', function($parse) { + return { + restrict: 'A', + link: function(scope, element, attrs) { + var model = $parse(attrs.fileModel); + var modelSetter = model.assign; + + element.bind('change', function() { + scope.$apply(function() { + modelSetter(scope, element[0].files[0]); + }); + }); + } + }; +} +]); diff --git a/src/main/resources/resource/WebGui/app/service/js/ClockGui.js b/src/main/resources/resource/WebGui/app/service/js/ClockGui.js index b5f4202fe6..46eaf8f62b 100644 --- a/src/main/resources/resource/WebGui/app/service/js/ClockGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/ClockGui.js @@ -20,7 +20,8 @@ angular.module('mrlapp.service.ClockGui', []).controller('ClockGuiCtrl', ['$scop $scope.$apply() break case 'onTime': - $scope.onTime = data + const date = new Date(data); + $scope.onTime = date.toLocaleString(); $scope.$apply() break case 'onEpoch': diff --git a/src/main/resources/resource/WebGui/app/service/js/CronGui.js b/src/main/resources/resource/WebGui/app/service/js/CronGui.js new file mode 100644 index 0000000000..7fcf13e0ff --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/CronGui.js @@ -0,0 +1,57 @@ +angular.module('mrlapp.service.CronGui', []).controller('CronGuiCtrl', ['$scope', 'mrl', function($scope, mrl) { + console.info('CronGuiCtrl') + var _self = this + var msg = this.msg + + // str verson of parameters from the input + // text form field + $scope.parameters = null + + $scope.newTask = { + id: null, + cronPattern: null, + name: null, + method: null, + data: null + } + + // GOOD TEMPLATE TO FOLLOW + this.updateState = function(service) { + $scope.service = service + } + + this.onMsg = function(inMsg) { + let data = inMsg.data[0] + switch (inMsg.method) { + case 'onState': + _self.updateState(data) + $scope.$apply() + break + default: + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } + } + + $scope.addNamedTask = function() { + if ($scope.parameters && $scope.parameters.length > 0) { + $scope.newTask.data = JSON.parse($scope.parameters) + } else { + $scope.newTask.data = null + } + + msg.send('addTask', $scope.newTask) + } + + $scope.removeTask = function(id) { + msg.send('removeTask', id) + } + + msg.subscribe(this) +} +]).filter('epochToLocalDate', function() { + return function(epochTime) { + return new Date(epochTime).toLocaleString(); + } + ; +}); diff --git a/src/main/resources/resource/WebGui/app/service/js/DocumentPipelineGui.js b/src/main/resources/resource/WebGui/app/service/js/DocumentPipelineGui.js new file mode 100755 index 0000000000..889f743a67 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/DocumentPipelineGui.js @@ -0,0 +1,40 @@ +angular.module('mrlapp.service.DocumentPipelineGui', []).controller('DocumentPipelineGuiCtrl', ['$scope', 'mrl', function($scope, mrl) { + console.info('FileConnectorGuiCtrl'); + var _self = this + var msg = this.msg + + $scope.document = ''; + + // GOOD TEMPLATE TO FOLLOW + this.updateState = function(service) { + $scope.service = service + } + + this.onMsg = function(inMsg) { + let data = inMsg.data[0] + switch (inMsg.method) { + case 'onDocument': + console.info("On Document!"); + $scope.document = data; + $scope.$apply(); + break + case 'onState': + _self.updateState(data) + $scope.$apply() + break + case 'onStatus': + $scope.status = data; + $scope.$apply() + break + default: + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } + + }; + + // This could result in a lot of data getting returned to the webgui.. we'll see. + // msg.subscribe('publishDocument'); + msg.subscribe(this); + + }]); \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/js/FileConnectorGui.js b/src/main/resources/resource/WebGui/app/service/js/FileConnectorGui.js new file mode 100755 index 0000000000..e67cce7115 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/FileConnectorGui.js @@ -0,0 +1,53 @@ +angular.module('mrlapp.service.FileConnectorGui', []).controller('FileConnectorGuiCtrl', ['$scope', 'mrl', function($scope, mrl) { + console.info('FileConnectorGuiCtrl'); + var _self = this + var msg = this.msg + + $scope.filepath = ''; + $scope.document = ''; + + // GOOD TEMPLATE TO FOLLOW + this.updateState = function(service) { + $scope.service = service + } + + this.onMsg = function(inMsg) { + let data = inMsg.data[0] + switch (inMsg.method) { + case 'onDocument': + console.info("On Document!"); + $scope.document = data; + $scope.$apply(); + break + case 'onState': + _self.updateState(data) + $scope.$apply() + break + case 'onStatus': + $scope.status = data; + $scope.$apply() + break + default: + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } + + }; + + $scope.startCrawling = function() { + console.info("Start crawling>" + $scope.filepath + "<") + mrl.sendTo($scope.service.name, "setDirectory", $scope.filepath) + mrl.sendTo($scope.service.name, "startCrawling"); + } + + $scope.stopCrawling = function() { + // TODO: this doesn't seem to work. + console.info("Stop crawling") + mrl.sendTo($scope.service.name, "stopCrawling"); + } + + // This could result in a lot of data getting returned to the webgui.. we'll see. + // msg.subscribe('publishDocument'); + msg.subscribe(this); + + }]); \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/js/IntroGui.js b/src/main/resources/resource/WebGui/app/service/js/IntroGui.js index 963741a2b0..ae72006958 100644 --- a/src/main/resources/resource/WebGui/app/service/js/IntroGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/IntroGui.js @@ -1,5 +1,5 @@ -angular.module('mrlapp.service.IntroGui', []).controller('IntroGuiCtrl', ['$scope', '$log', 'mrl', '$timeout', function($scope, $log, mrl, $timeout) { - $log.info('IntroGuiCtrl') +angular.module('mrlapp.service.IntroGui', []).controller('IntroGuiCtrl', ['$scope', 'mrl', '$timeout', function($scope, mrl, $timeout) { + console.info('IntroGuiCtrl') var _self = this var msg = this.msg $scope.mrl = mrl @@ -64,6 +64,11 @@ angular.module('mrlapp.service.IntroGui', []).controller('IntroGuiCtrl', ['$scop return ret } + $scope.start = function(name, type) { + msg.sendTo('runtime', 'start', name, type) + } + + // this method initializes subPanels when a new service becomes available this.onRegistered = function(panel) { if (panelNames.has(panel.displayName)) { diff --git a/src/main/resources/resource/WebGui/app/service/js/JMonkeyEngineGui.js b/src/main/resources/resource/WebGui/app/service/js/JMonkeyEngineGui.js index 5ad4d15528..2b3f084c1a 100644 --- a/src/main/resources/resource/WebGui/app/service/js/JMonkeyEngineGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/JMonkeyEngineGui.js @@ -1,5 +1,5 @@ -angular.module('mrlapp.service.JMonkeyEngineGui', []).controller('JMonkeyEngineGuiCtrl', ['$scope', '$log', 'mrl', '$timeout', function($scope, $log, mrl, $timeout) { - $log.info('JMonkeyEngineGuiCtrl') +angular.module('mrlapp.service.JMonkeyEngineGui', []).controller('JMonkeyEngineGuiCtrl', ['$scope', 'mrl', function($scope, mrl, ) { + console.info('JMonkeyEngineGuiCtrl') var _self = this var msg = this.msg @@ -9,24 +9,29 @@ angular.module('mrlapp.service.JMonkeyEngineGui', []).controller('JMonkeyEngineG } this.onMsg = function(inMsg) { + data = inMsg.data[0] switch (inMsg.method) { case 'onState': - $timeout(function() { - _self.updateState(inMsg.data[0]) - }) + _self.updateState(data) break - case 'onPulse': - $timeout(function() { - $scope.pulseData = inMsg.data[0] - }) + case 'onStatus': + console.info() + break + case 'onNodes': + $scope.pulseData = data + break + case 'onSelectedPath': + $scope.selectedPath = data + $scope.$apply() break default: - $log.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) break } } - // msg.subscribe('pulse') + msg.subscribe('getSelectedPath') + msg.subscribe('publishSelected') msg.subscribe(this) } diff --git a/src/main/resources/resource/WebGui/app/service/js/OakDGui.js b/src/main/resources/resource/WebGui/app/service/js/OakDGui.js new file mode 100644 index 0000000000..ce58bc2f80 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/OakDGui.js @@ -0,0 +1,14 @@ +angular.module('mrlapp.service.OakDGui', []).controller('OakDGuiCtrl', ['$scope', 'mrl', function($scope, mrl) { + console.info('OakDGuiCtrl') + var _self = this + var msg = this.msg + + // GOOD TEMPLATE TO FOLLOW + this.updateState = function(service) { + $scope.service = service + } + msg.subscribe('publishTime') + msg.subscribe('publishEpoch') + msg.subscribe(this) +} +]) \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/js/PirGui.js b/src/main/resources/resource/WebGui/app/service/js/PirGui.js index cef3c00463..a19bb57b78 100644 --- a/src/main/resources/resource/WebGui/app/service/js/PirGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/PirGui.js @@ -12,8 +12,14 @@ angular.module('mrlapp.service.PirGui', []).controller('PirGuiCtrl', ['$scope', // GOOD TEMPLATE TO FOLLOW this.updateState = function(service) { $scope.service = service - $scope.options.attachName = service.config.controller + $scope.options.attachName = service.config.controller $scope.options.isAttached = service.attached + $scope.options.interface = 'PinArrayControl' + + // since attach broadcasts we'll get the pin list here + if ($scope?.service?.config?.controller) { + msg.send('getPinList', $scope.service.config.controller) + } } // init scope variables @@ -27,6 +33,13 @@ angular.module('mrlapp.service.PirGui', []).controller('PirGuiCtrl', ['$scope', _self.updateState(data) $scope.$apply() break + case 'onPinList': + $scope.pinList = [] + for (var pinDef of data) { + $scope.pinList.push(pinDef.pin) + } + $scope.$apply() + break case 'onSense': console.info('onSense', data) $scope.service.active = data @@ -41,6 +54,9 @@ angular.module('mrlapp.service.PirGui', []).controller('PirGuiCtrl', ['$scope', _self.selectController = function(controller) { //$scope.service.controllerName = controller $scope.service.config.controller = controller + // get the pin list of the selected controller + msg.send('setPinArrayControl', controller) + msg.send('getPinList', controller) } $scope.options = { @@ -53,6 +69,7 @@ angular.module('mrlapp.service.PirGui', []).controller('PirGuiCtrl', ['$scope', $scope.attach = function() { msg.send('setPin', $scope.service.config.pin) msg.send('attach', $scope.service.config.controller) + msg.send('enable') } $scope.detach = function() { @@ -67,8 +84,10 @@ angular.module('mrlapp.service.PirGui', []).controller('PirGuiCtrl', ['$scope', } $scope.setPin = function() { - msg.send('setPin', $scope.service.config.pin) - msg.send('broadcastState') + if ($scope.service.config.pin) { + msg.send('setPin', $scope.service.config.pin) + msg.send('broadcastState') + } } $scope.disable = function() { @@ -76,7 +95,24 @@ angular.module('mrlapp.service.PirGui', []).controller('PirGuiCtrl', ['$scope', msg.send('broadcastState') } + $scope.getActiveImage = function() { + if ($scope.service.active) { + return '../../green.png' + } else if ($scope.service.active === false) { + return '../../red.png' + } else { + // undefined / unknown + return '../../grey.png' + } + } + msg.subscribe('publishSense') + msg.subscribe('getPinList') + + if ($scope?.service?.config?.controller) { + msg.send('getPinList', $scope.service.config.controller) + } + msg.subscribe(this) } ]) diff --git a/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js b/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js index 4e3b0b3d3e..38211728ff 100644 --- a/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js @@ -41,9 +41,6 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', properties: false } - // grab defaults. - $scope.newUserName = $scope.service.currentUserName - $scope.chatLog = [] // start info status @@ -54,7 +51,7 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', // use another scope var to transfer/merge selection // from user - service.currentSession is always read-only // all service data should never be written to, only read from - $scope.currentUserName = service.currentUserName + $scope.currentUserName = service.config.currentUserName $scope.service = service $scope.currentSessionKey = $scope.getCurrentSessionKey() @@ -124,7 +121,7 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', var textData = data $scope.chatLog.unshift({ type: 'Bot', - name: $scope.service.currentBotName, + name: $scope.service.config.currentBotName, text: $sce.trustAsHtml(data.msg) }) $scope.lastResponse = textData @@ -169,22 +166,22 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', $scope.getAimlFile = function(filename) { $scope.aimlFile = filename console.log('getting aiml file ' + filename) - msg.send('getAimlFile', $scope.service.currentBotName, filename) + msg.send('getAimlFile', $scope.service.config.currentBotName, filename) $scope.tabs.selected = 2 } $scope.saveAimlFile = function() { - msg.send("saveAimlFile", $scope.service.currentBotName, $scope.aimlFile, $scope.aimlFileData.data) + msg.send("saveAimlFile", $scope.service.config.currentBotName, $scope.aimlFile, $scope.aimlFileData.data) } $scope.setSessionKey = function() { - msg.send("setCurrentUserName", $scope.service.currentUserName) - msg.send("setCurrentBotName", $scope.service.currentBotName) + msg.send("setCurrentUserName", $scope.service.config.currentUserName) + msg.send("setCurrentBotName", $scope.service.config.currentBotName) } $scope.getBotInfo = function() { if ($scope.service && $scope.service.bots){ - return $scope.service.bots[$scope.service.currentBotName] + return $scope.service.bots[$scope.service.config.currentBotName] } return null } @@ -200,7 +197,7 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', } $scope.getCurrentSessionKey = function() { - return $scope.service.currentUserName + ' <-> ' + $scope.service.currentBotName + return $scope.service.config.currentUserName + ' <-> ' + $scope.service.config.currentBotName } $scope.test = function(session, utterance) { @@ -208,8 +205,8 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', } $scope.getSessionResponse = function(utterance) { - console.info("SESSION GET RESPONSE (" + $scope.currentUserName + " " + $scope.service.currentBotName + ")") - $scope.getResponse($scope.currentUserName, $scope.service.currentBotName, utterance) + console.info("SESSION GET RESPONSE (" + $scope.currentUserName + " " + $scope.service.config.currentBotName + ")") + $scope.getResponse($scope.currentUserName, $scope.service.config.currentBotName, utterance) } $scope.getResponse = function(username, botname, utterance) { @@ -254,7 +251,9 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', $scope.getProperty = function(propName) { try { - return $scope.getBotInfo()['properties'][propName] + if ($scope.getBotInfo() && $scope.getBotInfo()['properties']){ + return $scope.getBotInfo()['properties'][propName] + } } catch (error){ console.warn('getProperty(' + propName + ') not found') return null @@ -277,6 +276,14 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', console.log('aceChanged') } + $scope.getBotPath = function(e) { + if ($scope.service?.bots && $scope.service?.bots[$scope.service?.config?.currentBotName]?.path){ + return $scope.service?.bots[$scope.service?.config.currentBotName].path + } + return null + } + + $scope.getStatusLabel = function(level) { if (level == 'error') { return 'row label col-md-12 label-danger' diff --git a/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js b/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js index 809feca717..fe29a9edb7 100644 --- a/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js @@ -2,37 +2,21 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope' console.info('Py4jGuiCtrl') var _self = this var msg = this.msg - var name = $scope.name - // init scope values - // $scope.service = mrl.getService(name) - $scope.output = '' - $scope.activeTabIndex = 0 - $scope.scriptCount = 0 - $scope.activeScript = null - $scope.scripts = {} - $scope.test = null - $scope.openingScript = true - $scope.dropdownIsOpen = true - $scope.lastStatus = null + + // list of client keys + // cant come from service.clients + // because its non serializable + var clients = [] + + // filesystem list of scripts + $scope.scriptList = [] $scope.log = '' - // 2 dialogs - $scope.loadFile = false - $scope.newFile = false + // this UI's currently active script + $scope.activeKey = null _self.updateState = function(service) { $scope.service = service - $scope.scriptCount = 0 - - angular.forEach(service.openedScripts, function(value, key) { - if (!angular.isDefined($scope.scripts[key])) { - $scope.scripts[key] = value - } - $scope.scriptCount++ - }) - // this doesn't work - its the ace-ui callback that - // changes the activeTabIndex - $scope.activeTabIndex = $scope.scriptCount } this.onMsg = function(msg) { @@ -55,11 +39,18 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope' case 'onAppend': $scope.log = data + $scope.log $scope.$apply() - break + break + case 'onScriptList': + $scope.scriptList = data + $scope.$apply() + break + case 'onClients': + $scope.clients = data + $scope.$apply() + break case 'onStatus': - $scope.lastStatus = data - if (data.level == 'error'){ - $scope.log = data.detail + '\n' + $scope.log + if (data.level == 'error') { + $scope.log = data.detail + '\n' + $scope.log } console.info("onStatus ", data) $scope.$apply() @@ -70,98 +61,34 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope' } } - $scope.newScript = function(filename, script) { - if (!script) { - script = '# new awesome robot script\n' - } - msg.send('openScript', filename, script) - $scope.newName = '' - // clear input text - $scope.newFile = false - // close dialog - } - - // utility methods // - // gets script name from full path name - $scope.getName = function(path) { - if (path.indexOf("/") >= 0) { - return (path.split("/").pop()) - } - if (path.indexOf("\\") >= 0) { - return (path.split("\\").pop()) - } - return path - } - //----- ace editors related callbacks begin -----// $scope.aceLoaded = function(e) { console.info("ace loaded") - $scope.activeTabIndex = $scope.scriptCount } $scope.aceChanged = function(e) { console.info("ace changed") - } - //----- ace editors related callbacks end -----// - $scope.addScript = function() { - let scriptName = 'Untitled-' + $scope.scriptCount + 1 - var newScript = { - name: scriptName, - code: '' - } - $scope.scripts[scriptName] = newScript - console.log($scope.activeTabIndex) + activeScript = $scope.service.openedScripts[$scope.activeKey] + msg.send('updateScript', activeScript.file, activeScript.code) } $scope.closeScript = function(scriptName) { // FIXME - save first ? msg.send('closeScript', scriptName) - $scope.scriptCount-- - delete $scope.scripts[scriptName] - console.log("removed " + scriptName) } $scope.exec = function() { - // non-blocking exec - msg.send('exec', $scope.activeScript.code) + activeScript = $scope.service.openedScripts[$scope.activeKey] + msg.send('exec', activeScript.code) } $scope.tabSelected = function(script) { - console.info('here') - $scope.activeScript = script - // need to get a handle on hte tab's ui / text - // $scope.editors.setValue(script.code) - } - - $scope.getTabHeader = function(key) { - return $scope.getName(key) - //return key.substr(key.lastIndexOf('/') + 1) + console.info('tabSelected') + $scope.activeKey = script.file } $scope.saveScript = function() { - msg.send('saveScript', $scope.activeScript.file, $scope.activeScript.code) - } - - $scope.downloadScript = function() { - var textFileAsBlob = new Blob([$scope.activeScript.code],{ - type: 'text/plain' - }) - var downloadLink = document.createElement("a") - downloadLink.download = $scope.getName($scope.activeScript.file) - downloadLink.innerHTML = "Download File" - if (window.webkitURL != null) { - // Chrome allows the link to be clicked - // without actually adding it to the DOM. - downloadLink.href = window.webkitURL.createObjectURL(textFileAsBlob) - } else { - // Firefox requires the link to be added to the DOM - // before it can be clicked. - downloadLink.href = window.URL.createObjectURL(textFileAsBlob) - downloadLink.onclick = destroyClickedElement - downloadLink.style.display = "none" - document.body.appendChild(downloadLink) - } - - downloadLink.click() + activeScript = $scope.service.openedScripts[$scope.activeKey] + msg.send('saveScript', activeScript.file, activeScript.code) } $scope.getPossibleServices = function(item) { @@ -169,43 +96,124 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope' return ret } + $scope.addScript = function() { + var modalInstance = $uibModal.open({ + templateUrl: 'addScript.html', + controller: function($scope, $uibModalInstance) { + $scope.ok = function() { + if (!$scope.filename){ + console.error('filename cannot be null') + return + } + + msg.send('addScript', $scope.filename, '# new awesome robot script\n') + $uibModalInstance.close($scope.filename) + } + + $scope.cancel = function() { + $uibModalInstance.dismiss('cancel') + } + + $scope.checkEnterKey = function(event) { + if (event.keyCode === 13) { + $scope.ok() + } + } + + }, + size: 'sm' + }) - $scope.uploadFile = function() { - - var f = $scope.myFile; - var r = new FileReader(); + modalInstance.result.then(function(filename) { + // Do something with the filename + console.log("Filename: ", filename) + }, function() { + // Modal dismissed + console.log("Modal dismissed") + }) + } - r.onloadend = function(e) { - var data = e.target.result; - console.info('onloadend') - $scope.newScript(f.name, data) - $scope.loadFile = false - // close dialog + $scope.installPackage = function() { + var modalInstance = $uibModal.open({ + templateUrl: 'installPackage.html', + controller: function($scope, $uibModalInstance) { + $scope.ok = function() { + if (!$scope.packageName){ + console.error('filename cannot be null') + return + } + + msg.send('installPipPackages', [$scope.packageName]) + $uibModalInstance.close($scope.packageName) + } + + $scope.cancel = function() { + $uibModalInstance.dismiss('cancel') + } + + $scope.checkEnterKey = function(event) { + if (event.keyCode === 13) { + $scope.ok() + } + } + + }, + size: 'sm' + }) + + modalInstance.result.then(function(filename) { + // Do something with the filename + console.log("Filename: ", filename) + }, function() { + // Modal dismissed + console.log("Modal dismissed") + }) } - r.readAsBinaryString(f); - console.info('readAsBinaryString') + + $scope.openScript = function() { + + msg.send('getScriptList') + + var modalInstance = $uibModal.open({ + templateUrl: 'openScript.html', + scope: $scope, + controller: function($scope, $uibModalInstance) { + $scope.ok = function(file) { + msg.send('openScript', file) + $uibModalInstance.close() + } + + $scope.cancel = function() { + $uibModalInstance.dismiss('cancel') + } + + $scope.checkEnterKey = function(event) { + if (event.keyCode === 13) { + $scope.ok() + } + } + + }, + size: 'sm' + }) + + modalInstance.result.then(function(filename) { + // Do something with the filename + console.log("Filename: ", filename) + }, function() { + // Modal dismissed + console.log("Modal dismissed") + }) } + - // $scope.possibleServices = Object.values(mrl.getPossibleServices()) msg.subscribe('publishStdOut') msg.subscribe('publishAppend') + msg.subscribe('getClients') + msg.subscribe('getScriptList') + msg.send('getClients') + msg.send('getScriptList') msg.subscribe(this) - msg.send('newScript') -} -]).directive('fileModel', ['$parse', function($parse) { - return { - restrict: 'A', - link: function(scope, element, attrs) { - var model = $parse(attrs.fileModel); - var modelSetter = model.assign; - - element.bind('change', function() { - scope.$apply(function() { - modelSetter(scope, element[0].files[0]); - }); - }); - } - }; } -]); +]) diff --git a/src/main/resources/resource/WebGui/app/service/js/PythonGui.js b/src/main/resources/resource/WebGui/app/service/js/PythonGui.js index b527d0a2f0..0d53aee882 100644 --- a/src/main/resources/resource/WebGui/app/service/js/PythonGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/PythonGui.js @@ -1,41 +1,22 @@ -angular.module('mrlapp.service.PythonGui', []).controller('PythonGuiCtrl', ['$scope', 'mrl', '$uibModal', '$timeout', function($scope, mrl, $uibModal, $timeout) { +angular.module('mrlapp.service.PythonGui', []).controller('PythonGuiCtrl', ['$scope', 'mrl', '$uibModal', function($scope, mrl, $uibModal) { console.info('PythonGuiCtrl') var _self = this var msg = this.msg - var name = $scope.name - // init scope values - // $scope.service = mrl.getService(name) - $scope.output = '' - $scope.activeTabIndex = 0 - $scope.scriptCount = 0 - $scope.activeScript = null - $scope.scripts = {} - $scope.test = null - $scope.openingScript = true - $scope.dropdownIsOpen = true - $scope.lastStatus = null + + // list of client keys + // cant come from service.clients + // because its non serializable + var clients = [] + + // filesystem list of scripts + $scope.scriptList = [] $scope.log = '' - // 2 dialogs - $scope.loadFile = false - $scope.newFile = false + // this UI's currently active script + $scope.activeKey = null _self.updateState = function(service) { $scope.service = service - $scope.scriptCount = 0 - - $scope.scripts = {} - $scope.scriptCount = 0 - - angular.forEach(service.openedScripts, function(value, key) { - if (!angular.isDefined($scope.scripts[key])) { - $scope.scripts[key] = value - } - $scope.scriptCount++ - }) - // this doesn't work - its the ace-ui callback that - // changes the activeTabIndex - $scope.activeTabIndex = $scope.scriptCount } this.onMsg = function(msg) { @@ -59,8 +40,11 @@ angular.module('mrlapp.service.PythonGui', []).controller('PythonGuiCtrl', ['$sc $scope.log = data + $scope.log $scope.$apply() break + case 'onScriptList': + $scope.scriptList = data + $scope.$apply() + break case 'onStatus': - $scope.lastStatus = data if (data.level == 'error'){ $scope.log = data.detail + '\n' + $scope.log } @@ -73,97 +57,34 @@ angular.module('mrlapp.service.PythonGui', []).controller('PythonGuiCtrl', ['$sc } } - $scope.newScript = function(filename, script) { - if (!script) { - script = '# new awesome robot script\n' - } - msg.send('openScript', filename, script) - $scope.newName = '' - // clear input text - $scope.newFile = false - // close dialog - } - - // utility methods // - // gets script name from full path name - $scope.getName = function(path) { - if (path.indexOf("/") >= 0) { - return (path.split("/").pop()) - } - if (path.indexOf("\\") >= 0) { - return (path.split("\\").pop()) - } - return path - } - //----- ace editors related callbacks begin -----// $scope.aceLoaded = function(e) { console.info("ace loaded") - $scope.activeTabIndex = $scope.scriptCount } $scope.aceChanged = function(e) { console.info("ace changed") - } - //----- ace editors related callbacks end -----// - $scope.addScript = function() { - let scriptName = 'Untitled-' + $scope.scriptCount + 1 - var newScript = { - name: scriptName, - code: '' - } - $scope.scripts[scriptName] = newScript - console.log($scope.activeTabIndex) + activeScript = $scope.service.openedScripts[$scope.activeKey] + msg.send('updateScript', activeScript.file, activeScript.code) } $scope.closeScript = function(scriptName) { // FIXME - save first ? msg.send('closeScript', scriptName) - msg.broadcastState() - // console.log("removed " + scriptName) } $scope.exec = function() { - // non-blocking exec - msg.send('exec', $scope.activeScript.code, false) + activeScript = $scope.service.openedScripts[$scope.activeKey] + msg.send('exec', activeScript.code) } $scope.tabSelected = function(script) { - console.info('here') - $scope.activeScript = script - // need to get a handle on hte tab's ui / text - // $scope.editors.setValue(script.code) - } - - $scope.getTabHeader = function(key) { - return $scope.getName(key) - //return key.substr(key.lastIndexOf('/') + 1) + console.info('tabSelected') + $scope.activeKey = script.file } $scope.saveScript = function() { - msg.send('saveScript', $scope.activeScript.file, $scope.activeScript.code) - } - - $scope.downloadScript = function() { - var textFileAsBlob = new Blob([$scope.activeScript.code],{ - type: 'text/plain' - }) - var downloadLink = document.createElement("a") - downloadLink.download = $scope.getName($scope.activeScript.file) - downloadLink.innerHTML = "Download File" - if (window.webkitURL != null) { - // Chrome allows the link to be clicked - // without actually adding it to the DOM. - downloadLink.href = window.webkitURL.createObjectURL(textFileAsBlob) - } else { - // Firefox requires the link to be added to the DOM - // before it can be clicked. - downloadLink.href = window.URL.createObjectURL(textFileAsBlob) - downloadLink.onclick = destroyClickedElement - downloadLink.style.display = "none" - document.body.appendChild(downloadLink) - } - - downloadLink.click() + activeScript = $scope.service.openedScripts[$scope.activeKey] + msg.send('saveScript', activeScript.file, activeScript.code) } $scope.getPossibleServices = function(item) { @@ -171,43 +92,86 @@ angular.module('mrlapp.service.PythonGui', []).controller('PythonGuiCtrl', ['$sc return ret } + $scope.addScript = function() { + var modalInstance = $uibModal.open({ + templateUrl: 'addPythonScript.html', + controller: function($scope, $uibModalInstance) { + $scope.ok = function() { + if (!$scope.filename){ + console.error('filename cannot be null') + return + } + + msg.send('addScript', $scope.filename, '# new awesome robot script\n') + $uibModalInstance.close($scope.filename) + } - $scope.uploadFile = function() { + $scope.cancel = function() { + $uibModalInstance.dismiss('cancel') + } - var f = $scope.myFile; - var r = new FileReader(); + $scope.checkEnterKey = function(event) { + if (event.keyCode === 13) { + $scope.ok() + } + } - r.onloadend = function(e) { - var data = e.target.result; - console.info('onloadend') - $scope.newScript(f.name, data) - $scope.loadFile = false - // close dialog - } + }, + size: 'sm' + }) - r.readAsBinaryString(f); - console.info('readAsBinaryString') + modalInstance.result.then(function(filename) { + // Do something with the filename + console.log("Filename: ", filename) + }, function() { + // Modal dismissed + console.log("Modal dismissed") + }) } - // $scope.possibleServices = Object.values(mrl.getPossibleServices()) + + $scope.openScript = function() { + + msg.send('getScriptList') + + var modalInstance = $uibModal.open({ + templateUrl: 'openPythonScript.html', + scope: $scope, + controller: function($scope, $uibModalInstance) { + $scope.ok = function(file) { + msg.send('openScript', file) + $uibModalInstance.close() + } + + $scope.cancel = function() { + $uibModalInstance.dismiss('cancel') + } + + $scope.checkEnterKey = function(event) { + if (event.keyCode === 13) { + $scope.ok() + } + } + + }, + size: 'sm' + }) + + modalInstance.result.then(function(filename) { + // Do something with the filename + console.log("Filename: ", filename) + }, function() { + // Modal dismissed + console.log("Modal dismissed") + }) +} + + msg.subscribe('publishStdOut') msg.subscribe('publishAppend') + msg.subscribe('getClients') + msg.subscribe('getScriptList') + msg.send('getScriptList') msg.subscribe(this) - msg.send('newScript') -} -]).directive('fileModel', ['$parse', function($parse) { - return { - restrict: 'A', - link: function(scope, element, attrs) { - var model = $parse(attrs.fileModel); - var modelSetter = model.assign; - - element.bind('change', function() { - scope.$apply(function() { - modelSetter(scope, element[0].files[0]); - }); - }); } - }; -} -]); +]) diff --git a/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js b/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js index ea81d00671..a25240dbae 100644 --- a/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js @@ -15,13 +15,18 @@ angular.module('mrlapp.service.RasPiGui', []).controller('RasPiGuiCtrl', ['$scop _self.updateState(data) $scope.$apply() break - case 'XXXonPinDefinition': + case 'onPinDefinition': $scope.service.pinIndex[data.pin] = data - $scope.service.addressIndex[data.address] = data + $scope.$apply() + break + case 'onPinArray': + for (const pd of data){ + $scope.service.pinIndex[pd.pin].value = pd.value + } $scope.$apply() break default: - $log.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) break } } @@ -31,16 +36,28 @@ angular.module('mrlapp.service.RasPiGui', []).controller('RasPiGuiCtrl', ['$scop } $scope.write = function(pinDef){ - msg.send('write', pinDef.address, pinDef.valueDisplay?1:0) + msg.send('write', pinDef.pin, pinDef.valueDisplay?1:0) } - $scope.readWrite = function(pinDef) { + $scope.pinMode = function(pinDef) { console.info(pinDef) // FIXME - standardize interface with Arduino :( - msg.send('pinMode', pinDef.pin, pinDef.readWrite?1:0) + msg.send('pinMode', pinDef.pin, pinDef.mode) } msg.subscribe('publishPinDefinition') + msg.subscribe('publishPinArray') msg.subscribe(this) } ]) +.filter('toArray', function() { + return function(obj) { + if (obj instanceof Object) { + return Object.keys(obj).map(function(key) { + return obj[key]; + }); + } else { + return obj; + } + }; + }); \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js index ee67cfcb63..09eab9b6f6 100644 --- a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js @@ -12,7 +12,6 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ } $scope.locales = {} - $scope.platform = $scope.service.platform $scope.status = "" $scope.cmd = "" @@ -21,7 +20,8 @@ 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': { @@ -46,8 +46,8 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ $scope.categoryServiceTypes = null - $scope.disabled = undefined; - $scope.person = {}; + $scope.disabled = undefined + $scope.person = {} var msgKeys = {} @@ -56,7 +56,7 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ // $scope.categoryServiceTypes = $scope.service.serviceData.categoryTypes[$scope.category.selected].serviceTypes $scope.filterServices = function() { - var result = {}; + var result = {} // console.debug('$scope.category.selected is ' + $scope.category.selected) const entries = Object.entries($scope.service.serviceData.serviceTypes) @@ -75,10 +75,10 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ if (/*metaData.simpleName.toLowerCase().includes($scope.newType) && */ categoryServiceTypes != null && categoryServiceTypes.includes(metaData.name)) { - result[fullTypeName] = metaData; + result[fullTypeName] = metaData } } - return result; + return result } // FIXME - maintain contextPath !!! @@ -117,8 +117,8 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ } msg.send('start', $scope.newName, $scope.newType) - $scope.newName = null; - $scope.newType = null; + $scope.newName = null + $scope.newType = null } this.onMsg = function(inMsg) { @@ -130,6 +130,7 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ switch (inMsg.method) { case 'onState': _self.updateState(data) + $scope.$apply() break case 'onLocalServices': $scope.registry = data @@ -232,7 +233,7 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ break case 'onHeartbeat': let heartbeat = data - let hb = heartbeat.name + '@' + heartbeat.id + ' sent onHeartbeat - '; + let hb = heartbeat.name + '@' + heartbeat.id + ' sent onHeartbeat - ' $scope.heartbeatTs = heartbeat.ts $scope.$apply() @@ -247,7 +248,7 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ // its 'own' sub-registry ??? if (!serviceName in mrl.getRegistry()) { // - console.warn(serviceName + ' not defined in registry - sending registration request'); + console.warn(serviceName + ' not defined in registry - sending registration request') } // else already registered } @@ -329,21 +330,6 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ } } - $scope.saveConfig = function() { - console.info('saveConfig') - - let onOK = function() { - msg.sendTo('runtime', 'saveConfig', $scope.service.configName) - } - - let onCancel = function() { - console.info('save config cancelled') - } - - let ret = modalService.openOkCancel('widget/modal-dialog.view.html', 'Save Configuration', 'Save your current configuration in a directory named', onOK, onCancel, $scope); - console.info('ret ' + ret); - } - $scope.savePlan = function() { console.info('saveConfig') @@ -356,8 +342,8 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ 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); + 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() { @@ -373,6 +359,40 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ console.info('setAutoStart') msg.send('setAutoStart', b) } + + $scope.saveConfig = function() { + $scope.service.selectedOption = 'current' + 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.service.configName) + console.log('Selected Option: ' + $scope.service.selectedOption) + console.log('includePeers Option: ' + $scope.service.includePeers) + console.log('configType Option: ' + $scope.service.configType) + if ($scope.service.selectedOption == 'default'){ + msg.send('saveDefault', $scope.service.configName, $scope.service.defaultServiceName, $scope.service.configType, $scope.service.includePeers) + } else { + msg.sendTo('runtime', 'saveConfig', $scope.service.configName) + } + }, function() { + // Handle 'Cancel' button click or modal dismissal + console.log('Modal dismissed') + }) + } // $scope.serviceTypes = Object.values(mrl.getPossibleServices()) msg.subscribe("getStartYml") diff --git a/src/main/resources/resource/WebGui/app/service/js/ServoGui.js b/src/main/resources/resource/WebGui/app/service/js/ServoGui.js index b86ba30d87..2175f81481 100644 --- a/src/main/resources/resource/WebGui/app/service/js/ServoGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/ServoGui.js @@ -16,13 +16,37 @@ angular.module('mrlapp.service.ServoGui', []).controller('ServoGuiCtrl', ['$scop var firstTime = true $scope.state = { - controller:null, useEncoderData: false, - attached: false, showLimits: false, rest: 90 } + $scope.attachController = function(serviceName) { + console.info("attachController") + if ($scope.service.pin){ + msg.send("setPin", $scope.service.pin) + } + if ($scope.service.config.controller){ + msg.send("attach", $scope.service.config.controller) + } + msg.send("broadcastState") + } + + $scope.setController = function(serviceName) { + console.info("setController") + msg.send("setController", serviceName) + } + + $scope.servoOptions = { + interface: 'ServoController', + isAttached: $scope.service.isAttached, + // callback: function... + attach: $scope.setController, + attachName: $scope.service?.config?.controller, + controllerTitle: 'controller' + } + + // init $scope.min = 0 $scope.max = 180 @@ -86,23 +110,8 @@ angular.module('mrlapp.service.ServoGui', []).controller('ServoGuiCtrl', ['$scop // GOOD TEMPLATE TO FOLLOW this.updateState = function(service) { - if (service.controller){ - // set the ui state - if it has a value - $scope.state.controller = service.controller - } - $scope.service = service - // service.controller like many parts is overloaded status & control :( - // so the ui may change controller - but at that moment the service may not - // be attached - the "only" time its attached is when the service data comes - // directly from the service and service.controller != null - if (service.controller) { - $scope.state.attached = true - } else { - $scope.state.attached = false - } - // done correctly - speedDisplay is a 'status' display ! // its NOT used to set 'control' speed - control is sent // from the ui interface - but the ui component does not display what it sent @@ -115,6 +124,10 @@ angular.module('mrlapp.service.ServoGui', []).controller('ServoGuiCtrl', ['$scop // max range of slider bar } + if (service?.config?.controller){ + $scope.servoOptions.attachName = service?.config?.controller + } + $scope.idleSeconds = service.idleTimeout / 1000 // $scope.pos.options.minLimit = service.mapper.minX // $scope.pos.options.maxLimit = service.mapper.maxX @@ -131,7 +144,7 @@ angular.module('mrlapp.service.ServoGui', []).controller('ServoGuiCtrl', ['$scop // $scope.pos.value = service.currentInputPos $scope.sliderEnabled = true - // $scope.activeTabIndex = service.controller == null ? 0 : 1 + $scope.activeTabIndex = service.isAttached?1:0 $scope.state.inputMin = service.mapper.minX $scope.state.inputMax = service.mapper.maxX @@ -217,14 +230,9 @@ angular.module('mrlapp.service.ServoGui', []).controller('ServoGuiCtrl', ['$scop $scope.setAutoDisable = function() { msg.send("setIdleTimeout", $scope.service.idleTimeout) - msg.send("setAutoDisable", $scope.service.autoDisable) + msg.send("setAutoDisable", $scope.service.config.autoDisable) } - // regrettably the onMethodMap dynamic - // generation of methods failed on this overloaded - // sweep method - there are several overloads in the - // Java service - although msg.sweep() was tried for ng-click - // for some reason Js resolved msg.sweep(null, null, null, null) :P $scope.sweep = function() { msg.send('sweep') } @@ -237,13 +245,6 @@ angular.module('mrlapp.service.ServoGui', []).controller('ServoGuiCtrl', ['$scop msg.send('setIdleTimeout', idleTime * 1000) } - $scope.attachController = function() { - console.info("attachController") - msg.send("setPin", $scope.service.pin) - msg.send("attach", $scope.state.controller) - msg.send("broadcastState") - } - $scope.map = function() { if ($scope.lockInputOutput) { diff --git a/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js b/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js index 4c60b2cf1e..40d1a258de 100644 --- a/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js @@ -2,26 +2,44 @@ angular.module('mrlapp.service.ServoMixerGui', []).controller('ServoMixerGuiCtrl console.info('ServoMixerGuiCtrl') var _self = this var msg = this.msg + var globalPoseIndex = 0 $scope.minView = true + $scope.delay = 3 // initial + $scope.showGestureSave = false + + $scope.searchServo = { + displayName: null + } $scope.servos = [] $scope.sliders = [] // list of current pose files $scope.poseFiles = [] - $scope.sequenceFiles = [] + $scope.gestureFiles = [] $scope.state = { + // gestureIndex is a string representation from $index :( dumb + 'gestureIndex': "0", 'selectedPose': null, - 'selectedSequenceFile': null, - 'selectedSequence': null, - 'currentRunningPose': null, - 'currentSequence':{ - 'poses':[] + 'selectedGestureFile': null, + 'selectedGesture': null, + 'playingPose': null, + 'currentGesture':{ + 'parts':[] } } - // unique id for new poses added to sequence + $scope.options = []; + angular.forEach("a:alpha,b:beta,d:delta,g:gamma,e:eta,E:epsilon,o:omega,z:zeta".split(','), function(val) { + var parts = val.split(":"); + $scope.options.push({ + name: parts[0], + value: parts[1] + }); + }); + + // unique id for new poses added to gesture let id = 0 // FIXME - this should be done in a base class or in framework @@ -62,12 +80,30 @@ angular.module('mrlapp.service.ServoMixerGui', []).controller('ServoMixerGuiCtrl _self.updateState(data) $scope.$apply() break - case 'onPlayingPose': - $scope.state.currentRunningPose = data + case 'onSearch': + $scope.searchServo.displayName = data + $scope.searchServos(data) + // sets pose name from selected + $scope.state.selectedPose = data + "_" + globalPoseIndex++ + $scope.$apply() + break + case 'onPlayingGesturePart': + // FIXME rename + if (data.type != 'Delay'){ + $scope.state.playingPose = data + } else { + $scope.state.playingPose.value = data.value/1000 + } + $scope.$apply() + break + case 'onPlayingGesturePartIndex': + // FIXME rename + $scope.state.gestureIndex = data + "" + $scope.state.playingPoseIndex = data $scope.$apply() break case 'onStopPose': - $scope.state.currentRunningPose = ' ' + // $scope.state.playingPose = ' ' $scope.$apply() break case 'onServoEvent': @@ -80,14 +116,17 @@ angular.module('mrlapp.service.ServoMixerGui', []).controller('ServoMixerGuiCtrl if (data && data.length > 0) { $scope.state.selectedPose = data[data.length - 1] } - break - case 'onSequence': - $scope.state.currentSequence = data + case 'onGesture': + $scope.state.currentGesture = data $scope.$apply() break - case 'onSequenceFiles': - $scope.sequenceFiles = data + case 'onGestureFiles': + $scope.gestureFiles = data + if (!$scope.state.selectedGestureFile && $scope.gestureFiles && $scope.gestureFiles.length > 0){ + $scope.state.selectedGestureFile = $scope.gestureFiles[0] + msg.send('getGesture', $scope.state.selectedGestureFile) + } $scope.$apply() break case 'onListAllServos': @@ -136,60 +175,36 @@ angular.module('mrlapp.service.ServoMixerGui', []).controller('ServoMixerGuiCtrl } } - getIndexOfSelectedPoseInSequence = function() { - if (!$scope.state.selectedSequence) { - return 0 - } - - // change the selected sequence back into an object - let ssId = JSON.parse($scope.state.selectedSequence).id - let index = 0 - for (var p of $scope.state.currentSequence.poses) { - posId = $scope.state.currentSequence.poses[index].id - if (ssId == posId) { - return index - } - index++ - } - return index - } - - $scope.addPoseToSequence = function() { + $scope.addPoseToGesture = function() { // get pos entry let pose = { - 'id': id, 'name': $scope.state.selectedPose, - 'waitTimeMs': 3000 + 'type': 'Pose', + 'blocking': false } - // maintain unique id - ++id - - let currentIndex = getIndexOfSelectedPoseInSequence() - currentIndex++ - $scope.state.currentSequence.poses.splice(currentIndex, 0, pose) + $scope.state.currentGesture.parts.splice(parseInt($scope.state.gestureIndex) + 1, 0, pose) } - $scope.removePoseFromSequence = function() { - let currentIndex = getIndexOfSelectedPoseInSequence() - $scope.state.currentSequence.poses.splice(currentIndex, 1) + $scope.removePoseFromGesture = function() { + $scope.state.currentGesture.parts.splice($scope.state.gestureIndex, 1) } move = function(arr, fromIndex, toIndex) { var element = arr[fromIndex]; arr.splice(fromIndex, 1); arr.splice(toIndex, 0, element); + // stupid ass conversion back to string for list 'select' + $scope.state.gestureIndex = toIndex + '' } - $scope.moveUpPoseInSequence = function() { - let currentIndex = getIndexOfSelectedPoseInSequence() - move($scope.state.currentSequence.poses, currentIndex, currentIndex - 1) + $scope.moveUpPoseInGesture = function() { + move($scope.state.currentGesture.parts, $scope.state.gestureIndex, parseInt($scope.state.gestureIndex) - 1) } - $scope.moveDownPoseInSequence = function() { - let currentIndex = getIndexOfSelectedPoseInSequence() - move($scope.state.currentSequence.poses, currentIndex, currentIndex + 1) + $scope.moveDownPoseInGesture = function() { + move($scope.state.currentGesture.parts, $scope.state.gestureIndex, parseInt($scope.state.gestureIndex) + 1) } $scope.searchServos = function(searchText) { @@ -219,15 +234,15 @@ angular.module('mrlapp.service.ServoMixerGui', []).controller('ServoMixerGuiCtrl console.info('here') } - $scope.setSequence = function() { - let seq = JSON.parse($scope.state.selectedSequence) - $scope.delay = seq.waitTimeMs/1000 - console.info($scope.state.selectedSequence) - } - - $scope.moveSequenceContent = function(seqstr){ - let seq = JSON.parse(seqstr) - msg.send('moveToPose', seq.name) + $scope.step = function(){ + let index = parseInt($scope.state.gestureIndex) + let part = $scope.state.currentGesture.parts[index] + if (part.type === 'Pose'){ + msg.send('moveToPose', part.name) + } + index++ + $scope.state.gestureIndex = index + "" + } // initialize all services which have panel references in Intro @@ -236,41 +251,94 @@ angular.module('mrlapp.service.ServoMixerGui', []).controller('ServoMixerGuiCtrl this.onRegistered(servicePanelList[index]) } - $scope.saveSequence = function(name) { - $scope.state.currentSequence.name = name - /* - $scope.state.currentSequence.poses = [] - for (var p of $scope.state.currentSequence.poses) { - $scope.state.currentSequence.poses.push(p.name) - }*/ - - // because angular adds crap to identify select options :( - // let json = JSON.stringify($scope.state.currentSequence) - let json = angular.toJson($scope.state.currentSequence) - msg.send('saveSequence', name, json) + $scope.saveGesture = function(gestureName) { + // gestureName = $scope.state.selectedGestureFile + $scope.state.currentGesture.name = gestureName + if ($scope.gestureFiles.includes(gestureName)){ + // saving current file + msg.send('saveGesture', gestureName, $scope.state.currentGesture) + } else { + // saving new file + blankGesture = { + parts:[], + repeat: false + } + msg.send('saveGesture', gestureName, blankGesture) + } + + } + + $scope.addDelay = function(seconds){ + + let value = parseFloat(seconds) + + if (Number.isNaN(value)){ + console.error(seconds, "is not a valid number for delay") + return + } + + let delay = { + 'name': 'delay', + 'type': 'Delay', + 'value': value * 1000, + 'blocking': true + } + + $scope.state.currentGesture.parts.splice(parseInt($scope.state.gestureIndex) + 1, 0, delay) + } + + $scope.playGesture = function(gesture) { + if (gesture){ + msg.send('playGesture', gesture) + } else { + console.warn('gesture empty') + } } - $scope.addSequenceDelay = function(delay){ - let index = getIndexOfSelectedPoseInSequence() - if (delay == ""){ - $scope.state.currentSequence.poses[index].waitTimeMs = null + $scope.removeGesture = function(gesture) { + if (gesture){ + msg.send('removeGesture', gesture) } else { - $scope.state.currentSequence.poses[index].waitTimeMs = delay * 1000 + console.warn('removeGesture empty') } } + $scope.displayValue = function(pose) { + // !pose.value || Number.isNaN(pose.value)?'':pose.value/1000 + if (pose.type == 'Delay'){ + return pose.value/1000 + } else { + return pose.value + } + } + + $scope.speak = function() { + + let delay = { + 'name': 'speech', + 'type': 'Speech', + 'value': $scope.text, + 'blocking': true + } + + $scope.state.currentGesture.parts.splice(parseInt($scope.state.gestureIndex) + 1, 0, delay) + } + + msg.subscribe('getPoseFiles') - msg.subscribe('getSequence') - msg.subscribe('getSequenceFiles') + msg.subscribe('getGesture') + msg.subscribe('getGestureFiles') msg.subscribe('listAllServos') + msg.subscribe('search') + msg.send('listAllServos') msg.send('getPoseFiles'); - msg.send('getSequenceFiles'); + msg.send('getGestureFiles'); - msg.subscribe("publishPlayingPose") + msg.subscribe("publishPlayingGesturePart") + msg.subscribe("publishPlayingGesturePartIndex") msg.subscribe("publishStopPose") - mrl.subscribeToRegistered(this.onRegistered) mrl.subscribeToReleased(this.onReleased) diff --git a/src/main/resources/resource/WebGui/app/service/js/SolrGui.js b/src/main/resources/resource/WebGui/app/service/js/SolrGui.js index 5bcc5d64e9..1db0666642 100644 --- a/src/main/resources/resource/WebGui/app/service/js/SolrGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/SolrGui.js @@ -1,25 +1,113 @@ -angular.module('mrlapp.service.SolrGui', []) - .controller('SolrGuiCtrl', ['$scope', '$log', 'mrl', function ($scope, $log, mrl) { - $log.info('SolrGuiCtrl'); - // TODO: something useful?! - this.onMsg = function (msg) { - $log.info("Solr Msg ! - "); - $log.info(msg); - if (msg.method == "onResults") { - // Results! - var solrResults = msg.data[0]; - $scope.service.solrResults = solrResults; - $scope.$apply(); - } - }; - $scope.search = function(querystring) { - $log.info('SolrGuiCtrl - Search Clicked!' + querystring); - mrl.sendTo($scope.service.name, "search", querystring); - }; - +angular.module('mrlapp.service.SolrGui', []).controller('SolrGuiCtrl', ['$scope', 'mrl', function($scope, mrl) { + console.info('SolrGuiCtrl'); + var _self = this + var msg = this.msg + // TODO: something useful?! + $scope.solrResults = ''; + $scope.queryString = '*:*'; + $scope.startOffset = 0; + $scope.endOffset = 0; + $scope.numFound = 0; + $scope.pageSize = 50; + $scope.filters = []; + // TODO: maybe some other fields.. + // TODO: support range facets + $scope.facetFields = ['type', 'artist_facet', 'album_facet', 'genre_facet', 'year_facet', 'sender_type', 'sender','method']; + $scope.facetFields = ['type', 'artist_facet', 'album_facet', 'genre_facet', 'year_facet', 'sender_type', 'sender','method', 'content_type_facet']; + // GOOD TEMPLATE TO FOLLOW + this.updateState = function(service) { + $scope.service = service + } - - mrl.subscribe($scope.service.name, 'publishResults', $scope.service.results); -// $scope.panel.initDone(); + this.onMsg = function(inMsg) { + let data = inMsg.data[0] + switch (inMsg.method) { + case 'onResults': + var solrResults = JSON.parse(data); + console.info("On Results!"); + console.info(solrResults); + $scope.solrResults = solrResults; + // set the start/end offsets perhaps? + $scope.numFound = solrResults.numFound; + // TODO: this is conflated logic. + // $scope.startOffset = solrResults.start + $scope.endOffset = solrResults.size + solrResults.start + $scope.$apply(); + break + case 'onState': + _self.updateState(data) + $scope.$apply() + break + case 'onStatus': + $scope.status = data; + $scope.$apply() + break + default: + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } + + }; + + // start a new search + $scope.execNewSearch = function() { + console.info('SolrGuiCtrl - Search Clicked!' + $scope.querystring); + // this is someone clicking the search button.. we should clear the filters and reset pagination + $scope.filters = []; + $scope.startOffset = 0; + $scope.execSearch(); + }; + + // run the search based on the current query params selected. + $scope.execSearch = function() { + mrl.sendTo($scope.service.name, "searchWithFacets", $scope.queryString, $scope.pageSize, $scope.startOffset, $scope.facetFields, $scope.filters); + } + + $scope.filter = function(field, value) { + // add the filter and run the search + $scope.filters.push(field + ":\"" + value + "\""); + // reset to first page when adding a new filter + $scope.startOffset = 0; + $scope.execSearch(); + } + + $scope.removeFilter = function(filter) { + // remove the filter that was passed in. + $scope.filters = $scope.filters.filter(e => e !== filter); + $scope.execSearch(); + } + + $scope.prevPage = function() { + // update the start offset and run the search + $scope.startOffset -= $scope.pageSize; + if ($scope.startOffset < 0) { + $scope.startOffset = 0; + } + $scope.execSearch(); + } + + $scope.nextPage = function() { + // update the start offset and run the search + $scope.startOffset += $scope.pageSize; + if ($scope.startOffset > $scope.numFound) { + $scope.startOffset -= $scope.pageSize; + } + $scope.execSearch(); + } + + $scope.playFile = function(filepath) { + // stop the audiofile if it's currently playing. + mrl.sendTo("audiofile", "stop"); + // start the new song. + mrl.sendTo("audiofile", "playFile", filepath[0]); + + // mrl.sendTo("foobar", "play", filepath[0]); + // mrl.sendTo("solr", "play", filepath[0]); + } + + msg.subscribe('publishResults'); + msg.subscribe(this); + // mrl.subscribe($scope.service.name, 'publishResults', $scope.service.results); + // $scope.panel.initDone(); }]); \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/serviceBodyDirective.js b/src/main/resources/resource/WebGui/app/service/serviceBodyDirective.js index 6551bacd36..22a1decd1a 100644 --- a/src/main/resources/resource/WebGui/app/service/serviceBodyDirective.js +++ b/src/main/resources/resource/WebGui/app/service/serviceBodyDirective.js @@ -47,7 +47,7 @@ angular.module('mrlapp.service').directive('serviceBody', ['peer', '$compile', ' newscope.toggleVirtual = function(virtual) { var service = mrl.getService(scope.panel.name) //service.isVirtual = !service.isVirtual - mrl.sendTo(scope.panel.name, 'setVirtual', virtual) + mrl.sendTo(scope.panel.name, 'setVirtual', !service.isVirtual) } newscope.showPeers = function(show) { 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 5877c61257..4abedeb490 100644 --- a/src/main/resources/resource/WebGui/app/service/tab-header.html +++ b/src/main/resources/resource/WebGui/app/service/tab-header.html @@ -20,12 +20,6 @@   help -
  • - - -   release - -
  • @@ -39,29 +33,6 @@   hide peers
  • -
  • - - -   virtualization on -   virtualization off - -
  • -
  • - - -   save service - - - -
  • -
  • - - -   save default - - - -
  • @@ -94,17 +65,17 @@ - @@ -121,13 +92,14 @@ - + - {{peer.getActualName(service, key)}}xxx - - + + + + {{peer.getActualName(service, key)}} diff --git a/src/main/resources/resource/WebGui/app/service/views/ArduinoGui.html b/src/main/resources/resource/WebGui/app/service/views/ArduinoGui.html index 056eafa938..1bce530544 100644 --- a/src/main/resources/resource/WebGui/app/service/views/ArduinoGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/ArduinoGui.html @@ -26,9 +26,6 @@ -
    - -
    {{service.boardType}} {{connectedStatus}} {{versionStatus}}
    version {{boardInfo.version}} @@ -72,6 +69,9 @@ + + +
    diff --git a/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html b/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html index ef99c2a8fa..93e1cac98b 100644 --- a/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html @@ -11,17 +11,17 @@ +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + +
    +
    + + +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + {{service.displayFilter}} mouse x y {{samplePoint.x}}x{{samplePoint.y}} {{stats.fps}} fps latency {{stats.latency}} ms + + +
    +
    + + + + + + + +
    + + + + +
    + +
    + + RECORDING + + +
    +
    +
    + + + + + + + + + + + +
    + + + +
    + + + +
    +
    + +
    + +
    + +  
    +
    + Adaptive threshold applies a threshold value based on a small region around a pixel.und it. + So we get different thresholds for different regions of the same image which gives better results for images with varying illumination. +
    + Block Size {{getFilter().blockSize}} + Subtracted {{getFilter().param1}} + +
    +
    + AddMask allows you to add a png with transparency as an overlay. This could be useful to mask off information for training. For example, removing a face portrait from its background. +
    + +
    +
    + In Euclidean geometry, an affine transformation is a geometric transformation that preserves lines and parallelism (but not necessarily distances and angles). + Rotational transformation would be an example. +
    + angle {{getFilter().angle}} +
    +
    +
    + Canny edge detection is a technique to extract useful structural information from different vision objects and dramatically reduce the amount of data to be processed. + It has been widely applied in various computer vision systems. +
    +
    + Aperture Size {{getFilter().apertureSize}} +
    + Low Threshold {{getFilter().lowThreshold}} +
    + High Threshold {{getFilter().highThreshold}} +
    +
    + Optical flow + is the pattern of apparent motion of image objects between two consecutive frames caused by the movemement of object or camera. + It is 2D vector field where each vector is a displacement vector showing the movement of points from first frame to second.. +
    +
    + Max Points {{getFilter().maxPointCnt}} +
    + Min Distance {{getFilter().minDistance}} +
    + Block Size {{getFilter().blockSize}} +
    + Quality {{getFilter().quality/100}} + + +
    + +
    +
    + diff --git a/src/main/resources/resource/WebGui/app/service/views/ClockGui.html b/src/main/resources/resource/WebGui/app/service/views/ClockGui.html index 3a518ac09f..4107890158 100644 --- a/src/main/resources/resource/WebGui/app/service/views/ClockGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/ClockGui.html @@ -1,9 +1,7 @@
    - time

    {{onTime}}

    - epoch in ms

    {{onEpoch}}

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

    {{onEpoch}}

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

    Cron Tab

    + + cron help + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    idcronservicemethodparameters
    + + {{ value.id }}{{ value.cronPattern }}{{ value.name }}{{ value.method }}{{ value.data }}
    + + + + + + + + + +
    + +
    History
    {{history.id}}{{ history.processedTime | epochToLocalDate }}
    +
    +
    diff --git a/src/main/resources/resource/WebGui/app/service/views/DocumentPipelineGui.html b/src/main/resources/resource/WebGui/app/service/views/DocumentPipelineGui.html new file mode 100755 index 0000000000..48aee1f039 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/views/DocumentPipelineGui.html @@ -0,0 +1,35 @@ +
    + Document Pipeline! + +

    Workflow Config

    +
    + Name : {{service.workFlowConfig.name}} +
    + Number of Threads : {{service.workFlowConfig.numWorkerThreads}} +
    +

    Stages

    + + + + + +
    +
  • Stage {{stage.stageName}}
  • +
  • Class {{stage.stageClass}}
  • +
    + + + + +
    {{key}}{{value}}
    +
    +
    + +
    +

    Document!

    +
    + {{document}} +
    + + +
    diff --git a/src/main/resources/resource/WebGui/app/service/views/FileConnectorGui.html b/src/main/resources/resource/WebGui/app/service/views/FileConnectorGui.html new file mode 100755 index 0000000000..e307bb48ba --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/views/FileConnectorGui.html @@ -0,0 +1,14 @@ +
    + File Traverser! this service can scan a file system and publish documents to other services. +
    +
    + + + +
    + +

    Document!

    +
    + {{document}} +
    +
    diff --git a/src/main/resources/resource/WebGui/app/service/views/IntroGui.html b/src/main/resources/resource/WebGui/app/service/views/IntroGui.html index e0935f6dac..b85d70d473 100644 --- a/src/main/resources/resource/WebGui/app/service/views/IntroGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/IntroGui.html @@ -27,7 +27,7 @@   -
    +
     
    diff --git a/src/main/resources/resource/WebGui/app/service/views/JMonkeyEngineGui.html b/src/main/resources/resource/WebGui/app/service/views/JMonkeyEngineGui.html index 7069199dd2..67f610ff58 100644 --- a/src/main/resources/resource/WebGui/app/service/views/JMonkeyEngineGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/JMonkeyEngineGui.html @@ -1,4 +1,5 @@

    nodes

    +
    selected {{selectedPath}}
    diff --git a/src/main/resources/resource/WebGui/app/service/views/Mpu6050Gui.html b/src/main/resources/resource/WebGui/app/service/views/Mpu6050Gui.html index 9d4d5453b4..fab190b13f 100644 --- a/src/main/resources/resource/WebGui/app/service/views/Mpu6050Gui.html +++ b/src/main/resources/resource/WebGui/app/service/views/Mpu6050Gui.html @@ -14,8 +14,8 @@ - - + +
    diff --git a/src/main/resources/resource/WebGui/app/service/views/OakDGui.html b/src/main/resources/resource/WebGui/app/service/views/OakDGui.html new file mode 100644 index 0000000000..85f399e218 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/views/OakDGui.html @@ -0,0 +1,51 @@ +
    + + + + + + + + + + +
    + +
    + +
    + + + + +
    Camera
    + + + +
    + + + + + + +
    + +
    + + + + + +
    + + + + + + + diff --git a/src/main/resources/resource/WebGui/app/service/views/OpenCVGui.html b/src/main/resources/resource/WebGui/app/service/views/OpenCVGui.html index 3cf2ec2821..6431c8fd48 100644 --- a/src/main/resources/resource/WebGui/app/service/views/OpenCVGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/OpenCVGui.html @@ -226,9 +226,8 @@
    - diff --git a/src/main/resources/resource/WebGui/app/service/views/PirGui.html b/src/main/resources/resource/WebGui/app/service/views/PirGui.html index 5a1f8fe4e2..e359428b82 100644 --- a/src/main/resources/resource/WebGui/app/service/views/PirGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/PirGui.html @@ -1,21 +1,21 @@

    - poll rate {{service.config.rate}} hz + poll rate {{service.config.rate}} hz

    pin - - - - - + + + +
    -
    + \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html b/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html index 26ff84dd23..8a9d2876c0 100644 --- a/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html @@ -19,11 +19,11 @@ - +
    - + @@ -48,8 +48,9 @@ + topic {{predicates['topic']}} @@ -78,7 +79,7 @@

    Location

    - {{service.config.botDir}} + {{getBotPath()}} diff --git a/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html b/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html index 1ba90bf2e4..d13bbce690 100644 --- a/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html @@ -1,48 +1,61 @@
    -
    - -
    - -
      -
    • - -
    • -
    -
    - - - - - -
    -
    -
    -
    - -
    + + + + + + + + + + + + + + +
    + Connected clients {{clients.length}} {{service.scriptRootDir}} + +
    + + + +     + + +     +
    + +
      +
    • + +
    • +
    +
    +
        + +
    @@ -51,18 +64,19 @@
    - - - + + + - {{getTabHeader(key)}}    + {{key}}    -
    - console
    + + +
    {{log}}
    -
    + + + + + + diff --git a/src/main/resources/resource/WebGui/app/service/views/PythonGui.html b/src/main/resources/resource/WebGui/app/service/views/PythonGui.html index 1ba90bf2e4..fc55075e7b 100644 --- a/src/main/resources/resource/WebGui/app/service/views/PythonGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/PythonGui.html @@ -1,48 +1,48 @@
    -
    -
    @@ -51,18 +51,19 @@
    - - - + + + - {{getTabHeader(key)}}    + {{key}}    -
    - console
    + + +
    {{log}}
    -
    + + + diff --git a/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html b/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html index 9e4b0bb92f..27bfb2d312 100644 --- a/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html @@ -1,4 +1,8 @@
    +
    +

    + {{service.wrongPlatformError}} +

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

    -

    -

    -
    sudo apt-get install -y i2c-tools
    +
    sudo raspi-config
    +
    + +
    +
    + +
    +
    sudo apt-get install -y i2c-tools

    + +
    +
    More recent rasbian distributions require building and installing this library https://github.com/WiringPi/WiringPi +
    + For information regarding Wiring pin numbering vs BCM visit : https://pinout.xyz/pinout/wiringpi
    + + bus {{index}} + {{index}} {{pin}}
    @@ -50,17 +65,22 @@

    -
    - {{ pinDef.pin }} - +
    + {{ pinDef.pin }} + + + + + + {{pinDef.value}} + + Rx Tx - Pwm + Pwm Sda Scl - - - {{pinDef.value}} +
    @@ -73,7 +93,9 @@ - + +
    + courtesy https://pinout.xyz
    diff --git a/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html b/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html index 86e9094338..7e1fd406ff 100644 --- a/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html @@ -1,16 +1,22 @@ -

    -    {{platform.arch}}.{{platform.jvmBitness}}.{{platform.os}} {{platform.mrlVersion}}     - -

    - -
    +

    {{platform.arch}}.{{platform.jvmBitness}}.{{platform.os}} {{platform.mrlVersion}}

    - - {{$select.selected.tag}} -     {{localeData.tag}} {{localeData.value.displayLanguage}} {{localeData.value.displayCountry}}      - -
    - + + + + + + +
    + + {{$select.selected.tag}} +     {{localeData.tag}} {{localeData.value.displayLanguage}} {{localeData.value.displayCountry}}      + +   + + +
    installed services installed services @@ -38,20 +44,17 @@


    - create a service - create a service -
    + +
    - -
    {{defaultsSaved}} -
    +
    -
    -
    + +
    @@ -70,35 +73,20 @@

    --> -
    -
    - Auto Start - - - + + + -
     
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    +

    + + + network + network +
    + +
    route table default @@ -153,3 +141,48 @@

    +
    + + + \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/views/ServoGui.html b/src/main/resources/resource/WebGui/app/service/views/ServoGui.html index 4be67ca27f..fc0cb1faee 100644 --- a/src/main/resources/resource/WebGui/app/service/views/ServoGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/ServoGui.html @@ -1,3 +1,4 @@ +
    @@ -22,17 +23,14 @@
    Controller Pin   - Enable  Invert   AutoDisable - TimeOut = {{service.idleTimeout/1000}} s + TimeOut = {{service.idleTimeout/1000}} s + Enable - - + @@ -42,20 +40,23 @@
    - - + + - - + + + - - + + + - -    + + - - + + + @@ -203,3 +204,5 @@
    + + diff --git a/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html b/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html index d9e75cd4f6..506af5198c 100644 --- a/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html @@ -1,126 +1,154 @@ - - - -
    -
    - - - - - - -
    - - Pose - {{state.selectedPose}}  - -
    - - - -
    - - - - - - - - - Blocking - -
    - -
    Sequence {{state.selectedSequenceFile}}
    - - - - - -
    -
    Sequence content
    - - - Blocking - - - - - - - - - - - - - -
    + + +
    +

    {{state.selectedGestureFile}} {{state.playingPose.name}}

    + + + + + + + + +
    Directory {{service.config.gesturesDir}}
    + Gesture +
    + + + + + + + + + +
    - -

    -
    -
    -
    -
    -
    -
    -
    + Content +
    + + + + + + + + + + +
    + + + + + + +
    + + + + + +
    + + +
    + Pose + + + + + + + + +
    +
    +
    +
    +   +
    +
    + + + + + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/src/main/resources/resource/WebGui/app/service/views/SolrGui.html b/src/main/resources/resource/WebGui/app/service/views/SolrGui.html index cf16cbdcf5..210ddc84e4 100644 --- a/src/main/resources/resource/WebGui/app/service/views/SolrGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/SolrGui.html @@ -1,13 +1,76 @@
    - HEY!!! - SolrGUI here!
    -
    - QueryString:
    - -
    +
    +
    + + + +
    +
    +
    + {{filter}} [x] +
    + + + Displaying search results {{startOffset+1}} to {{endOffset}} of {{numFound}} in {{solrResults.responseHeader.QTime}} milliseconds. + + Previous Next +
    - Query String: {{querystring}}
    - Query Results: {{service.solrResults}}
    + + + + + +
    + + + + +
    + + + + + + + +
    {{field}}
  • {{bucket}} ({{count}})
  • +
    +
    + + + + +
    + +
    + + + +
    Artist: {{result.artist[0]}}
    +
    Album: {{result.album[0]}}
    +
    Year: {{result.year[0]}}
    +
    Genre: {{result.genre[0]}}
    + Filepath: {{result.filepath[0]}} +
    + +
    + + Doc: {{$index + startOffset + 1}} + + + + + + + + +
    {{key}}{{value}}{{value}}
    + +
    +
    +
    Small view
    diff --git a/src/main/resources/resource/WebGui/app/service/views/SpotMicroGui.html b/src/main/resources/resource/WebGui/app/service/views/SpotMicroGui.html index 2b634c3e6e..7b15e873fb 100644 --- a/src/main/resources/resource/WebGui/app/service/views/SpotMicroGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/SpotMicroGui.html @@ -51,12 +51,14 @@
    Start
    Gestures
    + +
    Simulator
    - +
    @@ -67,7 +69,7 @@
    - +
    @@ -115,7 +117,7 @@
    - +
    @@ -181,7 +183,7 @@
    - +
    @@ -257,7 +259,7 @@
    - +
    @@ -318,7 +320,7 @@
    - +
    @@ -368,5 +370,21 @@
    + + + + + + + + +
    + + +
    + + +
    + diff --git a/src/main/resources/resource/WebGui/app/widget/attach.html b/src/main/resources/resource/WebGui/app/widget/attach.html index e714dbac88..69a5535e79 100644 --- a/src/main/resources/resource/WebGui/app/widget/attach.html +++ b/src/main/resources/resource/WebGui/app/widget/attach.html @@ -1,10 +1,11 @@ +
    - diff --git a/src/main/resources/resource/WebGui/app/widget/attach.js b/src/main/resources/resource/WebGui/app/widget/attach.js index 597537c770..a7d47cfff9 100644 --- a/src/main/resources/resource/WebGui/app/widget/attach.js +++ b/src/main/resources/resource/WebGui/app/widget/attach.js @@ -9,7 +9,8 @@ */ angular.module('mrlapp.service').directive('attach', ['mrl', function(mrl) { return { - restrict: "E", /* element only */ + restrict: "E", + /* element only */ templateUrl: 'widget/attach.html', scope: { options: '='/* 2 way binding - isolated scope */ @@ -19,10 +20,13 @@ angular.module('mrlapp.service').directive('attach', ['mrl', function(mrl) { link: function(scope, element) { var _self = this scope.mrl = mrl - if (!scope.options.controllerTitle){ + scope.runtime = mrl.getPanel('runtime') + scope.possibleServices = [] + + if (!scope.options.controllerTitle) { scope.options.controllerTitle = 'controller' } - + // if this was full canonical name - would the msg.send be unusseary scope.interfaceName = scope.options.interface if (scope.interfaceName.indexOf('.') == -1) { @@ -33,6 +37,21 @@ angular.module('mrlapp.service').directive('attach', ['mrl', function(mrl) { scope.options.attach(serviceName) scope.options.attachName = serviceName } + + scope.loadServices = function(serviceName) { + scope.possibleServices = [] + let runtime = mrl.getPanel('runtime') + let registry = mrl.getRegistry() + for (var name in registry) { + if (registry.hasOwnProperty(name)) { + var service = registry[name] + console.info(service.interfaceSet) + if (service.interfaceSet?.hasOwnProperty(scope.interfaceName)) { + scope.possibleServices.push(name) + } + } + } + } } } } diff --git a/src/main/resources/resource/WebGui/app/widget/oscope.html b/src/main/resources/resource/WebGui/app/widget/oscope.html index c25c00c834..90b4981eb6 100644 --- a/src/main/resources/resource/WebGui/app/widget/oscope.html +++ b/src/main/resources/resource/WebGui/app/widget/oscope.html @@ -1,30 +1,37 @@
    -
    +
    - - - +
    +
    +
    +
    +
    +
    +
    -
    - +
    +
    -
    +
    diff --git a/src/main/resources/resource/WebGui/app/widget/oscope.js b/src/main/resources/resource/WebGui/app/widget/oscope.js index 66b8271af3..d45a9f9a20 100644 --- a/src/main/resources/resource/WebGui/app/widget/oscope.js +++ b/src/main/resources/resource/WebGui/app/widget/oscope.js @@ -28,25 +28,19 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { }, // scope: true, link: function(scope, element) { - var _self = this; - var name = scope.serviceName; - var service = mrl.getService(name); - var mode = 'read'; - // 'read' || 'write' - var width = 800; - var height = 100; - var margin = 10; - var minY = margin; - var maxY = height - margin; - var scaleX = 1; - var scaleY = 1; - scope.readWrite = 'read'; - // button toggle read/write - // scope.blah = {}; - // scope.blah.display = false; + var _self = this + var name = scope.serviceName + var service = mrl.getService(name) + var width = 800 + var height = 100 + var margin = 10 + var minY = margin + var maxY = height - margin + var scaleX = 1 + var scaleY = 1 + scope.readWrite = 'read' scope.pinIndex = service.pinIndex; - // scope.addressIndex = service.addressIndex; - var x = 0; + // var x = 0 var gradient = tinygradient([{ h: 0, s: 0.4, @@ -57,262 +51,247 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { s: 0.4, v: 1, a: 1 - }]); - scope.oscope = {}; - scope.oscope.traces = {}; - scope.oscope.writeStates = {}; - // display update interfaces - // defintion stage + }]) + scope.oscope = {} + scope.oscope.traces = {} + scope.oscope.writeStates = {} + var setTraceButtons = function(pinIndex) { - if (pinIndex == null) { - return; + + if (Object.keys(scope.oscope.traces).length > 0) { + return } + + // let pinIndex = service.pinIndex var size = Object.keys(pinIndex).length - if (size && size > 0) { - scope.pinIndex = pinIndex; - var colorsHsv = gradient.hsv(size); - // pass over pinIndex add display data - for (var key in pinIndex) { - if (!pinIndex.hasOwnProperty(key)) { - continue; - } - scope.oscope.traces[key] = {}; - var trace = scope.oscope.traces[key]; - var pinDef = pinIndex[key]; + if (size > 0) { + var colorsHsv = gradient.hsv(size) - // adding style - var color = colorsHsv[pinDef.address]; - trace.readStyle = { - 'background-color': color.toHexString() - }; - trace.writeStyle = { - 'background-color': '#eee' - }; - trace.color = color; - trace.state = false; - // off - trace.posX = 0; - trace.posY = 0; - trace.count = 0; - trace.colorHexString = color.toHexString(); - trace.stats = { - min: 0, - max: 1, - totalValue: 0, - totalSample: 1 + Object.keys(pinIndex).forEach(function(pin) { + if (!pinIndex.hasOwnProperty(pin)) { + return } - } + var trace = { + readStyle: {}, + state: false, + posX: 0, + posY: 0, + x0: 0, + y0: 0, + x1: 0, + y1: 0, + count: 0, + stats: { + min: 0, + max: 1, + totalValue: 0, + totalSample: 1 + } + } + var pinDef = pinIndex[pin] + var color = colorsHsv[pinDef.address] + if (!color) { + return + } + trace.readStyle['background-color'] = color.toHexString() + trace.color = color + trace.colorHexString = color.toHexString() + scope.oscope.traces[pin] = trace + }) } } // FIXME this should be _self.onMsg = function(inMsg) this.onMsg = function(inMsg) { - //console.log('CALLBACK - ' + msg.method); + //console.log('CALLBACK - ' + msg.method) switch (inMsg.method) { case 'onState': - // backend update - setTraceButtons(inMsg.data[0].pinIndex); - scope.$apply(); - break; + // backend update + scope.pinIndex = inMsg.data[0].pinIndex + setTraceButtons(inMsg.data[0].pinIndex) + scope.$apply() + break case 'onPinArray': - x++; - pinArray = inMsg.data[0]; + // all pin traces are going to be traced at the same x position + // x++ + pinArray = inMsg.data[0] for (i = 0; i < pinArray.length; ++i) { // get pin data & definition - pinData = pinArray[i]; - pinDef = scope.pinIndex[pinData.pin]; + pinData = pinArray[i] + pinDef = scope.pinIndex[pinData.pin] // get correct screen and references - var screen = document.getElementById('oscope-pin-' + pinData.pin); - var ctx = screen.getContext('2d'); - var trace = scope.oscope.traces[pinData.pin]; - var stats = trace.stats; + // change to LET !! + let screen = document.getElementById(scope.serviceName + '-oscope-pin-' + pinData.pin) + let ctx = screen.getContext('2d'); + let trace = scope.oscope.traces[pinData.pin] + let stats = trace.stats + + // blank screen if trace reaches end + if (trace.x1 > width || trace.x0 == 0) { + trace.state = true + scope.highlight(trace, true) + ctx.font = "10px Aria" + ctx.rect(0, 0, width, height) + ctx.fillStyle = "black" + ctx.fill() + var highlight = trace.color.getOriginalInput() + highlight.s = "90%" + var newColor = tinycolor(highlight) + ctx.fillStyle = trace.colorHexString + // TODO - highlight saturtion of text + ctx.fillText('MAX ' + stats.max + ' ' + pinDef.pin + ' ' + pinDef.address, 10, minY) + ctx.fillText(('AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11), 10, height / 2) + ctx.fillText('MIN ' + stats.min, 10, maxY) + trace.x0 = 0 + trace.x1 = 0 + } + // draw it + + + // TODO - sample rate Hz - trace.stats.totalSample++; - trace.stats.totalValue += pinData.value; + trace.stats.totalSample++ + trace.stats.totalValue += pinData.value if (pinData.value < trace.stats.min) { - trace.stats.min = pinData.value; + trace.stats.min = pinData.value } if (pinData.value > trace.stats.max) { - trace.stats.max = pinData.value; + trace.stats.max = pinData.value } - var maxX = trace.stats.max; - var minX = trace.stats.min; - var c = minY + ((pinData.value - minX) * (maxY - minY)) / (maxX - minX); - var y = height - c; - ctx.beginPath(); - // from - ctx.moveTo(trace.posX, trace.posY); - // to - ctx.lineTo(x, y); + var maxX = trace.stats.max + var minX = trace.stats.min + var c = minY + ((pinData.value - minX) * (maxY - minY)) / (maxX - minX) + var y = height - c + ctx.beginPath() + // move to last position... + ctx.moveTo(trace.x0, trace.y0) + trace.x1++ + trace.y1 = y + // draw line to x1,y1 + ctx.lineTo(trace.x1, trace.y1) // save current values - trace.posX = x; - trace.posY = y; + trace.x0 = trace.x1 + trace.y0 = trace.y1 // color - ctx.strokeStyle = trace.colorHexString; + ctx.strokeStyle = trace.colorHexString // blank screen // TODO - continuous pan would be better - ctx.stroke(); - // blank screen if trace reaches end - if (x > width) { - trace.state = true; - scope.highlight(trace, true); - //scope.toggleReadButton(pinDef); - ctx.font = "10px Aria"; - ctx.rect(0, 0, width, height); - ctx.fillStyle = "black"; - ctx.fill(); - var highlight = trace.color.getOriginalInput(); - highlight.s = "90%"; - var newColor = tinycolor(highlight); - ctx.fillStyle = trace.colorHexString; - // TODO - highlight saturtion of text - ctx.fillText('MAX ' + stats.max + ' ' + pinDef.pin + ' ' + pinDef.address, 10, minY); - ctx.fillText(('AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11), 10, height / 2); - ctx.fillText('MIN ' + stats.min, 10, maxY); - trace.posX = 0; - } - // draw it - ctx.closePath(); - } - // for each pin - if (x > width) { - x = 0; + ctx.stroke() + ctx.closePath() } - break; + break default: // since we subscribed to "All" of Arduino's methods - most will escape here // no reason to put an error .. however, it would be better to "Only" susbscribe to the ones // we want - // console.log("ERROR - unhandled method " + inMsg.method); - break; + // console.log("ERROR - unhandled method " + inMsg.method) + break } } - ; - scope.toggleReadWrite = function() { - scope.readWrite = (scope.readWrite == 'write') ? 'read' : 'write'; - } - ; + scope.clearScreen = function(pinArray) { - for (i = 0; i < pinArray.length; ++i) { - pinData = pinArray[i]; - pinDef = scope.pinIndex[pinData.pin]; - _self.ctx = screen.getContext('2d'); - // ctx.scale(1, -1); // flip y around for cartesian - bad idea :P - // width = screen.width; - //height = screen.height; - _self.ctx.rect(0, 0, width, height); - _self.ctx.fillStyle = "black"; - _self.ctx.fill(); - _self.ctx.fillStyle = "white"; - stats = pinDef.stats; - _self.ctx.fillText(pinDef.name + (' AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11) + ' MIN ' + stats.min + ' MAX ' + stats.max, 10, 18); + for (i = 0; i < scope.pinArray.length; ++i) { + pinData = pinArray[i] + pinDef = scope.pinIndex[pinData.pin] + _self.ctx = screen.getContext('2d') + _self.ctx.rect(0, 0, width, height) + _self.ctx.fillStyle = "black" + _self.ctx.fill() + _self.ctx.fillStyle = "white" + stats = pinDef.stats + _self.ctx.fillText(pinDef.name + (' AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11) + ' MIN ' + stats.min + ' MAX ' + stats.max, 10, 18) } } - scope.zoomIn = function() { - scaleX += 1; - scaleY += 1; - _self.ctx.scale(scaleX, scaleY); - } - ; // RENAME eanbleTrace - FIXME read values vs write values | ALL values from service not from ui !! - ui only sends commands scope.activateTrace = function(pinDef) { - var trace = scope.oscope.traces[pinDef.pin]; + var trace = scope.oscope.traces[pinDef.pin] if (trace.state) { - toggleReadButton(trace); - mrl.sendTo(name, 'disablePin', pinDef.pin); - trace.state = false; + toggleReadButton(trace) + mrl.sendTo(name, 'disablePin', pinDef.pin) + trace.state = false } else { - toggleReadButton(trace); - mrl.sendTo(name, 'enablePin', pinDef.pin); - trace.state = true; + toggleReadButton(trace) + // mrl.sendTo(name, 'enablePin', pinDef.pin) + mrl.sendTo(name, 'enablePin', pinDef.pin, 1) + trace.state = true } } - ; + scope.reset = function() { - mrl.sendTo(name, 'disablePins'); + mrl.sendTo(name, 'disablePins') } - ; - scope.write = function(pinDef) { - scope.toggleWriteButton(trace); - mrl.sendTo(name, 'digitalWrite', pinDef.pin, 1); - // trace.state = true; - /* 3 states READ/ENABLE | DIGITALWRITE | ANALOGWRITE - if (pinDef.pinName.charAt(0) == 'A') { - _self.toggleWriteButton(trace); - mrl.sendTo(name, 'analogWrite', 1); - trace.state = false; - } else { - _self.toggleWriteButton(trace); - mrl.sendTo(name, 'digitalWrite', pinDef.address); - trace.state = true; - } - */ - } - ; scope.reset = function() { - mrl.sendTo(name, 'disablePins'); + mrl.sendTo(name, 'disablePins') } - ; + var toggleReadButton = function(trace) { - var highlight = trace.color.getOriginalInput(); + var highlight = trace.color.getOriginalInput() if (trace.state) { - scope.highlight(trace, false); + scope.highlight(trace, false) } else { - scope.highlight(trace, true); + scope.highlight(trace, true) } - }; + } scope.highlight = function(trace, on) { - var highlight = trace.color.getOriginalInput(); + var highlight = trace.color.getOriginalInput() if (!on) { - // scope.blah.display = false; + // scope.blah.display = false // on to off - highlight.s = "40%"; - var newColor = color = tinycolor(highlight); + highlight.s = "40%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } else { - // scope.blah.display = true; + // scope.blah.display = true // off to on - highlight.s = "90%"; - var newColor = color = tinycolor(highlight); + highlight.s = "90%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } } - ; scope.toggleWriteButton = function(pinDef) { - var highlight = trace.color.getOriginalInput(); + var highlight = trace.color.getOriginalInput() if (trace.state) { - // scope.blah.display = false; + // scope.blah.display = false // on to off - highlight.s = "40%"; - var newColor = color = tinycolor(highlight); + highlight.s = "40%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } else { - // scope.blah.display = true; + // scope.blah.display = true // off to on - highlight.s = "90%"; - var newColor = color = tinycolor(highlight); + highlight.s = "90%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } } - ; // FIXME FIXME FIXME ->> THIS SHOULD WORK subscribeToServiceMethod <- but doesnt - mrl.subscribeToService(_self.onMsg, name); + mrl.subscribeToService(_self.onMsg, name) // this siphons off a single subscribe to the webgui // so it will be broadcasted back to angular - mrl.subscribe(name, 'publishPinArray'); - mrl.subscribeToServiceMethod(_self.onMsg, name, 'publishPinArray'); + mrl.subscribe(name, 'publishPinArray') + mrl.subscribeToServiceMethod(_self.onMsg, name, 'publishPinArray') // initializing display data - setTraceButtons(service.pinIndex); + setTraceButtons(service.pinIndex) } - }; + } } -]); +]).filter('toArray', function() { + return function(obj) { + if (!angular.isObject(obj)) { + return obj; + } + return Object.keys(obj).map(function(key) { + return obj[key]; + }); + } + ; +}); diff --git a/src/main/resources/resource/WebkitSpeechRecognition/webkitspeechrecognition.yml b/src/main/resources/resource/WebkitSpeechRecognition/webkitspeechrecognition.yml index ad60a72cfd..3a5114b024 100644 --- a/src/main/resources/resource/WebkitSpeechRecognition/webkitspeechrecognition.yml +++ b/src/main/resources/resource/WebkitSpeechRecognition/webkitspeechrecognition.yml @@ -1,4 +1,5 @@ !!org.myrobotlab.service.config.WebkitSpeechRecognitionConfig +afterSpeakingPauseMs: 2000 listeners: null listening: false peers: null @@ -6,3 +7,4 @@ recording: false textListeners: null type: WebkitSpeechRecognition wakeWord: null +wakeWordIdleTimeoutSeconds: 10 diff --git a/src/main/resources/resource/config/mediasearch/audiofile.yml b/src/main/resources/resource/config/mediasearch/audiofile.yml new file mode 100755 index 0000000000..f7864e5f28 --- /dev/null +++ b/src/main/resources/resource/config/mediasearch/audiofile.yml @@ -0,0 +1,15 @@ +!!org.myrobotlab.service.config.AudioFileConfig +audioListeners: [ + ] +currentPlaylist: default +currentTrack: default +listeners: null +mute: false +peakDelayMs: null +peakMultiplier: 100.0 +peakSampleInterval: 15.0 +peers: null +playlists: { + } +type: AudioFile +volume: 1.0 diff --git a/src/main/resources/resource/config/mediasearch/docproc.yml b/src/main/resources/resource/config/mediasearch/docproc.yml new file mode 100755 index 0000000000..159c6fd091 --- /dev/null +++ b/src/main/resources/resource/config/mediasearch/docproc.yml @@ -0,0 +1,36 @@ +!!org.myrobotlab.service.config.DocumentPipelineConfig +listeners: +- callback: onFlush + listener: solr + method: publishFlush +- callback: onDocument + listener: solr + method: publishDocument +peers: null +type: DocumentPipeline +workFlowConfig: + config: { + } + name: default + numWorkerThreads: 8 + queueLength: 50 + stages: + - config: + type: file + stageClass: org.myrobotlab.document.transformer.SetStaticFieldValue + stageName: SetTypeField + - config: { + } + stageClass: org.myrobotlab.document.transformer.TextExtractor + stageName: TextExtractor + - config: + fieldNameMap: + xmpdm_duration: duration + xmpdm_genre: genre + dc_title: title + xmpdm_tracknumber: tracknumber + xmpdm_artist: artist + xmpdm_album: album + xmpdm_releasedate: year + stageClass: org.myrobotlab.document.transformer.RenameFields + stageName: RenameFields diff --git a/src/main/resources/resource/config/mediasearch/fileconnector.yml b/src/main/resources/resource/config/mediasearch/fileconnector.yml new file mode 100755 index 0000000000..f505ff3e98 --- /dev/null +++ b/src/main/resources/resource/config/mediasearch/fileconnector.yml @@ -0,0 +1,11 @@ +!!org.myrobotlab.service.config.FileConnectorConfig +directory: Z:\Music +listeners: +- callback: onFlush + listener: docproc + method: publishFlush +- callback: onDocument + listener: docproc + method: publishDocument +peers: null +type: FileConnector diff --git a/src/main/resources/resource/config/mediasearch/runtime.yml b/src/main/resources/resource/config/mediasearch/runtime.yml new file mode 100755 index 0000000000..cf77865501 --- /dev/null +++ b/src/main/resources/resource/config/mediasearch/runtime.yml @@ -0,0 +1,19 @@ +!!org.myrobotlab.service.config.RuntimeConfig +enableCli: true +id: null +listeners: [ + ] +locale: null +logLevel: info +peers: null +registry: +- runtime +- security +- webgui +- solr +- audiofile +- docproc +- fileconnector +resource: resource +type: Runtime +virtual: false diff --git a/src/main/resources/resource/BoofCv/boofcv.yml b/src/main/resources/resource/config/mediasearch/security.yml old mode 100644 new mode 100755 similarity index 83% rename from src/main/resources/resource/BoofCv/boofcv.yml rename to src/main/resources/resource/config/mediasearch/security.yml index c3a1f232ff..aa33d68612 --- a/src/main/resources/resource/BoofCv/boofcv.yml +++ b/src/main/resources/resource/config/mediasearch/security.yml @@ -1,4 +1,4 @@ !!org.myrobotlab.service.config.ServiceConfig listeners: null peers: null -type: BoofCv +type: Security diff --git a/src/main/resources/resource/config/mediasearch/solr.yml b/src/main/resources/resource/config/mediasearch/solr.yml new file mode 100755 index 0000000000..34aba0b58b --- /dev/null +++ b/src/main/resources/resource/config/mediasearch/solr.yml @@ -0,0 +1,6 @@ +!!org.myrobotlab.service.config.SolrConfig +embedded: true +listeners: null +peers: null +solrUrl: http://localhost:8983/solr +type: Solr diff --git a/src/main/resources/resource/config/mediasearch/webgui.yml b/src/main/resources/resource/config/mediasearch/webgui.yml new file mode 100755 index 0000000000..4744de99ba --- /dev/null +++ b/src/main/resources/resource/config/mediasearch/webgui.yml @@ -0,0 +1,10 @@ +!!org.myrobotlab.service.config.WebGuiConfig +autoStartBrowser: true +enableMdns: false +listeners: null +peers: null +port: 8888 +resources: +- ./resource/WebGui/app +- ./resource +type: WebGui diff --git a/src/main/resources/resource/framework/pom.xml.template b/src/main/resources/resource/framework/pom.xml.template index 4e0df36252..8a72e3c9ef 100644 --- a/src/main/resources/resource/framework/pom.xml.template +++ b/src/main/resources/resource/framework/pom.xml.template @@ -141,6 +141,10 @@ **/*.java + + src/main/resources + ${project.basedir} + @@ -370,7 +374,7 @@ org.apache.maven.plugins 2.22.2 - -Djava.library.path=libraries/native -Djna.library.path=libraries/native ${argLine} + -Djava.library.path=libraries/native -Djna.library.path=libraries/native **/*Test.java @@ -378,12 +382,6 @@ **/integration/* - diff --git a/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java b/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java index 9af4c76287..064c8533fc 100644 --- a/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java +++ b/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java @@ -1,24 +1,28 @@ package org.myrobotlab.codec; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.Map; - +import org.bouncycastle.util.Strings; import org.junit.Ignore; import org.junit.Test; import org.myrobotlab.codec.json.JsonDeserializationException; import org.myrobotlab.framework.Message; +import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.StaticType; -import org.myrobotlab.framework.TimeoutException; -import org.myrobotlab.net.Http; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.data.Locale; +import org.myrobotlab.service.data.Orientation; import org.myrobotlab.test.AbstractTest; import org.myrobotlab.utils.ObjectTypePair; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + public class CodecUtilsTest extends AbstractTest { @Ignore /* @@ -67,6 +71,86 @@ public void testLocale() { "{\"language\":\"\",\"displayLanguage\":\"\",\"country\":\"US\",\"displayCountry\":\"United States\",\"tag\":\"-US\",\"class\":\"org.myrobotlab.service.data.Locale\"}", json); + } + + @Test + public void testExtractParams() { + // /runtime/connect/"http://blah:8888" + String input = null; + Object[] params = null; + double delta = 0.0001; + + input = "\"http://blah:8888\""; + params = CodecUtils.extractJsonParamsFromPath(input); + assertEquals("\"http://blah:8888\"", params[0]); + + input = "\"http://blah:8888/this/path/\""; + params = CodecUtils.extractJsonParamsFromPath(input); + assertEquals("\"http://blah:8888/this/path/\"", params[0]); + + input = "\"http://blah:8888/this/path/\"/5"; + params = CodecUtils.extractJsonParamsFromPath(input); + assertEquals("\"http://blah:8888/this/path/\"", params[0]); + assertEquals("5", params[1]); + + input = "[3,5,7,8]/[1.0, 2.0, 3.0]/true"; + params = CodecUtils.extractJsonParamsFromPath(input); + List list = (List)CodecUtils.fromJson((String)params[0]); + assertEquals(8, list.get(3)); + list = (List)CodecUtils.fromJson((String)params[1]); + assertEquals(3.0,(double)list.get(2), delta); + assertEquals(true, (boolean)CodecUtils.fromJson((String)params[2])); + + input = "[\"apple\",\"banana\",\"orange\"]/[\"a\",\"b\",\"c\"]/true"; + params = CodecUtils.extractJsonParamsFromPath(input); + list = (List)CodecUtils.fromJson((String)params[0]); + assertEquals("apple",list.get(0)); + list = (List)CodecUtils.fromJson((String)params[1]); + assertEquals("a", list.get(0)); + assertEquals(true, (boolean)CodecUtils.fromJson((String)params[2])); + + String[] files =new String[] {"f:\\testdir\\blah","/root/","/home/mydir/blah"}; + String[] abc = new String[] {"a", "b", "c"}; + + input = CodecUtils.toJson(files) + "/" + CodecUtils.toJson(abc) + "/" + CodecUtils.toJson(true); + + // input = "\"[\"f:\\testdir\\blah\",\"/root/\",\"/home/mydir/blah\"]\"/\"[\"a\",\"b\",\"c\"]\"/true"; + params = CodecUtils.extractJsonParamsFromPath(input); + assertEquals(3, params.length); + list = (List)CodecUtils.fromJson((String)params[0]); + assertEquals("/root/", list.get(1)); + // String[] files = CodecUtils.fromJson(params[0], String[].class); + // assertEquals("\"[\"apple\",\"banana\",\"orange\"]\"", CodecUtils.fromJson(params[0], String[].class)); + Orientation o = new Orientation(); + o.pitch = 2.2342; + o.yaw = 1.234; + o.roll = 0.343; + + input = CodecUtils.toJson(files) + "/" + CodecUtils.toJson(o) + "/" + CodecUtils.toJson(true); + params = CodecUtils.extractJsonParamsFromPath(input); + assertEquals(3, params.length); + Map t = (Map)CodecUtils.fromJson((String)params[1]); + + assertEquals(2.2342, (double)t.get("pitch"), delta); + + input = "This is invalid json /and a block of/ text between/and/a single character/ ."; + params = CodecUtils.extractJsonParamsFromPath(input); + assertEquals(6, params.length); + boolean strict = false; + try { + CodecUtils.fromJson(input); + } catch(Exception e) { + // strict is required + strict = true; + } + assertTrue(strict); + + input = "\"This is valid json /and a block of\"/\" text between/and/a single character/ .\""; + params = CodecUtils.extractJsonParamsFromPath(input); + assertEquals(2, params.length); + assertEquals("This is valid json /and a block of", CodecUtils.fromJson((String)params[0])); + + } @Test @@ -150,4 +234,35 @@ public void returnMessageTestSimpleTypes() { } + public void testNormalizeServiceName() { + Platform.getLocalInstance().setId("test-id"); + assertEquals("runtime@test-id", CodecUtils.getFullName("runtime")); + assertEquals("runtime@test-id", CodecUtils.getFullName("runtime@test-id")); + } + + @Test + public void testCheckServiceNameEqual() { + Platform.getLocalInstance().setId("test-id"); + assertTrue(CodecUtils.checkServiceNameEquality("runtime", "runtime")); + assertTrue(CodecUtils.checkServiceNameEquality("runtime", "runtime@test-id")); + assertTrue(CodecUtils.checkServiceNameEquality("runtime@test-id", "runtime")); + assertTrue(CodecUtils.checkServiceNameEquality("runtime@test-id", "runtime@test-id")); + assertFalse(CodecUtils.checkServiceNameEquality("runtime", "runtime@not-corr-id")); + assertFalse(CodecUtils.checkServiceNameEquality("runtime@not-corr-id", "runtime")); + + } + + @Test + public void testBase64() { + // not a very comprehensive test, but a sanity check none the less. + String input = "input string."; + String output = CodecUtils.toBase64(input.getBytes()); + assertEquals(output.length(), 20); + byte[] covertedBack = CodecUtils.fromBase64(output); + String result = Strings.fromByteArray(covertedBack); + assertEquals(input, result); + + } + + } diff --git a/src/test/java/org/myrobotlab/codec/ForeignProcessUtilsTest.java b/src/test/java/org/myrobotlab/codec/ForeignProcessUtilsTest.java new file mode 100644 index 0000000000..4665ffbba2 --- /dev/null +++ b/src/test/java/org/myrobotlab/codec/ForeignProcessUtilsTest.java @@ -0,0 +1,80 @@ +package org.myrobotlab.codec; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.myrobotlab.test.AbstractTest; + +public class ForeignProcessUtilsTest extends AbstractTest { + + @Test + public void testValidJavaClassName() { + assertTrue("False negative when checking valid class name", + ForeignProcessUtils.isValidJavaClassName("ExampleClass")); + + assertTrue("False negative when checking valid class name", + ForeignProcessUtils.isValidJavaClassName("com.example.ExampleClass")); + } + + @Test + public void testInvalidJavaClassName() { + assertFalse("False positive when checking empty class name", + ForeignProcessUtils.isValidJavaClassName("")); + + assertFalse("False positive when checking valid class name", + ForeignProcessUtils.isValidJavaClassName("^ExampleClass")); + + assertFalse("False positive when checking valid class name", + ForeignProcessUtils.isValidJavaClassName("py:com.example.ExampleClass")); + } + + @Test + public void testValidTypeKey() { + assertTrue("False negative when checking valid type key", + ForeignProcessUtils.isValidTypeKey("ExampleClass")); + + assertTrue("False negative when checking valid type key", + ForeignProcessUtils.isValidTypeKey("com.example.ExampleClass")); + + assertTrue("False negative when checking valid type key", + ForeignProcessUtils.isValidTypeKey("py:com.example.ExampleClass")); + } + + @Test + public void testInvalidTypeKey() { + assertFalse("False positive when checking empty type key", + ForeignProcessUtils.isValidTypeKey("")); + + assertFalse("False positive when checking invalid type key", + ForeignProcessUtils.isValidTypeKey("^com.example.ExampleClass")); + + assertFalse("False positive when checking invalid type key", + ForeignProcessUtils.isValidTypeKey(":py:com.example.ExampleClass")); + } + + @Test + public void testGetLanguageId() { + assertThrows(IllegalArgumentException.class, () -> ForeignProcessUtils.getLanguageId(":bad_id")); + String languageId = "py"; + String type_key = languageId + ":exampleService"; + assertEquals("Incorrect language id", languageId, ForeignProcessUtils.getLanguageId(type_key)); + + languageId = "rust"; + type_key = languageId + ":mrl::exampleService"; + assertEquals("Incorrect language id", languageId, ForeignProcessUtils.getLanguageId(type_key)); + } + + @Test + public void testGetLanguageSpecificTypeKey() { + assertThrows(IllegalArgumentException.class, () -> ForeignProcessUtils.getLanguageSpecificTypeKey(":bad_id")); + String langTypeKey = "exampleService"; + String typeKey = "py:" + langTypeKey; + assertEquals("Incorrect language-specific type key", langTypeKey, + ForeignProcessUtils.getLanguageSpecificTypeKey(typeKey)); + + langTypeKey = "mrl::exampleService"; + typeKey = "rust:" + langTypeKey; + assertEquals("Incorrect language-specific type key", langTypeKey, + ForeignProcessUtils.getLanguageSpecificTypeKey(typeKey)); + } + +} diff --git a/src/test/java/org/myrobotlab/codec/json/GsonPolymorphicTest.java b/src/test/java/org/myrobotlab/codec/json/GsonPolymorphicTest.java deleted file mode 100644 index 9233583688..0000000000 --- a/src/test/java/org/myrobotlab/codec/json/GsonPolymorphicTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.myrobotlab.codec.json; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.framework.Message; -import org.myrobotlab.framework.Registration; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.test.AbstractTest; -import org.slf4j.Logger; - -import java.io.Serializable; -import java.util.Arrays; - -public class GsonPolymorphicTest extends AbstractTest { - public final static Logger log = LoggerFactory.getLogger(GsonPolymorphicTest.class); - - private static Gson polymorphicGson; - - private static Gson regularGson; - - @BeforeClass - public static void setup() { - polymorphicGson = new GsonBuilder().registerTypeAdapterFactory(new GsonPolymorphicTypeAdapterFactory()) - .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").disableHtmlEscaping().create(); - - - regularGson = new GsonBuilder() - .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").disableHtmlEscaping().create(); - } - - @Test - public void testStringSer() { - String testString = "this is a test with spaces and $pecial characters!"; - String jsonString = polymorphicGson.toJson(testString); - log.debug("Encoded test string: " + jsonString); - String decodedString = regularGson.fromJson(jsonString, String.class); - log.debug("Decoded test string: " + decodedString); - Assert.assertEquals("String encoding not correct", testString, decodedString); - } - - @Test - public void testStringArraySer() { - String[] testStrings = new String[] {"This", "is", "a", "test", "array"}; - String jsonString = polymorphicGson.toJson(testStrings); - log.debug("Encoded test string: " + jsonString); - String[] decodedStrings = regularGson.fromJson(jsonString, String[].class); - log.debug("Decoded test strings: " + Arrays.toString(decodedStrings)); - Assert.assertArrayEquals("String array encoding incorrect", testStrings, decodedStrings); - } - - @Test - public void testNumberSer() { - int testInt = 42; - String jsonString = polymorphicGson.toJson(testInt); - log.debug("Encoded test string: " + jsonString); - int decodedInt = regularGson.fromJson(jsonString, Integer.class); - log.debug("Decoded test int: " + decodedInt); - Assert.assertEquals("Int encoding not correct", testInt, decodedInt); - } - - @Test - public void testBoolSer() { - boolean testBoolean = false; - String jsonString = polymorphicGson.toJson(testBoolean); - log.debug("Encoded test string: " + jsonString); - boolean decodedBoolean = regularGson.fromJson(jsonString, Boolean.class); - log.debug("Decoded test string: " + decodedBoolean); - Assert.assertEquals("Boolean encoding not correct", testBoolean, decodedBoolean); - } - - - @Test - public void testStringDeser() { - String testString = "this is a test with spaces and $pecial characters!"; - String jsonString = regularGson.toJson(testString); - log.debug("Encoded test string: " + jsonString); - String decodedString = polymorphicGson.fromJson(jsonString, String.class); - log.debug("Decoded test string: " + decodedString); - Assert.assertEquals("String decoding not correct", testString, decodedString); - } - - @Test - public void testStringArrayDeser() { - String[] testStrings = new String[] {"This", "is", "a", "test", "array"}; - String jsonString = regularGson.toJson(testStrings); - log.debug("Encoded test string: " + jsonString); - String[] decodedStrings = polymorphicGson.fromJson(jsonString, String[].class); - log.debug("Decoded test strings: " + Arrays.toString(decodedStrings)); - Assert.assertArrayEquals("String array decoding incorrect", testStrings, decodedStrings); - } - - @Test - public void testNumberDeser() { - int testInt = 42; - String jsonString = regularGson.toJson(testInt); - log.debug("Encoded test string: " + jsonString); - int decodedInt = polymorphicGson.fromJson(jsonString, Integer.class); - log.debug("Decoded test int: " + decodedInt); - Assert.assertEquals("Int decoding not correct", testInt, decodedInt); - } - - @Test - public void testBoolDeser() { - boolean testBoolean = false; - String jsonString = regularGson.toJson(testBoolean); - log.debug("Encoded test string: " + jsonString); - - boolean decodedBoolean = polymorphicGson.fromJson(jsonString, Boolean.class); - log.debug("Decoded test string: " + decodedBoolean); - Assert.assertEquals("Boolean decoding not correct", testBoolean, decodedBoolean); - } - - @Test - public void testApiDescriptionDeser() { - CodecUtils.ApiDescription description = new CodecUtils.ApiDescription("key", - "/path/", "{exampleURI}", "This is a description"); - String jsonString = regularGson.toJson(description); - log.debug("Encoded test string: " + jsonString); - CodecUtils.ApiDescription decodedDescription = polymorphicGson.fromJson(jsonString, CodecUtils.ApiDescription.class); - log.debug("Decoded test string: " + decodedDescription); - Assert.assertEquals("ApiDescription decoding not correct", description, decodedDescription); - } - -} diff --git a/src/test/java/org/myrobotlab/document/connector/WikipediaIndexer.java b/src/test/java/org/myrobotlab/document/connector/WikipediaIndexer.java index c6f813a952..20faa46fdc 100644 --- a/src/test/java/org/myrobotlab/document/connector/WikipediaIndexer.java +++ b/src/test/java/org/myrobotlab/document/connector/WikipediaIndexer.java @@ -72,7 +72,7 @@ public static void main(String[] args) throws ClassNotFoundException { docproc.initalize(); docproc.startService(); // attach the doc proc to the connector - wikipediaConnector.addDocumentListener(docproc); + wikipediaConnector.attachDocumentListener(docproc.getName()); wikipediaConnector.setBatchSize(500); // start crawling... wikipediaConnector.getOutbox().setMaxQueueSize(1); diff --git a/src/test/java/org/myrobotlab/framework/MethodCacheTest.java b/src/test/java/org/myrobotlab/framework/MethodCacheTest.java index a7b1117327..b92cc872c7 100644 --- a/src/test/java/org/myrobotlab/framework/MethodCacheTest.java +++ b/src/test/java/org/myrobotlab/framework/MethodCacheTest.java @@ -6,7 +6,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Arrays; import org.junit.BeforeClass; import org.junit.Test; @@ -14,7 +13,6 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.service.Clock; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.TestCatcher; import org.myrobotlab.service.TestCatcher.Ball; @@ -32,18 +30,8 @@ public class MethodCacheTest extends AbstractTest { @BeforeClass public static void setUpBeforeClass() throws Exception { cache = MethodCache.getInstance(); - cache.cacheMethodEntries(TestCatcher.class); cache.clear(); assertEquals("all clear should be 0", 0, cache.getObjectSize()); - - cache.cacheMethodEntries(Runtime.class); - cache.cacheMethodEntries(TestCatcher.class); - cache.cacheMethodEntries(Clock.class); - // non-service entry - cache.cacheMethodEntries(TestCatcher.class); - cache.cacheMethodEntries(TestCatcher.class); - assertEquals(String.format("cached 3 object's methods %s", Arrays.toString(cache.getCachedObjectNames().toArray())), 3, cache.getObjectSize()); - tester = (TestCatcher) Runtime.start("tester", "TestCatcher"); } diff --git a/src/test/java/org/myrobotlab/framework/ProxyFactoryTest.java b/src/test/java/org/myrobotlab/framework/ProxyFactoryTest.java new file mode 100644 index 0000000000..8515b33a08 --- /dev/null +++ b/src/test/java/org/myrobotlab/framework/ProxyFactoryTest.java @@ -0,0 +1,41 @@ +package org.myrobotlab.framework; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.myrobotlab.framework.interfaces.ServiceInterface; +import org.myrobotlab.service.interfaces.PinListener; +import org.myrobotlab.test.AbstractTest; + +import java.util.List; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ProxyFactoryTest extends AbstractTest { + + @BeforeClass + public static void setup() { + ProxyInterceptor.timeout = 1000; + } + + @Test + public void testProxyFactory() { + + Registration reg = new Registration("testID", "testService", "java:TestProxy"); + reg.interfaces = List.of(ServiceInterface.class.getName()); + ServiceInterface proxy = ProxyFactory.createProxyService(reg); + // TimeoutException indicates we're getting all the way to + // Service.waitOn(), so things *probably* work. + // FIXME use mocks or startup a second instance to rigorously test + assertThrows(TimeoutException.class, proxy::getName); + } + + @Test + public void testMultiInterfaces() { + Registration reg = new Registration("testID", "testService", "java:TestProxy"); + reg.interfaces = List.of(ServiceInterface.class.getName(), PinListener.class.getName()); + ServiceInterface proxy = ProxyFactory.createProxyService(reg); + assertTrue(PinListener.class.isAssignableFrom(proxy.getClass())); + assertThrows(TimeoutException.class, () -> ((PinListener) proxy).getPin()); + } +} diff --git a/src/test/java/org/myrobotlab/framework/ResourceTest.java b/src/test/java/org/myrobotlab/framework/ResourceTest.java index 19ee506c1b..7a20c7de86 100644 --- a/src/test/java/org/myrobotlab/framework/ResourceTest.java +++ b/src/test/java/org/myrobotlab/framework/ResourceTest.java @@ -29,9 +29,6 @@ public void test() throws InterruptedException, IOException { // === access as a service === Servo servo = (Servo) Runtime.start("servo", "Servo"); - String path = Service.getResourceRoot(); - assertTrue(path.startsWith("src")); - byte[] resource = Service.getServiceIcon(Servo.class); byte[] strParam = Service.getServiceIcon("Servo"); byte[] nonStatic = servo.getServiceIcon(); @@ -54,17 +51,6 @@ public void test() throws InterruptedException, IOException { assertTrue(rs != null && (rs.contentEquals(s) && rs.contentEquals(script) && rs.contentEquals(resourceString) && rs.contentEquals(strParams) && rs.contentEquals(b) && rs.contentEquals(bs) && rs.contentEquals(serviceScript) && rs.contentEquals(bstr))); - // make fake repo dir - and check overrides - String fs = File.separator; - File repoDir = new File(".." + fs + "Servo" + fs + "resource" + fs + "Servo"); - repoDir.mkdirs(); - rs = servo.getServiceScript(); - - // should now be null - if override is correct - assertTrue(rs == null); - - // cleanup - FileIO.rm(".." + fs + "Servo"); } } \ No newline at end of file diff --git a/src/test/java/org/myrobotlab/framework/ServiceTest.java b/src/test/java/org/myrobotlab/framework/ServiceTest.java new file mode 100644 index 0000000000..a9f180362c --- /dev/null +++ b/src/test/java/org/myrobotlab/framework/ServiceTest.java @@ -0,0 +1,46 @@ +package org.myrobotlab.framework; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.test.AbstractTest; + +public class ServiceTest extends AbstractTest { + + public static class TestService extends Service { + + private static final long serialVersionUID = 1L; + + /** + * Constructor of service, reservedkey typically is a services name and inId + * will be its process id + * + * @param reservedKey the service name + * @param inId process id + */ + public TestService(String reservedKey, String inId) { + super(reservedKey, inId); + } + } + + @Test + public void testConfigListenerFiltering() { + Platform.getLocalInstance().setId("test-id"); + TestService t = new TestService("test", "test-id"); + List listeners = List.of( + new MRLListener("meth", "webgui@webgui-client", "onMeth"), + new MRLListener("meth", "random@test-id", "onMeth"), + new MRLListener("meth", "random2@test-2-id", "onMeth") + ); + t.apply(new ServiceConfig()); + t.outbox.notifyList = Map.of("meth", listeners); + List filtered = t.getFilteredConfig().listeners; + assertEquals("random", filtered.get(0).listener); + assertEquals("random2@test-2-id", filtered.get(1).listener); + t.getFilteredConfig(); + } +} diff --git a/src/test/java/org/myrobotlab/io/FileIOTest.java b/src/test/java/org/myrobotlab/io/FileIOTest.java index 9f8d2ef39f..a8700219a8 100644 --- a/src/test/java/org/myrobotlab/io/FileIOTest.java +++ b/src/test/java/org/myrobotlab/io/FileIOTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.io.File; import java.io.FileNotFoundException; @@ -85,6 +86,27 @@ public void testGetRoot() { String ret = FileIO.getRoot(); assertNotNull(ret); } + + @Test + public void testCheckDir() throws IOException { + File check = new File("checkDir"); + check.delete(); + assertFalse(FileIO.checkDir(check.getName())); + check.mkdir(); + assertTrue(FileIO.checkDir(check.getName())); + check.delete(); + } + + @Test + public void testCheckFile() throws IOException { + File check = new File("checkFile.txt"); + check.delete(); + assertFalse(FileIO.checkFile(check.getName())); + FileIO.toFile(check, "check".getBytes()); + assertTrue(FileIO.checkFile(check.getName())); + check.delete(); + } + @Test public void testGetServiceList() throws IOException { diff --git a/src/test/java/org/myrobotlab/service/ArduinoChaosTest.java b/src/test/java/org/myrobotlab/service/ArduinoChaosTest.java index 32089148d8..2592ab4165 100644 --- a/src/test/java/org/myrobotlab/service/ArduinoChaosTest.java +++ b/src/test/java/org/myrobotlab/service/ArduinoChaosTest.java @@ -9,7 +9,6 @@ import org.myrobotlab.arduino.ArduinoUtils; import org.myrobotlab.framework.MrlException; -import com.pi4j.jni.Serial; @Ignore public class ArduinoChaosTest { @@ -123,9 +122,9 @@ public void testArduino() throws IOException, MrlException, InterruptedException // arduino.setDigitalTriggerOnly(false); Thread.sleep(1000); - arduino.setSerialRate(Serial.BAUD_RATE_57600); + arduino.setSerialRate(Serial.BAUD_115200); Thread.sleep(1000); - arduino.setSerialRate(Serial.BAUD_RATE_115200); + arduino.setSerialRate(Serial.BAUD_115200); Thread.sleep(1000); arduino.getBoardInfo(); diff --git a/src/test/java/org/myrobotlab/service/ArduinoTest.java b/src/test/java/org/myrobotlab/service/ArduinoTest.java index 97aa6974c3..5dff17f399 100644 --- a/src/test/java/org/myrobotlab/service/ArduinoTest.java +++ b/src/test/java/org/myrobotlab/service/ArduinoTest.java @@ -213,8 +213,12 @@ public final void pinArrayTest() { arduino01.enablePin(10); arduino01.enablePin(12); arduino01.enablePin(13); - sleep(100); + + // Wait for pin enablement to complete + sleep(200); arduino01.reset(); + // Wait for reset to complete + sleep(100); assertTrue("did not get pin array data D10", catcher.containsPinArrayFromPin(arduino01.getPin(10).getPinName())); assertTrue("did not get pin array data D12", catcher.containsPinArrayFromPin(arduino01.getPin(12).getPinName())); @@ -387,7 +391,9 @@ public final void testServo() throws Exception { // detach servo.detach(); - assertNull("detach did not remove controller", servo.getController()); + // assertNull("detach did not remove controller", servo.getController()); + // does not need to be null + assertTrue(!servo.isAttached()); // assertEquals("servoDetach/7/0\n", uart.decode()); arduino01.attach(servo); diff --git a/src/test/java/org/myrobotlab/service/HarryTest.java b/src/test/java/org/myrobotlab/service/HarryTest.java index d242e1e672..29527a0845 100755 --- a/src/test/java/org/myrobotlab/service/HarryTest.java +++ b/src/test/java/org/myrobotlab/service/HarryTest.java @@ -62,7 +62,7 @@ private void goLearnStuff(Solr solr, ProgramAB harry) throws InterruptedExceptio String rssUrl = "http://feeds.reuters.com/reuters/scienceNews"; RSSConnector rss = (RSSConnector) Runtime.start("rss", "RSSConnector"); rss.setRssUrl(rssUrl); - rss.addDocumentListener(solr); + rss.attachDocumentListener(solr.getName()); Thread.sleep(1000); @@ -224,7 +224,7 @@ public void testHarry() throws Exception { InMoov2 i01 = (InMoov2) Runtime.createAndStart("i01", "InMoov2"); i01.setMute(true); - i01.startAll(); + // i01.startAll(); // if startInMoov: // i01.startAll(leftPort, rightPort) // else: diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java index 3c02ea320c..450ffa04da 100644 --- a/src/test/java/org/myrobotlab/service/RandomTest.java +++ b/src/test/java/org/myrobotlab/service/RandomTest.java @@ -66,14 +66,14 @@ public void testService() throws Exception { // disable all random.disable(); - clock.setInterval(999999); sleep(200); + clock.setInterval(999999); assertTrue("clock should not be started", !clock.isClockRunning()); assertEquals(999999, (long)clock.getInterval()); // re-enable all that were previously enabled but not explicitly disabled ones random.enable(); - sleep(200); + sleep(1000); assertTrue("clock should not be started", !clock.isClockRunning()); assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval()); assertTrue(String.format("random method 3 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000); diff --git a/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java b/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java index 8d41ceaea3..07e1775110 100644 --- a/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java +++ b/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java @@ -1,6 +1,7 @@ package org.myrobotlab.service; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; @@ -30,14 +31,14 @@ public boolean contains(ByteArrayOutputStream out, String str) { @Test public void cliTest() throws Exception { - // from ,to null=runtime, data + // from ,to null=runtime, data String cwd = null; - Message msg = CodecUtils.cliToMsg(cwd, getName(), null, "ls"); + Message msg = CodecUtils.pathToMsg(getName(), "ls"); assertEquals("runtime", msg.getName()); assertEquals("ls", msg.method); assertEquals(getName(), msg.getSrcName()); - msg = CodecUtils.cliToMsg(null, getName() + "@someWhere", null, "ls"); + msg = CodecUtils.pathToMsg(getName() + "@someWhere", "ls"); assertEquals(getName(), msg.getSrcName()); assertEquals("someWhere", msg.getSrcId()); assertEquals(getName() + "@someWhere", msg.getSrcFullName()); @@ -46,21 +47,24 @@ public void cliTest() throws Exception { // Message msg = CodecUtils.cliToMsg(null, getName(), null, "/ls /runtime"); // FAILS - msg = CodecUtils.cliToMsg(cwd, getName() + "@someWhere", "blah@far", "ls"); - assertEquals("blah", msg.getName()); - assertEquals("blah@far", msg.getFullName()); - assertEquals("far", msg.getId()); + msg = CodecUtils.pathToMsg(getName() + "@someWhere", "ls"); + assertEquals("runtime", msg.getName()); + assertEquals("runtime", msg.getFullName()); + assertEquals(getName() + "@someWhere", msg.sender); + assertNull(msg.getId()); + assertEquals(0, msg.data.length); - cwd = "/"; - msg = CodecUtils.cliToMsg(cwd, getName(), null, "ls"); + cwd = "/runtime/"; + msg = CodecUtils.pathToMsg(getName(), cwd + "ls"); assertEquals("runtime", msg.getName()); assertEquals("ls", msg.method); assertEquals(getName(), msg.getSrcName()); + assertNull(msg.data); - cwd = "/blah"; - msg = CodecUtils.cliToMsg(cwd, getName(), null, "method"); + cwd = "/runtime/blahmethod"; + msg = CodecUtils.pathToMsg(getName(), cwd); assertEquals("runtime", msg.getName()); - assertEquals("ls", msg.method); + assertEquals("blahmethod", msg.method); assertEquals(getName(), msg.getSrcName()); // make sure runtime is running diff --git a/src/test/java/org/myrobotlab/service/RuntimeTest.java b/src/test/java/org/myrobotlab/service/RuntimeTest.java index b186b8bb0d..50c6c03268 100644 --- a/src/test/java/org/myrobotlab/service/RuntimeTest.java +++ b/src/test/java/org/myrobotlab/service/RuntimeTest.java @@ -1,5 +1,6 @@ package org.myrobotlab.service; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.Date; @@ -9,10 +10,13 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.myrobotlab.framework.DescribeQuery; +import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Registration; import org.myrobotlab.framework.interfaces.ServiceInterface; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.data.Locale; +import org.myrobotlab.service.interfaces.Gateway; import org.myrobotlab.test.AbstractTest; import org.slf4j.Logger; @@ -49,21 +53,19 @@ public void testGetLocalHardwareAddresses() { List addresses = Runtime.getLocalHardwareAddresses(); Assert.assertNotNull(addresses); } - + @Test public void registerRemoteService() { - + Registration registration = new Registration("remoteId", "clock", "Clock"); Runtime.register(registration); - - Clock clock = (Clock)Runtime.getService("clock@remoteId"); + + Clock clock = (Clock) Runtime.getService("clock@remoteId"); Assert.assertNotNull(clock); - + // cleanup Runtime.release("clock@remoteId"); } - - @Test public void testGetLocalServices() { @@ -94,12 +96,22 @@ public void testRuntimeLocale() { Runtime runtime = Runtime.getInstance(); runtime.setLocale("fr-FR"); - assertTrue("expecting concat fr-FR", runtime.getLocale().getTag().equals("fr-FR")); + assertEquals("expecting concat fr-FR", "fr-FR", runtime.getLocale().getTag()); - assertTrue(runtime.getLanguage().equals("fr")); + assertEquals("fr", runtime.getLanguage()); Locale l = runtime.getLocale(); - assertTrue(l.toString().equals("fr-FR")); + assertEquals("fr-FR", l.toString()); } -} \ No newline at end of file + @Test + public void testGetDescribeMessage() { + Message msg = Runtime.get().getDescribeMsg("testUUID"); + assertEquals("Incorrect method", "describe", msg.method); + assertEquals("Incorrect data length", 2, msg.data.length); + assertEquals("Incorrect UUID for describe message", Gateway.FILL_UUID_MAGIC_VAL, msg.data[0]); + assertTrue("Incorrect message second parameter type", DescribeQuery.class.isAssignableFrom(msg.data[1].getClass())); + assertEquals("Incorrect UUID in describe query", "testUUID", ((DescribeQuery) msg.data[1]).uuid); + } + +} diff --git a/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java b/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java index 1cb9b3465d..440ebeb59e 100644 --- a/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java +++ b/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java @@ -76,7 +76,7 @@ private boolean serviceInterfaceTest(String service) throws IOException { Assert.assertNotNull(foo.getDescription()); Assert.assertNotNull(foo.getName()); Assert.assertNotNull(foo.getSimpleName()); - Assert.assertNotNull(foo.getType()); + Assert.assertNotNull(foo.getTypeKey()); // TODO: add a bunch more tests here! foo.startService(); diff --git a/src/test/java/org/myrobotlab/service/ServiceSmokeTest.java b/src/test/java/org/myrobotlab/service/ServiceSmokeTest.java index 47fcf340cb..13d1529570 100755 --- a/src/test/java/org/myrobotlab/service/ServiceSmokeTest.java +++ b/src/test/java/org/myrobotlab/service/ServiceSmokeTest.java @@ -110,7 +110,7 @@ public void testSerialization(ServiceInterface s) { // TODO: perhaps some extra service type specific initialization?! String res = CodecUtils.toJson(s); assertNotNull(res); - log.info("Serialization successful for {}", s.getType()); + log.info("Serialization successful for {}", s.getTypeKey()); // ServiceInterface s = CodecUtils.fromJson(res, clazz) // assertNotNull(res); diff --git a/src/test/java/org/myrobotlab/service/ServoTest.java b/src/test/java/org/myrobotlab/service/ServoTest.java index b81aabf704..74bb958eda 100644 --- a/src/test/java/org/myrobotlab/service/ServoTest.java +++ b/src/test/java/org/myrobotlab/service/ServoTest.java @@ -5,6 +5,9 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; @@ -53,6 +56,19 @@ static public void afterClass() throws Exception { servo.releaseService(); arduino01.releaseService(); } + + @Test + public void autoDisableAfterAttach() { + // enable servo + servo.moveTo(100); + servo.detach(); + assertTrue(!servo.isAttached()); + servo.setAutoDisable(true); + servo.attach("arduinoServoTest"); + // after attach, must be disabled + sleep(servo.getIdleTimeout() + 1000); + assertTrue(!servo.isEnabled()); + } @Test public void disabledMove() throws Exception { @@ -173,7 +189,9 @@ public void testServo() throws Exception { // to come to 'eventual' synchronized consistency Service.sleep(300); - s.attach(arduino01, 10, 1.0); + s.setPin(10); + s.setPosition(1); + s.attach(arduino01); Service.sleep(300); s.enable(); assertTrue(s.isEnabled()); @@ -273,10 +291,43 @@ public void moveToBlockingTest() throws Exception { // log.info("Move to blocking took {} milliseconds", delta); assertTrue("Servo should be enabled", servo01.isEnabled()); assertFalse("Servo should not be moving now.", servo01.isMoving()); - // Now let's wait for the idle disable timer to kick off + 1000ms - // TODO: figure out why smaller values like 100ms cause this test to fail. - // there seems to be some lag - Thread.sleep(servo01.getIdleTimeout() + 1000); + + CountDownLatch moveLatch = new CountDownLatch(1); + CountDownLatch targetLatch = new CountDownLatch(1); + CountDownLatch disableLatch = new CountDownLatch(1); + + start = System.currentTimeMillis(); + + new Thread(() -> { + log.info("starting at {}", System.currentTimeMillis()); + servo01.moveTo(0); + moveLatch.countDown(); + }).start(); + + // wait for the move to start + moveLatch.await(); + + // wait for the move to complete using waitTargetPos + new Thread(() -> { + servo01.waitTargetPos(); + targetLatch.countDown(); + }).start(); + + // wait for the move to complete + targetLatch.await(); + log.info("finished at {}", System.currentTimeMillis()); + + delta = System.currentTimeMillis() - start; + assertTrue("Move to blocking should have taken 3 seconds or more. Time was " + delta, delta >= 3000); + + log.info("Move to blocking took {} milliseconds", delta); + assertTrue("Servo should be enabled", servo01.isEnabled()); + + // wait for the servo to stop moving + disableLatch.await(servo01.getIdleTimeout() + 1000, TimeUnit.MILLISECONDS); + assertFalse("Servo should not be moving now.", servo01.isMoving()); + + // verify disabled after autoDisable time assertFalse("Servo should be disabled.", servo01.isEnabled()); } @@ -291,7 +342,12 @@ public void testHelperMethods() throws Exception { // 60 degrees per second.. move from 0 to 180 in 3 seconds servo01.setSpeed(60.0); servo01.setPin(7); - servo01.attach("arduino01", 8, 1.0, 360.0); + servo01.setPin(8); + servo01.setSpeed(1.0); + servo01.detach(); + servo01.setController("blah"); + assertEquals("blah", servo01.getController()); + servo01.attach("arduino01"); assertEquals("arduino01", servo01.getController()); assertEquals(Integer.valueOf(8).toString(), servo01.getPin()); diff --git a/src/test/java/org/myrobotlab/service/SolrTest.java b/src/test/java/org/myrobotlab/service/SolrTest.java index dc8b1789e3..f236615198 100755 --- a/src/test/java/org/myrobotlab/service/SolrTest.java +++ b/src/test/java/org/myrobotlab/service/SolrTest.java @@ -5,9 +5,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import org.apache.commons.lang.StringUtils; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.QueryResponse; @@ -15,9 +13,11 @@ import org.apache.solr.common.SolrInputDocument; import org.bytedeco.opencv.opencv_core.IplImage; import org.junit.Assert; +import org.junit.Ignore; import org.myrobotlab.document.Document; import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Service; +import org.myrobotlab.image.Util; import org.myrobotlab.programab.Response; // @Ignore @@ -49,7 +49,7 @@ private SolrInputDocument makeImageDoc(Solr solr, String docId, IplImage image) SolrInputDocument doc = new SolrInputDocument(); doc.setField("id", docId); // load an image from file/resource - byte[] bytes = solr.imageToBytes(image); + byte[] bytes = Util.imageToBytes(image); doc.setField("bytes", bytes); return doc; } diff --git a/src/test/java/org/myrobotlab/service/WebGuiTest.java b/src/test/java/org/myrobotlab/service/WebGuiTest.java index ecd159f7eb..53a76f85cc 100644 --- a/src/test/java/org/myrobotlab/service/WebGuiTest.java +++ b/src/test/java/org/myrobotlab/service/WebGuiTest.java @@ -4,18 +4,14 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.List; import org.junit.Before; import org.junit.Test; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.MRLListener; -import org.atmosphere.cpr.AtmosphereResource; -import org.atmosphere.cpr.AtmosphereResourceImpl; -import org.junit.Before; -import org.junit.Test; -import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.StaticType; import org.myrobotlab.framework.TimeoutException; @@ -54,9 +50,9 @@ public void getTest() { } @Test - public void getTestWithParameter() { + public void getTestWithParameter() throws UnsupportedEncodingException { - byte[] bytes = Http.get("http://localhost:8889/api/service/runtime/isLocal/runtime"); + byte[] bytes = Http.get("http://localhost:8889/api/service/runtime/isLocal/%22runtime%22"); assertNotNull(bytes); String ret = new String(bytes); assertTrue(ret.contains("true")); @@ -86,6 +82,19 @@ public void postTest() { assertTrue(ret.contains("@")); + // second post - simple input (including array of strings) - complex return + // FIXME uncomment when ready - callbacks are not possible through the rest api + // org.myrobotlab.framework.TimeoutException: timeout of 3000 for proxyName@remoteId.toString exceeded + // org.myrobotlab.framework.TimeoutException: timeout of 3000 for proxyName@remoteId.getFullName exceeded +// postBody = "[\"remoteId\", \"proxyName\", \"py:myService\",[\"org.myrobotlab.framework.interfaces.ServiceInterface\"]]"; +// bytes = Http.post("http://localhost:8889/api/service/runtime/register", postBody); +// sleep(200); +// assertNotNull(bytes); +// ret = new String(bytes); +// assertTrue(ret.contains("remoteId")); + + + // post non primitive non string object MRLListener listener = new MRLListener("getRegistry", "runtime@webguittest", "onRegistry"); postBody = "[" + CodecUtils.toJson(listener) + "]"; @@ -137,14 +146,6 @@ public void urlEncodingTest() { assertEquals("true", ret); } - @Test - public void noQuotesTest() { - //exec(print) - byte[] bytes = Http.get("http://localhost:8889/api/service/pythonApiTest/exec/print"); - String ret = new String(bytes); - assertEquals("true", ret); - } - @Test public void sendBlockingTest() throws InterruptedException, TimeoutException { String retVal = "retVal"; diff --git a/src/test/java/org/myrobotlab/service/data/LocaleTest.java b/src/test/java/org/myrobotlab/service/data/LocaleTest.java index 62558fa29b..c76536be58 100644 --- a/src/test/java/org/myrobotlab/service/data/LocaleTest.java +++ b/src/test/java/org/myrobotlab/service/data/LocaleTest.java @@ -1,8 +1,7 @@ package org.myrobotlab.service.data; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import org.junit.Test; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.test.AbstractTest; import java.util.HashMap; @@ -128,20 +127,18 @@ public void testJavaLocale() { // java.util.Locale.setDefault(t); java.util.Locale[] locales = java.util.Locale.getAvailableLocales(); - String[] isoLanguages = java.util.Locale.getISOLanguages(); - String[] isoCountries = java.util.Locale.getISOCountries(); // is java.util.Locale serializable - Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").setPrettyPrinting().disableHtmlEscaping().create(); - String json = gson.toJson(locale); - java.util.Locale l = gson.fromJson(json, java.util.Locale.class); + String json = CodecUtils.toJson(locale); + + java.util.Locale l = CodecUtils.fromJson(json, java.util.Locale.class); java.util.Locale br = new java.util.Locale("en", "BR", "variant"); log.info("br getLanguage - {}", br.getLanguage()); log.info("br getDisplayLanguage - {}", br.getDisplayLanguage()); br.toLanguageTag(); - json = gson.toJson(br); - l = gson.fromJson(json, java.util.Locale.class); + json = CodecUtils.toJson(br); + l = CodecUtils.fromJson(json, java.util.Locale.class); br.equals(l); l = new java.util.Locale("en_BR_variant"); diff --git a/src/test/java/org/myrobotlab/service/interfaces/AbstractConnectorTest.java b/src/test/java/org/myrobotlab/service/interfaces/AbstractConnectorTest.java index db2f1b6ebb..ace69e8e18 100644 --- a/src/test/java/org/myrobotlab/service/interfaces/AbstractConnectorTest.java +++ b/src/test/java/org/myrobotlab/service/interfaces/AbstractConnectorTest.java @@ -23,7 +23,7 @@ public void test() { MockDocumentListener listener = createListener(); connector.startService(); listener.startService(); - connector.addDocumentListener(listener); + connector.attachDocumentListener(listener.getName()); connector.startCrawling(); // flush any current batch and wait until the outbox is empty. connector.flush(); diff --git a/src/test/java/org/myrobotlab/service/interfaces/MockDocumentListener.java b/src/test/java/org/myrobotlab/service/interfaces/MockDocumentListener.java index d28b90bdd8..b00aa62fc0 100644 --- a/src/test/java/org/myrobotlab/service/interfaces/MockDocumentListener.java +++ b/src/test/java/org/myrobotlab/service/interfaces/MockDocumentListener.java @@ -5,8 +5,9 @@ import org.myrobotlab.document.Document; import org.myrobotlab.document.ProcessingStatus; import org.myrobotlab.framework.Service; +import org.myrobotlab.service.config.ServiceConfig; -public class MockDocumentListener extends Service implements DocumentListener { +public class MockDocumentListener extends Service implements DocumentListener { private static final long serialVersionUID = 1L; private int count = 0; diff --git a/src/test/java/org/myrobotlab/service/interfaces/SpeechSynthesisTest.java b/src/test/java/org/myrobotlab/service/interfaces/SpeechSynthesisTest.java index 68ef3147af..034b5c8d23 100644 --- a/src/test/java/org/myrobotlab/service/interfaces/SpeechSynthesisTest.java +++ b/src/test/java/org/myrobotlab/service/interfaces/SpeechSynthesisTest.java @@ -7,6 +7,7 @@ import org.junit.Ignore; import org.junit.Test; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.repo.ServiceData; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.Runtime; @@ -14,9 +15,6 @@ import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis.Voice; import org.slf4j.Logger; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - // TODO: this unit test is so loud! we need a way to run it in mute mode // also it's long based on the length of the audio being generated/played. // TODO: find a way to validate that the mp3s are created, but don't actually play them. (checksum? file length? other approach?) @@ -49,20 +47,17 @@ public static void main(String[] args) { // Locale.setDefault(t); Locale[] locales = Locale.getAvailableLocales(); - String[] isoLanguages = Locale.getISOLanguages(); - String[] isoCountries = Locale.getISOCountries(); // is Locale serializable - Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").setPrettyPrinting().disableHtmlEscaping().create(); - String json = gson.toJson(locale); - Locale l = gson.fromJson(json, Locale.class); + String json = CodecUtils.toJson(locale); + Locale l = CodecUtils.fromJson(json, Locale.class); Locale br = new Locale("en", "BR", "variant"); log.info("br getLanguage - {}", br.getLanguage()); log.info("br getDisplayLanguage - {}", br.getDisplayLanguage()); br.toLanguageTag(); - json = gson.toJson(br); - l = gson.fromJson(json, Locale.class); + json = CodecUtils.toJson(br); + l = CodecUtils.fromJson(json, Locale.class); br.equals(l); l = new Locale("en_BR_variant"); diff --git a/src/test/java/org/myrobotlab/string/StringUtilTest.java b/src/test/java/org/myrobotlab/string/StringUtilTest.java index f75b693330..35403af57c 100644 --- a/src/test/java/org/myrobotlab/string/StringUtilTest.java +++ b/src/test/java/org/myrobotlab/string/StringUtilTest.java @@ -34,4 +34,10 @@ public void testChunkString() { assertEquals(text, StringUtils.join(result, " ")); } + + @Test + public void testRemoveEnd() { + String testStr = "python@test_runtime"; + assertEquals("python", StringUtil.removeEnd(testStr, "@test_runtime")); + } } diff --git a/src/test/resources/InMoov/Harry.py b/src/test/resources/InMoov/Harry.py index 000c76c864..091cca224f 100644 --- a/src/test/resources/InMoov/Harry.py +++ b/src/test/resources/InMoov/Harry.py @@ -73,7 +73,9 @@ def heard(data): i01 = runtime.start("i01", "InMoov2") i01.setMute(True) if startInMoov: - i01.startAll(leftPort, rightPort) + # use config to start + # i01.startAll(leftPort, rightPort) + pass else: i01.mouth = mouth diff --git a/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml b/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml index f23f2f513c..257d55e668 100644 --- a/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml +++ b/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml @@ -3,10 +3,10 @@ * -