diff --git a/src/consts.ts b/src/consts.ts index 268caa8..2412567 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -703,3 +703,27 @@ export enum MarioColor { BROWN = 0x6a00, CYAN = 0x4201, } + +/** + * @typedef CommandFeedback + * @param {number} TRANSMISSION_PENDING 0x00 waiting for previous commands to complete transmission or execution + * @param {number} TRANSMISSION_BUSY 0x10 waiting for device to acknowledge reception + * @param {number} TRANSMISSION_DISCARDED 0x44 interrupt command has been recieved or device disconnected + * @param {number} EXECUTION_PENDING 0x20 device is waiting for previous command to complete + * @param {number} EXECUTION_BUSY 0x21 device is executing the command + * @param {number} EXECUTION_DISCARDED 0x24 device discarded the command e.g. due to interrupt + * @param {number} EXECUTION_COMPLETED 0x22 device reported successful completion of command + * @param {number} FEEDBACK_MISSING 0x66 device disconnected or failed to report feedback + * @param {number} FEEDBACK_DISABLED 0x26 feedback not implemented for this command + */ +export enum CommandFeedback { + TRANSMISSION_PENDING = 0x00, + TRANSMISSION_BUSY = 0x10, + TRANSMISSION_DISCARDED = 0x44, + EXECUTION_PENDING = 0x20, + EXECUTION_BUSY = 0x21, + EXECUTION_DISCARDED = 0x24, + EXECUTION_COMPLETED = 0x22, + FEEDBACK_MISSING = 0x66, + FEEDBACK_DISABLED = 0x26, +} diff --git a/src/devices/absolutemotor.ts b/src/devices/absolutemotor.ts index 4ed895d..eb19e56 100644 --- a/src/devices/absolutemotor.ts +++ b/src/devices/absolutemotor.ts @@ -40,34 +40,29 @@ export class AbsoluteMotor extends TachoMotor { * @method AbsoluteMotor#gotoAngle * @param {number} angle Absolute position the motor should go to (degrees from 0). * @param {number} [speed=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). + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (i.e. once the motor is finished). */ - public gotoAngle (angle: [number, number] | number, speed: number = 100) { + public gotoAngle (angle: [number, number] | number, speed: number = 100, interrupt: boolean = false) { if (!this.isVirtualPort && angle instanceof Array) { throw new Error("Only virtual ports can accept multiple positions"); } if (this.isWeDo2SmartHub) { throw new Error("Absolute positioning is not available on the WeDo 2.0 Smart Hub"); } - this.cancelEventTimer(); - return new Promise((resolve) => { - if (speed === undefined || speed === null) { - speed = 100; - } - let message; - if (angle instanceof Array) { - message = Buffer.from([0x81, this.portId, 0x11, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); - message.writeInt32LE(normalizeAngle(angle[0]), 4); - message.writeInt32LE(normalizeAngle(angle[1]), 8); - } else { - message = Buffer.from([0x81, this.portId, 0x11, 0x0d, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); - message.writeInt32LE(normalizeAngle(angle), 4); - } - this.send(message); - this._finishedCallbacks.push(() => { - return resolve(); - }); - }); + if (speed === undefined || speed === null) { + speed = 100; + } + let message; + if (angle instanceof Array) { + message = Buffer.from([0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); + message.writeInt32LE(normalizeAngle(angle[0]), 1); + message.writeInt32LE(normalizeAngle(angle[1]), 5); + } else { + message = Buffer.from([0x0d, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); + message.writeInt32LE(normalizeAngle(angle), 1); + } + return this.sendPortOutputCommand(message, interrupt); } @@ -77,10 +72,10 @@ export class AbsoluteMotor extends TachoMotor { * Real zero is marked on Technic angular motors (SPIKE Prime). It is also available on Technic linear motors (Control+) but is unmarked. * @method AbsoluteMotor#gotoRealZero * @param {number} [speed=100] Speed between 1 - 100. Note that this will always take the shortest path to zero. - * @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished). + * @returns {Promise} Resolved upon completion of command (i.e. once the motor is finished). */ public gotoRealZero (speed: number = 100) { - return new Promise((resolve) => { + return new Promise((resolve) => { const oldMode = this.mode; let calibrated = false; this.on("absolute", async ({ angle }) => { @@ -95,7 +90,7 @@ export class AbsoluteMotor extends TachoMotor { if (oldMode) { this.subscribe(oldMode); } - return resolve(); + return resolve(Consts.CommandFeedback.FEEDBACK_DISABLED); } }); this.requestUpdate(); @@ -106,14 +101,12 @@ export class AbsoluteMotor extends TachoMotor { /** * Reset zero to current position * @method AbsoluteMotor#resetZero - * @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished). + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (i.e. once the motor is finished). */ - public resetZero () { - return new Promise((resolve) => { - const data = Buffer.from([0x81, this.portId, 0x11, 0x51, 0x02, 0x00, 0x00, 0x00, 0x00]); - this.send(data); - return resolve(); - }); + public resetZero (interrupt: boolean = false) { + const data = Buffer.from([0x51, 0x02, 0x00, 0x00, 0x00, 0x00]); + return this.sendPortOutputCommand(data, interrupt); } diff --git a/src/devices/basicmotor.ts b/src/devices/basicmotor.ts index 619bce3..ff6d8d2 100644 --- a/src/devices/basicmotor.ts +++ b/src/devices/basicmotor.ts @@ -1,9 +1,6 @@ import { Device } from "./device"; - import { IDeviceInterface } from "../interfaces"; - import * as Consts from "../consts"; - import { calculateRamp, mapSpeed } from "../utils"; /** @@ -22,13 +19,11 @@ export class BasicMotor extends Device { * Set the motor power. * @method BasicMotor#setPower * @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 issuance of the command. + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command. */ - public setPower (power: number, interrupt: boolean = true) { - if (interrupt) { - this.cancelEventTimer(); - } - return this.writeDirect(0x00, Buffer.from([mapSpeed(power)])); + public setPower (power: number, interrupt: boolean = false) { + return this.writeDirect(0x00, Buffer.from([mapSpeed(power)]), interrupt); } @@ -38,39 +33,35 @@ export class BasicMotor extends Device { * @param {number} fromPower For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0. * @param {number} toPower 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. + * @returns {Promise} Resolved upon completion of command. */ public rampPower (fromPower: number, toPower: number, time: number) { - this.cancelEventTimer(); - return new Promise((resolve) => { - calculateRamp(this, fromPower, toPower, time) - .on("changePower", (power) => { - this.setPower(power, false); - }) - .on("finished", resolve); + const powerValues = calculateRamp(fromPower, toPower, time); + powerValues.forEach(value => { + this.setPower(value); + this.addPortOutputSleep(Math.round(time/powerValues.length)); }); + return this.setPower(toPower); } /** - * Stop the motor. + * Stop the motor. Previous commands that have not completed are discarded. * @method BasicMotor#stop - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of command. */ public stop () { - this.cancelEventTimer(); - return this.setPower(0); + return this.setPower(0, true); } /** - * Brake the motor. + * Brake the motor. Previous commands that have not completed are discarded. * @method BasicMotor#brake - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of command. */ public brake () { - this.cancelEventTimer(); - return this.setPower(Consts.BrakingStyle.BRAKE); + return this.setPower(Consts.BrakingStyle.BRAKE, true); } diff --git a/src/devices/colordistancesensor.ts b/src/devices/colordistancesensor.ts index 38e62dd..6165c90 100644 --- a/src/devices/colordistancesensor.ts +++ b/src/devices/colordistancesensor.ts @@ -159,7 +159,7 @@ export class ColorDistanceSensor extends Device { * NOTE: Calling this with channel 5-8 with switch off extended channel mode for this receiver. * @method ColorDistanceSensor#setPFExtendedChannel * @param {number} channel Channel number, between 1-8 - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setPFExtendedChannel (channel: number) { let address = 0; @@ -181,7 +181,7 @@ export class ColorDistanceSensor extends Device { * @param {number} channel Channel number, between 1-4 * @param {string} output Outport port, "RED" (A) or "BLUE" (B) * @param {number} power -7 (full reverse) to 7 (full forward). 0 is stop. 8 is brake. - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setPFPower (channel: number, output: Output, power: number) { let address = 0; @@ -205,7 +205,7 @@ export class ColorDistanceSensor extends Device { * @param {Buffer} channel Channel number, between 1-4 * @param {Buffer} powerA -7 (full reverse) to 7 (full forward). 0 is stop. 8 is brake. * @param {Buffer} powerB -7 (full reverse) to 7 (full forward). 0 is stop. 8 is brake. - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public startPFMotors (channel: number, powerBlue: number, powerRed: number) { let address = 0; @@ -225,7 +225,7 @@ export class ColorDistanceSensor extends Device { * Send a raw Power Functions IR command * @method ColorDistanceSensor#sendPFIRMessage * @param {Buffer} message 2 byte payload making up a Power Functions protocol command. NOTE: Only specify nibbles 1-3, nibble 4 should be zeroed. - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public sendPFIRMessage (message: Buffer) { if (this.isWeDo2SmartHub) { @@ -244,41 +244,35 @@ export class ColorDistanceSensor extends Device { * Set the color of the LED on the sensor via a color value. * @method ColorDistanceSensor#setColor * @param {Color} color - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setColor (color: number | boolean) { - return new Promise((resolve) => { - if (color === false) { - color = 0; - } - if (this.isWeDo2SmartHub) { - throw new Error("Setting LED color is not available on the WeDo 2.0 Smart Hub"); - } else { - this.subscribe(Mode.LED); - this.writeDirect(0x05, Buffer.from([color as number])); - } - return resolve(); - }); + if (color === false) { + color = 0; + } + if (this.isWeDo2SmartHub) { + throw new Error("Setting LED color is not available on the WeDo 2.0 Smart Hub"); + } else { + this.subscribe(Mode.LED); + return this.writeDirect(0x05, Buffer.from([color as number])); + } } /** * Set the distance count value. * @method ColorDistanceSensor#setDistanceCount * @param {count} distance count between 0 and 2^32 - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setDistanceCount (count: number) { - return new Promise((resolve) => { - if (this.isWeDo2SmartHub) { - throw new Error("Setting distance count is not available on the WeDo 2.0 Smart Hub"); - } else { - const payload = Buffer.alloc(4); - payload.writeUInt32LE(count % 2**32); - // no need to subscribe, can be set in different mode - this.writeDirect(0x02, payload); - } - return resolve(); - }); + if (this.isWeDo2SmartHub) { + throw new Error("Setting distance count is not available on the WeDo 2.0 Smart Hub"); + } else { + const payload = Buffer.alloc(4); + payload.writeUInt32LE(count % 2**32); + // no need to subscribe, can be set in different mode + return this.writeDirect(0x02, payload); + } } private _pfPowerToPWM (power: number) { diff --git a/src/devices/device.ts b/src/devices/device.ts index 54e9624..2601c14 100644 --- a/src/devices/device.ts +++ b/src/devices/device.ts @@ -1,9 +1,14 @@ import { EventEmitter } from "events"; import { IDeviceInterface } from "../interfaces"; +import { PortOutputCommand } from "../portoutputcommand"; +import { PortOutputSleep } from "../portoutputsleep"; import * as Consts from "../consts"; +import Debug = require("debug"); +const debug = Debug("device"); + /** * @class Device * @extends EventEmitter @@ -14,8 +19,9 @@ export class Device extends EventEmitter { public values: {[event: string]: any} = {}; protected _mode: number | undefined; - protected _busy: boolean = false; - protected _finishedCallbacks: (() => void)[] = []; + protected _bufferLength: number = 0; + protected _nextPortOutputCommands: (PortOutputCommand | PortOutputSleep)[] = []; + protected _transmittedPortOutputCommands: PortOutputCommand[] = []; private _hub: IDeviceInterface; private _portId: number; @@ -25,7 +31,6 @@ export class Device extends EventEmitter { private _isWeDo2SmartHub: boolean; private _isVirtualPort: boolean = false; - private _eventTimer: NodeJS.Timeout | null = null; constructor (hub: IDeviceInterface, portId: number, modeMap: {[event: string]: number} = {}, type: Consts.DeviceType = Consts.DeviceType.UNKNOWN) { super(); @@ -126,11 +131,11 @@ export class Device extends EventEmitter { return this._isVirtualPort; } - public writeDirect (mode: number, data: Buffer) { + public writeDirect (mode: number, data: Buffer, interrupt: boolean = false) { if (this.isWeDo2SmartHub) { - return this.send(Buffer.concat([Buffer.from([this.portId, 0x01, 0x02]), data]), Consts.BLECharacteristic.WEDO2_MOTOR_VALUE_WRITE); + return this.send(Buffer.concat([Buffer.from([this.portId, 0x01, 0x02]), data]), Consts.BLECharacteristic.WEDO2_MOTOR_VALUE_WRITE).then(() => { return Consts.CommandFeedback.FEEDBACK_DISABLED; }); } else { - return this.send(Buffer.concat([Buffer.from([0x81, this.portId, 0x11, 0x51, mode]), data]), Consts.BLECharacteristic.LPF2_ALL); + return this.sendPortOutputCommand(Buffer.concat([Buffer.from([0x51, mode]), data]), interrupt); } } @@ -167,26 +172,175 @@ export class Device extends EventEmitter { this.send(Buffer.from([0x21, this.portId, 0x00])); } - public finish (message: number) { - if((message & 0x10) === 0x10) return; // "busy/full" - this._busy = (message & 0x01) === 0x01; - while(this._finishedCallbacks.length > Number(this._busy)) { - const callback = this._finishedCallbacks.shift(); - if(callback) { - callback(); + protected transmitNextPortOutputCommand() { + if(!this.connected) { + this._transmittedPortOutputCommands.forEach(command => command.resolve(Consts.CommandFeedback.FEEDBACK_MISSING)); + this._transmittedPortOutputCommands = []; + this._nextPortOutputCommands.forEach(command => command.resolve(Consts.CommandFeedback.TRANSMISSION_DISCARDED)); + this._nextPortOutputCommands = []; + return; + } + if(!this._nextPortOutputCommands.length) return; + const nextCommand = this._nextPortOutputCommands[0]; + if(nextCommand instanceof PortOutputSleep) { + if(nextCommand.state === Consts.CommandFeedback.EXECUTION_PENDING) { + nextCommand.state = Consts.CommandFeedback.EXECUTION_BUSY; + debug("sleep command ", nextCommand.duration); + setTimeout(() => { + if(nextCommand.state !== Consts.CommandFeedback.EXECUTION_BUSY) return; + const command = this._nextPortOutputCommands.shift(); + if(command) command.resolve(Consts.CommandFeedback.EXECUTION_COMPLETED); + this.transmitNextPortOutputCommand(); + }, nextCommand.duration); + } + return; + } + if(this._bufferLength !== this._transmittedPortOutputCommands.length) return; + if(this._bufferLength < 2 || nextCommand.interrupt) { + if(nextCommand.state === Consts.CommandFeedback.TRANSMISSION_PENDING) { + nextCommand.state = Consts.CommandFeedback.TRANSMISSION_BUSY; + debug("transmit command ", nextCommand.startupAndCompletion, nextCommand.data); + this.send(Buffer.concat([Buffer.from([0x81, this.portId, nextCommand.startupAndCompletion]), nextCommand.data])).then(() => { + if(nextCommand.state !== Consts.CommandFeedback.TRANSMISSION_BUSY) return; + const command = this._nextPortOutputCommands.shift(); + if(command instanceof PortOutputCommand) this._transmittedPortOutputCommands.push(command); + }); + this.transmitNextPortOutputCommand(); // if the next command is PortOutputSleep this starts sleep timeout + // one could start a timer here to ensure finish function is called } } } - public setEventTimer (timer: NodeJS.Timeout) { - this._eventTimer = timer; + public sendPortOutputCommand(data: Buffer, interrupt: boolean = false) { + if (this.isWeDo2SmartHub) { + throw new Error("PortOutputCommands are not available on the WeDo 2.0 Smart Hub"); + return; + } + const command = new PortOutputCommand(data, interrupt); + if(interrupt) { + this._nextPortOutputCommands.forEach(command => { + if(command.state !== Consts.CommandFeedback.TRANSMISSION_BUSY) { + command.resolve(Consts.CommandFeedback.TRANSMISSION_DISCARDED); + } + }); + this._nextPortOutputCommands = this._nextPortOutputCommands.filter(command => command.state === Consts.CommandFeedback.TRANSMISSION_BUSY); + } + this._nextPortOutputCommands.push(command); + process.nextTick(() => this.transmitNextPortOutputCommand()); + return command.promise; } - public cancelEventTimer () { - if (this._eventTimer) { - clearTimeout(this._eventTimer); - this._eventTimer = null; + public addPortOutputSleep(duration: number) { + const command = new PortOutputSleep(duration); + this._nextPortOutputCommands.push(command); + return command.promise; + } + + public finish (message: number) { + debug("recieved command feedback ", message); + if((message & 0x08) === 0x08) this._bufferLength = 0; + else if((message & 0x01) === 0x01) this._bufferLength = 1; + else if((message & 0x10) === 0x10) this._bufferLength = 2; + const completed = ((message & 0x02) === 0x02); + const discarded = ((message & 0x04) === 0x04); + + switch(this._transmittedPortOutputCommands.length) { + case 0: + break; + case 1: + if(!this._bufferLength && completed && !discarded) { + this._complete(); + } + else if(!this._bufferLength && !completed && discarded) { + this._discard(); + } + else if(this._bufferLength && !completed && !discarded) { + this._busy(); + } + else { + this._missing(); + } + break; + case 2: + if(!this._bufferLength && completed && discarded) { + this._discard(); + this._complete(); + } + else if(!this._bufferLength && completed && !discarded) { + this._complete(); + this._complete(); + } + else if(!this._bufferLength && !completed && discarded) { + this._discard(); + this._discard(); + } + else if(this._bufferLength === 1 && completed && !discarded) { + this._complete(); + this._busy(); + } + else if(this._bufferLength === 1 && !completed && discarded) { + this._discard(); + this._busy(); + } + else if(this._bufferLength === 1 && completed && discarded) { + this._missing(); + this._busy(); + } + else if(this._bufferLength === 2 && !completed && !discarded) { + this._busy(); + this._pending(); + } + else { + this._missing(); + this._missing(); + } + break; + case 3: + if(!this._bufferLength && completed && discarded) { + this._discard(); + this._discard(); + this._complete(); + } + else if(!this._bufferLength && completed && !discarded) { + this._complete(); + this._complete(); + this._complete(); + } + else if(!this._bufferLength && !completed && discarded) { + this._discard(); + this._discard(); + this._discard(); + } + else if(this._bufferLength === 1 && completed && discarded) { + this._discard(); + this._complete(); + this._busy(); + } + else if(this._bufferLength === 1 && completed && !discarded) { + this._complete(); + this._complete(); + this._busy(); + } + else if(this._bufferLength === 1 && !completed && discarded) { + this._discard(); + this._discard(); + this._busy(); + } + else if(this._bufferLength === 1 && !completed && !discarded) { + this._missing(); + this._missing(); + this._busy(); + } + // third command can only be interrupt, if this._bufferLength === 2 it was queued + else { + this._missing(); + this._missing(); + this._missing(); + } + break; } + + this.transmitNextPortOutputCommand(); } private _ensureConnected () { @@ -195,4 +349,24 @@ export class Device extends EventEmitter { } } + private _complete () { + const command = this._transmittedPortOutputCommands.shift(); + if(command) command.resolve(Consts.CommandFeedback.EXECUTION_COMPLETED); + } + private _discard () { + const command = this._transmittedPortOutputCommands.shift(); + if(command) command.resolve(Consts.CommandFeedback.EXECUTION_DISCARDED); + } + private _missing () { + const command = this._transmittedPortOutputCommands.shift(); + if(command) command.resolve(Consts.CommandFeedback.FEEDBACK_MISSING); + } + private _busy () { + const command = this._transmittedPortOutputCommands[0]; + if(command) command.state = Consts.CommandFeedback.EXECUTION_BUSY; + } + private _pending () { + const command = this._transmittedPortOutputCommands[1]; + if(command) command.state = Consts.CommandFeedback.EXECUTION_PENDING; + } } diff --git a/src/devices/duplotrainbasespeaker.ts b/src/devices/duplotrainbasespeaker.ts index efb3ce9..cf22c35 100644 --- a/src/devices/duplotrainbasespeaker.ts +++ b/src/devices/duplotrainbasespeaker.ts @@ -18,25 +18,22 @@ export class DuploTrainBaseSpeaker extends Device { * Play a built-in train sound. * @method DuploTrainBaseSpeaker#playSound * @param {DuploTrainBaseSound} sound - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of command. */ public playSound (sound: Consts.DuploTrainBaseSound) { - return new Promise((resolve) => { - this.subscribe(Mode.SOUND); - this.writeDirect(0x01, Buffer.from([sound])); - return resolve(); - }); + this.subscribe(Mode.SOUND); + return this.writeDirect(0x01, Buffer.from([sound])); } /** * Play a built-in system tone. * @method DuploTrainBaseSpeaker#playTone * @param {number} tone - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of command. */ public playTone (tone: number) { this.subscribe(Mode.TONE); - this.writeDirect(0x02, Buffer.from([tone])); + return this.writeDirect(0x02, Buffer.from([tone])); } } diff --git a/src/devices/hubled.ts b/src/devices/hubled.ts index c52cd0a..401fd54 100644 --- a/src/devices/hubled.ts +++ b/src/devices/hubled.ts @@ -20,22 +20,22 @@ export class HubLED extends Device { * Set the color of the LED on the Hub via a color value. * @method HubLED#setColor * @param {Color} color - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of command. */ public setColor (color: number | boolean) { - return new Promise((resolve) => { - if (typeof color === "boolean") { - color = 0; - } - if (this.isWeDo2SmartHub) { + if (typeof color === "boolean") { + color = 0; + } + if (this.isWeDo2SmartHub) { + return new Promise((resolve) => { this.send(Buffer.from([0x06, 0x17, 0x01, 0x01]), Consts.BLECharacteristic.WEDO2_PORT_TYPE_WRITE); - this.send(Buffer.from([0x06, 0x04, 0x01, color]), Consts.BLECharacteristic.WEDO2_MOTOR_VALUE_WRITE); - } else { - this.subscribe(Mode.COLOR); - this.writeDirect(0x00, Buffer.from([color])); - } - return resolve(); - }); + this.send(Buffer.from([0x06, 0x04, 0x01, Number(color)]), Consts.BLECharacteristic.WEDO2_MOTOR_VALUE_WRITE); + return resolve(Consts.CommandFeedback.FEEDBACK_DISABLED); + }) + } else { + this.subscribe(Mode.COLOR); + return this.writeDirect(0x00, Buffer.from([color])); + } } @@ -45,19 +45,19 @@ export class HubLED extends Device { * @param {number} red * @param {number} green * @param {number} blue - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of command. */ public setRGB (red: number, green: number, blue: number) { - return new Promise((resolve) => { - if (this.isWeDo2SmartHub) { + if (this.isWeDo2SmartHub) { + return new Promise((resolve) => { this.send(Buffer.from([0x06, 0x17, 0x01, 0x02]), Consts.BLECharacteristic.WEDO2_PORT_TYPE_WRITE); this.send(Buffer.from([0x06, 0x04, 0x03, red, green, blue]), Consts.BLECharacteristic.WEDO2_MOTOR_VALUE_WRITE); - } else { - this.subscribe(Mode.RGB); - this.writeDirect(0x01, Buffer.from([red, green, blue])); - } - return resolve(); - }); + resolve(Consts.CommandFeedback.FEEDBACK_DISABLED); + }); + } else { + this.subscribe(Mode.RGB); + return this.writeDirect(0x01, Buffer.from([red, green, blue])); + } } diff --git a/src/devices/light.ts b/src/devices/light.ts index c079085..33526ab 100644 --- a/src/devices/light.ts +++ b/src/devices/light.ts @@ -1,7 +1,5 @@ import { Device } from "./device"; - import { IDeviceInterface } from "../interfaces"; - import * as Consts from "../consts"; import { calculateRamp } from "../utils"; @@ -21,16 +19,12 @@ export class Light extends Device { * Set the light brightness. * @method Light#setBrightness * @param {number} brightness Brightness value between 0-100 (0 is off) - * @returns {Promise} Resolved upon successful completion of command. + * @param {number} brightness Brightness value between 0-100 (0 is off) + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command. */ - public setBrightness (brightness: number, interrupt: boolean = true) { - if (interrupt) { - this.cancelEventTimer(); - } - return new Promise((resolve) => { - this.writeDirect(0x00, Buffer.from([brightness])); - return resolve(); - }); + public setBrightness (brightness: number, interrupt: boolean = false) { + return this.writeDirect(0x00, Buffer.from([brightness]), interrupt); } @@ -40,18 +34,15 @@ export class Light extends Device { * @param {number} fromBrightness Brightness value between 0-100 (0 is off) * @param {number} toBrightness Brightness value between 0-100 (0 is off) * @param {number} time How long the ramp should last (in milliseconds). - * @returns {Promise} Resolved upon successful completion of command. + * @returns {Promise} Resolved upon completion of command. */ public rampBrightness (fromBrightness: number, toBrightness: number, time: number) { - this.cancelEventTimer(); - return new Promise((resolve) => { - calculateRamp(this, fromBrightness, toBrightness, time) - .on("changePower", (power) => { - this.setBrightness(power, false); - }) - .on("finished", resolve); + const powerValues = calculateRamp(fromBrightness, toBrightness, time); + powerValues.forEach(value => { + this.setBrightness(value); + this.addPortOutputSleep(Math.round(time/powerValues.length)); }); + return this.setBrightness(toBrightness); } - } diff --git a/src/devices/piezobuzzer.ts b/src/devices/piezobuzzer.ts index 76461c8..8de1e05 100644 --- a/src/devices/piezobuzzer.ts +++ b/src/devices/piezobuzzer.ts @@ -21,15 +21,15 @@ export class PiezoBuzzer extends Device { * @method PiezoBuzzer#playTone * @param {number} frequency * @param {number} time How long the tone should play for (in milliseconds). - * @returns {Promise} Resolved upon successful completion of command (ie. once the tone has finished playing). + * @returns {Promise} Resolved upon completion of command (i.e. once the tone has finished playing). */ public playTone (frequency: number, time: number) { - return new Promise((resolve) => { + return new Promise((resolve) => { const data = Buffer.from([0x05, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00]); data.writeUInt16LE(frequency, 3); data.writeUInt16LE(time, 5); this.send(data, Consts.BLECharacteristic.WEDO2_MOTOR_VALUE_WRITE); - global.setTimeout(resolve, time); + global.setTimeout(() => {return resolve(Consts.CommandFeedback.FEEDBACK_DISABLED)}, time); }); } diff --git a/src/devices/tachomotor.ts b/src/devices/tachomotor.ts index 357eff4..0e7df86 100644 --- a/src/devices/tachomotor.ts +++ b/src/devices/tachomotor.ts @@ -66,12 +66,14 @@ export class TachoMotor extends BasicMotor { * Set the global acceleration time * @method TachoMotor#setAccelerationTime * @param {number} time How long acceleration should last (in milliseconds). - * @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished). + * @param {number} profile 0 by default + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (i.e. once the motor is finished). */ - public setAccelerationTime (time: number, profile: number = 0x00) { - const message = Buffer.from([0x81, this.portId, 0x11, 0x05, 0x00, 0x00, profile]); - message.writeUInt16LE(time, 4); - this.send(message); + public setAccelerationTime (time: number, profile: number = 0x00, interrupt: boolean = false) { + const message = Buffer.from([0x05, 0x00, 0x00, profile]); + message.writeUInt16LE(time, 1); + return this.sendPortOutputCommand(message, interrupt); } @@ -79,12 +81,14 @@ export class TachoMotor extends BasicMotor { * Set the global deceleration time * @method TachoMotor#setDecelerationTime * @param {number} time How long deceleration should last (in milliseconds). - * @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished). + * @param {number} profile 0 by default + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (i.e. once the motor is finished). */ - public setDecelerationTime (time: number, profile: number = 0x00) { - const message = Buffer.from([0x81, this.portId, 0x11, 0x06, 0x00, 0x00, profile]); - message.writeUInt16LE(time, 4); - this.send(message); + public setDecelerationTime (time: number, profile: number = 0x00, interrupt: boolean = true) { + const message = Buffer.from([0x06, 0x00, 0x00, profile]); + message.writeUInt16LE(time, 1); + return this.sendPortOutputCommand(message, interrupt); } @@ -93,40 +97,35 @@ export class TachoMotor extends BasicMotor { * @method TachoMotor#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. * @param {number} time How long the motor should run for (in milliseconds). - * @returns {Promise} Resolved upon successful issuance of the command. + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (i.e. once the motor is finished). */ - public setSpeed (speed: [number, number] | number, time: number | undefined) { + public setSpeed (speed: [number, number] | number, time: number | undefined, interrupt: boolean = false) { if (!this.isVirtualPort && speed instanceof Array) { throw new Error("Only virtual ports can accept multiple speeds"); } if (this.isWeDo2SmartHub) { throw new Error("Motor speed is not available on the WeDo 2.0 Smart Hub"); } - this.cancelEventTimer(); - return new Promise((resolve) => { - if (speed === undefined || speed === null) { - speed = 100; + if (speed === undefined || speed === null) { + speed = 100; + } + let message; + if (time !== undefined) { + if (speed instanceof Array) { + message = Buffer.from([0x0a, 0x00, 0x00, mapSpeed(speed[0]), mapSpeed(speed[1]), this._maxPower, this._brakeStyle, this.useProfile()]); + } else { + message = Buffer.from([0x09, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); } - let message; - if (time !== undefined) { - if (speed instanceof Array) { - message = Buffer.from([0x81, this.portId, 0x11, 0x0a, 0x00, 0x00, mapSpeed(speed[0]), mapSpeed(speed[1]), this._maxPower, this._brakeStyle, this.useProfile()]); - } else { - message = Buffer.from([0x81, this.portId, 0x11, 0x09, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); - } - message.writeUInt16LE(time, 4); + message.writeUInt16LE(time, 1); + } else { + if (speed instanceof Array) { + message = Buffer.from([0x08, mapSpeed(speed[0]), mapSpeed(speed[1]), this._maxPower, this.useProfile()]); } else { - if (speed instanceof Array) { - message = Buffer.from([0x81, this.portId, 0x11, 0x08, mapSpeed(speed[0]), mapSpeed(speed[1]), this._maxPower, this.useProfile()]); - } else { - message = Buffer.from([0x81, this.portId, 0x11, 0x07, mapSpeed(speed), this._maxPower, this.useProfile()]); - } + message = Buffer.from([0x07, mapSpeed(speed), this._maxPower, this.useProfile()]); } - this.send(message); - this._finishedCallbacks.push(() => { - return resolve(); - }); - }); + } + return this.sendPortOutputCommand(message, interrupt); } /** @@ -134,32 +133,27 @@ export class TachoMotor extends BasicMotor { * @method TachoMotor#rotateByDegrees * @param {number} degrees 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. - * @returns {Promise} Resolved upon successful completion of command (ie. once the motor is finished). + * @param {boolean} [interrupt=false] If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (i.e. once the motor is finished). */ - public rotateByDegrees (degrees: number, speed: [number, number] | number) { + public rotateByDegrees (degrees: number, speed: [number, number] | number, interrupt: boolean = false) { if (!this.isVirtualPort && speed instanceof Array) { throw new Error("Only virtual ports can accept multiple speeds"); } if (this.isWeDo2SmartHub) { throw new Error("Rotation is not available on the WeDo 2.0 Smart Hub"); } - this.cancelEventTimer(); - return new Promise((resolve) => { - if (speed === undefined || speed === null) { - speed = 100; - } - let message; - if (speed instanceof Array) { - message = Buffer.from([0x81, this.portId, 0x11, 0x0c, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed[0]), mapSpeed(speed[1]), this._maxPower, this._brakeStyle, this.useProfile()]); - } else { - message = Buffer.from([0x81, this.portId, 0x11, 0x0b, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); - } - message.writeUInt32LE(degrees, 4); - this.send(message); - this._finishedCallbacks.push(() => { - return resolve(); - }); - }); + if (speed === undefined || speed === null) { + speed = 100; + } + let message; + if (speed instanceof Array) { + message = Buffer.from([0x0c, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed[0]), mapSpeed(speed[1]), this._maxPower, this._brakeStyle, this.useProfile()]); + } else { + message = Buffer.from([0x0b, 0x00, 0x00, 0x00, 0x00, mapSpeed(speed), this._maxPower, this._brakeStyle, this.useProfile()]); + } + message.writeUInt32LE(degrees, 1); + return this.sendPortOutputCommand(message, interrupt); } diff --git a/src/devices/technic3x3colorlightmatrix.ts b/src/devices/technic3x3colorlightmatrix.ts index 9fcbe4c..810f673 100644 --- a/src/devices/technic3x3colorlightmatrix.ts +++ b/src/devices/technic3x3colorlightmatrix.ts @@ -22,35 +22,32 @@ export class Technic3x3ColorLightMatrix extends Device { * Set the LED matrix, one color per LED * @method Technic3x3ColorLightMatrix#setMatrix * @param {Color[] | Color} colors Array of 9 colors, 9 Color objects, or a single color - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of command. */ public setMatrix (colors: number[] | number) { - return new Promise((resolve) => { - this.subscribe(Mode.PIX_0); - const colorArray = new Array(9); - for (let i = 0; i < colorArray.length; i++) { - if (typeof colors === 'number') { - // @ts-ignore - colorArray[i] = colors + (10 << 4); - } + this.subscribe(Mode.PIX_0); + const colorArray = new Array(9); + for (let i = 0; i < colorArray.length; i++) { + if (typeof colors === 'number') { // @ts-ignore - if (colors[i] instanceof Color) { - // @ts-ignore - colorArray[i] = colors[i].toValue(); - } + colorArray[i] = colors + (10 << 4); + } + // @ts-ignore + if (colors[i] instanceof Color) { // @ts-ignore - if (colors[i] === Consts.Color.NONE) { - colorArray[i] = Consts.Color.NONE; - } + colorArray[i] = colors[i].toValue(); + } + // @ts-ignore + if (colors[i] === Consts.Color.NONE) { + colorArray[i] = Consts.Color.NONE; + } + // @ts-ignore + if (colors[i] <= 10) { // @ts-ignore - if (colors[i] <= 10) { - // @ts-ignore - colorArray[i] = colors[i] + (10 << 4); // If a raw color value, set it to max brightness (10) - } + colorArray[i] = colors[i] + (10 << 4); // If a raw color value, set it to max brightness (10) } - this.writeDirect(Mode.PIX_0, Buffer.from(colorArray)); - return resolve(); - }); + } + return this.writeDirect(Mode.PIX_0, Buffer.from(colorArray)); } diff --git a/src/devices/techniccolorsensor.ts b/src/devices/techniccolorsensor.ts index b26c8d6..adc37b7 100644 --- a/src/devices/techniccolorsensor.ts +++ b/src/devices/techniccolorsensor.ts @@ -65,10 +65,10 @@ export class TechnicColorSensor extends Device { * @param {number} firstSegment First light segment. 0-100 brightness. * @param {number} secondSegment Second light segment. 0-100 brightness. * @param {number} thirdSegment Third light segment. 0-100 brightness. - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setBrightness (firstSegment: number, secondSegment: number, thirdSegment: number) { - this.writeDirect(0x03, Buffer.from([firstSegment, secondSegment, thirdSegment])); + return this.writeDirect(0x03, Buffer.from([firstSegment, secondSegment, thirdSegment])); } } diff --git a/src/devices/technicdistancesensor.ts b/src/devices/technicdistancesensor.ts index 7339c02..ecc9903 100644 --- a/src/devices/technicdistancesensor.ts +++ b/src/devices/technicdistancesensor.ts @@ -51,7 +51,7 @@ export class TechnicDistanceSensor extends Device { * @param {number} bottomLeft Bottom left quadrant (below left eye). 0-100 brightness. * @param {number} topRight Top right quadrant (above right eye). 0-100 brightness. * @param {number} bottomRight Bottom right quadrant (below right eye). 0-100 brightness. - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setBrightness (topLeft: number, bottomLeft: number, topRight: number, bottomRight: number) { this.writeDirect(0x05, Buffer.from([topLeft, topRight, bottomLeft, bottomRight])); diff --git a/src/devices/technicmediumhubtiltsensor.ts b/src/devices/technicmediumhubtiltsensor.ts index b8ca720..11d7372 100644 --- a/src/devices/technicmediumhubtiltsensor.ts +++ b/src/devices/technicmediumhubtiltsensor.ts @@ -66,44 +66,35 @@ export class TechnicMediumHubTiltSensor extends Device { * Set the impact count value. * @method TechnicMediumHubTiltSensor#setImpactCount * @param {count} impact count between 0 and 2^32 - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setImpactCount (count: number) { - return new Promise((resolve) => { - const payload = Buffer.alloc(4); - payload.writeUInt32LE(count % 2**32); - // no need to subscribe, can be set in different mode - this.writeDirect(0x01, payload); - return resolve(); - }); + const payload = Buffer.alloc(4); + payload.writeUInt32LE(count % 2**32); + // no need to subscribe, can be set in different mode + return this.writeDirect(0x01, payload); } /** * Set the impact threshold. * @method TechnicMediumHubTiltSensor#setImpactThreshold * @param {threshold} value between 1 and 127 - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setImpactThreshold (threshold: number) { this._impactThreshold = threshold; - return new Promise((resolve) => { - this.writeDirect(0x02, Buffer.from([this._impactThreshold, this._impactHoldoff])); - return resolve(); - }); + return this.writeDirect(0x02, Buffer.from([this._impactThreshold, this._impactHoldoff])); } /** * Set the impact holdoff time. * @method TechnicMediumHubTiltSensor#setImpactHoldoff * @param {holdoff} value between 1 and 127 - * @returns {Promise} Resolved upon successful issuance of the command. + * @returns {Promise} Resolved upon completion of the command. */ public setImpactHoldoff (holdoff: number) { this._impactHoldoff = holdoff; - return new Promise((resolve) => { - this.writeDirect(0x02, Buffer.from([this._impactThreshold, this._impactHoldoff])); - return resolve(); - }); + return this.writeDirect(0x02, Buffer.from([this._impactThreshold, this._impactHoldoff])); } } diff --git a/src/portoutputcommand.ts b/src/portoutputcommand.ts new file mode 100644 index 0000000..3ec0c85 --- /dev/null +++ b/src/portoutputcommand.ts @@ -0,0 +1,37 @@ +import { CommandFeedback } from "./consts"; +import Debug = require("debug"); +const debug = Debug("device"); + +export class PortOutputCommand { + + public data: Buffer; + public interrupt: boolean; + public state: CommandFeedback; + private _promise: Promise; + private _resolveCallback: any; + + constructor (data: Buffer, interrupt: boolean) { + this.data = data; + this.interrupt = interrupt; + this.state = CommandFeedback.TRANSMISSION_PENDING; + this._promise = new Promise((resolve) => { + this._resolveCallback = () => resolve(this.state); + }); + } + + public get startupAndCompletion () { + let val = 0x01; // request feedback + if(this.interrupt) val |= 0x10; + return val; + } + + public get promise () { + return this._promise; + } + + public resolve(feedback: CommandFeedback) { + debug("complete command ", this.startupAndCompletion, this.data, " result: ", feedback); + this.state = feedback; + this._resolveCallback(); + } +} diff --git a/src/portoutputsleep.ts b/src/portoutputsleep.ts new file mode 100644 index 0000000..c047671 --- /dev/null +++ b/src/portoutputsleep.ts @@ -0,0 +1,11 @@ +import { CommandFeedback } from "./consts"; +import { PortOutputCommand } from "./portoutputcommand"; + +export class PortOutputSleep extends PortOutputCommand { + public duration: number + constructor(duration: number) { + super(Buffer.alloc(0), false); + this.duration = duration; + this.state = CommandFeedback.EXECUTION_PENDING; + } +} diff --git a/src/utils.ts b/src/utils.ts index 3c6d10e..9a4d0f3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -59,7 +59,7 @@ export const roundAngleToNearest90 = (angle: number) => { return -180; }; -export const calculateRamp = (device: Device, fromPower: number, toPower: number, time: number) => { +export const calculateRamp = (fromPower: number, toPower: number, time: number) => { const emitter = new EventEmitter(); const steps = Math.abs(toPower - fromPower); let delay = time / steps; @@ -71,22 +71,7 @@ export const calculateRamp = (device: Device, fromPower: number, toPower: number if (fromPower > toPower) { increment = -increment; } - let i = 0; - const interval = setInterval(() => { - let power = Math.round(fromPower + (++i * increment)); - if (toPower > fromPower && power > toPower) { - power = toPower; - } else if (fromPower > toPower && power < toPower) { - power = toPower; - } - emitter.emit("changePower", power); - if (power === toPower) { - clearInterval(interval); - emitter.emit("finished"); - } - }, delay); - device.setEventTimer(interval); - return emitter; + return Array(Math.round(time/delay)).fill(0).map((element, index) => fromPower + index*increment); }; export const parseColor = (color: number) => { @@ -94,4 +79,4 @@ export const parseColor = (color: number) => { color = color + 1; } return color; -} \ No newline at end of file +}