From b94a76db8d2208251963fe1535c9d2c900e503dc Mon Sep 17 00:00:00 2001 From: bene Date: Sat, 28 May 2022 13:51:50 +0200 Subject: [PATCH 1/4] rework port output command handling use explicit queues for port output commands; this helps the device to remain responsive by preventing old port output commands from clogging the bluetooth transmission. add improved interrupt property; if true, ALL previous commands are discarded if false, NONE of the previous commands are discarded this changes the previous behavior where commands queued for transmission could not be discarded and transmitted commands would not wait for the device to complete the previous command. send commands to queue by default (except break and stop); this keeps and improves the ability to accept commands faster than they can be transmitted via bluetooth and ensures that no commands discard previous commands in case they are not explicitly chained. provide feedback about success of commands. --- src/consts.ts | 24 +++ src/devices/absolutemotor.ts | 55 +++---- src/devices/basicmotor.ts | 33 ++-- src/devices/colordistancesensor.ts | 52 +++--- src/devices/device.ts | 189 ++++++++++++++++++++-- src/devices/duplotrainbasespeaker.ts | 13 +- src/devices/hubled.ts | 44 ++--- src/devices/light.ts | 23 ++- src/devices/piezobuzzer.ts | 6 +- src/devices/tachomotor.ts | 100 ++++++------ src/devices/technic3x3colorlightmatrix.ts | 43 +++-- src/devices/techniccolorsensor.ts | 4 +- src/devices/technicdistancesensor.ts | 2 +- src/devices/technicmediumhubtiltsensor.ts | 27 ++-- src/portoutputcommand.ts | 37 +++++ 15 files changed, 418 insertions(+), 234 deletions(-) create mode 100644 src/portoutputcommand.ts diff --git a/src/consts.ts b/src/consts.ts index 268caa8a..e46e5e5d 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 comands to complete transmission or execution + * @param {number} TRANSMISSION_BUSY 0x10 waiting for device to acknowledge reception + * @param {number} TRANSMISSION_DISCARDED 0x44 other command for immediate execution 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 other command for immediate execution + * @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 4ed895d7..b46651c3 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 If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (ie. 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 (ie. 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 If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (ie. 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 619bce3c..2b09461a 100644 --- a/src/devices/basicmotor.ts +++ b/src/devices/basicmotor.ts @@ -22,13 +22,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 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 +36,38 @@ 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) => { + return new Promise((resolve) => { calculateRamp(this, fromPower, toPower, time) .on("changePower", (power) => { this.setPower(power, false); }) - .on("finished", resolve); + .on("finished", () => { + return resolve(Consts.CommandFeedback.FEEDBACK_DISABLED); + }) }); } /** - * 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 38e62ddc..6165c90f 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 54e96246..e0def561 100644 --- a/src/devices/device.ts +++ b/src/devices/device.ts @@ -1,9 +1,13 @@ import { EventEmitter } from "events"; import { IDeviceInterface } from "../interfaces"; +import { PortOutputCommand } from "../portoutputcommand"; import * as Consts from "../consts"; +import Debug = require("debug"); +const debug = Debug("device"); + /** * @class Device * @extends EventEmitter @@ -14,8 +18,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[] = []; + protected _transmittedPortOutputCommands: PortOutputCommand[] = []; private _hub: IDeviceInterface; private _portId: number; @@ -126,11 +131,14 @@ export class Device extends EventEmitter { return this._isVirtualPort; } - public writeDirect (mode: number, data: Buffer) { + public writeDirect (mode: number, data: Buffer, interrupt: boolean = false) { + if (interrupt) { + this.cancelEventTimer(); + } 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,18 +175,153 @@ 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._bufferLength !== this._transmittedPortOutputCommands.length) return; + if(!this._nextPortOutputCommands.length) return; + if(this._bufferLength < 2 || this._nextPortOutputCommands[0].interrupt) { + const command = this._nextPortOutputCommands.shift(); + if(command) { + debug("transmit command ", command.startupAndCompletion, command.data); + this.send(Buffer.concat([Buffer.from([0x81, this.portId, command.startupAndCompletion]), command.data])); + command.state = Consts.CommandFeedback.TRANSMISSION_BUSY; + this._transmittedPortOutputCommands.push(command); + // one could start a timer here to ensure finish function is called } } } - public setEventTimer (timer: NodeJS.Timeout) { + 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 => command.resolve(Consts.CommandFeedback.TRANSMISSION_DISCARDED)); + this._nextPortOutputCommands = [ command ]; + } + else { + this._nextPortOutputCommands.push(command); + } + this.transmitNextPortOutputCommand(); + 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 busy === 2 it was queued + else { + this._missing(); + this._missing(); + this._missing(); + } + break; + } + + this.transmitNextPortOutputCommand(); + } + + public setEventTimer (timer: NodeJS.Timer) { this._eventTimer = timer; } @@ -195,4 +338,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 efb3ce91..cf22c353 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 c52cd0a5..401fd544 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 c0790854..0b9a00bf 100644 --- a/src/devices/light.ts +++ b/src/devices/light.ts @@ -21,16 +21,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 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,16 +36,17 @@ 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) => { + return new Promise((resolve) => { calculateRamp(this, fromBrightness, toBrightness, time) .on("changePower", (power) => { this.setBrightness(power, false); }) - .on("finished", resolve); + .on("finished", () => { + return resolve(Consts.CommandFeedback.FEEDBACK_DISABLED); + }); }); } diff --git a/src/devices/piezobuzzer.ts b/src/devices/piezobuzzer.ts index 76461c8a..5a3f8fa5 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 (ie. 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 357eff47..233e6a1c 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 If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (ie. 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 If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (ie. 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 If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (ie. 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 If true, previous commands are discarded. + * @returns {Promise} Resolved upon completion of command (ie. 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 9fcbe4c4..810f673c 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 b26c8d63..adc37b76 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 7339c02c..ecc99036 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 b8ca7203..11d7372b 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 00000000..3ec0c85d --- /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(); + } +} From 8b205db9fb9c58e2b3a8f7fe2c10402cf7431e9b Mon Sep 17 00:00:00 2001 From: bene Date: Sun, 5 Jun 2022 17:49:50 +0200 Subject: [PATCH 2/4] rework event timer comands use new PortOutputSleep instead of timer; * the promise returned by rampPower and rampBrightness gets resolved if the command is interrupted. * rampPower and rampBrightness can no longer be altered by other commands while in progress. Other commands now queue after the ramp command or interrupt and stop its execution. --- src/consts.ts | 6 +++--- src/devices/basicmotor.ts | 16 +++++--------- src/devices/device.ts | 44 ++++++++++++++++++++++----------------- src/devices/light.ts | 16 +++++--------- src/portoutputsleep.ts | 11 ++++++++++ src/utils.ts | 21 +++---------------- 6 files changed, 52 insertions(+), 62 deletions(-) create mode 100644 src/portoutputsleep.ts diff --git a/src/consts.ts b/src/consts.ts index e46e5e5d..2412567e 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -706,12 +706,12 @@ export enum MarioColor { /** * @typedef CommandFeedback - * @param {number} TRANSMISSION_PENDING 0x00 waiting for previous comands to complete transmission or execution + * @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 other command for immediate execution has been recieved or device disconnected + * @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 other command for immediate execution + * @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 diff --git a/src/devices/basicmotor.ts b/src/devices/basicmotor.ts index 2b09461a..3041fd4d 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"; /** @@ -39,15 +36,12 @@ export class BasicMotor extends Device { * @returns {Promise} Resolved upon completion of command. */ public rampPower (fromPower: number, toPower: number, time: number) { - return new Promise((resolve) => { - calculateRamp(this, fromPower, toPower, time) - .on("changePower", (power) => { - this.setPower(power, false); - }) - .on("finished", () => { - return resolve(Consts.CommandFeedback.FEEDBACK_DISABLED); - }) + const powerValues = calculateRamp(fromPower, toPower, time); + powerValues.forEach(value => { + this.setPower(value); + this.addPortOutputSleep(Math.round(time/powerValues.length)); }); + return this.setPower(toPower); } diff --git a/src/devices/device.ts b/src/devices/device.ts index e0def561..c4ee1867 100644 --- a/src/devices/device.ts +++ b/src/devices/device.ts @@ -2,6 +2,7 @@ import { EventEmitter } from "events"; import { IDeviceInterface } from "../interfaces"; import { PortOutputCommand } from "../portoutputcommand"; +import { PortOutputSleep } from "../portoutputsleep"; import * as Consts from "../consts"; @@ -19,7 +20,7 @@ export class Device extends EventEmitter { protected _mode: number | undefined; protected _bufferLength: number = 0; - protected _nextPortOutputCommands: PortOutputCommand[] = []; + protected _nextPortOutputCommands: (PortOutputCommand | PortOutputSleep)[] = []; protected _transmittedPortOutputCommands: PortOutputCommand[] = []; private _hub: IDeviceInterface; @@ -30,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(); @@ -132,9 +132,6 @@ export class Device extends EventEmitter { } public writeDirect (mode: number, data: Buffer, interrupt: boolean = false) { - if (interrupt) { - this.cancelEventTimer(); - } if (this.isWeDo2SmartHub) { 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 { @@ -183,15 +180,29 @@ export class Device extends EventEmitter { this._nextPortOutputCommands = []; return; } - if(this._bufferLength !== this._transmittedPortOutputCommands.length) return; if(!this._nextPortOutputCommands.length) return; - if(this._bufferLength < 2 || this._nextPortOutputCommands[0].interrupt) { + 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(() => { + 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) { const command = this._nextPortOutputCommands.shift(); if(command) { debug("transmit command ", command.startupAndCompletion, command.data); this.send(Buffer.concat([Buffer.from([0x81, this.portId, command.startupAndCompletion]), command.data])); command.state = Consts.CommandFeedback.TRANSMISSION_BUSY; this._transmittedPortOutputCommands.push(command); + this.transmitNextPortOutputCommand(); // if PortOutputSleep this starts timeout // one could start a timer here to ensure finish function is called } } @@ -214,6 +225,12 @@ export class Device extends EventEmitter { return command.promise; } + 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; @@ -309,7 +326,7 @@ export class Device extends EventEmitter { this._missing(); this._busy(); } - // third command can only be interrupt, if busy === 2 it was queued + // third command can only be interrupt, if this._bufferLength === 2 it was queued else { this._missing(); this._missing(); @@ -321,17 +338,6 @@ export class Device extends EventEmitter { this.transmitNextPortOutputCommand(); } - public setEventTimer (timer: NodeJS.Timer) { - this._eventTimer = timer; - } - - public cancelEventTimer () { - if (this._eventTimer) { - clearTimeout(this._eventTimer); - this._eventTimer = null; - } - } - private _ensureConnected () { if (!this.connected) { throw new Error("Device is not connected"); diff --git a/src/devices/light.ts b/src/devices/light.ts index 0b9a00bf..b40abf02 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"; @@ -39,16 +37,12 @@ export class Light extends Device { * @returns {Promise} Resolved upon completion of command. */ public rampBrightness (fromBrightness: number, toBrightness: number, time: number) { - return new Promise((resolve) => { - calculateRamp(this, fromBrightness, toBrightness, time) - .on("changePower", (power) => { - this.setBrightness(power, false); - }) - .on("finished", () => { - return resolve(Consts.CommandFeedback.FEEDBACK_DISABLED); - }); + 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/portoutputsleep.ts b/src/portoutputsleep.ts new file mode 100644 index 00000000..c0476713 --- /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 3c6d10e2..9a4d0f38 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 +} From 3f8a6faee00c97235768de6fcd4489b859eeb0c5 Mon Sep 17 00:00:00 2001 From: bene Date: Mon, 6 Jun 2022 14:19:54 +0200 Subject: [PATCH 3/4] bugfix: wait for send before pushing to transmitted feedback and interrupts must not resolve commands which are being transmitted --- src/devices/device.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/devices/device.ts b/src/devices/device.ts index c4ee1867..2601c14b 100644 --- a/src/devices/device.ts +++ b/src/devices/device.ts @@ -187,6 +187,7 @@ export class Device extends EventEmitter { 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(); @@ -196,13 +197,15 @@ export class Device extends EventEmitter { } if(this._bufferLength !== this._transmittedPortOutputCommands.length) return; if(this._bufferLength < 2 || nextCommand.interrupt) { - const command = this._nextPortOutputCommands.shift(); - if(command) { - debug("transmit command ", command.startupAndCompletion, command.data); - this.send(Buffer.concat([Buffer.from([0x81, this.portId, command.startupAndCompletion]), command.data])); - command.state = Consts.CommandFeedback.TRANSMISSION_BUSY; - this._transmittedPortOutputCommands.push(command); - this.transmitNextPortOutputCommand(); // if PortOutputSleep this starts timeout + 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 } } @@ -215,13 +218,15 @@ export class Device extends EventEmitter { } const command = new PortOutputCommand(data, interrupt); if(interrupt) { - this._nextPortOutputCommands.forEach(command => command.resolve(Consts.CommandFeedback.TRANSMISSION_DISCARDED)); - this._nextPortOutputCommands = [ command ]; - } - else { - this._nextPortOutputCommands.push(command); + 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.transmitNextPortOutputCommand(); + this._nextPortOutputCommands.push(command); + process.nextTick(() => this.transmitNextPortOutputCommand()); return command.promise; } From 5a86ee793b942ff67c7a162a2a06f8fe0526abd4 Mon Sep 17 00:00:00 2001 From: bene Date: Sun, 14 Aug 2022 12:13:25 +0200 Subject: [PATCH 4/4] improve documentation --- src/devices/absolutemotor.ts | 10 +++++----- src/devices/basicmotor.ts | 2 +- src/devices/light.ts | 2 +- src/devices/piezobuzzer.ts | 2 +- src/devices/tachomotor.ts | 16 ++++++++-------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/devices/absolutemotor.ts b/src/devices/absolutemotor.ts index b46651c3..eb19e56c 100644 --- a/src/devices/absolutemotor.ts +++ b/src/devices/absolutemotor.ts @@ -40,8 +40,8 @@ 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. - * @param {boolean} interrupt If true, previous commands are discarded. - * @returns {Promise} Resolved upon 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, interrupt: boolean = false) { if (!this.isVirtualPort && angle instanceof Array) { @@ -72,7 +72,7 @@ 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 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) => { @@ -101,8 +101,8 @@ export class AbsoluteMotor extends TachoMotor { /** * Reset zero to current position * @method AbsoluteMotor#resetZero - * @param {boolean} interrupt If true, previous commands are discarded. - * @returns {Promise} Resolved upon 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 (interrupt: boolean = false) { const data = Buffer.from([0x51, 0x02, 0x00, 0x00, 0x00, 0x00]); diff --git a/src/devices/basicmotor.ts b/src/devices/basicmotor.ts index 3041fd4d..ff6d8d24 100644 --- a/src/devices/basicmotor.ts +++ b/src/devices/basicmotor.ts @@ -19,7 +19,7 @@ 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. - * @param {boolean} interrupt If true, previous commands are discarded. + * @param {boolean} [interrupt=false] If true, previous commands are discarded. * @returns {Promise} Resolved upon completion of command. */ public setPower (power: number, interrupt: boolean = false) { diff --git a/src/devices/light.ts b/src/devices/light.ts index b40abf02..33526ab9 100644 --- a/src/devices/light.ts +++ b/src/devices/light.ts @@ -20,7 +20,7 @@ export class Light extends Device { * @method Light#setBrightness * @param {number} brightness Brightness value between 0-100 (0 is off) * @param {number} brightness Brightness value between 0-100 (0 is off) - * @param {boolean} interrupt If true, previous commands are discarded. + * @param {boolean} [interrupt=false] If true, previous commands are discarded. * @returns {Promise} Resolved upon completion of command. */ public setBrightness (brightness: number, interrupt: boolean = false) { diff --git a/src/devices/piezobuzzer.ts b/src/devices/piezobuzzer.ts index 5a3f8fa5..8de1e056 100644 --- a/src/devices/piezobuzzer.ts +++ b/src/devices/piezobuzzer.ts @@ -21,7 +21,7 @@ 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 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) => { diff --git a/src/devices/tachomotor.ts b/src/devices/tachomotor.ts index 233e6a1c..0e7df863 100644 --- a/src/devices/tachomotor.ts +++ b/src/devices/tachomotor.ts @@ -67,8 +67,8 @@ export class TachoMotor extends BasicMotor { * @method TachoMotor#setAccelerationTime * @param {number} time How long acceleration should last (in milliseconds). * @param {number} profile 0 by default - * @param {boolean} interrupt If true, previous commands are discarded. - * @returns {Promise} Resolved upon 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 setAccelerationTime (time: number, profile: number = 0x00, interrupt: boolean = false) { const message = Buffer.from([0x05, 0x00, 0x00, profile]); @@ -82,8 +82,8 @@ export class TachoMotor extends BasicMotor { * @method TachoMotor#setDecelerationTime * @param {number} time How long deceleration should last (in milliseconds). * @param {number} profile 0 by default - * @param {boolean} interrupt If true, previous commands are discarded. - * @returns {Promise} Resolved upon 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 setDecelerationTime (time: number, profile: number = 0x00, interrupt: boolean = true) { const message = Buffer.from([0x06, 0x00, 0x00, profile]); @@ -97,8 +97,8 @@ 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). - * @param {boolean} interrupt If true, previous commands are discarded. - * @returns {Promise} Resolved upon 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 setSpeed (speed: [number, number] | number, time: number | undefined, interrupt: boolean = false) { if (!this.isVirtualPort && speed instanceof Array) { @@ -133,8 +133,8 @@ 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. - * @param {boolean} interrupt If true, previous commands are discarded. - * @returns {Promise} Resolved upon 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, interrupt: boolean = false) { if (!this.isVirtualPort && speed instanceof Array) {