diff --git a/README.md b/README.md index 15c47920..6b6a271e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ This project enables Node.js applications to control devices that speak the REV Hub Serial Protocol (such as the REV Robotics Expansion Hub). +### WiFi-control on Node.js + +If you are using node.js and wish to communicate +with a Control Hub via WiFi, you will need to add +a dependency on [ws](https://www.npmjs.com/package/ws). + +## USB-control of Control Hub + +If you wish to communicate with a Control Hub over +USB, you will need to set up port forwarding. This +requires a dependency on [adbkit](https://www.npmjs.com/package/adbkit). +See [adbkit setup](packages/sample/src/adb-setup.ts) +for an example of setting up port forwarding. + ## Package structure This project uses [lerna](https://lerna.js.org) for diff --git a/control-hub-cli.bat b/control-hub-cli.bat new file mode 100644 index 00000000..538193a6 --- /dev/null +++ b/control-hub-cli.bat @@ -0,0 +1,2 @@ + +node.exe output.js %* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 37c7dd3f..4a4a9f00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,11 +3,15 @@ "lockfileVersion": 2, "requires": true, "packages": { + "packages": {}, "": { "name": "root", "workspaces": [ "packages/*" ], + "dependencies": { + "node-addon-api": "^7.0.0" + }, "devDependencies": { "lerna": "^7.0.0", "nx": "^16.2.1", @@ -102,6 +106,11 @@ "node": ">=4" } }, + "node_modules/@devicefarmer/minicap-prebuilt": { + "version": "2.7.1", + "license": "Apache-2.0", + "optional": true + }, "node_modules/@gar/promisify": { "version": "1.1.3", "dev": true, @@ -688,6 +697,54 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "license": "BSD-3-Clause" + }, + "node_modules/@rev-robotics/control-hub": { + "resolved": "packages/control-hub", + "link": true + }, "node_modules/@rev-robotics/distance-sensor": { "resolved": "packages/distance-sensor", "link": true @@ -933,6 +990,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@types/debug": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "license": "MIT" + }, "node_modules/@types/minimatch": { "version": "3.0.5", "dev": true, @@ -943,16 +1012,103 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "0.7.31", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "16.18.25", - "dev": true, "license": "MIT" }, + "node_modules/@types/node-forge": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "dev": true, "license": "MIT" }, + "node_modules/@types/semver": { + "version": "7.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@u4/adbkit": { + "version": "4.1.19", + "license": "Apache-2.0", + "dependencies": { + "@u4/adbkit-logcat": "2.1.2", + "@u4/adbkit-monkey": "^1.0.5", + "@u4/minicap-prebuilt": "^1.0.0", + "@xmldom/xmldom": "^0.8.7", + "commander": "9.4.1", + "debug": "~4.3.4", + "get-port": "5.1.1", + "node-forge": "^1.3.1", + "promise-duplex": "^6.0.0", + "promise-readable": "^6.0.0", + "protobufjs": "^6.11.3", + "xpath": "^0.0.32" + }, + "bin": { + "adbkit": "bin/adbkit" + }, + "engines": { + "node": ">= 12.20.0" + }, + "funding": { + "url": "https://github.com/sponsors/urielch" + }, + "optionalDependencies": { + "@devicefarmer/minicap-prebuilt": "^2.7.1" + } + }, + "node_modules/@u4/adbkit-logcat": { + "version": "2.1.2", + "license": "Apache-2.0", + "engines": { + "node": ">= 12.20.0" + } + }, + "node_modules/@u4/adbkit-monkey": { + "version": "1.0.5", + "license": "Apache-2.0", + "engines": { + "node": ">= 12.20.0" + } + }, + "node_modules/@u4/adbkit/node_modules/commander": { + "version": "9.4.1", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@u4/minicap-prebuilt": { + "version": "1.0.0", + "license": "Apache-2.0" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "dev": true, @@ -1149,12 +1305,10 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "dev": true, "license": "MIT" }, "node_modules/axios": { "version": "1.4.0", - "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.0", @@ -1493,7 +1647,6 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -1659,6 +1812,15 @@ "node": ">=14" } }, + "node_modules/core-js": { + "version": "3.32.0", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "dev": true, @@ -1782,7 +1944,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -2146,7 +2307,6 @@ }, "node_modules/follow-redirects": { "version": "1.15.2", - "dev": true, "funding": [ { "type": "individual", @@ -2191,7 +2351,6 @@ }, "node_modules/form-data": { "version": "4.0.0", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -2286,7 +2445,6 @@ }, "node_modules/get-port": { "version": "5.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2999,6 +3157,13 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jackspeak": { "version": "2.2.1", "dev": true, @@ -3510,6 +3675,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "4.0.0", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "7.18.3", "dev": true, @@ -3757,7 +3926,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -3765,7 +3933,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -3998,8 +4165,9 @@ } }, "node_modules/node-addon-api": { - "version": "6.1.0", - "license": "MIT" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" }, "node_modules/node-fetch": { "version": "2.6.7", @@ -4020,6 +4188,13 @@ } } }, + "node_modules/node-forge": { + "version": "1.3.1", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-gyp": { "version": "9.3.1", "dev": true, @@ -5322,11 +5497,33 @@ "dev": true, "license": "MIT" }, + "node_modules/promise-duplex": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "core-js": "^3.6.5", + "promise-readable": "^6.0.0", + "promise-writable": "^6.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "dev": true, "license": "ISC" }, + "node_modules/promise-readable": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "core-js": "^3.6.5" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/promise-retry": { "version": "2.0.1", "dev": true, @@ -5339,6 +5536,13 @@ "node": ">=10" } }, + "node_modules/promise-writable": { + "version": "6.0.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/promzard": { "version": "1.0.0", "dev": true, @@ -5350,6 +5554,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/protobufjs": { + "version": "6.11.4", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, "node_modules/protocols": { "version": "2.0.1", "dev": true, @@ -5357,7 +5585,6 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "dev": true, "license": "MIT" }, "node_modules/pump": { @@ -5862,7 +6089,6 @@ }, "node_modules/semver": { "version": "7.5.1", - "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" @@ -5876,7 +6102,6 @@ }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -7051,6 +7276,32 @@ "node": ">=6" } }, + "node_modules/ws": { + "version": "8.13.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xpath": { + "version": "0.0.32", + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "dev": true, @@ -7069,7 +7320,6 @@ }, "node_modules/yallist": { "version": "4.0.0", - "dev": true, "license": "ISC" }, "node_modules/yargs": { @@ -7097,6 +7347,22 @@ "node": ">=10" } }, + "packages/control-hub": { + "name": "@rev-robotics/control-hub", + "version": "0.1.0", + "dependencies": { + "@rev-robotics/rev-hub-core": "^1.0.0", + "axios": "^1.4.0", + "isomorphic-ws": "^5.0.0", + "semver": "^7.5.1" + }, + "devDependencies": { + "@types/node": "^16.18.18", + "@types/semver": "^7.5.0", + "@types/ws": "^8.5.4", + "typescript": "^5.0.2" + } + }, "packages/core": { "name": "@rev-robotics/rev-hub-core", "version": "1.0.0", @@ -7147,16 +7413,40 @@ "typescript": "^5.0.2" } }, + "packages/librhsp/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "packages/node_modules/get-port": { + "version": "6.1.2", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/sample": { "name": "rev-hub-cli", "version": "1.0.0", "dependencies": { + "@rev-robotics/control-hub": "^0.1.0", "@rev-robotics/distance-sensor": "^1.0.0", "@rev-robotics/expansion-hub": "^1.0.0", - "commander": "^10.0.1" + "@u4/adbkit": "^4.1.19", + "commander": "^10.0.1", + "get-port": "^6.1.2", + "ws": "8.13.0" }, "bin": { "rev-hub-cli": "revhub" + }, + "devDependencies": { + "@types/debug": "^4.1.8", + "@types/node-forge": "^1.3.2", + "@types/ws": "^8.5.4" } } }, @@ -7221,6 +7511,10 @@ } } }, + "@devicefarmer/minicap-prebuilt": { + "version": "2.7.1", + "optional": true + }, "@gar/promisify": { "version": "1.1.3", "dev": true @@ -7610,6 +7904,53 @@ "dev": true, "optional": true }, + "@protobufjs/aspromise": { + "version": "1.1.2" + }, + "@protobufjs/base64": { + "version": "1.1.2" + }, + "@protobufjs/codegen": { + "version": "2.0.4" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2" + }, + "@protobufjs/inquire": { + "version": "1.1.0" + }, + "@protobufjs/path": { + "version": "1.1.2" + }, + "@protobufjs/pool": { + "version": "1.1.0" + }, + "@protobufjs/utf8": { + "version": "1.1.0" + }, + "@rev-robotics/control-hub": { + "version": "file:packages/control-hub", + "requires": { + "@rev-robotics/rev-hub-core": "^1.0.0", + "@types/node": "^16.18.18", + "@types/semver": "^7.5.0", + "@types/ws": "^8.5.4", + "axios": "^1.4.0", + "isomorphic-ws": "^5.0.0", + "semver": "^7.5.1", + "typescript": "^5.0.2" + } + }, "@rev-robotics/distance-sensor": { "version": "file:packages/distance-sensor", "requires": { @@ -7646,6 +7987,13 @@ "node-gyp-build": "^4.6.0", "prebuildify": "^5.0.1", "typescript": "^5.0.2" + }, + "dependencies": { + "node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + } } }, "@serialport/binding-mock": { @@ -7761,6 +8109,16 @@ } } }, + "@types/debug": { + "version": "4.1.8", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/long": { + "version": "4.0.2" + }, "@types/minimatch": { "version": "3.0.5", "dev": true @@ -7769,14 +8127,70 @@ "version": "1.2.2", "dev": true }, - "@types/node": { - "version": "16.18.25", + "@types/ms": { + "version": "0.7.31", "dev": true }, + "@types/node": { + "version": "16.18.25" + }, + "@types/node-forge": { + "version": "1.3.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.1", "dev": true }, + "@types/semver": { + "version": "7.5.0", + "dev": true + }, + "@types/ws": { + "version": "8.5.5", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@u4/adbkit": { + "version": "4.1.19", + "requires": { + "@devicefarmer/minicap-prebuilt": "^2.7.1", + "@u4/adbkit-logcat": "2.1.2", + "@u4/adbkit-monkey": "^1.0.5", + "@u4/minicap-prebuilt": "^1.0.0", + "@xmldom/xmldom": "^0.8.7", + "commander": "9.4.1", + "debug": "~4.3.4", + "get-port": "5.1.1", + "node-forge": "^1.3.1", + "promise-duplex": "^6.0.0", + "promise-readable": "^6.0.0", + "protobufjs": "^6.11.3", + "xpath": "^0.0.32" + }, + "dependencies": { + "commander": { + "version": "9.4.1" + } + } + }, + "@u4/adbkit-logcat": { + "version": "2.1.2" + }, + "@u4/adbkit-monkey": { + "version": "1.0.5" + }, + "@u4/minicap-prebuilt": { + "version": "1.0.0" + }, + "@xmldom/xmldom": { + "version": "0.8.10" + }, "@yarnpkg/lockfile": { "version": "1.1.0", "dev": true @@ -7904,12 +8318,10 @@ "dev": true }, "asynckit": { - "version": "0.4.0", - "dev": true + "version": "0.4.0" }, "axios": { "version": "1.4.0", - "dev": true, "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -8115,7 +8527,6 @@ }, "combined-stream": { "version": "1.0.8", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -8227,6 +8638,9 @@ "meow": "^8.1.2" } }, + "core-js": { + "version": "3.32.0" + }, "core-util-is": { "version": "1.0.3", "dev": true @@ -8298,8 +8712,7 @@ "dev": true }, "delayed-stream": { - "version": "1.0.0", - "dev": true + "version": "1.0.0" }, "delegates": { "version": "1.0.0", @@ -8538,8 +8951,7 @@ "dev": true }, "follow-redirects": { - "version": "1.15.2", - "dev": true + "version": "1.15.2" }, "foreground-child": { "version": "3.1.1", @@ -8557,7 +8969,6 @@ }, "form-data": { "version": "4.0.0", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8621,8 +9032,7 @@ } }, "get-port": { - "version": "5.1.1", - "dev": true + "version": "5.1.1" }, "get-stream": { "version": "6.0.0", @@ -9073,6 +9483,10 @@ "version": "3.0.1", "dev": true }, + "isomorphic-ws": { + "version": "5.0.0", + "requires": {} + }, "jackspeak": { "version": "2.2.1", "dev": true, @@ -9429,6 +9843,9 @@ "is-unicode-supported": "^0.1.0" } }, + "long": { + "version": "4.0.0" + }, "lru-cache": { "version": "7.18.3", "dev": true @@ -9590,12 +10007,10 @@ } }, "mime-db": { - "version": "1.52.0", - "dev": true + "version": "1.52.0" }, "mime-types": { "version": "2.1.35", - "dev": true, "requires": { "mime-db": "1.52.0" } @@ -9741,7 +10156,9 @@ } }, "node-addon-api": { - "version": "6.1.0" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" }, "node-fetch": { "version": "2.6.7", @@ -9750,6 +10167,9 @@ "whatwg-url": "^5.0.0" } }, + "node-forge": { + "version": "1.3.1" + }, "node-gyp": { "version": "9.3.1", "dev": true, @@ -10600,10 +11020,24 @@ "version": "2.0.1", "dev": true }, + "promise-duplex": { + "version": "6.0.0", + "requires": { + "core-js": "^3.6.5", + "promise-readable": "^6.0.0", + "promise-writable": "^6.0.0" + } + }, "promise-inflight": { "version": "1.0.1", "dev": true }, + "promise-readable": { + "version": "6.0.0", + "requires": { + "core-js": "^3.6.5" + } + }, "promise-retry": { "version": "2.0.1", "dev": true, @@ -10612,6 +11046,9 @@ "retry": "^0.12.0" } }, + "promise-writable": { + "version": "6.0.0" + }, "promzard": { "version": "1.0.0", "dev": true, @@ -10619,13 +11056,30 @@ "read": "^2.0.0" } }, + "protobufjs": { + "version": "6.11.4", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "protocols": { "version": "2.0.1", "dev": true }, "proxy-from-env": { - "version": "1.1.0", - "dev": true + "version": "1.1.0" }, "pump": { "version": "3.0.0", @@ -10878,9 +11332,16 @@ "rev-hub-cli": { "version": "file:packages/sample", "requires": { + "@rev-robotics/control-hub": "^0.1.0", "@rev-robotics/distance-sensor": "^1.0.0", "@rev-robotics/expansion-hub": "^1.0.0", - "commander": "^10.0.1" + "@types/debug": "^4.1.8", + "@types/node-forge": "^1.3.2", + "@types/ws": "^8.5.4", + "@u4/adbkit": "^4.1.19", + "commander": "^10.0.1", + "get-port": "^6.1.2", + "ws": "8.13.0" } }, "rimraf": { @@ -10932,14 +11393,12 @@ }, "semver": { "version": "7.5.1", - "dev": true, "requires": { "lru-cache": "^6.0.0" }, "dependencies": { "lru-cache": { "version": "6.0.0", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -11731,6 +12190,13 @@ } } }, + "ws": { + "version": "8.13.0", + "requires": {} + }, + "xpath": { + "version": "0.0.32" + }, "xtend": { "version": "4.0.2", "dev": true @@ -11740,8 +12206,7 @@ "dev": true }, "yallist": { - "version": "4.0.0", - "dev": true + "version": "4.0.0" }, "yargs": { "version": "16.2.0", diff --git a/package.json b/package.json index 7a83d032..83532300 100644 --- a/package.json +++ b/package.json @@ -8,5 +8,8 @@ "lerna": "^7.0.0", "prettier": "2.8.8", "nx": "^16.2.1" + }, + "dependencies": { + "node-addon-api": "^7.0.0" } } diff --git a/packages/control-hub/package.json b/packages/control-hub/package.json new file mode 100644 index 00000000..0a419861 --- /dev/null +++ b/packages/control-hub/package.json @@ -0,0 +1,22 @@ +{ + "name": "@rev-robotics/control-hub", + "version": "0.1.0", + "description": "High level library for the REV Robotics Control Hub", + "main": "dist/index.js", + "type": "module", + "dependencies": { + "isomorphic-ws": "^5.0.0", + "axios": "^1.4.0", + "semver": "^7.5.1", + "@rev-robotics/rev-hub-core": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^16.18.18", + "@types/semver": "^7.5.0", + "@types/ws": "^8.5.4", + "typescript": "^5.0.2" + }, + "scripts": { + "build": "tsc" + } +} diff --git a/packages/control-hub/src/index.ts b/packages/control-hub/src/index.ts new file mode 100644 index 00000000..59dcd373 --- /dev/null +++ b/packages/control-hub/src/index.ts @@ -0,0 +1,12 @@ +import { ControlHubInternal } from "./internal/ControlHub.js"; +import { ControlHub } from "@rev-robotics/rev-hub-core"; + +export async function openWifiControlHub( + serialNumber: string, + moduleAddress: number, + port: number, +): Promise { + let hub = new ControlHubInternal(serialNumber, moduleAddress); + await hub.open("127.0.0.1", port + 1); + return hub; +} diff --git a/packages/control-hub/src/internal/ControlHub.ts b/packages/control-hub/src/internal/ControlHub.ts new file mode 100644 index 00000000..274873e4 --- /dev/null +++ b/packages/control-hub/src/internal/ControlHub.ts @@ -0,0 +1,703 @@ +import axios from "axios"; +import semver from "semver"; +import WebSocket from "isomorphic-ws"; +import { + BulkInputData, + ClosedLoopControlAlgorithm, + ControlHub, + DebugGroup, + DigitalChannelDirection, + DigitalState, + ExpansionHub, + I2CReadStatus, + I2CSpeedCode, + I2CWriteStatus, + LedPattern, + ModuleInterface, + ModuleStatus, + MotorMode, + ParentExpansionHub, + ParentRevHub, + PidCoefficients, + PidfCoefficients, + RevHub, + RevHubType, + Rgb, + TimeoutError, + VerbosityLevel, + Version, + ImuData, + Quaternion, + AngularVelocity, +} from "@rev-robotics/rev-hub-core"; +import { clearTimeout } from "timers"; +import { ControlHubConnectedExpansionHub } from "./ControlHubConnectedExpansionHub.js"; + +export class ControlHubInternal implements ControlHub { + readonly isOpen: boolean = true; + readonly moduleAddress: number; + responseTimeoutMs: number = 0; + type: RevHubType = RevHubType.ControlHub; + readonly serialNumber: string; + + /** + * All children connected over USB, as well as the one embedded hub. + */ + readonly children: RevHub[] = []; + + /** + * All children connected over USB. + */ + readonly usbChildren: RevHub[] = []; + + private id: any | null; + private keyGenerator = 0; + + private supportedManualControlMajorVersion = 0; + private supportedManualControlMinorVersion = 1; + + /** + * Whether the websocket is currently open. + */ + isConnected = false; + + /** + * The board for this control hub. All Expansion Hub commands go through + * this delegate. + */ + private embedded!: ControlHubConnectedExpansionHub; + + private webSocketConnection!: WebSocket; + private currentActiveCommands = new Map< + any, + (response: any | undefined, error: any | undefined) => void + >(); + + constructor(serialNumber: string, moduleAddress: number) { + this.serialNumber = serialNumber; + this.moduleAddress = moduleAddress; + } + + isParent(): this is ParentRevHub { + return true; + } + + isControlHub(): this is ControlHub { + return true; + } + + async open(ip: string, port: number): Promise { + this.webSocketConnection = new WebSocket(`ws://${ip}:${port}`); + + this.webSocketConnection.on("message", (data) => { + let rawMessage = JSON.parse(data.toString()); + + if (rawMessage.commandKey !== undefined) { + let key = rawMessage.commandKey; + let callback = this.currentActiveCommands.get(key); + + let response = rawMessage.response + ? JSON.parse(rawMessage.response) + : undefined; + let error = rawMessage.error ? JSON.parse(rawMessage.error) : undefined; + + if (callback) { + callback(response, error); + } + } else { + //we have a message + if (rawMessage.type === "sessionEnded") { + let allHubs = this.flattenChildren(); + for (let hub of allHubs) { + hub.emit("sessionEnded"); + } + } else if (rawMessage.type === "hubAddressChanged") { + //notify that hub address changed + let payload = rawMessage.payload; + let addressChangedPayload = JSON.parse(payload); + let handles: number[] = addressChangedPayload.hIds; + + let allHubs = this.flattenChildren(); + + for (let hub of allHubs) { + if (handles.includes(hub.id)) { + hub.moduleAddress = addressChangedPayload.na; + hub.emit( + "addressChanged", + addressChangedPayload.oa, + addressChangedPayload.na, + ); + } + } + } else if (rawMessage.type === "hubStatusChanged") { + let payloadString = rawMessage.payload; + let payload = JSON.parse(payloadString); + let handles = payload.hIds; + let status: ModuleStatus = { + statusWord: payload.sBf, + motorAlerts: payload.maBf, + }; + + let allHubs = this.flattenChildren(); + + for (let hub of allHubs) { + if (handles.includes(hub.id)) { + hub.emit("statusChanged", status); + } + } + } + } + }); + + this.webSocketConnection.on("close", () => { + console.log(`Connection was closed`); + this.isConnected = false; + }); + + this.webSocketConnection.on("error", (_: WebSocket, err: Error) => { + console.log("Websocket error"); + console.log(err); + this.isConnected = false; + }); + + return new Promise((resolve, reject) => { + this.webSocketConnection.on("open", async () => { + this.isConnected = true; + await this.subscribe(); + let apiVersion: { majorVersion: number; minorVersion: number } = + await this.sendCommand("start", {}); + + this.id = await this.openHub("(embedded)", this.moduleAddress); + + this.embedded = new ControlHubConnectedExpansionHub( + true, + RevHubType.ControlHub, + this.sendCommand.bind(this), + "(embedded)", + this.moduleAddress, + this.id, + ); + + if ( + apiVersion.majorVersion !== this.supportedManualControlMajorVersion || + apiVersion.minorVersion < this.supportedManualControlMinorVersion + ) { + reject( + new Error( + `API Version ${apiVersion.majorVersion}.${apiVersion.minorVersion} is not supported`, + ), + ); + } + + resolve(); + }); + }); + } + + async isWiFiConnected(): Promise { + try { + let response = await axios.get("http://192.168.43.1:8080/js/rcInfo.json", { + timeout: 1000, + }); + if (response.data) { + let rcVersion: string | undefined = response.data.sdkVersion; + if(rcVersion === undefined) { + return false; + } + return semver.satisfies(rcVersion, ">=8.2"); + } + + return false; + } catch (e) { + return false; + } + } + + async subscribe(): Promise { + let payload = { + namespace: "system", + type: "subscribeToNamespace", + payload: "MC", + }; + this.webSocketConnection.send(JSON.stringify(payload)); + } + + async openHub( + serialNumber: string, + parentAddress: number, + address: number = parentAddress, + ): Promise { + return await this.sendCommand("openHub", { + parentSerialNumber: serialNumber, + parentHubAddress: parentAddress, + hubAddress: address, + }); + } + + // ToDo(landry): Always call close() on the parent hub when the sample exits + close() { + this.embedded.close(); + this.sendCommand("stop", {}).then(() => this.webSocketConnection.close()); + } + + async getAnalogInput(channel: number): Promise { + return this.embedded.getAnalogInput(channel); + } + + async get5VBusVoltage(): Promise { + return this.embedded.get5VBusVoltage(); + } + + async getBatteryCurrent(): Promise { + return this.embedded.getBatteryCurrent(); + } + + async getBatteryVoltage(): Promise { + return this.embedded.getBatteryVoltage(); + } + + async getDigitalBusCurrent(): Promise { + return this.embedded.getDigitalBusCurrent(); + } + + async getI2CCurrent(): Promise { + return this.embedded.getI2CCurrent(); + } + + async getMotorCurrent(motorChannel: number): Promise { + return this.embedded.getMotorCurrent(motorChannel); + } + + async getServoCurrent(): Promise { + return this.embedded.getServoCurrent(); + } + + async getTemperature(): Promise { + return this.embedded.getTemperature(); + } + + async getBulkInputData(): Promise { + return this.embedded.getBulkInputData(); + } + + async getFTDIResetControl(): Promise { + return this.embedded.getFTDIResetControl(); + } + + async getI2CChannelConfiguration(i2cChannel: number): Promise { + return this.embedded.getI2CChannelConfiguration(i2cChannel); + } + + async getInterfacePacketID( + interfaceName: string, + functionNumber: number, + ): Promise { + return this.embedded.getInterfacePacketID(interfaceName, functionNumber); + } + + async getModuleLedColor(): Promise { + return this.embedded.getModuleLedColor(); + } + + getModuleLedPattern(): Promise { + return this.embedded.getModuleLedPattern(); + } + + async getModuleStatus(clearStatusAfterResponse: boolean): Promise { + return this.embedded.getModuleStatus(clearStatusAfterResponse); + } + + async getMotorAtTarget(motorChannel: number): Promise { + return this.embedded.getMotorAtTarget(motorChannel); + } + + async getMotorChannelCurrentAlertLevel(motorChannel: number): Promise { + return this.embedded.getMotorChannelCurrentAlertLevel(motorChannel); + } + + async getMotorChannelEnable(motorChannel: number): Promise { + return this.embedded.getMotorChannelEnable(motorChannel); + } + + async getMotorChannelMode( + motorChannel: number, + ): Promise<{ motorMode: number; floatAtZero: boolean }> { + return this.embedded.getMotorChannelMode(motorChannel); + } + + async getMotorConstantPower(motorChannel: number): Promise { + return this.embedded.getMotorConstantPower(motorChannel); + } + + async getMotorEncoderPosition(motorChannel: number): Promise { + return this.embedded.getMotorEncoderPosition(motorChannel); + } + + async getMotorTargetPosition( + motorChannel: number, + ): Promise<{ targetPosition: number; targetTolerance: number }> { + return this.embedded.getMotorTargetPosition(motorChannel); + } + + async getMotorTargetVelocity(motorChannel: number): Promise { + return this.embedded.getMotorTargetVelocity(motorChannel); + } + + async getPhoneChargeControl(): Promise { + return this.embedded.getPhoneChargeControl(); + } + + async getServoConfiguration(servoChannel: number): Promise { + return this.embedded.getServoConfiguration(servoChannel); + } + + async getServoEnable(servoChannel: number): Promise { + return this.embedded.getServoEnable(servoChannel); + } + + async getServoPulseWidth(servoChannel: number): Promise { + return this.embedded.getServoPulseWidth(servoChannel); + } + + async injectDataLogHint(hintText: string): Promise { + return this.embedded.injectDataLogHint(hintText); + } + + isExpansionHub(): this is ExpansionHub { + return true; + } + + on(eventName: "error", listener: (error: Error) => void): this; + on(eventName: "statusChanged", listener: (status: ModuleStatus) => void): this; + on( + eventName: "addressChanged", + listener: (oldAddress: number, newAddress: number) => void, + ): this; + on(eventName: "sessionEnded", listener: () => void): this; + + on( + eventName: "error" | "statusChanged" | "addressChanged" | "sessionEnded", + listener: (...args: any) => void, + ): this { + this.embedded.on(eventName, listener); + return this; + } + + async queryInterface(interfaceName: string): Promise { + return this.embedded.queryInterface(interfaceName); + } + + readI2CRegister( + i2cChannel: number, + targetAddress: number, + numBytesToRead: number, + register: number, + ): Promise { + return this.embedded.readI2CRegister( + i2cChannel, + targetAddress, + numBytesToRead, + register, + ); + } + + readI2CMultipleBytes( + i2cChannel: number, + slaveAddress: number, + numBytesToRead: number, + ): Promise { + return this.embedded.readI2CMultipleBytes( + i2cChannel, + slaveAddress, + numBytesToRead, + ); + } + + readI2CSingleByte(i2cChannel: number, slaveAddress: number): Promise { + return this.embedded.readI2CSingleByte(i2cChannel, slaveAddress); + } + + async readVersion(): Promise { + return this.embedded.readVersion(); + } + + async readVersionString(): Promise { + return this.embedded.readVersionString(); + } + + async resetMotorEncoder(motorChannel: number): Promise { + return this.embedded.resetMotorEncoder(motorChannel); + } + + async sendFailSafe(): Promise { + return this.embedded.sendFailSafe(); + } + + async sendKeepAlive(): Promise { + return this.embedded.sendKeepAlive(); + } + + async sendReadCommand(packetTypeID: number, payload: number[]): Promise { + return this.embedded.sendReadCommand(packetTypeID, payload); + } + + sendWriteCommand(packetTypeID: number, payload: number[]): Promise { + return this.embedded.sendWriteCommand(packetTypeID, payload); + } + + async setDebugLogLevel( + debugGroup: DebugGroup, + verbosityLevel: VerbosityLevel, + ): Promise { + return this.embedded.setDebugLogLevel(debugGroup, verbosityLevel); + } + + async getAllDigitalInputs(): Promise { + return await this.embedded.getAllDigitalInputs(); + } + + async getDigitalInput(digitalChannel: number): Promise { + return await this.embedded.getDigitalInput(digitalChannel); + } + + async getDigitalDirection(dioPin: number): Promise { + return this.embedded.getDigitalDirection(dioPin); + } + + async setDigitalDirection(dioPin: number, direction: DigitalChannelDirection): Promise { + return await this.embedded.setDigitalDirection(dioPin, direction); + } + + async setDigitalOutput(digitalChannel: number, value: DigitalState): Promise { + await this.embedded.setDigitalOutput(digitalChannel, value); + } + + async setAllDigitalOutputs(bitPackedField: number): Promise { + await this.embedded.setAllDigitalOutputs(bitPackedField); + } + + async setFTDIResetControl(ftdiResetControl: boolean): Promise { + return this.embedded.setFTDIResetControl(ftdiResetControl); + } + + async setI2CChannelConfiguration( + i2cChannel: number, + speedCode: I2CSpeedCode, + ): Promise { + return this.embedded.setI2CChannelConfiguration(i2cChannel, speedCode); + } + + async setModuleLedColor(red: number, green: number, blue: number): Promise { + return this.embedded.setModuleLedColor(red, green, blue); + } + + async setModuleLedPattern(ledPattern: LedPattern): Promise { + return this.embedded.setModuleLedPattern(ledPattern); + } + + async setMotorChannelCurrentAlertLevel( + motorChannel: number, + currentLimit_mA: number, + ): Promise { + return this.embedded.setMotorChannelCurrentAlertLevel( + motorChannel, + currentLimit_mA, + ); + } + + async setMotorChannelEnable(motorChannel: number, enable: boolean): Promise { + return this.embedded.setMotorChannelEnable(motorChannel, enable); + } + + async setMotorChannelMode( + motorChannel: number, + motorMode: number, + floatAtZero: boolean, + ): Promise { + return this.embedded.setMotorChannelMode(motorChannel, motorMode, floatAtZero); + } + + async setMotorConstantPower(motorChannel: number, powerLevel: number): Promise { + return this.embedded.setMotorConstantPower(motorChannel, powerLevel); + } + + async setMotorTargetPosition( + motorChannel: number, + targetPosition_counts: number, + targetTolerance_counts: number, + ): Promise { + return this.embedded.setMotorTargetPosition( + motorChannel, + targetPosition_counts, + targetTolerance_counts, + ); + } + + async setMotorTargetVelocity( + motorChannel: number, + velocity_cps: number, + ): Promise { + return this.embedded.setMotorTargetVelocity(motorChannel, velocity_cps); + } + + async getMotorClosedLoopControlCoefficients(motorChannel: number, motorMode: MotorMode): Promise { + return await this.embedded.getMotorClosedLoopControlCoefficients(motorChannel, motorMode); + } + + setMotorClosedLoopControlCoefficients(motorChannel: number, motorMode: MotorMode, algorithm: ClosedLoopControlAlgorithm.Pid, pid: PidCoefficients): Promise; + setMotorClosedLoopControlCoefficients(motorChannel: number, motorMode: MotorMode, algorithm: ClosedLoopControlAlgorithm.Pidf, pidf: PidfCoefficients): Promise; + setMotorClosedLoopControlCoefficients(motorChannel: number, motorMode: MotorMode, algorithm: ClosedLoopControlAlgorithm, pid: PidCoefficients | PidfCoefficients): Promise; + async setMotorClosedLoopControlCoefficients(motorChannel: number, motorMode: MotorMode, algorithm: ClosedLoopControlAlgorithm.Pid | ClosedLoopControlAlgorithm.Pidf | ClosedLoopControlAlgorithm, pid: PidCoefficients | PidfCoefficients): Promise { + return await this.embedded.setMotorClosedLoopControlCoefficients(motorChannel, motorMode, algorithm, pid); + } + + async setNewModuleAddress(newModuleAddress: number): Promise { + return this.embedded.setNewModuleAddress(newModuleAddress); + } + + async setPhoneChargeControl(chargeEnable: boolean): Promise { + return this.embedded.setPhoneChargeControl(chargeEnable); + } + + async setServoConfiguration( + servoChannel: number, + framePeriod: number, + ): Promise { + return this.embedded.setServoConfiguration(servoChannel, framePeriod); + } + + async setServoEnable(servoChannel: number, enable: boolean): Promise { + return this.embedded.setServoEnable(servoChannel, enable); + } + + async setServoPulseWidth(servoChannel: number, pulseWidth: number): Promise { + return this.embedded.setServoPulseWidth(servoChannel, pulseWidth); + } + + writeI2CMultipleBytes( + i2cChannel: number, + slaveAddress: number, + bytes: number[], + ): Promise { + return this.embedded.writeI2CMultipleBytes(i2cChannel, slaveAddress, bytes); + } + + writeI2CSingleByte( + i2cChannel: number, + slaveAddress: number, + byte: number, + ): Promise { + return this.embedded.writeI2CSingleByte(i2cChannel, slaveAddress, byte); + } + + async initializeImu() { + return await this.embedded.initializeImu(); + } + + async getImuData(): Promise { + return await this.embedded.getImuData(); + } + + async addChildByAddress(moduleAddress: number): Promise { + let id = await this.openHub("(embedded)", this.moduleAddress, moduleAddress); + + let newHub = new ControlHubConnectedExpansionHub( + false, + RevHubType.ExpansionHub, + this.sendCommand.bind(this), + "(embedded)", + moduleAddress, + id, + ); + + this.children.push(newHub); + + if (newHub.isParentHub) { + throw new Error("A child hub without a serial number must not be a parent."); + } + return newHub; + } + + async addUsbConnectedHub( + serialNumber: string, + moduleAddress: number, + ): Promise { + let id = await this.openHub(serialNumber, moduleAddress, moduleAddress); + + let newHub = new ControlHubConnectedExpansionHub( + true, + RevHubType.ExpansionHub, + this.sendCommand.bind(this), + serialNumber, + moduleAddress, + id, + ); + + this.usbChildren.push(newHub); + this.children.push(newHub); + + if (!newHub.isParentHub) { + throw new Error("A child hub with a serial number must also be a parent."); + } + return newHub; + } + + async sendCommand(type: string, params: P, timeout: number = 1000): Promise { + let key = this.keyGenerator++; + let messagePayload = { + commandKey: key, + commandPayload: JSON.stringify(params), + }; + let payload = { + namespace: "MC", + type: type, + payload: JSON.stringify(messagePayload), + }; + + this.webSocketConnection.send(JSON.stringify(payload)); + + let callbackPromise: Promise = new Promise((resolve, reject) => { + this.currentActiveCommands.set(key, (response, error) => { + if (response !== undefined) { + resolve(response); + } else { + console.error(`Got error for ${type}`); + let e = new Error(); + Object.assign(e, error); + reject(e); + } + }); + }); + + let timer!: NodeJS.Timer; + let timeoutPromise: Promise = new Promise((_, reject) => { + timer = setTimeout(() => { + console.error(`Got timeout for ${type}`); + reject(new TimeoutError()); + }, timeout); + }); + + return await Promise.race([callbackPromise, timeoutPromise]).finally(() => { + clearTimeout(timer); + }); + } + + /** + * Returns all connected hubs in the hierarchy as a flat list. Intended for + * operations that could affect all hubs. + * @private + */ + private flattenChildren(): ControlHubConnectedExpansionHub[] { + let result: ControlHubConnectedExpansionHub[] = []; + result.push(this.embedded); + + for (let child of this.children) { + if (child instanceof ControlHubConnectedExpansionHub) { + result.push(child); + result.push(...child.flattenChildren()); + } + } + + return result; + } +} diff --git a/packages/control-hub/src/internal/ControlHubConnectedExpansionHub.ts b/packages/control-hub/src/internal/ControlHubConnectedExpansionHub.ts new file mode 100644 index 00000000..5e399398 --- /dev/null +++ b/packages/control-hub/src/internal/ControlHubConnectedExpansionHub.ts @@ -0,0 +1,790 @@ +import { + AngularVelocity, + BulkInputData, + ClosedLoopControlAlgorithm, + ControlHub, + DebugGroup, + DigitalChannelDirection, + DigitalState, + ExpansionHub, I2CSpeedCode, + ImuData, + LedPattern, + ModuleInterface, + ModuleStatus, + MotorMode, + ParentExpansionHub, + ParentRevHub, + PidCoefficients, + PidfCoefficients, + Quaternion, + RevHub, + RevHubType, + Rgb, + VerbosityLevel, + Version, +} from "@rev-robotics/rev-hub-core"; +import { EventEmitter } from "events"; + +export class ControlHubConnectedExpansionHub implements ParentExpansionHub { + isParentHub: boolean; + type: RevHubType; + id: Exclude>; + serialNumber: string; + moduleAddress: number; + sendCommand: (name: string, params: P, timeout?: number) => Promise; + + isOpen: boolean = false; + + responseTimeoutMs = 1000; + + readonly children: RevHub[] = []; + private emitter = new EventEmitter(); + + constructor( + isParent: boolean, + type: RevHubType, + sendCommand: (name: string, params: P, timeout?: number) => Promise, + serialNumber: string, + moduleAddress: number, + id: any, + ) { + this.isParentHub = isParent; + this.type = type; + this.id = id; + this.serialNumber = serialNumber; + this.moduleAddress = moduleAddress; + this.sendCommand = sendCommand; + } + + isParent(): this is ParentRevHub { + return this.isParentHub; + } + + isExpansionHub(): this is ExpansionHub { + return true; + } + + isControlHub(): this is ControlHub { + //this class represents the expansion hub board, so it is not a control hub. + return false; + } + + close(): void { + // noinspection JSIgnoredPromiseFromCall + this.sendCommand("closeHub", { + hId: this.id, + }); + } + + async getAnalogInput(channel: number): Promise { + return await this.sendCommand("getAnalogInput", { + hId: this.id, + c: channel, + }); + } + + async get5VBusVoltage(): Promise { + return await this.sendCommand("get5VBusVoltage", { + hId: this.id, + }); + } + + async getBatteryCurrent(): Promise { + return await this.sendCommand("getBatteryCurrent", { + hId: this.id, + }); + } + + async getBatteryVoltage(): Promise { + return await this.sendCommand("getBatteryVoltage", { + hId: this.id, + }); + } + + async getDigitalBusCurrent(): Promise { + return await this.sendCommand("getDigitalBusCurrent", { + hId: this.id, + }); + } + + async getI2CCurrent(): Promise { + return await this.sendCommand("getI2cCurrent", { + hId: this.id, + }); + } + + async getMotorCurrent(motorChannel: number): Promise { + return await this.sendCommand("getMotorCurrent", { + hId: this.id, + c: motorChannel, + }); + } + + async getServoCurrent(): Promise { + return await this.sendCommand("getServoCurrent", { + hId: this.id, + }); + } + + async getTemperature(): Promise { + return await this.sendCommand("getTemperature", { + hId: this.id, + }); + } + + async getBulkInputData(): Promise { + let rawData: any = await this.sendCommand("getBulkInputData", { + hId: this.id, + }); + + return { + analog0_mV: rawData.a0, + analog1_mV: rawData.a1, + analog2_mV: rawData.a2, + analog3_mV: rawData.a3, + digitalInputs: rawData.diBf, + motor0position_enc: rawData.m0ep, + motor1position_enc: rawData.m1ep, + motor2position_enc: rawData.m2ep, + motor3position_enc: rawData.m3ep, + motor0velocity_cps: rawData.m0v, + motor1velocity_cps: rawData.m1v, + motor2velocity_cps: rawData.m2v, + motor3velocity_cps: rawData.m3v, + motorStatus: rawData.msBf, + }; + } + + async getAllDigitalInputs(): Promise { + return await this.sendCommand("getAllDigitalInputs", { + hId: this.id, + }); + } + + async getDigitalDirection(dioPin: number): Promise { + let isOutput = await this.sendCommand("getDigitalDirection", { + hId: this.id, + channel: dioPin, + }); + + return isOutput ? DigitalChannelDirection.Output : DigitalChannelDirection.Input; + } + + async getDigitalInput(dioPin: number): Promise { + let result: boolean = await this.sendCommand("getDigitalInput", { + hId: this.id, + c: dioPin, + }); + + return result ? DigitalState.HIGH : DigitalState.LOW; + } + + async setAllDigitalOutputs(bitPackedField: number): Promise { + await this.sendCommand("setAllDigitalOutputs", { + hId: this.id, + bf: bitPackedField, + }); + } + + async setDigitalDirection(dioPin: number, direction: DigitalChannelDirection): Promise { + await this.sendCommand("setDigitalDirection", { + hId: this.id, + c: dioPin, + o: direction == DigitalChannelDirection.Output, + }); + } + + async setDigitalOutput(dioPin: number, value: DigitalState): Promise { + await this.sendCommand("setDigitalOutput", { + hId: this.id, + c: dioPin, + v: value.isHigh(), + }); + } + + async getFTDIResetControl(): Promise { + return false; + } + + async setFTDIResetControl(_: boolean): Promise { + } + + async getInterfacePacketID( + interfaceName: string, + functionNumber: number, + ): Promise { + return await this.sendCommand("getInterfacePacketId", { + hId: this.id, + interfaceName: interfaceName, + functionNumber: functionNumber, + }); + } + + async getModuleStatus(_: boolean): Promise { + return await this.sendCommand("getModuleStatus", { + hId: this.id, + }); + } + + async getMotorAtTarget(motorChannel: number): Promise { + return await this.sendCommand("getIsMotorAtTarget", { + hId: this.id, + c: motorChannel, + }); + } + + async getMotorChannelCurrentAlertLevel(motorChannel: number): Promise { + return await this.sendCommand("getMotorAlertLevel", { + hId: this.id, + c: motorChannel, + }); + } + + async getMotorChannelEnable(motorChannel: number): Promise { + return await this.sendCommand("getMotorEnable", { + hId: this.id, + c: motorChannel, + }); + } + + async getMotorChannelMode( + motorChannel: number, + ): Promise<{ motorMode: number; floatAtZero: boolean }> { + let result: any = await this.sendCommand("getMotorMode", { + hId: this.id, + c: motorChannel, + }); + return { + motorMode: result.m, + floatAtZero: result.faz + } + } + + async getMotorConstantPower(motorChannel: number): Promise { + return await this.sendCommand("getMotorConstantPower", { + hId: this.id, + c: motorChannel, + }); + } + + async getMotorEncoderPosition(motorChannel: number): Promise { + return await this.sendCommand("getMotorEncoder", { + hId: this.id, + c: motorChannel, + }); + } + + async resetMotorEncoder(motorChannel: number): Promise { + await this.sendCommand("resetMotorEncoder", { + hId: this.id, + c: motorChannel, + }); + } + + async getMotorTargetPosition( + motorChannel: number, + ): Promise<{ targetPosition: number; targetTolerance: number }> { + let result: { tpc: number; ttc: number } = await this.sendCommand( + "getMotorTargetPosition", + { + hId: this.id, + c: motorChannel, + }, + ); + + return { + targetPosition: result.tpc, + targetTolerance: result.ttc, + }; + } + + async getMotorTargetVelocity(motorChannel: number): Promise { + return await this.sendCommand("getMotorTargetVelocity", { + hId: this.id, + c: motorChannel, + }); + } + + async setI2CChannelConfiguration( + i2cChannel: number, + speedCode: I2CSpeedCode, + ): Promise { + await this.sendCommand("setI2CChannelConfiguration", { + hId: this.id, + c: i2cChannel, + sc: speedCode, + }); + } + + async getI2CChannelConfiguration(i2cChannel: number): Promise { + let speedCode = await this.sendCommand("getI2CChannelConfiguration", { + hId: this.id, + c: i2cChannel, + }); + + return speedCode == 1 + ? I2CSpeedCode.SpeedCode400_Kbps + : I2CSpeedCode.SpeedCode100_Kbps; + } + + async readI2CRegister( + i2cChannel: number, + targetAddress: number, + numBytesToRead: number, + register: number, + ): Promise { + return await this.sendCommand("readI2cRegister", { + hId: this.id, + a: targetAddress, + c: i2cChannel, + cb: numBytesToRead, + r: register, + }); + } + + async readI2CMultipleBytes( + i2cChannel: number, + targetAddress: number, + numBytesToRead: number, + ): Promise { + return await this.sendCommand("readI2cData", { + hId: this.id, + a: targetAddress, + c: i2cChannel, + cb: numBytesToRead, + }); + } + + async readI2CSingleByte(i2cChannel: number, targetAddress: number): Promise { + return (await this.readI2CMultipleBytes(i2cChannel, targetAddress, 1))[0]; + } + + async writeI2CMultipleBytes( + i2cChannel: number, + targetAddress: number, + bytes: number[], + ): Promise { + await this.sendCommand("writeI2cData", { + hId: this.id, + a: targetAddress, + c: i2cChannel, + d: bytes, + }); + } + + async writeI2CSingleByte( + i2cChannel: number, + targetAddress: number, + byte: number, + ): Promise { + await this.writeI2CMultipleBytes(i2cChannel, targetAddress, [byte]); + } + + async setMotorChannelCurrentAlertLevel( + motorChannel: number, + currentLimit_mA: number, + ): Promise { + await this.sendCommand("setMotorAlertLevel", { + hId: this.id, + c: motorChannel, + cl: currentLimit_mA, + }); + } + + async setMotorChannelEnable(motorChannel: number, enable: boolean): Promise { + await this.sendCommand("setMotorEnabled", { + hId: this.id, + c: motorChannel, + enable: enable, + }); + } + + async setMotorChannelMode( + motorChannel: number, + motorMode: number, + floatAtZero: boolean, + ): Promise { + await this.sendCommand("setMotorMode", { + hId: this.id, + c: motorChannel, + m: motorMode, + faz: floatAtZero, + }); + } + + async getMotorClosedLoopControlCoefficients(motorChannel: number, motorMode: MotorMode): Promise { + let result: any = await this.sendCommand("getClosedLoopControlCoefficients", { + hId: this.id, + c: motorChannel, + m: motorMode, + }); + + if(result.algorithm == ClosedLoopControlAlgorithm.Pidf) { + return { + p: result.p, + i: result.i, + d: result.d, + f: result.f, + algorithm: ClosedLoopControlAlgorithm.Pidf + } + } else { + return { + p: result.p, + i: result.i, + d: result.d, + algorithm: ClosedLoopControlAlgorithm.Pid + } + } + } + + async setMotorClosedLoopControlCoefficients( + motorChannel: number, + motorMode: MotorMode, + algorithm: ClosedLoopControlAlgorithm, + pid: PidCoefficients | PidfCoefficients, + ): Promise { + if (algorithm === ClosedLoopControlAlgorithm.Pidf) { + await this.setMotorPIDFCoefficients(motorChannel, motorMode, pid as PidfCoefficients); + } else { + await this.setMotorPIDCoefficients(motorChannel, motorMode, pid as PidCoefficients); + } + } + + async setMotorConstantPower(motorChannel: number, powerLevel: number): Promise { + await this.sendCommand("setMotorConstantPower", { + hId: this.id, + c: motorChannel, + p: powerLevel, + }); + } + + async setMotorPIDCoefficients( + motorChannel: number, + motorMode: number, + pid: PidCoefficients, + ): Promise { + await this.sendCommand("setMotorPidCoefficients", { + hId: this.id, + c: motorChannel, + m: motorMode, + p: pid.p, + i: pid.i, + d: pid.d, + }); + } + + + async setMotorPIDFCoefficients( + motorChannel: number, + motorMode: number, + pid: PidfCoefficients, + ): Promise { + await this.sendCommand("setMotorPidfCoefficients", { + hId: this.id, + c: motorChannel, + m: motorMode, + p: pid.p, + i: pid.i, + d: pid.d, + f: pid.f + }); + } + + async setMotorTargetPosition( + motorChannel: number, + targetPosition_counts: number, + targetTolerance_counts: number, + ): Promise { + await this.sendCommand("setMotorTargetPosition", { + hId: this.id, + c: motorChannel, + tpc: targetPosition_counts, + ttc: targetTolerance_counts, + }); + } + + async setMotorTargetVelocity( + motorChannel: number, + velocity_cps: number, + ): Promise { + await this.sendCommand("setMotorTargetVelocity", { + hId: this.id, + c: motorChannel, + tv: velocity_cps, + }); + } + + async getPhoneChargeControl(): Promise { + return await this.sendCommand("getPhoneChargeControl", { + hId: this.id, + }); + } + + async setPhoneChargeControl(chargeEnable: boolean): Promise { + await this.sendCommand("setPhoneChargeControl", { + hId: this.id, + enabled: chargeEnable, + }); + } + + async getServoConfiguration(servoChannel: number): Promise { + return await this.sendCommand("getServoConfiguration", { + hId: this.id, + c: servoChannel, + }); + } + + async getServoEnable(servoChannel: number): Promise { + return await this.sendCommand("getServoEnable", { + hId: this.id, + c: servoChannel, + }); + } + + async getServoPulseWidth(servoChannel: number): Promise { + return await this.sendCommand("getServoPulseWidth", { + hId: this.id, + c: servoChannel, + }); + } + + async setServoConfiguration( + servoChannel: number, + framePeriod: number, + ): Promise { + await this.sendCommand("setServoConfiguration", { + hId: this.id, + c: servoChannel, + fp: framePeriod, + }); + } + + async setServoEnable(servoChannel: number, enable: boolean): Promise { + await this.sendCommand("setServoEnable", { + hId: this.id, + c: servoChannel, + enable: enable, + }); + } + + async setServoPulseWidth(servoChannel: number, pulseWidth: number): Promise { + await this.sendCommand("setServoPulseWidth", { + hId: this.id, + c: servoChannel, + pw: pulseWidth, + }); + } + + async injectDataLogHint(hintText: string): Promise { + await this.sendCommand("injectDebugLogHint", { + hId: this.id, + hint: hintText, + }); + } + + async queryInterface(interfaceName: string): Promise { + let result: { name: string; firstPacketId: number; numberIds: number } = + await this.sendCommand("queryInterface", { + hId: this.id, + interfaceName: interfaceName, + }); + + return { + name: result.name, + firstPacketID: result.firstPacketId, + numberIDValues: result.numberIds, + }; + } + + async readVersion(): Promise { + let versionString = await this.readVersionString(); + let parts = versionString.split("."); + if (parts.length != 3) { + throw new Error(`Version ${versionString} does not have 3 parts`); + } + return { + majorVersion: Number(parts[0]), + minorVersion: Number(parts[1]), + engineeringRevision: Number(parts[2]), + minorHwRevision: 0, //hardcoded in RHSPlib_device_control.c + majorHwRevision: 2, //hardcoded in RHSPlib_device_control.c + hwType: 0x311153, //hardcoded in RHSPlib_device_control.c + }; + } + + async readVersionString(): Promise { + return await this.sendCommand("getHubFwVersionString", { + hId: this.id, + }); + } + + async sendFailSafe(): Promise { + await this.sendCommand("sendFailSafe", { + hId: this.id, + }); + } + + async sendKeepAlive(): Promise {} + + async setDebugLogLevel( + debugGroup: DebugGroup, + verbosityLevel: VerbosityLevel, + ): Promise { + await this.sendCommand("setDebugLogLevel", { + hId: this.id, + debugGroup: debugGroup, + verbosityLevel: verbosityLevel, + }); + } + + async setModuleLedColor(red: number, green: number, blue: number): Promise { + await this.sendCommand("setLedColor", { + hId: this.id, + r: red, + g: green, + b: blue, + }); + } + + async setModuleLedPattern(ledPattern: LedPattern): Promise { + await this.sendCommand("setLedPattern", { + hId: this.id, + s0: ledPattern.rgbtPatternStep0, + s1: ledPattern.rgbtPatternStep1, + s2: ledPattern.rgbtPatternStep2, + s3: ledPattern.rgbtPatternStep3, + s4: ledPattern.rgbtPatternStep4, + s5: ledPattern.rgbtPatternStep5, + s6: ledPattern.rgbtPatternStep6, + s7: ledPattern.rgbtPatternStep7, + s8: ledPattern.rgbtPatternStep8, + s9: ledPattern.rgbtPatternStep9, + s10: ledPattern.rgbtPatternStep10, + s11: ledPattern.rgbtPatternStep11, + s12: ledPattern.rgbtPatternStep12, + s13: ledPattern.rgbtPatternStep13, + s14: ledPattern.rgbtPatternStep14, + s15: ledPattern.rgbtPatternStep15, + }); + } + + async getModuleLedColor(): Promise { + let result: { r: number; g: number; b: number } = await this.sendCommand( + "getLedColor", + { + hId: this.id, + }, + ); + + return { + red: result.r, + green: result.g, + blue: result.b, + }; + } + + async getModuleLedPattern(): Promise { + let pattern: any = await this.sendCommand("getLedPattern", { + hId: this.id, + }); + + return { + rgbtPatternStep0: pattern.s0, + rgbtPatternStep1: pattern.s1, + rgbtPatternStep2: pattern.s2, + rgbtPatternStep3: pattern.s3, + rgbtPatternStep4: pattern.s4, + rgbtPatternStep5: pattern.s5, + rgbtPatternStep6: pattern.s6, + rgbtPatternStep7: pattern.s7, + rgbtPatternStep8: pattern.s8, + rgbtPatternStep9: pattern.s9, + rgbtPatternStep10: pattern.s10, + rgbtPatternStep11: pattern.s11, + rgbtPatternStep12: pattern.s12, + rgbtPatternStep13: pattern.s13, + rgbtPatternStep14: pattern.s14, + rgbtPatternStep15: pattern.s15, + }; + } + + async setNewModuleAddress(newModuleAddress: number): Promise { + await this.sendCommand("setHubAddress", { + hId: this.id, + newAddress: newModuleAddress, + }); + } + async initializeImu() { + await this.sendCommand("initializeImu", { + logoFacing: "UP", + usbFacing: "BACKWARD", + }); + } + + async getImuData(): Promise { + let imuData: any = await this.sendCommand("getImuData", { + u: "degrees", + }); + + return { + quaternion: new Quaternion(imuData.w, imuData.x, imuData.y, imuData.z), + angularVelocity: new AngularVelocity(imuData.xr, imuData.yr, imuData.zr), + }; + } + + async addChildByAddress(moduleAddress: number): Promise { + let id = await this.sendCommand("openHub", { + parentSerialNumber: this.serialNumber, + parentHubAddress: this.moduleAddress, + hubAddress: moduleAddress, + }); + let newHub = new ControlHubConnectedExpansionHub( + false, + RevHubType.ExpansionHub, + this.sendCommand.bind(this), + this.serialNumber, + moduleAddress, + id, + ); + + this.children.push(newHub); + + return newHub; + } + + emit(eventName: "error", e: Error): void; + emit(eventName: "statusChanged", status: ModuleStatus): void; + emit(eventName: "addressChanged", oldAddress: number, newAddress: number): void; + emit(eventName: "sessionEnded"): void; + emit(eventName: string, ...args: any): void { + this.emitter.emit(eventName, ...args); + } + + on( + eventName: "error" | "statusChanged" | "addressChanged" | "sessionEnded", + listener: (...param: any) => void, + ): this { + this.emitter.on(eventName, listener); + return this; + } + + sendReadCommand(packetTypeID: number, payload: number[]): Promise { + return Promise.resolve([]); + } + + sendWriteCommand(packetTypeID: number, payload: number[]): Promise { + return Promise.resolve([]); + } + + flattenChildren(): ControlHubConnectedExpansionHub[] { + let result: ControlHubConnectedExpansionHub[] = []; + for (let child of this.children) { + if (child instanceof ControlHubConnectedExpansionHub) { + result.push(child); + result.push(...child.flattenChildren()); + } + } + + return result; + } +} diff --git a/packages/control-hub/src/open-control-hub.ts b/packages/control-hub/src/open-control-hub.ts new file mode 100644 index 00000000..54be968f --- /dev/null +++ b/packages/control-hub/src/open-control-hub.ts @@ -0,0 +1,12 @@ +import { ControlHub } from "@rev-robotics/rev-hub-core"; +import { ControlHubInternal } from "./internal/ControlHub.js"; + +export async function openControlHub( + serialNumber: string, + moduleAddress: number, + port: number, +): Promise { + let hub = new ControlHubInternal(serialNumber, moduleAddress); + await hub.open("127.0.0.1", (port + 1)); + return hub; +} diff --git a/packages/control-hub/tsconfig.json b/packages/control-hub/tsconfig.json new file mode 100644 index 00000000..5c86a3d2 --- /dev/null +++ b/packages/control-hub/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + } +} diff --git a/packages/core/src/AngularVelocity.ts b/packages/core/src/AngularVelocity.ts new file mode 100644 index 00000000..939e006d --- /dev/null +++ b/packages/core/src/AngularVelocity.ts @@ -0,0 +1,11 @@ +export class AngularVelocity { + xRotationRate: number; + yRotationRate: number; + zRotationRate: number; + + constructor(xRotationRate: number, yRotationRate: number, zRotationRate: number) { + this.xRotationRate = xRotationRate; + this.yRotationRate = yRotationRate; + this.zRotationRate = zRotationRate; + } +} diff --git a/packages/core/src/ControlHub.ts b/packages/core/src/ControlHub.ts new file mode 100644 index 00000000..a990332c --- /dev/null +++ b/packages/core/src/ControlHub.ts @@ -0,0 +1,22 @@ +import { ExpansionHub, ParentExpansionHub } from "./ExpansionHub.js"; +import { ParentRevHub } from "./RevHub.js"; +import { ModuleStatus } from "./ModuleStatus.js"; +import { ImuData } from "./ImuData.js"; + +export interface ControlHub extends ExpansionHub, ParentRevHub { + on(eventName: "error", listener: (error: Error) => void): this; + on(eventName: "statusChanged", listener: (status: ModuleStatus) => void): this; + on( + eventName: "addressChanged", + listener: (oldAddress: number, newAddress: number) => void, + ): this; + on(eventName: "sessionEnded", listener: () => void): this; + + addUsbConnectedHub( + serialNumber: string, + moduleAddress: number, + ): Promise; + + initializeImu(): Promise; + getImuData(): Promise; +} diff --git a/packages/core/src/ExpansionHub.ts b/packages/core/src/ExpansionHub.ts index 8b29124c..4580ddf8 100644 --- a/packages/core/src/ExpansionHub.ts +++ b/packages/core/src/ExpansionHub.ts @@ -1,21 +1,19 @@ import { ParentRevHub, RevHub } from "./RevHub.js"; -import { ModuleStatus } from "./ModuleStatus.js"; +import { DigitalState } from "./DigitalState.js"; import { ModuleInterface } from "./ModuleInterface.js"; import { Rgb } from "./Rgb.js"; import { LedPattern } from "./LedPattern.js"; +import { ModuleStatus } from "./ModuleStatus.js"; import { DebugGroup } from "./DebugGroup.js"; import { VerbosityLevel } from "./VerbosityLevel.js"; -import { BulkInputData } from "./BulkInputData.js"; import { Version } from "./Version.js"; -import { DigitalState } from "./DigitalState.js"; -import { I2CSpeedCode } from "./I2CSpeedCode.js"; -import { I2CWriteStatus } from "./I2CWriteStatus.js"; -import { I2CReadStatus } from "./I2CReadStatus.js"; -import { PidCoefficients } from "./PidCoefficients.js"; import { DigitalChannelDirection } from "./DigitalChannelDirection.js"; +import { I2CSpeedCode } from "./I2CSpeedCode.js"; import { MotorMode } from "./MotorMode.js"; +import { PidCoefficients } from "./PidCoefficients.js"; import { PidfCoefficients } from "./PidfCoefficients.js"; import { ClosedLoopControlAlgorithm } from "./ClosedLoopControlAlgorithm.js"; +import { BulkInputData } from "./BulkInputData.js"; export type ParentExpansionHub = ParentRevHub & ExpansionHub; @@ -30,8 +28,6 @@ export interface ExpansionHub extends RevHub { * it has been closed. */ close(): void; - sendWriteCommand(packetTypeID: number, payload: number[]): Promise; - sendReadCommand(packetTypeID: number, payload: number[]): Promise; getModuleStatus(clearStatusAfterResponse: boolean): Promise; sendKeepAlive(): Promise; sendFailSafe(): Promise; diff --git a/packages/core/src/ImuData.ts b/packages/core/src/ImuData.ts new file mode 100644 index 00000000..1033ca31 --- /dev/null +++ b/packages/core/src/ImuData.ts @@ -0,0 +1,12 @@ +import { AngularVelocity } from "./AngularVelocity.js"; +import { Quaternion } from "./Quaternion.js"; + +export class ImuData { + angularVelocity: AngularVelocity; + quaternion: Quaternion; + + constructor(angularVelocity: AngularVelocity, quaternion: Quaternion) { + this.angularVelocity = angularVelocity; + this.quaternion = quaternion; + } +} diff --git a/packages/core/src/Quaternion.ts b/packages/core/src/Quaternion.ts new file mode 100644 index 00000000..dd92862d --- /dev/null +++ b/packages/core/src/Quaternion.ts @@ -0,0 +1,13 @@ +export class Quaternion { + w: number; + x: number; + y: number; + z: number; + + constructor(w: number, x: number, y: number, z: number) { + this.w = w; + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/packages/core/src/RevHub.ts b/packages/core/src/RevHub.ts index d1ed07ec..1ba7e9bb 100644 --- a/packages/core/src/RevHub.ts +++ b/packages/core/src/RevHub.ts @@ -1,5 +1,6 @@ import { RevHubType } from "./RevHubType.js"; import { ExpansionHub } from "./ExpansionHub.js"; +import { ControlHub } from "./ControlHub.js"; export interface RevHub { readonly moduleAddress: number; @@ -7,6 +8,7 @@ export interface RevHub { isParent(): this is ParentRevHub; isExpansionHub(): this is ExpansionHub; + isControlHub(): this is ControlHub; /** * Listen for errors that do not happen as a result of a specific function call @@ -15,7 +17,7 @@ export interface RevHub { * @param listener */ on(eventName: "error", listener: (error: Error) => void): RevHub; - close(): any; + close(): void; } export interface ParentRevHub extends RevHub { diff --git a/packages/core/src/RevHubError.ts b/packages/core/src/RevHubError.ts index 936a1d5a..67438ffe 100644 --- a/packages/core/src/RevHubError.ts +++ b/packages/core/src/RevHubError.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf } from "./nack-errors/NackError.js"; +import { setPrototypeOf } from "./nack-errors/set-prototype.js"; export class RevHubError extends Error { constructor(message: string) { diff --git a/packages/core/src/RevHubType.ts b/packages/core/src/RevHubType.ts index c68871a1..de4220b4 100644 --- a/packages/core/src/RevHubType.ts +++ b/packages/core/src/RevHubType.ts @@ -1,3 +1,4 @@ export enum RevHubType { ExpansionHub, + ControlHub, } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 01795b0e..37e9b28b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,15 +1,21 @@ -export * from "./nack-errors/nack-codes.js"; export * from "./ExpansionHub.js"; -export * from "./serial-errors.js"; +export * from "./ControlHub.js"; export * from "./RevHub.js"; +export * from "./ControlHub.js" export * from "./RevHubType.js"; +export * from "./nack-errors/nack-codes.js"; +export * from "./serial-errors.js"; export * from "./led-pattern.js"; export * from "./BulkInputData.js"; +export * from "./DigitalState.js"; +export * from "./DigitalChannelDirection.js"; export * from "./DebugGroup.js"; export * from "./DigitalChannelDirection.js"; -export * from "./DigitalState.js"; export * from "./DiscoveredAddresses.js"; export * from "./I2CReadStatus.js"; +export * from "./ImuData.js"; +export * from "./Quaternion.js"; +export * from "./AngularVelocity.js"; export * from "./I2CSpeedCode.js"; export * from "./I2CWriteStatus.js"; export * from "./LedPattern.js"; @@ -25,6 +31,8 @@ export * from "./SerialParity.js"; export * from "./VerbosityLevel.js"; export * from "./Version.js"; export * from "./serial-errors.js"; +export * from "./nack-errors/set-prototype.js"; +export * from "./nack-errors/nack-codes.js"; export * from "./nack-errors/NackError.js"; export * from "./nack-errors/BatteryTooLowError.js"; export * from "./nack-errors/diagnostic-errors.js"; diff --git a/packages/core/src/nack-errors/BatteryTooLowError.ts b/packages/core/src/nack-errors/BatteryTooLowError.ts index fb9b3a80..3ebbb452 100644 --- a/packages/core/src/nack-errors/BatteryTooLowError.ts +++ b/packages/core/src/nack-errors/BatteryTooLowError.ts @@ -1,4 +1,5 @@ -import {NackError, setPrototypeOf} from "./NackError.js"; +import { NackError } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; export class BatteryTooLowError extends NackError { constructor(nackCode: number, message: string) { diff --git a/packages/core/src/nack-errors/GeneralSerialError.ts b/packages/core/src/nack-errors/GeneralSerialError.ts index 5910c1cb..6a4870f6 100644 --- a/packages/core/src/nack-errors/GeneralSerialError.ts +++ b/packages/core/src/nack-errors/GeneralSerialError.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; /** * Indicates an error in the Serial connection. diff --git a/packages/core/src/nack-errors/NackError.ts b/packages/core/src/nack-errors/NackError.ts index 1f36f159..098c6326 100644 --- a/packages/core/src/nack-errors/NackError.ts +++ b/packages/core/src/nack-errors/NackError.ts @@ -15,11 +15,7 @@ Modified to use Object.setPrototypeOf instead of __proto__ and to be a global function */ import { RevHubError } from "../RevHubError.js"; - -export function setPrototypeOf(obj: any, proto: any) { - Object.setPrototypeOf(obj, proto); - return obj; -} +import { setPrototypeOf } from "./set-prototype.js"; export class NackError extends RevHubError { nackCode: number; diff --git a/packages/core/src/nack-errors/NoExpansionHubWithAddressError.ts b/packages/core/src/nack-errors/NoExpansionHubWithAddressError.ts index 111013fd..12341830 100644 --- a/packages/core/src/nack-errors/NoExpansionHubWithAddressError.ts +++ b/packages/core/src/nack-errors/NoExpansionHubWithAddressError.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; export class NoExpansionHubWithAddressError extends Error { moduleAddress: number; diff --git a/packages/core/src/nack-errors/ParameterOutOfRangeError.ts b/packages/core/src/nack-errors/ParameterOutOfRangeError.ts index 324af1c4..ba9c7ca4 100644 --- a/packages/core/src/nack-errors/ParameterOutOfRangeError.ts +++ b/packages/core/src/nack-errors/ParameterOutOfRangeError.ts @@ -1,4 +1,5 @@ -import { NackError, setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; +import { NackError } from "./NackError.js"; export class ParameterOutOfRangeError extends NackError { /** diff --git a/packages/core/src/nack-errors/TimeoutError.ts b/packages/core/src/nack-errors/TimeoutError.ts index 7d42caa6..5b8779ca 100644 --- a/packages/core/src/nack-errors/TimeoutError.ts +++ b/packages/core/src/nack-errors/TimeoutError.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; export class TimeoutError extends Error { constructor() { diff --git a/packages/core/src/nack-errors/UnrecognizedNackError.ts b/packages/core/src/nack-errors/UnrecognizedNackError.ts index bf0233b0..b0985c31 100644 --- a/packages/core/src/nack-errors/UnrecognizedNackError.ts +++ b/packages/core/src/nack-errors/UnrecognizedNackError.ts @@ -1,4 +1,5 @@ -import { NackError, setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; +import { NackError } from "./NackError.js"; export class UnrecognizedNackError extends NackError { constructor(nackCode: number) { diff --git a/packages/core/src/nack-errors/diagnostic-errors.ts b/packages/core/src/nack-errors/diagnostic-errors.ts index fb22c32e..e9e67bdf 100644 --- a/packages/core/src/nack-errors/diagnostic-errors.ts +++ b/packages/core/src/nack-errors/diagnostic-errors.ts @@ -1,5 +1,6 @@ -import { NackError, setPrototypeOf } from "./NackError.js"; +import { NackError } from "./NackError.js"; import { NackCode } from "./nack-codes.js"; +import { setPrototypeOf } from "./set-prototype.js"; export class CommandImplementationPendingError extends NackError { constructor() { diff --git a/packages/core/src/nack-errors/digital-channel-errors.ts b/packages/core/src/nack-errors/digital-channel-errors.ts index dafa459a..f9adc116 100644 --- a/packages/core/src/nack-errors/digital-channel-errors.ts +++ b/packages/core/src/nack-errors/digital-channel-errors.ts @@ -1,4 +1,5 @@ -import { NackError, setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; +import { NackError } from "./NackError.js"; import { NackCode } from "./nack-codes.js"; export class DigitalChannelNotConfiguredForOutputError extends NackError { diff --git a/packages/core/src/nack-errors/i2c-errors.ts b/packages/core/src/nack-errors/i2c-errors.ts index 42d6b110..45b0845a 100644 --- a/packages/core/src/nack-errors/i2c-errors.ts +++ b/packages/core/src/nack-errors/i2c-errors.ts @@ -1,4 +1,5 @@ -import { NackError, setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; +import { NackError } from "./NackError.js"; import { NackCode } from "./nack-codes.js"; export class I2cControllerBusyError extends NackError { diff --git a/packages/core/src/nack-errors/motor-errors.ts b/packages/core/src/nack-errors/motor-errors.ts index df5e341f..87285c15 100644 --- a/packages/core/src/nack-errors/motor-errors.ts +++ b/packages/core/src/nack-errors/motor-errors.ts @@ -1,4 +1,5 @@ -import { NackError, setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; +import { NackError } from "./NackError.js"; import { BatteryTooLowError } from "./BatteryTooLowError.js"; import { NackCode } from "./nack-codes.js"; diff --git a/packages/core/src/nack-errors/servo-errors.ts b/packages/core/src/nack-errors/servo-errors.ts index 49c61925..75bb5f54 100644 --- a/packages/core/src/nack-errors/servo-errors.ts +++ b/packages/core/src/nack-errors/servo-errors.ts @@ -1,4 +1,5 @@ -import { NackError, setPrototypeOf } from "./NackError.js"; +import { setPrototypeOf } from "./set-prototype.js"; +import { NackError } from "./NackError.js"; import { BatteryTooLowError } from "./BatteryTooLowError.js"; import { NackCode } from "./nack-codes.js"; diff --git a/packages/core/src/nack-errors/set-prototype.ts b/packages/core/src/nack-errors/set-prototype.ts new file mode 100644 index 00000000..0995d06a --- /dev/null +++ b/packages/core/src/nack-errors/set-prototype.ts @@ -0,0 +1,4 @@ +export function setPrototypeOf(obj: any, proto: any) { + Object.setPrototypeOf(obj, proto); + return obj; +} diff --git a/packages/core/src/serial-errors.ts b/packages/core/src/serial-errors.ts index f56fe3c9..0e0948d9 100644 --- a/packages/core/src/serial-errors.ts +++ b/packages/core/src/serial-errors.ts @@ -1,4 +1,4 @@ -import { setPrototypeOf } from "./nack-errors/NackError.js"; +import { setPrototypeOf } from "./nack-errors/set-prototype.js"; export class UnableToOpenSerialError extends Error { constructor(serialPort: string) { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 5c86a3d2..11324309 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "esModuleInterop": true, "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "target": "es2017" } } diff --git a/packages/distance-sensor/src/drivers/vl53l0x.ts b/packages/distance-sensor/src/drivers/vl53l0x.ts index ba456f08..d103b2f9 100644 --- a/packages/distance-sensor/src/drivers/vl53l0x.ts +++ b/packages/distance-sensor/src/drivers/vl53l0x.ts @@ -11,6 +11,7 @@ import { writeShort, } from "../i2c-utils.js"; import { DistanceSensorDriver } from "./DistanceSensorDriver.js"; +import { ExpansionHub } from "@rev-robotics/rev-hub-core"; export class VL53L0X implements DistanceSensorDriver { constructor(hub: ExpansionHub, channel: number) { diff --git a/packages/expansion-hub/src/index.ts b/packages/expansion-hub/src/index.ts index 5e72861d..12bab18c 100644 --- a/packages/expansion-hub/src/index.ts +++ b/packages/expansion-hub/src/index.ts @@ -47,4 +47,8 @@ export { BatteryTooLowToRunServoError, TimeoutError, UnrecognizedNackError, + RevHubType, + RevHub, + ParentRevHub, + ExpansionHub, } from "@rev-robotics/rev-hub-core"; diff --git a/packages/expansion-hub/src/internal/ExpansionHub.ts b/packages/expansion-hub/src/internal/ExpansionHub.ts index 131ad2f5..73ae1c2e 100644 --- a/packages/expansion-hub/src/internal/ExpansionHub.ts +++ b/packages/expansion-hub/src/internal/ExpansionHub.ts @@ -1,44 +1,39 @@ -import { - NackCode, - NativeRevHub, - RhspLibErrorCode, - Serial as SerialPort, -} from "@rev-robotics/rhsplib"; import { BulkInputData, ClosedLoopControlAlgorithm, - CommandNotSupportedError, + CommandNotSupportedError, ControlHub, DebugGroup, DigitalChannelDirection, DigitalState, - ExpansionHub, - GeneralSerialError, + ExpansionHub, GeneralSerialError, I2CReadStatus, I2CSpeedCode, I2CWriteStatus, LedPattern, ModuleInterface, ModuleStatus, - MotorMode, + PidCoefficients, + Rgb, + VerbosityLevel, + Version, + TimeoutError, nackCodeToError, NoExpansionHubWithAddressError, ParameterOutOfRangeError, I2cOperationInProgressError, ParentRevHub, - PidCoefficients, - PidfCoefficients, RevHub, - RevHubType, - Rgb, - TimeoutError, - VerbosityLevel, - Version, + MotorMode, + I2cOperationInProgressError, NackCode, PidfCoefficients, } from "@rev-robotics/rev-hub-core"; -import { closeSerialPort } from "../open-rev-hub.js"; import { EventEmitter } from "events"; +import { RevHubType } from "@rev-robotics/rev-hub-core"; import { RhspLibError } from "../errors/RhspLibError.js"; -import { startKeepAlive } from "../start-keep-alive.js"; import { performance } from "perf_hooks"; +import { startKeepAlive } from "../start-keep-alive.js"; +import { closeSerialPort } from "../serial.js"; +import { NativeRevHub, RhspLibErrorCode } from "@rev-robotics/rhsplib"; +import { SerialPort } from "serialport"; export class ExpansionHubInternal implements ExpansionHub { constructor(isParent: true, serial: SerialPort, serialNumber: string); @@ -63,7 +58,7 @@ export class ExpansionHubInternal implements ExpansionHub { keepAliveTimer?: NodeJS.Timer; - type = RevHubType.ExpansionHub; + type: RevHubType = RevHubType.ExpansionHub; private emitter = new EventEmitter(); isParent(): this is ParentRevHub { @@ -74,6 +69,10 @@ export class ExpansionHubInternal implements ExpansionHub { return true; } + isControlHub(): this is ControlHub { + return false; + } + close(): void { //Closing a parent closes the serial port and all children if (this.isParent()) { @@ -198,11 +197,11 @@ export class ExpansionHubInternal implements ExpansionHub { } async getDigitalInput(digitalChannel: number): Promise { - return (await this.convertErrorPromise(() => { - return this.nativeRevHub.getDigitalSingleInput(digitalChannel); - })) - ? DigitalState.HIGH - : DigitalState.LOW; + return this.convertErrorPromise(async () => { + return (await this.nativeRevHub.getDigitalSingleInput(digitalChannel)) + ? DigitalState.HIGH + : DigitalState.LOW; + }); } getFTDIResetControl(): Promise { diff --git a/packages/expansion-hub/src/open-rev-hub.ts b/packages/expansion-hub/src/open-rev-hub.ts index 23f1f8cc..7052f712 100644 --- a/packages/expansion-hub/src/open-rev-hub.ts +++ b/packages/expansion-hub/src/open-rev-hub.ts @@ -2,25 +2,17 @@ import { DiscoveredAddresses, NativeRevHub, NativeSerial, - SerialError, - SerialFlowControl, } from "@rev-robotics/rhsplib"; import { SerialPort as SerialLister } from "serialport"; import { ExpansionHubInternal } from "./internal/ExpansionHub.js"; import { startKeepAlive } from "./start-keep-alive.js"; import { - GeneralSerialError, - InvalidSerialArguments, NoExpansionHubWithAddressError, ParentExpansionHub, - RevHub, - SerialConfigurationError, - SerialIoError, - SerialParity, TimeoutError, - UnableToOpenSerialError, } from "@rev-robotics/rev-hub-core"; import { performance } from "perf_hooks"; +import { getSerial } from "./serial.js"; /** * Maps the serial port path (/dev/tty1 or COM3 for example) to an open @@ -44,11 +36,7 @@ export async function openParentExpansionHub( ): Promise { let serialPortPath = await getSerialPortPathForExHubSerial(serialNumber); - if (openSerialMap.get(serialPortPath) == undefined) { - openSerialMap.set(serialPortPath, await openSerialPort(serialPortPath)); - } - - let serialPort = openSerialMap.get(serialPortPath)!; + let serialPort = await getSerial(serialPortPath); let parentHub = new ExpansionHubInternal(true, serialPort, serialNumber); @@ -100,11 +88,7 @@ export async function openExpansionHubAndAllChildren( ): Promise { let serialPortPath = await getSerialPortPathForExHubSerial(serialNumber); - if (openSerialMap.get(serialPortPath) == undefined) { - openSerialMap.set(serialPortPath, await openSerialPort(serialPortPath)); - } - - let serialPort = openSerialMap.get(serialPortPath)!; + let serialPort = await getSerial(serialPortPath); let discoveredModules = await NativeRevHub.discoverRevHubs(serialPort); let parentAddress = discoveredModules.parentAddress; @@ -133,46 +117,3 @@ async function getSerialPortPathForExHubSerial(serialNumber: string): Promise { - let serial = new NativeSerial(); - try { - await serial.open( - serialPortPath, - 460800, - 8, - SerialParity.None, - 1, - SerialFlowControl.None, - ); - } catch (e: any) { - let code = e.errorCode; - if (code == SerialError.INVALID_ARGS) { - throw new InvalidSerialArguments(serialPortPath); - } else if (code == SerialError.UNABLE_TO_OPEN) { - throw new UnableToOpenSerialError(serialPortPath); - } else if (code == SerialError.CONFIGURATION_ERROR) { - throw new SerialConfigurationError(serialPortPath); - } else if (code == SerialError.IO_ERROR) { - throw new SerialIoError(serialPortPath); - } else if (code == SerialError.GENERAL_ERROR) { - throw new GeneralSerialError(serialPortPath); - } - } - return serial; -} diff --git a/packages/expansion-hub/src/serial.ts b/packages/expansion-hub/src/serial.ts new file mode 100644 index 00000000..d5725774 --- /dev/null +++ b/packages/expansion-hub/src/serial.ts @@ -0,0 +1,74 @@ +import { + NativeSerial, + Serial, + SerialError, + SerialFlowControl, +} from "@rev-robotics/rhsplib"; +import { + GeneralSerialError, + InvalidSerialArguments, + SerialConfigurationError, + SerialIoError, + SerialParity, + UnableToOpenSerialError, +} from "@rev-robotics/rev-hub-core"; + +/** + * Maps the serial port path (/dev/tty1 or COM3 for example) to an open + * Serial object at that path. The {@link NativeSerial} object should be removed from + * the map upon closing. + */ +const openSerialMap = new Map(); + +/** + * Closes the given Serial port and removes it from the open serial ports + * list. This should be the preferred way to close a Serial port. + * + * @param serialPort the Serial port to close + */ +export function closeSerialPort(serialPort: typeof NativeSerial) { + for (let [path, port] of openSerialMap.entries()) { + if (port === serialPort) { + openSerialMap.delete(path); + } + } + serialPort.close(); +} + +export async function getSerial(serialPortPath: string): Promise { + if (openSerialMap.get(serialPortPath) == undefined) { + openSerialMap.set(serialPortPath, await openSerialPort(serialPortPath)); + } + + return openSerialMap.get(serialPortPath); +} + +async function openSerialPort(serialPortPath: string): Promise { + console.log(NativeSerial); + console.log(JSON.stringify(NativeSerial)); + let serial = new NativeSerial(); + try { + await serial.open( + serialPortPath, + 460800, + 8, + SerialParity.None, + 1, + SerialFlowControl.None, + ); + } catch (e: any) { + let code = e.errorCode; + if (code == SerialError.INVALID_ARGS) { + throw new InvalidSerialArguments(serialPortPath); + } else if (code == SerialError.UNABLE_TO_OPEN) { + throw new UnableToOpenSerialError(serialPortPath); + } else if (code == SerialError.CONFIGURATION_ERROR) { + throw new SerialConfigurationError(serialPortPath); + } else if (code == SerialError.IO_ERROR) { + throw new SerialIoError(serialPortPath); + } else if (code == SerialError.GENERAL_ERROR) { + throw new GeneralSerialError(serialPortPath); + } + } + return serial; +} diff --git a/packages/librhsp/lib/binding.ts b/packages/librhsp/lib/binding.ts index 8f66dba5..8b699e4f 100644 --- a/packages/librhsp/lib/binding.ts +++ b/packages/librhsp/lib/binding.ts @@ -24,10 +24,7 @@ import { } from "@rev-robotics/rev-hub-core"; import { SerialParity } from "@rev-robotics/rev-hub-core"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const require = createRequire(import.meta.url); -const addon = require("node-gyp-build")(path.join(__dirname, "..")); +const addon: any = {}; export * from "./error-codes.js"; export * from "./serial-errors.js"; @@ -127,24 +124,24 @@ export declare class RevHub { getI2CChannelConfiguration(i2cChannel: number): Promise; writeI2CSingleByte( i2cChannel: number, - slaveAddress: number, + targetAddress: number, byte: number, ): Promise; writeI2CMultipleBytes( i2cChannel: number, - slaveAddress: number, + targetAddress: number, bytes: number[], ): Promise; getI2CWriteStatus(i2cChannel: number): Promise; - readI2CSingleByte(i2cChannel: number, slaveAddress: number): Promise; + readI2CSingleByte(i2cChannel: number, targetAddress: number): Promise; readI2CMultipleBytes( i2cChannel: number, - slaveAddress: number, + targetAddress: number, numBytesToRead: number, ): Promise; writeI2CReadMultipleBytes( i2cChannel: number, - slaveAddress: number, + targetAddress: number, numBytesToRead: number, startAddress: number, ): Promise; diff --git a/packages/sample/package.json b/packages/sample/package.json index c35ccee7..56901577 100644 --- a/packages/sample/package.json +++ b/packages/sample/package.json @@ -9,7 +9,16 @@ "dependencies": { "@rev-robotics/distance-sensor": "^1.0.0", "@rev-robotics/expansion-hub": "^1.0.0", - "commander": "^10.0.1" + "@rev-robotics/control-hub": "^0.1.0", + "ws": "8.13.0", + "commander": "^10.0.1", + "@u4/adbkit": "^4.1.19", + "get-port": "^6.1.2" + }, + "devDependencies": { + "@types/ws": "^8.5.4", + "@types/node-forge": "^1.3.2", + "@types/debug": "^4.1.8" }, "scripts": { "build": "tsc", @@ -20,6 +29,9 @@ "temperature": "node dist/main.js temperature --continuous", "error": "node dist/main.js testErrorHandling", "list": "node dist/main.js list", - "led": "node dist/main.js led" + "led": "node dist/main.js led", + "motor": "node dist/main.js motor power 1 0.60", + "motor-position": "node dist/main.js --count 3000", + "distance": "node dist/main.js distance 0" } } diff --git a/packages/sample/rollup.config.js b/packages/sample/rollup.config.js new file mode 100644 index 00000000..b8adfb68 --- /dev/null +++ b/packages/sample/rollup.config.js @@ -0,0 +1,33 @@ +import excludeDependenciesFromBundle from "rollup-plugin-exclude-dependencies-from-bundle"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import addon from "rollup-plugin-natives"; + +import pkg from "./package.json" assert { type: "json" }; + +const PACKAGE_NAME = process.cwd(); + +const extensions = [".js", ".ts", ".tsx"]; +const nodeOptions = { + extensions, +}; +const commonjsOptions = { + ignoreGlobal: true, + ignoreDynamicRequires: true, + include: /node_modules/, +}; + +export default { + input: `${PACKAGE_NAME}/dist/main.js`, + output: [ + { + file: "dist/output.js", + format: "cjs", + }, + ], + plugins: [nodeResolve(nodeOptions), commonjs(commonjsOptions), json()], + external: { + dependencies: pkg.dependencies, + }, +}; diff --git a/packages/sample/src/HubStringify.ts b/packages/sample/src/HubStringify.ts index b607320a..e7938bab 100644 --- a/packages/sample/src/HubStringify.ts +++ b/packages/sample/src/HubStringify.ts @@ -1,4 +1,21 @@ -import { RevHub } from "@rev-robotics/rev-hub-core"; +import { ControlHub, RevHub } from "@rev-robotics/rev-hub-core"; + +export function controlHubHierarchyToString(hub: ControlHub): string { + let result = `Control Hub: ${hub.serialNumber} ${hub.moduleAddress}\n`; + for (const child of hub.children) { + if (child.isParent()) { + result += `\tUSB Hub: ${child.serialNumber} ${child.moduleAddress}\n`; + + for (const grandchild of child.children) { + result += `\t\tRS-485 Hub: ${grandchild.moduleAddress}\n`; + } + } else { + result += `\tRS-485 Hub: ${child.moduleAddress}\n`; + } + } + + return result; +} export function hubHierarchyToString(hub: RevHub): string { let serialNumberText: string = ""; diff --git a/packages/sample/src/adb-setup.ts b/packages/sample/src/adb-setup.ts new file mode 100644 index 00000000..2f5102ba --- /dev/null +++ b/packages/sample/src/adb-setup.ts @@ -0,0 +1,89 @@ +import { Adb, DeviceClient } from "@u4/adbkit"; +import getPort from "get-port"; +import { ControlHub, ParentRevHub } from "@rev-robotics/rev-hub-core"; +import { openWifiControlHub } from "@rev-robotics/control-hub"; +import { ControlHubInternal } from "@rev-robotics/control-hub/dist/internal/ControlHub.js"; + +export async function openUsbControlHubs(): Promise { + let adbClient = Adb.createClient(); + let controlHubs: ControlHub[] = []; + + let devices = await adbClient.listDevices(); + for (const device of devices) { + let deviceClient = device.getClient(); + let isHub = await isControlHub(deviceClient); + if (isHub) { + let port = await configureHubTcp(deviceClient); + let serialNumber = device.id; + + let hub = await openWifiControlHub(serialNumber, 173, port) as ControlHubInternal; + controlHubs.push(hub); + + let addresses: Record< + string, + { + serialNumber: string; + parentHubAddress: number; + childAddresses: number[]; + } + > = await hub.sendCommand("scanAndDiscover", {}, 20000); + + for (let serialNumber in addresses) { + let parentHub: ParentRevHub; + let childAddresses = addresses[serialNumber].childAddresses; + + if (serialNumber === "(embedded)") { + parentHub = hub; + } else { + let parentHubInfo = addresses[serialNumber]; + parentHub = await hub.addUsbConnectedHub( + serialNumber, + parentHubInfo.parentHubAddress, + ); + } + + for (let childAddress of childAddresses) { + await parentHub.addChildByAddress(childAddress); + } + } + } + } + + return controlHubs; +} + +async function isControlHub(deviceClient: DeviceClient): Promise { + let serialasusb = await deviceClient.execOut( + "getprop persist.ftcandroid.serialasusb", + "utf8", + ); + return serialasusb.startsWith("true"); +} + +async function configureHubTcp(deviceClient: DeviceClient): Promise { + let port = await findAdjacentPorts(); + await deviceClient.forward(`tcp:${port}`, `tcp:8080`); + await deviceClient.forward(`tcp:${port + 1}`, `tcp:8081`); + + return port; +} + +/** + * Returns the first of two adjacent ports. + * @param host + */ +export async function findAdjacentPorts(host: string = "127.0.0.1"): Promise { + try { + //const getPort = (await import("get-port")).default; + let port = await getPort({ host: host }); + let adjacent = await getPort({ host: host, port: port + 1 }); + if (adjacent === port + 1) return port; + else { + return await findAdjacentPorts(host); + } + } catch (e) { + console.log("Got error checking ports"); + console.log(e); + throw e; + } +} diff --git a/packages/sample/src/command/distance.ts b/packages/sample/src/command/distance.ts index 99615eac..4db57ecb 100644 --- a/packages/sample/src/command/distance.ts +++ b/packages/sample/src/command/distance.ts @@ -1,5 +1,5 @@ -import { ExpansionHub } from "@rev-robotics/rev-hub-core"; import { DistanceSensor } from "@rev-robotics/distance-sensor"; +import { ExpansionHub } from "@rev-robotics/rev-hub-core"; export async function distance( hub: ExpansionHub, diff --git a/packages/sample/src/command/error.ts b/packages/sample/src/command/error.ts index ebf524e5..93043d16 100644 --- a/packages/sample/src/command/error.ts +++ b/packages/sample/src/command/error.ts @@ -49,5 +49,4 @@ export async function error() { console.log("Got error opening child hub with invalid address"); console.log(e); } - hubs[0].close(); } diff --git a/packages/sample/src/command/imu.ts b/packages/sample/src/command/imu.ts new file mode 100644 index 00000000..876e445d --- /dev/null +++ b/packages/sample/src/command/imu.ts @@ -0,0 +1,12 @@ +import { ControlHub } from "@rev-robotics/rev-hub-core"; + +export async function imu(hub: ControlHub, continuous: boolean) { + await hub.initializeImu(); + + while (true) { + let imuData = await hub.getImuData(); + console.log(JSON.stringify(imuData)); + + if (!continuous) break; + } +} diff --git a/packages/sample/src/command/list.ts b/packages/sample/src/command/list.ts index 6a18dcd6..4501ea43 100644 --- a/packages/sample/src/command/list.ts +++ b/packages/sample/src/command/list.ts @@ -1,18 +1,25 @@ -import { hubHierarchyToString } from "../HubStringify.js"; +import { openConnectedExpansionHubs } from "@rev-robotics/expansion-hub"; +import { controlHubHierarchyToString } from "../HubStringify.js"; import { ExpansionHub } from "@rev-robotics/rev-hub-core"; +import { openUsbControlHubsAndChildren } from "../open-usb-control-hub.js"; -export async function list(hubs: ExpansionHub[]) { +export async function list() { + let usbControlHubs = await openUsbControlHubsAndChildren(); + for (const hub of usbControlHubs) { + let hierarchy = controlHubHierarchyToString(hub); + console.log(hierarchy); + hub.close(); + } + + const hubs: ExpansionHub[] = await openConnectedExpansionHubs(); for (const hub of hubs) { hub.on("error", (e: any) => { console.log(`Got error:`); console.log(e); }); - console.log(hubHierarchyToString(hub)); + //console.log(controlHubHierarchyToString(hub)); } - - setTimeout(() => { - hubs.forEach(async (hub) => { - hub.close(); - }); - }, 2000); + hubs.forEach((hub) => { + hub.close(); + }); } diff --git a/packages/sample/src/command/log.ts b/packages/sample/src/command/log.ts index 666f996c..6656cd58 100644 --- a/packages/sample/src/command/log.ts +++ b/packages/sample/src/command/log.ts @@ -1,5 +1,4 @@ -import { DebugGroup, VerbosityLevel } from "@rev-robotics/rhsplib"; -import { ExpansionHub } from "@rev-robotics/rev-hub-core"; +import { DebugGroup, ExpansionHub, VerbosityLevel } from "@rev-robotics/rev-hub-core"; export async function injectLog(hub: ExpansionHub, hint: string) { await hub.injectDataLogHint(hint); diff --git a/packages/sample/src/command/servo.ts b/packages/sample/src/command/servo.ts index 140ad98a..9b909b3f 100644 --- a/packages/sample/src/command/servo.ts +++ b/packages/sample/src/command/servo.ts @@ -1,12 +1,10 @@ import { ExpansionHub } from "@rev-robotics/rev-hub-core"; -export async function runServo( - hub: ExpansionHub, - channel: number, - pulseWidth: number, - framePeriod: number, -) { +export async function runServo(hub: ExpansionHub, channel: number, pulseWidth: number, framePeriod: number) { await hub.setServoConfiguration(channel, framePeriod); await hub.setServoPulseWidth(channel, pulseWidth); await hub.setServoEnable(channel, true); + setTimeout(() => { + hub.close(); + }, 10000); } diff --git a/packages/sample/src/command/set-hub-address.ts b/packages/sample/src/command/set-hub-address.ts new file mode 100644 index 00000000..dfec0217 --- /dev/null +++ b/packages/sample/src/command/set-hub-address.ts @@ -0,0 +1,8 @@ +import { ControlHub } from "@rev-robotics/rev-hub-core"; + +export async function setHubAddress(hub: ControlHub, address: number) { + hub.on("addressChanged", (oldAddress, newAddress) => { + console.log(`Address changed from ${oldAddress} -> ${newAddress}`); + }); + await hub.setNewModuleAddress(address); +} diff --git a/packages/sample/src/command/status.ts b/packages/sample/src/command/status.ts new file mode 100644 index 00000000..2564427a --- /dev/null +++ b/packages/sample/src/command/status.ts @@ -0,0 +1,15 @@ +import { ControlHub, ModuleStatus } from "@rev-robotics/rev-hub-core"; + +export async function status(hub: ControlHub) { + hub.on("statusChanged", (status: ModuleStatus) => { + console.log(`Status Changed:\n${JSON.stringify(status)}`); + }); + + hub.on("addressChanged", (oldAddress, newAddress) => { + console.log(`Address Changed: ${oldAddress} -> ${newAddress}`); + }); + + hub.on("sessionEnded", () => { + console.log("Session ended"); + }); +} diff --git a/packages/sample/src/main.ts b/packages/sample/src/main.ts index 55aba7db..f81be1f0 100644 --- a/packages/sample/src/main.ts +++ b/packages/sample/src/main.ts @@ -31,11 +31,10 @@ import { } from "@rev-robotics/expansion-hub"; import { getLed, getLedPattern, led, ledPattern } from "./command/led.js"; import { runServo } from "./command/servo.js"; -import { ExpansionHub, ParentExpansionHub, RevHub } from "@rev-robotics/rev-hub-core"; +import { ControlHub, DigitalState, ExpansionHub, ParentExpansionHub, RevHub } from "@rev-robotics/rev-hub-core"; import { injectLog, setDebugLogLevel } from "./command/log.js"; import { firmwareVersion } from "./command/firmware-version.js"; import { getBulkInputData } from "./command/bulkinput.js"; -import { DigitalState } from "@rev-robotics/rev-hub-core"; import { digitalRead, digitalReadAll, @@ -45,6 +44,11 @@ import { import { distance } from "./command/distance.js"; import { sendFailSafe } from "./command/failsafe.js"; import { queryInterface } from "./command/query.js"; +import {openUsbControlHubsAndChildren} from "./open-usb-control-hub.js"; +import { imu } from "./command/imu.js"; +import { distance } from "./command/distance.js"; +import { status } from "./command/status.js"; +import { setHubAddress } from "./command/set-hub-address.js"; function runOnSigint(block: () => void) { process.on("SIGINT", () => { @@ -55,6 +59,8 @@ function runOnSigint(block: () => void) { const program = new Command(); +program.name("ch"); + program.version("1.0.0"); program @@ -69,6 +75,9 @@ program .option( "-c --child
", "communicate with the specified child Expansion Hub instead of its parent; requires parent address to be specified when the parent is an Expansion Hub", + ).option( + "--control", + "Specify that this hub is a control hub. Default is expansion hub" ); program @@ -85,8 +94,7 @@ program .command("list") .description("List all connected expansion hubs") .action(async () => { - let hubs = await openConnectedExpansionHubs(); - await list(hubs); + await list(); }); program @@ -98,7 +106,7 @@ program "green, 0.5FF0000 for half-second red.", ) .action(async (steps) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -112,20 +120,17 @@ program .command("get-pattern") .description("Get LED Pattern steps") .action(async () => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); }); - - await getLedPattern(hub); - close(); }); program .command("led ") .description("Set LED color") .action(async (r, g, b) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); }); @@ -134,27 +139,26 @@ program let gValue = Number(g); let bValue = Number(b); await led(hub, rValue, gValue, bValue); + + process.on("SIGINT", () => { + close(); + }); }); program .command("get-led") .description("Get LED color. Values are [0,255]") .action(async () => { - let [hub, close] = await getExpansionHubOrThrow(); - - runOnSigint(() => { - close(); - }); + let [hub, close] = await getRevHubOrThrow(); await getLed(hub); - - hub.close(); + close(); }); program .command("query ") .description("Query interface information") .action(async (name) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); await queryInterface(hub, name); close(); @@ -166,23 +170,42 @@ program .option("--continuous", "run continuously") .action(async (options) => { let isContinuous = options.continuous !== undefined; - let [hub, close] = await getExpansionHubOrThrow(); - - runOnSigint(() => { - close(); - }); + let [hub, close] = await getRevHubOrThrow(); await getBulkInputData(hub, isContinuous); close(); }); +program + .command("log ") + .description("Inject a log hint") + .action(async (text) => { + let [hub, close] = await getRevHubOrThrow(); + await injectLog(hub, text); + close(); + }); + +program + .command("loglevel ") + .description( + "Set log level. Valid values for group are: Main, " + + "TransmitterToHost, ReceiverFromHost, ADC, PWMAndServo, ModuleLED, " + + "DigitalIO, I2C, Motor0, Motor1, Motor2, or Motor3. Valid values for level are [0,3]", + ) + .action(async (group, level) => { + let [hub, close] = await getRevHubOrThrow(); + let levelNumber = Number(level); + await setDebugLogLevel(hub, group, levelNumber); + close(); + }); + program .command("failsafe") .description( "Start servo 0 for 2 seconds, then send failsafe. Wait 2 more seconds to close. The servo should stop after 2 seconds.", ) .action(async () => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -195,12 +218,54 @@ program .command("version") .description("Get firmware version") .action(async () => { - let [hub, close] = await getExpansionHubOrThrow(); - + let [hub, close] = await getRevHubOrThrow(); await firmwareVersion(hub); close(); }); +program + .command("set-address
") + .description("Set Module Address") + .action(async (address) => { + let addressNumber = Number(address); + let [hub, close] = await getRevHubOrThrow(); + await setHubAddress(hub as ControlHub, addressNumber); + close(); + }); + +program.command("status").action(async () => { + let hubs = await openUsbControlHubsAndChildren(); + let allHubs = hubs.flatMap((hub) => [hub, ...hub.children]); + for (let hub of allHubs) { + await status(hub as ControlHub); + } + + process.on("SIGINT", () => { + for (let hub of hubs) { + hub.close(); + } + process.exit(); + }); +}); + +program + .command("bulkInput") + .description("Get all input data at once. Specify --continuous to run continuously.") + .option("--continuous", "run continuously") + .description("Get the current encoder position of a motor") + .action(async (channel, options) => { + let channelNumber = Number(channel); + let [hub, close] = await getRevHubOrThrow(); + if (options.reset) { + await resetEncoder(hub, channelNumber); + close(); + } else { + let isContinuous = options.continuous !== undefined; + await readEncoder(hub, channelNumber, isContinuous); + close(); + } + }); + let digitalCommand = program.command("digital"); digitalCommand @@ -218,7 +283,7 @@ digitalCommand } let digitalState = stateBoolean ? DigitalState.HIGH : DigitalState.LOW; - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -236,7 +301,7 @@ digitalCommand let isContinuous = options.continuous !== undefined; let channelNumber = Number(channel); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -253,7 +318,7 @@ digitalCommand .action(async (options) => { let isContinuous = options.continuous !== undefined; - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -273,7 +338,7 @@ digitalCommand let bitfieldValue = parseInt(bitfield, 2); let bitmaskValue = parseInt(bitmask, 2); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -294,7 +359,7 @@ motorCommand .action(async (channel, options) => { let isContinuous = options.continuous !== undefined; let channelNumber = Number(channel); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -311,7 +376,7 @@ motorCommand .description("Get the current encoder position of a motor") .action(async (channel, options) => { let channelNumber = Number(channel); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -336,7 +401,7 @@ pidCommand let pValue = Number(p); let iValue = Number(i); let dValue = Number(d); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -351,14 +416,14 @@ pidCommand .description("Get PID coefficients for regulated velocity mode for a motor") .action(async (channel) => { let channelNumber = Number(channel); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); }); await getMotorRegulatedVelocityPidf(hub, channelNumber); - hub.close(); + close(); }); let pidfCommand = motorCommand @@ -374,11 +439,11 @@ pidfCommand let iValue = Number(i); let dValue = Number(d); let fValue = Number(f); - let hubs = await openConnectedExpansionHubs(); - let hub = hubs[0]; + let [hub, close] = await getRevHubOrThrow(); + runOnSigint(() => { - hub.close(); + close(); }); await setMotorRegulatedVelocityPidf( @@ -389,7 +454,7 @@ pidfCommand dValue, fValue, ); - hub.close(); + close(); }); pidfCommand @@ -397,15 +462,14 @@ pidfCommand .description("Get PIDF coefficients for regulated velocity mode for a motor") .action(async (channel) => { let channelNumber = Number(channel); - let hubs = await openConnectedExpansionHubs(); - let hub = hubs[0]; + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { - hub.close(); + close(); }); await getMotorRegulatedVelocityPidf(hub, channelNumber); - hub.close(); + close(); }); let alertCommand = motorCommand @@ -417,7 +481,7 @@ alertCommand .description("Get motor alert current (mA)") .action(async (channel) => { let channelNumber = Number(channel); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -435,7 +499,7 @@ alertCommand .action(async (channel, current) => { let channelNumber = Number(channel); let currentValue = Number(current); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -453,7 +517,7 @@ motorCommand .action(async (channel, power) => { let channelNumber = Number(channel); let powerNumber = Number(power); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { hub.setMotorChannelEnable(channelNumber, false); @@ -469,7 +533,7 @@ motorCommand .action(async (channel, speed) => { let channelNumber = Number(channel); let speedNumber = Number(speed); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { hub.setMotorChannelEnable(channelNumber, false); @@ -487,7 +551,7 @@ motorCommand let positionNumber = Number(position); let toleranceNumber = Number(tolerance); let velocityNumber = Number(velocity); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { hub.setMotorChannelEnable(channelNumber, false); @@ -525,13 +589,13 @@ program .command("analog ") .option("--continuous", "Run continuously") .description( - "Read the analog value of the given port. Specify" + + "Read the analog value of the given port. Specify " + "--continuous to run continuously.", ) .action(async (port, options) => { let isContinuous = options.continuous !== undefined; let portNumber = Number(port); - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -549,7 +613,7 @@ program "Specify --continuous to run continuously", ) .action(async (options) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); let isContinuous = options.continuous !== undefined; runOnSigint(() => { @@ -557,6 +621,7 @@ program }); await temperature(hub, isContinuous); + close(); }); program @@ -567,7 +632,8 @@ program ) .action(async (options) => { let isContinuous = options.continuous !== undefined; - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); + runOnSigint(() => { close(); }); @@ -588,7 +654,7 @@ batteryCommand ) .action(async (options) => { let isContinuous = options.continuous !== undefined; - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -601,10 +667,12 @@ batteryCommand batteryCommand .command("current") .option("--continuous", "Run continuously") - .description("Read the battery current. Specify --continuous to run continuously") + .description( + "Read the current battery current (mA). Specify --continuous to run continuously", + ) .action(async (options) => { let isContinuous = options.continuous !== undefined; - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -622,7 +690,7 @@ program ) .action(async (options) => { let isContinuous = options.continuous !== undefined; - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -637,7 +705,7 @@ program .option("--continuous", "Run continuously") .description("Read the digital bus current. Specify --continuous to run continuously") .action(async (options) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); let isContinuous = options.continuous !== undefined; runOnSigint(() => { @@ -655,7 +723,7 @@ program "Read the total current through all servos. Specify --continuous to run continuously", ) .action(async (options) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); let isContinuous = options.continuous !== undefined; runOnSigint(() => { @@ -670,14 +738,14 @@ program .command("log ") .description("Inject a log hint") .action(async (text) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); }); await injectLog(hub, text); - hub.close(); + close(); }); program @@ -688,7 +756,7 @@ program "DigitalIO, I2C, Motor0, Motor1, Motor2, or Motor3. Valid values for level are [0,3]", ) .action(async (group, level) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); runOnSigint(() => { close(); @@ -696,38 +764,99 @@ program let levelNumber = Number(level); await setDebugLogLevel(hub, group, levelNumber); - hub.close(); + close(); }); program .command("servo [frameWidth]") .description("Run a servo with pulse width and optional frame width") .action(async (channel, pulseWidth, frameWidth) => { - let [hub, close] = await getExpansionHubOrThrow(); + let [hub, close] = await getRevHubOrThrow(); let channelValue = Number(channel); let pulseWidthValue = Number(pulseWidth); let frameWidthValue = frameWidth ? Number(frameWidth) : 4000; + runOnSigint(async () => { await hub.setServoEnable(channelValue, false); close(); }); await runServo(hub, channelValue, pulseWidthValue, frameWidthValue); + + runOnSigint(() => { + close(); + }); }); +program + .command("distance ") + .option("--continuous", "run continuously") + .description("Read distance from a REV 2m distance sensor") + .action(async (channel, options): Promise => { + let isContinuous = options.continuous !== undefined; + let channelNumber = Number(channel); + let [hub, close] = await getRevHubOrThrow(); + await distance(hub, channelNumber, isContinuous); + + runOnSigint(() => { + close(); + }); + }); + +program + .command("imu") + .option("--continuous", "run continuously") + .description("Get IMU data. Specify --continuous to run continuously.") + .action(async (options) => { + let isContinuous = options.continuous !== undefined; + let [hub, close] = await getRevHubOrThrow(); + + runOnSigint(() => { + close(); + }); + + if (hub.isControlHub()) { + await imu(hub, isContinuous); + } else { + throw new Error("Hub is not a control hub."); + } + close(); + }); + +program + .command("set-address
") + .description("Set Module Address") + .action(async (address) => { + let addressNumber = Number(address); + let [hub, close] = await getRevHubOrThrow(); + await setHubAddress(hub as ControlHub, addressNumber); + close(); + }); + +program.command("status").action(async () => { + let [hub, close] = await getRevHubOrThrow(); + await status(hub as ControlHub); + + process.on("SIGINT", () => { + close(); + process.exit(); + }); +}); + program.parse(process.argv); /** - * Returns the expansion hub referred to by the options provided to the program. + * Returns the rev hub referred to by the options provided to the program. * This method also returns a close method. Other hubs may need to be opened, so * prefer calling the returned close method over closing the hub directly. */ -async function getExpansionHubOrThrow(): Promise<[hub: ExpansionHub, close: () => void]> { +async function getRevHubOrThrow(): Promise<[hub: ExpansionHub, close: () => void]> { let options = program.opts(); let serialNumber = options.serial; // options.child and options.parent are strings, so a specified address of "0" will be treated as truthy, and will not be ignored. let childAddress = options.child ? Number(options.child) : undefined; let parentAddress = options.parent ? Number(options.parent) : undefined; + let isControlHub = options.control ?? false; if (childAddress !== undefined && (childAddress < 1 || childAddress > 255)) { throw new Error(`${childAddress} is not a valid child address`); } else if ( @@ -747,31 +876,76 @@ async function getExpansionHubOrThrow(): Promise<[hub: ExpansionHub, close: () = "parent address must be specified if serial number is specified.", ); } - return openExpansionHubWithSerialNumber( - serialNumber, - parentAddress, - childAddress, - ); + if(isControlHub) { + let hubs = await openUsbControlHubsAndChildren(); + let onClose = () => { hubs.forEach((hub) => hub.close()) }; + if(childAddress === undefined) { + return [hubs[0], onClose]; + } + + for(let controlHub of hubs) { + if(controlHub.moduleAddress == childAddress) { + return [controlHub as ExpansionHub, onClose]; + } + for(let hub of controlHub.children) { + if(hub.moduleAddress == childAddress) { + return [hub as ExpansionHub, onClose]; + } + } + } + } else { + return openExpansionHubWithSerialNumber( + serialNumber, + parentAddress, + childAddress, + ); + } } else if (parentAddress !== undefined) { - return openExpansionHubWithAddress(parentAddress, childAddress); - } - - let connectedHubs: ParentExpansionHub[] = await openConnectedExpansionHubs(); - if (connectedHubs.length == 0) { - throw new Error("No hubs are connected"); - } - if (connectedHubs.length > 1) { - throw new Error("Multiple hubs connected. You must specify a serialNumber."); + if(isControlHub) { + let hubs = await openUsbControlHubsAndChildren(); + let onClose = () => { hubs.forEach((hub) => hub.close()) }; + + for(let controlHub of hubs) { + if(controlHub.moduleAddress == childAddress) { + return [controlHub as ExpansionHub, onClose]; + } + for(let hub of controlHub.children) { + if(hub.moduleAddress == childAddress) { + return [hub as ExpansionHub, onClose]; + } + } + } + } else { + return openExpansionHubWithAddress(parentAddress, childAddress); + } } - // Open the only Hub that is connected - - let closeHubs = () => { - for (let hub of connectedHubs) { - hub.close(); + if(isControlHub) { + let hubs = await openUsbControlHubsAndChildren(); + if (hubs.length == 0) { + throw new Error("No hubs are connected"); + } + if (hubs.length > 1) { + throw new Error("Multiple hubs connected. You must specify a serialNumber."); + } + return [hubs[0], () => { hubs.forEach((hub) => hub.close()) }]; + } else { + let connectedHubs: ParentExpansionHub[] = await openConnectedExpansionHubs(); + if (connectedHubs.length == 0) { + throw new Error("No hubs are connected"); } - }; - return [connectedHubs[0], closeHubs]; + if (connectedHubs.length > 1) { + throw new Error("Multiple hubs connected. You must specify a serialNumber."); + } + + // Open the only Hub that is connected + let closeHubs = () => { + for (let hub of connectedHubs) { + hub.close(); + } + }; + return [connectedHubs[0], closeHubs]; + } } /** diff --git a/packages/sample/src/open-usb-control-hub.ts b/packages/sample/src/open-usb-control-hub.ts new file mode 100644 index 00000000..ec086b99 --- /dev/null +++ b/packages/sample/src/open-usb-control-hub.ts @@ -0,0 +1,42 @@ +import { ControlHub, ParentRevHub } from "@rev-robotics/rev-hub-core"; +import { ControlHubInternal } from "@rev-robotics/control-hub/dist/internal/ControlHub.js"; +import { openUsbControlHubs } from "./adb-setup.js"; + +export async function openUsbControlHubsAndChildren(): Promise { + let hubs = await openUsbControlHubs(); + let result: ControlHub[] = []; + + for (let hub of hubs) { + let controlHub = hub as ControlHubInternal; + let addresses: Record< + string, + { + serialNumber: string; + parentHubAddress: number; + childAddresses: number[]; + } + > = await controlHub.sendCommand("scanAndDiscover", {}, 20000); + + for (let serialNumber in addresses) { + let parentHub: ParentRevHub; + let childAddresses = addresses[serialNumber].childAddresses; + + if (serialNumber === "(embedded)") { + parentHub = controlHub; + } else { + let parentHubInfo = addresses[serialNumber]; + parentHub = await controlHub.addUsbConnectedHub( + serialNumber, + parentHubInfo.parentHubAddress, + ); + } + + for (let childAddress of childAddresses) { + await parentHub.addChildByAddress(childAddress); + } + } + result.push(controlHub); + } + + return result; +} diff --git a/tsconfig.json b/tsconfig.json index 5698646d..0a85fede 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "strict": true, "lib": ["es2021"], "target": "es2021", + "sourceMap": true, "types": [ "node" ],