From 659c0ff0355a1804724868125482bf69fb00754a Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 17 Sep 2018 21:14:56 -0400 Subject: [PATCH 001/110] some minor fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modifications: server.js - updated to reference read and write characteristics of vehicles by array slot rather than search for them. Some Node versions don’t like the lambda search. build.gradle - updated to add a gradle task to start server. ./gradlew ndm_start wouldn’t keep the server alive. Now, ./gradle server will. README.md - updated to reflect new usage of NodeJS server bluetooth gateway. Also added main class AnkiConnectionTest.java to try out the API. AnkiConnectionTest.java - added to illustrate usage and step-by-step instructions to get some example communication going with a vehicle. AnkiConnector.java - commented-out if-statement in void connect(Vehicle) method responsible for error-handling. This caused the connection with the bluetooth gateway to fail because the bluetooth gateway sometimes likes to timeout upon first connection. Removing this should fix that (at the expense of error handling, at the moment). Model.java - updated for new ANKI Supertrucks “Freewheel” and “X52Ice.” Added placeholder for currently known, but untested models (i.e. X52 is probably 0x10 and 0x12 exists, but no clue what it is). --- .gitignore | 19 +-- README.md | 8 +- build.gradle | 7 + .../java/de/adesso/anki/AnkiConnector.java | 6 +- src/main/java/de/adesso/anki/Model.java | 18 ++- .../cs/CPSLab/anki/AnkiConnectionTest.java | 125 ++++++++++++++++++ src/main/nodejs/server.js | 15 ++- 7 files changed, 177 insertions(+), 21 deletions(-) create mode 100644 src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java diff --git a/.gitignore b/.gitignore index 6915dd01..7055195d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ -# anki-connector -node_modules/** -.gradle/** -build/** -bin/** -.classpath -.project -.settings/** -.idea/ +# anki-connector +node_modules/** +.gradle/** +build/** +bin/** +.classpath +.project +.settings/** +.idea/ +/.nb-gradle/ \ No newline at end of file diff --git a/README.md b/README.md index ca4fe0b5..5f61a56c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ cd anki-drive-java Start the Node.js gateway service: ``` -./gradlew npm_run +./gradlew server ``` ### Add the Java library @@ -73,6 +73,12 @@ Start scanning for vehicles: List vehicles = anki.findVehicles(); ``` +### Test File +To try a connection, start the server and run the class: +```java +edu.oswego.cs.CPSLab.anki.AnkiConnectionTest +``` + ## Contributing Contributions are always welcome! Feel free to fork this repository and submit diff --git a/build.gradle b/build.gradle index 211ef54e..ab822c3a 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,9 @@ buildscript { } apply plugin: 'java' +apply plugin: 'application' apply plugin: 'com.moowork.node' +mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' repositories { jcenter() @@ -35,4 +37,9 @@ task fatJar(type: Jar) { with jar } +task server(type: NodeTask) { + script = file('src/main/nodejs/server.js') + ignoreExitValue = true +} + processResources.dependsOn(['npmInstall']) diff --git a/src/main/java/de/adesso/anki/AnkiConnector.java b/src/main/java/de/adesso/anki/AnkiConnector.java index ad9217b6..7ac56fb1 100644 --- a/src/main/java/de/adesso/anki/AnkiConnector.java +++ b/src/main/java/de/adesso/anki/AnkiConnector.java @@ -76,11 +76,13 @@ public synchronized List findVehicles() { synchronized void connect(Vehicle vehicle) throws InterruptedException { writer.println("CONNECT;"+vehicle.getAddress()); String response = reader.waitFor("CONNECT;"); - + +/* commented out because it caused connections to fail every other call if (response.equals("CONNECT;ERROR")) { throw new RuntimeException("connect failed"); } - + */ + NotificationListener carsNotificationListener = (line) -> { if (line.startsWith(vehicle.getAddress())) { String messageString = line.replaceFirst(vehicle.getAddress()+";", ""); diff --git a/src/main/java/de/adesso/anki/Model.java b/src/main/java/de/adesso/anki/Model.java index acdba004..b7bd75aa 100644 --- a/src/main/java/de/adesso/anki/Model.java +++ b/src/main/java/de/adesso/anki/Model.java @@ -4,9 +4,17 @@ import java.util.Map; /** - * Enumerates all currently known Anki vehicle models. - * + * Enumerates some currently known Anki vehicle models. + * Known vehicles currently available, but not considered in this class: + * Supercars: + * - NUKE Phantom + * - Fast & Furious Dom's Charger + * - Fast & Furious Hobbs' MXT + * Supertrucks: + * - X52 (probably ID 0x10). + * * @author Yannick Eckey + * @author B. Tenbergen */ public enum Model { KOURAI(0x01), @@ -21,7 +29,11 @@ public enum Model { THERMO(0x0a, "#a11c20"), NUKE(0x0b, "#bed62f"), GUARDIAN(0x0d, "#42b1d7"), - BIGBANG(0x0e, "#4e674d"); + BIGBANG(0x0e, "#4e674d"), + FREEHWEEL(0x0f), //BT update on 9/14/18 to add new supertruck +// __SOMECAR1(0x10), //TODO: figure out which car this is (BT)... probably X52 + X52ICE(0x11); //BT update on 9/14/18 to add new supertruck +// __SOMECAR3(0x12); //TODO: figure out which car this is (BT) private int id; private String color = "#f00"; diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java new file mode 100644 index 00000000..d9b4ded3 --- /dev/null +++ b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java @@ -0,0 +1,125 @@ +package edu.oswego.cs.CPSLab.anki; + +import de.adesso.anki.AnkiConnector; +import de.adesso.anki.MessageListener; +import de.adesso.anki.Vehicle; +import de.adesso.anki.messages.BatteryLevelRequestMessage; +import de.adesso.anki.messages.BatteryLevelResponseMessage; +import de.adesso.anki.messages.LightsPatternMessage; +import de.adesso.anki.messages.LightsPatternMessage.LightConfig; +import de.adesso.anki.messages.PingRequestMessage; +import de.adesso.anki.messages.PingResponseMessage; +import de.adesso.anki.messages.SdkModeMessage; +import de.adesso.anki.messages.SetSpeedMessage; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +/** + * + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public class AnkiConnectionTest { + + static long pingReceivedAt; + static long pingSentAt; + + public static void main(String[] args) throws IOException, InterruptedException { + + System.out.println("Launching connector..."); + AnkiConnector anki = new AnkiConnector("localhost", 5000); + System.out.print("...looking for cars..."); + List vehicles = anki.findVehicles(); + + if (vehicles.isEmpty()) { + System.out.println(" NO CARS FOUND. I guess that means we're done."); + + } else { + System.out.println(" FOUND " + vehicles.size() + " CARS! They are:"); + + Iterator iter = vehicles.iterator(); + while (iter.hasNext()) { + Vehicle v = iter.next(); + System.out.println(" " + v); + System.out.println(" ID: " + v.getAdvertisement().getIdentifier()); + System.out.println(" Model: " + v.getAdvertisement().getModel()); + System.out.println(" Model ID: " + v.getAdvertisement().getModelId()); + System.out.println(" Product ID: " + v.getAdvertisement().getProductId()); + System.out.println(" Address: " + v.getAddress()); + System.out.println(" Color: " + v.getColor()); + System.out.println(" charging? " + v.getAdvertisement().isCharging()); + } + + System.out.println("\nNow connecting to and doing stuff to your cars.\n\n"); + + iter = vehicles.iterator(); + while (iter.hasNext()) { + Vehicle v = iter.next(); + System.out.println("\nConnecting to " + v + " @ " + v.getAddress()); + v.connect(); + System.out.print(" Connected. Setting SDK mode..."); //always set the SDK mode FIRST! + v.sendMessage(new SdkModeMessage()); + System.out.println(" SDK Mode set."); + + System.out.println(" Sending asynchronous Battery Level Request. The Response will come in eventually."); + //we have to set up a response handler first, in order to handle async responses + BatteryLevelResponseHandler blrh = new BatteryLevelResponseHandler(); + //now we tell the car, who is listenening to the replies + v.addMessageListener(BatteryLevelResponseMessage.class, blrh); + //now we can actually send it. + v.sendMessage(new BatteryLevelRequestMessage()); + + System.out.println(" Sending Ping Request..."); + //again, some async set-up required... + PingResponseHandler prh = new PingResponseHandler(); + v.addMessageListener(PingResponseMessage.class, prh); + AnkiConnectionTest.pingSentAt = System.currentTimeMillis(); + v.sendMessage(new PingRequestMessage()); + + System.out.println(" Flashing lights..."); + LightConfig lc = new LightConfig(LightsPatternMessage.LightChannel.TAIL, LightsPatternMessage.LightEffect.STROBE, 0, 0, 0); + LightsPatternMessage lpm = new LightsPatternMessage(); + lpm.add(lc); + v.sendMessage(lpm); + System.out.println(" Setting Speed..."); + v.sendMessage(new SetSpeedMessage(200, 75)); + //Thread.sleep(1000); + //gs.sendMessage(new TurnMessage()); + System.out.print("Sleeping for 10secs... "); + Thread.sleep(10000); + v.disconnect(); + System.out.println("disconnected from " + v + "\n"); + } + } + anki.close(); + System.exit(0); + } + + /** + * Handles the response from the vehicle from the BatteryLevelRequestMessage. + * We need handler classes because responses from the vehicles are asynchronous. + */ + private static class BatteryLevelResponseHandler implements MessageListener { + @Override + public void messageReceived(BatteryLevelResponseMessage m) { + System.out.println(" Battery Level is: " + m.getBatteryLevel() + " mV"); + } + } + + /** + * Handles the response from the vehicle from the PingRequestMessage. + * We need handler classes because responses from the vehicles are asynchronous. + */ + private static class PingResponseHandler implements MessageListener { + @Override + public void messageReceived(PingResponseMessage m) { + AnkiConnectionTest.pingReceivedAt = System.currentTimeMillis(); + System.out.println(" Ping response received. Roundtrip: " + (AnkiConnectionTest.pingReceivedAt - AnkiConnectionTest.pingSentAt) + " msec."); + } + } +} diff --git a/src/main/nodejs/server.js b/src/main/nodejs/server.js index 8eed257b..0d25d6b8 100644 --- a/src/main/nodejs/server.js +++ b/src/main/nodejs/server.js @@ -6,7 +6,7 @@ var server = net.createServer(function(client) { client.vehicles = []; client.on("error", (err) => { - console.log("connection error"); // client disconnected? + console.log("connection error (client disconnected?)"); // client disconnected? client.vehicles.forEach((vehicle) => vehicle.disconnect()); }); client.on("data", function(data) { @@ -59,9 +59,12 @@ var server = net.createServer(function(client) { vehicle.discoverSomeServicesAndCharacteristics( ["be15beef6186407e83810bd89c4d8df4"], ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"], - function(error, services, characteristics) { - vehicle.reader = characteristics.find(x => !x.properties.includes("write")); - vehicle.writer = characteristics.find(x => x.properties.includes("write")); + function(error, services, characteristics) { + // console.log("!!!!!!!!!!!!!!!!!"); + // console.log(characteristics); + // console.log("!!!!!!!!!!!!!!!!!"); + vehicle.reader = characteristics[1];//.find(x => !x.properties.includes("write")); + vehicle.writer = characteristics[0];//.find(x => x.properties.includes("write")); vehicle.reader.notify(true); vehicle.reader.on('read', function(data, isNotification) { @@ -79,7 +82,7 @@ var server = net.createServer(function(client) { setTimeout(() => { if (!success) { client.write("CONNECT;ERROR\n"); - console.log("connect error"); + console.log("connect error (timeout)"); } }, 500); @@ -113,4 +116,4 @@ var server = net.createServer(function(client) { server.listen(5000); -console.log("Server gestartet") +console.log("Server started") From 97a1d8053c351c23e723eef49a47e585815e648d Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Tue, 18 Sep 2018 11:03:12 -0400 Subject: [PATCH 002/110] fixed failing build (hopefully) Travis build fails because Node.js 4 and 5 support are dropped from later Node.js versions, but Travis CI uses current Node.js version. This should fix it. Tested locally and runs, but that of course means nothing. --- build.gradle | 6 +- package-lock.json | 992 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- 3 files changed, 997 insertions(+), 5 deletions(-) create mode 100644 package-lock.json diff --git a/build.gradle b/build.gradle index ab822c3a..66f0de9c 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'com.moowork.gradle:gradle-node-plugin:0.13' + classpath 'com.moowork.gradle:gradle-node-plugin:1.1.0' //was 0.13 } } @@ -22,8 +22,8 @@ dependencies { } node { - version = '4.4.7' - npmVersion = '3.10.5' + version = '8.12.0'//'4.4.7' + npmVersion = '6.4.1'//'3.10.5' distBaseUrl = 'https://nodejs.org/dist' download = true diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..06453abd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,992 @@ +{ + "name": "anki-connector", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bluetooth-hci-socket": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.1.tgz", + "integrity": "sha1-774hUk/Bz10/rl1RNl1WHUq77Qs=", + "optional": true, + "requires": { + "debug": "^2.2.0", + "nan": "^2.0.5", + "usb": "^1.1.0" + } + }, + "bplist-parser": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.0.6.tgz", + "integrity": "sha1-ONo0cYF9+dRKs4kuJ3B7u9daEbk=", + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=" + }, + "callback-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz", + "integrity": "sha1-RwGlEmbwbgbqpx/BcjOCLYdfSQg=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "> 1.0.0 < 3.0.0" + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "commist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.0.0.tgz", + "integrity": "sha1-wMNSUBz29S6RJOPvicmAbiAi6+8=", + "requires": { + "leven": "^1.0.0", + "minimist": "^1.1.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "^0.10.9" + } + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "optional": true + }, + "duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha1-WSkD9dgLONA3IgVBJk1poZj7NBA=", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", + "requires": { + "once": "^1.4.0" + } + }, + "es5-ext": { + "version": "0.10.46", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", + "integrity": "sha1-79mfZ8Wn7Hibqj2qf3mHA4j39XI=", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha1-OWCDLT8VdBCDQtr9OmezMsCWnfE=", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true + }, + "help-me": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", + "integrity": "sha1-jy1QjQYAtKRW2i8IZVbn5cBWo8Y=", + "requires": { + "callback-stream": "^1.0.2", + "glob-stream": "^6.1.0", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "optional": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha1-OV4a6EsR8mrReV5zwXN45IowFXY=", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha1-obtpNc6MXboei5dUubLcwCDiJg0=", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha1-1zHoiY7QkKEsNSrS6u1Qla0yLJ0=", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "leven": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/leven/-/leven-1.0.2.tgz", + "integrity": "sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minipass": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", + "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mqtt": { + "version": "2.18.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.8.tgz", + "integrity": "sha1-nSE8yrkhUazPsh7owIYNxoZqslk=", + "requires": { + "commist": "^1.0.0", + "concat-stream": "^1.6.2", + "end-of-stream": "^1.4.1", + "es6-map": "^0.1.5", + "help-me": "^1.0.1", + "inherits": "^2.0.3", + "minimist": "^1.2.0", + "mqtt-packet": "^5.6.0", + "pump": "^3.0.0", + "readable-stream": "^2.3.6", + "reinterval": "^1.1.0", + "split2": "^2.1.1", + "websocket-stream": "^5.1.2", + "xtend": "^4.0.1" + } + }, + "mqtt-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-5.6.0.tgz", + "integrity": "sha1-kj+3BNDOC9asgcfhzAlGmxUS0v0=", + "requires": { + "bl": "^1.2.1", + "inherits": "^2.0.3", + "process-nextick-args": "^2.0.0", + "safe-buffer": "^5.1.0" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + }, + "nan": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", + "integrity": "sha1-V042Dk2VSrFpZuwQLAwEn9lhoJk=", + "optional": true + }, + "needle": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.3.tgz", + "integrity": "sha512-GPL22d/U9cai87FcCPO6e+MT3vyHS2j+zwotakDc7kE2DtUAqFKMXLJCTtRp+5S75vXIwQPvIxkvlctxf9q4gQ==", + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "noble": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/noble/-/noble-1.9.1.tgz", + "integrity": "sha1-LM0x6tjsktv/bxmkLkILJYvNzdA=", + "requires": { + "bluetooth-hci-socket": "^0.5.1", + "bplist-parser": "0.0.6", + "debug": "~2.2.0", + "xpc-connection": "~0.1.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", + "optional": true + }, + "npm-packlist": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", + "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "requires": { + "readable-stream": "^2.0.1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "optional": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha1-tKIRaBW94vTh6mAjVOjHVWUQemQ=", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha1-NlE74karJ1cLGjdKXOJ4v9dDcM4=", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha1-Ejma3W5M91Jtlzy8i1zi4pCLOQk=", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "optional": true + }, + "semver": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "optional": true + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha1-GGsldbz4PoW30YRldWI47k7kJJM=", + "requires": { + "through2": "^2.0.2" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "optional": true + }, + "tar": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", + "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.3", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + }, + "through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=" + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "requires": { + "json-stable-stringify": "^1.0.0", + "through2-filter": "^2.0.0" + } + }, + "usb": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/usb/-/usb-1.3.3.tgz", + "integrity": "sha512-WRBxI54yEs2QPj28G6kITI3Wu7VxrtHbqiDvDRUDKdg97lcK1pTP8y9LoDWF22OiCCrEvrdeq0lNcr84QOzjXQ==", + "optional": true, + "requires": { + "nan": "^2.8.0", + "node-pre-gyp": "^0.10.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "websocket-stream": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.1.2.tgz", + "integrity": "sha1-HDHGJ7zfNPGpvazJ2qFb+kgW2a0=", + "requires": { + "duplexify": "^3.5.1", + "inherits": "^2.0.1", + "readable-stream": "^2.3.3", + "safe-buffer": "^5.1.1", + "ws": "^3.2.0", + "xtend": "^4.0.0" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xpc-connection": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xpc-connection/-/xpc-connection-0.1.4.tgz", + "integrity": "sha1-3Nf6oq7Gt6bhjMXdrQQvejTHcVY=", + "optional": true, + "requires": { + "nan": "^2.0.5" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + } + } +} diff --git a/package.json b/package.json index 0d1579aa..0501459b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "author": "Yannick Eckey", "license": "UNLICENSED", "dependencies": { - "mqtt": "^2.0.1", - "noble": "^1.6.0" + "mqtt": "^2.18.8", + "noble": "^1.9.0" } } From dc5197ad83b8f8132cc61913f9243e9fb469a490 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Tue, 18 Sep 2018 11:36:45 -0400 Subject: [PATCH 003/110] Told Travis to install libudev-dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Travis CI builds on Linux, for which optional dependency node-usb1.3.3 isn’t available. Build will fail. Telling travis to install libudev-dev will hopefully fix this. --- .travis.yml | 4 ++++ README.md | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9bcf9994..9742d0f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +before_install: + - sudo apt-get update + - sudo apt-get install -y libudev-dev + language: java jdk: - oraclejdk8 diff --git a/README.md b/README.md index 5f61a56c..ed816dd3 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,18 @@ To build and use the SDK in your own project you will need: To install the SDK and all required dependencies run the following commands: ``` -git clone https://github.com/yeckey/anki-drive-java +git clone https://github.com/adessoAG/anki-drive-java cd anki-drive-java ./gradlew build ``` +### On Linux + +Optional Dependency node-usb will not be installed. Instead, install +``` +sudo apt-get install libudev-dev +``` + ## Usage Start the Node.js gateway service: From 6a90e94f49536e17d9f33c71ee98d731052a027b Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 08:16:25 -0400 Subject: [PATCH 004/110] trying to force libudev install before gradle building on linux trying to force libudev install before gradle building on linux. also some minor commenting things. --- README.md | 2 +- build.gradle | 10 ++++++++++ src/main/java/de/adesso/anki/Vehicle.java | 1 + .../edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java | 10 ++++------ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ed816dd3..dff99633 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ cd anki-drive-java ### On Linux -Optional Dependency node-usb will not be installed. Instead, install +Optional Dependency node-usb will not be installed. So, run: ``` sudo apt-get install libudev-dev ``` diff --git a/build.gradle b/build.gradle index 66f0de9c..d931ee05 100644 --- a/build.gradle +++ b/build.gradle @@ -42,4 +42,14 @@ task server(type: NodeTask) { ignoreExitValue = true } +task installLibUdev(type:Exec) { + commandLine 'sudo apt-get install libudev-dev' +} + +installLibUdev.onlyIf { org.gradle.nativeplatform.platform.OperatingSystem.isLinux() } + +build.doFirst { + installLibUdev +} + processResources.dependsOn(['npmInstall']) diff --git a/src/main/java/de/adesso/anki/Vehicle.java b/src/main/java/de/adesso/anki/Vehicle.java index 175d240b..9b56f773 100644 --- a/src/main/java/de/adesso/anki/Vehicle.java +++ b/src/main/java/de/adesso/anki/Vehicle.java @@ -40,6 +40,7 @@ public void setAdvertisement(AdvertisementData advertisement) { this.advertisement = advertisement; } + @Override public String toString() { return advertisement.toString(); } diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java index d9b4ded3..adfbdd78 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java @@ -15,13 +15,11 @@ import java.util.Iterator; import java.util.List; -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ /** - * + * A simple test program to test a connection to your Anki 'Supercars' and 'Supertrucks' using the NodeJS Bluetooth gateway. + * Simple follow the installation instructions at http://github.com/adessoAG/anki-drive-java, build this project, start the + * bluetooth gateway using ./gradlew server, and run this class. + * * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) */ public class AnkiConnectionTest { From e085781ad4ca64754c712fade9ec79fd33333374 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 08:22:01 -0400 Subject: [PATCH 005/110] trying to fix JitPack build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trying to force libudev install before gradle building on linux… just for JitPack --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index d931ee05..d2391706 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,7 @@ task server(type: NodeTask) { } task installLibUdev(type:Exec) { + println "OS is UNIX family. Installing libudev-dev..." commandLine 'sudo apt-get install libudev-dev' } From 657d00023d0c54914b71378a9b3db67d70c60f81 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 15:01:10 -0400 Subject: [PATCH 006/110] still fighting with jitpack to build this thing still fighting with jitpack to build this thing --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index d2391706..6af3393d 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ task server(type: NodeTask) { task installLibUdev(type:Exec) { println "OS is UNIX family. Installing libudev-dev..." commandLine 'sudo apt-get install libudev-dev' + standardOutput = new ByteArrayOutputStream() + ext.output = { return standardOutput.toString() } + ignoreExitValue = false } installLibUdev.onlyIf { org.gradle.nativeplatform.platform.OperatingSystem.isLinux() } From ba56ecd2c6b4162ee0cb5104a1d053f99c2f3452 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 15:13:30 -0400 Subject: [PATCH 007/110] still fighting with jitpack to build this thing still fighting with jitpack to build this thing --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6af3393d..9dcd2948 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ task server(type: NodeTask) { task installLibUdev(type:Exec) { println "OS is UNIX family. Installing libudev-dev..." - commandLine 'sudo apt-get install libudev-dev' + commandLine 'sudo', 'apt-get', 'install', 'libudev-dev' standardOutput = new ByteArrayOutputStream() ext.output = { return standardOutput.toString() } ignoreExitValue = false From c847f11745503469e6114f6c15444f4c70e3cdc7 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 15:52:59 -0400 Subject: [PATCH 008/110] apparently, jitpack allows adding yml configs... so I made one. Same as .travis.yml --- jitpack.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 jitpack.yml diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000..9742d0f9 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,7 @@ +before_install: + - sudo apt-get update + - sudo apt-get install -y libudev-dev + +language: java +jdk: + - oraclejdk8 From befd380d75e26dc6bb63ac5fbf3c5490357dff06 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 17:15:15 -0400 Subject: [PATCH 009/110] fixed jitpack build fixed jitpack build --- build.gradle | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/build.gradle b/build.gradle index 9dcd2948..66f0de9c 100644 --- a/build.gradle +++ b/build.gradle @@ -42,18 +42,4 @@ task server(type: NodeTask) { ignoreExitValue = true } -task installLibUdev(type:Exec) { - println "OS is UNIX family. Installing libudev-dev..." - commandLine 'sudo', 'apt-get', 'install', 'libudev-dev' - standardOutput = new ByteArrayOutputStream() - ext.output = { return standardOutput.toString() } - ignoreExitValue = false -} - -installLibUdev.onlyIf { org.gradle.nativeplatform.platform.OperatingSystem.isLinux() } - -build.doFirst { - installLibUdev -} - processResources.dependsOn(['npmInstall']) From 2795954736f4b01fb3b3c365f3a6e22e287720c3 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 17:23:06 -0400 Subject: [PATCH 010/110] fixing jitpack build fixing jitpack build --- jitpack.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index 9742d0f9..a80242ac 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,6 +1,6 @@ before_install: - - sudo apt-get update - - sudo apt-get install -y libudev-dev + - apt-get update + - apt-get install -y libudev-dev language: java jdk: From 0d196c7ff9bfe5c19569a657b43d89e4747739a0 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 20 Sep 2018 17:25:43 -0400 Subject: [PATCH 011/110] fixing jitpack build fixing jitpack build --- jitpack.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index a80242ac..9742d0f9 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,6 +1,6 @@ before_install: - - apt-get update - - apt-get install -y libudev-dev + - sudo apt-get update + - sudo apt-get install -y libudev-dev language: java jdk: From 2d96587c786d79a0ac51d4b036ea7f4c20bbc0a1 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 27 Sep 2018 08:55:41 -0400 Subject: [PATCH 012/110] jitpack documentation is very sparse... jitpack documentation is very sparse... --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 66f0de9c..483f1de4 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'application' +apply plugin: 'maven' apply plugin: 'com.moowork.node' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' From 8caf26b614041881507216cbe988e0d8323e264b Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 27 Sep 2018 09:10:52 -0400 Subject: [PATCH 013/110] updating gradle node plugin updating gradle node plugin --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 483f1de4..d5ae5917 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,13 @@ buildscript { repositories { jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } } dependencies { - classpath 'com.moowork.gradle:gradle-node-plugin:1.1.0' //was 0.13 + classpath 'com.moowork.gradle:gradle-node-plugin:1.2.0' //was 0.13 } } From 76ffb0c17c12dba2391e38dc693798cd329737f3 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 12:24:22 -0400 Subject: [PATCH 014/110] updating grade to 4.10.2 updating grade version to 4.10.2 --- build.gradle | 15 ++++++++++----- gradle/wrapper/gradle-wrapper.properties | 12 ++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index d5ae5917..d49fb113 100644 --- a/build.gradle +++ b/build.gradle @@ -7,27 +7,32 @@ buildscript { } dependencies { - classpath 'com.moowork.gradle:gradle-node-plugin:1.2.0' //was 0.13 + // classpath 'com.moowork.gradle:gradle-node-plugin:1.2.0' //was 0.13 } } +plugins { + id "com.moowork.node" version "1.2.0" +} + apply plugin: 'java' apply plugin: 'application' apply plugin: 'maven' -apply plugin: 'com.moowork.node' +//apply plugin: 'com.moowork.node' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' +group = 'com.github.tenbergen' repositories { jcenter() } dependencies { - compile 'org.reflections:reflections:0.9.10' + compile 'org.reflections:reflections:0.9.11' //was 0.9.10 } node { - version = '8.12.0'//'4.4.7' - npmVersion = '6.4.1'//'3.10.5' + version = '8.12.0'// was '4.4.7' + npmVersion = '6.4.1'// was '3.10.5' distBaseUrl = 'https://nodejs.org/dist' download = true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b11535d6..22b32a7a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Aug 25 11:24:49 CEST 2016 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-bin.zip +#Thu Oct 11 11:59:53 EDT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip From 116f389279e7cea959a490bf5d7c3139a9369ffe Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 12:27:02 -0400 Subject: [PATCH 015/110] updating gradle to 4.10.2 updating gradle to 4.10.2 --- jitpack.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index 9742d0f9..a80242ac 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,6 +1,6 @@ before_install: - - sudo apt-get update - - sudo apt-get install -y libudev-dev + - apt-get update + - apt-get install -y libudev-dev language: java jdk: From 01c773c6c589a12779dda6c1e7721f1764a79c2e Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 12:37:25 -0400 Subject: [PATCH 016/110] adding apt-get travis-CI style without sudo adding apt-get travis-CI style without sudo --- jitpack.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jitpack.yml b/jitpack.yml index a80242ac..50d4d6c3 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,3 +1,10 @@ +addons: + apt: + packages: + - cmake + - time + - libudev-dev + before_install: - apt-get update - apt-get install -y libudev-dev From 25058c4053ddbf2a82a638d1082efb0866fdcf98 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 13:56:12 -0400 Subject: [PATCH 017/110] manifested build OS manifested build OS --- jitpack.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jitpack.yml b/jitpack.yml index 50d4d6c3..ce9d2594 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,3 +1,7 @@ +sudo: + - required +os: + - osx addons: apt: packages: From 64bf825bc55144e786701af6fae46324ff3700ab Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 13:59:57 -0400 Subject: [PATCH 018/110] manifested build OS manifested build OS --- jitpack.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index ce9d2594..d8a981c4 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,17 +1,13 @@ -sudo: - - required -os: - - osx +sudo: required +dist: precise addons: apt: packages: - - cmake - - time - libudev-dev -before_install: - - apt-get update - - apt-get install -y libudev-dev +#before_install: +# - apt-get update +# - apt-get install -y libudev-dev language: java jdk: From 609291c70bb3bade5799c01dee920c07514c25a9 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 14:25:15 -0400 Subject: [PATCH 019/110] forcing mkdirp forcing mkdirp --- .travis.yml | 13 ++++++++++--- jitpack.yml | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9742d0f9..db767e92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,13 @@ -before_install: - - sudo apt-get update - - sudo apt-get install -y libudev-dev +sudo: required +dist: precise +addons: + apt: + packages: + - libudev-dev + +#before_install: +# - sudo apt-get update +# - sudo apt-get install -y libudev-dev language: java jdk: diff --git a/jitpack.yml b/jitpack.yml index d8a981c4..f81a3206 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -5,7 +5,8 @@ addons: packages: - libudev-dev -#before_install: +before_install: + - npm install -g mkdirp # - apt-get update # - apt-get install -y libudev-dev From 17120a12880f4b57d07e0a1a2fbfe0519f992da6 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 14:27:13 -0400 Subject: [PATCH 020/110] unforcing mkdirp unforcing mkdirp --- build.gradle | 5 ----- jitpack.yml | 1 - 2 files changed, 6 deletions(-) diff --git a/build.gradle b/build.gradle index d49fb113..13367854 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,6 @@ buildscript { url "https://plugins.gradle.org/m2/" } } - - dependencies { - // classpath 'com.moowork.gradle:gradle-node-plugin:1.2.0' //was 0.13 - } } plugins { @@ -18,7 +14,6 @@ plugins { apply plugin: 'java' apply plugin: 'application' apply plugin: 'maven' -//apply plugin: 'com.moowork.node' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' diff --git a/jitpack.yml b/jitpack.yml index f81a3206..49143b2f 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -6,7 +6,6 @@ addons: - libudev-dev before_install: - - npm install -g mkdirp # - apt-get update # - apt-get install -y libudev-dev From 68a561b834b5019ff33fb96ca44abbf5803381b8 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 11 Oct 2018 14:30:47 -0400 Subject: [PATCH 021/110] fixing travis build fixing travis build --- .travis.yml | 13 +++---------- jitpack.yml | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index db767e92..f37d8072 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,6 @@ -sudo: required -dist: precise -addons: - apt: - packages: - - libudev-dev - -#before_install: -# - sudo apt-get update -# - sudo apt-get install -y libudev-dev +before_install: + - sudo apt-get update + - sudo apt-get install -y libudev-dev language: java jdk: diff --git a/jitpack.yml b/jitpack.yml index 49143b2f..20ccbdb0 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,5 @@ sudo: required -dist: precise +dist: trusty addons: apt: packages: From d36cd07abad48e5e88caa3fb62dd9aae168f33bc Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Thu, 29 Nov 2018 10:20:25 -0500 Subject: [PATCH 022/110] added turn message info added turn message info --- src/main/java/de/adesso/anki/messages/TurnMessage.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/de/adesso/anki/messages/TurnMessage.java b/src/main/java/de/adesso/anki/messages/TurnMessage.java index ff6dcb6c..c36212ce 100644 --- a/src/main/java/de/adesso/anki/messages/TurnMessage.java +++ b/src/main/java/de/adesso/anki/messages/TurnMessage.java @@ -18,6 +18,11 @@ public TurnMessage() { this.type = TYPE; } + /** + * Allows the user to request the vehicle to turn. Currently, according to the manufacturer's API, only U-turns (180deg turns) are supported by the ANKI vehicles. + * @param turnType The "type" of turn. U-Turns are type 3. Other types currently unkown. + * @param trigger When to turn. "1" means immediately, "0" means upon transition to next RoadPiece. + */ public TurnMessage(int turnType, int trigger) { this(); From 488e5252d594619642ca37558f3429f87df20d41 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Fri, 8 Feb 2019 09:11:14 -0500 Subject: [PATCH 023/110] adding robustness to discovery, based on suggestions by Peter Muir from Fraunhofer IESE --- build.gradle | 99 +++--- gradle/wrapper/gradle-wrapper.properties | 4 +- package-lock.json | 153 +++++---- package.json | 2 +- .../java/de/adesso/anki/AnkiConnector.java | 298 ++++++++++-------- src/main/nodejs/server.js | 249 ++++++++------- 6 files changed, 442 insertions(+), 363 deletions(-) diff --git a/build.gradle b/build.gradle index 13367854..5fcad0a9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,49 +1,50 @@ -buildscript { - repositories { - jcenter() - maven { - url "https://plugins.gradle.org/m2/" - } - } -} - -plugins { - id "com.moowork.node" version "1.2.0" -} - -apply plugin: 'java' -apply plugin: 'application' -apply plugin: 'maven' -mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' -group = 'com.github.tenbergen' - -repositories { - jcenter() -} - -dependencies { - compile 'org.reflections:reflections:0.9.11' //was 0.9.10 -} - -node { - version = '8.12.0'// was '4.4.7' - npmVersion = '6.4.1'// was '3.10.5' - distBaseUrl = 'https://nodejs.org/dist' - download = true - - workDir = file("${project.buildDir}/nodejs") - nodeModulesDir = file("${project.projectDir}") -} - -task fatJar(type: Jar) { - baseName = project.name + '-all' - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } - with jar -} - -task server(type: NodeTask) { - script = file('src/main/nodejs/server.js') - ignoreExitValue = true -} - -processResources.dependsOn(['npmInstall']) +buildscript { + repositories { + jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } + } +} + +plugins { + id "com.moowork.node" version "1.2.0" +} + +apply plugin: 'java' +apply plugin: 'application' +apply plugin: 'maven' +mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' +group = 'com.github.tenbergen' + +repositories { + jcenter() +} + +dependencies { + compile 'org.reflections:reflections:0.9.11' //was 0.9.10 + compile 'javax.xml.bind:jaxb-api:2.3.0' //new: +} + +node { + version = '10.15.1'//was '8.12.0'// was '4.4.7' + npmVersion = '6.7.0'// was '6.4.1'// was '3.10.5' + distBaseUrl = 'https://nodejs.org/dist' + download = true + + workDir = file("${project.buildDir}/nodejs") + nodeModulesDir = file("${project.projectDir}") +} + +task fatJar(type: Jar) { + baseName = project.name + '-all' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} + +task server(type: NodeTask) { + script = file('src/main/nodejs/server.js') + ignoreExitValue = true +} + +processResources.dependsOn(['npmInstall']) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 22b32a7a..e41d512b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 11 11:59:53 EDT 2018 +#Thu Feb 07 12:31:46 EST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/package-lock.json b/package-lock.json index 06453abd..c21c472e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ansi-regex": { "version": "2.1.1", @@ -18,14 +17,12 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -41,6 +38,12 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "bindings": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", + "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==", + "optional": true + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -93,8 +96,7 @@ "chownr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "optional": true + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" }, "code-point-at": { "version": "1.1.0", @@ -136,6 +138,18 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -155,20 +169,17 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "optional": true + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "duplexify": { "version": "3.6.0", @@ -261,7 +272,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "optional": true, "requires": { "minipass": "^2.2.1" } @@ -275,7 +285,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -329,8 +338,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "help-me": { "version": "1.1.0", @@ -347,7 +355,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -356,7 +363,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "optional": true, "requires": { "minimatch": "^3.0.4" } @@ -378,8 +384,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "optional": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "is-absolute": { "version": "1.0.0", @@ -442,6 +447,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, "json-stable-stringify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", @@ -486,7 +496,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", - "optional": true, "requires": { "minipass": "^2.2.1" } @@ -549,11 +558,15 @@ "integrity": "sha1-V042Dk2VSrFpZuwQLAwEn9lhoJk=", "optional": true }, + "napi-thread-safe-callback": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/napi-thread-safe-callback/-/napi-thread-safe-callback-0.0.6.tgz", + "integrity": "sha512-X7uHCOCdY4u0yamDxDrv3jF2NtYc8A1nvPzBQgvpoSX+WB3jAe2cVNsY448V1ucq7Whf9Wdy02HEUoLW5rJKWg==" + }, "needle": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.3.tgz", "integrity": "sha512-GPL22d/U9cai87FcCPO6e+MT3vyHS2j+zwotakDc7kE2DtUAqFKMXLJCTtRp+5S75vXIwQPvIxkvlctxf9q4gQ==", - "optional": true, "requires": { "debug": "^2.1.2", "iconv-lite": "^0.4.4", @@ -565,6 +578,11 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, "noble": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/noble/-/noble-1.9.1.tgz", @@ -576,11 +594,26 @@ "xpc-connection": "~0.1.4" } }, + "noble-mac": { + "version": "git://github.com/Timeular/noble-mac.git#3b9d1dc7ee46181f3d0e7ff3a66246a74802495c", + "from": "git://github.com/Timeular/noble-mac.git", + "requires": { + "cross-spawn": "^6.0.5", + "napi-thread-safe-callback": "0.0.6", + "noble": "^1.9.1", + "node-addon-api": "^1.1.0", + "node-pre-gyp": "^0.10.0" + } + }, + "node-addon-api": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.6.2.tgz", + "integrity": "sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA==" + }, "node-pre-gyp": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", - "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -598,7 +631,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "optional": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -607,14 +639,12 @@ "npm-bundled": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", - "optional": true + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" }, "npm-packlist": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", - "optional": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" @@ -624,7 +654,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -640,8 +669,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "optional": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "once": { "version": "1.4.0", @@ -662,20 +690,17 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "optional": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "optional": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "optional": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -691,6 +716,11 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -730,7 +760,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -766,7 +795,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "optional": true, "requires": { "glob": "^7.0.5" } @@ -779,32 +807,40 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "optional": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", - "optional": true + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "optional": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "optional": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "split2": { "version": "2.2.0", @@ -848,14 +884,12 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "optional": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "tar": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", - "optional": true, "requires": { "chownr": "^1.0.1", "fs-minipass": "^1.2.5", @@ -945,11 +979,18 @@ "xtend": "^4.0.0" } }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "optional": true, "requires": { "string-width": "^1.0.2 || 2" } @@ -970,12 +1011,12 @@ } }, "xpc-connection": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/xpc-connection/-/xpc-connection-0.1.4.tgz", - "integrity": "sha1-3Nf6oq7Gt6bhjMXdrQQvejTHcVY=", + "version": "0.1.5", + "resolved": "git://github.com/taoyuan/node-xpc-connection.git#8ff8b20e1146a1cb13cc57f9802593995014f31e", "optional": true, "requires": { - "nan": "^2.0.5" + "bindings": "~1.3.0", + "nan": "^2.4.0" } }, "xtend": { diff --git a/package.json b/package.json index 0501459b..4dc43dcd 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,6 @@ "license": "UNLICENSED", "dependencies": { "mqtt": "^2.18.8", - "noble": "^1.9.0" + "noble-mac": "git://github.com/Timeular/noble-mac.git" } } diff --git a/src/main/java/de/adesso/anki/AnkiConnector.java b/src/main/java/de/adesso/anki/AnkiConnector.java index 7ac56fb1..28b2d4e5 100644 --- a/src/main/java/de/adesso/anki/AnkiConnector.java +++ b/src/main/java/de/adesso/anki/AnkiConnector.java @@ -1,139 +1,159 @@ -package de.adesso.anki; - -import java.io.IOException; -import java.io.PrintWriter; -import java.net.Socket; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; - -import de.adesso.anki.messages.Message; - -/** - * Manages a Bluetooth LE connection by communicating with the Node.js socket. - * - * @author Yannick Eckey - */ -@SuppressWarnings("rawtypes") -public class AnkiConnector { - - private Socket socket; - private final String host; - private final int port; - - private PrintWriter writer; - private NotificationReader reader; - - private Multimap messageListeners; - private Map notificationListeners; - - public AnkiConnector(String host, int port) throws IOException { - this.host = host; - this.port = port; - socket = new Socket(host, port); - writer = new PrintWriter(socket.getOutputStream(), true); - reader = new NotificationReader(socket.getInputStream()); - - messageListeners = ArrayListMultimap.create(); - notificationListeners = new HashMap(); - } - - public AnkiConnector(String host) throws IOException { - this(host, 5000); - } - - public AnkiConnector(AnkiConnector anki) throws IOException{ - this(anki.host, anki.port); - } - - public synchronized List findVehicles() { - writer.println("SCAN"); - List foundVehicles = new ArrayList<>(); - boolean expectingResponse = true; - while (expectingResponse) - { - String response = reader.waitFor("SCAN;"); - if (response.equals("SCAN;COMPLETED")) { - expectingResponse = false; - } - else { - String[] parts = response.split(";"); - - String address = parts[1]; - String manufacturerData = parts[2]; - String localName = parts[3]; - - foundVehicles.add(new Vehicle(this, address, manufacturerData, localName)); - } - } - return foundVehicles; - } - - synchronized void connect(Vehicle vehicle) throws InterruptedException { - writer.println("CONNECT;"+vehicle.getAddress()); - String response = reader.waitFor("CONNECT;"); - -/* commented out because it caused connections to fail every other call - if (response.equals("CONNECT;ERROR")) { - throw new RuntimeException("connect failed"); - } - */ - - NotificationListener carsNotificationListener = (line) -> { - if (line.startsWith(vehicle.getAddress())) { - String messageString = line.replaceFirst(vehicle.getAddress()+";", ""); - Message message = Message.parse(messageString); - fireMessageReceived(vehicle, message); - } - }; - // check if there is a notification listener -> if yes, remove it! (otherwise it will be a listener we cannot track anymore...) - if(notificationListeners.containsKey(vehicle)){ - reader.removeListener(notificationListeners.get(vehicle)); - } - notificationListeners.put(vehicle, carsNotificationListener); - reader.addListener(carsNotificationListener); - } - - synchronized void sendMessage(Vehicle vehicle, Message message) { - writer.println(vehicle.getAddress() + ";" + message.toHex()); - writer.flush(); - } - - public void addMessageListener(Vehicle vehicle, MessageListener listener) { - messageListeners.put(vehicle, listener); - } - - public void removeMessageListener(Vehicle vehicle, MessageListener listener) { - messageListeners.remove(vehicle, listener); - } - - @SuppressWarnings("unchecked") - public void fireMessageReceived(Vehicle vehicle, Message message) { - for (MessageListener l : messageListeners.get(vehicle)) { - l.messageReceived(message); - } - } - - synchronized void disconnect(Vehicle vehicle) { - writer.println("DISCONNECT;"+vehicle.getAddress()); - reader.waitFor("DISCONNECT;"); - reader.removeListener(notificationListeners.remove(vehicle)); - } - - public void close() { - reader.close(); - writer.close(); - - try { - socket.close(); - } - catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } -} +package de.adesso.anki; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; + +import de.adesso.anki.messages.Message; + +/** + * Manages a Bluetooth LE connection by communicating with the Node.js socket. + * + * @author Yannick Eckey + */ +@SuppressWarnings("rawtypes") +public class AnkiConnector { + + private Socket socket; + private final String host; + private final int port; + + private PrintWriter writer; + private NotificationReader reader; + + private Multimap messageListeners; + private Map notificationListeners; + + public AnkiConnector(String host, int port) throws IOException { + this.host = host; + this.port = port; + socket = new Socket(host, port); + writer = new PrintWriter(socket.getOutputStream(), true); + reader = new NotificationReader(socket.getInputStream()); + + messageListeners = ArrayListMultimap.create(); + notificationListeners = new HashMap(); + } + + public AnkiConnector(String host) throws IOException { + this(host, 5000); + } + + public AnkiConnector(AnkiConnector anki) throws IOException { + this(anki.host, anki.port); + } + + public synchronized List findVehicles() { + boolean retry = false; + List foundVehicles = new ArrayList<>(); + do { + try { + writer.println("SCAN"); + + boolean expectingResponse = true; + while (expectingResponse) { + String response = reader.waitFor("SCAN;"); + if (response.equals("SCAN;COMPLETED")) { + expectingResponse = false; + } else { + String[] parts = response.split(";"); + + + if (4 <= parts.length) { //Checks that it is a valid response, else, retries + String address = parts[1]; + String manufacturerData = parts[2]; + String localName = parts[3]; + boolean addressAlreadyExists = false; + for (Vehicle v : foundVehicles) { + if (v.getAddress().equals(address)) { + addressAlreadyExists = true; + break; + } + } + if (!addressAlreadyExists) { + foundVehicles.add(new Vehicle(this, address, manufacturerData, localName)); + } + } else { + System.out.println("Invalid response: " + response); + } //debug message + } + } + retry = false; + } catch (NullPointerException e) { + System.out.println("no reponse, retrying..."); + retry = true; + } + } while (retry); + return foundVehicles; + } + + synchronized void connect(Vehicle vehicle) throws InterruptedException { + writer.println("CONNECT;" + vehicle.getAddress()); + String response = reader.waitFor("CONNECT;"); + +/* commented out because it caused connections to fail every other call + if (response.equals("CONNECT;ERROR")) { + throw new RuntimeException("connect failed"); + } + */ + + NotificationListener carsNotificationListener = (line) -> { + if (line.startsWith(vehicle.getAddress())) { + String messageString = line.replaceFirst(vehicle.getAddress() + ";", ""); + Message message = Message.parse(messageString); + fireMessageReceived(vehicle, message); + } + }; + // check if there is a notification listener -> if yes, remove it! (otherwise it will be a listener we cannot track anymore...) + if (notificationListeners.containsKey(vehicle)) { + reader.removeListener(notificationListeners.get(vehicle)); + } + notificationListeners.put(vehicle, carsNotificationListener); + reader.addListener(carsNotificationListener); + } + + synchronized void sendMessage(Vehicle vehicle, Message message) { + writer.println(vehicle.getAddress() + ";" + message.toHex()); + writer.flush(); + } + + public void addMessageListener(Vehicle vehicle, MessageListener listener) { + messageListeners.put(vehicle, listener); + } + + public void removeMessageListener(Vehicle vehicle, MessageListener listener) { + messageListeners.remove(vehicle, listener); + } + + @SuppressWarnings("unchecked") + public void fireMessageReceived(Vehicle vehicle, Message message) { + for (MessageListener l : messageListeners.get(vehicle)) { + l.messageReceived(message); + } + } + + synchronized void disconnect(Vehicle vehicle) { + writer.println("DISCONNECT;" + vehicle.getAddress()); + reader.waitFor("DISCONNECT;"); + reader.removeListener(notificationListeners.remove(vehicle)); + } + + public void close() { + reader.close(); + writer.close(); + + try { + socket.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } +} diff --git a/src/main/nodejs/server.js b/src/main/nodejs/server.js index 0d25d6b8..1d207622 100644 --- a/src/main/nodejs/server.js +++ b/src/main/nodejs/server.js @@ -1,119 +1,136 @@ -var net = require('net'); -var noble = require('noble'); -var util = require('util'); - -var server = net.createServer(function(client) { - client.vehicles = []; - - client.on("error", (err) => { - console.log("connection error (client disconnected?)"); // client disconnected? - client.vehicles.forEach((vehicle) => vehicle.disconnect()); - }); - client.on("data", function(data) { - data.toString().split("\r\n").forEach(function(line) { - var command = line.toString().trim().split(";"); - if (command[0]) - console.log(command) - switch(command[0]) - { - case "SCAN": - console.log(noble); - if (noble.state === 'poweredOn') { - var discover = function(device) { - client.write(util.format("SCAN;%s;%s;%s\n", - device.id, - device.advertisement.manufacturerData.toString('hex'), - new Buffer(device.advertisement.localName).toString('hex'))); - }; - - noble.on('discover', discover); - noble.startScanning(['be15beef6186407e83810bd89c4d8df4']); - - setTimeout(function() { - noble.stopScanning(); - noble.removeListener('discover', discover) - client.write("SCAN;COMPLETED\n"); - }, 2000); - } - else { - client.write("SCAN;ERROR\n"); - } - break; - - case "CONNECT": - console.log("connect begin"); - if (command.length != 2) { - client.write("CONNECT;ERROR\n"); - break; - } - - var vehicle = noble._peripherals[command[1]]; - if (vehicle === undefined) { - client.write("CONNECT;ERROR\n"); - break; - } - - var success = false; - - vehicle.connect(function(error) { - vehicle.discoverSomeServicesAndCharacteristics( - ["be15beef6186407e83810bd89c4d8df4"], - ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"], +var net = require('net'); +var noble = require('noble'); +var util = require('util'); + +var server = net.createServer(function(client) { + client.vehicles = []; + + client.on("error", (err) => { + console.log("connection error (client disconnected?)"); // client disconnected? + client.vehicles.forEach((vehicle) => vehicle.disconnect()); + }); + client.on("data", function(data) { + data.toString().split("\r\n").forEach(function(line) { + var command = line.toString().trim().split(";"); + if (command[0]) + console.log(command) + switch(command[0]) + { + case "SCAN": + console.log(noble); + if (noble.state === 'poweredOn') { + var discover = function(device) { + //Peter Muir: edited to more reliably connect to the cars. Hardcoded localName + //and txPowerLevel. Should change to a dynamic setup if necessary. + //(Context: some cars connect with undefined localName, crashing the server.) + if(undefined === device.advertisement.localName){ + console.log("No localName. Defaulting"); + device.advertisement.txPowerLevel = 0; + //The two cars have the same localName and have uuids starting with 'e' + if(device.id.charAt(0) === 'e'){ + device.advertisement.localName = "\u0001`0\u0001 Drive\u0000"; + }else{ + device.advertisement.localName = "\u0010`0\u0001 Drive\u0000"; + }; + }; + //DEBUG message + console.log(util.format("SCAN;%s;%s\n", + device.id, + device.advertisement.manufacturerData.toString('hex'))); + client.write(util.format("SCAN;%s;%s;%s\n", + device.id, + device.advertisement.manufacturerData.toString('hex'), + new Buffer(device.advertisement.localName).toString('hex'))); + }; + + noble.on('discover', discover); + noble.startScanning(['be15beef6186407e83810bd89c4d8df4']); + + setTimeout(function() { + noble.stopScanning(); + noble.removeListener('discover', discover) + client.write("SCAN;COMPLETED\n"); + }, 2000); + } + else { + client.write("SCAN;ERROR\n"); + } + break; + + case "CONNECT": + console.log("connect begin"); + if (command.length != 2) { + client.write("CONNECT;ERROR\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("CONNECT;ERROR\n"); + break; + } + + var success = false; + + vehicle.connect(function(error) { + vehicle.discoverSomeServicesAndCharacteristics( + ["be15beef6186407e83810bd89c4d8df4"], + ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"], function(error, services, characteristics) { // console.log("!!!!!!!!!!!!!!!!!"); // console.log(characteristics); - // console.log("!!!!!!!!!!!!!!!!!"); - vehicle.reader = characteristics[1];//.find(x => !x.properties.includes("write")); - vehicle.writer = characteristics[0];//.find(x => x.properties.includes("write")); - - vehicle.reader.notify(true); - vehicle.reader.on('read', function(data, isNotification) { - client.write(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); - //console.log(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); - }); - client.write("CONNECT;SUCCESS\n"); - client.vehicles.push(vehicle); - console.log("connect success"); - success = true; - } - ); - }); - - setTimeout(() => { - if (!success) { - client.write("CONNECT;ERROR\n"); - console.log("connect error (timeout)"); - } - }, 500); - - break; - - case "DISCONNECT": - if (command.length != 2) { - client.write("DISCONNECT;ERROR\n"); - break; - } - - var vehicle = noble._peripherals[command[1]]; - if (vehicle === undefined) { - client.write("DISCONNECT;ERROR\n"); - break; - } - - vehicle.disconnect(); - client.write("DISCONNECT;SUCCESS\n"); - break; - - default: - if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { - var vehicle = noble._peripherals[command[0]]; - vehicle.writer.write(new Buffer(command[1], 'hex')); - } - } - }); - }); -}); - -server.listen(5000); - -console.log("Server started") + // console.log("!!!!!!!!!!!!!!!!!"); + vehicle.reader = characteristics[1];//.find(x => !x.properties.includes("write")); + vehicle.writer = characteristics[0];//.find(x => x.properties.includes("write")); + + vehicle.reader.notify(true); + vehicle.reader.on('data', function(data, isNotification) { + client.write(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + //console.log(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + }); + client.write("CONNECT;SUCCESS\n"); + client.vehicles.push(vehicle); + console.log("connect success"); + success = true; + } + ); + }); + + setTimeout(() => { + if (!success) { + client.write("CONNECT;ERROR\n"); + console.log("connect error (timeout)"); + } + }, 500); + + break; + + case "DISCONNECT": + if (command.length != 2) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + vehicle.disconnect(); + client.write("DISCONNECT;SUCCESS\n"); + break; + + default: + if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { + var vehicle = noble._peripherals[command[0]]; + vehicle.writer.write(new Buffer(command[1], 'hex')); + } + } + }); + }); +}); + +server.listen(5000); + +console.log("Server started") From 9dde2e556b359a1653e554dce2b7c149aead786d Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Fri, 8 Feb 2019 09:21:05 -0500 Subject: [PATCH 024/110] adding robustness to discovery, based on suggestions by Peter Muir from Fraunhofer IESE --- package.json | 2 + src/main/java/META-INF/MANIFEST.MF | 3 + src/main/nodejs/server.js.backup | 119 +++++++++++++++++++++++++ src/main/nodejs/server.js.chris | 128 +++++++++++++++++++++++++++ src/main/nodejs/server.js.peter | 136 +++++++++++++++++++++++++++++ 5 files changed, 388 insertions(+) create mode 100644 src/main/java/META-INF/MANIFEST.MF create mode 100644 src/main/nodejs/server.js.backup create mode 100644 src/main/nodejs/server.js.chris create mode 100644 src/main/nodejs/server.js.peter diff --git a/package.json b/package.json index 4dc43dcd..f5d29cd4 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "license": "UNLICENSED", "dependencies": { "mqtt": "^2.18.8", + "noble": "^1.9.0", + "xpc-connection": "git://github.com/taoyuan/node-xpc-connection.git", "noble-mac": "git://github.com/Timeular/noble-mac.git" } } diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 00000000..80f215be --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: edu.oswego.cs.CPSLab.anki.AnkiConnectionTest + diff --git a/src/main/nodejs/server.js.backup b/src/main/nodejs/server.js.backup new file mode 100644 index 00000000..df4b3d3d --- /dev/null +++ b/src/main/nodejs/server.js.backup @@ -0,0 +1,119 @@ +var net = require('net'); +var noble = require('noble'); +var util = require('util'); + +var server = net.createServer(function(client) { + client.vehicles = []; + + client.on("error", (err) => { + console.log("connection error (client disconnected?)"); // client disconnected? + client.vehicles.forEach((vehicle) => vehicle.disconnect()); + }); + client.on("data", function(data) { + data.toString().split("\r\n").forEach(function(line) { + var command = line.toString().trim().split(";"); + if (command[0]) + console.log(command) + switch(command[0]) + { + case "SCAN": + console.log(noble); + if (noble.state === 'poweredOn') { + var discover = function(device) { + client.write(util.format("SCAN;%s;%s;%s\n", + device.id, + device.advertisement.manufacturerData.toString('hex'), + new Buffer(device.advertisement.localName).toString('hex'))); + }; + + noble.on('discover', discover); + noble.startScanning(['be15beef6186407e83810bd89c4d8df4']); + + setTimeout(function() { + noble.stopScanning(); + noble.removeListener('discover', discover) + client.write("SCAN;COMPLETED\n"); + }, 2000); + } + else { + client.write("SCAN;ERROR\n"); + } + break; + + case "CONNECT": + console.log("connect begin"); + if (command.length != 2) { + client.write("CONNECT;ERROR\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("CONNECT;ERROR\n"); + break; + } + + var success = false; + + vehicle.connect(function(error) { + vehicle.discoverSomeServicesAndCharacteristics( + ["be15beef6186407e83810bd89c4d8df4"], + ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"], + function(error, services, characteristics) { + // console.log("!!!!!!!!!!!!!!!!!"); + // console.log(characteristics); + // console.log("!!!!!!!!!!!!!!!!!"); + vehicle.reader = characteristics[1];//.find(x => !x.properties.includes("write")); + vehicle.writer = characteristics[0];//.find(x => x.properties.includes("write")); + + vehicle.reader.notify(true); + vehicle.reader.on('read', function(data, isNotification) { + client.write(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + //console.log(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + }); + client.write("CONNECT;SUCCESS\n"); + client.vehicles.push(vehicle); + console.log("connect success"); + success = true; + } + ); + }); + + setTimeout(() => { + if (!success) { + client.write("CONNECT;ERROR\n"); + console.log("connect error (timeout)"); + } + }, 500); + + break; + + case "DISCONNECT": + if (command.length != 2) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + vehicle.disconnect(); + client.write("DISCONNECT;SUCCESS\n"); + break; + + default: + if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { + var vehicle = noble._peripherals[command[0]]; + vehicle.writer.write(new Buffer(command[1], 'hex')); + } + } + }); + }); +}); + +server.listen(5000); + +console.log("Server started") diff --git a/src/main/nodejs/server.js.chris b/src/main/nodejs/server.js.chris new file mode 100644 index 00000000..d917b693 --- /dev/null +++ b/src/main/nodejs/server.js.chris @@ -0,0 +1,128 @@ +var net = require('net'); +var noble = require('noble'); +var util = require('util'); + +var server = net.createServer(function(client) { + client.vehicles = []; + + noble.on('statechange', function(state) { + if (state == 'poweredOn') { + console.log("Noble on"); + } else { + noble.log("Noble not on"); + } + }); + client.on("error", (err) => { + console.log("connection error"); // client disconnected? + client.vehicles.forEach((vehicle) => vehicle.disconnect()); + }); + client.on("data", function(data) { + data.toString().split("\r\n").forEach(function(line) { + var command = line.toString().trim().split(";"); + switch(command[0]) + { + case "SCAN": + console.log("Beginning scan"); + //if (noble.state === 'poweredOn') { + console.log("Is powered on"); + var discover = function(device) { + client.write(util.format("SCAN;%s;%s;%s\n", + device.id, + device.advertisement.manufacturerData.toString('hex'), + new Buffer(device.advertisement.localName).toString('hex'))); + }; + + noble.on('discover', discover); + noble.startScanning(['be15beef6186407e83810bd89c4d8df4']); + + setTimeout(function() { + noble.stopScanning(); + noble.removeListener('discover', discover) + client.write("SCAN;COMPLETED\n"); + }, 2000); + //} + //else { + //console.log("Noble not powered on"); + //client.write("SCAN;ERROR\n"); + //} + break; + + case "CONNECT": + console.log("connect begin"); + if (command.length != 2) { + client.write("CONNECT;ERROR-BAD-COMMAND\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("CONNECT;ERROR-CONNECTING-VEHICLE\n"); + break; + } + + var success = false; + + vehicle.connect(function(error) { + vehicle.discoverSomeServicesAndCharacteristics( + ["be15beef6186407e83810bd89c4d8df4"], + ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"], + function(error, services, characteristics) { + vehicle.reader = characteristics.find(x => !x.properties.includes("write")); + vehicle.writer = characteristics.find(x => x.properties.includes("write")); + + vehicle.reader.notify(true); + vehicle.reader.on('read', function(data, isNotification) { + client.write(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + //console.log(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + }); + client.write("CONNECT;SUCCESS\n"); + client.vehicles.push(vehicle); + console.log("connect success"); + success = true; + } + ); + }); + + setTimeout(() => { + if (!success) { + client.write("CONNECT;ERROR-TIMEOUT\n"); + console.log("connect error"); + } + }, 500); + + break; + + case "DISCONNECT": + if (command.length != 2) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + vehicle.disconnect(); + client.write("DISCONNECT;SUCCESS\n"); + break; + + case "EXIT": + client.write("BYE\n"); + server.close(function () { console.log('Server closed!'); }); + client.destroy(); + break; + default: + if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { + var vehicle = noble._peripherals[command[0]]; + vehicle.writer.write(new Buffer(command[1], 'hex')); + } + } + }); + }); +}); + +server.listen(5000); + +console.log("Server gestartet") diff --git a/src/main/nodejs/server.js.peter b/src/main/nodejs/server.js.peter new file mode 100644 index 00000000..1d207622 --- /dev/null +++ b/src/main/nodejs/server.js.peter @@ -0,0 +1,136 @@ +var net = require('net'); +var noble = require('noble'); +var util = require('util'); + +var server = net.createServer(function(client) { + client.vehicles = []; + + client.on("error", (err) => { + console.log("connection error (client disconnected?)"); // client disconnected? + client.vehicles.forEach((vehicle) => vehicle.disconnect()); + }); + client.on("data", function(data) { + data.toString().split("\r\n").forEach(function(line) { + var command = line.toString().trim().split(";"); + if (command[0]) + console.log(command) + switch(command[0]) + { + case "SCAN": + console.log(noble); + if (noble.state === 'poweredOn') { + var discover = function(device) { + //Peter Muir: edited to more reliably connect to the cars. Hardcoded localName + //and txPowerLevel. Should change to a dynamic setup if necessary. + //(Context: some cars connect with undefined localName, crashing the server.) + if(undefined === device.advertisement.localName){ + console.log("No localName. Defaulting"); + device.advertisement.txPowerLevel = 0; + //The two cars have the same localName and have uuids starting with 'e' + if(device.id.charAt(0) === 'e'){ + device.advertisement.localName = "\u0001`0\u0001 Drive\u0000"; + }else{ + device.advertisement.localName = "\u0010`0\u0001 Drive\u0000"; + }; + }; + //DEBUG message + console.log(util.format("SCAN;%s;%s\n", + device.id, + device.advertisement.manufacturerData.toString('hex'))); + client.write(util.format("SCAN;%s;%s;%s\n", + device.id, + device.advertisement.manufacturerData.toString('hex'), + new Buffer(device.advertisement.localName).toString('hex'))); + }; + + noble.on('discover', discover); + noble.startScanning(['be15beef6186407e83810bd89c4d8df4']); + + setTimeout(function() { + noble.stopScanning(); + noble.removeListener('discover', discover) + client.write("SCAN;COMPLETED\n"); + }, 2000); + } + else { + client.write("SCAN;ERROR\n"); + } + break; + + case "CONNECT": + console.log("connect begin"); + if (command.length != 2) { + client.write("CONNECT;ERROR\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("CONNECT;ERROR\n"); + break; + } + + var success = false; + + vehicle.connect(function(error) { + vehicle.discoverSomeServicesAndCharacteristics( + ["be15beef6186407e83810bd89c4d8df4"], + ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"], + function(error, services, characteristics) { + // console.log("!!!!!!!!!!!!!!!!!"); + // console.log(characteristics); + // console.log("!!!!!!!!!!!!!!!!!"); + vehicle.reader = characteristics[1];//.find(x => !x.properties.includes("write")); + vehicle.writer = characteristics[0];//.find(x => x.properties.includes("write")); + + vehicle.reader.notify(true); + vehicle.reader.on('data', function(data, isNotification) { + client.write(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + //console.log(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); + }); + client.write("CONNECT;SUCCESS\n"); + client.vehicles.push(vehicle); + console.log("connect success"); + success = true; + } + ); + }); + + setTimeout(() => { + if (!success) { + client.write("CONNECT;ERROR\n"); + console.log("connect error (timeout)"); + } + }, 500); + + break; + + case "DISCONNECT": + if (command.length != 2) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + var vehicle = noble._peripherals[command[1]]; + if (vehicle === undefined) { + client.write("DISCONNECT;ERROR\n"); + break; + } + + vehicle.disconnect(); + client.write("DISCONNECT;SUCCESS\n"); + break; + + default: + if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { + var vehicle = noble._peripherals[command[0]]; + vehicle.writer.write(new Buffer(command[1], 'hex')); + } + } + }); + }); +}); + +server.listen(5000); + +console.log("Server started") From 4b7a88ab09d8bc163570c53026901c48b6727299 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Fri, 8 Feb 2019 09:24:25 -0500 Subject: [PATCH 025/110] adding robustness to discovery, based on suggestions by Peter Muir from Fraunhofer IESE --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index f5d29cd4..730c4f40 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "dependencies": { "mqtt": "^2.18.8", "noble": "^1.9.0", - "xpc-connection": "git://github.com/taoyuan/node-xpc-connection.git", "noble-mac": "git://github.com/Timeular/noble-mac.git" } } From 8b36e964d78d1b5cf88b79780f29384aa9410ad0 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Fri, 8 Feb 2019 10:08:30 -0500 Subject: [PATCH 026/110] adding robustness to discovery, based on suggestions by Peter Muir from Fraunhofer IESE --- build.gradle | 4 ++-- src/main/nodejs/server.js | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 5fcad0a9..1c5fa29d 100644 --- a/build.gradle +++ b/build.gradle @@ -27,8 +27,8 @@ dependencies { } node { - version = '10.15.1'//was '8.12.0'// was '4.4.7' - npmVersion = '6.7.0'// was '6.4.1'// was '3.10.5' + version = '8.12.0'// was '4.4.7' + npmVersion = '6.4.1'// was '3.10.5' distBaseUrl = 'https://nodejs.org/dist' download = true diff --git a/src/main/nodejs/server.js b/src/main/nodejs/server.js index 1d207622..3c538f2a 100644 --- a/src/main/nodejs/server.js +++ b/src/main/nodejs/server.js @@ -16,9 +16,11 @@ var server = net.createServer(function(client) { console.log(command) switch(command[0]) { - case "SCAN": + case "SCAN": console.log(noble); - if (noble.state === 'poweredOn') { +// Bastian Tenbergen: removed - node never seems to show this state. +// However, causes AnkiConnector to get NullPointerExceptions sometimes. +// if (noble.state === 'poweredOn') { var discover = function(device) { //Peter Muir: edited to more reliably connect to the cars. Hardcoded localName //and txPowerLevel. Should change to a dynamic setup if necessary. @@ -51,10 +53,10 @@ var server = net.createServer(function(client) { noble.removeListener('discover', discover) client.write("SCAN;COMPLETED\n"); }, 2000); - } - else { - client.write("SCAN;ERROR\n"); - } +// } +// else { +// client.write("SCAN;ERROR\n"); +// } break; case "CONNECT": From eb8da4456afbe052ce61f0822080d20cbf856eb9 Mon Sep 17 00:00:00 2001 From: dmyrdek Date: Fri, 8 Feb 2019 12:49:06 -0500 Subject: [PATCH 027/110] added libudev-dev to jitpack --- jitpack.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index 20ccbdb0..f2c8e517 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -6,8 +6,8 @@ addons: - libudev-dev before_install: -# - apt-get update -# - apt-get install -y libudev-dev + - apt-get update + - apt-get install -y libudev-dev language: java jdk: From ffd5686dc23ab9893d3e2a86a433732e5c1be086 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Sat, 9 Feb 2019 13:30:32 -0500 Subject: [PATCH 028/110] jitpack doesn't like pre-build sudo directives, or so I thought. --- jitpack.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index 20ccbdb0..f2c8e517 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -6,8 +6,8 @@ addons: - libudev-dev before_install: -# - apt-get update -# - apt-get install -y libudev-dev + - apt-get update + - apt-get install -y libudev-dev language: java jdk: From c1f7f21dc5ce8a1a7947f580a860fc9702208cfe Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Sun, 31 Mar 2019 11:18:48 -0400 Subject: [PATCH 029/110] Corrected ID of vehicle "Guardian" from 0x0d to 0x0c. Added new vehicles: - Fast & Furious Edition Ice Charger - Fast & Furious Edition Intl. MXT - NUKE Phantom - X52 Supertruck - unknown model 0x0d Added support in Vehicle.java to handle unknown models. Added edu.oswego.cs.CPSLab.anki.FourWayStop.* for CSC436 Spring 2019 semester project. --- src/main/java/de/adesso/anki/Model.java | 111 ++++--- src/main/java/de/adesso/anki/Vehicle.java | 289 +++++++++--------- .../cs/CPSLab/anki/AnkiConnectionTest.java | 4 +- .../anki/FourWayStop/IntersectionHandler.java | 19 ++ .../CPSLab/anki/FourWayStop/VehicleInfo.java | 22 ++ src/main/nodejs/server.js.backup | 119 -------- 6 files changed, 248 insertions(+), 316 deletions(-) create mode 100644 src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java create mode 100644 src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java delete mode 100644 src/main/nodejs/server.js.backup diff --git a/src/main/java/de/adesso/anki/Model.java b/src/main/java/de/adesso/anki/Model.java index b7bd75aa..ae30f63a 100644 --- a/src/main/java/de/adesso/anki/Model.java +++ b/src/main/java/de/adesso/anki/Model.java @@ -1,57 +1,56 @@ -package de.adesso.anki; - -import java.util.HashMap; -import java.util.Map; - -/** - * Enumerates some currently known Anki vehicle models. - * Known vehicles currently available, but not considered in this class: - * Supercars: - * - NUKE Phantom - * - Fast & Furious Dom's Charger - * - Fast & Furious Hobbs' MXT - * Supertrucks: - * - X52 (probably ID 0x10). - * - * @author Yannick Eckey - * @author B. Tenbergen - */ -public enum Model { - KOURAI(0x01), - BOSON(0x02), - RHO(0x03), - KATAL(0x04), - HADION(0x05), - SPEKTRIX(0x06), - CORAX(0x07), - GROUNDSHOCK(0x08, "#2994f1"), - SKULL(0x09, "#df3232"), - THERMO(0x0a, "#a11c20"), - NUKE(0x0b, "#bed62f"), - GUARDIAN(0x0d, "#42b1d7"), - BIGBANG(0x0e, "#4e674d"), - FREEHWEEL(0x0f), //BT update on 9/14/18 to add new supertruck -// __SOMECAR1(0x10), //TODO: figure out which car this is (BT)... probably X52 - X52ICE(0x11); //BT update on 9/14/18 to add new supertruck -// __SOMECAR3(0x12); //TODO: figure out which car this is (BT) - - private int id; - private String color = "#f00"; - - private Model(int id) { this.id = id; } - private Model(int id, String color) { this.id = id; this.color = color; } - - public String getColor() { - return color; - } - - public static Model fromId(int id) { - return idToModel.get(id); - } - - private static final Map idToModel = new HashMap() {{ - for (Model m : Model.values()) { - put(m.id, m); - } - }}; +package de.adesso.anki; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumerates some currently known Anki vehicle models. + * Updated on 9/14/19 and 3/31/19 to include extra models. Model 0x0d, previously "Guardian" reassigned to + * 0x0c. Model 0x0d is now unknown model. + * + * @author Yannick Eckey + * @author B. Tenbergen + * @version 2019-03-31 + */ +public enum Model { + KOURAI(0x01), + BOSON(0x02), + RHO(0x03), + KATAL(0x04), + HADION(0x05), + SPEKTRIX(0x06), + CORAX(0x07), + GROUNDSHOCK(0x08, "#2994f1"), + SKULL(0x09, "#df3232"), + THERMO(0x0a, "#a11c20"), + NUKE(0x0b, "#bed62f"), + GUARDIAN(0x0c, "#42b1d7"), //BT update on 3/31/19 to correct ID of model "Guardian" + __SOMECAR2(0x0d), //BT update on 3/31/19 to add ID of unknown model + BIGBANG(0x0e, "#4e674d"), + FREEHWEEL(0x0f, "#25bc00"), //BT update on 9/14/18 to add new supertruck Freewheel + X52(0x10, "#990909"), //BT update on 3/31/19 to add new supertruck X52 + X52ICE(0x11, "#d1e9ff"), //BT update on 9/14/18 to add new supertruck X52 Ice + MXT(0x12, "#475666"), //BT update on 3/31/19 to add Fast & Furious Ed. Intl. MXT + CHARGER(0x13, "#6d7175"), //BT update on 3/31/19 to add Fast & Furious Ed. Ice Charger + PHANTOM(0x14, "#2d2d2d"); //BT update on 3/31/19 to add NUKE Phantom model + + private int id; + private String color = "#f00"; + + private Model(int id) { this.id = id; } + private Model(int id, String color) { this.id = id; this.color = color; } + + public String getColor() { + return color; + } + + public static Model fromId(int id) { + return idToModel.get(id); + } + + private static final Map idToModel = new HashMap() {{ + for (Model m : Model.values()) { + put(m.id, m); + } + }}; } \ No newline at end of file diff --git a/src/main/java/de/adesso/anki/Vehicle.java b/src/main/java/de/adesso/anki/Vehicle.java index 9b56f773..c000de37 100644 --- a/src/main/java/de/adesso/anki/Vehicle.java +++ b/src/main/java/de/adesso/anki/Vehicle.java @@ -1,139 +1,150 @@ -package de.adesso.anki; - -import java.io.IOException; -import java.time.LocalTime; -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.Multimap; - -import de.adesso.anki.messages.Message; - -// TODO: Manage connection status and fail gracefully if disconnected - -/** - * Represents a vehicle and allows communicating with it. - * - * @author Yannick Eckey - */ -public class Vehicle { - - private String address; - private AdvertisementData advertisement; - - private AnkiConnector anki; - - private Multimap, MessageListener> listeners; - private MessageListener defaultListener; - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public AdvertisementData getAdvertisement() { - return advertisement; - } - - public void setAdvertisement(AdvertisementData advertisement) { - this.advertisement = advertisement; - } - - @Override - public String toString() { - return advertisement.toString(); - } - - public void connect() { - try { - int count = 0; - int maxTries = 5; - boolean connected = false; - - while (!connected) { - try { - anki.connect(this); - connected = true; - } catch (RuntimeException e) { - if (++count == maxTries) - throw e; - } - } - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - defaultListener = (message) -> fireMessageReceived(message); - anki.addMessageListener(this, defaultListener); - } - - public void disconnect() { - anki.removeMessageListener(this, defaultListener); - anki.disconnect(this); - } - - public void sendMessage(Message message) { - anki.sendMessage(this, message); - System.out.println(String.format("[%s] > %s: %s", LocalTime.now(), this, message)); - } - - @Deprecated - public void addMessageListener(MessageListener listener) { - this.addMessageListener(Message.class, listener); - } - - @Deprecated - public void removeMessageListener(MessageListener listener) { - this.removeMessageListener(Message.class, listener); - } - - public void addMessageListener(Class klass, MessageListener listener) { - this.listeners.put(klass, listener); - } - - public void removeMessageListener(Class klass, MessageListener listener) { - this.listeners.remove(klass, listener); - } - - private void fireMessageReceived(T message) { - for (MessageListener l : this.listeners.get(Message.class)) { - l.messageReceived(message); - } - if (message.getClass() != Message.class) { - for (MessageListener l : this.listeners.get(message.getClass())) { - l.messageReceived(message); - } - } - } - - public Vehicle(AnkiConnector anki, String address, String manufacturerData, String localName) { - try { - this.anki = new AnkiConnector(anki); - } catch (IOException e) { - this.anki = anki; - } - this.address = address; - this.advertisement = new AdvertisementData(manufacturerData, localName); - - this.listeners = LinkedListMultimap.create(); - } - - public String getColor() { - return advertisement.getModel().getColor(); - } - - @Override - public int hashCode() { - return address.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if(obj instanceof Vehicle){ - return ((Vehicle) obj).getAddress().equals(this.getAddress()); - } - return false; - } -} +package de.adesso.anki; + +import java.io.IOException; +import java.time.LocalTime; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; + +import de.adesso.anki.messages.Message; + +// TODO: Manage connection status and fail gracefully if disconnected + +/** + * Represents a vehicle and allows communicating with it. + * + * @author Yannick Eckey + */ +public class Vehicle { + + private String address; + private AdvertisementData advertisement; + + private AnkiConnector anki; + + private Multimap, MessageListener> listeners; + private MessageListener defaultListener; + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public AdvertisementData getAdvertisement() { + return advertisement; + } + + public void setAdvertisement(AdvertisementData advertisement) { + this.advertisement = advertisement; + } + + @Override + public String toString() { + return advertisement.toString(); + } + + public void connect() { + try { + int count = 0; + int maxTries = 5; + boolean connected = false; + + while (!connected) { + try { + anki.connect(this); + connected = true; + } catch (RuntimeException e) { + if (++count == maxTries) + throw e; + } + } + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + defaultListener = (message) -> fireMessageReceived(message); + anki.addMessageListener(this, defaultListener); + } + + public void disconnect() { + anki.removeMessageListener(this, defaultListener); + anki.disconnect(this); + } + + public void sendMessage(Message message) { + anki.sendMessage(this, message); + System.out.println(String.format("[%s] > %s: %s", LocalTime.now(), this, message)); + } + + @Deprecated + public void addMessageListener(MessageListener listener) { + this.addMessageListener(Message.class, listener); + } + + @Deprecated + public void removeMessageListener(MessageListener listener) { + this.removeMessageListener(Message.class, listener); + } + + public void addMessageListener(Class klass, MessageListener listener) { + this.listeners.put(klass, listener); + } + + public void removeMessageListener(Class klass, MessageListener listener) { + this.listeners.remove(klass, listener); + } + + private void fireMessageReceived(T message) { + for (MessageListener l : this.listeners.get(Message.class)) { + l.messageReceived(message); + } + if (message.getClass() != Message.class) { + for (MessageListener l : this.listeners.get(message.getClass())) { + l.messageReceived(message); + } + } + } + + public Vehicle(AnkiConnector anki, String address, String manufacturerData, String localName) { + try { + this.anki = new AnkiConnector(anki); + } catch (IOException e) { + this.anki = anki; + } + this.address = address; + this.advertisement = new AdvertisementData(manufacturerData, localName); + + this.listeners = LinkedListMultimap.create(); + } + + /** + * Returns the color of the vehicle. + * Update 3/31/19: added functionality to cope with missing color attribute in previously unknown models. + * @author Yannick Eckey + * @author Bastian Tenbergen + * @return The color of the vehicle or some error string. + * @version 2019-03-31 + */ + public String getColor() { + if (advertisement == null) return "ERROR! Advertisement is null."; + else if (advertisement.getModel() == null) return "ERROR! unknown model"; + else if (advertisement.getModel().getColor() == null) return "unkown"; + else return advertisement.getModel().getColor(); + } + + @Override + public int hashCode() { + return address.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if(obj instanceof Vehicle){ + return ((Vehicle) obj).getAddress().equals(this.getAddress()); + } + return false; + } +} diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java index adfbdd78..52fc1359 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java @@ -30,7 +30,7 @@ public class AnkiConnectionTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("localhost", 5000); + AnkiConnector anki = new AnkiConnector("interplexus.local", 5000); System.out.print("...looking for cars..."); List vehicles = anki.findVehicles(); @@ -85,7 +85,7 @@ public static void main(String[] args) throws IOException, InterruptedException lpm.add(lc); v.sendMessage(lpm); System.out.println(" Setting Speed..."); - v.sendMessage(new SetSpeedMessage(200, 75)); + v.sendMessage(new SetSpeedMessage(500, 100)); //Thread.sleep(1000); //gs.sendMessage(new TurnMessage()); System.out.print("Sleeping for 10secs... "); diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java b/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java new file mode 100644 index 00000000..f1bd027e --- /dev/null +++ b/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java @@ -0,0 +1,19 @@ +package edu.oswego.cs.CPSLab.anki.FourWayStop; + +import de.adesso.anki.AdvertisementData; +import de.adesso.anki.Vehicle; + +import java.util.Queue; + +/** + * An interface to handle communication between cyber-physical vehicles. + * @author Shakhar Dasgupta + */ +public interface IntersectionHandler { + void awaitClearIntersection(); + void broadcast(AdvertisementData vehicleInfo); + void clearIntersection(); + void notify(Vehicle target, Queue queue); + void listenToBroadcast(); + void becomeMaster(); +} \ No newline at end of file diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java b/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java new file mode 100644 index 00000000..0f696b0e --- /dev/null +++ b/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java @@ -0,0 +1,22 @@ +package edu.oswego.cs.CPSLab.anki.FourWayStop; + +import java.time.Instant; +import java.util.Queue; +import java.io.Serializable; + +/** + * A class to exchange information about cyber-physical vehicles. + * @author Benjamin Groman + */ +public class VehicleInfo implements Serializable { + private static final long serialVersionUID = 436L; + public int locationID; + public float offsetFromRoadCenter; + public int speed; + public Instant timestamp; + public String MACid; + public int roadPieceID; + public boolean isClear; + public boolean isMaster; + public Queue otherVehicles;//should be null if not master +} \ No newline at end of file diff --git a/src/main/nodejs/server.js.backup b/src/main/nodejs/server.js.backup deleted file mode 100644 index df4b3d3d..00000000 --- a/src/main/nodejs/server.js.backup +++ /dev/null @@ -1,119 +0,0 @@ -var net = require('net'); -var noble = require('noble'); -var util = require('util'); - -var server = net.createServer(function(client) { - client.vehicles = []; - - client.on("error", (err) => { - console.log("connection error (client disconnected?)"); // client disconnected? - client.vehicles.forEach((vehicle) => vehicle.disconnect()); - }); - client.on("data", function(data) { - data.toString().split("\r\n").forEach(function(line) { - var command = line.toString().trim().split(";"); - if (command[0]) - console.log(command) - switch(command[0]) - { - case "SCAN": - console.log(noble); - if (noble.state === 'poweredOn') { - var discover = function(device) { - client.write(util.format("SCAN;%s;%s;%s\n", - device.id, - device.advertisement.manufacturerData.toString('hex'), - new Buffer(device.advertisement.localName).toString('hex'))); - }; - - noble.on('discover', discover); - noble.startScanning(['be15beef6186407e83810bd89c4d8df4']); - - setTimeout(function() { - noble.stopScanning(); - noble.removeListener('discover', discover) - client.write("SCAN;COMPLETED\n"); - }, 2000); - } - else { - client.write("SCAN;ERROR\n"); - } - break; - - case "CONNECT": - console.log("connect begin"); - if (command.length != 2) { - client.write("CONNECT;ERROR\n"); - break; - } - - var vehicle = noble._peripherals[command[1]]; - if (vehicle === undefined) { - client.write("CONNECT;ERROR\n"); - break; - } - - var success = false; - - vehicle.connect(function(error) { - vehicle.discoverSomeServicesAndCharacteristics( - ["be15beef6186407e83810bd89c4d8df4"], - ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"], - function(error, services, characteristics) { - // console.log("!!!!!!!!!!!!!!!!!"); - // console.log(characteristics); - // console.log("!!!!!!!!!!!!!!!!!"); - vehicle.reader = characteristics[1];//.find(x => !x.properties.includes("write")); - vehicle.writer = characteristics[0];//.find(x => x.properties.includes("write")); - - vehicle.reader.notify(true); - vehicle.reader.on('read', function(data, isNotification) { - client.write(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); - //console.log(util.format("%s;%s\n", vehicle.id, data.toString("hex"))); - }); - client.write("CONNECT;SUCCESS\n"); - client.vehicles.push(vehicle); - console.log("connect success"); - success = true; - } - ); - }); - - setTimeout(() => { - if (!success) { - client.write("CONNECT;ERROR\n"); - console.log("connect error (timeout)"); - } - }, 500); - - break; - - case "DISCONNECT": - if (command.length != 2) { - client.write("DISCONNECT;ERROR\n"); - break; - } - - var vehicle = noble._peripherals[command[1]]; - if (vehicle === undefined) { - client.write("DISCONNECT;ERROR\n"); - break; - } - - vehicle.disconnect(); - client.write("DISCONNECT;SUCCESS\n"); - break; - - default: - if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { - var vehicle = noble._peripherals[command[0]]; - vehicle.writer.write(new Buffer(command[1], 'hex')); - } - } - }); - }); -}); - -server.listen(5000); - -console.log("Server started") From 6a110e76aa1195ed829d1c80b68225fa5410c204 Mon Sep 17 00:00:00 2001 From: dmyrdek Date: Mon, 6 May 2019 09:38:58 -0400 Subject: [PATCH 030/110] Working on mac fix --- package.json | 3 +-- src/main/nodejs/server.js | 15 +++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 730c4f40..a19e88ab 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "license": "UNLICENSED", "dependencies": { "mqtt": "^2.18.8", - "noble": "^1.9.0", - "noble-mac": "git://github.com/Timeular/noble-mac.git" + "noble-mac": "https://github.com/Timeular/noble-mac.git" } } diff --git a/src/main/nodejs/server.js b/src/main/nodejs/server.js index 3c538f2a..7aa1119f 100644 --- a/src/main/nodejs/server.js +++ b/src/main/nodejs/server.js @@ -1,5 +1,5 @@ var net = require('net'); -var noble = require('noble'); +var noble = require('noble-mac'); var util = require('util'); var server = net.createServer(function(client) { @@ -42,7 +42,7 @@ var server = net.createServer(function(client) { client.write(util.format("SCAN;%s;%s;%s\n", device.id, device.advertisement.manufacturerData.toString('hex'), - new Buffer(device.advertisement.localName).toString('hex'))); + Buffer.from(device.advertisement.localName).toString('hex'))); }; noble.on('discover', discover); @@ -125,8 +125,15 @@ var server = net.createServer(function(client) { default: if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { - var vehicle = noble._peripherals[command[0]]; - vehicle.writer.write(new Buffer(command[1], 'hex')); + var vehicle = noble._peripherals[command[0]]; + var buffer = Buffer.from(command[1]); + buffer.toString('hex'); + //buffer.toString('hex'); + vehicle.writer.on('data', function(buffer, true) { + console.log(data.readUInt8(0)); + }); + //vehicle.writer.subscribe(); + vehicle.writer.write(buffer, true); } } }); From 31eb4290d22795fd6fd01041d7f42b73ee59f8ca Mon Sep 17 00:00:00 2001 From: dmyrdek Date: Mon, 6 May 2019 10:04:03 -0400 Subject: [PATCH 031/110] fixed for macOS --- src/main/nodejs/server.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/nodejs/server.js b/src/main/nodejs/server.js index 7aa1119f..4a2620b1 100644 --- a/src/main/nodejs/server.js +++ b/src/main/nodejs/server.js @@ -126,13 +126,8 @@ var server = net.createServer(function(client) { default: if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { var vehicle = noble._peripherals[command[0]]; - var buffer = Buffer.from(command[1]); - buffer.toString('hex'); + var buffer = Buffer.from(command[1], 'hex'); //buffer.toString('hex'); - vehicle.writer.on('data', function(buffer, true) { - console.log(data.readUInt8(0)); - }); - //vehicle.writer.subscribe(); vehicle.writer.write(buffer, true); } } From 286639d426e71efd5abb8b890acbe558c370249e Mon Sep 17 00:00:00 2001 From: dmyrdek Date: Wed, 8 May 2019 10:31:12 -0400 Subject: [PATCH 032/110] Updated README.md --- README.md | 15 +++++++++++++++ package-lock.json | 22 ++++++++-------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index dff99633..99ac99e6 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,24 @@ To install the SDK and all required dependencies run the following commands: ``` git clone https://github.com/adessoAG/anki-drive-java cd anki-drive-java +npm install ./gradlew build ``` +### On MacOS + +If you get a "node-pre-gyp build fail error" when running npm install run: +``` +rm -rf node_modules/ +npm install --build-from-resource +``` + +Once connected, if your cars time out follow these steps: +1. Stop the server +2. From the Mac desktop, hold down the Shift+Option keys and then click on the Bluetooth menu item to reveal the hidden Debug menu +3. Select “Reset the Bluetooth module” from the Debug menu list +4. Once finished reboot your Mac + ### On Linux Optional Dependency node-usb will not be installed. So, run: diff --git a/package-lock.json b/package-lock.json index c21c472e..efe0f6de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,12 +38,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "bindings": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", - "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==", - "optional": true - }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -553,9 +547,9 @@ "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" }, "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha1-V042Dk2VSrFpZuwQLAwEn9lhoJk=", + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", "optional": true }, "napi-thread-safe-callback": { @@ -595,7 +589,7 @@ } }, "noble-mac": { - "version": "git://github.com/Timeular/noble-mac.git#3b9d1dc7ee46181f3d0e7ff3a66246a74802495c", + "version": "git://github.com/Timeular/noble-mac.git#363a2811867cdef48633988925767e45d38405bf", "from": "git://github.com/Timeular/noble-mac.git", "requires": { "cross-spawn": "^6.0.5", @@ -1011,12 +1005,12 @@ } }, "xpc-connection": { - "version": "0.1.5", - "resolved": "git://github.com/taoyuan/node-xpc-connection.git#8ff8b20e1146a1cb13cc57f9802593995014f31e", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xpc-connection/-/xpc-connection-0.1.4.tgz", + "integrity": "sha1-3Nf6oq7Gt6bhjMXdrQQvejTHcVY=", "optional": true, "requires": { - "bindings": "~1.3.0", - "nan": "^2.4.0" + "nan": "^2.0.5" } }, "xtend": { From f059ed6ab09d9f82c04f93081c424c396b9187c5 Mon Sep 17 00:00:00 2001 From: dmyrdek Date: Wed, 8 May 2019 10:41:43 -0400 Subject: [PATCH 033/110] cleaned up server.js and README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 99ac99e6..63962879 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ npm install ### On MacOS +Prerequisites for macOS: +- Node.js v6.14.2 or later. +- macOS 10.7 or later + If you get a "node-pre-gyp build fail error" when running npm install run: ``` rm -rf node_modules/ From 8dab20daecdfd576b033c9ef5a37e45be96fa9ed Mon Sep 17 00:00:00 2001 From: dmyrdek Date: Wed, 8 May 2019 10:43:15 -0400 Subject: [PATCH 034/110] cleaned up server.js and README.md --- src/main/nodejs/server.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/nodejs/server.js b/src/main/nodejs/server.js index 4a2620b1..80844194 100644 --- a/src/main/nodejs/server.js +++ b/src/main/nodejs/server.js @@ -127,8 +127,7 @@ var server = net.createServer(function(client) { if (command.length == 2 && noble._peripherals[command[0]] !== undefined) { var vehicle = noble._peripherals[command[0]]; var buffer = Buffer.from(command[1], 'hex'); - //buffer.toString('hex'); - vehicle.writer.write(buffer, true); + vehicle.writer.write(buffer, true); } } }); From e742674b48662552af5472072a3abc8d57af1c5d Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 17 Jun 2019 10:33:03 -0400 Subject: [PATCH 035/110] Corrected ID of vehicle "Guardian" from 0x0d to 0x0c. Added new vehicles: - Fast & Furious Edition Ice Charger - Fast & Furious Edition Intl. MXT - NUKE Phantom - X52 Supertruck - unknown model 0x0d Added support in Vehicle.java to handle unknown models. Added edu.oswego.cs.CPSLab.anki.FourWayStop.* for CSC436 Spring 2019 semester project. --- src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java index 52fc1359..5fabd313 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java @@ -30,7 +30,7 @@ public class AnkiConnectionTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("interplexus.local", 5000); + AnkiConnector anki = new AnkiConnector("localhost", 5000); System.out.print("...looking for cars..."); List vehicles = anki.findVehicles(); From 9bc4dfc81001456b0369bffa0ae9bb9698765f6c Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 20 Aug 2019 10:43:17 -0400 Subject: [PATCH 036/110] updated license and readme. --- LICENSE | 1 + README.md | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6dc2e9ef..6529ade6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 adesso AG +Copyright (c) 2018-2019 Bastian Tenbergen, The State University of New York at Oswego Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 63962879..9f8ad1c8 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ The authors of this software are in no way affiliated to Anki. All naming rights for Anki, Anki Drive and Anki Overdrive are property of [Anki](http://anki.com). +This is a forked repository from [adessoAG/anki-drive-java](https://github.com/adessoAG/anki-drive-java), which, sadly, +appears to be abandoned. We are maintaining this SDK to serve our [tenbergen/Automotive-CPS](https://github.com/tenbergen/Automotive-CPS) project. + ## About Unfortunately, there is currently no cross-platform Java library to interface @@ -32,7 +35,7 @@ To build and use the SDK in your own project you will need: To install the SDK and all required dependencies run the following commands: ``` -git clone https://github.com/adessoAG/anki-drive-java +git clone https://github.com/tenbergen/anki-drive-java cd anki-drive-java npm install ./gradlew build From 67eacadbb641f3f2cc687233add9d790c3608dcb Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 20 Aug 2019 10:46:32 -0400 Subject: [PATCH 037/110] updated license and readme. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f37d8072..f6465cc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ before_install: language: java jdk: - - oraclejdk8 + - oraclejdk9 From 2d5c56a1d8fea152362f1b82af4a8840f972feb0 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 20 Aug 2019 10:46:52 -0400 Subject: [PATCH 038/110] updated license and readme. --- jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index f2c8e517..8066364a 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,4 +11,4 @@ before_install: language: java jdk: - - oraclejdk8 + - oraclejdk9 From 2013c4393866d93302ed65abf02520b5de48a8b1 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 20 Aug 2019 11:06:03 -0400 Subject: [PATCH 039/110] added AnkiCommander SSH Test Server developed by Shakhar Dasgupta (https://github.com/sha224/anki-commander) for this project. increased OpenJDK to 9 for Travis. --- build.gradle | 23 +- .../com/shakhar/anki/commander/AnkiShell.java | 217 ++++++++++++++++++ .../com/shakhar/anki/commander/AnkiSshd.java | 25 ++ 3 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/shakhar/anki/commander/AnkiShell.java create mode 100644 src/main/java/com/shakhar/anki/commander/AnkiSshd.java diff --git a/build.gradle b/build.gradle index 1c5fa29d..8e0c7e16 100644 --- a/build.gradle +++ b/build.gradle @@ -22,13 +22,22 @@ repositories { } dependencies { - compile 'org.reflections:reflections:0.9.11' //was 0.9.10 - compile 'javax.xml.bind:jaxb-api:2.3.0' //new: + //included by adessoAG/anki-drive-java, but maybe updated + compile 'org.reflections:reflections:0.9.11' //was 0.9.10 + compile 'javax.xml.bind:jaxb-api:2.3.0' + + //new dependencies included by BT + + //dependencies for AnkiCommander (https://github.com/sha224/anki-commander) + implementation 'org.apache.sshd:sshd-core:2.2.0' + implementation 'org.jline:jline:3.10.0' + implementation 'org.slf4j:slf4j-api:1.7.26' + implementation 'org.slf4j:slf4j-simple:1.7.26' } node { - version = '8.12.0'// was '4.4.7' - npmVersion = '6.4.1'// was '3.10.5' + version = '8.12.0' //was '4.4.7' + npmVersion = '6.4.1' //was '3.10.5' distBaseUrl = 'https://nodejs.org/dist' download = true @@ -47,4 +56,10 @@ task server(type: NodeTask) { ignoreExitValue = true } +//added exec task for Anki Commander SSH test server +task ssh(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'com.shakhar.anki.commander.AnkiSshd' +} + processResources.dependsOn(['npmInstall']) diff --git a/src/main/java/com/shakhar/anki/commander/AnkiShell.java b/src/main/java/com/shakhar/anki/commander/AnkiShell.java new file mode 100644 index 00000000..f79147b9 --- /dev/null +++ b/src/main/java/com/shakhar/anki/commander/AnkiShell.java @@ -0,0 +1,217 @@ +package com.shakhar.anki.commander; + +import de.adesso.anki.AnkiConnector; +import de.adesso.anki.Vehicle; +import de.adesso.anki.messages.*; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.command.Command; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AnkiShell implements Command, Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(AnkiShell.class); + + private InputStream in; + private OutputStream out; + private OutputStream err; + private ExitCallback callback; + private Thread thread; + private Terminal terminal; + private AnkiConnector ankiConnector; + + private Map vehicleMap; + private Vehicle controlVehicle; + + @Override + public void setInputStream(InputStream in) { + this.in = in; + } + + @Override + public void setOutputStream(OutputStream out) { + this.out = out; + } + + @Override + public void setErrorStream(OutputStream err) { + this.err = err; + } + + @Override + public void setExitCallback(ExitCallback callback) { + this.callback = callback; + } + + @Override + public void start(Environment env) throws IOException { + thread = new Thread(this); + thread.start(); + } + + @Override + public void destroy() throws Exception { + if (terminal != null) + terminal.close(); + thread.interrupt(); + } + + @Override + public void run() { + try { + terminal = TerminalBuilder.builder().system(false).streams(in, out).build(); + LineReader reader = LineReaderBuilder.builder().terminal(terminal).build(); + String line; + while ((line = reader.readLine("anki>")) != null && handleInput(line)); + } catch (IOException e) { + LOGGER.error("IOException thrown", e); + } catch (UserInterruptException e) { + LOGGER.error("UserInterruptException thrown", e); + } finally { + callback.onExit(0); + } + } + + private boolean handleInput(String command) { + String[] args = command.split("\\s+"); + switch (args[0]) { + case "connect": + handleConnect(args); + break; + case "scan": + handleScan(); + break; + case "control": + handleControl(args); + break; + case "speed": + handleSpeed(args); + break; + case "turn": + handleTurn(args); + break; + case "lane": + handleLane(args); + break; + case "light": + handleLight(args); + break; + case "help": + handleHelp(); + break; + case "exit": + handleExit(); + return false; + default: + write("Unknown command"); + } + return true; + } + + private void write(String s) { + terminal.writer().println(s); + terminal.writer().flush(); + } + + private void handleConnect(String[] args) { + try { + String host = "localhost"; + int port = 5000; + if (args.length >= 2) + host = args[1]; + if (args.length >= 3) + port = Integer.parseInt(args[2]); + ankiConnector = new AnkiConnector(host, port); + } catch (IOException e) { + e.printStackTrace(terminal.writer()); + } + vehicleMap = new HashMap<>(); + } + + private void handleScan() { + vehicleMap.clear(); + List vehicles = ankiConnector.findVehicles(); + if (vehicles.isEmpty()) + write("No Vehicles Found."); + else { + write("Found " + vehicles.size() + " vehicle(s):"); + for (Vehicle vehicle : vehicles) { + vehicleMap.put(vehicle.getAddress(), vehicle); + write(vehicle.getAddress() + ": " + vehicle.getAdvertisement()); + } + } + } + + private void handleControl(String[] args) { + if (controlVehicle != null) + controlVehicle.disconnect(); + controlVehicle = vehicleMap.get(args[1]); + if (controlVehicle != null) { + controlVehicle.connect(); + controlVehicle.sendMessage(new SdkModeMessage()); + } + } + + private void handleSpeed(String[] args) { + int speed = Integer.parseInt(args[1]); + int acceleration = Integer.parseInt(args[2]); + controlVehicle.sendMessage(new SetSpeedMessage(speed, acceleration)); + } + + private void handleTurn(String[] args) { + int turnType = Integer.parseInt(args[1]); + int trigger = Integer.parseInt(args[2]); + controlVehicle.sendMessage(new TurnMessage(turnType, trigger)); + } + + private void handleLane(String[] args) { + int offsetFromCenter = Integer.parseInt(args[1]); + int horizontalSpeed = Integer.parseInt(args[2]); + int horizontalAcceleration = Integer.parseInt(args[3]); + controlVehicle.sendMessage(new ChangeLaneMessage(offsetFromCenter, horizontalSpeed, horizontalAcceleration)); + } + + private void handleLight(String[] args) { + LightsPatternMessage message = new LightsPatternMessage(); + for (int i = 1; i < args.length; i++) { + String[] config = args[i].split(","); + LightsPatternMessage.LightChannel channel = LightsPatternMessage.LightChannel.valueOf(config[0]); + LightsPatternMessage.LightEffect effect = LightsPatternMessage.LightEffect.valueOf(config[1]); + int start = Integer.parseInt(config[2]); + int end = Integer.parseInt(config[3]); + int cycles = Integer.parseInt(config[4]); + message.add(new LightsPatternMessage.LightConfig(channel, effect, start, end, cycles)); + } + controlVehicle.sendMessage(message); + } + + private void handleHelp() { + write("connect - Connects to the Anki Server. This should always be the first command. If host and port are not specified, default values of localhost and 5000 are used.\n\n" + + "scan - Scans for all the available vehicles and prints out information about them. This should always be the second command after connect.\n\n" + + "control
- Connects to the vehicle with the specified address. The address can be found from the output of scan.\n\n" + + "speed - Changes speed of the connected vehicle.\n\n" + + "turn - Turns the connected vehicle. For a u-turn, use turn_type 3 and trigger 0 or 1.\n\n" + + "lane - Makes the connected vehicle do a lane change to the specified offset.\n\n" + + "light - Sets the lights of the connected vehicle using the specified configs. Each config is a comma-separated list of the format: ,,,,. Possible values of channels are ENGINE_RED, TAIL, ENGINE_BLUE, ENGINE_GREEN, FRONT_RED, FRONT_GREEN. Possible values of effects are STEADY, FADE, THROB, FLASH, STROBE."); + } + + private void handleExit() { + if (controlVehicle != null) + controlVehicle.disconnect(); + if (ankiConnector != null) + ankiConnector.close(); + } +} diff --git a/src/main/java/com/shakhar/anki/commander/AnkiSshd.java b/src/main/java/com/shakhar/anki/commander/AnkiSshd.java new file mode 100644 index 00000000..be3252bc --- /dev/null +++ b/src/main/java/com/shakhar/anki/commander/AnkiSshd.java @@ -0,0 +1,25 @@ +package com.shakhar.anki.commander; + +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Paths; + +public class AnkiSshd { + + private static final Logger LOGGER = LoggerFactory.getLogger(AnkiSshd.class); + + public static void main(String[] args) throws IOException, InterruptedException { + SshServer sshd = SshServer.setUpDefaultServer(); + sshd.setPort(6000); + sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(Paths.get("hostkey.ser"))); + sshd.setPasswordAuthenticator(((username, password, session) -> password.equals(username + "pass"))); + sshd.setShellFactory(() -> new AnkiShell()); + sshd.start(); + LOGGER.info("SSH Server started"); + Thread.sleep(Long.MAX_VALUE); + } +} From 7aebad1dc707e726700d4db26c4cfbfe07280532 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Sun, 22 Sep 2019 10:40:14 -0400 Subject: [PATCH 040/110] added getter for AnkiConnector to Vehicle.java as it's needed by project dependency --- src/main/java/de/adesso/anki/Vehicle.java | 2 ++ src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/adesso/anki/Vehicle.java b/src/main/java/de/adesso/anki/Vehicle.java index c000de37..227581c2 100644 --- a/src/main/java/de/adesso/anki/Vehicle.java +++ b/src/main/java/de/adesso/anki/Vehicle.java @@ -20,6 +20,8 @@ public class Vehicle { private AdvertisementData advertisement; private AnkiConnector anki; + + private AnkiConnector getAnkiConnector() { return this.anki; } private Multimap, MessageListener> listeners; private MessageListener defaultListener; diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java index 5fabd313..f212dec6 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java @@ -30,7 +30,7 @@ public class AnkiConnectionTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("localhost", 5000); + AnkiConnector anki = new AnkiConnector("192.168.1.100", 5000); System.out.print("...looking for cars..."); List vehicles = anki.findVehicles(); From 8b8de9029678cfc606353e20f2ebf78e4d29e927 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 23 Sep 2019 11:50:18 -0400 Subject: [PATCH 041/110] added getter for AnkiConnector to Vehicle.java as it's needed by project dependency. and now its even public! --- src/main/java/de/adesso/anki/Vehicle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/adesso/anki/Vehicle.java b/src/main/java/de/adesso/anki/Vehicle.java index 227581c2..f9008e45 100644 --- a/src/main/java/de/adesso/anki/Vehicle.java +++ b/src/main/java/de/adesso/anki/Vehicle.java @@ -21,7 +21,7 @@ public class Vehicle { private AnkiConnector anki; - private AnkiConnector getAnkiConnector() { return this.anki; } + public AnkiConnector getAnkiConnector() { return this.anki; } private Multimap, MessageListener> listeners; private MessageListener defaultListener; From 9692e15b2a938c0ac772f81fb62b90e093f1f59f Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 12:00:47 -0400 Subject: [PATCH 042/110] added getter for AnkiConnector to Vehicle.java as it's needed by project dependency. and now its even public! --- pom.xml | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..40f16e94 --- /dev/null +++ b/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + + com.github.jitpack + maven-simple + SNAPSHOT + jar + + Simple Maven example + https://jitpack.io/#jitpack/maven-simple/0.1 + + + + junit + junit + 4.10 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + 1.7 + 1.7 + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + \ No newline at end of file From 685c6efbf9fdaaac63e2fa9019eb7668e48612f8 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 13:39:04 -0400 Subject: [PATCH 043/110] fix some new issues with jitpack --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 8e0c7e16..df550fdc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,7 @@ buildscript { + dependencies { + classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' // Add this line + } repositories { jcenter() maven { @@ -14,6 +17,7 @@ plugins { apply plugin: 'java' apply plugin: 'application' apply plugin: 'maven' +apply plugin: 'com.github.dcendents.android-maven' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' From 4730d61cd86365fb9b92545f6302f4f8e516a7c6 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 13:39:18 -0400 Subject: [PATCH 044/110] fix some new issues with jitpack --- Anki Drive Programming Guide.pdf | Bin 0 -> 105031 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Anki Drive Programming Guide.pdf diff --git a/Anki Drive Programming Guide.pdf b/Anki Drive Programming Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..427ccb6e91c3e3b66eea32d728ca9de867410fa1 GIT binary patch literal 105031 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 CZCQ8# literal 0 HcmV?d00001 From 65c9e7d889dbbc9814d410e539d7e98467eb73cf Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 13:44:42 -0400 Subject: [PATCH 045/110] fix some new issues with jitpack --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index df550fdc..e17abd39 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' // Add this line + classpath 'com.github.dcendents:android-maven-gradle-plugin:4.6' // Add this line } repositories { jcenter() From 73f4b45069517231485e5ad78bba23f927a37ef4 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 13:47:57 -0400 Subject: [PATCH 046/110] fix some new issues with jitpack --- build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/build.gradle b/build.gradle index e17abd39..c141b0a0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,4 @@ buildscript { - dependencies { - classpath 'com.github.dcendents:android-maven-gradle-plugin:4.6' // Add this line - } repositories { jcenter() maven { From 2503c7566822bf3da0a8b170c7f7c56f8aeddbc0 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 13:50:45 -0400 Subject: [PATCH 047/110] fix some new issues with jitpack --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c141b0a0..b9947df2 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ plugins { apply plugin: 'java' apply plugin: 'application' apply plugin: 'maven' -apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'android-maven' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' From a5ab8e87dbf0a482b6b92ba98d8fb44375f595f1 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 13:53:46 -0400 Subject: [PATCH 048/110] fix some new issues with jitpack --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index b9947df2..8e0c7e16 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,6 @@ plugins { apply plugin: 'java' apply plugin: 'application' apply plugin: 'maven' -apply plugin: 'android-maven' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' From 82f58ac7167f59440836101b683f306bfa14aca3 Mon Sep 17 00:00:00 2001 From: "B. Tenbergen" Date: Mon, 7 Oct 2019 14:18:55 -0400 Subject: [PATCH 049/110] fix some new issues with jitpack --- pom.xml | 63 ------------------------------------------------- settings.gradle | 1 + 2 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 pom.xml create mode 100644 settings.gradle diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 40f16e94..00000000 --- a/pom.xml +++ /dev/null @@ -1,63 +0,0 @@ - - 4.0.0 - - com.github.jitpack - maven-simple - SNAPSHOT - jar - - Simple Maven example - https://jitpack.io/#jitpack/maven-simple/0.1 - - - - junit - junit - 4.10 - test - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.2 - - 1.7 - 1.7 - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - - - \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..3732968d --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':library' \ No newline at end of file From 96feed18374b6aa46f75691cc4f304bcdce493c1 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Wed, 29 Apr 2020 15:47:03 -0400 Subject: [PATCH 050/110] added test command to gradle build --- README.md | 8 ++++++-- build.gradle | 6 ++++++ src/main/java/de/adesso/anki/Vehicle.java | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9f8ad1c8..e74cd97a 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,13 @@ List vehicles = anki.findVehicles(); ``` ### Test File -To try a connection, start the server and run the class: +To try a connection, start the server and run: +``` +./gradlew test +``` +which will execute ```java -edu.oswego.cs.CPSLab.anki.AnkiConnectionTest +edu.oswego.cs.CPSLab.AnkiConnectionTest ``` ## Contributing diff --git a/build.gradle b/build.gradle index 8e0c7e16..b9d88630 100644 --- a/build.gradle +++ b/build.gradle @@ -62,4 +62,10 @@ task ssh(type: JavaExec) { main = 'com.shakhar.anki.commander.AnkiSshd' } +//added exec task for AnkiConnection test program +task ssh(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'edu.oswego.cs.CPSLab' +} + processResources.dependsOn(['npmInstall']) diff --git a/src/main/java/de/adesso/anki/Vehicle.java b/src/main/java/de/adesso/anki/Vehicle.java index f9008e45..08c95c8f 100644 --- a/src/main/java/de/adesso/anki/Vehicle.java +++ b/src/main/java/de/adesso/anki/Vehicle.java @@ -109,7 +109,7 @@ private void fireMessageReceived(T message) { } } } - + public Vehicle(AnkiConnector anki, String address, String manufacturerData, String localName) { try { this.anki = new AnkiConnector(anki); From 87f65a241fde72139eda3ba0c53fe935e75b2167 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Wed, 29 Apr 2020 15:50:47 -0400 Subject: [PATCH 051/110] added test command to gradle build --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index b9d88630..af8a1fc4 100644 --- a/build.gradle +++ b/build.gradle @@ -63,9 +63,9 @@ task ssh(type: JavaExec) { } //added exec task for AnkiConnection test program -task ssh(type: JavaExec) { +task test(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath - main = 'edu.oswego.cs.CPSLab' + main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } processResources.dependsOn(['npmInstall']) From e8cabf1cae72dc74aacddcf2e9ddf0c050e769d7 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Wed, 29 Apr 2020 15:54:37 -0400 Subject: [PATCH 052/110] added test command to gradle build --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e74cd97a..e592ebe4 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ List vehicles = anki.findVehicles(); ### Test File To try a connection, start the server and run: ``` -./gradlew test +./gradlew ankiConnectionTest ``` which will execute ```java diff --git a/build.gradle b/build.gradle index af8a1fc4..766d3b11 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ task ssh(type: JavaExec) { } //added exec task for AnkiConnection test program -task test(type: JavaExec) { +task ankiConnectionTest(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } From 7daa6adddec875b536e902af813be5705029c0da Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Wed, 29 Apr 2020 16:00:00 -0400 Subject: [PATCH 053/110] added test command to gradle build. removed unnecessary interfaces. --- src/main/java/META-INF/MANIFEST.MF | 2 +- .../CPSLab/{anki => }/AnkiConnectionTest.java | 2 +- .../anki/FourWayStop/IntersectionHandler.java | 19 ---------------- .../CPSLab/anki/FourWayStop/VehicleInfo.java | 22 ------------------- 4 files changed, 2 insertions(+), 43 deletions(-) rename src/main/java/edu/oswego/cs/CPSLab/{anki => }/AnkiConnectionTest.java (99%) delete mode 100644 src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java delete mode 100644 src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF index 80f215be..cf9f44d6 100644 --- a/src/main/java/META-INF/MANIFEST.MF +++ b/src/main/java/META-INF/MANIFEST.MF @@ -1,3 +1,3 @@ Manifest-Version: 1.0 -Main-Class: edu.oswego.cs.CPSLab.anki.AnkiConnectionTest +Main-Class: edu.oswego.cs.CPSLab.AnkiConnectionTest diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java similarity index 99% rename from src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java rename to src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index f212dec6..5fe7ba17 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/anki/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -1,4 +1,4 @@ -package edu.oswego.cs.CPSLab.anki; +package edu.oswego.cs.CPSLab; import de.adesso.anki.AnkiConnector; import de.adesso.anki.MessageListener; diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java b/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java deleted file mode 100644 index f1bd027e..00000000 --- a/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/IntersectionHandler.java +++ /dev/null @@ -1,19 +0,0 @@ -package edu.oswego.cs.CPSLab.anki.FourWayStop; - -import de.adesso.anki.AdvertisementData; -import de.adesso.anki.Vehicle; - -import java.util.Queue; - -/** - * An interface to handle communication between cyber-physical vehicles. - * @author Shakhar Dasgupta - */ -public interface IntersectionHandler { - void awaitClearIntersection(); - void broadcast(AdvertisementData vehicleInfo); - void clearIntersection(); - void notify(Vehicle target, Queue queue); - void listenToBroadcast(); - void becomeMaster(); -} \ No newline at end of file diff --git a/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java b/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java deleted file mode 100644 index 0f696b0e..00000000 --- a/src/main/java/edu/oswego/cs/CPSLab/anki/FourWayStop/VehicleInfo.java +++ /dev/null @@ -1,22 +0,0 @@ -package edu.oswego.cs.CPSLab.anki.FourWayStop; - -import java.time.Instant; -import java.util.Queue; -import java.io.Serializable; - -/** - * A class to exchange information about cyber-physical vehicles. - * @author Benjamin Groman - */ -public class VehicleInfo implements Serializable { - private static final long serialVersionUID = 436L; - public int locationID; - public float offsetFromRoadCenter; - public int speed; - public Instant timestamp; - public String MACid; - public int roadPieceID; - public boolean isClear; - public boolean isMaster; - public Queue otherVehicles;//should be null if not master -} \ No newline at end of file From f02eca2e46f8931f085ae4645bc04c8d09c2c0b2 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Fri, 8 May 2020 18:28:35 -0400 Subject: [PATCH 054/110] Cleaned AnkiConnectionTest; make the cars move to the next finish line. --- package-lock.json | 383 ++++++++++++++---- .../java/de/adesso/anki/AnkiConnector.java | 10 + src/main/java/de/adesso/anki/Model.java | 2 +- src/main/java/de/adesso/anki/Vehicle.java | 2 +- .../oswego/cs/CPSLab/AnkiConnectionTest.java | 179 ++++---- 5 files changed, 433 insertions(+), 143 deletions(-) diff --git a/package-lock.json b/package-lock.json index efe0f6de..4326982a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,21 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "optional": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -73,6 +88,16 @@ "concat-map": "0.0.1" } }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "optional": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -88,9 +113,9 @@ } }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "code-point-at": { "version": "1.1.0", @@ -160,6 +185,15 @@ "ms": "0.7.1" } }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -257,17 +291,35 @@ "es5-ext": "~0.10.14" } }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "requires": { - "minipass": "^2.2.1" + "minipass": "^2.6.0" } }, "fs.realpath": { @@ -290,6 +342,12 @@ "wide-align": "^1.1.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "optional": true + }, "glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", @@ -353,10 +411,16 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "optional": true + }, "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "requires": { "minimatch": "^3.0.4" } @@ -464,6 +528,12 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-1.0.2.tgz", "integrity": "sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=" }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -478,37 +548,43 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "minipass": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", - "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "requires": { - "minipass": "^2.2.1" + "minipass": "^2.9.0" } }, "mkdirp": { - "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" }, "dependencies": { "minimist": { - "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } }, + "mkdirp-classic": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz", + "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==", + "optional": true + }, "mqtt": { "version": "2.18.8", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.8.tgz", @@ -547,9 +623,15 @@ "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" }, "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "optional": true + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", "optional": true }, "napi-thread-safe-callback": { @@ -558,13 +640,28 @@ "integrity": "sha512-X7uHCOCdY4u0yamDxDrv3jF2NtYc8A1nvPzBQgvpoSX+WB3jAe2cVNsY448V1ucq7Whf9Wdy02HEUoLW5rJKWg==" }, "needle": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.3.tgz", - "integrity": "sha512-GPL22d/U9cai87FcCPO6e+MT3vyHS2j+zwotakDc7kE2DtUAqFKMXLJCTtRp+5S75vXIwQPvIxkvlctxf9q4gQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz", + "integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==", "requires": { - "debug": "^2.1.2", + "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "next-tick": { @@ -589,8 +686,8 @@ } }, "noble-mac": { - "version": "git://github.com/Timeular/noble-mac.git#363a2811867cdef48633988925767e45d38405bf", - "from": "git://github.com/Timeular/noble-mac.git", + "version": "git+https://github.com/Timeular/noble-mac.git#b446a668a8821a8690512a0f2a18181c5ae4d350", + "from": "git+https://github.com/Timeular/noble-mac.git", "requires": { "cross-spawn": "^6.0.5", "napi-thread-safe-callback": "0.0.6", @@ -599,10 +696,19 @@ "node-pre-gyp": "^0.10.0" } }, + "node-abi": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.16.0.tgz", + "integrity": "sha512-+sa0XNlWDA6T+bDLmkCUYn6W5k5W6BPRL6mqzSCs6H/xUgtl4D5x2fORKDzopKiU6wsyn/+wXlRXwXeSp+mtoA==", + "optional": true, + "requires": { + "semver": "^5.4.1" + } + }, "node-addon-api": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.6.2.tgz", - "integrity": "sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA==" + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz", + "integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==" }, "node-pre-gyp": { "version": "0.10.3", @@ -621,27 +727,42 @@ "tar": "^4" } }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", + "optional": true + }, "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "requires": { "abbrev": "1", "osenv": "^0.1.4" } }, "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-packlist": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", - "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "requires": { "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" } }, "npmlog": { @@ -715,6 +836,29 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, + "prebuild-install": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", + "integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==", + "optional": true, + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + } + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -786,11 +930,11 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { @@ -809,9 +953,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "set-blocking": { "version": "2.0.0", @@ -832,9 +976,26 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "optional": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "split2": { "version": "2.2.0", @@ -881,17 +1042,73 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "tar": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", - "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.3", - "minizlib": "^1.1.0", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "yallist": "^3.0.3" + } + }, + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "optional": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "optional": true, + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "optional": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "optional": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "through2": { @@ -921,6 +1138,15 @@ "is-negated-glob": "^1.0.0" } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -946,13 +1172,22 @@ } }, "usb": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/usb/-/usb-1.3.3.tgz", - "integrity": "sha512-WRBxI54yEs2QPj28G6kITI3Wu7VxrtHbqiDvDRUDKdg97lcK1pTP8y9LoDWF22OiCCrEvrdeq0lNcr84QOzjXQ==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/usb/-/usb-1.6.3.tgz", + "integrity": "sha512-23KYMjaWydACd8wgGKMQ4MNwFspAT6Xeim4/9Onqe5Rz/nMb4TM/WHL+qPT0KNFxzNKzAs63n1xQWGEtgaQ2uw==", "optional": true, "requires": { - "nan": "^2.8.0", - "node-pre-gyp": "^0.10.0" + "bindings": "^1.4.0", + "nan": "2.13.2", + "prebuild-install": "^5.3.3" + }, + "dependencies": { + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "optional": true + } } }, "util-deprecate": { @@ -981,6 +1216,12 @@ "isexe": "^2.0.0" } }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "optional": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -1019,9 +1260,9 @@ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } } diff --git a/src/main/java/de/adesso/anki/AnkiConnector.java b/src/main/java/de/adesso/anki/AnkiConnector.java index 28b2d4e5..da70438f 100644 --- a/src/main/java/de/adesso/anki/AnkiConnector.java +++ b/src/main/java/de/adesso/anki/AnkiConnector.java @@ -21,6 +21,8 @@ @SuppressWarnings("rawtypes") public class AnkiConnector { + private boolean debug = false; + private Socket socket; private final String host; private final int port; @@ -50,6 +52,14 @@ public AnkiConnector(AnkiConnector anki) throws IOException { this(anki.host, anki.port); } + public void toggleDebugMode() { + this.debug = !this.debug; + } + + public boolean getDebugMode() { + return this.debug; + } + public synchronized List findVehicles() { boolean retry = false; List foundVehicles = new ArrayList<>(); diff --git a/src/main/java/de/adesso/anki/Model.java b/src/main/java/de/adesso/anki/Model.java index ae30f63a..2437d406 100644 --- a/src/main/java/de/adesso/anki/Model.java +++ b/src/main/java/de/adesso/anki/Model.java @@ -25,7 +25,7 @@ public enum Model { THERMO(0x0a, "#a11c20"), NUKE(0x0b, "#bed62f"), GUARDIAN(0x0c, "#42b1d7"), //BT update on 3/31/19 to correct ID of model "Guardian" - __SOMECAR2(0x0d), //BT update on 3/31/19 to add ID of unknown model + __SOMECAR2(0x0d), //BT update on 3/31/19 to add ID of unknown model BIGBANG(0x0e, "#4e674d"), FREEHWEEL(0x0f, "#25bc00"), //BT update on 9/14/18 to add new supertruck Freewheel X52(0x10, "#990909"), //BT update on 3/31/19 to add new supertruck X52 diff --git a/src/main/java/de/adesso/anki/Vehicle.java b/src/main/java/de/adesso/anki/Vehicle.java index 08c95c8f..b80efd3a 100644 --- a/src/main/java/de/adesso/anki/Vehicle.java +++ b/src/main/java/de/adesso/anki/Vehicle.java @@ -78,7 +78,7 @@ public void disconnect() { public void sendMessage(Message message) { anki.sendMessage(this, message); - System.out.println(String.format("[%s] > %s: %s", LocalTime.now(), this, message)); + if (anki.getDebugMode()) System.out.println(String.format("[%s] > %s: %s", LocalTime.now(), this, message)); } @Deprecated diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index 5fe7ba17..f0f5b47e 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -3,14 +3,10 @@ import de.adesso.anki.AnkiConnector; import de.adesso.anki.MessageListener; import de.adesso.anki.Vehicle; -import de.adesso.anki.messages.BatteryLevelRequestMessage; -import de.adesso.anki.messages.BatteryLevelResponseMessage; -import de.adesso.anki.messages.LightsPatternMessage; +import de.adesso.anki.messages.*; import de.adesso.anki.messages.LightsPatternMessage.LightConfig; -import de.adesso.anki.messages.PingRequestMessage; -import de.adesso.anki.messages.PingResponseMessage; -import de.adesso.anki.messages.SdkModeMessage; -import de.adesso.anki.messages.SetSpeedMessage; +import de.adesso.anki.roadmap.roadpieces.FinishRoadpiece; + import java.io.IOException; import java.util.Iterator; import java.util.List; @@ -19,105 +15,148 @@ * A simple test program to test a connection to your Anki 'Supercars' and 'Supertrucks' using the NodeJS Bluetooth gateway. * Simple follow the installation instructions at http://github.com/adessoAG/anki-drive-java, build this project, start the * bluetooth gateway using ./gradlew server, and run this class. - * + * * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) */ -public class AnkiConnectionTest { +public class AnkiConnectionTest implements Runnable { - static long pingReceivedAt; - static long pingSentAt; - public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("192.168.1.100", 5000); + AnkiConnector anki = new AnkiConnector("192.168.1.62", 5000); System.out.print("...looking for cars..."); List vehicles = anki.findVehicles(); if (vehicles.isEmpty()) { System.out.println(" NO CARS FOUND. I guess that means we're done."); - + } else { - System.out.println(" FOUND " + vehicles.size() + " CARS! They are:"); - - Iterator iter = vehicles.iterator(); - while (iter.hasNext()) { - Vehicle v = iter.next(); - System.out.println(" " + v); - System.out.println(" ID: " + v.getAdvertisement().getIdentifier()); - System.out.println(" Model: " + v.getAdvertisement().getModel()); - System.out.println(" Model ID: " + v.getAdvertisement().getModelId()); - System.out.println(" Product ID: " + v.getAdvertisement().getProductId()); - System.out.println(" Address: " + v.getAddress()); - System.out.println(" Color: " + v.getColor()); - System.out.println(" charging? " + v.getAdvertisement().isCharging()); - } - + System.out.println(" FOUND " + vehicles.size() + " CARS!"); System.out.println("\nNow connecting to and doing stuff to your cars.\n\n"); - iter = vehicles.iterator(); + Iterator iter = vehicles.iterator(); while (iter.hasNext()) { Vehicle v = iter.next(); - System.out.println("\nConnecting to " + v + " @ " + v.getAddress()); - v.connect(); - System.out.print(" Connected. Setting SDK mode..."); //always set the SDK mode FIRST! - v.sendMessage(new SdkModeMessage()); - System.out.println(" SDK Mode set."); - - System.out.println(" Sending asynchronous Battery Level Request. The Response will come in eventually."); - //we have to set up a response handler first, in order to handle async responses - BatteryLevelResponseHandler blrh = new BatteryLevelResponseHandler(); - //now we tell the car, who is listenening to the replies - v.addMessageListener(BatteryLevelResponseMessage.class, blrh); - //now we can actually send it. - v.sendMessage(new BatteryLevelRequestMessage()); - - System.out.println(" Sending Ping Request..."); - //again, some async set-up required... - PingResponseHandler prh = new PingResponseHandler(); - v.addMessageListener(PingResponseMessage.class, prh); - AnkiConnectionTest.pingSentAt = System.currentTimeMillis(); - v.sendMessage(new PingRequestMessage()); - - System.out.println(" Flashing lights..."); - LightConfig lc = new LightConfig(LightsPatternMessage.LightChannel.TAIL, LightsPatternMessage.LightEffect.STROBE, 0, 0, 0); - LightsPatternMessage lpm = new LightsPatternMessage(); - lpm.add(lc); - v.sendMessage(lpm); - System.out.println(" Setting Speed..."); - v.sendMessage(new SetSpeedMessage(500, 100)); - //Thread.sleep(1000); - //gs.sendMessage(new TurnMessage()); - System.out.print("Sleeping for 10secs... "); - Thread.sleep(10000); - v.disconnect(); - System.out.println("disconnected from " + v + "\n"); + //Thread ct = new Thread(new AnkiConnectionTest((v))); + //ct.run(); + AnkiConnectionTest act = new AnkiConnectionTest(v); + act.run(); } } anki.close(); - System.exit(0); + } + + private Vehicle v; + + public AnkiConnectionTest(Vehicle v) { + this.v = v; + } + + @Override + public void run() { + System.out.println("\nConnecting to " + v + " @ " + v.getAddress()); + v.connect(); + System.out.println("Vehicle Advertisement Data:"); + System.out.println(" " + v); + System.out.println(" ID: " + v.getAdvertisement().getIdentifier()); + System.out.println(" Model: " + v.getAdvertisement().getModel()); + System.out.println(" Model ID: " + v.getAdvertisement().getModelId()); + System.out.println(" Product ID: " + v.getAdvertisement().getProductId()); + System.out.println(" Address: " + v.getAddress()); + System.out.println(" Color: " + v.getColor()); + System.out.println(" charging? " + v.getAdvertisement().isCharging()); + + + System.out.print(" Connected. Setting SDK mode..."); //always set the SDK mode FIRST! + v.sendMessage(new SdkModeMessage()); + System.out.println(" SDK Mode set."); + + System.out.println(" Sending asynchronous Battery Level Request. The Response will come in eventually."); + //we have to set up a response handler first, in order to handle async responses + BatteryLevelResponseHandler blrh = new BatteryLevelResponseHandler(); + //now we tell the car, who is listening to the replies + v.addMessageListener(BatteryLevelResponseMessage.class, blrh); + //now we can actually send it. + v.sendMessage(new BatteryLevelRequestMessage()); + + System.out.println(" Sending Ping Request..."); + //again, some async set-up required... + PingResponseHandler prh = new PingResponseHandler(); + v.addMessageListener(PingResponseMessage.class, prh); + prh.pingSentAt = System.currentTimeMillis(); + v.sendMessage(new PingRequestMessage()); + + System.out.println(" Flashing lights..."); + //Lights require configurations. So first construct a lights configuration, + //then add it to the lights pattern message, then send the message. No handler required. + LightConfig lc = new LightConfig(LightsPatternMessage.LightChannel.TAIL, LightsPatternMessage.LightEffect.STROBE, 0, 0, 0); + LightsPatternMessage lpm = new LightsPatternMessage(); + lpm.add(lc); + v.sendMessage(lpm); + + System.out.println(" Setting Speed..."); + //Speed is easy. Just tell the car how fast to go and how quickly to accelerate. + v.sendMessage(new SetSpeedMessage(500, 100)); + + System.out.println(" Looking for finish line..."); + //Use the sensor on the bottom to check the road pieces. This is like a response/request, but will + //update whenever there's a new value. + FinishLineDetector fld = new FinishLineDetector(); + v.addMessageListener(LocalizationPositionUpdateMessage.class, fld); + v.sendMessage(new LocalizationPositionUpdateMessage()); + while (!fld.stop) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // System.out.print("continue"); + // System.out.print(" Looking for finish line..."); + } + // v.disconnect(); + System.out.println("disconnected from " + v + "\n"); + } + + /** + * Handles the response from the vehicle on which road piece the vehicle is + * and sets a stop flag to true if it's the finish line. + */ + private class FinishLineDetector implements MessageListener { + + private int finishLineId = FinishRoadpiece.ROADPIECE_IDS[0]; + private boolean stop = false; + + @Override + public void messageReceived(LocalizationPositionUpdateMessage m) { + // System.out.println(m.toString()); + if (m.getRoadPieceId() == finishLineId) stop = true; + } } /** * Handles the response from the vehicle from the BatteryLevelRequestMessage. * We need handler classes because responses from the vehicles are asynchronous. */ - private static class BatteryLevelResponseHandler implements MessageListener { + private class BatteryLevelResponseHandler implements MessageListener { + private int batt_level; @Override public void messageReceived(BatteryLevelResponseMessage m) { System.out.println(" Battery Level is: " + m.getBatteryLevel() + " mV"); } } - + /** * Handles the response from the vehicle from the PingRequestMessage. * We need handler classes because responses from the vehicles are asynchronous. */ - private static class PingResponseHandler implements MessageListener { + private class PingResponseHandler implements MessageListener { + private long pingReceivedAt; + private long pingSentAt; + @Override public void messageReceived(PingResponseMessage m) { - AnkiConnectionTest.pingReceivedAt = System.currentTimeMillis(); - System.out.println(" Ping response received. Roundtrip: " + (AnkiConnectionTest.pingReceivedAt - AnkiConnectionTest.pingSentAt) + " msec."); + pingReceivedAt = System.currentTimeMillis(); + System.out.println(" Ping response received. Roundtrip: " + (pingReceivedAt - pingSentAt) + " msec."); } } } From b32808516d0c625959e6e9b3204e2e887c3b476e Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 12:40:36 -0400 Subject: [PATCH 055/110] improvements to test program. --- .../oswego/cs/CPSLab/AnkiConnectionTest.java | 101 +++++++++--------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index f0f5b47e..89158219 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -13,18 +13,18 @@ /** * A simple test program to test a connection to your Anki 'Supercars' and 'Supertrucks' using the NodeJS Bluetooth gateway. - * Simple follow the installation instructions at http://github.com/adessoAG/anki-drive-java, build this project, start the + * Simple follow the installation instructions at http://github.com/tenbergen/anki-drive-java, build this project, start the * bluetooth gateway using ./gradlew server, and run this class. * * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) */ -public class AnkiConnectionTest implements Runnable { +public class AnkiConnectionTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("192.168.1.62", 5000); - System.out.print("...looking for cars..."); + AnkiConnector anki = new AnkiConnector("localhost", 5000); + System.out.print(" looking for cars..."); List vehicles = anki.findVehicles(); if (vehicles.isEmpty()) { @@ -32,18 +32,20 @@ public static void main(String[] args) throws IOException, InterruptedException } else { System.out.println(" FOUND " + vehicles.size() + " CARS!"); - System.out.println("\nNow connecting to and doing stuff to your cars.\n\n"); + System.out.println(" Now connecting to and doing stuff to your cars."); Iterator iter = vehicles.iterator(); while (iter.hasNext()) { Vehicle v = iter.next(); - //Thread ct = new Thread(new AnkiConnectionTest((v))); - //ct.run(); AnkiConnectionTest act = new AnkiConnectionTest(v); act.run(); + System.out.println("NEXT CAR in 5sec"); } + System.out.println("last car done"); } anki.close(); + System.out.println("Test complete."); + System.exit(0); } private Vehicle v; @@ -52,8 +54,7 @@ public AnkiConnectionTest(Vehicle v) { this.v = v; } - @Override - public void run() { + public void run() throws InterruptedException { System.out.println("\nConnecting to " + v + " @ " + v.getAddress()); v.connect(); System.out.println("Vehicle Advertisement Data:"); @@ -69,22 +70,27 @@ public void run() { System.out.print(" Connected. Setting SDK mode..."); //always set the SDK mode FIRST! v.sendMessage(new SdkModeMessage()); - System.out.println(" SDK Mode set."); + System.out.println(" done."); - System.out.println(" Sending asynchronous Battery Level Request. The Response will come in eventually."); + System.out.print(" Sending ping..."); //we have to set up a response handler first, in order to handle async responses - BatteryLevelResponseHandler blrh = new BatteryLevelResponseHandler(); - //now we tell the car, who is listening to the replies - v.addMessageListener(BatteryLevelResponseMessage.class, blrh); - //now we can actually send it. - v.sendMessage(new BatteryLevelRequestMessage()); - - System.out.println(" Sending Ping Request..."); - //again, some async set-up required... PingResponseHandler prh = new PingResponseHandler(); + //now we tell the car, who is listening to the replies v.addMessageListener(PingResponseMessage.class, prh); - prh.pingSentAt = System.currentTimeMillis(); + //now we can actually send it. v.sendMessage(new PingRequestMessage()); + prh.pingSentAt = System.currentTimeMillis(); + System.out.print(" sent. Waiting for pong..."); + while (!prh.pingReceived) { + Thread.sleep(10); + } + System.out.println(" received. Roundtrip: " + prh.roundTrip + " msec."); + + System.out.println(" Sending asynchronous Battery Level Request. Response will come eventually."); + BatteryLevelResponseHandler blrh = new BatteryLevelResponseHandler(); + //works just like a Ping Request + v.addMessageListener(BatteryLevelResponseMessage.class, blrh); + v.sendMessage(new BatteryLevelRequestMessage()); System.out.println(" Flashing lights..."); //Lights require configurations. So first construct a lights configuration, @@ -93,70 +99,67 @@ public void run() { LightsPatternMessage lpm = new LightsPatternMessage(); lpm.add(lc); v.sendMessage(lpm); + //we should sleep for at least as long as the ping roundtrip to give the Vehicle time to set the lights + Thread.sleep(prh.roundTrip); System.out.println(" Setting Speed..."); //Speed is easy. Just tell the car how fast to go and how quickly to accelerate. v.sendMessage(new SetSpeedMessage(500, 100)); - System.out.println(" Looking for finish line..."); + System.out.println(" Driving to finish line..."); //Use the sensor on the bottom to check the road pieces. This is like a response/request, but will //update whenever there's a new value. FinishLineDetector fld = new FinishLineDetector(); v.addMessageListener(LocalizationPositionUpdateMessage.class, fld); v.sendMessage(new LocalizationPositionUpdateMessage()); while (!fld.stop) { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - // System.out.print("continue"); - // System.out.print(" Looking for finish line..."); + Thread.sleep(prh.roundTrip); } - // v.disconnect(); - System.out.println("disconnected from " + v + "\n"); + v.disconnect(); } /** - * Handles the response from the vehicle on which road piece the vehicle is - * and sets a stop flag to true if it's the finish line. + * Handles the response from the vehicle from the PingRequestMessage. + * We need handler classes because responses from the vehicles are asynchronous. + * Sets a received flag to true and computes the roundtrip time. */ - private class FinishLineDetector implements MessageListener { - - private int finishLineId = FinishRoadpiece.ROADPIECE_IDS[0]; - private boolean stop = false; + private class PingResponseHandler implements MessageListener { + private boolean pingReceived = false; + private long pingSentAt = System.currentTimeMillis(); + private long roundTrip = -1; @Override - public void messageReceived(LocalizationPositionUpdateMessage m) { - // System.out.println(m.toString()); - if (m.getRoadPieceId() == finishLineId) stop = true; + public void messageReceived(PingResponseMessage m) { + this.pingReceived = true; + this.roundTrip = System.currentTimeMillis() - pingSentAt; } } /** * Handles the response from the vehicle from the BatteryLevelRequestMessage. - * We need handler classes because responses from the vehicles are asynchronous. + * Updates the battery level whenever a BatteryLEvelRequestMessage is sent. */ private class BatteryLevelResponseHandler implements MessageListener { private int batt_level; + @Override public void messageReceived(BatteryLevelResponseMessage m) { - System.out.println(" Battery Level is: " + m.getBatteryLevel() + " mV"); + this.batt_level = m.getBatteryLevel(); + System.out.println(" Battery Level is: " + this.batt_level + " mV"); } } /** - * Handles the response from the vehicle from the PingRequestMessage. - * We need handler classes because responses from the vehicles are asynchronous. + * Handles the response from the vehicle on which road piece the vehicle is + * and sets a stop flag to true if it's the finish line. */ - private class PingResponseHandler implements MessageListener { - private long pingReceivedAt; - private long pingSentAt; + private class FinishLineDetector implements MessageListener { + private int finishLineId = FinishRoadpiece.ROADPIECE_IDS[0]; + private boolean stop = false; @Override - public void messageReceived(PingResponseMessage m) { - pingReceivedAt = System.currentTimeMillis(); - System.out.println(" Ping response received. Roundtrip: " + (pingReceivedAt - pingSentAt) + " msec."); + public void messageReceived(LocalizationPositionUpdateMessage m) { + if (m.getRoadPieceId() == finishLineId) this.stop = true; } } } From d7a6f28cd691ef23b1166e133fab00687f5ce656 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 13:39:36 -0400 Subject: [PATCH 056/110] improvements to test program. --- .../edu/oswego/cs/CPSLab/AnkiConnectionTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index 89158219..eea524a5 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -39,9 +39,7 @@ public static void main(String[] args) throws IOException, InterruptedException Vehicle v = iter.next(); AnkiConnectionTest act = new AnkiConnectionTest(v); act.run(); - System.out.println("NEXT CAR in 5sec"); } - System.out.println("last car done"); } anki.close(); System.out.println("Test complete."); @@ -80,11 +78,11 @@ public void run() throws InterruptedException { //now we can actually send it. v.sendMessage(new PingRequestMessage()); prh.pingSentAt = System.currentTimeMillis(); - System.out.print(" sent. Waiting for pong..."); + System.out.print(" sent. Waiting at most 10secs for pong..."); while (!prh.pingReceived) { Thread.sleep(10); } - System.out.println(" received. Roundtrip: " + prh.roundTrip + " msec."); + System.out.println(" Roundtrip: " + prh.roundTrip + " msec."); System.out.println(" Sending asynchronous Battery Level Request. Response will come eventually."); BatteryLevelResponseHandler blrh = new BatteryLevelResponseHandler(); @@ -99,8 +97,8 @@ public void run() throws InterruptedException { LightsPatternMessage lpm = new LightsPatternMessage(); lpm.add(lc); v.sendMessage(lpm); - //we should sleep for at least as long as the ping roundtrip to give the Vehicle time to set the lights - Thread.sleep(prh.roundTrip); + //we should sleep for at least some factor of the ping roundtrip to give the Vehicle time to set the lights + Thread.sleep(prh.roundTrip * 10); System.out.println(" Setting Speed..."); //Speed is easy. Just tell the car how fast to go and how quickly to accelerate. @@ -112,10 +110,11 @@ public void run() throws InterruptedException { FinishLineDetector fld = new FinishLineDetector(); v.addMessageListener(LocalizationPositionUpdateMessage.class, fld); v.sendMessage(new LocalizationPositionUpdateMessage()); - while (!fld.stop) { + while (!fld.stop ) { Thread.sleep(prh.roundTrip); } v.disconnect(); + System.out.println("Disconnected from " + v); } /** From 160a0519a2ca63259f14fb77108d183d4d07c7bd Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 14:08:09 -0400 Subject: [PATCH 057/110] Cleaned AnkiConnectionTest; make the cars move to the next finish line. --- .travis.yml | 2 +- jitpack.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f6465cc0..e967f481 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ before_install: language: java jdk: - - oraclejdk9 + - oraclejdk9 \ No newline at end of file diff --git a/jitpack.yml b/jitpack.yml index 8066364a..f2c8e517 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,4 +11,4 @@ before_install: language: java jdk: - - oraclejdk9 + - oraclejdk8 From e487de2eab5f5b3deca45e5f11d959bec54be037 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 14:09:50 -0400 Subject: [PATCH 058/110] increase java version to 11 for jitpack/travis --- .travis.yml | 2 +- jitpack.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e967f481..4846a0eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ before_install: language: java jdk: - - oraclejdk9 \ No newline at end of file + - openjdk11 \ No newline at end of file diff --git a/jitpack.yml b/jitpack.yml index f2c8e517..0886106d 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,4 +11,4 @@ before_install: language: java jdk: - - oraclejdk8 + - openjdk11 From 1f7f3cf1b06b51a0d836dda069b56334450173a0 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 14:15:38 -0400 Subject: [PATCH 059/110] forcing openjdk 8 for jitpack --- jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index 0886106d..e93a52c8 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,4 +11,4 @@ before_install: language: java jdk: - - openjdk11 + - openjdk8 From 53eb706c461f13aa4fd64fb28ffc844f126acfce Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 14:24:44 -0400 Subject: [PATCH 060/110] increase moowork node gradle plugin version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 766d3b11..f856f5a2 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } plugins { - id "com.moowork.node" version "1.2.0" + id "com.moowork.node" version "1.3.1" } apply plugin: 'java' From c5b06790267b4dc21af6e9b13e2de80d3fc245e5 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 14:50:30 -0400 Subject: [PATCH 061/110] force jitpack to clean and build --- jitpack.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index e93a52c8..24d60ec9 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -9,6 +9,10 @@ before_install: - apt-get update - apt-get install -y libudev-dev +install: + - ./gradlew clean + - ./gradlew build + language: java jdk: - - openjdk8 + - openjdk11 From ef57e4d6eed27690b3c38caf774cda53214c6cc3 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 14:53:48 -0400 Subject: [PATCH 062/110] force jitpack to clean, build, and npm install --- jitpack.yml | 1 + src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index 24d60ec9..dee9f221 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,6 +11,7 @@ before_install: install: - ./gradlew clean + - npm install --build-from-resource - ./gradlew build language: java diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index eea524a5..563dc642 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -21,9 +21,8 @@ public class AnkiConnectionTest { public static void main(String[] args) throws IOException, InterruptedException { - System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("localhost", 5000); + AnkiConnector anki = new AnkiConnector("192.168.1.60", 5000); System.out.print(" looking for cars..."); List vehicles = anki.findVehicles(); From d3f1c4edb1b940f8c59ef10441e2f59eef9a1474 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 15:00:34 -0400 Subject: [PATCH 063/110] force jitpack to clean, build, and npm install --- jitpack.yml | 3 ++- src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index dee9f221..46ff78ae 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,7 +11,8 @@ before_install: install: - ./gradlew clean - - npm install --build-from-resource + - rm -rf node_modules + - npm install - ./gradlew build language: java diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index 563dc642..c41c9b53 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -22,7 +22,7 @@ public class AnkiConnectionTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("192.168.1.60", 5000); + AnkiConnector anki = new AnkiConnector("localhost", 5000); System.out.print(" looking for cars..."); List vehicles = anki.findVehicles(); From cd6ead4565141a93ed4eec33cb8b10e135a4125c Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 15:06:32 -0400 Subject: [PATCH 064/110] force jitpack to clean, build, and npm install --- jitpack.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index 46ff78ae..fdd80b2f 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -8,11 +8,10 @@ addons: before_install: - apt-get update - apt-get install -y libudev-dev + - rm -rf build/node_modules/ + - ./gradlew clean install: - - ./gradlew clean - - rm -rf node_modules - - npm install - ./gradlew build language: java From bbd898eb381ea6567f9105db6c1928e416298b24 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 15:16:57 -0400 Subject: [PATCH 065/110] removed dependency on npmInstall --- .travis.yml | 2 +- build.gradle | 2 +- jitpack.yml | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4846a0eb..69530a7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ before_install: language: java jdk: - - openjdk11 \ No newline at end of file + - openjdk8 \ No newline at end of file diff --git a/build.gradle b/build.gradle index f856f5a2..89e6c743 100644 --- a/build.gradle +++ b/build.gradle @@ -68,4 +68,4 @@ task ankiConnectionTest(type: JavaExec) { main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } -processResources.dependsOn(['npmInstall']) +//processResources.dependsOn(['npmInstall']) diff --git a/jitpack.yml b/jitpack.yml index fdd80b2f..1fa6fa75 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -8,12 +8,11 @@ addons: before_install: - apt-get update - apt-get install -y libudev-dev - - rm -rf build/node_modules/ - - ./gradlew clean + - npm install -- install: - ./gradlew build language: java jdk: - - openjdk11 + - openjdk8 From f1f4d85542186fdbe17f7c9541c405092ec6edd7 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 15:28:28 -0400 Subject: [PATCH 066/110] force ignore exist code for jitpack installs --- build.gradle | 8 +++++++- jitpack.yml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 89e6c743..e6b2fd07 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ plugins { apply plugin: 'java' apply plugin: 'application' apply plugin: 'maven' +apply plugin: 'com.moowork.node' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' @@ -68,4 +69,9 @@ task ankiConnectionTest(type: JavaExec) { main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } -//processResources.dependsOn(['npmInstall']) +tasks.npmInstall { + ignoreExitValue = true +} + +processResources.dependsOn(['npmInstall']) +npm_install.dependsOn(npm_cache_clean) diff --git a/jitpack.yml b/jitpack.yml index 1fa6fa75..b6605875 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -8,7 +8,7 @@ addons: before_install: - apt-get update - apt-get install -y libudev-dev - - npm install -- + - npm install --build-from-resource install: - ./gradlew build From a80da4449caacec7f332a9a397c2cfbb188b649d Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 15:35:01 -0400 Subject: [PATCH 067/110] force npm install from resource --- build.gradle | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index e6b2fd07..2abd0101 100644 --- a/build.gradle +++ b/build.gradle @@ -68,10 +68,6 @@ task ankiConnectionTest(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } - -tasks.npmInstall { - ignoreExitValue = true -} +npmInstall.args = ['--build-from-resource'] processResources.dependsOn(['npmInstall']) -npm_install.dependsOn(npm_cache_clean) From 49e319fe52d1c0ad19d2d73c5838eef854f1fecd Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 15:50:57 -0400 Subject: [PATCH 068/110] force npm install from resource --- build.gradle | 5 ++--- jitpack.yml | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 2abd0101..72fa1c88 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ task fatJar(type: Jar) { with jar } -task server(type: NodeTask) { +task server(type: NodeTask, dependsOn: npmInstall) { script = file('src/main/nodejs/server.js') ignoreExitValue = true } @@ -68,6 +68,5 @@ task ankiConnectionTest(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } -npmInstall.args = ['--build-from-resource'] -processResources.dependsOn(['npmInstall']) +//processResources.dependsOn(['npmInstall']) diff --git a/jitpack.yml b/jitpack.yml index b6605875..b255516a 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -8,7 +8,6 @@ addons: before_install: - apt-get update - apt-get install -y libudev-dev - - npm install --build-from-resource install: - ./gradlew build From 600ff128b236a23c2e799c5ea672772ca92cc41d Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 15:53:31 -0400 Subject: [PATCH 069/110] change from application to library --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 72fa1c88..bd44899c 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ plugins { } apply plugin: 'java' -apply plugin: 'application' +apply plugin: 'library' apply plugin: 'maven' apply plugin: 'com.moowork.node' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' From 0eee53fdc0718e2a24e15959230dc4672c9ef333 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 16:09:04 -0400 Subject: [PATCH 070/110] change from application to library --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index bd44899c..04380f2d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,9 +11,9 @@ plugins { id "com.moowork.node" version "1.3.1" } -apply plugin: 'java' -apply plugin: 'library' -apply plugin: 'maven' +apply plugin: 'java-library' +//apply plugin: 'application' +apply plugin: 'maven-publish' apply plugin: 'com.moowork.node' mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' From 7fe7009181b3f08037490c01cd6de192117d5370 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 16:10:15 -0400 Subject: [PATCH 071/110] change from application to library --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 04380f2d..fc7f457f 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'java-library' //apply plugin: 'application' apply plugin: 'maven-publish' apply plugin: 'com.moowork.node' -mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' +//mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' repositories { From f2c82dd7c20a0253301ce163d828e904ec8adcaf Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 16:23:13 -0400 Subject: [PATCH 072/110] change from application to library --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fc7f457f..85d66c5e 100644 --- a/build.gradle +++ b/build.gradle @@ -12,10 +12,10 @@ plugins { } apply plugin: 'java-library' -//apply plugin: 'application' +apply plugin: 'application' apply plugin: 'maven-publish' apply plugin: 'com.moowork.node' -//mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' +mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' repositories { From 4324457e25a2ae565ffca2c8311331b90e0c9efb Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 16:39:01 -0400 Subject: [PATCH 073/110] increase gradle wrapper version --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e41d512b..4b095592 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip From b923d419f1227e7241dbc9d920aabea5e6135411 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 16:42:15 -0400 Subject: [PATCH 074/110] increase gradle wrapper version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 85d66c5e..7561a63c 100644 --- a/build.gradle +++ b/build.gradle @@ -69,4 +69,4 @@ task ankiConnectionTest(type: JavaExec) { main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } -//processResources.dependsOn(['npmInstall']) +processResources.dependsOn(['npmInstall']) From c92f84088da182e5868eeedafa1fcb2ad09b0fc9 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 16:44:02 -0400 Subject: [PATCH 075/110] increase gradle wrapper version --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 7561a63c..fc7f457f 100644 --- a/build.gradle +++ b/build.gradle @@ -12,10 +12,10 @@ plugins { } apply plugin: 'java-library' -apply plugin: 'application' +//apply plugin: 'application' apply plugin: 'maven-publish' apply plugin: 'com.moowork.node' -mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' +//mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' repositories { @@ -69,4 +69,4 @@ task ankiConnectionTest(type: JavaExec) { main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } -processResources.dependsOn(['npmInstall']) +//processResources.dependsOn(['npmInstall']) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4b095592..e41d512b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip From eb421a2211780c531abe27a2e9d9c3b1ec68a239 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 16:50:15 -0400 Subject: [PATCH 076/110] increase gradle wrapper version --- gradle/wrapper/gradle-wrapper.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e41d512b..2ad5f0c7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +#distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip From 9c27bbeaf7d2758790ad91c7bdddcacf7c16c8f9 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 17:02:23 -0400 Subject: [PATCH 077/110] increase gradle wrapper version --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index fc7f457f..567ad49b 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { apply plugin: 'java-library' //apply plugin: 'application' -apply plugin: 'maven-publish' +apply plugin: 'android-maven' apply plugin: 'com.moowork.node' //mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' @@ -30,10 +30,10 @@ dependencies { //new dependencies included by BT //dependencies for AnkiCommander (https://github.com/sha224/anki-commander) - implementation 'org.apache.sshd:sshd-core:2.2.0' - implementation 'org.jline:jline:3.10.0' - implementation 'org.slf4j:slf4j-api:1.7.26' - implementation 'org.slf4j:slf4j-simple:1.7.26' + compile 'org.apache.sshd:sshd-core:2.2.0' + compile 'org.jline:jline:3.10.0' + compile 'org.slf4j:slf4j-api:1.7.26' + compile 'org.slf4j:slf4j-simple:1.7.26' } node { From 37f333e080282f1d192b71dc4236625e4ca8196c Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 17:03:59 -0400 Subject: [PATCH 078/110] increase gradle wrapper version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 567ad49b..15e68cf1 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { apply plugin: 'java-library' //apply plugin: 'application' -apply plugin: 'android-maven' +apply plugin: 'maven' apply plugin: 'com.moowork.node' //mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' From b4c99faf65c89f7777b123ee8a6a567a69bda5ba Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 17:06:28 -0400 Subject: [PATCH 079/110] increase gradle wrapper version --- build.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 15e68cf1..0887db8a 100644 --- a/build.gradle +++ b/build.gradle @@ -30,10 +30,10 @@ dependencies { //new dependencies included by BT //dependencies for AnkiCommander (https://github.com/sha224/anki-commander) - compile 'org.apache.sshd:sshd-core:2.2.0' - compile 'org.jline:jline:3.10.0' - compile 'org.slf4j:slf4j-api:1.7.26' - compile 'org.slf4j:slf4j-simple:1.7.26' + // compile 'org.apache.sshd:sshd-core:2.2.0' + // compile 'org.jline:jline:3.10.0' + // compile 'org.slf4j:slf4j-api:1.7.26' + // compile 'org.slf4j:slf4j-simple:1.7.26' } node { @@ -58,10 +58,10 @@ task server(type: NodeTask, dependsOn: npmInstall) { } //added exec task for Anki Commander SSH test server -task ssh(type: JavaExec) { - classpath = sourceSets.main.runtimeClasspath - main = 'com.shakhar.anki.commander.AnkiSshd' -} +//task ssh(type: JavaExec) { +// classpath = sourceSets.main.runtimeClasspath +// main = 'com.shakhar.anki.commander.AnkiSshd' +//} //added exec task for AnkiConnection test program task ankiConnectionTest(type: JavaExec) { From 2ca94a4eff235e0ad9e5ebbadbfa09116b5f1b9e Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 17:08:59 -0400 Subject: [PATCH 080/110] increase gradle wrapper version --- build.gradle | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 0887db8a..3eb51450 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { id "com.moowork.node" version "1.3.1" } -apply plugin: 'java-library' +apply plugin: 'library' //apply plugin: 'application' apply plugin: 'maven' apply plugin: 'com.moowork.node' @@ -30,10 +30,10 @@ dependencies { //new dependencies included by BT //dependencies for AnkiCommander (https://github.com/sha224/anki-commander) - // compile 'org.apache.sshd:sshd-core:2.2.0' - // compile 'org.jline:jline:3.10.0' - // compile 'org.slf4j:slf4j-api:1.7.26' - // compile 'org.slf4j:slf4j-simple:1.7.26' + implementation 'org.apache.sshd:sshd-core:2.2.0' + implementation 'org.jline:jline:3.10.0' + implementation 'org.slf4j:slf4j-api:1.7.26' + implementation 'org.slf4j:slf4j-simple:1.7.26' } node { @@ -58,10 +58,10 @@ task server(type: NodeTask, dependsOn: npmInstall) { } //added exec task for Anki Commander SSH test server -//task ssh(type: JavaExec) { -// classpath = sourceSets.main.runtimeClasspath -// main = 'com.shakhar.anki.commander.AnkiSshd' -//} +task ssh(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'com.shakhar.anki.commander.AnkiSshd' +} //added exec task for AnkiConnection test program task ankiConnectionTest(type: JavaExec) { From bfd0d317cb64925fbbb37c1df9df2b9e3d204fb8 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 17:23:58 -0400 Subject: [PATCH 081/110] change to java library --- build.gradle | 5 +++-- settings.gradle | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 3eb51450..613974ef 100644 --- a/build.gradle +++ b/build.gradle @@ -8,10 +8,11 @@ buildscript { } plugins { - id "com.moowork.node" version "1.3.1" + id "com.moowork.node" version "1.3.1" + id 'java-library' } -apply plugin: 'library' +apply plugin: 'java-library' //apply plugin: 'application' apply plugin: 'maven' apply plugin: 'com.moowork.node' diff --git a/settings.gradle b/settings.gradle index 3732968d..e07a388f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ -include ':library' \ No newline at end of file +rootProject.name = 'anki-drive-java' +include ':Library' \ No newline at end of file From 14132825eba923fd9182e1a2698664122366b081 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 17:26:36 -0400 Subject: [PATCH 082/110] change to java library --- jitpack.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/jitpack.yml b/jitpack.yml index b255516a..e93a52c8 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -9,9 +9,6 @@ before_install: - apt-get update - apt-get install -y libudev-dev -install: - - ./gradlew build - language: java jdk: - openjdk8 From af0838e430f00fc600d4a6c8901a49f6826d24f7 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 18:30:33 -0400 Subject: [PATCH 083/110] downgrading Gradle to 4.10.2 again and fix jitpack issues. --- build.gradle | 4 +--- gradle/wrapper/gradle-wrapper.properties | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 613974ef..ac9af8a5 100644 --- a/build.gradle +++ b/build.gradle @@ -13,10 +13,8 @@ plugins { } apply plugin: 'java-library' -//apply plugin: 'application' apply plugin: 'maven' apply plugin: 'com.moowork.node' -//mainClassName = 'edu.oswego.cs.CPSLab.anki.AnkiConnectionTest' group = 'com.github.tenbergen' repositories { @@ -70,4 +68,4 @@ task ankiConnectionTest(type: JavaExec) { main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } -//processResources.dependsOn(['npmInstall']) +//processResources.dependsOn(['npmInstall']) // moved to server task because only that needs node. Fixed Jitpack issues. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ad5f0c7..e41d512b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,5 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -#distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip -distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip From 740dffb58e8487fa8cb2e2ec7c4f5ad8a0a875ea Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 9 May 2020 18:50:44 -0400 Subject: [PATCH 084/110] Updated README.md to reflect latest changes. --- README.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e592ebe4..f3b641bb 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # Anki Drive SDK for Java The Anki Drive SDK for Java is an implementation of the message protocols -and data parsing routines necessary for communicating with Anki Drive vehicles. +and data parsing routines necessary for communicating with Anki Drive and Overdrive vehicles. This library is an updated +version of the one found [adessoAG/anki-drive-java](https://github.com/adessoAG/anki-drive-java). *See [anki/drive-sdk](https://github.com/anki/drive-sdk) for the official SDK written in C.* ### Disclaimer -The authors of this software are in no way affiliated to Anki. +The authors of this software are in no way affiliated to Anki nor adesso AG. All naming rights for Anki, Anki Drive and Anki Overdrive are property of -[Anki](http://anki.com). +[Anki](http://anki.com), initial concept and implementation of the Java version by the folks at adesso. This is a forked repository from [adessoAG/anki-drive-java](https://github.com/adessoAG/anki-drive-java), which, sadly, appears to be abandoned. We are maintaining this SDK to serve our [tenbergen/Automotive-CPS](https://github.com/tenbergen/Automotive-CPS) project. @@ -37,7 +38,6 @@ To install the SDK and all required dependencies run the following commands: ``` git clone https://github.com/tenbergen/anki-drive-java cd anki-drive-java -npm install ./gradlew build ``` @@ -59,18 +59,23 @@ Once connected, if your cars time out follow these steps: 3. Select “Reset the Bluetooth module” from the Debug menu list 4. Once finished reboot your Mac -### On Linux +### On Linux / Raspberry Pi Optional Dependency node-usb will not be installed. So, run: ``` sudo apt-get install libudev-dev ``` +### On Windows + +Node.js server is currently not supported on Windows. However, you can run the Node.js server on a Linux device change +`edu.oswego.cs.CPSLab.AnkiConnectionTest` to connect to the IP of the Raspberry Pi instead of `localhost`. + ## Usage Start the Node.js gateway service: ``` -./gradlew server +sudo ./gradlew server ``` ### Add the Java library @@ -84,11 +89,11 @@ repositories { dependencies { // : commit hash or tag - compile 'com.github.adessoAG:anki-drive-java:' + compile 'com.github.tenbergen:anki-drive-java:-SNAPSHOT' } ``` -For the Maven instructions see the [JitPack.io website](https://jitpack.io/#adessoAG/anki-drive-java). +For the Maven instructions see the [JitPack.io website](https://jitpack.io/tenbergen/anki-drive-java). ### API usage @@ -114,5 +119,7 @@ edu.oswego.cs.CPSLab.AnkiConnectionTest ## Contributing -Contributions are always welcome! Feel free to fork this repository and submit +WINDOWS SERVER WANTED! + +Other contributions are welcome as well. Feel free to fork this repository and submit a pull request. From 71121dcc4c7085ec1dfc8b2423a6dd97de862769 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Mon, 11 May 2020 18:32:15 -0400 Subject: [PATCH 085/110] Added PowerzoneRoadpiece from Fast & Furious edition. --- build.gradle | 6 +++++ .../roadpieces/PowerzoneRoadpiece.java | 22 +++++++++++++++++++ .../oswego/cs/CPSLab/AnkiConnectionTest.java | 13 +++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/adesso/anki/roadmap/roadpieces/PowerzoneRoadpiece.java diff --git a/build.gradle b/build.gradle index ac9af8a5..809dc7fa 100644 --- a/build.gradle +++ b/build.gradle @@ -68,4 +68,10 @@ task ankiConnectionTest(type: JavaExec) { main = 'edu.oswego.cs.CPSLab.AnkiConnectionTest' } +//added exec task for RoadmapScanner test program +task scanTrack(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'edu.oswego.cs.CPSLab.RoadmapScannerTest' +} + //processResources.dependsOn(['npmInstall']) // moved to server task because only that needs node. Fixed Jitpack issues. diff --git a/src/main/java/de/adesso/anki/roadmap/roadpieces/PowerzoneRoadpiece.java b/src/main/java/de/adesso/anki/roadmap/roadpieces/PowerzoneRoadpiece.java new file mode 100644 index 00000000..da85edcb --- /dev/null +++ b/src/main/java/de/adesso/anki/roadmap/roadpieces/PowerzoneRoadpiece.java @@ -0,0 +1,22 @@ +package de.adesso.anki.roadmap.roadpieces; + +import de.adesso.anki.roadmap.Position; +import de.adesso.anki.roadmap.Section; + +/** + * Represents a Powerzone Roadpiece from the Fast & Furious tracks. + * @since 2020-05-11 + * @version 2020-05-11 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public class PowerzoneRoadpiece extends Roadpiece { + + public final static int[] ROADPIECE_IDS = { 57 }; + public final static Position ENTRY = Position.at(-280, 0); + public final static Position EXIT = Position.at(280, 0); + + public PowerzoneRoadpiece() { + this.section = new Section(this, ENTRY, EXIT); + } + +} diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index c41c9b53..9ba9b2ef 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -46,6 +46,7 @@ public static void main(String[] args) throws IOException, InterruptedException } private Vehicle v; + private long interval = 10; public AnkiConnectionTest(Vehicle v) { this.v = v; @@ -78,9 +79,12 @@ public void run() throws InterruptedException { v.sendMessage(new PingRequestMessage()); prh.pingSentAt = System.currentTimeMillis(); System.out.print(" sent. Waiting at most 10secs for pong..."); - while (!prh.pingReceived) { - Thread.sleep(10); + long timeout = 10000; + while (!prh.pingReceived && timeout > 0) { + Thread.sleep(interval); + timeout -= interval; } + this.interval = prh.roundTrip; System.out.println(" Roundtrip: " + prh.roundTrip + " msec."); System.out.println(" Sending asynchronous Battery Level Request. Response will come eventually."); @@ -97,7 +101,7 @@ public void run() throws InterruptedException { lpm.add(lc); v.sendMessage(lpm); //we should sleep for at least some factor of the ping roundtrip to give the Vehicle time to set the lights - Thread.sleep(prh.roundTrip * 10); + Thread.sleep(interval * 10); System.out.println(" Setting Speed..."); //Speed is easy. Just tell the car how fast to go and how quickly to accelerate. @@ -110,8 +114,9 @@ public void run() throws InterruptedException { v.addMessageListener(LocalizationPositionUpdateMessage.class, fld); v.sendMessage(new LocalizationPositionUpdateMessage()); while (!fld.stop ) { - Thread.sleep(prh.roundTrip); + Thread.sleep(interval); } + v.sendMessage(new SetSpeedMessage(0, 12500)); v.disconnect(); System.out.println("Disconnected from " + v); } From fe81a1c68154a85b2c9dc1c605ad4790cf329991 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 12 May 2020 13:23:00 -0400 Subject: [PATCH 086/110] Improve Roadmap.java class with ability to compare Roadmaps. Added some helper methods, too, for convenience. --- .../java/de/adesso/anki/roadmap/Roadmap.java | 222 +++++++++++++----- 1 file changed, 169 insertions(+), 53 deletions(-) diff --git a/src/main/java/de/adesso/anki/roadmap/Roadmap.java b/src/main/java/de/adesso/anki/roadmap/Roadmap.java index bc6b447d..555fbb66 100644 --- a/src/main/java/de/adesso/anki/roadmap/Roadmap.java +++ b/src/main/java/de/adesso/anki/roadmap/Roadmap.java @@ -2,62 +2,178 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.NoSuchElementException; import de.adesso.anki.roadmap.roadpieces.Roadpiece; +/** + * Roadmap object created by de.adesso.anki.RoadmapScanner. + * 2020-05-10 - Updated from original adesso version with ability to compare Roadmaps against each other. + * Roadmaps against each other. + * + * @since 2016-12-13 + * @version 2020-05-12 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ public class Roadmap { - - private Section anchor; - private Section current; - - public void setAnchor(Section anchor) { - this.anchor = anchor; - } - - public void addSection(Section section) { - if (current == null) { - anchor = current = section; - anchor.getPiece().setPosition(Position.at(0,0,180)); - } - else { - current.connect(section); - current = section; - - Position currentExit = current.getPiece().getPosition().transform(current.getExit()); - Position anchorEntry = anchor.getPiece().getPosition().transform(anchor.getEntry()); - if (currentExit.distance(anchorEntry) < 1) { - current.connect(anchor); - } - } - } - - public void add(int roadpieceId, int locationId, boolean reverse) { - Roadpiece piece = Roadpiece.createFromId(roadpieceId); - Section section = piece.getSectionByLocation(locationId, reverse); - - this.addSection(section); - } - - public List toList() { - if (anchor == null) return Collections.emptyList(); - - List list = new ArrayList<>(); - list.add(anchor.getPiece()); - - Section iterator = anchor.getNext(); - while (iterator != null && iterator != anchor) { - if (iterator.getPiece() != null) { - list.add(iterator.getPiece()); - } - iterator = iterator.getNext(); - } - - return Collections.unmodifiableList(list); - } - - public boolean isComplete() { - return anchor != null && anchor.getPrev() != null; - } - + + private Section anchor; + private Section current; + + public void setAnchor(Section anchor) { + this.anchor = anchor; + } + + public void addSection(Section section) { + if (current == null) { + anchor = current = section; + anchor.getPiece().setPosition(Position.at(0, 0, 180)); + } else { + current.connect(section); + current = section; + + Position currentExit = current.getPiece().getPosition().transform(current.getExit()); + Position anchorEntry = anchor.getPiece().getPosition().transform(anchor.getEntry()); + if (currentExit.distance(anchorEntry) < 1) { + current.connect(anchor); + } + } + } + + /** + * Adds a Roadpiece that was encountered to the current Roadmap. + * 2020-05-12 - Updated to report unknown Roadpieces. + * @param roadpieceId The ID of the Roadpiece. Must correspond to an Integer in ROADPIECE_IDS field of Roadpiece subclasses. + * @param locationId The location on the Roadpiece from which the Vehicle entered. + * @param reverse flag to determine if the Roadpiece is reversed. + * @since 2016-12-13 + * @version 2020-05-11 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ + public void add(int roadpieceId, int locationId, boolean reverse) { + Roadpiece piece = Roadpiece.createFromId(roadpieceId); + if (piece == null) { + System.out.println("Error scanning track: Unknown roadpiece with ID " + roadpieceId + ", location: " + locationId + ", revesed: " + reverse); + return; + } + Section section = piece.getSectionByLocation(locationId, reverse); + this.addSection(section); + } + + public List toList() { + if (anchor == null) return Collections.emptyList(); + + List list = new ArrayList<>(); + list.add(anchor.getPiece()); + + Section iterator = anchor.getNext(); + while (iterator != null && iterator != anchor) { + if (iterator.getPiece() != null) { + list.add(iterator.getPiece()); + } + iterator = iterator.getNext(); + } + + return Collections.unmodifiableList(list); + } + + public boolean isComplete() { + return anchor != null && anchor.getPrev() != null; + } + + /** + * Computes the length of the Roadmap in number of Roadpieces. + * + * @return The number of Roadpieces in this Roadmap, identical to toList().size() + * @since 2020-05-10 + * @version 2020-05-11 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ + public int getLength() { + return this.toList().size(); + } + + /** + * Compares two Roadmaps for equality. General idea: + * - if both roadmaps aren't of the same length, return false. + * - advance in both roadmaps + * - until you find the first piece that differ - so return false. + * - if you find an end piece, + * - they must both be the end piece - so return true + * - else return false. + * + * @param o The other Roadmap to be compared to. + * @return true if the Roadmaps are of same type, length, and contain the same sequence of Roadpieces, false else. + * @since 2020-05-10 + * @version 2020-05-11 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ + @Override + public boolean equals(Object o) { + //if it's the same object, they are equal + if (this == o) return true; + //if they aren't of the same class, they can't be equal + if (o == null || getClass() != o.getClass()) return false; + + Roadmap roadmap = (Roadmap) o; + //if they aren't of the same length, they can't be equal + if (roadmap.getLength() != this.getLength()) return false; + + Iterator theseRoadpieces = this.toList().iterator(); + Iterator thoseRoadpieces = ((Roadmap) o).toList().iterator(); + + //advance in both Roadmaps until you find a reason why they aren't equal. + while (theseRoadpieces.hasNext()) { + Roadpiece thisCurrent = theseRoadpieces.next(); + Roadpiece thatCurrent; + try { + thatCurrent = thoseRoadpieces.next(); + + if (!thisCurrent.getType().equals(thatCurrent.getType())) { + System.out.println("NOT EQUAL! pieces don't match."); + return false; //if the two Roadpieces are not of the same class, then the Roadmaps are different + } + // PROBLEM 1: what if the curve isn't the same direction? This would be considered equal: + // start -> (left) curve -> (left) curve -> straight -> (left) curve -> (left) curve -> Finish + // and + // start -> (right) curve -> (right) curve -> straight -> (right) curve -> (right) curve -> Finish + //fix might involve checking Sections and Positions rather than Roadpieces + + // PROBLEM 2: should identify two of the same track but one traveled in reverse as the same track + //fix might require second .equals method with boolean "ignoreReversed" + } catch (NoSuchElementException nseo) { + //if there are no more elements in the other roadmap, it's shorter, so they are obviously not equal. + //Hence, return false. + return false; + } + } + //if you get all the way to the end without failing, they must be equal. + return true; + } + + /** + * Provides a neatly formatted string representation of all Roadpieces in the Roadmap in their correct order. + * @return String representation of this Roadmap. + * @since 2020-05-10 + * @version 2020-05-11 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ + public String toString() { + + StringBuilder sb = new StringBuilder(); + Iterator iter = this.toList().iterator(); + int i = 1; + while (iter.hasNext()) { + sb.append(i); + sb.append(": "); + sb.append(iter.next().getType()); + sb.append("\n"); + i++; + } + return sb.toString(); + } } From eb10bfd69a4564a6a1a7629237c7b66ded62c49e Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Fri, 15 May 2020 21:23:49 -0400 Subject: [PATCH 087/110] Improved Roadmap and RoadmapScanner. RoadmapScanner no longer moves cars but instead robustly recognizes Roadpieces and creates Roadmaps. Added helper functions to manipulate and compare Roadmaps. Added facility to save and restore Roadmaps to/from disk. --- .../java/de/adesso/anki/RoadmapScanner.java | 157 +++++++++++------- .../java/de/adesso/anki/roadmap/Position.java | 24 ++- .../adesso/anki/roadmap/ReverseSection.java | 14 ++ .../java/de/adesso/anki/roadmap/Roadmap.java | 157 ++++++++++++++++-- .../java/de/adesso/anki/roadmap/Section.java | 25 ++- .../roadmap/roadpieces/CurvedRoadpiece.java | 15 ++ .../anki/roadmap/roadpieces/Roadpiece.java | 12 +- 7 files changed, 328 insertions(+), 76 deletions(-) diff --git a/src/main/java/de/adesso/anki/RoadmapScanner.java b/src/main/java/de/adesso/anki/RoadmapScanner.java index 82736618..1f7750f6 100644 --- a/src/main/java/de/adesso/anki/RoadmapScanner.java +++ b/src/main/java/de/adesso/anki/RoadmapScanner.java @@ -2,68 +2,111 @@ import de.adesso.anki.messages.LocalizationPositionUpdateMessage; import de.adesso.anki.messages.LocalizationTransitionUpdateMessage; -import de.adesso.anki.messages.SetSpeedMessage; +//import de.adesso.anki.messages.SetSpeedMessage; import de.adesso.anki.roadmap.Roadmap; +/** + * Scans a track from start to finish. Cars may start on any track piece; scan will complete once they arrive again on + * the same track piece. + * 2020-05-10 - Updated to no longer move the vehicles. Didn't work reliably due to asynchronous messages interlacing on some architecttures. It's not the caller's responsibility to move the vehicle. + * 2020-05-11 - Updated to normalize the Roadmap, i.e., the first track piece is a StartRoadpiece, the last one is a FinishRoadpiece + * Usage: + * 1. create new RoadmapScanner with a Vehicle + * 2. call startScanning() + * 3. send SpeedMessage to the same Vehicle + * 4. wait until isComplete() is true + * 5. call stopScanning() + * 6. Roadmap object will be available using + * + * @since 2016-12-13 + * @version 2020-05-10 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ public class RoadmapScanner { - private Vehicle vehicle; - private Roadmap roadmap; - - private LocalizationPositionUpdateMessage lastPosition; - - public RoadmapScanner(Vehicle vehicle) { - this.vehicle = vehicle; - this.roadmap = new Roadmap(); - } - - public void startScanning() { - vehicle.addMessageListener( - LocalizationPositionUpdateMessage.class, - (message) -> handlePositionUpdate(message) - ); - - vehicle.addMessageListener( - LocalizationTransitionUpdateMessage.class, - (message) -> handleTransitionUpdate(message) - ); - - vehicle.sendMessage(new SetSpeedMessage(500, 12500)); - } - - public void stopScanning() { - vehicle.sendMessage(new SetSpeedMessage(0, 12500)); - } - - public boolean isComplete() { - return roadmap.isComplete(); - } - - public Roadmap getRoadmap() { - return roadmap; - } - - public void reset(){ - this.roadmap = new Roadmap(); - this.lastPosition = null; - } + private Vehicle vehicle; + private Roadmap roadmap; - private void handlePositionUpdate(LocalizationPositionUpdateMessage message) { - lastPosition = message; - } + private LocalizationPositionUpdateMessage lastPosition; - protected void handleTransitionUpdate(LocalizationTransitionUpdateMessage message) { - if (lastPosition != null) { - roadmap.add( - lastPosition.getRoadPieceId(), - lastPosition.getLocationId(), - lastPosition.isParsedReverse() - ); - - if (roadmap.isComplete()) { - this.stopScanning(); - } + public RoadmapScanner(Vehicle vehicle) { + this.vehicle = vehicle; + this.roadmap = new Roadmap(); + } + + /** + * Starts the scan by adding message listeners to the car. + * Updated from original version, which would also move the car. + * + * @since 2016-12-13 + * @version 2020-05-10 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ + public void startScanning() { + vehicle.addMessageListener( + LocalizationPositionUpdateMessage.class, + (message) -> handlePositionUpdate(message) + ); + + vehicle.addMessageListener( + LocalizationTransitionUpdateMessage.class, + (message) -> handleTransitionUpdate(message) + ); + //vehicle.sendMessage(new SetSpeedMessage(500, 12500)); + } + + /** + * Stops the scan by removing the message listeners from the car. + * Updated from original version, which would just stop the car. + * + * @since 2016-12-13 + * @version 2020-05-10 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ + public void stopScanning() { + vehicle.removeMessageListener( + LocalizationPositionUpdateMessage.class, + (message) -> handlePositionUpdate(message) + ); + + vehicle.removeMessageListener( + LocalizationTransitionUpdateMessage.class, + (message) -> handleTransitionUpdate(message) + ); + //vehicle.sendMessage(new SetSpeedMessage(0, 12500)); + } + + public boolean isComplete() { + return roadmap.isComplete(); + } + + public Roadmap getRoadmap() { + return roadmap; + } + + public void reset() { + this.roadmap = new Roadmap(); + this.lastPosition = null; + } + + private void handlePositionUpdate(LocalizationPositionUpdateMessage message) { + lastPosition = message; + } + + protected void handleTransitionUpdate(LocalizationTransitionUpdateMessage message) { + if (lastPosition != null) { + roadmap.add( + lastPosition.getRoadPieceId(), + lastPosition.getLocationId(), + lastPosition.isParsedReverse() + ); + + if (roadmap.isComplete()) { + this.stopScanning(); + } + } } - } - } diff --git a/src/main/java/de/adesso/anki/roadmap/Position.java b/src/main/java/de/adesso/anki/roadmap/Position.java index 06401939..46182b78 100644 --- a/src/main/java/de/adesso/anki/roadmap/Position.java +++ b/src/main/java/de/adesso/anki/roadmap/Position.java @@ -1,6 +1,18 @@ package de.adesso.anki.roadmap; -public class Position { +import java.io.Serializable; + +/** + * Position object used to differentiate Sections. Entering the same Section using a Roadpiece might have different + * entry and exist positions. This is relevant for curves as well as intersections. + * 2020-05-12 - Updated with toString helper function for debugging. Added Serializable marker for saving Roadmaps. + * + * @since 2016-12-13 + * @version 2020-05-12 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public class Position implements Serializable { private double x; private double y; private double angle; @@ -80,4 +92,14 @@ public double getAngle() { return angle; } + /** + * Provides a neatly formatted string representation of a Position. + * @return String representation of this Position. + * @since 2020-05-12 + * @version 2020-05-12 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ + public String toString() { + return "X: " + getX() + ", Y: " + getY() + ", Angle: " + getAngle(); + } } diff --git a/src/main/java/de/adesso/anki/roadmap/ReverseSection.java b/src/main/java/de/adesso/anki/roadmap/ReverseSection.java index 5ec514f7..7bde3e2d 100644 --- a/src/main/java/de/adesso/anki/roadmap/ReverseSection.java +++ b/src/main/java/de/adesso/anki/roadmap/ReverseSection.java @@ -2,9 +2,23 @@ import de.adesso.anki.roadmap.roadpieces.Roadpiece; +/** + * ReverseSection object used to differentiate reversed and regular roadpieces, curves, and intersections from one another + * Entering the same Section using a Roadpiece might have different entry and exist positions. + * This is mostly relevant for curves as well as intersections, e.g., left curves will be ReverseSections, and right + * curves will be regular Sections. + * This is the original adesso version, but updated with a Serializable marker and SerialVersionUID for saving Roadmaps. + * + * @since 2016-12-13 + * @version 2020-05-12 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ public class ReverseSection extends Section { private Section original; + private boolean isReversed = true; + private static final long serialVersionUID = 4217292978578338519L; public ReverseSection(Section original) { this.original = original; diff --git a/src/main/java/de/adesso/anki/roadmap/Roadmap.java b/src/main/java/de/adesso/anki/roadmap/Roadmap.java index 555fbb66..cabfe6f0 100644 --- a/src/main/java/de/adesso/anki/roadmap/Roadmap.java +++ b/src/main/java/de/adesso/anki/roadmap/Roadmap.java @@ -1,27 +1,30 @@ package de.adesso.anki.roadmap; +import java.io.*; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; -import de.adesso.anki.roadmap.roadpieces.Roadpiece; +import de.adesso.anki.roadmap.roadpieces.*; /** * Roadmap object created by de.adesso.anki.RoadmapScanner. * 2020-05-10 - Updated from original adesso version with ability to compare Roadmaps against each other. * Roadmaps against each other. * - * @since 2016-12-13 - * @version 2020-05-12 * @author adesso AG * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @version 2020-05-12 + * @since 2016-12-13 */ -public class Roadmap { +public class Roadmap implements Serializable { + private Section nonNormalizedAnchor = null; private Section anchor; private Section current; + private static final long serialVersionUID = -8832115043850834353L; public void setAnchor(Section anchor) { this.anchor = anchor; @@ -46,21 +49,25 @@ public void addSection(Section section) { /** * Adds a Roadpiece that was encountered to the current Roadmap. * 2020-05-12 - Updated to report unknown Roadpieces. + * * @param roadpieceId The ID of the Roadpiece. Must correspond to an Integer in ROADPIECE_IDS field of Roadpiece subclasses. - * @param locationId The location on the Roadpiece from which the Vehicle entered. - * @param reverse flag to determine if the Roadpiece is reversed. - * @since 2016-12-13 - * @version 2020-05-11 + * @param locationId The location on the Roadpiece from which the Vehicle entered. + * @param reverse flag to determine if the Roadpiece is reversed. + * @version 2020-05-12 * @author adesso AG * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2016-12-13 */ public void add(int roadpieceId, int locationId, boolean reverse) { Roadpiece piece = Roadpiece.createFromId(roadpieceId); + // System.out.println(piece.getType()); if (piece == null) { System.out.println("Error scanning track: Unknown roadpiece with ID " + roadpieceId + ", location: " + locationId + ", revesed: " + reverse); return; } Section section = piece.getSectionByLocation(locationId, reverse); + // System.out.println(" entry: " + section.getEntry()); + // System.out.println(" exit: " + section.getExit()); this.addSection(section); } @@ -81,6 +88,69 @@ public List toList() { return Collections.unmodifiableList(list); } + /** + * Normalized this Roadmap. Normalized means that the StartRoadpiece is the first Roadpiece in the Roadmap. This + * is done by setting the first StartRoadpiece to the anchor. + * @version 2020-05-14 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-13 + */ + public void normalize() { + this.nonNormalizedAnchor = this.anchor; + System.out.println("normalizing..."); + if (this.anchor.getPiece().getType().equals(StartRoadpiece.class.getSimpleName())) { + System.out.println("anchor is StartPiece"); + return; //if the anchor is a Startpiece, it's already normalized. + } + + Section iter = this.anchor.getNext(); + while (iter != this.anchor) { + if (iter.getPiece().getType().equals(StartRoadpiece.class.getSimpleName())) { + this.anchor = iter; + return; + } + iter = iter.getNext(); + } + //if there is no Startpiece in the Roadmap, normalized Roadmap is the same as the original one. + } + + /** + * De-Normalized this Roadmap by restoring the anchor back to what it was before normalize() was called. + * @version 2020-05-14 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-13 + */ + public void deNormalize() { + this.anchor = this.nonNormalizedAnchor; + this.nonNormalizedAnchor = null; + } + + /** + * Reverses this Roadmap. Reversed means that the order of Roadpieces is reversed. + * Known issue: Roadpieces themselves are NOT reversed, meaning that a reversed Roadmap is also mirrer-reflected. + * (i.e., from Start-left-left-straight-left-left-finish, it would be turned into Finish-left-left-straight-left-left-start). + * @version 2020-05-14 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-13 + */ + public void reverse() { + this.anchor = this.anchor.getPrev(); + Section curr = this.anchor; + Section next = curr.getNext(); + Section prev = curr.getPrev(); + curr.setPrev(next); + curr.setNext(prev); + curr = curr.getNext(); + + while (curr != null && curr != this.anchor) { + next = curr.getNext(); + prev = curr.getPrev(); + curr.setPrev(next); + curr.setNext(prev); + curr = curr.getNext(); + } + } + public boolean isComplete() { return anchor != null && anchor.getPrev() != null; } @@ -89,9 +159,9 @@ public boolean isComplete() { * Computes the length of the Roadmap in number of Roadpieces. * * @return The number of Roadpieces in this Roadmap, identical to toList().size() - * @since 2020-05-10 * @version 2020-05-11 * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-10 */ public int getLength() { return this.toList().size(); @@ -108,9 +178,9 @@ public int getLength() { * * @param o The other Roadmap to be compared to. * @return true if the Roadmaps are of same type, length, and contain the same sequence of Roadpieces, false else. - * @since 2020-05-10 - * @version 2020-05-11 + * @version 2020-05-13 * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-10 */ @Override public boolean equals(Object o) { @@ -136,6 +206,30 @@ public boolean equals(Object o) { if (!thisCurrent.getType().equals(thatCurrent.getType())) { System.out.println("NOT EQUAL! pieces don't match."); return false; //if the two Roadpieces are not of the same class, then the Roadmaps are different + } else if (thisCurrent.getType().equals(CurvedRoadpiece.class.getSimpleName())) { + //if the two Roadpieces are both a Curve, it matters if they are "left curves" or "right curves." + //Only if not the order of Roadpieces and also their direction are the same, the Roadmaps can + //truly be equal. + //Here, we check if the entry point is the same in both pieces. If they aren't, they are not the same curve. + thisCurrent = (CurvedRoadpiece) thisCurrent; + thatCurrent = (CurvedRoadpiece) thatCurrent; + if (thisCurrent.getSections().get(0).getEntry() != thatCurrent.getSections().get(0).getEntry()) { + return false; //if they are both reversed (or both not reversed), they are both left or both right. + } + } else if (thisCurrent.getType().equals(IntersectionRoadpiece.class.getSimpleName())) { + //if the two Roadpieces are both an Intersection, the direction in which the car enters (i.e., + //North, East, West, South) matters. Just like in curves + //Only if not the order of Roadpieces and also their direction are the same, the Roadmaps can + //truly be equal. + thisCurrent = (IntersectionRoadpiece) thisCurrent; + thatCurrent = (IntersectionRoadpiece) thatCurrent; + if (thisCurrent.getSections().get(0).getEntry() != thatCurrent.getSections().get(0).getEntry()) { + return false; //if they are both reversed (or both not reversed), they are both left or both right. + } + } else { + //the direction of straights or Powerzones doesn't matter. + //Finishpieces and Startpieces matter, but are treated as different Roadpieces, so we don't need to + //do anything. } // PROBLEM 1: what if the curve isn't the same direction? This would be considered equal: // start -> (left) curve -> (left) curve -> straight -> (left) curve -> (left) curve -> Finish @@ -145,6 +239,7 @@ public boolean equals(Object o) { // PROBLEM 2: should identify two of the same track but one traveled in reverse as the same track //fix might require second .equals method with boolean "ignoreReversed" + //will need a reverse() function for the Roadmap } catch (NoSuchElementException nseo) { //if there are no more elements in the other roadmap, it's shorter, so they are obviously not equal. //Hence, return false. @@ -157,10 +252,11 @@ public boolean equals(Object o) { /** * Provides a neatly formatted string representation of all Roadpieces in the Roadmap in their correct order. + * * @return String representation of this Roadmap. - * @since 2020-05-10 * @version 2020-05-11 * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-10 */ public String toString() { @@ -176,4 +272,41 @@ public String toString() { } return sb.toString(); } + + /** + * Writes a Roadmap to disk. + * @param toSave The Roadmap to save. + * @param path The path where to save it to. Filename and extension doesn't matter. + * @return True, if saving was successful, false else. + */ + public static boolean saveRoadmap(Roadmap toSave, String path) { + try { + FileOutputStream fileWriter = new FileOutputStream(path); + ObjectOutputStream rmWriter = new ObjectOutputStream(fileWriter); + rmWriter.writeObject(toSave); + rmWriter.close(); + } catch (IOException ex) { + System.err.println(ex.toString()); + return false; + } + return true; + } + + /** + * Loads a Roadmap from disk. + * @param path The path where to load from. + * @return The Roadmap as it was written to disk, or null if there was a failure. + */ + public static Roadmap loadRoadmap(String path) { + Roadmap rm = null; + try { + FileInputStream fileReader = new FileInputStream(path); + ObjectInputStream rmReader = new ObjectInputStream(fileReader); + rm = (Roadmap) rmReader.readObject(); + rmReader.close(); + } catch (IOException | ClassNotFoundException ex) { + System.err.println(ex.toString()); + } + return rm; + } } diff --git a/src/main/java/de/adesso/anki/roadmap/Section.java b/src/main/java/de/adesso/anki/roadmap/Section.java index 36821260..3e065592 100644 --- a/src/main/java/de/adesso/anki/roadmap/Section.java +++ b/src/main/java/de/adesso/anki/roadmap/Section.java @@ -2,10 +2,27 @@ import de.adesso.anki.roadmap.roadpieces.Roadpiece; -public class Section { +import java.io.Serializable; + +/** + * Section object used to differentiate reversed and regular roadpieces, curves, and intersections from one another + * Entering the same Section using a Roadpiece might have different entry and exist positions. + * This is mostly relevant for curves as well as intersections, e.g., left curves will be ReverseSections, and right + * curves will be regular Sections. + * This is the original adesso version, but updated with a Serializable marker and SerialVersionUID for saving Roadmaps. + * + * @since 2016-12-13 + * @version 2020-05-12 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public class Section implements Serializable { private Roadpiece piece; - + private Position entry; + + private boolean isReversed = false; + private static final long serialVersionUID = -2053150693350041204L; protected Section() { } @@ -57,7 +74,5 @@ public void connect(Section other) { other.getPiece().setPosition(otherPos); } - public Section reverse() { - return new ReverseSection(this); - } + public Section reverse() { return new ReverseSection(this); } } diff --git a/src/main/java/de/adesso/anki/roadmap/roadpieces/CurvedRoadpiece.java b/src/main/java/de/adesso/anki/roadmap/roadpieces/CurvedRoadpiece.java index abbb632d..19fc7f6b 100644 --- a/src/main/java/de/adesso/anki/roadmap/roadpieces/CurvedRoadpiece.java +++ b/src/main/java/de/adesso/anki/roadmap/roadpieces/CurvedRoadpiece.java @@ -3,6 +3,21 @@ import de.adesso.anki.roadmap.Position; import de.adesso.anki.roadmap.Section; +/** + * + * Right curves are not reversed. + * Left curves are reversed. + */ +/** + * Roadpiece subclass representing instances of type Curve. This is the same version as the orignal adesso version, + * however we found it prudent to write down somewhere that a curve to the right is reversed == TRUE and references a + * Section object. A left curve is hence reversed == FALSE and references a ReversedSection object. + * + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @version 2020-05-12 + * @since 2016-12-13 + */ public class CurvedRoadpiece extends Roadpiece { public final static int[] ROADPIECE_IDS = { 17, 18, 20, 23, 24, 27 }; diff --git a/src/main/java/de/adesso/anki/roadmap/roadpieces/Roadpiece.java b/src/main/java/de/adesso/anki/roadmap/roadpieces/Roadpiece.java index ccb16d13..27750568 100644 --- a/src/main/java/de/adesso/anki/roadmap/roadpieces/Roadpiece.java +++ b/src/main/java/de/adesso/anki/roadmap/roadpieces/Roadpiece.java @@ -1,5 +1,6 @@ package de.adesso.anki.roadmap.roadpieces; +import java.io.Serializable; import java.util.Arrays; import java.util.List; import java.util.Set; @@ -11,7 +12,16 @@ import de.adesso.anki.roadmap.Roadmap; import de.adesso.anki.roadmap.Section; -public abstract class Roadpiece { +/** + * Roadpiece object used to differentiate types of track. This is the original adesso version, however added Serializeable marker interface for serialization. + * 2020-05-12 - Added Serializable marker for saving Roadmaps. + * + * @since 2016-12-13 + * @version 2020-05-12 + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public abstract class Roadpiece implements Serializable { private final static Reflections reflections = new Reflections("de.adesso.anki.roadmap.roadpieces"); private Position position; From c2c14a6827a2964dbd0cd6501dbfb6370ecba994 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Fri, 15 May 2020 21:50:54 -0400 Subject: [PATCH 088/110] Added test program and demonstrator for RoadmapScanner. --- src/main/java/de/adesso/anki/roadmap/Roadmap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/adesso/anki/roadmap/Roadmap.java b/src/main/java/de/adesso/anki/roadmap/Roadmap.java index cabfe6f0..8bb88bce 100644 --- a/src/main/java/de/adesso/anki/roadmap/Roadmap.java +++ b/src/main/java/de/adesso/anki/roadmap/Roadmap.java @@ -304,7 +304,7 @@ public static Roadmap loadRoadmap(String path) { ObjectInputStream rmReader = new ObjectInputStream(fileReader); rm = (Roadmap) rmReader.readObject(); rmReader.close(); - } catch (IOException | ClassNotFoundException ex) { + } catch (IOException | ClassNotFoundException | NullPointerException ex) { System.err.println(ex.toString()); } return rm; From bf9a447dde6c4e010cd7b7563a32e7f52e35a26a Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Fri, 15 May 2020 21:55:51 -0400 Subject: [PATCH 089/110] Added test program and demonstrator for RoadmapScanner. --- .../oswego/cs/CPSLab/RoadmapScannerTest.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java diff --git a/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java new file mode 100644 index 00000000..9697aa4e --- /dev/null +++ b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java @@ -0,0 +1,119 @@ +package edu.oswego.cs.CPSLab; + +import de.adesso.anki.AnkiConnector; +import de.adesso.anki.MessageListener; +import de.adesso.anki.RoadmapScanner; +import de.adesso.anki.Vehicle; +import de.adesso.anki.messages.*; +import de.adesso.anki.roadmap.Roadmap; +import de.adesso.anki.roadmap.roadpieces.FinishRoadpiece; +import de.adesso.anki.roadmap.roadpieces.StartRoadpiece; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +/** + * A simple test program to demonstrate how to scan a track with Overdrive vehicles. + * + * @since 2020-05-10 + * @version 2020-05-11 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public class RoadmapScannerTest { + + public static void main(String[] args) throws IOException, InterruptedException { + + System.out.print("Loading Roadmap... "); + Roadmap rm0 = Roadmap.loadRoadmap(System.getenv("user.home" + "/" + "Roadmap.ovrdrv")); + if (rm0 != null) { + System.out.println("loaded track is:"); + System.out.println(rm0.toString()); + + System.out.println("Reversed Roadmap:"); + rm0.reverse(); + System.out.println(rm0.toString()); + + System.out.println("Normalized Roadmap:"); + rm0.reverse(); // since .reverse() works on the Roadmap itself, gotta reverse again, + rm0.normalize(); // else we get a reversed AND normalized Roadmap. + System.out.println(rm0.toString()); + + } else { + System.out.println("Sorry, no Roadmap found on disk."); + } + + System.out.println("Now, let's look for any car and try to scan the track..."); + + System.exit(0); + + AnkiConnector anki = new AnkiConnector("localhost", 5000); + List vehicles = anki.findVehicles(); + + if (vehicles.isEmpty()) { + System.out.println(" NO CARS FOUND. I guess that means we're done."); + + } else { + System.out.println(" FOUND " + vehicles.size() + " CARS!"); + System.out.println(" Now connecting to cars and scanning track."); + + Iterator iter = vehicles.iterator(); + Vehicle v = null; + while (iter.hasNext()) { + v = iter.next(); + if (v.getAdvertisement().isCharging()) { + System.out.println("Skipping " + v + " because it is charging."); + continue; + } + } + + RoadmapScannerTest rmst = new RoadmapScannerTest(v); + Roadmap rm2 = rmst.scan(); + + + System.out.println("Scanned track is:"); + System.out.println(rm2.toString()); + System.out.println("Trying to save Roadmap..." + Roadmap.saveRoadmap(rm2, "user.home" + "/" + "Roadmap.ovrdrv")); + + System.out.println("Are the loaded and the scanned Roadmap equal?"); + System.out.println(rm0.equals(rm2)); + + } + anki.close(); + System.out.println("Scan complete."); + System.exit(0); + } + + private Vehicle v; + private long interval = 10; + + public RoadmapScannerTest(Vehicle v) { + this.v = v; + } + + public Roadmap scan() throws InterruptedException { + System.out.println("Scanning with: " + v); + v.connect(); + v.sendMessage(new SdkModeMessage()); + + Thread.sleep(interval * 10); + + System.out.print("Moving car..."); + v.sendMessage(new SetSpeedMessage(500, 100)); + + System.out.print(" scanning..."); + RoadmapScanner rms = new RoadmapScanner(this.v); + rms.startScanning(); + while (!rms.isComplete()) { + Thread.sleep(interval); + } + rms.stopScanning(); + System.out.println(" complete. Stopping."); + v.sendMessage(new SetSpeedMessage(0, 100)); + + v.disconnect(); + System.out.println("Disconnected from " + v); + + return rms.getRoadmap(); + } +} From cd250b956feb639fad6cd0ed4226064e44d63d19 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 16 May 2020 21:16:12 -0400 Subject: [PATCH 090/110] testing OpenJDK11 for jitpack --- jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index e93a52c8..0886106d 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,4 +11,4 @@ before_install: language: java jdk: - - openjdk8 + - openjdk11 From 995648e9b6d65883d337f7aec6ba3d6f63bdcc28 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 16 May 2020 21:17:58 -0400 Subject: [PATCH 091/110] testing OpenJDK11 for jitpack --- jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index 0886106d..e93a52c8 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,4 +11,4 @@ before_install: language: java jdk: - - openjdk11 + - openjdk8 From 4e36b2a7e0b98dad2c88681181a1aaffeda1d899 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Sat, 16 May 2020 21:23:09 -0400 Subject: [PATCH 092/110] reverting to OpenJDK8 for jitpack --- jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index e93a52c8..2dae2510 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -11,4 +11,4 @@ before_install: language: java jdk: - - openjdk8 + - openjdk8 \ No newline at end of file From 3cce51a28bf539c01dd88dc70dd51e40212cba11 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Mon, 18 May 2020 11:35:10 -0400 Subject: [PATCH 093/110] Added failsafe to test programs in case BT gateway isn't running. --- .../edu/oswego/cs/CPSLab/AnkiConnectionTest.java | 11 +++++++++-- .../edu/oswego/cs/CPSLab/RoadmapScannerTest.java | 13 +++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java index 9ba9b2ef..8095f284 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -20,9 +20,16 @@ */ public class AnkiConnectionTest { - public static void main(String[] args) throws IOException, InterruptedException { + public static void main(String[] args) throws InterruptedException { System.out.println("Launching connector..."); - AnkiConnector anki = new AnkiConnector("localhost", 5000); + AnkiConnector anki = null; + try { + anki = new AnkiConnector("localhost", 5000); + } catch (IOException ioe) { + System.out.println("Error connecting to server. Is it running?"); + System.out.println("Exiting."); + System.exit(0); + } System.out.print(" looking for cars..."); List vehicles = anki.findVehicles(); diff --git a/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java index 9697aa4e..e1810c48 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java @@ -22,7 +22,7 @@ */ public class RoadmapScannerTest { - public static void main(String[] args) throws IOException, InterruptedException { + public static void main(String[] args) throws InterruptedException { System.out.print("Loading Roadmap... "); Roadmap rm0 = Roadmap.loadRoadmap(System.getenv("user.home" + "/" + "Roadmap.ovrdrv")); @@ -45,9 +45,14 @@ public static void main(String[] args) throws IOException, InterruptedException System.out.println("Now, let's look for any car and try to scan the track..."); - System.exit(0); - - AnkiConnector anki = new AnkiConnector("localhost", 5000); + AnkiConnector anki = null; + try { + anki = new AnkiConnector("localhost", 5000); + } catch (IOException ioe) { + System.out.println("Error connecting to server. Is it running?"); + System.out.println("Exiting."); + System.exit(0); + } List vehicles = anki.findVehicles(); if (vehicles.isEmpty()) { From 4b87726e2cd70b76c14c7eeb4ba9806a44d1ff01 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Mon, 18 May 2020 17:57:18 -0400 Subject: [PATCH 094/110] added JumpRoadpiece and LandingRoadpiece to accommodate Overdrive Launch kits. --- .../roadmap/roadpieces/JumpRoadpiece.java | 22 +++++++++++++++++++ .../roadmap/roadpieces/LandingRoadpiece.java | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/main/java/de/adesso/anki/roadmap/roadpieces/JumpRoadpiece.java create mode 100644 src/main/java/de/adesso/anki/roadmap/roadpieces/LandingRoadpiece.java diff --git a/src/main/java/de/adesso/anki/roadmap/roadpieces/JumpRoadpiece.java b/src/main/java/de/adesso/anki/roadmap/roadpieces/JumpRoadpiece.java new file mode 100644 index 00000000..29ad3ffc --- /dev/null +++ b/src/main/java/de/adesso/anki/roadmap/roadpieces/JumpRoadpiece.java @@ -0,0 +1,22 @@ +package de.adesso.anki.roadmap.roadpieces; + +import de.adesso.anki.roadmap.Position; +import de.adesso.anki.roadmap.Section; + +/** + * Represents a Jump Roadpiece from the Overdrive Launch Kit. + * @since 2020-05-18 + * @version 2020-05-18 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public class JumpRoadpiece extends Roadpiece { + + public final static int[] ROADPIECE_IDS = { 58 }; + public final static Position ENTRY = Position.at(-280, 0); + public final static Position EXIT = Position.at(280, 0); + + public JumpRoadpiece() { + this.section = new Section(this, ENTRY, EXIT); + } + +} diff --git a/src/main/java/de/adesso/anki/roadmap/roadpieces/LandingRoadpiece.java b/src/main/java/de/adesso/anki/roadmap/roadpieces/LandingRoadpiece.java new file mode 100644 index 00000000..57ab337f --- /dev/null +++ b/src/main/java/de/adesso/anki/roadmap/roadpieces/LandingRoadpiece.java @@ -0,0 +1,22 @@ +package de.adesso.anki.roadmap.roadpieces; + +import de.adesso.anki.roadmap.Position; +import de.adesso.anki.roadmap.Section; + +/** + * Represents a Landing Roadpiece from the Overdrive Launch Kit. + * @since 2020-05-18 + * @version 2020-05-18 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + */ +public class LandingRoadpiece extends Roadpiece { + + public final static int[] ROADPIECE_IDS = { 63 }; + public final static Position ENTRY = Position.at(-280, 0); + public final static Position EXIT = Position.at(280, 0); + + public LandingRoadpiece() { + this.section = new Section(this, ENTRY, EXIT); + } + +} From 79c44d0c7f9b67faf7813489483375a730e125ed Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 13:53:26 -0400 Subject: [PATCH 095/110] changed exit code --- src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java index e1810c48..e930da7c 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java @@ -51,7 +51,7 @@ public static void main(String[] args) throws InterruptedException { } catch (IOException ioe) { System.out.println("Error connecting to server. Is it running?"); System.out.println("Exiting."); - System.exit(0); + System.exit(1); } List vehicles = anki.findVehicles(); From 43d0dc1c36eddf72eb162e95dbeffb00e389f67d Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 14:26:18 -0400 Subject: [PATCH 096/110] source compatibility Java 1.8 --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 809dc7fa..07e6c511 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ apply plugin: 'java-library' apply plugin: 'maven' apply plugin: 'com.moowork.node' group = 'com.github.tenbergen' +sourceCompatibility = '1.8' repositories { jcenter() From 8c0ce93c99c152566eb066130d94ca7dd9f65c8a Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 14:28:43 -0400 Subject: [PATCH 097/110] source compatibility Java 1.8 --- build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 07e6c511..609fb2c1 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,11 @@ apply plugin: 'java-library' apply plugin: 'maven' apply plugin: 'com.moowork.node' group = 'com.github.tenbergen' -sourceCompatibility = '1.8' + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} repositories { jcenter() From 978f1baae4fb01eed17084df0506aba59f404567 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 14:34:40 -0400 Subject: [PATCH 098/110] source compatibility Java 1.8 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 609fb2c1..25c247d2 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,9 @@ apply plugin: 'maven' apply plugin: 'com.moowork.node' group = 'com.github.tenbergen' +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 From 837859834d3c3870564a5d0356a33c10f12b1b8e Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 14:48:06 -0400 Subject: [PATCH 099/110] source compatibility Java 1.8 --- build.gradle | 2 ++ gradle.properties | 1 + 2 files changed, 3 insertions(+) create mode 100644 gradle.properties diff --git a/build.gradle b/build.gradle index 25c247d2..7d489656 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,8 @@ apply plugin: 'maven' apply plugin: 'com.moowork.node' group = 'com.github.tenbergen' +compileJava.options.fork = true + sourceCompatibility = '1.8' targetCompatibility = '1.8' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..39744e38 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +targetJavaVersion=1.8 \ No newline at end of file From 81a5bfe32f7af3cc3692ea1d8eb272ab1186a367 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 14:49:35 -0400 Subject: [PATCH 100/110] source compatibility Java 1.8 --- gradle.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 39744e38..e1c827bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ -targetJavaVersion=1.8 \ No newline at end of file +targetJavaVersion=1.8 +org.gradle.jvm.version=8 \ No newline at end of file From 3328cd524e2f9f12b01857969315d4c87dbe73e7 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 14:56:36 -0400 Subject: [PATCH 101/110] source compatibility Java 1.8 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e1c827bf..30af5469 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ targetJavaVersion=1.8 -org.gradle.jvm.version=8 \ No newline at end of file +org.gradle.jvm.version=1.8 \ No newline at end of file From da45097dfa79b1557030e4725b03acb92466bdbd Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 14:58:59 -0400 Subject: [PATCH 102/110] source compatibility Java 1.8 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 30af5469..e1c827bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ targetJavaVersion=1.8 -org.gradle.jvm.version=1.8 \ No newline at end of file +org.gradle.jvm.version=8 \ No newline at end of file From b9cf937963038d43a74d55c2e519edcd8a2194f0 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 16 Jun 2020 15:00:59 -0400 Subject: [PATCH 103/110] source compatibility Java 1.8 --- build.gradle | 4 ++-- gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 7d489656..120e018c 100644 --- a/build.gradle +++ b/build.gradle @@ -19,8 +19,8 @@ group = 'com.github.tenbergen' compileJava.options.fork = true -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/gradle.properties b/gradle.properties index e1c827bf..2942b253 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -targetJavaVersion=1.8 -org.gradle.jvm.version=8 \ No newline at end of file +targetJavaVersion=JavaVersion.VERSION_1_8 +org.gradle.jvm.version=JavaVersion.VERSION_1_8 \ No newline at end of file From c16c13cd2b61028ce87c6b6a0b3301d79388aab8 Mon Sep 17 00:00:00 2001 From: Ka Ying Chan <43018746+kchan2@users.noreply.github.com> Date: Thu, 23 Jul 2020 16:54:23 -0400 Subject: [PATCH 104/110] Included pieceIDs and reverses in RoadmapScanner Testing --- src/main/java/de/adesso/anki/RoadmapScanner.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/de/adesso/anki/RoadmapScanner.java b/src/main/java/de/adesso/anki/RoadmapScanner.java index 1f7750f6..6c2e1f89 100644 --- a/src/main/java/de/adesso/anki/RoadmapScanner.java +++ b/src/main/java/de/adesso/anki/RoadmapScanner.java @@ -27,12 +27,17 @@ public class RoadmapScanner { private Vehicle vehicle; private Roadmap roadmap; + private boolean initReverse; + private ArrayList pieceIDs; + private ArrayList reverses; private LocalizationPositionUpdateMessage lastPosition; public RoadmapScanner(Vehicle vehicle) { this.vehicle = vehicle; this.roadmap = new Roadmap(); + this.pieceIDs = new ArrayList(); + this.reverses = new ArrayList(); } /** @@ -103,6 +108,13 @@ protected void handleTransitionUpdate(LocalizationTransitionUpdateMessage messag lastPosition.getLocationId(), lastPosition.isParsedReverse() ); + + pieceIDs.add(lastPosition.getRoadPieceId()); + reverses.add(lastPosition.isParsedReverse()); + + if (lastPosition.getRoadPieceId() == 33) { + initReverse = lastPosition.isParsedReverse(); + } if (roadmap.isComplete()) { this.stopScanning(); From a4c8efceaf2de6c96f16d1281aaa40df910de360 Mon Sep 17 00:00:00 2001 From: Ka Ying Chan <43018746+kchan2@users.noreply.github.com> Date: Mon, 27 Jul 2020 21:50:37 -0400 Subject: [PATCH 105/110] Improved RoadmapScanner Can now "normalize" the lists of pieceIDs and reverses --- .../java/de/adesso/anki/RoadmapScanner.java | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/adesso/anki/RoadmapScanner.java b/src/main/java/de/adesso/anki/RoadmapScanner.java index 6c2e1f89..1fc31a32 100644 --- a/src/main/java/de/adesso/anki/RoadmapScanner.java +++ b/src/main/java/de/adesso/anki/RoadmapScanner.java @@ -10,6 +10,7 @@ * the same track piece. * 2020-05-10 - Updated to no longer move the vehicles. Didn't work reliably due to asynchronous messages interlacing on some architecttures. It's not the caller's responsibility to move the vehicle. * 2020-05-11 - Updated to normalize the Roadmap, i.e., the first track piece is a StartRoadpiece, the last one is a FinishRoadpiece + * 2020-07-27 - Updated to include the creation of lists of pieceIDs and reverses * Usage: * 1. create new RoadmapScanner with a Vehicle * 2. call startScanning() @@ -22,6 +23,7 @@ * @version 2020-05-10 * @author adesso AG * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @author Ka Ying Chan (kchan2@oswego.edu) */ public class RoadmapScanner { @@ -82,6 +84,18 @@ public void stopScanning() { (message) -> handleTransitionUpdate(message) ); //vehicle.sendMessage(new SetSpeedMessage(0, 12500)); + if (initReverse) { + Collections.reverse(pieceIDs); + Collections.reverse(reverses); + for (int i = 0; i < pieceIDs.size(); i++) { + if (pieceIDs.get(i) != 10) { + reverses.set(i, !reverses.get(i)); + } + } + } + int distance = pieceIDs.size() - 1 - pieceIDs.indexOf(34); + Collections.rotate(pieceIDs, distance); + Collections.rotate(reverses, distance); } public boolean isComplete() { @@ -92,6 +106,18 @@ public Roadmap getRoadmap() { return roadmap; } + public boolean getInitReverse() { + return initReverse; + } + + public ArrayList getPieceIDs() { + return pieceIDs; + } + + public ArrayList getReverses() { + return reverses; + } + public void reset() { this.roadmap = new Roadmap(); this.lastPosition = null; @@ -108,11 +134,12 @@ protected void handleTransitionUpdate(LocalizationTransitionUpdateMessage messag lastPosition.getLocationId(), lastPosition.isParsedReverse() ); - + pieceIDs.add(lastPosition.getRoadPieceId()); reverses.add(lastPosition.isParsedReverse()); - - if (lastPosition.getRoadPieceId() == 33) { +// System.out.println("Added a piece... " + pieceIDs.get(pieceIDs.size() - 1)); + + if (lastPosition.getRoadPieceId() == 33 || lastPosition.getRoadPieceId() == 34) { initReverse = lastPosition.isParsedReverse(); } From 74e765081c8f65952ee436067fc71d81ecdb9fd6 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Tue, 28 Jul 2020 09:04:26 -0400 Subject: [PATCH 106/110] Added features to keep track of road pieces in a Roadmap to RoadmapScanner --- src/main/java/de/adesso/anki/RoadmapScanner.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/de/adesso/anki/RoadmapScanner.java b/src/main/java/de/adesso/anki/RoadmapScanner.java index 1fc31a32..d8fcaa06 100644 --- a/src/main/java/de/adesso/anki/RoadmapScanner.java +++ b/src/main/java/de/adesso/anki/RoadmapScanner.java @@ -1,5 +1,7 @@ package de.adesso.anki; +import java.util.ArrayList; +import java.util.Collections; import de.adesso.anki.messages.LocalizationPositionUpdateMessage; import de.adesso.anki.messages.LocalizationTransitionUpdateMessage; //import de.adesso.anki.messages.SetSpeedMessage; From dd75e5487a7cb0919efe3fa4226dbc813ddda574 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Mon, 3 Aug 2020 14:37:47 -0400 Subject: [PATCH 107/110] added Cloneable marker interface to Roadmap --- src/main/java/de/adesso/anki/roadmap/Roadmap.java | 2 +- .../java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/adesso/anki/roadmap/Roadmap.java b/src/main/java/de/adesso/anki/roadmap/Roadmap.java index 8bb88bce..5fa5bffc 100644 --- a/src/main/java/de/adesso/anki/roadmap/Roadmap.java +++ b/src/main/java/de/adesso/anki/roadmap/Roadmap.java @@ -19,7 +19,7 @@ * @version 2020-05-12 * @since 2016-12-13 */ -public class Roadmap implements Serializable { +public class Roadmap implements Serializable, Cloneable { private Section nonNormalizedAnchor = null; private Section anchor; diff --git a/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java index e930da7c..2bfa4749 100644 --- a/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java +++ b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java @@ -25,7 +25,7 @@ public class RoadmapScannerTest { public static void main(String[] args) throws InterruptedException { System.out.print("Loading Roadmap... "); - Roadmap rm0 = Roadmap.loadRoadmap(System.getenv("user.home" + "/" + "Roadmap.ovrdrv")); + /* Roadmap rm0 = Roadmap.loadRoadmap(System.getenv("user.home" + "/" + "Roadmap.ovrdrv")); if (rm0 != null) { System.out.println("loaded track is:"); System.out.println(rm0.toString()); @@ -42,12 +42,12 @@ public static void main(String[] args) throws InterruptedException { } else { System.out.println("Sorry, no Roadmap found on disk."); } - +*/ System.out.println("Now, let's look for any car and try to scan the track..."); AnkiConnector anki = null; try { - anki = new AnkiConnector("localhost", 5000); + anki = new AnkiConnector("192.168.1.101", 5000); } catch (IOException ioe) { System.out.println("Error connecting to server. Is it running?"); System.out.println("Exiting."); @@ -80,8 +80,8 @@ public static void main(String[] args) throws InterruptedException { System.out.println(rm2.toString()); System.out.println("Trying to save Roadmap..." + Roadmap.saveRoadmap(rm2, "user.home" + "/" + "Roadmap.ovrdrv")); - System.out.println("Are the loaded and the scanned Roadmap equal?"); - System.out.println(rm0.equals(rm2)); + // System.out.println("Are the loaded and the scanned Roadmap equal?"); + // System.out.println(rm0.equals(rm2)); } anki.close(); From 6b9889b9a189979430503a6cab9bffd33987fc4c Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Mon, 3 Aug 2020 14:40:53 -0400 Subject: [PATCH 108/110] added clone() to Roadmap --- src/main/java/de/adesso/anki/roadmap/Roadmap.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/de/adesso/anki/roadmap/Roadmap.java b/src/main/java/de/adesso/anki/roadmap/Roadmap.java index 5fa5bffc..932aabf6 100644 --- a/src/main/java/de/adesso/anki/roadmap/Roadmap.java +++ b/src/main/java/de/adesso/anki/roadmap/Roadmap.java @@ -273,6 +273,10 @@ public String toString() { return sb.toString(); } + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + /** * Writes a Roadmap to disk. * @param toSave The Roadmap to save. From 95287bcaf2bb806b1b930f3455253f3d9ee550f5 Mon Sep 17 00:00:00 2001 From: Bastian Tenbergen Date: Mon, 3 Aug 2020 14:48:13 -0400 Subject: [PATCH 109/110] added getCopy() method --- src/main/java/de/adesso/anki/roadmap/Roadmap.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/de/adesso/anki/roadmap/Roadmap.java b/src/main/java/de/adesso/anki/roadmap/Roadmap.java index 932aabf6..89abec70 100644 --- a/src/main/java/de/adesso/anki/roadmap/Roadmap.java +++ b/src/main/java/de/adesso/anki/roadmap/Roadmap.java @@ -277,6 +277,16 @@ public Object clone() throws CloneNotSupportedException { return super.clone(); } + public Roadmap getCopy() { + Roadmap rm; + try { + rm = (Roadmap) this.clone(); + } catch (CloneNotSupportedException ce) { + return null; + } + return rm; + } + /** * Writes a Roadmap to disk. * @param toSave The Roadmap to save. From 81e9ae89fabde05f31781fbdab961ebb18ca430e Mon Sep 17 00:00:00 2001 From: Gregory Maldonado Date: Sun, 11 Dec 2022 22:18:13 -0500 Subject: [PATCH 110/110] go tcp/bluetooth server --- src/main/golang/AutomotiveCpsServer.go | 305 ++++++++++++++++++ src/main/golang/README.md | 5 + src/main/golang/go.mod | 20 ++ src/main/golang/go.sum | 118 +++++++ src/main/golang/serverconf.yml | 2 + .../golang/uml/server-sequence-diagram.pdf | Bin 0 -> 37570 bytes 6 files changed, 450 insertions(+) create mode 100644 src/main/golang/AutomotiveCpsServer.go create mode 100644 src/main/golang/README.md create mode 100644 src/main/golang/go.mod create mode 100644 src/main/golang/go.sum create mode 100644 src/main/golang/serverconf.yml create mode 100644 src/main/golang/uml/server-sequence-diagram.pdf diff --git a/src/main/golang/AutomotiveCpsServer.go b/src/main/golang/AutomotiveCpsServer.go new file mode 100644 index 00000000..a3afcfd4 --- /dev/null +++ b/src/main/golang/AutomotiveCpsServer.go @@ -0,0 +1,305 @@ +/* + * State University of New York, College at Oswego + * + * A tcp client that acts as a middle man between ANKI Drive vehicles and the ANKI Drive SDK for Java. + * Forms a tcp/ip connection to the SDK and uses tinygo BLE module for connecting to each ANKI Drive vehicle. + * ANKI Drive vehicle firmware and message protocol can be found here: + * https://github.com/tenbergen/anki-drive-java/blob/master/Anki%20Drive%20Programming%20Guide.pdf + * + * Date: November 13, 2022 + * Author: Bastian Tenbergen, PhD & Gregory Maldonado {bastian.tenbergen | gmaldona}@oswego.edu + * Version: 1.0 + * + */ + +package main + +import ( + "bytes" + "encoding/hex" + "fmt" + cmap "github.com/orcaman/concurrent-map/v2" + "gopkg.in/yaml.v3" + "io/ioutil" + "log" + "net" + "regexp" + "strings" + "time" + "tinygo.org/x/bluetooth" +) + +const ( + ANSI_RESET = "\u001B[0m" + ANSI_RED = "\u001B[31m" + ANSI_GREEN = "\u001B[32m" +) + +var ( + server Server + Adapter = bluetooth.DefaultAdapter + AdapterEnabled = false + ANKI_STR_SERVICE_UUID = bluetooth.NewUUID([16]byte{0xBE, 0x15, 0xBE, 0xEF, 0x61, 0x86, 0x40, 0x7E, 0x83, 0x81, 0x0B, 0xD8, 0x9C, 0x4D, 0x8D, 0xF4}) + ANKI_STR_CHR_READ_UUID = bluetooth.NewUUID([16]byte{0xBE, 0x15, 0xBE, 0xE0, 0x61, 0x86, 0x40, 0x7E, 0x83, 0x81, 0x0B, 0xD8, 0x9C, 0x4D, 0x8D, 0xF4}) + ANKI_STR_CHR_WRITE_UUID = bluetooth.NewUUID([16]byte{0xBE, 0x15, 0xBE, 0xE1, 0x61, 0x86, 0x40, 0x7E, 0x83, 0x81, 0x0B, 0xD8, 0x9C, 0x4D, 0x8D, 0xF4}) +) + +type Server struct { + DiscoveredDevices cmap.ConcurrentMap[string, AnkiVehicle] + ConnectedDevices cmap.ConcurrentMap[string, *bluetooth.Device] + DeviceCharacteristics cmap.ConcurrentMap[string, []bluetooth.DeviceCharacteristic] +} + +type AnkiVehicle struct { + Address string + ManufacturerData string + LocalName string + Addresser bluetooth.Addresser +} + +type ServerConf struct { + Host string `yaml:"host"` + Port string `yaml:"port"` +} + +func main() { + server.DiscoveredDevices = cmap.New[AnkiVehicle]() + server.ConnectedDevices = cmap.New[*bluetooth.Device]() + server.DeviceCharacteristics = cmap.New[[]bluetooth.DeviceCharacteristic]() + + file, err := ioutil.ReadFile("serverconf.yml") + if err != nil { + displayError(err.Error()) + } + + serverConf := ServerConf{} + err = yaml.Unmarshal(file, &serverConf) + if err != nil { + displayError(err.Error()) + } + + // Listen for connections on host and port + l, err := net.Listen("tcp", serverConf.Host+":"+serverConf.Port) + if err != nil { + displayError(err.Error()) + } + + // terminate server on port when disconnected + defer func(l net.Listener) { + l.Close() + }(l) + displayInfo("Starting Server... Listening on " + serverConf.Host + ":" + serverConf.Port) + for { + // Listen for an incoming connection. + conn, err := l.Accept() + displayInfo("Connection established.") + + if err != nil { + displayError(err.Error()) + } + // Handle connections in a new goroutine. + go handleRequest(conn) + } +} + +// Handles the incoming requests from the tcp connection +func handleRequest(conn net.Conn) { + + // Keep grabbing messages from tcp connection until server termination + for { + // Read the incoming connection into the buffer. + buf := make([]byte, 1024) + _, err := conn.Read(buf) + // if err, then probably a client disconnect + if err != nil { + displayInfo("Client disconnect? Disconnecting all devices...") + for _, device := range server.ConnectedDevices.Items() { + device.Disconnect() + } + server.ConnectedDevices = cmap.New[*bluetooth.Device]() + conn.Close() + return + } + + // Create a goroutine for incoming msg and listen for the next msg + go func(buf []byte) { + // parsing msg so the payload can go to the vehicle - payload is at index [1] + re, _ := regexp.Compile(";") + split := re.Split(string(buf), -1) + var set []string + + for i := range split { + set = append(set, strings.Replace(split[i], "\n", "", -1)) + } + + address := set[0] + var msg string + + if len(set) > 1 { + msg = set[1] + } + + // Perform different actions based on the tcp msg received from ANKI SDK + switch { + // SCAN request from java + case strings.Contains(string(buf), "SCAN"): + displayInfo("Scanning...") + // call scan function to search for nearby vehicles + server.DiscoveredDevices = scan() + for _, device := range server.DiscoveredDevices.Items() { + // for each found device, send a tcp msg to java saying found + conn.Write([]byte("SCAN;" + device.Address + ";" + device.ManufacturerData + ";" + device.LocalName + "\n")) + + displayInfo("Found device: " + device.Address) + time.Sleep(500 * time.Millisecond) + } + // Stops scanning on java side + conn.Write([]byte("SCAN;COMPLETED\n")) + fmt.Println(ANSI_GREEN + "Scanning Completed." + ANSI_RESET) + return + + //DISCONNECT request from java + case strings.Contains(string(buf), "DISCONNECT"): + + // disconnect the vehicle with the address in the buffer + address := string(bytes.Trim([]byte(set[1]), "\x00")) + connectedDevice, ok := server.ConnectedDevices.Get(address) + if !ok { + displayError("Address: " + address + " could not be found.") + } + connectedDevice.Disconnect() + server.ConnectedDevices.Remove(address) + + conn.Write([]byte("DISCONNECT;SUCCESS\n")) + displayInfo(address + " Disconnected.") + + // CONNECT request from java + case strings.Contains(set[0], "CONNECT"): + // ignore 0x0 fillers + payload := bytes.Trim([]byte(set[1]), "\x00") + + device, _ := server.DiscoveredDevices.Get(string(payload)) + + // connect to device + connectedDevice, err := Adapter.Connect(device.Addresser, bluetooth.ConnectionParams{}) + if err != nil { + displayError(err.Error()) + } + + // add device to concurrent map of devices + server.ConnectedDevices.Set(device.Address, connectedDevice) + fmt.Println(ANSI_GREEN + "Connected to " + device.Address + ANSI_RESET) + + services, _ := connectedDevice.DiscoverServices([]bluetooth.UUID{ANKI_STR_SERVICE_UUID}) + if err != nil { + displayInfo(err.Error()) + } + + // Getting the writers and readers services + service := services[0] + characteristics, _ := service.DiscoverCharacteristics([]bluetooth.UUID{ANKI_STR_CHR_READ_UUID, ANKI_STR_CHR_WRITE_UUID}) + server.DeviceCharacteristics.Set(device.Address, characteristics) + + readService := characteristics[1] + + // Each time the vehicle sends a msg through bluetooth, the event is triggered + readService.EnableNotifications(func(value []byte) { + encodedBytes := hex.EncodeToString(value) + // Send the vehicle respond back to java + conn.Write([]byte(device.Address + ";" + encodedBytes + "\n")) + displayInfo("RECEIVED: [" + device.Address + ";" + encodedBytes + "]") + }) + + // terminate connection request to java + conn.Write([]byte("CONNECT;SUCCESS\n")) + fmt.Println(ANSI_GREEN + "CONNECT COMPLETED." + ANSI_RESET) + + /* Any other request is assumed to be a command given to the car. Each byte in the buffer represents an action that is + outlined in https://github.com/tenbergen/anki-drive-java/blob/master/Anki%20Drive%20Programming%20Guide.pdf + */ + default: + if len(set) == 2 { + // Get the writer characteristic + characteristics, _ := server.DeviceCharacteristics.Get(address) + writeService := characteristics[0] + payload, _ := hex.DecodeString(msg) + + // write payload to anki vehicle + _, err := writeService.WriteWithoutResponse(payload) + if err != nil { + displayError(err.Error()) + } + + displayInfo("SENDING: [" + strings.Replace(string(buf), "\n", "", -1) + "]") + } + } + }(buf) + } +} + +// function for scanning nearby vehicles returns a map of addresses to vehicles +func scan() cmap.ConcurrentMap[string, AnkiVehicle] { + devicesFound := cmap.New[AnkiVehicle]() + + channel := make(chan string, 1) + // func that is wrapped, so it can time out in some number of seconds + go func() { + + if !AdapterEnabled { + must("enable BLE stack", Adapter.Enable()) + AdapterEnabled = true + } + + err := Adapter.Scan(func(adapter *bluetooth.Adapter, device bluetooth.ScanResult) { + // only scan for devices that contain "Drive" for anki drive + if strings.Contains(device.LocalName(), "Drive") { + if !devicesFound.Has(device.Address.String()) { + var manufacturerData = "" + for _, data := range device.ManufacturerData() { + manufacturerData = "beef" + hex.EncodeToString(data) + } + var localname = "10603001202020204472697665" + // ANKI device properties + devicesFound.Set(strings.Replace(device.Address.String(), "-", "", -1), AnkiVehicle{ + Address: strings.Replace(device.Address.String(), "-", "", -1), + ManufacturerData: manufacturerData, + LocalName: localname, + Addresser: device.Address, + }) + } + } + }) + if err != nil { + return + } + //must("start scan", err) + //must("enable BLE stack", Adapter.StopScan()) + + }() + + // timeout scan + select { + case <-channel: + channel <- "break" + break + case <-time.After(5 * time.Second): + break + } + + return devicesFound +} + +func must(action string, err error) { + if err != nil { + panic("failed to " + action + ": " + err.Error()) + } +} + +func displayInfo(msg string) { + fmt.Println(ANSI_GREEN + "[INFO] " + ANSI_RESET + msg) +} + +func displayError(msg string) { + fmt.Print(ANSI_RED + "[ERROR] " + ANSI_RESET) + log.Fatalln(msg) +} diff --git a/src/main/golang/README.md b/src/main/golang/README.md new file mode 100644 index 00000000..712370ce --- /dev/null +++ b/src/main/golang/README.md @@ -0,0 +1,5 @@ +# Automotive Cyber Physical System (CPS) Bluetooth Server + +This server is a TCP/IP server that acts as a middle man between ANKI SDK for Java and the firmware on a ANKI Drive device. This server is meant to replace the node.js bluetooth server that comes with the ANKI SDK for Java. The server is a piece of research software that is meant to pair with Automotive-CPS. + + diff --git a/src/main/golang/go.mod b/src/main/golang/go.mod new file mode 100644 index 00000000..45d5e38f --- /dev/null +++ b/src/main/golang/go.mod @@ -0,0 +1,20 @@ +module automotivecps + +go 1.18 + +require ( + github.com/orcaman/concurrent-map/v2 v2.0.1 + tinygo.org/x/bluetooth v0.6.0 +) + +require ( + github.com/fatih/structs v1.1.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/godbus/dbus/v5 v5.0.3 // indirect + github.com/muka/go-bluetooth v0.0.0-20220830075246-0746e3a1ea53 // indirect + github.com/saltosystems/winrt-go v0.0.0-20220826130236-ddc8202da421 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/tinygo-org/cbgo v0.0.4 // indirect + golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/src/main/golang/go.sum b/src/main/golang/go.sum new file mode 100644 index 00000000..89729957 --- /dev/null +++ b/src/main/golang/go.sum @@ -0,0 +1,118 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/bgould/http v0.0.0-20190627042742-d268792bdee7/go.mod h1:BTqvVegvwifopl4KTEDth6Zezs9eR+lCWhvGKvkxJHE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= +github.com/glerchundi/subcommands v0.0.0-20181212083838-923a6ccb11f8/go.mod h1:r0g3O7Y5lrWXgDfcFBRgnAKzjmPgTzwoMC2ieB345FY= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hajimehoshi/go-jisx0208 v1.0.0/go.mod h1:yYxEStHL7lt9uL+AbdWgW9gBumwieDoZCiB1f/0X0as= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/muka/go-bluetooth v0.0.0-20220830075246-0746e3a1ea53 h1:zfLHhuGzmSbthZ00FfbEjgAHUOOj7NGiITojMTCFy6U= +github.com/muka/go-bluetooth v0.0.0-20220830075246-0746e3a1ea53/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= +github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= +github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sago35/go-bdf v0.0.0-20200313142241-6c17821c91c4/go.mod h1:rOebXGuMLsXhZAC6mF/TjxONsm45498ZyzVhel++6KM= +github.com/saltosystems/winrt-go v0.0.0-20220826130236-ddc8202da421 h1:eOgynOew0HzvLwtAsughGzqkrcuTJ6XFpT7+WNCuRNU= +github.com/saltosystems/winrt-go v0.0.0-20220826130236-ddc8202da421/go.mod h1:UvKm1lyhg+8ehk99i8g5Q7AX1LXUJgks0lRyAkG/ahQ= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8= +github.com/tdakkota/win32metadata v0.1.0/go.mod h1:77e6YvX0LIVW+O81fhWLnXAxxcyu/wdZdG7iwed7Fyk= +github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU= +github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= +github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +tinygo.org/x/bluetooth v0.6.0 h1:5RTUh28WBtWfRtwFcsDcdiCvlSWr9F7fHxRikQZW/Io= +tinygo.org/x/bluetooth v0.6.0/go.mod h1:tiW1IiKOupcsvM2CX0PwLsf6aZRL+ciSIqP2YlgYOtQ= +tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= +tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= +tinygo.org/x/drivers v0.16.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= +tinygo.org/x/drivers v0.19.0/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI= +tinygo.org/x/drivers v0.23.0/go.mod h1:J4+51Li1kcfL5F93kmnDWEEzQF3bLGz0Am3Q7E2a8/E= +tinygo.org/x/tinyfont v0.2.1/go.mod h1:eLqnYSrFRjt5STxWaMeOWJTzrKhXqpWw7nU3bPfKOAM= +tinygo.org/x/tinyfont v0.3.0/go.mod h1:+TV5q0KpwSGRWnN+ITijsIhrWYJkoUCp9MYELjKpAXk= +tinygo.org/x/tinyfs v0.1.0/go.mod h1:ysc8Y92iHfhTXeyEM9+c7zviUQ4fN9UCFgSOFfMWv20= +tinygo.org/x/tinyfs v0.2.0/go.mod h1:6ZHYdvB3sFYeMB3ypmXZCNEnFwceKc61ADYTYHpep1E= +tinygo.org/x/tinyterm v0.1.0/go.mod h1:/DDhNnGwNF2/tNgHywvyZuCGnbH3ov49Z/6e8LPLRR4= diff --git a/src/main/golang/serverconf.yml b/src/main/golang/serverconf.yml new file mode 100644 index 00000000..548d11bd --- /dev/null +++ b/src/main/golang/serverconf.yml @@ -0,0 +1,2 @@ +host: 127.0.0.1 +port: 5000 \ No newline at end of file diff --git a/src/main/golang/uml/server-sequence-diagram.pdf b/src/main/golang/uml/server-sequence-diagram.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fb8d03cedd4bc4acc852b26bc85b4beb5b46704a GIT binary patch literal 37570 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 xE;G?1s literal 0 HcmV?d00001