Merge pull request #3 from nathankellenicki/master

Merge from base
This commit is contained in:
Shane Church 2019-10-12 17:48:51 -05:00 committed by GitHub
commit 41ca97e31a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 30887 additions and 3400 deletions

View File

@ -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
View 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
View File

@ -2,3 +2,4 @@ node_modules/
dist/
.vscode/
*.tgz
**/.DS_store

View File

@ -1,22 +1,15 @@
[![CircleCI](https://circleci.com/gh/nathankellenicki/node-poweredup.svg?style=shield)](https://circleci.com/gh/nathankellenicki/node-poweredup)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/node-poweredup?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![NPM Version](https://img.shields.io/npm/v/node-poweredup.svg?style=flat)
[![Drone](https://drone.kellenicki.com/api/badges/nkellenicki/node-poweredup/status.svg)](https://drone.kellenicki.com/nkellenicki/node-poweredup)
[![NPM Version](https://img.shields.io/npm/v/node-poweredup.svg?style=flat)](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
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.
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 Installation
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
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 |
| ------------------------------- | ------------ | ------------- | ------------------ | -------------- | -------------- | ------------ |
| 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 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 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> |
| 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 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> |
| 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 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> |
| 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 | 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 | 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 | 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 | 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 | 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 | 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.
@ -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> |
| 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 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 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> |
| 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
* 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.
* Plugging two Boost Tacho Motors into the Powered UP Hub will crash the Hub (This requires a firmware update from LEGO to fix).
* When used with the Boost Move Hub, the Control+ Motors do not currently accept commands.
### Documentation
[Full documentation is available here.](https://nathankellenicki.github.io/node-poweredup/)
### Sample Usage
### Node.js Sample Usage
```javascript
const PoweredUP = require("node-poweredup");
const poweredUP = new PoweredUP.PoweredUP();
poweredUP.on("discover", async (hub) => { // Wait to discover a Hub
console.log(`Discovered ${hub.name}!`);
await hub.connect(); // Connect to the Hub
console.log("Connected");
await hub.sleep(3000); // Sleep for 3 seconds before starting
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
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.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.sleep(1000); // Do nothing for 1 second
}
});
poweredUP.scan(); // Start scanning for Hubs
console.log("Scanning for Hubs...");
```
More examples are available in the "examples" directory.
### 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

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

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

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

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

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

367
docs/poweredup-node.js.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -93,6 +93,7 @@ var __importStar = (this &amp;&amp; this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const bledevice_1 = require("./bledevice");
const boostmovehub_1 = require("./boostmovehub");
const duplotrainbase_1 = require("./duplotrainbase");
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);
}
async _discoveryEventHandler(peripheral) {
const device = new bledevice_1.BLEDevice(peripheral);
let hub;
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)) {
hub = new boostmovehub_1.BoostMoveHub(peripheral, this.autoSubscribe);
hub = new boostmovehub_1.BoostMoveHub(device, this.autoSubscribe);
}
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)) {
hub = new pupremote_1.PUPRemote(peripheral, this.autoSubscribe);
hub = new pupremote_1.PUPRemote(device, this.autoSubscribe);
}
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 {
return;
@ -216,7 +218,7 @@ class PoweredUP extends events_1.EventEmitter {
// if (!isBrowserContext) {
// startScanning();
// }
hub.on("discoverComplete", () => {
device.on("discoverComplete", () => {
hub.on("connect", () => {
debug(`Hub ${hub.uuid} connected`);
this._connectedHubs[hub.uuid] = hub;
@ -284,7 +286,7 @@ exports.PoweredUP = PoweredUP;
<span class="jsdoc-message">
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>.
</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

View File

@ -60,8 +60,6 @@ const trains = [
poweredUP.on("discover", async (hub) => {
if (hub instanceof PoweredUP.PUPRemote) {
await hub.connect();
hub._currentTrain = 2;
hub.on("button", (button, state) => {
if (button === "GREEN") {
@ -71,7 +69,14 @@ poweredUP.on("discover", async (hub) => {
hub._currentTrain = 0;
}
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) {
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);
console.log(`Connected to Powered UP remote (${hub.name})`);
return;
@ -112,12 +119,8 @@ poweredUP.on("discover", async (hub) => {
for (let trainHub in train.hubs) {
trainHub = train.hubs[trainHub];
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) => {
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);
}
});
@ -125,6 +128,10 @@ poweredUP.on("discover", async (hub) => {
console.log(`Disconnected from ${train.name} (${hub.name})`);
delete trainHub._hub;
})
await hub.connect();
trainHub._hub = hub;
hub.setLEDColor(train.color);
console.log(`Connected to ${train.name} (${hub.name})`);
}
}
}

View 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">&nbsp;</div>
</div>
</body>
</html>

View 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;
}

View 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">&nbsp;</div>
</div>
<div>
<table id="hubs">
<thead class="headings"><td>Name</td><td>Type</td></thead>
</table>
</div>
</body>
</html>

View File

@ -10,7 +10,7 @@
"dateFormat": "ddd MMM Do YYYY",
"outputSourceFiles": true,
"outputSourcePath": true,
"systemName": "DocStrap",
"systemName": "node-poweredup",
"footer": "",
"copyright": "node-poweredup by Nathan Kellenicki licensed under the MIT license.",
"navType": "vertical",
@ -25,4 +25,4 @@
"parser": "gfm",
"hardwrap": true
}
}
}

4640
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,39 @@
{
"name": "node-poweredup",
"version": "1.9.1",
"description": "A Node.js module to interface with LEGO Powered UP components.",
"version": "3.5.1",
"description": "A Javascript module to interface with LEGO Powered Up components.",
"homepage": "https://github.com/nathankellenicki/node-poweredup/",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/node/index-node.js",
"types": "dist/node/index-node.d.ts",
"scripts": {
"build": "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",
"all": "npm run build && npm run docs",
"prepublishOnly": "npm run build"
"build:node": "tslint -c tslint.json \"./src/*.ts\" && tsc",
"build:browser": "tslint -c tslint.json \"./src/*.ts\" && webpack --mode=production",
"build:all": "tslint -c tslint.json \"./src/*.ts\" && tsc && webpack --mode=production",
"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>",
"license": "MIT",
"dependencies": {
"compare-versions": "^3.5.0",
"debug": "^4.1.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": {
"@types/debug": "0.0.31",
"@types/noble": "0.0.38",
"@types/node": "^10.12.18",
"@types/debug": "4.1.4",
"@types/noble": "0.0.39",
"@types/node": "^11.13.7",
"@types/web-bluetooth": "0.0.4",
"ink-docstrap": "^1.3.2",
"jsdoc": "^3.5.5",
"jsdoc-to-markdown": "^4.0.1",
"tslint": "^5.12.0",
"typescript": "^3.2.2"
"jsdoc": "^3.6.3",
"jsdoc-to-markdown": "^5.0.0",
"ts-loader": "^5.4.3",
"tslint": "^5.16.0",
"typescript": "^3.4.5",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1"
},
"resolutions": {
"xpc-connection": "sandeepmistry/node-xpc-connection#pull/26/head"

View File

@ -1,3 +1,4 @@
import compareVersion from "compare-versions";
import { Peripheral } from "noble";
import { LPF2Hub } from "./lpf2hub";
@ -6,6 +7,7 @@ import { Port } from "./port";
import * as Consts from "./consts";
import Debug = require("debug");
import { IBLEDevice } from "./interfaces";
const debug = Debug("boostmovehub");
@ -18,30 +20,26 @@ const debug = Debug("boostmovehub");
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) {
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) {
super(peripheral, autoSubscribe);
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
super(device, autoSubscribe);
this.type = Consts.HubType.BOOST_MOVE_HUB;
this._ports = {
"A": new Port("A", 55),
"B": new Port("B", 56),
"AB": new Port("AB", 57),
"TILT": new Port("TILT", 58),
"C": new Port("C", 1),
"D": new Port("D", 2)
"A": new Port("A", 0),
"B": new Port("B", 1),
"C": new Port("C", 2),
"D": new Port("D", 3),
"TILT": new Port("TILT", 58)
};
this.on("attach", (port, type) => {
this._combinePorts(port, type);
});
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) {
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`);
}
let cancelEventTimer = true;
@ -82,10 +80,15 @@ export class BoostMoveHub extends LPF2Hub {
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) {
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 (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]);
} else {
// @ts-ignore: The type of speed is properly checked at the start
@ -111,10 +114,15 @@ export class BoostMoveHub extends LPF2Hub {
} 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;
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]);
} else {
// @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) {
const portObj = this._portLookup(port);
if (!(portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR || portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR)) {
throw new Error("Angle rotation is only available when using a Boost Tacho Motor or 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
)) {
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`);
}
portObj.cancelEventTimer();
return new Promise((resolve, reject) => {
portObj.busy = true;
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]);
} else {
// @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.
* @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.`);
}
}
}

View File

@ -6,6 +6,7 @@
* @property {number} POWERED_UP_HUB 3
* @property {number} POWERED_UP_REMOTE 4
* @property {number} DUPLO_TRAIN_HUB 5
* @property {number} CONTROL_PLUS_HUB 6
*/
export enum HubType {
UNKNOWN = 0,
@ -13,10 +14,19 @@ export enum HubType {
BOOST_MOVE_HUB = 2,
POWERED_UP_HUB = 3,
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
* @property {number} UNKNOWN 0
@ -34,6 +44,8 @@ export enum HubType {
* @property {number} DUPLO_TRAIN_BASE_SPEAKER 42
* @property {number} DUPLO_TRAIN_BASE_COLOR 43
* @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
*/
export enum DeviceType {
@ -52,10 +64,20 @@ export enum DeviceType {
DUPLO_TRAIN_BASE_SPEAKER = 42,
DUPLO_TRAIN_BASE_COLOR = 43,
DUPLO_TRAIN_BASE_SPEEDOMETER = 44,
CONTROL_PLUS_LARGE_MOTOR = 46,
CONTROL_PLUS_XLARGE_MOTOR = 47,
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
* @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
* @property {number} PRESSED 0
@ -122,15 +152,20 @@ export enum DuploTrainBaseSound {
export enum BLEManufacturerData {
DUPLO_TRAIN_HUB_ID = 32,
BOOST_MOVE_HUB_ID = 64,
POWERED_UP_HUB_ID = 65,
POWERED_UP_REMOTE_ID = 66,
DUPLO_TRAIN_HUB_ID = 32
CONTROL_PLUS_LARGE_HUB = 128
}
export enum BLEService {
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"
}

304
src/controlplushub.ts Normal file
View 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();
}
});
}
}

View File

@ -6,6 +6,7 @@ import { Port } from "./port";
import * as Consts from "./consts";
import Debug = require("debug");
import { IBLEDevice } from "./interfaces";
const debug = Debug("duplotrainbase");
@ -18,51 +19,15 @@ const debug = Debug("duplotrainbase");
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) {
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) {
super(peripheral, autoSubscribe);
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
super(device, autoSubscribe);
this.type = Consts.HubType.DUPLO_TRAIN_HUB;
this._ports = {
"MOTOR": new Port("MOTOR", 0),
@ -91,7 +56,7 @@ export class DuploTrainBase extends LPF2Hub {
*/
public setLEDColor (color: number | boolean) {
return new Promise((resolve, reject) => {
if (color === false) {
if (typeof color === "boolean") {
color = 0;
}
const data = Buffer.from([0x81, 0x11, 0x11, 0x51, 0x00, color]);

View File

@ -1,6 +1,6 @@
import { EventEmitter } from "events";
import { Characteristic, Peripheral, Service } from "noble";
import { IBLEDevice, IFirmwareInfo } from "./interfaces";
import { Port } from "./port";
import * as Consts from "./consts";
@ -9,14 +9,6 @@ import Debug = require("debug");
const debug = Debug("hub");
export interface IFirmwareInfo {
major: number;
minor: number;
bugFix: number;
build: number;
}
/**
* @class Hub
* @extends EventEmitter
@ -29,7 +21,7 @@ export class Hub extends EventEmitter {
public type: Consts.HubType = Consts.HubType.UNKNOWN;
protected _ports: {[port: string]: Port} = {};
protected _characteristics: {[uuid: string]: Characteristic} = {};
protected _virtualPorts: {[port: string]: Port} = {};
protected _name: string = "";
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 _current: number = 0;
private _peripheral: Peripheral;
private _uuid: string;
protected _bleDevice: IBLEDevice;
private _rssi: number = -100;
private _isConnecting = false;
private _isConnected = false;
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
super();
this.autoSubscribe = !!autoSubscribe;
this._peripheral = peripheral;
this._uuid = peripheral.uuid;
// 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 = peripheral.advertisement.localName;
this.emit("discoverComplete");
}, 1000);
this._bleDevice = device;
device.on("disconnect", () => {
this.emit("disconnect");
});
}
@ -62,7 +50,7 @@ export class Hub extends EventEmitter {
* @property {string} name Name of the hub
*/
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
*/
public get uuid () {
return this._uuid;
return this._bleDevice.uuid;
}
@ -111,13 +99,13 @@ export class Hub extends EventEmitter {
}
// /**
// * @readonly
// * @property {number} current Current usage of the hub (Amps)
// */
// public get current () {
// return this._current;
// }
/**
* @readonly
* @property {number} current Current usage of the hub (Milliamps)
*/
public get current () {
return this._current;
}
/**
@ -126,69 +114,15 @@ export class Hub extends EventEmitter {
* @returns {Promise} Resolved upon successful connect.
*/
public connect () {
return new Promise((connectResolve, connectReject) => {
const self = this;
if (this._isConnecting) {
return new Promise(async (connectResolve, connectReject) => {
if (this._bleDevice.connecting) {
return connectReject("Already connecting");
} else if (this._isConnected) {
} else if (this._bleDevice.connected) {
return connectReject("Already connected");
}
this._isConnecting = true;
this._peripheral.connect((err: string) => {
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();
});
});
});
await this._bleDevice.connect();
return connectResolve();
});
}
@ -199,12 +133,9 @@ export class Hub extends EventEmitter {
* @method Hub#disconnect
* @returns {Promise} Resolved upon successful disconnect.
*/
public disconnect () {
return new Promise((resolve, reject) => {
this._peripheral.disconnect(() => {
return resolve();
});
});
public async disconnect () {
this.emit("disconnect");
this._bleDevice.disconnect();
}
@ -292,21 +223,21 @@ export class Hub extends EventEmitter {
}
protected _getCharacteristic (uuid: string) {
return this._characteristics[uuid.replace(/-/g, "")];
}
// protected _getCharacteristic (uuid: string) {
// return this._characteristics[uuid.replace(/-/g, "")];
// }
protected _subscribeToCharacteristic (characteristic: Characteristic, callback: (data: Buffer) => void) {
characteristic.on("data", (data: Buffer) => {
return callback(data);
});
characteristic.subscribe((err) => {
if (err) {
this.emit("error", err);
}
});
}
// protected _subscribeToCharacteristic (characteristic: Characteristic, callback: (data: Buffer) => void) {
// characteristic.on("data", (data: Buffer) => {
// return callback(data);
// });
// characteristic.subscribe((err) => {
// if (err) {
// this.emit("error", err);
// }
// });
// }
protected _activatePortDevice (port: number, type: number, mode: number, format: number, callback?: () => void) {
@ -345,6 +276,9 @@ export class Hub extends EventEmitter {
* @event Hub#detach
* @param {string} port
*/
if (this._virtualPorts[port.id]) {
delete this._virtualPorts[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;
}
@ -415,10 +355,10 @@ export class Hub extends EventEmitter {
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`);
}
return this._ports[port];
return this._ports[port] || this._virtualPorts[port];
}
@ -440,6 +380,10 @@ export class Hub extends EventEmitter {
return 0x02;
case Consts.DeviceType.BOOST_MOVE_HUB_MOTOR:
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:
return (this.type === Consts.HubType.WEDO2_SMART_HUB ? 0x00 : 0x08);
case Consts.DeviceType.BOOST_TILT:

15
src/index-browser.ts Normal file
View 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 };

View File

@ -1,11 +1,15 @@
import { BoostMoveHub } from "./boostmovehub";
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";
import { PoweredUP } from "./poweredup-node";
import { PUPHub } from "./puphub";
import { PUPRemote } from "./pupremote";
import { WeDo2SmartHub } from "./wedo2smarthub";
import { isWebBluetooth } from "./utils";
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
View 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;
}

View File

@ -7,6 +7,7 @@ import * as Consts from "./consts";
import Debug = require("debug");
const debug = Debug("lpf2hub");
const modeInfoDebug = Debug("lpf2hubmodeinfo");
/**
@ -17,6 +18,7 @@ export class LPF2Hub extends Hub {
private _lastTiltX: number = 0;
private _lastTiltY: number = 0;
private _lastTiltZ: number = 0;
private _messageBuffer: Buffer = Buffer.alloc(0);
@ -24,19 +26,21 @@ export class LPF2Hub extends Hub {
public connect () {
return new Promise(async (resolve, reject) => {
await super.connect();
const characteristic = this._getCharacteristic(Consts.BLECharacteristic.LPF2_ALL);
this._subscribeToCharacteristic(characteristic, this._parseMessage.bind(this));
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.LPF2_HUB);
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.LPF2_ALL, this._parseMessage.bind(this));
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, 0x06, 0x02])); // Activate battery level reports
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x3c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])); // Activate voltage reports
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x3b, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])); // Activate current reports
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.emit("connect");
resolve();
setTimeout(() => {
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, 0x06, 0x02])); // Activate battery level reports
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x3c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])); // Activate voltage reports
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x3b, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])); // Activate current reports
if (this.type === Consts.HubType.DUPLO_TRAIN_HUB) {
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, Buffer.from([0x41, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01]));
}
}, 1000);
return resolve();
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) => {
let data = Buffer.from([0x41, 0x32, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
if (color === false) {
if (typeof color === "boolean") {
color = 0;
}
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) {
const characteristic = this._getCharacteristic(uuid);
if (characteristic) {
message = Buffer.concat([Buffer.alloc(2), message]);
message[0] = message.length;
debug("Sent Message (LPF2_ALL)", message);
characteristic.write(message, false, callback);
message = Buffer.concat([Buffer.alloc(2), message]);
message[0] = message.length;
debug("Sent Message (LPF2_ALL)", message);
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);
break;
}
case 0x43: {
this._parsePortInformationResponse(message);
break;
}
case 0x44: {
this._parseModeInformationResponse(message);
break;
}
case 0x45: {
this._parseSensorMessage(message);
break;
@ -216,6 +246,7 @@ export class LPF2Hub extends Hub {
const major = data.readUInt8(8) >>> 4;
const minor = data.readUInt8(8) & 0xf;
this._firmwareInfo = { major, minor, bugFix, build };
this._checkFirmware(this.firmwareVersion);
// Battery level reports
} else if (data[3] === 0x06) {
@ -227,18 +258,90 @@ export class LPF2Hub extends Hub {
private _parsePortMessage (data: Buffer) {
const port = this._getPortForPortNumber(data[3]);
let port = this._getPortForPortNumber(data[3]);
if (!port) {
return;
if (data[4] === 0x01) {
this._sendPortInformationRequest(data[3]);
}
port.connected = (data[4] === 1 || data[4] === 2) ? true : false;
this._registerDeviceAttachment(port, data[5]);
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]);
} 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) {
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)
data = this._padMessage(data, 6);
const voltage = data.readUInt16LE(4) / 500;
this._voltage = voltage;
const voltage = data.readUInt16LE(4);
this._voltage = 6400.0 * voltage / 3200.0 / 1000.0;
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);
const current = data.readUInt16LE(4);
this._current = current;
this._current = 2444 * current / 4095.0;
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;
this._voltage = voltage;
}
if ((data[3] === 0x62 && this.type === Consts.HubType.CONTROL_PLUS_HUB)) { // Control+ Accelerometer
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;
} 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;
this._current = current;
}
if ((data[3] === 0x63 && this.type === Consts.HubType.CONTROL_PLUS_HUB)) { // Control+ Accelerometer
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;
}
@ -355,11 +503,12 @@ export class LPF2Hub extends Hub {
/**
* Emits when a tilt sensor is activated.
* @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} 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;
}
case Consts.DeviceType.BOOST_TACHO_MOTOR: {
@ -378,10 +527,22 @@ export class LPF2Hub extends Hub {
this.emit("rotate", port.id, rotation);
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: {
const tiltX = data[4] > 160 ? data[4] - 255 : data[4];
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;
}
case Consts.DeviceType.POWERED_UP_REMOTE_BUTTON: {

149
src/nobledevice.ts Normal file
View 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
View 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);
});
}
}

View File

@ -1,14 +1,14 @@
import { Peripheral } from "noble-mac";
import { BoostMoveHub } from "./boostmovehub";
import { ControlPlusHub } from "./controlplushub";
import { DuploTrainBase } from "./duplotrainbase";
import { Hub } from "./hub";
import { NobleDevice } from "./nobledevice";
import { PUPHub } from "./puphub";
import { PUPRemote } from "./pupremote";
import { WeDo2SmartHub } from "./wedo2smarthub";
import { isBrowserContext } from "./utils";
import * as Consts from "./consts";
import { EventEmitter } from "events";
@ -22,11 +22,7 @@ let wantScan = false;
let discoveryEventAttached = false;
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) => {
@ -64,7 +60,7 @@ export class PoweredUP extends EventEmitter {
* Begin scanning for Powered UP Hub devices.
* @method PoweredUP#scan
*/
public scan () {
public async scan () {
wantScan = true;
if (!discoveryEventAttached) {
@ -76,6 +72,8 @@ export class PoweredUP extends EventEmitter {
debug("Scanning started");
startScanning();
}
return true;
}
@ -129,29 +127,28 @@ export class PoweredUP extends EventEmitter {
private async _discoveryEventHandler (peripheral: Peripheral) {
peripheral.removeAllListeners();
const device = new NobleDevice(peripheral);
let hub: Hub;
if (await WeDo2SmartHub.IsWeDo2SmartHub(peripheral)) {
hub = new WeDo2SmartHub(peripheral, this.autoSubscribe);
hub = new WeDo2SmartHub(device, this.autoSubscribe);
} else if (await BoostMoveHub.IsBoostMoveHub(peripheral)) {
hub = new BoostMoveHub(peripheral, this.autoSubscribe);
hub = new BoostMoveHub(device, this.autoSubscribe);
} else if (await PUPHub.IsPUPHub(peripheral)) {
hub = new PUPHub(peripheral, this.autoSubscribe);
hub = new PUPHub(device, this.autoSubscribe);
} else if (await PUPRemote.IsPUPRemote(peripheral)) {
hub = new PUPRemote(peripheral, this.autoSubscribe);
hub = new PUPRemote(device, this.autoSubscribe);
} 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 {
return;
}
peripheral.removeAllListeners();
// noble.stopScanning();
// if (!isBrowserContext) {
// startScanning();
// }
hub.on("discoverComplete", () => {
device.on("discoverComplete", () => {
hub.on("connect", () => {
debug(`Hub ${hub.uuid} connected`);
@ -172,7 +169,7 @@ export class PoweredUP extends EventEmitter {
/**
* Emits when a Powered UP Hub device is found.
* @event PoweredUP#discover
* @param {WeDo2SmartHub | BoostMoveHub | PUPHub | PUPRemote | DuploTrainBase} hub
* @param {WeDo2SmartHub | BoostMoveHub | ControlPlusHub | PUPHub | PUPRemote | DuploTrainBase} hub
*/
this.emit("discover", hub);

View File

@ -1,3 +1,4 @@
import compareVersion from "compare-versions";
import { Peripheral } from "noble";
import { LPF2Hub } from "./lpf2hub";
@ -6,6 +7,7 @@ import { Port } from "./port";
import * as Consts from "./consts";
import Debug = require("debug");
import { IBLEDevice } from "./interfaces";
const debug = Debug("puphub");
@ -18,32 +20,23 @@ const debug = Debug("puphub");
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) {
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) {
super(peripheral, autoSubscribe);
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
super(device, autoSubscribe);
this.type = Consts.HubType.POWERED_UP_HUB;
this._ports = {
"A": new Port("A", 0),
"B": new Port("B", 1),
"AB": new Port("AB", 57)
"B": new Port("B", 1)
};
this.on("attach", (port, type) => {
this._combinePorts(port, type);
});
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) {
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`);
}
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;
if (typeof time === "boolean") {
if (time === true) {
@ -90,35 +76,60 @@ export class PUPHub extends LPF2Hub {
}
return new Promise((resolve, reject) => {
if (time && typeof time === "number") {
let data = null;
if (portObj.id === "AB") {
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed)]);
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
data = Buffer.from([0x81, portObj.value, 0x11, 0x60, 0x00, this._mapSpeed(speed), 0x00, 0x00]);
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);
}
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
const timeout = global.setTimeout(() => {
} else {
if (portObj.type === Consts.DeviceType.BOOST_TACHO_MOTOR || portObj.type === Consts.DeviceType.BOOST_MOVE_HUB_MOTOR) {
portObj.busy = true;
let data = null;
if (portObj.id === "AB") {
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, 0x00, 0x00]);
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 {
data = Buffer.from([0x81, portObj.value, 0x11, 0x60, 0x00, 0x00, 0x00, 0x00]);
// @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);
return resolve();
}, time);
portObj.setEventTimer(timeout);
} else {
let data = null;
if (portObj.id === "AB") {
data = Buffer.from([0x81, portObj.value, 0x11, 0x02, this._mapSpeed(speed instanceof Array ? speed[0] : speed), this._mapSpeed(speed instanceof Array ? speed[1] : speed)]);
portObj.finished = () => {
return resolve();
};
} else {
// @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]);
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);
return resolve();
}
});
}
@ -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.
* @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.`);
}
}
}

View File

@ -6,6 +6,7 @@ import { Port } from "./port";
import * as Consts from "./consts";
import Debug = require("debug");
import { IBLEDevice } from "./interfaces";
const debug = Debug("pupremote");
@ -18,51 +19,15 @@ const debug = Debug("pupremote");
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) {
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) {
super(peripheral, autoSubscribe);
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
super(device, autoSubscribe);
this.type = Consts.HubType.POWERED_UP_REMOTE;
this._ports = {
"LEFT": new Port("LEFT", 0),
@ -92,7 +57,7 @@ export class PUPRemote extends LPF2Hub {
return new Promise((resolve, reject) => {
let data = Buffer.from([0x41, 0x34, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
this._writeMessage(Consts.BLECharacteristic.LPF2_ALL, data);
if (color === false) {
if (typeof color === "boolean") {
color = 0;
}
data = Buffer.from([0x81, 0x34, 0x11, 0x51, 0x00, color]);

View File

@ -1,2 +1,2 @@
// @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
View 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, "");
}
}

View File

@ -6,6 +6,8 @@ import { Port } from "./port";
import * as Consts from "./consts";
import Debug = require("debug");
import { IBLEDevice } from "./interfaces";
import { isWebBluetooth } from "./utils";
const debug = Debug("wedo2smarthub");
@ -17,16 +19,10 @@ const debug = Debug("wedo2smarthub");
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) {
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;
constructor (peripheral: Peripheral, autoSubscribe: boolean = true) {
super(peripheral, autoSubscribe);
constructor (device: IBLEDevice, autoSubscribe: boolean = true) {
super(device, autoSubscribe);
this.type = Consts.HubType.WEDO2_SMART_HUB;
this._ports = {
"A": new Port("A", 1),
@ -49,23 +45,53 @@ export class WeDo2SmartHub extends Hub {
return new Promise(async (resolve, reject) => {
debug("Connecting to WeDo 2.0 Smart Hub");
await super.connect();
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_PORT_TYPE), this._parsePortMessage.bind(this));
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_SENSOR_VALUE), this._parseSensorMessage.bind(this));
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_BUTTON), this._parseSensorMessage.bind(this));
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY), this._parseBatteryMessage.bind(this));
this._subscribeToCharacteristic(this._getCharacteristic(Consts.BLECharacteristic.WEDO2_HIGH_CURRENT_ALERT), this._parseHighCurrentAlert.bind(this));
this._getCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY).read((err, data) => {
this._parseBatteryMessage(data);
});
this._getCharacteristic(Consts.BLECharacteristic.WEDO2_FIRMWARE_REVISION).read((err, data) => {
this._parseFirmwareRevisionString(data);
});
setTimeout(() => {
this._activatePortDevice(0x03, 0x15, 0x00, 0x00); // Activate voltage reports
this._activatePortDevice(0x04, 0x14, 0x00, 0x00); // Activate current reports
}, 1000);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_2);
if (!isWebBluetooth) {
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_3);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_4);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_5);
} else {
await this._bleDevice.discoverCharacteristicsForService("battery_service");
await this._bleDevice.discoverCharacteristicsForService("device_information");
}
this._activatePortDevice(0x03, 0x15, 0x00, 0x00); // Activate voltage reports
this._activatePortDevice(0x04, 0x14, 0x00, 0x00); // Activate current reports
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) => {
let data = Buffer.from([0x06, 0x17, 0x01, 0x01]);
this._writeMessage(Consts.BLECharacteristic.WEDO2_PORT_TYPE_WRITE, data);
if (color === false) {
if (typeof color === "boolean") {
color = 0;
}
data = Buffer.from([0x06, 0x04, 0x01, color]);
@ -278,13 +304,10 @@ export class WeDo2SmartHub extends Hub {
private _writeMessage (uuid: string, message: Buffer, callback?: () => void) {
const characteristic = this._getCharacteristic(uuid);
if (characteristic) {
if (debug.enabled) {
debug(`Sent Message (${this._getCharacteristicNameFromUUID(uuid)})`, message);
}
characteristic.write(message, false, callback);
if (debug.enabled) {
debug(`Sent Message (${this._getCharacteristicNameFromUUID(uuid)})`, message);
}
this._bleDevice.writeToCharacteristic(uuid, message, callback);
}
@ -302,7 +325,6 @@ export class WeDo2SmartHub extends Hub {
private _parseHighCurrentAlert (data: Buffer) {
debug("Received Message (WEDO2_HIGH_CURRENT_ALERT)", data);
// console.log(data);
}
@ -424,6 +446,17 @@ export class WeDo2SmartHub extends Hub {
* @param {number} 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;
}
}
}

View File

@ -9,9 +9,9 @@
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates 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. */
"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. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */

73
webble_test.html Normal file
View 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
View 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")
}
};