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/.travis.yml b/.travis.yml index 9bcf9994..69530a7d 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 + - openjdk8 \ No newline at end of file diff --git a/Anki Drive Programming Guide.pdf b/Anki Drive Programming Guide.pdf new file mode 100644 index 00000000..427ccb6e Binary files /dev/null and b/Anki Drive Programming Guide.pdf differ 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 ca4fe0b5..f3b641bb 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # 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. ## About @@ -32,16 +36,46 @@ 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/tenbergen/anki-drive-java cd anki-drive-java ./gradlew build ``` +### 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/ +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 / 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 npm_run +sudo ./gradlew server ``` ### Add the Java library @@ -55,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 @@ -73,7 +107,19 @@ Start scanning for vehicles: List vehicles = anki.findVehicles(); ``` +### Test File +To try a connection, start the server and run: +``` +./gradlew ankiConnectionTest +``` +which will execute +```java +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. diff --git a/build.gradle b/build.gradle index 211ef54e..120e018c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,38 +1,87 @@ -buildscript { - repositories { - jcenter() - } - - dependencies { - classpath 'com.moowork.gradle:gradle-node-plugin:0.13' - } -} - -apply plugin: 'java' -apply plugin: 'com.moowork.node' - -repositories { - jcenter() -} - -dependencies { - compile 'org.reflections:reflections:0.9.10' -} - -node { - version = '4.4.7' - npmVersion = '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 -} - -processResources.dependsOn(['npmInstall']) +buildscript { + repositories { + jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } + } +} + +plugins { + id "com.moowork.node" version "1.3.1" + id 'java-library' +} + +apply plugin: 'java-library' +apply plugin: 'maven' +apply plugin: 'com.moowork.node' +group = 'com.github.tenbergen' + +compileJava.options.fork = true + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +repositories { + jcenter() +} + +dependencies { + //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' + 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, dependsOn: npmInstall) { + script = file('src/main/nodejs/server.js') + 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' +} + +//added exec task for AnkiConnection test program +task ankiConnectionTest(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + 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/gradle.properties b/gradle.properties new file mode 100644 index 00000000..2942b253 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +targetJavaVersion=JavaVersion.VERSION_1_8 +org.gradle.jvm.version=JavaVersion.VERSION_1_8 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b11535d6..e41d512b 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 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-all.zip diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000..2dae2510 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,14 @@ +sudo: required +dist: trusty +addons: + apt: + packages: + - libudev-dev + +before_install: + - apt-get update + - apt-get install -y libudev-dev + +language: java +jdk: + - openjdk8 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4326982a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1268 @@ +{ + "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==" + }, + "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==" + }, + "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==", + "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=" + }, + "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", + "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": { + "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", + "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.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "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=" + }, + "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", + "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" + } + }, + "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", + "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=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "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" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "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=", + "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" + } + }, + "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", + "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=" + }, + "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==", + "requires": { + "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.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "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==" + }, + "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=" + }, + "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", + "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=" + }, + "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", + "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.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.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "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": "^1.2.5" + }, + "dependencies": { + "minimist": { + "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", + "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.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": { + "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.4.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz", + "integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==", + "requires": { + "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": { + "version": "1.0.0", + "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", + "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" + } + }, + "noble-mac": { + "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", + "noble": "^1.9.1", + "node-addon-api": "^1.1.0", + "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.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", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "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" + } + }, + "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.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.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.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-normalize-package-bin": "^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==", + "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=" + }, + "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=" + }, + "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=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "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=" + }, + "path-key": { + "version": "2.0.1", + "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", + "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==", + "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.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "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==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "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", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "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.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", + "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=" + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.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": { + "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" + } + }, + "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", + "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.6.3", + "resolved": "https://registry.npmjs.org/usb/-/usb-1.6.3.tgz", + "integrity": "sha512-23KYMjaWydACd8wgGKMQ4MNwFspAT6Xeim4/9Onqe5Rz/nMb4TM/WHL+qPT0KNFxzNKzAs63n1xQWGEtgaQ2uw==", + "optional": true, + "requires": { + "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": { + "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" + } + }, + "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" + } + }, + "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", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "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.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } +} diff --git a/package.json b/package.json index 0d1579aa..a19e88ab 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-mac": "https://github.com/Timeular/noble-mac.git" } } diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e07a388f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'anki-drive-java' +include ':Library' \ No newline at end of file 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 00000000..fb8d03ce Binary files /dev/null and b/src/main/golang/uml/server-sequence-diagram.pdf differ diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 00000000..cf9f44d6 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: edu.oswego.cs.CPSLab.AnkiConnectionTest + 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); + } +} diff --git a/src/main/java/de/adesso/anki/AnkiConnector.java b/src/main/java/de/adesso/anki/AnkiConnector.java index ad9217b6..da70438f 100644 --- a/src/main/java/de/adesso/anki/AnkiConnector.java +++ b/src/main/java/de/adesso/anki/AnkiConnector.java @@ -1,137 +1,169 @@ -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;"); - - 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 boolean debug = false; + + 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 void toggleDebugMode() { + this.debug = !this.debug; + } + + public boolean getDebugMode() { + return this.debug; + } + + 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/java/de/adesso/anki/Model.java b/src/main/java/de/adesso/anki/Model.java index acdba004..2437d406 100644 --- a/src/main/java/de/adesso/anki/Model.java +++ b/src/main/java/de/adesso/anki/Model.java @@ -1,45 +1,56 @@ -package de.adesso.anki; - -import java.util.HashMap; -import java.util.Map; - -/** - * Enumerates all currently known Anki vehicle models. - * - * @author Yannick Eckey - */ -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"); - - 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/RoadmapScanner.java b/src/main/java/de/adesso/anki/RoadmapScanner.java index 82736618..d8fcaa06 100644 --- a/src/main/java/de/adesso/anki/RoadmapScanner.java +++ b/src/main/java/de/adesso/anki/RoadmapScanner.java @@ -1,69 +1,153 @@ 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; +//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 + * 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() + * 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) + * @author Ka Ying Chan (kchan2@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 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(); - } + 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(); + } + + /** + * 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)); + 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() { + return roadmap.isComplete(); + } + + 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; + } + + private void handlePositionUpdate(LocalizationPositionUpdateMessage message) { + lastPosition = message; + } + + protected void handleTransitionUpdate(LocalizationTransitionUpdateMessage message) { + if (lastPosition != null) { + roadmap.add( + lastPosition.getRoadPieceId(), + lastPosition.getLocationId(), + lastPosition.isParsedReverse() + ); + + pieceIDs.add(lastPosition.getRoadPieceId()); + reverses.add(lastPosition.isParsedReverse()); +// System.out.println("Added a piece... " + pieceIDs.get(pieceIDs.size() - 1)); + + if (lastPosition.getRoadPieceId() == 33 || lastPosition.getRoadPieceId() == 34) { + initReverse = lastPosition.isParsedReverse(); + } + + if (roadmap.isComplete()) { + this.stopScanning(); + } + } } - } - } diff --git a/src/main/java/de/adesso/anki/Vehicle.java b/src/main/java/de/adesso/anki/Vehicle.java index 175d240b..b80efd3a 100644 --- a/src/main/java/de/adesso/anki/Vehicle.java +++ b/src/main/java/de/adesso/anki/Vehicle.java @@ -1,138 +1,152 @@ -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; - } - - 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; + + public AnkiConnector getAnkiConnector() { return this.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); + if (anki.getDebugMode()) 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/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(); 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 bc6b447d..89abec70 100644 --- a/src/main/java/de/adesso/anki/roadmap/Roadmap.java +++ b/src/main/java/de/adesso/anki/roadmap/Roadmap.java @@ -1,63 +1,326 @@ 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; - -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; - } - +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. + * + * @author adesso AG + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @version 2020-05-12 + * @since 2016-12-13 + */ +public class Roadmap implements Serializable, Cloneable { + + private Section nonNormalizedAnchor = null; + private Section anchor; + private Section current; + private static final long serialVersionUID = -8832115043850834353L; + + 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. + * @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); + } + + 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); + } + + /** + * 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; + } + + /** + * Computes the length of the Roadmap in number of Roadpieces. + * + * @return The number of Roadpieces in this Roadmap, identical to toList().size() + * @version 2020-05-11 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-10 + */ + 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. + * @version 2020-05-13 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-10 + */ + @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 + } 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 + // 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" + //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. + 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. + * @version 2020-05-11 + * @author Bastian Tenbergen (bastian.tenbergen@oswego.edu) + * @since 2020-05-10 + */ + 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(); + } + + 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. + * @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 | NullPointerException 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/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); + } + +} 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/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; diff --git a/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java new file mode 100644 index 00000000..8095f284 --- /dev/null +++ b/src/main/java/edu/oswego/cs/CPSLab/AnkiConnectionTest.java @@ -0,0 +1,175 @@ +package edu.oswego.cs.CPSLab; + +import de.adesso.anki.AnkiConnector; +import de.adesso.anki.MessageListener; +import de.adesso.anki.Vehicle; +import de.adesso.anki.messages.*; +import de.adesso.anki.messages.LightsPatternMessage.LightConfig; +import de.adesso.anki.roadmap.roadpieces.FinishRoadpiece; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +/** + * 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/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 { + + public static void main(String[] args) throws InterruptedException { + System.out.println("Launching connector..."); + 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(); + + 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 and doing stuff to your cars."); + + Iterator iter = vehicles.iterator(); + while (iter.hasNext()) { + Vehicle v = iter.next(); + AnkiConnectionTest act = new AnkiConnectionTest(v); + act.run(); + } + } + anki.close(); + System.out.println("Test complete."); + System.exit(0); + } + + private Vehicle v; + private long interval = 10; + + public AnkiConnectionTest(Vehicle v) { + this.v = v; + } + + public void run() throws InterruptedException { + 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(" done."); + + System.out.print(" Sending ping..."); + //we have to set up a response handler first, in order to handle async responses + PingResponseHandler prh = new PingResponseHandler(); + //now we tell the car, who is listening to the replies + v.addMessageListener(PingResponseMessage.class, prh); + //now we can actually send it. + v.sendMessage(new PingRequestMessage()); + prh.pingSentAt = System.currentTimeMillis(); + System.out.print(" sent. Waiting at most 10secs for pong..."); + 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."); + 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, + //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); + //we should sleep for at least some factor of the ping roundtrip to give the Vehicle time to set the lights + 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. + v.sendMessage(new SetSpeedMessage(500, 100)); + + 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 ) { + Thread.sleep(interval); + } + v.sendMessage(new SetSpeedMessage(0, 12500)); + v.disconnect(); + System.out.println("Disconnected from " + v); + } + + /** + * 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 PingResponseHandler implements MessageListener { + private boolean pingReceived = false; + private long pingSentAt = System.currentTimeMillis(); + private long roundTrip = -1; + + @Override + public void messageReceived(PingResponseMessage m) { + this.pingReceived = true; + this.roundTrip = System.currentTimeMillis() - pingSentAt; + } + } + + /** + * Handles the response from the vehicle from the BatteryLevelRequestMessage. + * Updates the battery level whenever a BatteryLEvelRequestMessage is sent. + */ + private class BatteryLevelResponseHandler implements MessageListener { + private int batt_level; + + @Override + public void messageReceived(BatteryLevelResponseMessage m) { + this.batt_level = m.getBatteryLevel(); + System.out.println(" Battery Level is: " + this.batt_level + " mV"); + } + } + + /** + * 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) { + if (m.getRoadPieceId() == finishLineId) this.stop = true; + } + } +} 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..2bfa4749 --- /dev/null +++ b/src/main/java/edu/oswego/cs/CPSLab/RoadmapScannerTest.java @@ -0,0 +1,124 @@ +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 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..."); + + AnkiConnector anki = null; + try { + 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."); + System.exit(1); + } + 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(); + } +} diff --git a/src/main/nodejs/server.js b/src/main/nodejs/server.js index 8eed257b..80844194 100644 --- a/src/main/nodejs/server.js +++ b/src/main/nodejs/server.js @@ -1,116 +1,139 @@ -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.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) { - 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\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; - - 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") +var net = require('net'); +var noble = require('noble-mac'); +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); +// 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. + //(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'), + Buffer.from(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]]; + var buffer = Buffer.from(command[1], 'hex'); + vehicle.writer.write(buffer, true); + } + } + }); + }); +}); + +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")