From a02249f6968edf509cfd6bf52f4da52a62100ff1 Mon Sep 17 00:00:00 2001 From: Nathan Kellenicki Date: Sun, 8 Dec 2019 22:57:44 -0800 Subject: [PATCH] Implemented basic lights, device busy-ness --- examples/new_device_test.js | 22 +++++++++-- examples/winter_village_train.js | 60 +++++++++++++++++++++++++++++ src/basicmotor.ts | 10 ++--- src/boostmovehub.ts | 4 +- src/controlplushub.ts | 4 +- src/device.ts | 12 +++++- src/lights.ts | 29 ++++++++++++++ src/lpf2hub.ts | 65 ++++++++++++++++---------------- src/puphub.ts | 4 +- src/pupremote.ts | 4 +- src/tachomotor.ts | 11 ++++-- src/utils.ts | 9 +++++ src/wedo2smarthub.ts | 15 +++++--- 13 files changed, 189 insertions(+), 60 deletions(-) create mode 100644 examples/winter_village_train.js create mode 100644 src/lights.ts diff --git a/examples/new_device_test.js b/examples/new_device_test.js index 3eb8cf8..c291333 100644 --- a/examples/new_device_test.js +++ b/examples/new_device_test.js @@ -16,22 +16,36 @@ poweredUP.on("discover", async (hub) => { // Wait to discover hubs await hub.connect(); // Connect to hub console.log(`Connected to ${hub.name}!`); - hub.on("attach", (device) => { + hub.on("attach", async (device) => { + + console.log(`Attached device ${device.type} to ${device.port}`) if (device instanceof PoweredUP.ControlPlusLargeMotor) { const motor = device; - motor.setSpeed(30); + + motor.on("rotate", (angle) => { + console.log(`Rotate ${angle}`); + }); + + await motor.rotateByAngle(9000, 50); + await motor.rotateByAngle(9000, -50); + await motor.rotateByAngle(9000, 50); + await motor.rotateByAngle(9000, -50); + motor.power(100); } if (device instanceof PoweredUP.ColorDistanceSensor) { const sensor = device; sensor.on("distance", (distance) => { // Adding an event handler for distance automatically subscribes to distance notifications console.log(`Distance ${distance}`); - }) + }); + sensor.on("color", (color) => { + console.log(`Color ${color}`); + }); } device.on("detach", () => { - console.log(device.connected); + console.log(`Detached device ${device.type} from ${device.port}`) }) }); diff --git a/examples/winter_village_train.js b/examples/winter_village_train.js new file mode 100644 index 0000000..7e27f40 --- /dev/null +++ b/examples/winter_village_train.js @@ -0,0 +1,60 @@ +/* + * + * This runs the train under our Christmas tree. It uses the 10254 Winter Holiday Train retrofitted with a Powered UP hub and train motor. + * It also uses a WeDo 2.0 hub with Powered UP distance sensor to detect the train approaching the station and slow it down. + * + * Note that if you want to use this yourself you don't need to use a WeDo 2.0 hub, you can use any hub that can accept a distance or color/distance sensor. + * + * The maximum speed of the train is set to a constant 50. A further improvement can be made by scaling the speed up according to the current battery voltage, + * so the speed doesn't slow as the battery starts dying. + * + */ + +const PoweredUP = require(".."); + +const poweredUP = new PoweredUP.PoweredUP(); +poweredUP.scan(); // Start scanning for hubs + +console.log("Looking for Hubs..."); + +let train = null; +let sensor = null; + +let ramping = false; + +poweredUP.on("discover", async (hub) => { // Wait to discover hubs + + if (hub.name === "NK_Winter_Train") { + await hub.connect(); // Connect to hub + console.log(`Connected to train!`); + train = hub; + } else if (hub.name === "NK_Winter_Sensor") { + await hub.connect(); // Connect to hub + console.log(`Connected to sensor!`); + sensor = hub; + + sensor.on("distance", (_, distance) => { + if (distance < 5 && !ramping) { + await stopTrain(); + } + }); + + } + + if (train && sensor) { + console.log("Train and sensor connected, starting!"); + await startTrain(); + } + +}); + +const startTrain = async () => { + ramping = true; + await train.rampMotorSpeed("A", 0, 50, 2000); + ramping = false; +} + +const stopTrain = async () => { + ramping = true; + await train.rampMotorSpeed("A", 50, 0, 2000); +} \ No newline at end of file diff --git a/src/basicmotor.ts b/src/basicmotor.ts index b0b3a93..4330416 100644 --- a/src/basicmotor.ts +++ b/src/basicmotor.ts @@ -15,13 +15,13 @@ export class BasicMotor extends Device { /** * Set the motor speed. - * @method BasicMotor#setSpeed - * @param {number} speed For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0. + * @method BasicMotor#power + * @param {number} power For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0. * @returns {Promise} Resolved upon successful completion of command. */ - public setSpeed (speed: number) { + public power (power: number) { return new Promise((resolve) => { - const data = Buffer.from([0x81, this.portId, 0x11, 0x51, 0x00, mapSpeed(speed)]); + const data = Buffer.from([0x81, this.portId, 0x11, 0x51, 0x00, mapSpeed(power)]); this.send(data); return resolve(); }); @@ -34,7 +34,7 @@ export class BasicMotor extends Device { * @returns {Promise} Resolved upon successful completion of command. */ public brake () { - return this.setSpeed(127); + return this.power(127); } diff --git a/src/boostmovehub.ts b/src/boostmovehub.ts index e363eb5..e36eee3 100644 --- a/src/boostmovehub.ts +++ b/src/boostmovehub.ts @@ -1,13 +1,13 @@ import { Peripheral } from "@abandonware/noble"; import compareVersion from "compare-versions"; +import { IBLEAbstraction } from "./interfaces"; + import { LPF2Hub } from "./lpf2hub"; -import { Port } from "./port"; import * as Consts from "./consts"; import Debug = require("debug"); -import { IBLEAbstraction } from "./interfaces"; const debug = Debug("boostmovehub"); diff --git a/src/controlplushub.ts b/src/controlplushub.ts index 4d0a7b9..8d8cdaf 100644 --- a/src/controlplushub.ts +++ b/src/controlplushub.ts @@ -1,12 +1,12 @@ import { Peripheral } from "@abandonware/noble"; +import { IBLEAbstraction } from "./interfaces"; + import { LPF2Hub } from "./lpf2hub"; -import { Port } from "./port"; import * as Consts from "./consts"; import Debug = require("debug"); -import { IBLEAbstraction } from "./interfaces"; const debug = Debug("ControlPlusHub"); diff --git a/src/device.ts b/src/device.ts index bbf8760..b49e529 100644 --- a/src/device.ts +++ b/src/device.ts @@ -8,13 +8,13 @@ export class Device extends EventEmitter { public autoSubscribe: boolean = true; protected _mode: number = 0x00; + protected _busy: boolean = false; + protected _finished: (() => void) | undefined; private _hub: Hub; private _portId: number; private _connected: boolean = true; private _type: number; - private _busy: boolean = false; - private _finished: (() => void) | null = null; constructor (hub: Hub, portId: number, type: number = Consts.DeviceType.UNKNOWN) { super(); @@ -69,4 +69,12 @@ export class Device extends EventEmitter { this.emit("receive", message); } + public finish () { + this._busy = false; + if (this._finished) { + this._finished(); + this._finished = undefined; + } + } + } diff --git a/src/lights.ts b/src/lights.ts new file mode 100644 index 0000000..411d864 --- /dev/null +++ b/src/lights.ts @@ -0,0 +1,29 @@ +import { Device } from "./device"; +import { Hub } from "./hub"; + +import * as Consts from "./consts"; + +export class Lights extends Device { + + + constructor (hub: Hub, portId: number) { + super(hub, portId, Consts.DeviceType.LED_LIGHTS); + } + + + /** + * Set the light brightness. + * @method Light#brightness + * @param {number} brightness Brightness value between 0-100 (0 is off) + * @returns {Promise} Resolved upon successful completion of command. + */ + public brightness (brightness: number) { + return new Promise((resolve) => { + const data = Buffer.from([0x81, this.portId, 0x11, 0x51, 0x00, brightness]); + this.send(data); + return resolve(); + }); + } + + +} diff --git a/src/lpf2hub.ts b/src/lpf2hub.ts index d945222..8f569ee 100644 --- a/src/lpf2hub.ts +++ b/src/lpf2hub.ts @@ -1,12 +1,13 @@ import { Device } from "./device"; import { Hub } from "./hub"; -import { Port } from "./port"; import { ColorDistanceSensor } from "./colordistancesensor"; import { ControlPlusLargeMotor } from "./controlpluslargemotor"; +import { Lights } from "./lights"; import * as Consts from "./consts"; -import { toBin, toHex } from "./utils"; + +import { decodeMACAddress, decodeVersion, toBin, toHex } from "./utils"; import Debug = require("debug"); const debug = Debug("lpf2hub"); @@ -18,14 +19,6 @@ const modeInfoDebug = Debug("lpf2hubmodeinfo"); * @extends Hub */ export class LPF2Hub extends Hub { - private static decodeVersion(v: number) { - const t = v.toString(16).padStart(8, "0"); - return [t[0], t[1], t.substring(2, 4), t.substring(4)].join("."); - } - - private static decodeMACAddress(v: Uint8Array) { - return Array.from(v).map((n) => toHex(n, 2)).join(":"); - } protected _ledPort: number = 0x32; protected _voltagePort: number | undefined; @@ -265,12 +258,12 @@ export class LPF2Hub extends Hub { // Firmware version } else if (data[3] === 0x03) { - this._firmwareVersion = LPF2Hub.decodeVersion(data.readInt32LE(5)); + this._firmwareVersion = decodeVersion(data.readInt32LE(5)); this._checkFirmware(this._firmwareVersion); // Hardware version } else if (data[3] === 0x04) { - this._hardwareVersion = LPF2Hub.decodeVersion(data.readInt32LE(5)); + this._hardwareVersion = decodeVersion(data.readInt32LE(5)); // RSSI update } else if (data[3] === 0x05) { @@ -282,7 +275,7 @@ export class LPF2Hub extends Hub { // primary MAC Address } else if (data[3] === 0x0d) { - this._primaryMACAddress = LPF2Hub.decodeMACAddress(data.slice(5)); + this._primaryMACAddress = decodeMACAddress(data.slice(5)); // Battery level reports } else if (data[3] === 0x06) { @@ -291,11 +284,11 @@ export class LPF2Hub extends Hub { } - private _parsePortMessage (data: Buffer) { + private _parsePortMessage (message: Buffer) { - const portId = data[3]; - const event = data[4]; - const deviceType = event ? data.readUInt16LE(5) : 0; + const portId = message[3]; + const event = message[4]; + const deviceType = event ? message.readUInt16LE(5) : 0; // Handle device attachments if (event === 0x01) { @@ -303,6 +296,9 @@ export class LPF2Hub extends Hub { let device; switch (deviceType) { + case Consts.DeviceType.LED_LIGHTS: + device = new Lights(this, portId); + break; case Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR: device = new ControlPlusLargeMotor(this, portId); break; @@ -314,6 +310,15 @@ export class LPF2Hub extends Hub { break; } + if (modeInfoDebug.enabled) { + const deviceTypeName = Consts.DeviceTypeNames[message[5]] || "Unknown"; + modeInfoDebug(`Port ${toHex(portId)}, type ${toHex(deviceType, 4)} (${deviceTypeName})`); + const hwVersion = decodeVersion(message.readInt32LE(7)); + const swVersion = decodeVersion(message.readInt32LE(11)); + modeInfoDebug(`Port ${toHex(portId)}, hardware version ${hwVersion}, software version ${swVersion}`); + this._sendPortInformationRequest(portId); + } + this._attachDevice(device); // Handle device detachments @@ -324,19 +329,8 @@ export class LPF2Hub extends Hub { } } - - // let port = this._getPortForPortNumber(data[3]); - // if (data[4] === 0x01 && modeInfoDebug.enabled) { - // const typeName = Consts.DeviceTypeNames[data[5]] || "unknown"; - // modeInfoDebug(`Port ${toHex(data[3])}, type ${toHex(deviceType, 4)} (${typeName})`); - // const hwVersion = LPF2Hub.decodeVersion(data.readInt32LE(7)); - // const swVersion = LPF2Hub.decodeVersion(data.readInt32LE(11)); - // modeInfoDebug(`Port ${toHex(data[3])}, hardware version ${hwVersion}, software version ${swVersion}`); - // this._sendPortInformationRequest(data[3]); - // } - // if (!port) { // if (data[4] === 0x02) { // const portA = this._getPortForPortNumber(data[7]); @@ -356,9 +350,6 @@ export class LPF2Hub extends Hub { // } else { // return; // } - // } else { - // port.connected = (data[4] === 0x01 || data[4] === 0x02) ? true : false; - // this._registerDeviceAttachment(port, deviceType); // } } @@ -431,7 +422,17 @@ export class LPF2Hub extends Hub { } - private _parsePortAction (data: Buffer) { + private _parsePortAction (message: Buffer) { + + const portId = message[3]; + const device = this._getDeviceByPortId(portId); + + if (device) { + const finished = (message[4] === 0x0a); + if (finished) { + device.finish(); + } + } // const port = this._getPortForPortNumber(data[3]); diff --git a/src/puphub.ts b/src/puphub.ts index 8902bf9..4c91643 100644 --- a/src/puphub.ts +++ b/src/puphub.ts @@ -1,13 +1,13 @@ import { Peripheral } from "@abandonware/noble"; import compareVersion from "compare-versions"; +import { IBLEAbstraction } from "./interfaces"; + import { LPF2Hub } from "./lpf2hub"; -import { Port } from "./port"; import * as Consts from "./consts"; import Debug = require("debug"); -import { IBLEAbstraction } from "./interfaces"; const debug = Debug("puphub"); diff --git a/src/pupremote.ts b/src/pupremote.ts index 2a25bb1..3f8b372 100644 --- a/src/pupremote.ts +++ b/src/pupremote.ts @@ -1,12 +1,12 @@ import { Peripheral } from "@abandonware/noble"; +import { IBLEAbstraction } from "./interfaces"; + import { LPF2Hub } from "./lpf2hub"; -import { Port } from "./port"; import * as Consts from "./consts"; import Debug = require("debug"); -import { IBLEAbstraction } from "./interfaces"; const debug = Debug("pupremote"); diff --git a/src/tachomotor.ts b/src/tachomotor.ts index 32240cd..624a1e7 100644 --- a/src/tachomotor.ts +++ b/src/tachomotor.ts @@ -40,15 +40,18 @@ export class TachoMotor extends BasicMotor { * Rotate a motor by a given angle. * @method TachoMotor#setMotorAngle * @param {number} angle How much the motor should be rotated (in degrees). - * @param {number} [speed=100] For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. + * @param {number} [power=100] For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. * @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished). */ - public rotateByAngle (port: string, angle: number, speed: number = 100) { + public rotateByAngle (angle: number, power: number = 100) { return new Promise((resolve) => { - const message = Buffer.from([0x81, this.portId, 0x11, 0x0b, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed), 0x64, 0x7f, 0x03]); + this._busy = true; + const message = Buffer.from([0x81, this.portId, 0x11, 0x0b, 0x00, 0x00, 0x00, 0x00, mapSpeed(power), 0x64, 0x7f, 0x03]); message.writeUInt32LE(angle, 4); this.send(message); - return resolve(); + this._finished = () => { + return resolve(); + }; }); } diff --git a/src/utils.ts b/src/utils.ts index e463214..4bff8d5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,3 +20,12 @@ export const mapSpeed = (speed: number) => { } return speed; }; + +export const decodeVersion = (version: number) => { + const parts = version.toString(16).padStart(8, "0"); + return [parts[0], parts[1], parts.substring(2, 4), parts.substring(4)].join("."); +}; + +export const decodeMACAddress = (address: Uint8Array) => { + return Array.from(address).map((part) => toHex(part, 2)).join(":"); +}; diff --git a/src/wedo2smarthub.ts b/src/wedo2smarthub.ts index 4142e36..bd6ba7e 100644 --- a/src/wedo2smarthub.ts +++ b/src/wedo2smarthub.ts @@ -1,17 +1,19 @@ import { Peripheral } from "@abandonware/noble"; +import { IBLEAbstraction } from "./interfaces"; + import { Device } from "./device"; import { Hub } from "./hub"; -import { Port } from "./port"; import { ColorDistanceSensor } from "./colordistancesensor"; import { ControlPlusLargeMotor } from "./controlpluslargemotor"; +import { Lights } from "./lights"; import * as Consts from "./consts"; -import Debug = require("debug"); -import { IBLEAbstraction } from "./interfaces"; import { isWebBluetooth } from "./utils"; + +import Debug = require("debug"); const debug = Debug("wedo2smarthub"); @@ -350,6 +352,9 @@ export class WeDo2SmartHub extends Hub { let device; switch (deviceType) { + case Consts.DeviceType.LED_LIGHTS: + device = new Lights(this, portId); + break; case Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR: device = new ControlPlusLargeMotor(this, portId); break; @@ -357,9 +362,9 @@ export class WeDo2SmartHub extends Hub { device = new ColorDistanceSensor(this, portId); break; default: - device = new Device(this, portId); + device = new Device(this, portId, deviceType); break; - } + } this._attachDevice(device);