commit
41ca97e31a
@ -1,54 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
defaults: &defaults
|
|
||||||
working_directory: ~/repo
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:8.11.4
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
<<: *defaults
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-dependencies-{{ checksum "package.json" }}
|
|
||||||
- v1-dependencies-
|
|
||||||
- run: sudo apt-get install -y bluetooth bluez libbluetooth-dev libudev-dev
|
|
||||||
- run: npm install
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
key: v1-dependencies-{{ checksum "package.json" }}
|
|
||||||
- run: npm run all
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: ~/repo
|
|
||||||
paths: .
|
|
||||||
deploy:
|
|
||||||
<<: *defaults
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: ~/repo
|
|
||||||
- run:
|
|
||||||
name: Authenticate with registry
|
|
||||||
command: echo "//registry.npmjs.org/:_authToken=$npm_TOKEN" > ~/repo/.npmrc
|
|
||||||
- run:
|
|
||||||
name: Publish package
|
|
||||||
command: npm publish
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build-deploy:
|
|
||||||
jobs:
|
|
||||||
- build:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /^v.*/
|
|
||||||
- deploy:
|
|
||||||
requires:
|
|
||||||
- build
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /^v.*/
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
22
.drone.yml
Normal file
22
.drone.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
pipeline:
|
||||||
|
|
||||||
|
install:
|
||||||
|
image: node:10.15.1
|
||||||
|
commands:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y bluetooth bluez libbluetooth-dev libudev-dev
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: node:10.15.1
|
||||||
|
commands:
|
||||||
|
- npm run all
|
||||||
|
|
||||||
|
publish:
|
||||||
|
image: plugins/npm
|
||||||
|
username: nathankellenicki
|
||||||
|
token:
|
||||||
|
from_secret: NPM_TOKEN
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- refs/tags/v*
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.tgz
|
*.tgz
|
||||||
|
**/.DS_store
|
||||||
|
63
README.md
63
README.md
@ -1,22 +1,15 @@
|
|||||||
[](https://circleci.com/gh/nathankellenicki/node-poweredup)
|
[](https://drone.kellenicki.com/nkellenicki/node-poweredup)
|
||||||
[](https://gitter.im/node-poweredup?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://www.npmjs.com/package/node-poweredup)
|
||||||

|
|
||||||
|
|
||||||
# **node-poweredup** - A Node.js module to interface with LEGO Powered UP components.
|
# **node-poweredup** - A Javascript module to interface with LEGO Powered Up components.
|
||||||
|
|
||||||
### Introduction
|
### Introduction
|
||||||
|
|
||||||
LEGO Powered UP is the successor to Power Functions, the system for adding electronics to LEGO models. Powered UP is a collection of ranges - starting with LEGO WeDo 2.0 released in 2016, LEGO Boost released in 2017, and LEGO Powered UP released in 2018. It also includes the 2018 Duplo App-Controlled Train sets.
|
LEGO Powered Up is the successor to Power Functions, the system for adding electronics to LEGO models. Powered Up is a collection of ranges - starting with LEGO WeDo 2.0 released in 2016, LEGO Boost released in 2017, LEGO Powered Up released in 2018, and LEGO Technic CONTROL+ released in 2019. It also includes the 2018 Duplo App-Controlled Train sets.
|
||||||
|
|
||||||
Powered UP has a few improvements over Power Functions:
|
This library allows communication and control of Powered Up devices and peripherals via Javascript, both from Node.js and from the browser using Web Bluetooth.
|
||||||
|
|
||||||
1. The use of Bluetooth Low Energy makes it easy to control from a computer, and even write code for.
|
### Node.js Installation
|
||||||
|
|
||||||
2. The ability to use sensors to react to events happening in the real world opens up a whole new world of possibilities.
|
|
||||||
|
|
||||||
3. As Powered UP hubs and remotes pair with each other, the system allows for a near unlimited number of independently controlled models in the same room. Power Functions was limited to 8 due to the use of infra-red for communication.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
Node.js v8.0 required.
|
Node.js v8.0 required.
|
||||||
|
|
||||||
@ -30,17 +23,19 @@ Note: node-poweredup has been tested on macOS 10.13 and Debian/Raspbian on the R
|
|||||||
|
|
||||||
### Compatibility
|
### Compatibility
|
||||||
|
|
||||||
While most Powered UP components and Hubs are compatible with each other, there are exceptions. For example, there is limited backwards compatibility between newer components and the WeDo 2.0 Smart Hub. However WeDo 2.0 components are fully forwards compatible with newer Hubs.
|
While most Powered Up components and Hubs are compatible with each other, there are exceptions. For example, there is limited backwards compatibility between newer components and the WeDo 2.0 Smart Hub. However WeDo 2.0 components are fully forwards compatible with newer Hubs.
|
||||||
|
|
||||||
| Device Name | Product Code | Type | WeDo 2.0 Smart Hub | Boost Move Hub | Powered UP Hub | Availability |
|
| Device Name | Product Code | Type | WeDo 2.0 Smart Hub | Boost Move Hub | Powered Up Hub | Control+ Hub | Availability |
|
||||||
| ------------------------------- | ------------ | ------------- | ------------------ | -------------- | -------------- | ------------ |
|
| ------------------------------- | ------------ | ------------- | ------------------ | -------------- | -------------- | ------------ | ----- |
|
||||||
| WeDo 2.0 Tilt Sensor | <a href="https://brickset.com/sets/45305-1/">45305</a> | Sensor | Yes | Yes | Yes | <a href="https://brickset.com/sets/45300-1/">45300</a> |
|
| WeDo 2.0 Tilt Sensor | <a href="https://brickset.com/sets/45305-1/">45305</a> | Sensor | Yes | Yes | Yes | Yes | <a href="https://brickset.com/sets/45300-1/">45300</a> |
|
||||||
| WeDo 2.0 Motion Sensor | <a href="https://brickset.com/sets/45304-1/">45304</a> | Sensor | Yes | Yes | Yes | <a href="https://brickset.com/sets/45300-1/">45300</a> |
|
| WeDo 2.0 Motion Sensor | <a href="https://brickset.com/sets/45304-1/">45304</a> | Sensor | Yes | Yes | Yes | Yes | <a href="https://brickset.com/sets/45300-1/">45300</a> |
|
||||||
| WeDo 2.0 Medium Motor | <a href="https://brickset.com/sets/45303-1/">45303</a> | Motor | Yes | Yes | Yes | <a href="https://brickset.com/sets/45300-1/">45300</a><br /> <a href="https://brickset.com/sets/76112-1/">76112</a> |
|
| WeDo 2.0 Medium Motor | <a href="https://brickset.com/sets/45303-1/">45303</a> | Motor | Yes | Yes | Yes | Yes | <a href="https://brickset.com/sets/45300-1/">45300</a><br /> <a href="https://brickset.com/sets/76112-1/">76112</a> |
|
||||||
| Boost Color and Distance Sensor | <a href="https://brickset.com/sets/88007-1/">88007</a> | Sensor | *Partial* | Yes | Yes | <a href="https://brickset.com/sets/17101-1/">17101</a> |
|
| Boost Color and Distance Sensor | <a href="https://brickset.com/sets/88007-1/">88007</a> | Sensor | *Partial* | Yes | Yes | Yes | <a href="https://brickset.com/sets/17101-1/">17101</a> |
|
||||||
| Boost Tacho Motor | <a href="https://brickset.com/sets/88008-1/">88008</a> | Motor/Sensor | *Partial* | Yes | *Partial* | <a href="https://brickset.com/sets/17101-1/">17101</a> |
|
| Boost Tacho Motor | <a href="https://brickset.com/sets/88008-1/">88008</a> | Motor/Sensor | *Partial* | Yes | Yes | Yes | <a href="https://brickset.com/sets/17101-1/">17101</a> |
|
||||||
| Powered UP Train Motor | <a href="https://brickset.com/sets/88011-1/">88011</a> | Motor | Yes | Yes | Yes | <a href="https://brickset.com/sets/60197-1/">60197</a><br /><a href="https://brickset.com/sets/60198-1/">60198</a> |
|
| Powered Up Train Motor | <a href="https://brickset.com/sets/88011-1/">88011</a> | Motor | Yes | Yes | Yes | Yes | <a href="https://brickset.com/sets/60197-1/">60197</a><br /><a href="https://brickset.com/sets/60198-1/">60198</a> |
|
||||||
| Powered UP LED Lights | <a href="https://brickset.com/sets/88005-1/">88005</a> | Light | Yes | Yes | Yes | <a href="https://brickset.com/sets/88005-1/">88005</a> |
|
| Powered Up LED Lights | <a href="https://brickset.com/sets/88005-1/">88005</a> | Light | Yes | Yes | Yes | Yes | <a href="https://brickset.com/sets/88005-1/">88005</a> |
|
||||||
|
| Control+ Large Motor | 22169 | Motor/Sensor | *Partial* | No | Yes | Yes | <a href="https://brickset.com/sets/42099-1/">42099</a> |
|
||||||
|
| Control+ XLarge Motor | 22172 | Motor/Sensor | *Partial* | No | Yes | Yes | <a href="https://brickset.com/sets/42099-1/">42099</a><br /><a href="https://brickset.com/sets/42100-1/">42100</a> |
|
||||||
|
|
||||||
In addition, the Hubs themselves have certain built-in features which this library exposes.
|
In addition, the Hubs themselves have certain built-in features which this library exposes.
|
||||||
|
|
||||||
@ -48,50 +43,54 @@ In addition, the Hubs themselves have certain built-in features which this libra
|
|||||||
| ------------------ | ------------ | ---------------------- | ------------ |
|
| ------------------ | ------------ | ---------------------- | ------------ |
|
||||||
| WeDo 2.0 Smart hub | <a href="https://brickset.com/sets/45301-1/">45301</a> | RGB LED<br />Piezo Buzzer<br />Button | <a href="https://brickset.com/sets/45300-1/">45300</a> |
|
| WeDo 2.0 Smart hub | <a href="https://brickset.com/sets/45301-1/">45301</a> | RGB LED<br />Piezo Buzzer<br />Button | <a href="https://brickset.com/sets/45300-1/">45300</a> |
|
||||||
| Boost Move Hub | <a href="https://brickset.com/sets/88006-1/">88006</a> | RGB LED<br />Tilt Sensor<br />2x Tacho Motors<br />Button | <a href="https://brickset.com/sets/17101-1/">17101</a> |
|
| Boost Move Hub | <a href="https://brickset.com/sets/88006-1/">88006</a> | RGB LED<br />Tilt Sensor<br />2x Tacho Motors<br />Button | <a href="https://brickset.com/sets/17101-1/">17101</a> |
|
||||||
| Powered UP Hub | <a href="https://brickset.com/sets/88009-1/">88009</a> | RGB LED<br />Button | <a href="https://brickset.com/sets/60197-1/">60197</a><br /><a href="https://brickset.com/sets/60198-1/">60198</a><br /><a href="https://brickset.com/sets/76112-1/">76112</a> |
|
| Powered Up Hub | <a href="https://brickset.com/sets/88009-1/">88009</a> | RGB LED<br />Button | <a href="https://brickset.com/sets/60197-1/">60197</a><br /><a href="https://brickset.com/sets/60198-1/">60198</a><br /><a href="https://brickset.com/sets/76112-1/">76112</a> |
|
||||||
| Powered UP Remote | <a href="https://brickset.com/sets/88010-1/">88010</a> | RGB LED<br />Left and Right Control Buttons<br />Button | <a href="https://brickset.com/sets/60197-1/">60197</a><br /><a href="https://brickset.com/sets/60198-1/">60198</a> |
|
| Powered Up Remote | <a href="https://brickset.com/sets/88010-1/">88010</a> | RGB LED<br />Left and Right Control Buttons<br />Button | <a href="https://brickset.com/sets/60197-1/">60197</a><br /><a href="https://brickset.com/sets/60198-1/">60198</a> |
|
||||||
| Duplo Train Base | 28743 | RGB LED/Headlights<br />Speaker<br />Speedometer<br />Motor<br />Color and Distance Sensor<br />Button | <a href="https://brickset.com/sets/10874-1/">10874</a><br /><a href="https://brickset.com/sets/10875-1/">10875</a> |
|
| Duplo Train Base | 28743 | RGB LED/Headlights<br />Speaker<br />Speedometer<br />Motor<br />Color and Distance Sensor<br />Button | <a href="https://brickset.com/sets/10874-1/">10874</a><br /><a href="https://brickset.com/sets/10875-1/">10875</a> |
|
||||||
|
| Control+ Hub | 22127 | RGB LED<br />Button<br />Tilt Sensor<br />Accelerometer | <a href="https://brickset.com/sets/42099-1/">42099</a><br /><a href="https://brickset.com/sets/42100-1/">42100</a> |
|
||||||
|
|
||||||
### Known Issues and Limitations
|
### Known Issues and Limitations
|
||||||
|
|
||||||
* The Boost Color and Distance sensor only works in color mode with the WeDo 2.0 Smart Hub.
|
* The Boost Color and Distance sensor only works in color mode with the WeDo 2.0 Smart Hub.
|
||||||
|
|
||||||
* When used with the WeDo 2.0 Smart Hub, the Boost Tacho Motor does not support rotating the motor by angle.
|
* When used with the WeDo 2.0 Smart Hub, the Boost Tacho Motor and Control+ Motors do not support rotating the motor by angle.
|
||||||
|
|
||||||
* When used with the Powered UP Hub, the Boost Tacho Motor does not support rotating the motor by angle. It also does not support rotation detection.
|
* When used with the Boost Move Hub, the Control+ Motors do not currently accept commands.
|
||||||
|
|
||||||
* Plugging two Boost Tacho Motors into the Powered UP Hub will crash the Hub (This requires a firmware update from LEGO to fix).
|
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
[Full documentation is available here.](https://nathankellenicki.github.io/node-poweredup/)
|
[Full documentation is available here.](https://nathankellenicki.github.io/node-poweredup/)
|
||||||
|
|
||||||
### Sample Usage
|
### Node.js Sample Usage
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const PoweredUP = require("node-poweredup");
|
const PoweredUP = require("node-poweredup");
|
||||||
const poweredUP = new PoweredUP.PoweredUP();
|
const poweredUP = new PoweredUP.PoweredUP();
|
||||||
|
|
||||||
poweredUP.on("discover", async (hub) => { // Wait to discover a Hub
|
poweredUP.on("discover", async (hub) => { // Wait to discover a Hub
|
||||||
|
console.log(`Discovered ${hub.name}!`);
|
||||||
await hub.connect(); // Connect to the Hub
|
await hub.connect(); // Connect to the Hub
|
||||||
|
console.log("Connected");
|
||||||
await hub.sleep(3000); // Sleep for 3 seconds before starting
|
await hub.sleep(3000); // Sleep for 3 seconds before starting
|
||||||
|
|
||||||
while (true) { // Repeat indefinitely
|
while (true) { // Repeat indefinitely
|
||||||
|
console.log("Running motor B at speed 75");
|
||||||
hub.setMotorSpeed("B", 75); // Start a motor attached to port B to run a 3/4 speed (75) indefinitely
|
hub.setMotorSpeed("B", 75); // Start a motor attached to port B to run a 3/4 speed (75) indefinitely
|
||||||
|
console.log("Running motor A at speed 100 for 2 seconds");
|
||||||
await hub.setMotorSpeed("A", 100, 2000); // Run a motor attached to port A for 2 seconds at maximum speed (100) then stop
|
await hub.setMotorSpeed("A", 100, 2000); // Run a motor attached to port A for 2 seconds at maximum speed (100) then stop
|
||||||
await hub.sleep(1000); // Do nothing for 1 second
|
await hub.sleep(1000); // Do nothing for 1 second
|
||||||
|
console.log("Running motor A at speed -50 for 1 seconds");
|
||||||
await hub.setMotorSpeed("A", -50, 1000); // Run a motor attached to port A for 1 second at 1/2 speed in reverse (-50) then stop
|
await hub.setMotorSpeed("A", -50, 1000); // Run a motor attached to port A for 1 second at 1/2 speed in reverse (-50) then stop
|
||||||
await hub.sleep(1000); // Do nothing for 1 second
|
await hub.sleep(1000); // Do nothing for 1 second
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
poweredUP.scan(); // Start scanning for Hubs
|
poweredUP.scan(); // Start scanning for Hubs
|
||||||
|
console.log("Scanning for Hubs...");
|
||||||
```
|
```
|
||||||
|
|
||||||
More examples are available in the "examples" directory.
|
More examples are available in the "examples" directory.
|
||||||
|
|
||||||
### Credits
|
### Credits
|
||||||
|
|
||||||
Thanks go to Jorge Pereira ([@JorgePe](https://github.com/JorgePe)), Sebastian Raff ([@hobbyquaker](https://github.com/hobbyquaker)), Valentin Heun ([@vheun](https://github.com/vheun)), Johan Korten ([@jakorten](https://github.com/jakorten)), and Andrey Pokhilko ([@undera](https://github.com/undera)) for their various works, contributions, and assistance on figuring out the LEGO Boost, WeDo 2.0, and Powered UP protocols.
|
Thanks go to Jorge Pereira ([@JorgePe](https://github.com/JorgePe)), Sebastian Raff ([@hobbyquaker](https://github.com/hobbyquaker)), Valentin Heun ([@vheun](https://github.com/vheun)), Johan Korten ([@jakorten](https://github.com/jakorten)), and Andrey Pokhilko ([@undera](https://github.com/undera)) for their various works, contributions, and assistance on figuring out the LEGO Boost, WeDo 2.0, and Powered Up protocols.
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
6816
docs/ControlPlusHub.html
Normal file
6816
docs/ControlPlusHub.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
163
docs/Hub.html
163
docs/Hub.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1639
docs/PUPHub.html
1639
docs/PUPHub.html
File diff suppressed because one or more lines are too long
1806
docs/PUPRemote.html
1806
docs/PUPRemote.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10722
docs/classes.list.html
10722
docs/classes.list.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
502
docs/controlplushub.js.html
Normal file
502
docs/controlplushub.js.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
172
docs/global.html
172
docs/global.html
File diff suppressed because one or more lines are too long
145
docs/hub.js.html
145
docs/hub.js.html
File diff suppressed because one or more lines are too long
118
docs/index.html
118
docs/index.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
367
docs/poweredup-node.js.html
Normal file
367
docs/poweredup-node.js.html
Normal file
File diff suppressed because one or more lines are too long
@ -93,6 +93,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const bledevice_1 = require("./bledevice");
|
||||||
const boostmovehub_1 = require("./boostmovehub");
|
const boostmovehub_1 = require("./boostmovehub");
|
||||||
const duplotrainbase_1 = require("./duplotrainbase");
|
const duplotrainbase_1 = require("./duplotrainbase");
|
||||||
const puphub_1 = require("./puphub");
|
const puphub_1 = require("./puphub");
|
||||||
@ -192,21 +193,22 @@ class PoweredUP extends events_1.EventEmitter {
|
|||||||
return Object.keys(this._connectedHubs).map((uuid) => this._connectedHubs[uuid]).filter((hub) => hub.name === name);
|
return Object.keys(this._connectedHubs).map((uuid) => this._connectedHubs[uuid]).filter((hub) => hub.name === name);
|
||||||
}
|
}
|
||||||
async _discoveryEventHandler(peripheral) {
|
async _discoveryEventHandler(peripheral) {
|
||||||
|
const device = new bledevice_1.BLEDevice(peripheral);
|
||||||
let hub;
|
let hub;
|
||||||
if (await wedo2smarthub_1.WeDo2SmartHub.IsWeDo2SmartHub(peripheral)) {
|
if (await wedo2smarthub_1.WeDo2SmartHub.IsWeDo2SmartHub(peripheral)) {
|
||||||
hub = new wedo2smarthub_1.WeDo2SmartHub(peripheral, this.autoSubscribe);
|
hub = new wedo2smarthub_1.WeDo2SmartHub(device, this.autoSubscribe);
|
||||||
}
|
}
|
||||||
else if (await boostmovehub_1.BoostMoveHub.IsBoostMoveHub(peripheral)) {
|
else if (await boostmovehub_1.BoostMoveHub.IsBoostMoveHub(peripheral)) {
|
||||||
hub = new boostmovehub_1.BoostMoveHub(peripheral, this.autoSubscribe);
|
hub = new boostmovehub_1.BoostMoveHub(device, this.autoSubscribe);
|
||||||
}
|
}
|
||||||
else if (await puphub_1.PUPHub.IsPUPHub(peripheral)) {
|
else if (await puphub_1.PUPHub.IsPUPHub(peripheral)) {
|
||||||
hub = new puphub_1.PUPHub(peripheral, this.autoSubscribe);
|
hub = new puphub_1.PUPHub(device, this.autoSubscribe);
|
||||||
}
|
}
|
||||||
else if (await pupremote_1.PUPRemote.IsPUPRemote(peripheral)) {
|
else if (await pupremote_1.PUPRemote.IsPUPRemote(peripheral)) {
|
||||||
hub = new pupremote_1.PUPRemote(peripheral, this.autoSubscribe);
|
hub = new pupremote_1.PUPRemote(device, this.autoSubscribe);
|
||||||
}
|
}
|
||||||
else if (await duplotrainbase_1.DuploTrainBase.IsDuploTrainBase(peripheral)) {
|
else if (await duplotrainbase_1.DuploTrainBase.IsDuploTrainBase(peripheral)) {
|
||||||
hub = new duplotrainbase_1.DuploTrainBase(peripheral, this.autoSubscribe);
|
hub = new duplotrainbase_1.DuploTrainBase(device, this.autoSubscribe);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@ -216,7 +218,7 @@ class PoweredUP extends events_1.EventEmitter {
|
|||||||
// if (!isBrowserContext) {
|
// if (!isBrowserContext) {
|
||||||
// startScanning();
|
// startScanning();
|
||||||
// }
|
// }
|
||||||
hub.on("discoverComplete", () => {
|
device.on("discoverComplete", () => {
|
||||||
hub.on("connect", () => {
|
hub.on("connect", () => {
|
||||||
debug(`Hub ${hub.uuid} connected`);
|
debug(`Hub ${hub.uuid} connected`);
|
||||||
this._connectedHubs[hub.uuid] = hub;
|
this._connectedHubs[hub.uuid] = hub;
|
||||||
@ -284,7 +286,7 @@ exports.PoweredUP = PoweredUP;
|
|||||||
<span class="jsdoc-message">
|
<span class="jsdoc-message">
|
||||||
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
|
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
|
||||||
|
|
||||||
on Mon Feb 4th 2019
|
on Wed Feb 13th 2019
|
||||||
|
|
||||||
using the <a href="https://github.com/docstrap/docstrap">DocStrap template</a>.
|
using the <a href="https://github.com/docstrap/docstrap">DocStrap template</a>.
|
||||||
</span>
|
</span>
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -60,8 +60,6 @@ const trains = [
|
|||||||
poweredUP.on("discover", async (hub) => {
|
poweredUP.on("discover", async (hub) => {
|
||||||
|
|
||||||
if (hub instanceof PoweredUP.PUPRemote) {
|
if (hub instanceof PoweredUP.PUPRemote) {
|
||||||
await hub.connect();
|
|
||||||
hub._currentTrain = 2;
|
|
||||||
hub.on("button", (button, state) => {
|
hub.on("button", (button, state) => {
|
||||||
|
|
||||||
if (button === "GREEN") {
|
if (button === "GREEN") {
|
||||||
@ -71,7 +69,14 @@ poweredUP.on("discover", async (hub) => {
|
|||||||
hub._currentTrain = 0;
|
hub._currentTrain = 0;
|
||||||
}
|
}
|
||||||
hub.setLEDColor(trains[hub._currentTrain].color);
|
hub.setLEDColor(trains[hub._currentTrain].color);
|
||||||
console.log(`Switched active train on remote ${hub.name} to ${trains[hub._currentTrain].name}`);
|
const batteryLevels = [];
|
||||||
|
const train = trains[hub._currentTrain];
|
||||||
|
for (let trainHub of train.hubs) {
|
||||||
|
if (trainHub._hub) {
|
||||||
|
batteryLevels.push(`${trainHub.name}: ${trainHub._hub.batteryLevel}%`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Switched active train on remote ${hub.name} to ${trains[hub._currentTrain].name} (${batteryLevels.join(", ")})`);
|
||||||
}
|
}
|
||||||
} else if ((button === "LEFT" || button === "RIGHT") && state !== PoweredUP.Consts.ButtonState.RELEASED) {
|
} else if ((button === "LEFT" || button === "RIGHT") && state !== PoweredUP.Consts.ButtonState.RELEASED) {
|
||||||
trains[hub._currentTrain]._speed = trains[hub._currentTrain]._speed || 0;
|
trains[hub._currentTrain]._speed = trains[hub._currentTrain]._speed || 0;
|
||||||
@ -102,6 +107,8 @@ poweredUP.on("discover", async (hub) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
await hub.connect();
|
||||||
|
hub._currentTrain = 2;
|
||||||
hub.setLEDColor(trains[hub._currentTrain].color);
|
hub.setLEDColor(trains[hub._currentTrain].color);
|
||||||
console.log(`Connected to Powered UP remote (${hub.name})`);
|
console.log(`Connected to Powered UP remote (${hub.name})`);
|
||||||
return;
|
return;
|
||||||
@ -112,12 +119,8 @@ poweredUP.on("discover", async (hub) => {
|
|||||||
for (let trainHub in train.hubs) {
|
for (let trainHub in train.hubs) {
|
||||||
trainHub = train.hubs[trainHub];
|
trainHub = train.hubs[trainHub];
|
||||||
if (hub.name === trainHub.name) {
|
if (hub.name === trainHub.name) {
|
||||||
await hub.connect();
|
|
||||||
trainHub._hub = hub;
|
|
||||||
hub.setLEDColor(train.color);
|
|
||||||
console.log(`Connected to ${train.name} (${hub.name})`);
|
|
||||||
hub.on("attach", (port, type) => {
|
hub.on("attach", (port, type) => {
|
||||||
if (type === PoweredUP.Consts.DeviceType.LED_LIGHTS && trainHub.lights && trainHub.lights.indexOf(port) >= 0) {
|
if (trainHub.lights && trainHub.lights.indexOf(port) >= 0) {
|
||||||
hub.setLightBrightness(port, 100);
|
hub.setLightBrightness(port, 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -125,6 +128,10 @@ poweredUP.on("discover", async (hub) => {
|
|||||||
console.log(`Disconnected from ${train.name} (${hub.name})`);
|
console.log(`Disconnected from ${train.name} (${hub.name})`);
|
||||||
delete trainHub._hub;
|
delete trainHub._hub;
|
||||||
})
|
})
|
||||||
|
await hub.connect();
|
||||||
|
trainHub._hub = hub;
|
||||||
|
hub.setLEDColor(train.color);
|
||||||
|
console.log(`Connected to ${train.name} (${hub.name})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
97
examples/vernie_ds4_remote.html
Normal file
97
examples/vernie_ds4_remote.html
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Vernie / PlayStation DualShock 4 Remote Control</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/node-poweredup@latest/dist/browser/poweredup.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="./web_bluetooth.css" />
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const poweredUP = new PoweredUP.PoweredUP();
|
||||||
|
|
||||||
|
let vernie = null;
|
||||||
|
let controller = null;
|
||||||
|
|
||||||
|
poweredUP.on("discover", async (hub) => { // Wait to discover Vernie
|
||||||
|
|
||||||
|
if (hub instanceof PoweredUP.BoostMoveHub) {
|
||||||
|
vernie = hub;
|
||||||
|
await vernie.connect();
|
||||||
|
vernie.setLEDColor(PoweredUP.Consts.Color.BLUE);
|
||||||
|
document.getElementById("color").style.backgroundColor = PoweredUP.Consts.ColorNames[PoweredUP.Consts.Color.BLUE];
|
||||||
|
console.log(`Connected to Vernie (${vernie.name})!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vernie && controller) {
|
||||||
|
console.log("You're now ready to go!");
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const scan = function () {
|
||||||
|
if (PoweredUP.isWebBluetooth) {
|
||||||
|
poweredUP.scan(); // Start scanning for hubs
|
||||||
|
} else {
|
||||||
|
alert("Your browser does not support the Web Bluetooth specification.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("gamepadconnected", (event) => {
|
||||||
|
|
||||||
|
controller = event.gamepad;
|
||||||
|
if (!(controller.id.indexOf("54c") >= 0 && controller.id.indexOf("5c4") >= 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Connected to PlayStation DualShock 4!");
|
||||||
|
|
||||||
|
let start = null;
|
||||||
|
let previousLeft = 0;
|
||||||
|
let previousRight = 0;
|
||||||
|
|
||||||
|
const inputLoop = async () => {
|
||||||
|
controller = navigator.getGamepads()[0];
|
||||||
|
if (vernie) {
|
||||||
|
|
||||||
|
if (controller.buttons[14].pressed) { // Move the head left when left is pressed
|
||||||
|
await vernie.setMotorAngle("D", 35, -20);
|
||||||
|
} else if (controller.buttons[15].pressed) { // Move the head right when right is pressed
|
||||||
|
await vernie.setMotorAngle("D", 35, 20);
|
||||||
|
} else if (controller.buttons[17].pressed) { // Fire when the touchpad is pressed down
|
||||||
|
await vernie.setMotorAngle("D", 80, 20);
|
||||||
|
await vernie.setMotorAngle("D", 80, -20);
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = Math.floor(50 * (controller.axes[1] * -1));
|
||||||
|
const right = Math.floor(50 * (controller.axes[3] * -1));
|
||||||
|
if (left !== previousLeft || right !== previousRight) {
|
||||||
|
vernie.setMotorSpeed("AB", [left, right]); // Move tracks based on analog stick input
|
||||||
|
previousLeft = left;
|
||||||
|
previousRight = right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = requestAnimationFrame(inputLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputLoop();
|
||||||
|
|
||||||
|
if (vernie && controller) {
|
||||||
|
console.log("You're now ready to go!");
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div><h1>Vernie / PlayStation DualShock 4 Remote Control</h1></div>
|
||||||
|
<div>
|
||||||
|
<a class="button" href="#" onclick="scan();">Scan for Vernie</a>
|
||||||
|
</div>
|
||||||
|
<div id="current_color">
|
||||||
|
<span>Current Color: </span><div id="color"> </div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
53
examples/web_bluetooth.css
Normal file
53
examples/web_bluetooth.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
body {
|
||||||
|
font-family: Verdana;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#current_color {
|
||||||
|
position: absolute;
|
||||||
|
top: 61px;
|
||||||
|
left: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#color {
|
||||||
|
border: 1px solid #666666;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: 92px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
width: 150px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.disconnect_btn {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.disconnect_btn a {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead.headings td {
|
||||||
|
background-color: #666666;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr td {
|
||||||
|
background-color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button {
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #666666;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
78
examples/web_bluetooth.html
Normal file
78
examples/web_bluetooth.html
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Web Bluetooth node-poweredup Example</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/node-poweredup@latest/dist/browser/poweredup.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="./web_bluetooth.css" />
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const poweredUP = new PoweredUP.PoweredUP();
|
||||||
|
|
||||||
|
poweredUP.on("discover", async (hub) => { // Wait to discover hubs
|
||||||
|
|
||||||
|
await hub.connect(); // Connect to hub
|
||||||
|
console.log(`Connected to ${hub.name}!`);
|
||||||
|
|
||||||
|
hub.on("disconnect", () => {
|
||||||
|
console.log("Hub disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
renderHub(hub);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
let color = 1;
|
||||||
|
setInterval(() => {
|
||||||
|
|
||||||
|
const hubs = poweredUP.getConnectedHubs(); // Get an array of all connected hubs
|
||||||
|
document.getElementById("color").style.backgroundColor = PoweredUP.Consts.ColorNames[color];
|
||||||
|
hubs.forEach((hub) => {
|
||||||
|
hub.setLEDColor(color); // Set the color
|
||||||
|
})
|
||||||
|
color++;
|
||||||
|
if (color > 10) {
|
||||||
|
color = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
const renderHub = function (hub) {
|
||||||
|
const element = document.createElement("tr");
|
||||||
|
element.setAttribute("id", `hub-${encodeURIComponent(hub.uuid)}`);
|
||||||
|
element.innerHTML = `<td>${hub.name}</td><td>${PoweredUP.Consts.HubTypeNames[hub.type]}</td><td class="disconnect_btn"><a href="#" onclick="disconnect('${encodeURIComponent(hub.uuid)}');">Disconnect</a></td>`;
|
||||||
|
document.getElementById("hubs").appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scan = function () {
|
||||||
|
if (PoweredUP.isWebBluetooth) {
|
||||||
|
poweredUP.scan(); // Start scanning for hubs
|
||||||
|
} else {
|
||||||
|
alert("Your browser does not support the Web Bluetooth specification.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnect = function (uuid) {
|
||||||
|
poweredUP.getConnectedHubByUUID(decodeURIComponent(uuid)).disconnect();
|
||||||
|
document.getElementById(`hub-${uuid}`).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div><h1>Web Bluetooth node-poweredup Example</h1></div>
|
||||||
|
<div>
|
||||||
|
<a class="button" href="#" onclick="scan();">Add new Hub</a>
|
||||||
|
</div>
|
||||||
|
<div id="current_color">
|
||||||
|
<span>Current Color: </span><div id="color"> </div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<table id="hubs">
|
||||||
|
<thead class="headings"><td>Name</td><td>Type</td></thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -10,7 +10,7 @@
|
|||||||
"dateFormat": "ddd MMM Do YYYY",
|
"dateFormat": "ddd MMM Do YYYY",
|
||||||
"outputSourceFiles": true,
|
"outputSourceFiles": true,
|
||||||
"outputSourcePath": true,
|
"outputSourcePath": true,
|
||||||
"systemName": "DocStrap",
|
"systemName": "node-poweredup",
|
||||||
"footer": "",
|
"footer": "",
|
||||||
"copyright": "node-poweredup by Nathan Kellenicki licensed under the MIT license.",
|
"copyright": "node-poweredup by Nathan Kellenicki licensed under the MIT license.",
|
||||||
"navType": "vertical",
|
"navType": "vertical",
|
||||||
@ -25,4 +25,4 @@
|
|||||||
"parser": "gfm",
|
"parser": "gfm",
|
||||||
"hardwrap": true
|
"hardwrap": true
|
||||||
}
|
}
|
||||||
}
|
}
|
4632
package-lock.json
generated
4632
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@ -1,32 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "node-poweredup",
|
"name": "node-poweredup",
|
||||||
"version": "1.9.1",
|
"version": "3.5.1",
|
||||||
"description": "A Node.js module to interface with LEGO Powered UP components.",
|
"description": "A Javascript module to interface with LEGO Powered Up components.",
|
||||||
"homepage": "https://github.com/nathankellenicki/node-poweredup/",
|
"homepage": "https://github.com/nathankellenicki/node-poweredup/",
|
||||||
"main": "dist/index.js",
|
"main": "dist/node/index-node.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/node/index-node.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tslint -c tslint.json \"src/*.ts\" && tsc",
|
"build:node": "tslint -c tslint.json \"./src/*.ts\" && tsc",
|
||||||
"docs": "jsdoc -d docs -c jsdoc.conf.json -t ./node_modules/ink-docstrap/template -R README.md dist/consts.js dist/poweredup.js dist/lpf2hub.js dist/wedo2smarthub.js dist/boostmovehub.js dist/puphub.js dist/pupremote.js dist/duplotrainbase.js dist/hub.js dist/consts.js",
|
"build:browser": "tslint -c tslint.json \"./src/*.ts\" && webpack --mode=production",
|
||||||
"all": "npm run build && npm run docs",
|
"build:all": "tslint -c tslint.json \"./src/*.ts\" && tsc && webpack --mode=production",
|
||||||
"prepublishOnly": "npm run build"
|
"docs": "jsdoc -d docs -c jsdoc.conf.json -t ./node_modules/ink-docstrap/template -R README.md dist/node/consts.js dist/node/poweredup-node.js dist/node/lpf2hub.js dist/node/wedo2smarthub.js dist/node/boostmovehub.js dist/node/puphub.js dist/node/pupremote.js dist/node/duplotrainbase.js dist/node/controlplushub.js dist/node/hub.js dist/node/consts.js",
|
||||||
|
"all": "npm run build:all && npm run docs",
|
||||||
|
"prepublishOnly": "npm run build:node"
|
||||||
},
|
},
|
||||||
"author": "Nathan Kellenicki <nathan@kellenicki.com>",
|
"author": "Nathan Kellenicki <nathan@kellenicki.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"compare-versions": "^3.5.0",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"noble": "1.9.1",
|
"noble": "1.9.1",
|
||||||
"noble-mac": "https://github.com/Timeular/noble-mac.git#af4418e"
|
"noble-mac": "git+https://github.com/Timeular/noble-mac.git#af4418e"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debug": "0.0.31",
|
"@types/debug": "4.1.4",
|
||||||
"@types/noble": "0.0.38",
|
"@types/noble": "0.0.39",
|
||||||
"@types/node": "^10.12.18",
|
"@types/node": "^11.13.7",
|
||||||
|
"@types/web-bluetooth": "0.0.4",
|
||||||
"ink-docstrap": "^1.3.2",
|
"ink-docstrap": "^1.3.2",
|
||||||
"jsdoc": "^3.5.5",
|
"jsdoc": "^3.6.3",
|
||||||
"jsdoc-to-markdown": "^4.0.1",
|
"jsdoc-to-markdown": "^5.0.0",
|
||||||
"tslint": "^5.12.0",
|
"ts-loader": "^5.4.3",
|
||||||
"typescript": "^3.2.2"
|
"tslint": "^5.16.0",
|
||||||
|
"typescript": "^3.4.5",
|
||||||
|
"webpack": "^4.30.0",
|
||||||
|
"webpack-cli": "^3.3.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"xpc-connection": "sandeepmistry/node-xpc-connection#pull/26/head"
|
"xpc-connection": "sandeepmistry/node-xpc-connection#pull/26/head"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import compareVersion from "compare-versions";
|
||||||
import { Peripheral } from "noble";
|
import { Peripheral } from "noble";
|
||||||
|
|
||||||
import { LPF2Hub } from "./lpf2hub";
|
import { LPF2Hub } from "./lpf2hub";
|
||||||
@ -6,6 +7,7 @@ import { Port } from "./port";
|
|||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
import Debug = require("debug");
|
import Debug = require("debug");
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
const debug = Debug("boostmovehub");
|
const debug = Debug("boostmovehub");
|
||||||
|
|
||||||
|
|
||||||
@ -18,30 +20,26 @@ const debug = Debug("boostmovehub");
|
|||||||
export class BoostMoveHub extends LPF2Hub {
|
export class BoostMoveHub extends LPF2Hub {
|
||||||
|
|
||||||
|
|
||||||
// We set JSDoc to ignore these events as a Boost Move Hub will never emit them.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event BoostMoveHub#speed
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
public static IsBoostMoveHub (peripheral: Peripheral) {
|
public static IsBoostMoveHub (peripheral: Peripheral) {
|
||||||
return (peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.BOOST_MOVE_HUB_ID);
|
return (peripheral.advertisement &&
|
||||||
|
peripheral.advertisement.serviceUuids &&
|
||||||
|
peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.BOOST_MOVE_HUB_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
|
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
|
||||||
super(peripheral, autoSubscribe);
|
super(device, autoSubscribe);
|
||||||
this.type = Consts.HubType.BOOST_MOVE_HUB;
|
this.type = Consts.HubType.BOOST_MOVE_HUB;
|
||||||
this._ports = {
|
this._ports = {
|
||||||
"A": new Port("A", 55),
|
"A": new Port("A", 0),
|
||||||
"B": new Port("B", 56),
|
"B": new Port("B", 1),
|
||||||
"AB": new Port("AB", 57),
|
"C": new Port("C", 2),
|
||||||
"TILT": new Port("TILT", 58),
|
"D": new Port("D", 3),
|
||||||
"C": new Port("C", 1),
|
"TILT": new Port("TILT", 58)
|
||||||
"D": new Port("D", 2)
|
|
||||||
};
|
};
|
||||||
|
this.on("attach", (port, type) => {
|
||||||
|
this._combinePorts(port, type);
|
||||||
|
});
|
||||||
debug("Discovered Boost Move Hub");
|
debug("Discovered Boost Move Hub");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +64,7 @@ export class BoostMoveHub extends LPF2Hub {
|
|||||||
*/
|
*/
|
||||||
public setMotorSpeed (port: string, speed: number | [number, number], time?: number | boolean) {
|
public setMotorSpeed (port: string, speed: number | [number, number], time?: number | boolean) {
|
||||||
const portObj = this._portLookup(port);
|
const portObj = this._portLookup(port);
|
||||||
if (portObj.id !== "AB" && speed instanceof Array) {
|
if (!this._virtualPorts[portObj.id] && speed instanceof Array) {
|
||||||
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
||||||
}
|
}
|
||||||
let cancelEventTimer = true;
|
let cancelEventTimer = true;
|
||||||
@ -82,10 +80,15 @@ export class BoostMoveHub extends LPF2Hub {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (time && typeof time === "number") {
|
if (time && typeof time === "number") {
|
||||||
|
|
||||||
if (portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR || portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR) {
|
if (
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
) {
|
||||||
portObj.busy = true;
|
portObj.busy = true;
|
||||||
let data = null;
|
let data = null;
|
||||||
if (portObj.id === "AB") {
|
if (this._virtualPorts[portObj.id]) {
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x0a, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0a, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore: The type of speed is properly checked at the start
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
@ -111,10 +114,15 @@ export class BoostMoveHub extends LPF2Hub {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR || portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR) {
|
if (
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
) {
|
||||||
portObj.busy = true;
|
portObj.busy = true;
|
||||||
let data = null;
|
let data = null;
|
||||||
if (portObj.id === "AB") {
|
if (this._virtualPorts[portObj.id]) {
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore: The type of speed is properly checked at the start
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
@ -167,17 +175,22 @@ export class BoostMoveHub extends LPF2Hub {
|
|||||||
*/
|
*/
|
||||||
public setMotorAngle (port: string, angle: number, speed: number | [number, number] = 100) {
|
public setMotorAngle (port: string, angle: number, speed: number | [number, number] = 100) {
|
||||||
const portObj = this._portLookup(port);
|
const portObj = this._portLookup(port);
|
||||||
if (!(portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR || portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR)) {
|
if (!(
|
||||||
throw new Error("Angle rotation is only available when using a Boost Tacho Motor or Boost Move Hub Motor");
|
portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Angle rotation is only available when using a Boost Tacho Motor, Boost Move Hub Motor, Control+ Medium Motor, or Control+ Large Motor");
|
||||||
}
|
}
|
||||||
if (portObj.id !== "AB" && speed instanceof Array) {
|
if (!this._virtualPorts[portObj.id] && speed instanceof Array) {
|
||||||
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
||||||
}
|
}
|
||||||
portObj.cancelEventTimer();
|
portObj.cancelEventTimer();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
portObj.busy = true;
|
portObj.busy = true;
|
||||||
let data = null;
|
let data = null;
|
||||||
if (portObj.id === "AB") {
|
if (this._virtualPorts[portObj.id]) {
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x0c, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0c, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore: The type of speed is properly checked at the start
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
@ -192,6 +205,65 @@ export class BoostMoveHub extends LPF2Hub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell motor to goto an absolute position
|
||||||
|
* @method BoostMoveHub#setAbsolutePosition
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} pos The position of the motor to go to
|
||||||
|
* @param {number | Array.<number>} [speed=100] A value between 1 - 100 should be set (Direction does not apply when going to absolute position)
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public setAbsolutePosition (port: string, pos: number, speed: number = 100) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Absolute positioning is only available when using a Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
portObj.busy = true;
|
||||||
|
let data = null;
|
||||||
|
if (this._virtualPorts[portObj.id]) {
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
data.writeInt32LE(pos, 4);
|
||||||
|
data.writeInt32LE(pos, 8);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0d, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
data.writeInt32LE(pos, 4);
|
||||||
|
}
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the current motor position as absolute position zero
|
||||||
|
* @method BoostMoveHub#resetAbsolutePosition
|
||||||
|
* @param {string} port
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public resetAbsolutePosition (port: string) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Absolute positioning is only available when using a Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x02, 0x00, 0x00, 0x00, 0x00]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fully (hard) stop the motor on a given port.
|
* Fully (hard) stop the motor on a given port.
|
||||||
* @method BoostMoveHub#brakeMotor
|
* @method BoostMoveHub#brakeMotor
|
||||||
@ -231,4 +303,11 @@ export class BoostMoveHub extends LPF2Hub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected _checkFirmware (version: string) {
|
||||||
|
if (compareVersion("2.0.00.0023", version) === 1) {
|
||||||
|
throw new Error(`Your Boost Move Hub's (${this.name}) firmware is out of date and unsupported by this library. Please update it via the official Powered Up app.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
* @property {number} POWERED_UP_HUB 3
|
* @property {number} POWERED_UP_HUB 3
|
||||||
* @property {number} POWERED_UP_REMOTE 4
|
* @property {number} POWERED_UP_REMOTE 4
|
||||||
* @property {number} DUPLO_TRAIN_HUB 5
|
* @property {number} DUPLO_TRAIN_HUB 5
|
||||||
|
* @property {number} CONTROL_PLUS_HUB 6
|
||||||
*/
|
*/
|
||||||
export enum HubType {
|
export enum HubType {
|
||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
@ -13,10 +14,19 @@ export enum HubType {
|
|||||||
BOOST_MOVE_HUB = 2,
|
BOOST_MOVE_HUB = 2,
|
||||||
POWERED_UP_HUB = 3,
|
POWERED_UP_HUB = 3,
|
||||||
POWERED_UP_REMOTE = 4,
|
POWERED_UP_REMOTE = 4,
|
||||||
DUPLO_TRAIN_HUB = 5
|
DUPLO_TRAIN_HUB = 5,
|
||||||
|
CONTROL_PLUS_HUB = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
|
export let HubTypeNames = Object.keys(HubType).reduce((result: {[hubType: string]: string}, item) => {
|
||||||
|
// @ts-ignore
|
||||||
|
result[HubType[item]] = item;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef DeviceType
|
* @typedef DeviceType
|
||||||
* @property {number} UNKNOWN 0
|
* @property {number} UNKNOWN 0
|
||||||
@ -34,6 +44,8 @@ export enum HubType {
|
|||||||
* @property {number} DUPLO_TRAIN_BASE_SPEAKER 42
|
* @property {number} DUPLO_TRAIN_BASE_SPEAKER 42
|
||||||
* @property {number} DUPLO_TRAIN_BASE_COLOR 43
|
* @property {number} DUPLO_TRAIN_BASE_COLOR 43
|
||||||
* @property {number} DUPLO_TRAIN_BASE_SPEEDOMETER 44
|
* @property {number} DUPLO_TRAIN_BASE_SPEEDOMETER 44
|
||||||
|
* @property {number} CONTROL_PLUS_LARGE_MOTOR 46
|
||||||
|
* @property {number} CONTROL_PLUS_XLARGE_MOTOR 47
|
||||||
* @property {number} POWERED_UP_REMOTE_BUTTON 55
|
* @property {number} POWERED_UP_REMOTE_BUTTON 55
|
||||||
*/
|
*/
|
||||||
export enum DeviceType {
|
export enum DeviceType {
|
||||||
@ -52,10 +64,20 @@ export enum DeviceType {
|
|||||||
DUPLO_TRAIN_BASE_SPEAKER = 42,
|
DUPLO_TRAIN_BASE_SPEAKER = 42,
|
||||||
DUPLO_TRAIN_BASE_COLOR = 43,
|
DUPLO_TRAIN_BASE_COLOR = 43,
|
||||||
DUPLO_TRAIN_BASE_SPEEDOMETER = 44,
|
DUPLO_TRAIN_BASE_SPEEDOMETER = 44,
|
||||||
|
CONTROL_PLUS_LARGE_MOTOR = 46,
|
||||||
|
CONTROL_PLUS_XLARGE_MOTOR = 47,
|
||||||
POWERED_UP_REMOTE_BUTTON = 55
|
POWERED_UP_REMOTE_BUTTON = 55
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
|
export let DeviceTypeNames = Object.keys(DeviceType).reduce((result: {[deviceType: string]: string}, item) => {
|
||||||
|
// @ts-ignore
|
||||||
|
result[DeviceType[item]] = item;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef Color
|
* @typedef Color
|
||||||
* @property {number} BLACK 0
|
* @property {number} BLACK 0
|
||||||
@ -87,6 +109,14 @@ export enum Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
|
export let ColorNames = Object.keys(Color).reduce((result: {[color: string]: string}, item) => {
|
||||||
|
// @ts-ignore
|
||||||
|
result[Color[item]] = item;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef ButtonState
|
* @typedef ButtonState
|
||||||
* @property {number} PRESSED 0
|
* @property {number} PRESSED 0
|
||||||
@ -122,15 +152,20 @@ export enum DuploTrainBaseSound {
|
|||||||
|
|
||||||
|
|
||||||
export enum BLEManufacturerData {
|
export enum BLEManufacturerData {
|
||||||
|
DUPLO_TRAIN_HUB_ID = 32,
|
||||||
BOOST_MOVE_HUB_ID = 64,
|
BOOST_MOVE_HUB_ID = 64,
|
||||||
POWERED_UP_HUB_ID = 65,
|
POWERED_UP_HUB_ID = 65,
|
||||||
POWERED_UP_REMOTE_ID = 66,
|
POWERED_UP_REMOTE_ID = 66,
|
||||||
DUPLO_TRAIN_HUB_ID = 32
|
CONTROL_PLUS_LARGE_HUB = 128
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export enum BLEService {
|
export enum BLEService {
|
||||||
WEDO2_SMART_HUB = "00001523-1212-efde-1523-785feabcd123",
|
WEDO2_SMART_HUB = "00001523-1212-efde-1523-785feabcd123",
|
||||||
|
WEDO2_SMART_HUB_2 = "00004f0e-1212-efde-1523-785feabcd123",
|
||||||
|
WEDO2_SMART_HUB_3 = "2a19",
|
||||||
|
WEDO2_SMART_HUB_4 = "180f",
|
||||||
|
WEDO2_SMART_HUB_5 = "180a",
|
||||||
LPF2_HUB = "00001623-1212-efde-1623-785feabcd123"
|
LPF2_HUB = "00001623-1212-efde-1623-785feabcd123"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
304
src/controlplushub.ts
Normal file
304
src/controlplushub.ts
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import compareVersion from "compare-versions";
|
||||||
|
import { Peripheral } from "noble";
|
||||||
|
|
||||||
|
import { LPF2Hub } from "./lpf2hub";
|
||||||
|
import { Port } from "./port";
|
||||||
|
|
||||||
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
|
import Debug = require("debug");
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
|
const debug = Debug("ControlPlusHub");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ControlPlusHub is emitted if the discovered device is a Control+ Hub.
|
||||||
|
* @class ControlPlusHub
|
||||||
|
* @extends LPF2Hub
|
||||||
|
* @extends Hub
|
||||||
|
*/
|
||||||
|
export class ControlPlusHub extends LPF2Hub {
|
||||||
|
|
||||||
|
|
||||||
|
public static IsControlPlusHub (peripheral: Peripheral) {
|
||||||
|
return (peripheral.advertisement &&
|
||||||
|
peripheral.advertisement.serviceUuids &&
|
||||||
|
peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.CONTROL_PLUS_LARGE_HUB);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
|
||||||
|
super(device, autoSubscribe);
|
||||||
|
this.type = Consts.HubType.CONTROL_PLUS_HUB;
|
||||||
|
this._ports = {
|
||||||
|
"A": new Port("A", 0),
|
||||||
|
"B": new Port("B", 1),
|
||||||
|
"C": new Port("C", 2),
|
||||||
|
"D": new Port("D", 3),
|
||||||
|
// "TILT": new Port("TILT", 60)
|
||||||
|
};
|
||||||
|
this.on("attach", (port, type) => {
|
||||||
|
this._combinePorts(port, type);
|
||||||
|
});
|
||||||
|
debug("Discovered Control+ Hub");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public connect () {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
debug("Connecting to Control+ Hub");
|
||||||
|
await super.connect();
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x62, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01])); // Accelerometer
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x63, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])); // Gyro/Tilt
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x3d, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01])); // Temperature
|
||||||
|
debug("Connect completed");
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the motor speed on a given port.
|
||||||
|
* @method ControlPlusHub#setMotorSpeed
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number | Array.<number>} speed For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0. If you are specifying port AB to control both motors, you can optionally supply a tuple of speeds.
|
||||||
|
* @param {number} [time] How long to activate the motor for (in milliseconds). Leave empty to turn the motor on indefinitely.
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command. If time is specified, this is once the motor is finished.
|
||||||
|
*/
|
||||||
|
public setMotorSpeed (port: string, speed: number | [number, number], time?: number | boolean) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!this._virtualPorts[portObj.id] && speed instanceof Array) {
|
||||||
|
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
||||||
|
}
|
||||||
|
let cancelEventTimer = true;
|
||||||
|
if (typeof time === "boolean") {
|
||||||
|
if (time === true) {
|
||||||
|
cancelEventTimer = false;
|
||||||
|
}
|
||||||
|
time = undefined;
|
||||||
|
}
|
||||||
|
if (cancelEventTimer) {
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (time && typeof time === "number") {
|
||||||
|
|
||||||
|
if (
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
) {
|
||||||
|
portObj.busy = true;
|
||||||
|
let data = null;
|
||||||
|
if (this._virtualPorts[portObj.id]) {
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0a, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x09, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
}
|
||||||
|
data.writeUInt16LE(time > 65535 ? 65535 : time, 4);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, this._mapSpeed(speed)]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
const timeout = global.setTimeout(() => {
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, 0x00]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
return resolve();
|
||||||
|
// @ts-ignore: The type of time is properly checked at the start
|
||||||
|
}, time);
|
||||||
|
portObj.setEventTimer(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR || portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR) {
|
||||||
|
portObj.busy = true;
|
||||||
|
let data = null;
|
||||||
|
if (this._virtualPorts[portObj.id]) {
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x01, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
}
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, this._mapSpeed(speed)]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ramp the motor speed on a given port.
|
||||||
|
* @method ControlPlusHub#rampMotorSpeed
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} fromSpeed For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0.
|
||||||
|
* @param {number} toSpeed For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0.
|
||||||
|
* @param {number} time How long the ramp should last (in milliseconds).
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command.
|
||||||
|
*/
|
||||||
|
public rampMotorSpeed (port: string, fromSpeed: number, toSpeed: number, time: number) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._calculateRamp(fromSpeed, toSpeed, time, portObj)
|
||||||
|
.on("changeSpeed", (speed) => {
|
||||||
|
this.setMotorSpeed(port, speed, true);
|
||||||
|
})
|
||||||
|
.on("finished", resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate a motor by a given angle.
|
||||||
|
* @method ControlPlusHub#setMotorAngle
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} angle How much the motor should be rotated (in degrees).
|
||||||
|
* @param {number | Array.<number>} [speed=100] For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0. If you are specifying port AB to control both motors, you can optionally supply a tuple of speeds.
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public setMotorAngle (port: string, angle: number, speed: number | [number, number] = 100) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Angle rotation is only available when using a Boost Tacho Motor, Boost Move Hub Motor, Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
if (!this._virtualPorts[portObj.id] && speed instanceof Array) {
|
||||||
|
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
||||||
|
}
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
portObj.busy = true;
|
||||||
|
let data = null;
|
||||||
|
if (this._virtualPorts[portObj.id]) {
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0c, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0b, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
}
|
||||||
|
data.writeUInt32LE(angle, 4);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell motor to goto an absolute position
|
||||||
|
* @method ControlPlusHub#setAbsolutePosition
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} pos The position of the motor to go to
|
||||||
|
* @param {number | Array.<number>} [speed=100] A value between 1 - 100 should be set (Direction does not apply when going to absolute position)
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public setAbsolutePosition (port: string, pos: number, speed: number = 100) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Absolute positioning is only available when using a Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
portObj.busy = true;
|
||||||
|
let data = null;
|
||||||
|
if (this._virtualPorts[portObj.id]) {
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
data.writeInt32LE(pos, 4);
|
||||||
|
data.writeInt32LE(pos, 8);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0d, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
data.writeInt32LE(pos, 4);
|
||||||
|
}
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the current motor position as absolute position zero
|
||||||
|
* @method ControlPlusHub#resetAbsolutePosition
|
||||||
|
* @param {string} port
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public resetAbsolutePosition (port: string) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Absolute positioning is only available when using a Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x02, 0x00, 0x00, 0x00, 0x00]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully (hard) stop the motor on a given port.
|
||||||
|
* @method ControlPlusHub#brakeMotor
|
||||||
|
* @param {string} port
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command.
|
||||||
|
*/
|
||||||
|
public brakeMotor (port: string) {
|
||||||
|
return this.setMotorSpeed(port, 127);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the light brightness on a given port.
|
||||||
|
* @method ControlPlusHub#setLightBrightness
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} brightness Brightness value between 0-100 (0 is off)
|
||||||
|
* @param {number} [time] How long to turn the light on (in milliseconds). Leave empty to turn the light on indefinitely.
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command. If time is specified, this is once the light is turned off.
|
||||||
|
*/
|
||||||
|
public setLightBrightness (port: string, brightness: number, time?: number) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, brightness]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
if (time) {
|
||||||
|
const timeout = global.setTimeout(() => {
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, 0x00]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
return resolve();
|
||||||
|
}, time);
|
||||||
|
portObj.setEventTimer(timeout);
|
||||||
|
} else {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -6,6 +6,7 @@ import { Port } from "./port";
|
|||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
import Debug = require("debug");
|
import Debug = require("debug");
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
const debug = Debug("duplotrainbase");
|
const debug = Debug("duplotrainbase");
|
||||||
|
|
||||||
|
|
||||||
@ -18,51 +19,15 @@ const debug = Debug("duplotrainbase");
|
|||||||
export class DuploTrainBase extends LPF2Hub {
|
export class DuploTrainBase extends LPF2Hub {
|
||||||
|
|
||||||
|
|
||||||
// We set JSDoc to ignore these events as a Duplo Train Base will never emit them.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event DuploTrainBase#distance
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event DuploTrainBase#colorAndDistance
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event DuploTrainBase#tilt
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event DuploTrainBase#rotate
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event DuploTrainBase#button
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event DuploTrainBase#attach
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event DuploTrainBase#detach
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
public static IsDuploTrainBase (peripheral: Peripheral) {
|
public static IsDuploTrainBase (peripheral: Peripheral) {
|
||||||
return (peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.DUPLO_TRAIN_HUB_ID);
|
return (peripheral.advertisement &&
|
||||||
|
peripheral.advertisement.serviceUuids &&
|
||||||
|
peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.DUPLO_TRAIN_HUB_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
|
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
|
||||||
super(peripheral, autoSubscribe);
|
super(device, autoSubscribe);
|
||||||
this.type = Consts.HubType.DUPLO_TRAIN_HUB;
|
this.type = Consts.HubType.DUPLO_TRAIN_HUB;
|
||||||
this._ports = {
|
this._ports = {
|
||||||
"MOTOR": new Port("MOTOR", 0),
|
"MOTOR": new Port("MOTOR", 0),
|
||||||
@ -91,7 +56,7 @@ export class DuploTrainBase extends LPF2Hub {
|
|||||||
*/
|
*/
|
||||||
public setLEDColor (color: number | boolean) {
|
public setLEDColor (color: number | boolean) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (color === false) {
|
if (typeof color === "boolean") {
|
||||||
color = 0;
|
color = 0;
|
||||||
}
|
}
|
||||||
const data = Buffer.from([0x81, 0x11, 0x11, 0x51, 0x00, color]);
|
const data = Buffer.from([0x81, 0x11, 0x11, 0x51, 0x00, color]);
|
||||||
|
160
src/hub.ts
160
src/hub.ts
@ -1,6 +1,6 @@
|
|||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
import { Characteristic, Peripheral, Service } from "noble";
|
import { IBLEDevice, IFirmwareInfo } from "./interfaces";
|
||||||
import { Port } from "./port";
|
import { Port } from "./port";
|
||||||
|
|
||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
@ -9,14 +9,6 @@ import Debug = require("debug");
|
|||||||
const debug = Debug("hub");
|
const debug = Debug("hub");
|
||||||
|
|
||||||
|
|
||||||
export interface IFirmwareInfo {
|
|
||||||
major: number;
|
|
||||||
minor: number;
|
|
||||||
bugFix: number;
|
|
||||||
build: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class Hub
|
* @class Hub
|
||||||
* @extends EventEmitter
|
* @extends EventEmitter
|
||||||
@ -29,7 +21,7 @@ export class Hub extends EventEmitter {
|
|||||||
public type: Consts.HubType = Consts.HubType.UNKNOWN;
|
public type: Consts.HubType = Consts.HubType.UNKNOWN;
|
||||||
|
|
||||||
protected _ports: {[port: string]: Port} = {};
|
protected _ports: {[port: string]: Port} = {};
|
||||||
protected _characteristics: {[uuid: string]: Characteristic} = {};
|
protected _virtualPorts: {[port: string]: Port} = {};
|
||||||
|
|
||||||
protected _name: string = "";
|
protected _name: string = "";
|
||||||
protected _firmwareInfo: IFirmwareInfo = { major: 0, minor: 0, bugFix: 0, build: 0 };
|
protected _firmwareInfo: IFirmwareInfo = { major: 0, minor: 0, bugFix: 0, build: 0 };
|
||||||
@ -37,23 +29,19 @@ export class Hub extends EventEmitter {
|
|||||||
protected _voltage: number = 0;
|
protected _voltage: number = 0;
|
||||||
protected _current: number = 0;
|
protected _current: number = 0;
|
||||||
|
|
||||||
private _peripheral: Peripheral;
|
protected _bleDevice: IBLEDevice;
|
||||||
private _uuid: string;
|
|
||||||
private _rssi: number = -100;
|
private _rssi: number = -100;
|
||||||
|
|
||||||
private _isConnecting = false;
|
private _isConnecting = false;
|
||||||
private _isConnected = false;
|
private _isConnected = false;
|
||||||
|
|
||||||
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
|
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
|
||||||
super();
|
super();
|
||||||
this.autoSubscribe = !!autoSubscribe;
|
this.autoSubscribe = !!autoSubscribe;
|
||||||
this._peripheral = peripheral;
|
this._bleDevice = device;
|
||||||
this._uuid = peripheral.uuid;
|
device.on("disconnect", () => {
|
||||||
// NK: This hack allows LPF2.0 hubs to send a second advertisement packet consisting of the hub name before we try to read it
|
this.emit("disconnect");
|
||||||
setTimeout(() => {
|
});
|
||||||
this._name = peripheral.advertisement.localName;
|
|
||||||
this.emit("discoverComplete");
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -62,7 +50,7 @@ export class Hub extends EventEmitter {
|
|||||||
* @property {string} name Name of the hub
|
* @property {string} name Name of the hub
|
||||||
*/
|
*/
|
||||||
public get name () {
|
public get name () {
|
||||||
return this._name;
|
return this._bleDevice.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -80,7 +68,7 @@ export class Hub extends EventEmitter {
|
|||||||
* @property {string} uuid UUID of the hub
|
* @property {string} uuid UUID of the hub
|
||||||
*/
|
*/
|
||||||
public get uuid () {
|
public get uuid () {
|
||||||
return this._uuid;
|
return this._bleDevice.uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -111,13 +99,13 @@ export class Hub extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * @readonly
|
* @readonly
|
||||||
// * @property {number} current Current usage of the hub (Amps)
|
* @property {number} current Current usage of the hub (Milliamps)
|
||||||
// */
|
*/
|
||||||
// public get current () {
|
public get current () {
|
||||||
// return this._current;
|
return this._current;
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,71 +114,17 @@ export class Hub extends EventEmitter {
|
|||||||
* @returns {Promise} Resolved upon successful connect.
|
* @returns {Promise} Resolved upon successful connect.
|
||||||
*/
|
*/
|
||||||
public connect () {
|
public connect () {
|
||||||
return new Promise((connectResolve, connectReject) => {
|
return new Promise(async (connectResolve, connectReject) => {
|
||||||
|
if (this._bleDevice.connecting) {
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (this._isConnecting) {
|
|
||||||
return connectReject("Already connecting");
|
return connectReject("Already connecting");
|
||||||
} else if (this._isConnected) {
|
} else if (this._bleDevice.connected) {
|
||||||
return connectReject("Already connected");
|
return connectReject("Already connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isConnecting = true;
|
this._isConnecting = true;
|
||||||
this._peripheral.connect((err: string) => {
|
await this._bleDevice.connect();
|
||||||
|
|
||||||
this._rssi = this._peripheral.rssi;
|
|
||||||
const rssiUpdateInterval = setInterval(() => {
|
|
||||||
this._peripheral.updateRssi((err: string, rssi: number) => {
|
|
||||||
if (!err) {
|
|
||||||
if (this._rssi !== rssi) {
|
|
||||||
this._rssi = rssi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
self._peripheral.on("disconnect", () => {
|
|
||||||
clearInterval(rssiUpdateInterval);
|
|
||||||
this._isConnecting = false;
|
|
||||||
this._isConnected = false;
|
|
||||||
this.emit("disconnect");
|
|
||||||
});
|
|
||||||
|
|
||||||
self._peripheral.discoverServices([], (err: string, services: Service[]) => {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
this.emit("error", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug("Service/characteristic discovery started");
|
|
||||||
const servicePromises: Array<Promise<null>> = [];
|
|
||||||
services.forEach((service) => {
|
|
||||||
servicePromises.push(new Promise((resolve, reject) => {
|
|
||||||
service.discoverCharacteristics([], (err, characteristics) => {
|
|
||||||
characteristics.forEach((characteristic) => {
|
|
||||||
this._characteristics[characteristic.uuid] = characteristic;
|
|
||||||
});
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(servicePromises).then(() => {
|
|
||||||
debug("Service/characteristic discovery finished");
|
|
||||||
this._isConnecting = false;
|
|
||||||
this._isConnected = true;
|
|
||||||
this.emit("connect");
|
|
||||||
return connectResolve();
|
return connectResolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -199,12 +133,9 @@ export class Hub extends EventEmitter {
|
|||||||
* @method Hub#disconnect
|
* @method Hub#disconnect
|
||||||
* @returns {Promise} Resolved upon successful disconnect.
|
* @returns {Promise} Resolved upon successful disconnect.
|
||||||
*/
|
*/
|
||||||
public disconnect () {
|
public async disconnect () {
|
||||||
return new Promise((resolve, reject) => {
|
this.emit("disconnect");
|
||||||
this._peripheral.disconnect(() => {
|
this._bleDevice.disconnect();
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -292,21 +223,21 @@ export class Hub extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected _getCharacteristic (uuid: string) {
|
// protected _getCharacteristic (uuid: string) {
|
||||||
return this._characteristics[uuid.replace(/-/g, "")];
|
// return this._characteristics[uuid.replace(/-/g, "")];
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
protected _subscribeToCharacteristic (characteristic: Characteristic, callback: (data: Buffer) => void) {
|
// protected _subscribeToCharacteristic (characteristic: Characteristic, callback: (data: Buffer) => void) {
|
||||||
characteristic.on("data", (data: Buffer) => {
|
// characteristic.on("data", (data: Buffer) => {
|
||||||
return callback(data);
|
// return callback(data);
|
||||||
});
|
// });
|
||||||
characteristic.subscribe((err) => {
|
// characteristic.subscribe((err) => {
|
||||||
if (err) {
|
// if (err) {
|
||||||
this.emit("error", err);
|
// this.emit("error", err);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
protected _activatePortDevice (port: number, type: number, mode: number, format: number, callback?: () => void) {
|
protected _activatePortDevice (port: number, type: number, mode: number, format: number, callback?: () => void) {
|
||||||
@ -345,6 +276,9 @@ export class Hub extends EventEmitter {
|
|||||||
* @event Hub#detach
|
* @event Hub#detach
|
||||||
* @param {string} port
|
* @param {string} port
|
||||||
*/
|
*/
|
||||||
|
if (this._virtualPorts[port.id]) {
|
||||||
|
delete this._virtualPorts[port.id];
|
||||||
|
}
|
||||||
this.emit("detach", port.id);
|
this.emit("detach", port.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,6 +293,12 @@ export class Hub extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(this._virtualPorts)) {
|
||||||
|
if (this._virtualPorts[key].value === num) {
|
||||||
|
return this._virtualPorts[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -415,10 +355,10 @@ export class Hub extends EventEmitter {
|
|||||||
|
|
||||||
|
|
||||||
protected _portLookup (port: string) {
|
protected _portLookup (port: string) {
|
||||||
if (!this._ports[port.toUpperCase()]) {
|
if (!this._ports[port.toUpperCase()] && !this._virtualPorts[port.toUpperCase()]) {
|
||||||
throw new Error(`Port ${port.toUpperCase()} does not exist on this Hub type`);
|
throw new Error(`Port ${port.toUpperCase()} does not exist on this Hub type`);
|
||||||
}
|
}
|
||||||
return this._ports[port];
|
return this._ports[port] || this._virtualPorts[port];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -440,6 +380,10 @@ export class Hub extends EventEmitter {
|
|||||||
return 0x02;
|
return 0x02;
|
||||||
case Consts.DeviceType.BOOST_MOVE_HUB_MOTOR:
|
case Consts.DeviceType.BOOST_MOVE_HUB_MOTOR:
|
||||||
return 0x02;
|
return 0x02;
|
||||||
|
case Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR:
|
||||||
|
return 0x02;
|
||||||
|
case Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR:
|
||||||
|
return 0x02;
|
||||||
case Consts.DeviceType.BOOST_DISTANCE:
|
case Consts.DeviceType.BOOST_DISTANCE:
|
||||||
return (this.type === Consts.HubType.WEDO2_SMART_HUB ? 0x00 : 0x08);
|
return (this.type === Consts.HubType.WEDO2_SMART_HUB ? 0x00 : 0x08);
|
||||||
case Consts.DeviceType.BOOST_TILT:
|
case Consts.DeviceType.BOOST_TILT:
|
||||||
|
15
src/index-browser.ts
Normal file
15
src/index-browser.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
|
import { BoostMoveHub } from "./boostmovehub";
|
||||||
|
import { ControlPlusHub } from "./controlplushub";
|
||||||
|
import { DuploTrainBase } from "./duplotrainbase";
|
||||||
|
import { Hub } from "./hub";
|
||||||
|
import { PoweredUP } from "./poweredup-browser";
|
||||||
|
import { PUPHub } from "./puphub";
|
||||||
|
import { PUPRemote } from "./pupremote";
|
||||||
|
import { WeDo2SmartHub } from "./wedo2smarthub";
|
||||||
|
|
||||||
|
import { isWebBluetooth } from "./utils";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.PoweredUP = { PoweredUP, Hub, WeDo2SmartHub, BoostMoveHub, ControlPlusHub, PUPHub, PUPRemote, DuploTrainBase, Consts, isWebBluetooth };
|
@ -1,11 +1,15 @@
|
|||||||
import { BoostMoveHub } from "./boostmovehub";
|
|
||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
|
import { BoostMoveHub } from "./boostmovehub";
|
||||||
|
import { ControlPlusHub } from "./controlplushub";
|
||||||
import { DuploTrainBase } from "./duplotrainbase";
|
import { DuploTrainBase } from "./duplotrainbase";
|
||||||
import { Hub } from "./hub";
|
import { Hub } from "./hub";
|
||||||
import { PoweredUP } from "./poweredup";
|
import { PoweredUP } from "./poweredup-node";
|
||||||
import { PUPHub } from "./puphub";
|
import { PUPHub } from "./puphub";
|
||||||
import { PUPRemote } from "./pupremote";
|
import { PUPRemote } from "./pupremote";
|
||||||
import { WeDo2SmartHub } from "./wedo2smarthub";
|
import { WeDo2SmartHub } from "./wedo2smarthub";
|
||||||
|
|
||||||
|
import { isWebBluetooth } from "./utils";
|
||||||
|
|
||||||
export default PoweredUP;
|
export default PoweredUP;
|
||||||
export { PoweredUP, Hub, WeDo2SmartHub, BoostMoveHub, PUPHub, PUPRemote, DuploTrainBase, Consts };
|
export { PoweredUP, Hub, WeDo2SmartHub, BoostMoveHub, ControlPlusHub, PUPHub, PUPRemote, DuploTrainBase, Consts, isWebBluetooth };
|
23
src/interfaces.ts
Normal file
23
src/interfaces.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
export interface IFirmwareInfo {
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
bugFix: number;
|
||||||
|
build: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface IBLEDevice extends EventEmitter {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
connecting: boolean;
|
||||||
|
connected: boolean;
|
||||||
|
connect: () => Promise<any>;
|
||||||
|
disconnect: () => Promise<any>;
|
||||||
|
discoverCharacteristicsForService: (uuid: string) => Promise<any>;
|
||||||
|
subscribeToCharacteristic: (uuid: string, callback: (data: Buffer) => void) => void;
|
||||||
|
addToCharacteristicMailbox: (uuid: string, data: Buffer) => void;
|
||||||
|
readFromCharacteristic: (uuid: string, callback: (err: string | null, data: Buffer | null) => void) => void;
|
||||||
|
writeToCharacteristic: (uuid: string, data: Buffer, callback?: () => void) => void;
|
||||||
|
}
|
217
src/lpf2hub.ts
217
src/lpf2hub.ts
@ -7,6 +7,7 @@ import * as Consts from "./consts";
|
|||||||
|
|
||||||
import Debug = require("debug");
|
import Debug = require("debug");
|
||||||
const debug = Debug("lpf2hub");
|
const debug = Debug("lpf2hub");
|
||||||
|
const modeInfoDebug = Debug("lpf2hubmodeinfo");
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,6 +18,7 @@ export class LPF2Hub extends Hub {
|
|||||||
|
|
||||||
private _lastTiltX: number = 0;
|
private _lastTiltX: number = 0;
|
||||||
private _lastTiltY: number = 0;
|
private _lastTiltY: number = 0;
|
||||||
|
private _lastTiltZ: number = 0;
|
||||||
|
|
||||||
private _messageBuffer: Buffer = Buffer.alloc(0);
|
private _messageBuffer: Buffer = Buffer.alloc(0);
|
||||||
|
|
||||||
@ -24,9 +26,8 @@ export class LPF2Hub extends Hub {
|
|||||||
public connect () {
|
public connect () {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
await super.connect();
|
await super.connect();
|
||||||
const characteristic = this._getCharacteristic(Consts.BLECharacteristic.LPF2_ALL);
|
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.LPF2_HUB);
|
||||||
this._subscribeToCharacteristic(characteristic, this._parseMessage.bind(this));
|
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.LPF2_ALL, this._parseMessage.bind(this));
|
||||||
setTimeout(() => {
|
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x01, 0x02, 0x02])); // Activate button reports
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x01, 0x02, 0x02])); // Activate button reports
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x01, 0x03, 0x05])); // Request firmware version
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x01, 0x03, 0x05])); // Request firmware version
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x01, 0x06, 0x02])); // Activate battery level reports
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x01, 0x06, 0x02])); // Activate battery level reports
|
||||||
@ -35,8 +36,11 @@ export class LPF2Hub extends Hub {
|
|||||||
if (this.type === Consts.HubType.DUPLO_TRAIN_HUB) {
|
if (this.type === Consts.HubType.DUPLO_TRAIN_HUB) {
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01]));
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01]));
|
||||||
}
|
}
|
||||||
}, 1000);
|
this.emit("connect");
|
||||||
return resolve();
|
resolve();
|
||||||
|
setTimeout(() => {
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x01, 0x03, 0x05])); // Request firmware version again
|
||||||
|
}, 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +91,7 @@ export class LPF2Hub extends Hub {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let data = Buffer.from([0x41, 0x32, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
|
let data = Buffer.from([0x41, 0x32, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
if (color === false) {
|
if (typeof color === "boolean") {
|
||||||
color = 0;
|
color = 0;
|
||||||
}
|
}
|
||||||
data = Buffer.from([0x81, 0x32, 0x11, 0x51, 0x00, color]);
|
data = Buffer.from([0x81, 0x32, 0x11, 0x51, 0x00, color]);
|
||||||
@ -136,13 +140,31 @@ export class LPF2Hub extends Hub {
|
|||||||
|
|
||||||
|
|
||||||
protected _writeMessage (uuid: string, message: Buffer, callback?: () => void) {
|
protected _writeMessage (uuid: string, message: Buffer, callback?: () => void) {
|
||||||
const characteristic = this._getCharacteristic(uuid);
|
|
||||||
if (characteristic) {
|
|
||||||
message = Buffer.concat([Buffer.alloc(2), message]);
|
message = Buffer.concat([Buffer.alloc(2), message]);
|
||||||
message[0] = message.length;
|
message[0] = message.length;
|
||||||
debug("Sent Message (LPF2_ALL)", message);
|
debug("Sent Message (LPF2_ALL)", message);
|
||||||
characteristic.write(message, false, callback);
|
this._bleDevice.writeToCharacteristic(uuid, message, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected _combinePorts (port: string, type: number) {
|
||||||
|
if (!this._ports[port]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (portObj) {
|
||||||
|
Object.keys(this._ports).forEach((id) => {
|
||||||
|
if (this._ports[id].type === type && this._ports[id].value !== portObj.value && !this._virtualPorts[`${portObj.value < this._ports[id].value ? portObj.id : this._ports[id].id}${portObj.value > this._ports[id].value ? portObj.id : this._ports[id].id}`]) {
|
||||||
|
debug("Combining ports", portObj.value < this._ports[id].value ? portObj.id : id, portObj.value > this._ports[id].value ? portObj.id : id);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x61, 0x01, portObj.value < this._ports[id].value ? portObj.value : this._ports[id].value, portObj.value > this._ports[id].value ? portObj.value : this._ports[id].value]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected _checkFirmware (version: string) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -173,6 +195,14 @@ export class LPF2Hub extends Hub {
|
|||||||
this._parsePortMessage(message);
|
this._parsePortMessage(message);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 0x43: {
|
||||||
|
this._parsePortInformationResponse(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x44: {
|
||||||
|
this._parseModeInformationResponse(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 0x45: {
|
case 0x45: {
|
||||||
this._parseSensorMessage(message);
|
this._parseSensorMessage(message);
|
||||||
break;
|
break;
|
||||||
@ -216,6 +246,7 @@ export class LPF2Hub extends Hub {
|
|||||||
const major = data.readUInt8(8) >>> 4;
|
const major = data.readUInt8(8) >>> 4;
|
||||||
const minor = data.readUInt8(8) & 0xf;
|
const minor = data.readUInt8(8) & 0xf;
|
||||||
this._firmwareInfo = { major, minor, bugFix, build };
|
this._firmwareInfo = { major, minor, bugFix, build };
|
||||||
|
this._checkFirmware(this.firmwareVersion);
|
||||||
|
|
||||||
// Battery level reports
|
// Battery level reports
|
||||||
} else if (data[3] === 0x06) {
|
} else if (data[3] === 0x06) {
|
||||||
@ -227,18 +258,90 @@ export class LPF2Hub extends Hub {
|
|||||||
|
|
||||||
private _parsePortMessage (data: Buffer) {
|
private _parsePortMessage (data: Buffer) {
|
||||||
|
|
||||||
const port = this._getPortForPortNumber(data[3]);
|
let port = this._getPortForPortNumber(data[3]);
|
||||||
|
|
||||||
if (!port) {
|
if (data[4] === 0x01) {
|
||||||
return;
|
this._sendPortInformationRequest(data[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
port.connected = (data[4] === 1 || data[4] === 2) ? true : false;
|
if (!port) {
|
||||||
|
if (data[4] === 0x02) {
|
||||||
|
const portA = this._getPortForPortNumber(data[7]);
|
||||||
|
const portB = this._getPortForPortNumber(data[8]);
|
||||||
|
if (portA && portB) {
|
||||||
|
this._virtualPorts[`${portA.id}${portB.id}`] = new Port(`${portA.id}${portB.id}`, data[3]);
|
||||||
|
port = this._getPortForPortNumber(data[3]);
|
||||||
|
if (port) {
|
||||||
|
port.connected = true;
|
||||||
this._registerDeviceAttachment(port, data[5]);
|
this._registerDeviceAttachment(port, data[5]);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
port.connected = (data[4] === 0x01 || data[4] === 0x02) ? true : false;
|
||||||
|
this._registerDeviceAttachment(port, data[5]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _sendPortInformationRequest (port: number) {
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x21, port, 0x01]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _parsePortInformationResponse (data: Buffer) {
|
||||||
|
const port = data[3];
|
||||||
|
const count = data[6];
|
||||||
|
const input = data.readUInt16LE(7);
|
||||||
|
const output = data.readUInt16LE(9);
|
||||||
|
modeInfoDebug(`Port ${port}, total modes ${count}, input modes ${input.toString(2)}, output modes ${output.toString(2)}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this._sendModeInformationRequest(port, i, 0x00); // Mode Name
|
||||||
|
this._sendModeInformationRequest(port, i, 0x01); // RAW Range
|
||||||
|
this._sendModeInformationRequest(port, i, 0x02); // PCT Range
|
||||||
|
this._sendModeInformationRequest(port, i, 0x03); // SI Range
|
||||||
|
this._sendModeInformationRequest(port, i, 0x04); // SI Symbol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _sendModeInformationRequest (port: number, mode: number, type: number) {
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x22, port, mode, type]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _parseModeInformationResponse (data: Buffer) {
|
||||||
|
const port = data[3];
|
||||||
|
const mode = data[4];
|
||||||
|
const type = data[5];
|
||||||
|
switch (type) {
|
||||||
|
case 0x00: // Mode Name
|
||||||
|
modeInfoDebug(`Port ${port}, mode ${mode}, name ${data.slice(6, data.length).toString()}`);
|
||||||
|
break;
|
||||||
|
case 0x01: // RAW Range
|
||||||
|
modeInfoDebug(`Port ${port}, mode ${mode}, RAW min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`);
|
||||||
|
break;
|
||||||
|
case 0x02: // PCT Range
|
||||||
|
modeInfoDebug(`Port ${port}, mode ${mode}, PCT min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`);
|
||||||
|
break;
|
||||||
|
case 0x03: // SI Range
|
||||||
|
modeInfoDebug(`Port ${port}, mode ${mode}, SI min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`);
|
||||||
|
break;
|
||||||
|
case 0x04: // SI Symbol
|
||||||
|
modeInfoDebug(`Port ${port}, mode ${mode}, SI symbol ${data.slice(6, data.length).toString()}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private _parsePortAction (data: Buffer) {
|
private _parsePortAction (data: Buffer) {
|
||||||
|
|
||||||
const port = this._getPortForPortNumber(data[3]);
|
const port = this._getPortForPortNumber(data[3]);
|
||||||
@ -270,23 +373,68 @@ export class LPF2Hub extends Hub {
|
|||||||
|
|
||||||
if ((data[3] === 0x3b && this.type === Consts.HubType.POWERED_UP_REMOTE)) { // Voltage (PUP Remote)
|
if ((data[3] === 0x3b && this.type === Consts.HubType.POWERED_UP_REMOTE)) { // Voltage (PUP Remote)
|
||||||
data = this._padMessage(data, 6);
|
data = this._padMessage(data, 6);
|
||||||
const voltage = data.readUInt16LE(4) / 500;
|
const voltage = data.readUInt16LE(4);
|
||||||
this._voltage = voltage;
|
this._voltage = 6400.0 * voltage / 3200.0 / 1000.0;
|
||||||
return;
|
return;
|
||||||
} else if (data[3] === 0x3c && this.type === Consts.HubType.POWERED_UP_REMOTE) { // Current (PUP Remote)
|
} else if ((data[3] === 0x3c && this.type === Consts.HubType.POWERED_UP_HUB)) { // Voltage (PUP Hub)
|
||||||
|
data = this._padMessage(data, 6);
|
||||||
|
const voltage = data.readUInt16LE(4);
|
||||||
|
this._voltage = 9620.0 * voltage / 3893.0 / 1000.0;
|
||||||
|
return;
|
||||||
|
} else if ((data[3] === 0x3c && this.type === Consts.HubType.CONTROL_PLUS_HUB)) { // Voltage (Control+ Hub)
|
||||||
|
data = this._padMessage(data, 6);
|
||||||
|
const voltage = data.readUInt16LE(4);
|
||||||
|
this._voltage = 9615.0 * voltage / 4095.0 / 1000.0;
|
||||||
|
return;
|
||||||
|
} else if (data[3] === 0x3c) { // Voltage (Others)
|
||||||
|
data = this._padMessage(data, 6);
|
||||||
|
const voltage = data.readUInt16LE(4);
|
||||||
|
this._voltage = 9600.0 * voltage / 3893.0 / 1000.0;
|
||||||
|
return;
|
||||||
|
} else if (data[3] === 0x3c && this.type === Consts.HubType.POWERED_UP_REMOTE) { // RSSI (PUP Remote)
|
||||||
|
return;
|
||||||
|
} else if (data[3] === 0x3b) { // Current (Others)
|
||||||
data = this._padMessage(data, 6);
|
data = this._padMessage(data, 6);
|
||||||
const current = data.readUInt16LE(4);
|
const current = data.readUInt16LE(4);
|
||||||
this._current = current;
|
this._current = 2444 * current / 4095.0;
|
||||||
return;
|
return;
|
||||||
} else if (data[3] === 0x3c && this.type !== Consts.HubType.POWERED_UP_REMOTE) { // Voltage (Non-PUP Remote)
|
}
|
||||||
data = this._padMessage(data, 6);
|
|
||||||
const voltage = data.readUInt16LE(4) / 400;
|
if ((data[3] === 0x62 && this.type === Consts.HubType.CONTROL_PLUS_HUB)) { // Control+ Accelerometer
|
||||||
this._voltage = voltage;
|
const accelX = Math.round((data.readInt16LE(4) / 28571) * 2000);
|
||||||
|
const accelY = Math.round((data.readInt16LE(6) / 28571) * 2000);
|
||||||
|
const accelZ = Math.round((data.readInt16LE(8) / 28571) * 2000);
|
||||||
|
/**
|
||||||
|
* Emits when accelerometer detects movement. Measured in DPS - degrees per second.
|
||||||
|
* @event LPF2Hub#accel
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @param {number} z
|
||||||
|
*/
|
||||||
|
this.emit("accel", "ACCEL", accelX, accelY, accelZ);
|
||||||
return;
|
return;
|
||||||
} else if (data[3] === 0x3b && this.type !== Consts.HubType.POWERED_UP_REMOTE) { // Current (Non-PUP Remote)
|
}
|
||||||
data = this._padMessage(data, 6);
|
|
||||||
const current = data.readUInt16LE(4) / 1000;
|
if ((data[3] === 0x63 && this.type === Consts.HubType.CONTROL_PLUS_HUB)) { // Control+ Accelerometer
|
||||||
this._current = current;
|
const tiltZ = data.readInt16LE(4);
|
||||||
|
const tiltY = data.readInt16LE(6);
|
||||||
|
const tiltX = data.readInt16LE(8);
|
||||||
|
this._lastTiltX = tiltX;
|
||||||
|
this._lastTiltY = tiltY;
|
||||||
|
this._lastTiltZ = tiltZ;
|
||||||
|
this.emit("tilt", "TILT", this._lastTiltX, this._lastTiltY, this._lastTiltZ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((data[3] === 0x3d && this.type === Consts.HubType.CONTROL_PLUS_HUB)) { // Control+ CPU Temperature
|
||||||
|
/**
|
||||||
|
* Emits when a change is detected on a temperature sensor. Measured in degrees centigrade.
|
||||||
|
* @event LPF2Hub#temp
|
||||||
|
* @param {string} port For Control+ Hubs, port will be "CPU" as the sensor reports CPU temperature.
|
||||||
|
* @param {number} temp
|
||||||
|
*/
|
||||||
|
this.emit("temp", "CPU", ((data.readInt16LE(4) / 900) * 90).toFixed(2));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,11 +503,12 @@ export class LPF2Hub extends Hub {
|
|||||||
/**
|
/**
|
||||||
* Emits when a tilt sensor is activated.
|
* Emits when a tilt sensor is activated.
|
||||||
* @event LPF2Hub#tilt
|
* @event LPF2Hub#tilt
|
||||||
* @param {string} port If the event is fired from the Move Hub's in-built tilt sensor, the special port "TILT" is used.
|
* @param {string} port If the event is fired from the Move Hub or Control+ Hub's in-built tilt sensor, the special port "TILT" is used.
|
||||||
* @param {number} x
|
* @param {number} x
|
||||||
* @param {number} y
|
* @param {number} y
|
||||||
|
* @param {number} z (Only available when using a Control+ Hub)
|
||||||
*/
|
*/
|
||||||
this.emit("tilt", port.id, this._lastTiltX, this._lastTiltY);
|
this.emit("tilt", port.id, this._lastTiltX, this._lastTiltY, this._lastTiltZ);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Consts.DeviceType.BOOST_TACHO_MOTOR: {
|
case Consts.DeviceType.BOOST_TACHO_MOTOR: {
|
||||||
@ -378,10 +527,22 @@ export class LPF2Hub extends Hub {
|
|||||||
this.emit("rotate", port.id, rotation);
|
this.emit("rotate", port.id, rotation);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR: {
|
||||||
|
const rotation = data.readInt32LE(4);
|
||||||
|
this.emit("rotate", port.id, rotation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR: {
|
||||||
|
const rotation = data.readInt32LE(4);
|
||||||
|
this.emit("rotate", port.id, rotation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Consts.DeviceType.BOOST_TILT: {
|
case Consts.DeviceType.BOOST_TILT: {
|
||||||
const tiltX = data[4] > 160 ? data[4] - 255 : data[4];
|
const tiltX = data[4] > 160 ? data[4] - 255 : data[4];
|
||||||
const tiltY = data[5] > 160 ? 255 - data[5] : data[5] - (data[5] * 2);
|
const tiltY = data[5] > 160 ? 255 - data[5] : data[5] - (data[5] * 2);
|
||||||
this.emit("tilt", port.id, tiltX, tiltY);
|
this._lastTiltX = tiltX;
|
||||||
|
this._lastTiltY = tiltY;
|
||||||
|
this.emit("tilt", port.id, this._lastTiltX, this._lastTiltY, this._lastTiltZ);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Consts.DeviceType.POWERED_UP_REMOTE_BUTTON: {
|
case Consts.DeviceType.POWERED_UP_REMOTE_BUTTON: {
|
||||||
|
149
src/nobledevice.ts
Normal file
149
src/nobledevice.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { Characteristic, Peripheral, Service } from "noble";
|
||||||
|
|
||||||
|
import Debug = require("debug");
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
|
const debug = Debug("bledevice");
|
||||||
|
|
||||||
|
|
||||||
|
export class NobleDevice extends EventEmitter implements IBLEDevice {
|
||||||
|
|
||||||
|
private _noblePeripheral: Peripheral;
|
||||||
|
|
||||||
|
private _uuid: string;
|
||||||
|
private _name: string = "";
|
||||||
|
|
||||||
|
private _listeners: {[uuid: string]: any} = {};
|
||||||
|
private _characteristics: {[uuid: string]: Characteristic} = {};
|
||||||
|
|
||||||
|
private _queue: Promise<any> = Promise.resolve();
|
||||||
|
private _mailbox: Buffer[] = [];
|
||||||
|
|
||||||
|
private _connected: boolean = false;
|
||||||
|
private _connecting: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
|
constructor (device: any) {
|
||||||
|
super();
|
||||||
|
this._noblePeripheral = device;
|
||||||
|
this._uuid = device.uuid;
|
||||||
|
device.on("disconnect", () => {
|
||||||
|
this._connected = false;
|
||||||
|
this._connected = false;
|
||||||
|
this.emit("disconnect");
|
||||||
|
});
|
||||||
|
// NK: This hack allows LPF2.0 hubs to send a second advertisement packet consisting of the hub name before we try to read it
|
||||||
|
setTimeout(() => {
|
||||||
|
this._name = device.advertisement.localName;
|
||||||
|
this.emit("discoverComplete");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get uuid () {
|
||||||
|
return this._uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get name () {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get connecting () {
|
||||||
|
return this._connecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get connected () {
|
||||||
|
return this._connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public connect () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._connecting = true;
|
||||||
|
this._noblePeripheral.connect((err: string) => {
|
||||||
|
this._connecting = false;
|
||||||
|
this._connected = true;
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public disconnect () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._noblePeripheral.disconnect();
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public discoverCharacteristicsForService (uuid: string) {
|
||||||
|
return new Promise(async (discoverResolve, discoverReject) => {
|
||||||
|
uuid = this._sanitizeUUID(uuid);
|
||||||
|
this._noblePeripheral.discoverServices([uuid], (err: string, services: Service[]) => {
|
||||||
|
if (err) {
|
||||||
|
return discoverReject(err);
|
||||||
|
}
|
||||||
|
debug("Service/characteristic discovery started");
|
||||||
|
const servicePromises: Array<Promise<null>> = [];
|
||||||
|
services.forEach((service) => {
|
||||||
|
servicePromises.push(new Promise((resolve, reject) => {
|
||||||
|
service.discoverCharacteristics([], (err, characteristics) => {
|
||||||
|
characteristics.forEach((characteristic) => {
|
||||||
|
this._characteristics[characteristic.uuid] = characteristic;
|
||||||
|
});
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(servicePromises).then(() => {
|
||||||
|
debug("Service/characteristic discovery finished");
|
||||||
|
return discoverResolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public subscribeToCharacteristic (uuid: string, callback: (data: Buffer) => void) {
|
||||||
|
uuid = this._sanitizeUUID(uuid);
|
||||||
|
this._characteristics[uuid].on("data", (data: Buffer) => {
|
||||||
|
return callback(data);
|
||||||
|
});
|
||||||
|
this._characteristics[uuid].subscribe((err) => {
|
||||||
|
if (err) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public addToCharacteristicMailbox (uuid: string, data: Buffer) {
|
||||||
|
this._mailbox.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public readFromCharacteristic (uuid: string, callback: (err: string | null, data: Buffer | null) => void) {
|
||||||
|
uuid = this._sanitizeUUID(uuid);
|
||||||
|
this._characteristics[uuid].read((err: string, data: Buffer) => {
|
||||||
|
return callback(err, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public writeToCharacteristic (uuid: string, data: Buffer, callback?: () => void) {
|
||||||
|
uuid = this._sanitizeUUID(uuid);
|
||||||
|
this._characteristics[uuid].write(data, false, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _sanitizeUUID (uuid: string) {
|
||||||
|
return uuid.replace(/-/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
222
src/poweredup-browser.ts
Normal file
222
src/poweredup-browser.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import { BoostMoveHub } from "./boostmovehub";
|
||||||
|
import { ControlPlusHub } from "./controlplushub";
|
||||||
|
import { DuploTrainBase } from "./duplotrainbase";
|
||||||
|
import { Hub } from "./hub";
|
||||||
|
import { PUPHub } from "./puphub";
|
||||||
|
import { PUPRemote } from "./pupremote";
|
||||||
|
import { WebBLEDevice } from "./webbledevice";
|
||||||
|
import { WeDo2SmartHub } from "./wedo2smarthub";
|
||||||
|
|
||||||
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
import Debug = require("debug");
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
|
const debug = Debug("poweredup");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class PoweredUP
|
||||||
|
* @extends EventEmitter
|
||||||
|
*/
|
||||||
|
export class PoweredUP extends EventEmitter {
|
||||||
|
|
||||||
|
|
||||||
|
public autoSubscribe: boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
private _connectedHubs: {[uuid: string]: Hub} = {};
|
||||||
|
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
super();
|
||||||
|
this._discoveryEventHandler = this._discoveryEventHandler.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin scanning for Powered UP Hub devices.
|
||||||
|
* @method PoweredUP#scan
|
||||||
|
*/
|
||||||
|
public async scan () {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
services: [
|
||||||
|
Consts.BLEService.WEDO2_SMART_HUB
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
services: [
|
||||||
|
Consts.BLEService.LPF2_HUB
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
optionalServices: [
|
||||||
|
Consts.BLEService.WEDO2_SMART_HUB_2,
|
||||||
|
"battery_service",
|
||||||
|
"device_information"
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const server = await device.gatt.connect();
|
||||||
|
this._discoveryEventHandler.call(this, server);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a list of Powered UP Hubs.
|
||||||
|
* @method PoweredUP#getConnectedHubs
|
||||||
|
* @returns {Hub[]}
|
||||||
|
*/
|
||||||
|
public getConnectedHubs () {
|
||||||
|
return Object.keys(this._connectedHubs).map((uuid) => this._connectedHubs[uuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a Powered UP Hub by UUID.
|
||||||
|
* @method PoweredUP#getConnectedHubByUUID
|
||||||
|
* @param {string} uuid
|
||||||
|
* @returns {Hub | null}
|
||||||
|
*/
|
||||||
|
public getConnectedHubByUUID (uuid: string) {
|
||||||
|
return this._connectedHubs[uuid];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a list of Powered UP Hub by name.
|
||||||
|
* @method PoweredUP#getConnectedHubsByName
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {Hub[]}
|
||||||
|
*/
|
||||||
|
public getConnectedHubsByName (name: string) {
|
||||||
|
return Object.keys(this._connectedHubs).map((uuid) => this._connectedHubs[uuid]).filter((hub) => hub.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _determineLPF2HubType (device: IBLEDevice): Promise<Consts.HubType> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let buf: Buffer = Buffer.alloc(0);
|
||||||
|
device.subscribeToCharacteristic(Consts.BLECharacteristic.LPF2_ALL, (data: Buffer) => {
|
||||||
|
buf = Buffer.concat([buf, data]);
|
||||||
|
const len = buf[0];
|
||||||
|
if (len >= buf.length) {
|
||||||
|
const message = buf.slice(0, len);
|
||||||
|
buf = buf.slice(len);
|
||||||
|
if (message[2] === 0x01 && message[3] === 0x0b) {
|
||||||
|
process.nextTick(() => {
|
||||||
|
switch (message[5]) {
|
||||||
|
case Consts.BLEManufacturerData.POWERED_UP_REMOTE_ID:
|
||||||
|
resolve(Consts.HubType.POWERED_UP_REMOTE);
|
||||||
|
break;
|
||||||
|
case Consts.BLEManufacturerData.BOOST_MOVE_HUB_ID:
|
||||||
|
resolve(Consts.HubType.BOOST_MOVE_HUB);
|
||||||
|
break;
|
||||||
|
case Consts.BLEManufacturerData.POWERED_UP_HUB_ID:
|
||||||
|
resolve(Consts.HubType.POWERED_UP_HUB);
|
||||||
|
break;
|
||||||
|
case Consts.BLEManufacturerData.DUPLO_TRAIN_HUB_ID:
|
||||||
|
resolve(Consts.HubType.DUPLO_TRAIN_HUB);
|
||||||
|
break;
|
||||||
|
case Consts.BLEManufacturerData.CONTROL_PLUS_LARGE_HUB:
|
||||||
|
resolve(Consts.HubType.CONTROL_PLUS_HUB);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
device.addToCharacteristicMailbox(Consts.BLECharacteristic.LPF2_ALL, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
device.writeToCharacteristic(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x05, 0x00, 0x01, 0x0b, 0x05]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async _discoveryEventHandler (server: BluetoothRemoteGATTServer) {
|
||||||
|
|
||||||
|
const device = new WebBLEDevice(server);
|
||||||
|
|
||||||
|
let hub: Hub;
|
||||||
|
|
||||||
|
let hubType = Consts.HubType.UNKNOWN;
|
||||||
|
let isLPF2Hub = false;
|
||||||
|
try {
|
||||||
|
await device.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB);
|
||||||
|
hubType = Consts.HubType.WEDO2_SMART_HUB;
|
||||||
|
// tslint:disable-next-line
|
||||||
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
if (hubType !== Consts.HubType.WEDO2_SMART_HUB) {
|
||||||
|
await device.discoverCharacteristicsForService(Consts.BLEService.LPF2_HUB);
|
||||||
|
isLPF2Hub = true;
|
||||||
|
}
|
||||||
|
// tslint:disable-next-line
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
if (isLPF2Hub) {
|
||||||
|
hubType = await this._determineLPF2HubType(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (hubType) {
|
||||||
|
case Consts.HubType.WEDO2_SMART_HUB:
|
||||||
|
hub = new WeDo2SmartHub(device, this.autoSubscribe);
|
||||||
|
break;
|
||||||
|
case Consts.HubType.BOOST_MOVE_HUB:
|
||||||
|
hub = new BoostMoveHub(device, this.autoSubscribe);
|
||||||
|
break;
|
||||||
|
case Consts.HubType.POWERED_UP_HUB:
|
||||||
|
hub = new PUPHub(device, this.autoSubscribe);
|
||||||
|
break;
|
||||||
|
case Consts.HubType.POWERED_UP_REMOTE:
|
||||||
|
hub = new PUPRemote(device, this.autoSubscribe);
|
||||||
|
break;
|
||||||
|
case Consts.HubType.DUPLO_TRAIN_HUB:
|
||||||
|
hub = new DuploTrainBase(device, this.autoSubscribe);
|
||||||
|
break;
|
||||||
|
case Consts.HubType.CONTROL_PLUS_HUB:
|
||||||
|
hub = new ControlPlusHub(device, this.autoSubscribe);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.on("discoverComplete", () => {
|
||||||
|
|
||||||
|
hub.on("connect", () => {
|
||||||
|
debug(`Hub ${hub.uuid} connected`);
|
||||||
|
this._connectedHubs[hub.uuid] = hub;
|
||||||
|
});
|
||||||
|
|
||||||
|
hub.on("disconnect", () => {
|
||||||
|
debug(`Hub ${hub.uuid} disconnected`);
|
||||||
|
delete this._connectedHubs[hub.uuid];
|
||||||
|
});
|
||||||
|
|
||||||
|
debug(`Hub ${hub.uuid} discovered`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when a Powered UP Hub device is found.
|
||||||
|
* @event PoweredUP#discover
|
||||||
|
* @param {WeDo2SmartHub | BoostMoveHub | ControlPlusHub | PUPHub | PUPRemote | DuploTrainBase} hub
|
||||||
|
*/
|
||||||
|
this.emit("discover", hub);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import { Peripheral } from "noble-mac";
|
import { Peripheral } from "noble-mac";
|
||||||
|
|
||||||
import { BoostMoveHub } from "./boostmovehub";
|
import { BoostMoveHub } from "./boostmovehub";
|
||||||
|
import { ControlPlusHub } from "./controlplushub";
|
||||||
import { DuploTrainBase } from "./duplotrainbase";
|
import { DuploTrainBase } from "./duplotrainbase";
|
||||||
import { Hub } from "./hub";
|
import { Hub } from "./hub";
|
||||||
|
import { NobleDevice } from "./nobledevice";
|
||||||
import { PUPHub } from "./puphub";
|
import { PUPHub } from "./puphub";
|
||||||
import { PUPRemote } from "./pupremote";
|
import { PUPRemote } from "./pupremote";
|
||||||
import { WeDo2SmartHub } from "./wedo2smarthub";
|
import { WeDo2SmartHub } from "./wedo2smarthub";
|
||||||
|
|
||||||
import { isBrowserContext } from "./utils";
|
|
||||||
|
|
||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
@ -22,11 +22,7 @@ let wantScan = false;
|
|||||||
let discoveryEventAttached = false;
|
let discoveryEventAttached = false;
|
||||||
|
|
||||||
const startScanning = () => {
|
const startScanning = () => {
|
||||||
if (isBrowserContext) {
|
|
||||||
noble.startScanning([Consts.BLEService.WEDO2_SMART_HUB, Consts.BLEService.LPF2_HUB]);
|
|
||||||
} else {
|
|
||||||
noble.startScanning();
|
noble.startScanning();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
noble.on("stateChange", (state: string) => {
|
noble.on("stateChange", (state: string) => {
|
||||||
@ -64,7 +60,7 @@ export class PoweredUP extends EventEmitter {
|
|||||||
* Begin scanning for Powered UP Hub devices.
|
* Begin scanning for Powered UP Hub devices.
|
||||||
* @method PoweredUP#scan
|
* @method PoweredUP#scan
|
||||||
*/
|
*/
|
||||||
public scan () {
|
public async scan () {
|
||||||
wantScan = true;
|
wantScan = true;
|
||||||
|
|
||||||
if (!discoveryEventAttached) {
|
if (!discoveryEventAttached) {
|
||||||
@ -76,6 +72,8 @@ export class PoweredUP extends EventEmitter {
|
|||||||
debug("Scanning started");
|
debug("Scanning started");
|
||||||
startScanning();
|
startScanning();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -129,29 +127,28 @@ export class PoweredUP extends EventEmitter {
|
|||||||
|
|
||||||
private async _discoveryEventHandler (peripheral: Peripheral) {
|
private async _discoveryEventHandler (peripheral: Peripheral) {
|
||||||
|
|
||||||
|
peripheral.removeAllListeners();
|
||||||
|
const device = new NobleDevice(peripheral);
|
||||||
|
|
||||||
let hub: Hub;
|
let hub: Hub;
|
||||||
|
|
||||||
if (await WeDo2SmartHub.IsWeDo2SmartHub(peripheral)) {
|
if (await WeDo2SmartHub.IsWeDo2SmartHub(peripheral)) {
|
||||||
hub = new WeDo2SmartHub(peripheral, this.autoSubscribe);
|
hub = new WeDo2SmartHub(device, this.autoSubscribe);
|
||||||
} else if (await BoostMoveHub.IsBoostMoveHub(peripheral)) {
|
} else if (await BoostMoveHub.IsBoostMoveHub(peripheral)) {
|
||||||
hub = new BoostMoveHub(peripheral, this.autoSubscribe);
|
hub = new BoostMoveHub(device, this.autoSubscribe);
|
||||||
} else if (await PUPHub.IsPUPHub(peripheral)) {
|
} else if (await PUPHub.IsPUPHub(peripheral)) {
|
||||||
hub = new PUPHub(peripheral, this.autoSubscribe);
|
hub = new PUPHub(device, this.autoSubscribe);
|
||||||
} else if (await PUPRemote.IsPUPRemote(peripheral)) {
|
} else if (await PUPRemote.IsPUPRemote(peripheral)) {
|
||||||
hub = new PUPRemote(peripheral, this.autoSubscribe);
|
hub = new PUPRemote(device, this.autoSubscribe);
|
||||||
} else if (await DuploTrainBase.IsDuploTrainBase(peripheral)) {
|
} else if (await DuploTrainBase.IsDuploTrainBase(peripheral)) {
|
||||||
hub = new DuploTrainBase(peripheral, this.autoSubscribe);
|
hub = new DuploTrainBase(device, this.autoSubscribe);
|
||||||
|
} else if (await ControlPlusHub.IsControlPlusHub(peripheral)) {
|
||||||
|
hub = new ControlPlusHub(device, this.autoSubscribe);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
peripheral.removeAllListeners();
|
device.on("discoverComplete", () => {
|
||||||
// noble.stopScanning();
|
|
||||||
// if (!isBrowserContext) {
|
|
||||||
// startScanning();
|
|
||||||
// }
|
|
||||||
|
|
||||||
hub.on("discoverComplete", () => {
|
|
||||||
|
|
||||||
hub.on("connect", () => {
|
hub.on("connect", () => {
|
||||||
debug(`Hub ${hub.uuid} connected`);
|
debug(`Hub ${hub.uuid} connected`);
|
||||||
@ -172,7 +169,7 @@ export class PoweredUP extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Emits when a Powered UP Hub device is found.
|
* Emits when a Powered UP Hub device is found.
|
||||||
* @event PoweredUP#discover
|
* @event PoweredUP#discover
|
||||||
* @param {WeDo2SmartHub | BoostMoveHub | PUPHub | PUPRemote | DuploTrainBase} hub
|
* @param {WeDo2SmartHub | BoostMoveHub | ControlPlusHub | PUPHub | PUPRemote | DuploTrainBase} hub
|
||||||
*/
|
*/
|
||||||
this.emit("discover", hub);
|
this.emit("discover", hub);
|
||||||
|
|
193
src/puphub.ts
193
src/puphub.ts
@ -1,3 +1,4 @@
|
|||||||
|
import compareVersion from "compare-versions";
|
||||||
import { Peripheral } from "noble";
|
import { Peripheral } from "noble";
|
||||||
|
|
||||||
import { LPF2Hub } from "./lpf2hub";
|
import { LPF2Hub } from "./lpf2hub";
|
||||||
@ -6,6 +7,7 @@ import { Port } from "./port";
|
|||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
import Debug = require("debug");
|
import Debug = require("debug");
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
const debug = Debug("puphub");
|
const debug = Debug("puphub");
|
||||||
|
|
||||||
|
|
||||||
@ -18,32 +20,23 @@ const debug = Debug("puphub");
|
|||||||
export class PUPHub extends LPF2Hub {
|
export class PUPHub extends LPF2Hub {
|
||||||
|
|
||||||
|
|
||||||
// We set JSDoc to ignore these events as a Powered UP Remote will never emit them.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPHub#rotate
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPHub#speed
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
public static IsPUPHub (peripheral: Peripheral) {
|
public static IsPUPHub (peripheral: Peripheral) {
|
||||||
return (peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.POWERED_UP_HUB_ID);
|
return (peripheral.advertisement &&
|
||||||
|
peripheral.advertisement.serviceUuids &&
|
||||||
|
peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.POWERED_UP_HUB_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
|
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
|
||||||
super(peripheral, autoSubscribe);
|
super(device, autoSubscribe);
|
||||||
this.type = Consts.HubType.POWERED_UP_HUB;
|
this.type = Consts.HubType.POWERED_UP_HUB;
|
||||||
this._ports = {
|
this._ports = {
|
||||||
"A": new Port("A", 0),
|
"A": new Port("A", 0),
|
||||||
"B": new Port("B", 1),
|
"B": new Port("B", 1)
|
||||||
"AB": new Port("AB", 57)
|
|
||||||
};
|
};
|
||||||
|
this.on("attach", (port, type) => {
|
||||||
|
this._combinePorts(port, type);
|
||||||
|
});
|
||||||
debug("Discovered Powered UP Hub");
|
debug("Discovered Powered UP Hub");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,16 +61,9 @@ export class PUPHub extends LPF2Hub {
|
|||||||
*/
|
*/
|
||||||
public setMotorSpeed (port: string, speed: number | [number, number], time?: number | boolean) {
|
public setMotorSpeed (port: string, speed: number | [number, number], time?: number | boolean) {
|
||||||
const portObj = this._portLookup(port);
|
const portObj = this._portLookup(port);
|
||||||
if (portObj.id !== "AB" && speed instanceof Array) {
|
if (!this._virtualPorts[portObj.id] && speed instanceof Array) {
|
||||||
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
||||||
}
|
}
|
||||||
if (portObj.id === "AB") {
|
|
||||||
const portObjA = this._portLookup("A");
|
|
||||||
const portObjB = this._portLookup("B");
|
|
||||||
if (portObjA.type !== portObjB.type) {
|
|
||||||
throw new Error(`Port ${portObj.id} requires both motors be of the same type`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let cancelEventTimer = true;
|
let cancelEventTimer = true;
|
||||||
if (typeof time === "boolean") {
|
if (typeof time === "boolean") {
|
||||||
if (time === true) {
|
if (time === true) {
|
||||||
@ -90,35 +76,60 @@ export class PUPHub extends LPF2Hub {
|
|||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (time && typeof time === "number") {
|
if (time && typeof time === "number") {
|
||||||
|
|
||||||
|
if (
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
) {
|
||||||
|
portObj.busy = true;
|
||||||
let data = null;
|
let data = null;
|
||||||
if (portObj.id === "AB") {
|
if (this._virtualPorts[portObj.id]) {
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed)]);
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0a, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore: The type of speed is properly checked at the start
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x60, 0x00, this._mapSpeed(speed), 0x00, 0x00]);
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x09, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
}
|
}
|
||||||
|
data.writeUInt16LE(time > 65535 ? 65535 : time, 4);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, this._mapSpeed(speed)]);
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
const timeout = global.setTimeout(() => {
|
const timeout = global.setTimeout(() => {
|
||||||
let data = null;
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, 0x00]);
|
||||||
if (portObj.id === "AB") {
|
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, 0x00, 0x00]);
|
|
||||||
} else {
|
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x60, 0x00, 0x00, 0x00, 0x00]);
|
|
||||||
}
|
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
return resolve();
|
return resolve();
|
||||||
|
// @ts-ignore: The type of time is properly checked at the start
|
||||||
}, time);
|
}, time);
|
||||||
portObj.setEventTimer(timeout);
|
portObj.setEventTimer(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
if (portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR || portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR) {
|
||||||
|
portObj.busy = true;
|
||||||
let data = null;
|
let data = null;
|
||||||
if (portObj.id === "AB") {
|
if (this._virtualPorts[portObj.id]) {
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed)]);
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore: The type of speed is properly checked at the start
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
data = Buffer.from([0x81, portObj.value, 0x11, 0x60, 0x00, this._mapSpeed(speed), 0x00, 0x00]);
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x01, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
}
|
}
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
return resolve();
|
return resolve();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x00, this._mapSpeed(speed)]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -146,6 +157,105 @@ export class PUPHub extends LPF2Hub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate a motor by a given angle.
|
||||||
|
* @method PUPHub#setMotorAngle
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} angle How much the motor should be rotated (in degrees).
|
||||||
|
* @param {number | Array.<number>} [speed=100] For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0. If you are specifying port AB to control both motors, you can optionally supply a tuple of speeds.
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public setMotorAngle (port: string, angle: number, speed: number | [number, number] = 100) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Angle rotation is only available when using a Boost Tacho Motor, Boost Move Hub Motor, Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
if (!this._virtualPorts[portObj.id] && speed instanceof Array) {
|
||||||
|
throw new Error(`Port ${portObj.id} can only accept a single speed`);
|
||||||
|
}
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
portObj.busy = true;
|
||||||
|
let data = null;
|
||||||
|
if (this._virtualPorts[portObj.id]) {
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0c, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed), 0x64, 0x7f, 0x03]);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0b, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
}
|
||||||
|
data.writeUInt32LE(angle, 4);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell motor to goto an absolute position
|
||||||
|
* @method PUPHub#setAbsolutePosition
|
||||||
|
* @param {string} port
|
||||||
|
* @param {number} pos The position of the motor to go to
|
||||||
|
* @param {number | Array.<number>} [speed=100] A value between 1 - 100 should be set (Direction does not apply when going to absolute position)
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public setAbsolutePosition (port: string, pos: number, speed: number = 100) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Absolute positioning is only available when using a Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
portObj.cancelEventTimer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
portObj.busy = true;
|
||||||
|
let data = null;
|
||||||
|
if (this._virtualPorts[portObj.id]) {
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
data.writeInt32LE(pos, 4);
|
||||||
|
data.writeInt32LE(pos, 8);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore: The type of speed is properly checked at the start
|
||||||
|
data = Buffer.from([0x81, portObj.value, 0x11, 0x0d, 0x00, 0x00, 0x00, 0x00, this._mapSpeed(speed), 0x64, 0x7f, 0x03]);
|
||||||
|
data.writeInt32LE(pos, 4);
|
||||||
|
}
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
portObj.finished = () => {
|
||||||
|
return resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the current motor position as absolute position zero
|
||||||
|
* @method PUPHub#resetAbsolutePosition
|
||||||
|
* @param {string} port
|
||||||
|
* @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished).
|
||||||
|
*/
|
||||||
|
public resetAbsolutePosition (port: string) {
|
||||||
|
const portObj = this._portLookup(port);
|
||||||
|
if (!(
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR ||
|
||||||
|
portObj.type === Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR
|
||||||
|
)) {
|
||||||
|
throw new Error("Absolute positioning is only available when using a Control+ Medium Motor, or Control+ Large Motor");
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const data = Buffer.from([0x81, portObj.value, 0x11, 0x51, 0x02, 0x00, 0x00, 0x00, 0x00]);
|
||||||
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fully (hard) stop the motor on a given port.
|
* Fully (hard) stop the motor on a given port.
|
||||||
* @method PUPHub#brakeMotor
|
* @method PUPHub#brakeMotor
|
||||||
@ -185,4 +295,11 @@ export class PUPHub extends LPF2Hub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected _checkFirmware (version: string) {
|
||||||
|
if (compareVersion("1.1.00.0004", version) === 1) {
|
||||||
|
throw new Error(`Your Powered Up Hub's (${this.name}) firmware is out of date and unsupported by this library. Please update it via the official Powered Up app.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { Port } from "./port";
|
|||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
import Debug = require("debug");
|
import Debug = require("debug");
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
const debug = Debug("pupremote");
|
const debug = Debug("pupremote");
|
||||||
|
|
||||||
|
|
||||||
@ -18,51 +19,15 @@ const debug = Debug("pupremote");
|
|||||||
export class PUPRemote extends LPF2Hub {
|
export class PUPRemote extends LPF2Hub {
|
||||||
|
|
||||||
|
|
||||||
// We set JSDoc to ignore these events as a Powered UP Remote will never emit them.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPRemote#distance
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPRemote#color
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPRemote#tilt
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPRemote#rotate
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPRemote#speed
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPRemote#attach
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event PUPRemote#detach
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
public static IsPUPRemote (peripheral: Peripheral) {
|
public static IsPUPRemote (peripheral: Peripheral) {
|
||||||
return (peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.POWERED_UP_REMOTE_ID);
|
return (peripheral.advertisement &&
|
||||||
|
peripheral.advertisement.serviceUuids &&
|
||||||
|
peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.LPF2_HUB.replace(/-/g, "")) >= 0 && peripheral.advertisement.manufacturerData[3] === Consts.BLEManufacturerData.POWERED_UP_REMOTE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
|
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
|
||||||
super(peripheral, autoSubscribe);
|
super(device, autoSubscribe);
|
||||||
this.type = Consts.HubType.POWERED_UP_REMOTE;
|
this.type = Consts.HubType.POWERED_UP_REMOTE;
|
||||||
this._ports = {
|
this._ports = {
|
||||||
"LEFT": new Port("LEFT", 0),
|
"LEFT": new Port("LEFT", 0),
|
||||||
@ -92,7 +57,7 @@ export class PUPRemote extends LPF2Hub {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let data = Buffer.from([0x41, 0x34, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
|
let data = Buffer.from([0x41, 0x34, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
|
||||||
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
|
||||||
if (color === false) {
|
if (typeof color === "boolean") {
|
||||||
color = 0;
|
color = 0;
|
||||||
}
|
}
|
||||||
data = Buffer.from([0x81, 0x34, 0x11, 0x51, 0x00, color]);
|
data = Buffer.from([0x81, 0x34, 0x11, 0x51, 0x00, color]);
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const isBrowserContext = (typeof navigator !== "undefined" && navigator && navigator.bluetooth);
|
export const isWebBluetooth = (typeof navigator !== "undefined" && navigator && navigator.bluetooth);
|
||||||
|
149
src/webbledevice.ts
Normal file
149
src/webbledevice.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import Debug = require("debug");
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
|
const debug = Debug("bledevice");
|
||||||
|
|
||||||
|
|
||||||
|
export class WebBLEDevice extends EventEmitter implements IBLEDevice {
|
||||||
|
|
||||||
|
private _webBLEServer: any;
|
||||||
|
|
||||||
|
private _uuid: string;
|
||||||
|
private _name: string = "";
|
||||||
|
|
||||||
|
private _listeners: {[uuid: string]: any} = {};
|
||||||
|
private _characteristics: {[uuid: string]: any} = {};
|
||||||
|
|
||||||
|
private _queue: Promise<any> = Promise.resolve();
|
||||||
|
private _mailbox: Buffer[] = [];
|
||||||
|
|
||||||
|
private _connected: boolean = false;
|
||||||
|
private _connecting: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
|
constructor (device: any) {
|
||||||
|
super();
|
||||||
|
this._webBLEServer = device;
|
||||||
|
this._uuid = device.device.id;
|
||||||
|
this._name = device.device.name;
|
||||||
|
device.device.addEventListener("gattserverdisconnected", () => {
|
||||||
|
this._connected = false;
|
||||||
|
this._connected = false;
|
||||||
|
this.emit("disconnect");
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emit("discoverComplete");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get uuid () {
|
||||||
|
return this._uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get name () {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get connecting () {
|
||||||
|
return this._connecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get connected () {
|
||||||
|
return this._connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public connect () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._connected = true;
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public disconnect () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._webBLEServer.device.gatt.disconnect();
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public discoverCharacteristicsForService (uuid: string) {
|
||||||
|
return new Promise(async (discoverResolve, discoverReject) => {
|
||||||
|
debug("Service/characteristic discovery started");
|
||||||
|
let service;
|
||||||
|
try {
|
||||||
|
service = await this._webBLEServer.getPrimaryService(uuid);
|
||||||
|
} catch (err) {
|
||||||
|
return discoverReject(err);
|
||||||
|
}
|
||||||
|
const characteristics = await service.getCharacteristics();
|
||||||
|
for (const characteristic of characteristics) {
|
||||||
|
this._characteristics[characteristic.uuid] = characteristic;
|
||||||
|
}
|
||||||
|
debug("Service/characteristic discovery finished");
|
||||||
|
return discoverResolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public subscribeToCharacteristic (uuid: string, callback: (data: Buffer) => void) {
|
||||||
|
if (this._listeners[uuid]) {
|
||||||
|
this._characteristics[uuid].removeEventListener("characteristicvaluechanged", this._listeners[uuid]);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
this._listeners[uuid] = (event) => {
|
||||||
|
const buf = Buffer.alloc(event.target.value.buffer.byteLength);
|
||||||
|
const view = new Uint8Array(event.target.value.buffer);
|
||||||
|
for (let i = 0; i < buf.length; i++) {
|
||||||
|
buf[i] = view[i];
|
||||||
|
}
|
||||||
|
return callback(buf);
|
||||||
|
};
|
||||||
|
this._characteristics[uuid].addEventListener("characteristicvaluechanged", this._listeners[uuid]);
|
||||||
|
for (const data of this._mailbox) {
|
||||||
|
callback(data);
|
||||||
|
}
|
||||||
|
this._mailbox = [];
|
||||||
|
this._characteristics[uuid].startNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public addToCharacteristicMailbox (uuid: string, data: Buffer) {
|
||||||
|
this._mailbox.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public readFromCharacteristic (uuid: string, callback: (err: string | null, data: Buffer | null) => void) {
|
||||||
|
// @ts-ignore
|
||||||
|
this._characteristics[uuid].readValue().then((data) => {
|
||||||
|
const buf = Buffer.alloc(data.buffer.byteLength);
|
||||||
|
const view = new Uint8Array(data.buffer);
|
||||||
|
for (let i = 0; i < buf.length; i++) {
|
||||||
|
buf[i] = view[i];
|
||||||
|
}
|
||||||
|
callback(null, buf);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public writeToCharacteristic (uuid: string, data: Buffer, callback?: () => void) {
|
||||||
|
this._queue = this._queue.then(() => this._characteristics[uuid].writeValue(data)).then(() => {
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _sanitizeUUID (uuid: string) {
|
||||||
|
return uuid.replace(/-/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -6,6 +6,8 @@ import { Port } from "./port";
|
|||||||
import * as Consts from "./consts";
|
import * as Consts from "./consts";
|
||||||
|
|
||||||
import Debug = require("debug");
|
import Debug = require("debug");
|
||||||
|
import { IBLEDevice } from "./interfaces";
|
||||||
|
import { isWebBluetooth } from "./utils";
|
||||||
const debug = Debug("wedo2smarthub");
|
const debug = Debug("wedo2smarthub");
|
||||||
|
|
||||||
|
|
||||||
@ -17,16 +19,10 @@ const debug = Debug("wedo2smarthub");
|
|||||||
export class WeDo2SmartHub extends Hub {
|
export class WeDo2SmartHub extends Hub {
|
||||||
|
|
||||||
|
|
||||||
// We set JSDoc to ignore these events as a WeDo 2.0 Smart Hub will never emit them.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @event WeDo2SmartHub#speed
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
public static IsWeDo2SmartHub (peripheral: Peripheral) {
|
public static IsWeDo2SmartHub (peripheral: Peripheral) {
|
||||||
return (peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.WEDO2_SMART_HUB.replace(/-/g, "")) >= 0);
|
return (peripheral.advertisement &&
|
||||||
|
peripheral.advertisement.serviceUuids &&
|
||||||
|
peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.WEDO2_SMART_HUB.replace(/-/g, "")) >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -34,8 +30,8 @@ export class WeDo2SmartHub extends Hub {
|
|||||||
private _lastTiltY: number = 0;
|
private _lastTiltY: number = 0;
|
||||||
|
|
||||||
|
|
||||||
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
|
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
|
||||||
super(peripheral, autoSubscribe);
|
super(device, autoSubscribe);
|
||||||
this.type = Consts.HubType.WEDO2_SMART_HUB;
|
this.type = Consts.HubType.WEDO2_SMART_HUB;
|
||||||
this._ports = {
|
this._ports = {
|
||||||
"A": new Port("A", 1),
|
"A": new Port("A", 1),
|
||||||
@ -49,23 +45,53 @@ export class WeDo2SmartHub extends Hub {
|
|||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
debug("Connecting to WeDo 2.0 Smart Hub");
|
debug("Connecting to WeDo 2.0 Smart Hub");
|
||||||
await super.connect();
|
await super.connect();
|
||||||
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_PORT_TYPE), this._parsePortMessage.bind(this));
|
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB);
|
||||||
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_SENSOR_VALUE), this._parseSensorMessage.bind(this));
|
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_2);
|
||||||
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_BUTTON), this._parseSensorMessage.bind(this));
|
if (!isWebBluetooth) {
|
||||||
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY), this._parseBatteryMessage.bind(this));
|
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_3);
|
||||||
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_HIGH_CURRENT_ALERT), this._parseHighCurrentAlert.bind(this));
|
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_4);
|
||||||
this._getCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY).read((err, data) => {
|
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_5);
|
||||||
this._parseBatteryMessage(data);
|
} else {
|
||||||
});
|
await this._bleDevice.discoverCharacteristicsForService("battery_service");
|
||||||
this._getCharacteristic(Consts.BLECharacteristic.WEDO2_FIRMWARE_REVISION).read((err, data) => {
|
await this._bleDevice.discoverCharacteristicsForService("device_information");
|
||||||
this._parseFirmwareRevisionString(data);
|
}
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
this._activatePortDevice(0x03, 0x15, 0x00, 0x00); // Activate voltage reports
|
this._activatePortDevice(0x03, 0x15, 0x00, 0x00); // Activate voltage reports
|
||||||
this._activatePortDevice(0x04, 0x14, 0x00, 0x00); // Activate current reports
|
this._activatePortDevice(0x04, 0x14, 0x00, 0x00); // Activate current reports
|
||||||
}, 1000);
|
|
||||||
debug("Connect completed");
|
debug("Connect completed");
|
||||||
return resolve();
|
this.emit("connect");
|
||||||
|
resolve();
|
||||||
|
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_PORT_TYPE, this._parsePortMessage.bind(this));
|
||||||
|
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_SENSOR_VALUE, this._parseSensorMessage.bind(this));
|
||||||
|
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_BUTTON, this._parseSensorMessage.bind(this));
|
||||||
|
if (!isWebBluetooth) {
|
||||||
|
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY, this._parseBatteryMessage.bind(this));
|
||||||
|
this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY, (err, data) => {
|
||||||
|
if (data) {
|
||||||
|
this._parseBatteryMessage(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._bleDevice.readFromCharacteristic("00002a19-0000-1000-8000-00805f9b34fb", (err, data) => {
|
||||||
|
if (data) {
|
||||||
|
this._parseBatteryMessage(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._bleDevice.subscribeToCharacteristic("00002a19-0000-1000-8000-00805f9b34fb", this._parseHighCurrentAlert.bind(this));
|
||||||
|
}
|
||||||
|
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_HIGH_CURRENT_ALERT, this._parseHighCurrentAlert.bind(this));
|
||||||
|
if (!isWebBluetooth) {
|
||||||
|
this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.WEDO2_FIRMWARE_REVISION, (err, data) => {
|
||||||
|
if (data) {
|
||||||
|
this._parseFirmwareRevisionString(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._bleDevice.readFromCharacteristic("00002a26-0000-1000-8000-00805f9b34fb", (err, data) => {
|
||||||
|
if (data) {
|
||||||
|
this._parseFirmwareRevisionString(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +127,7 @@ export class WeDo2SmartHub extends Hub {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let data = Buffer.from([0x06, 0x17, 0x01, 0x01]);
|
let data = Buffer.from([0x06, 0x17, 0x01, 0x01]);
|
||||||
this._writeMessage(Consts.BLECharacteristic.WEDO2_PORT_TYPE_WRITE, data);
|
this._writeMessage(Consts.BLECharacteristic.WEDO2_PORT_TYPE_WRITE, data);
|
||||||
if (color === false) {
|
if (typeof color === "boolean") {
|
||||||
color = 0;
|
color = 0;
|
||||||
}
|
}
|
||||||
data = Buffer.from([0x06, 0x04, 0x01, color]);
|
data = Buffer.from([0x06, 0x04, 0x01, color]);
|
||||||
@ -278,13 +304,10 @@ export class WeDo2SmartHub extends Hub {
|
|||||||
|
|
||||||
|
|
||||||
private _writeMessage (uuid: string, message: Buffer, callback?: () => void) {
|
private _writeMessage (uuid: string, message: Buffer, callback?: () => void) {
|
||||||
const characteristic = this._getCharacteristic(uuid);
|
|
||||||
if (characteristic) {
|
|
||||||
if (debug.enabled) {
|
if (debug.enabled) {
|
||||||
debug(`Sent Message (${this._getCharacteristicNameFromUUID(uuid)})`, message);
|
debug(`Sent Message (${this._getCharacteristicNameFromUUID(uuid)})`, message);
|
||||||
}
|
}
|
||||||
characteristic.write(message, false, callback);
|
this._bleDevice.writeToCharacteristic(uuid, message, callback);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -302,7 +325,6 @@ export class WeDo2SmartHub extends Hub {
|
|||||||
|
|
||||||
private _parseHighCurrentAlert (data: Buffer) {
|
private _parseHighCurrentAlert (data: Buffer) {
|
||||||
debug("Received Message (WEDO2_HIGH_CURRENT_ALERT)", data);
|
debug("Received Message (WEDO2_HIGH_CURRENT_ALERT)", data);
|
||||||
// console.log(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -424,6 +446,17 @@ export class WeDo2SmartHub extends Hub {
|
|||||||
* @param {number} rotation
|
* @param {number} rotation
|
||||||
*/
|
*/
|
||||||
this.emit("rotate", port.id, rotation);
|
this.emit("rotate", port.id, rotation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR: {
|
||||||
|
const rotation = data.readInt32LE(2);
|
||||||
|
this.emit("rotate", port.id, rotation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR: {
|
||||||
|
const rotation = data.readInt32LE(2);
|
||||||
|
this.emit("rotate", port.id, rotation);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
"outDir": "./dist/node", /* Redirect output structure to the directory. */
|
||||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
// "composite": true, /* Enable project compilation */
|
// "composite": true, /* Enable project compilation */
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
73
webble_test.html
Normal file
73
webble_test.html
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>node-poweredup Web Bluetooth Test</title>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const scan = async function () {
|
||||||
|
|
||||||
|
const WEDO2_SMART_HUB = "00001523-1212-efde-1523-785feabcd123";
|
||||||
|
const LPF2_HUB = "00001623-1212-efde-1623-785feabcd123";
|
||||||
|
|
||||||
|
const LPF2_ALL = "00001624-1212-efde-1623-785feabcd123"
|
||||||
|
|
||||||
|
const device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
services: [
|
||||||
|
WEDO2_SMART_HUB
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
services: [
|
||||||
|
LPF2_HUB
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const server = await device.gatt.connect();
|
||||||
|
console.log(server);
|
||||||
|
|
||||||
|
let connectComplete = false;
|
||||||
|
let hubType = 0;
|
||||||
|
let isLPF2Hub = false;
|
||||||
|
let service;
|
||||||
|
try {
|
||||||
|
service = await server.getPrimaryService(WEDO2_SMART_HUB);
|
||||||
|
hubType = 1;
|
||||||
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
service = await server.getPrimaryService(LPF2_HUB);
|
||||||
|
isLPF2Hub = true;
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
const characteristics = await service.getCharacteristics();
|
||||||
|
const charMap = {};
|
||||||
|
for (const characteristic of characteristics) {
|
||||||
|
charMap[characteristic.uuid] = characteristic;
|
||||||
|
}
|
||||||
|
|
||||||
|
charMap[LPF2_ALL].addEventListener("characteristicvaluechanged", (event) => {
|
||||||
|
console.log(event.target.value.buffer);
|
||||||
|
});
|
||||||
|
|
||||||
|
charMap[LPF2_ALL].startNotifications();
|
||||||
|
|
||||||
|
if (isLPF2Hub) {
|
||||||
|
const hubTypeCmd = new Uint8Array([0x05, 0x00, 0x01, 0x0b, 0x05]);
|
||||||
|
charMap[LPF2_ALL].writeValue(hubTypeCmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<button onclick="scan()">Scan</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
26
webpack.config.js
Normal file
26
webpack.config.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: "./src/index-browser.ts",
|
||||||
|
devtool: "source-map",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts?$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: {
|
||||||
|
"noble": "noble",
|
||||||
|
"noble-mac": "noble-mac"
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js"]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: "poweredup.js",
|
||||||
|
path: path.resolve(__dirname, "dist", "browser")
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user