diff --git a/src/consts.ts b/src/consts.ts index 4a853e04..e692b297 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -186,3 +186,18 @@ export enum BLECharacteristic { WEDO2_NAME_ID = "00001524-1212-efde-1523-785feabcd123", // "1524" LPF2_ALL = "00001624-1212-efde-1623-785feabcd123" } + + +export enum ValueType { + Int8, + Int16, + Int32, + Float +} + +export const VALUE_SIZE = { + [ValueType.Int8]: 1, + [ValueType.Int16]: 2, + [ValueType.Int32]: 4, + [ValueType.Float]: 4 +}; diff --git a/src/hub.ts b/src/hub.ts index 684fd5c6..dbcda968 100644 --- a/src/hub.ts +++ b/src/hub.ts @@ -161,12 +161,29 @@ export class Hub extends EventEmitter { if (mode !== undefined) { newMode = mode; } + this._ports[port].mode = newMode; this._activatePortDevice(this._portLookup(port).value, this._portLookup(port).type, newMode, 0x00, () => { return resolve(); }); }); } + public subscribeByModeName (port: string, mode?: string) { + const { modes } = this._portLookup(port); + const modeCode = modes.reduce( + (foundMode: number | null, definition, index) => { + return definition.name === mode ? index : foundMode; + }, + null + ); + + if (!modeCode) { + throw new Error('Unknown device mode'); + } else { + return this.subscribe(port, modeCode); + } + } + /** * Unsubscribe to sensor notifications on a given port. * @method Hub#unsubscribe @@ -176,6 +193,7 @@ export class Hub extends EventEmitter { public unsubscribe (port: string) { return new Promise((resolve, reject) => { const mode = this._getModeForDeviceType(this._portLookup(port).type); + this._ports[port].mode = null; this._deactivatePortDevice(this._portLookup(port).value, this._portLookup(port).type, mode, 0x00, () => { return resolve(); }); @@ -268,7 +286,7 @@ export class Hub extends EventEmitter { if (port.connected) { port.type = type; if (this.autoSubscribe) { - this._activatePortDevice(port.value, type, this._getModeForDeviceType(type), 0x00); + this.subscribe(port.id, this._getModeForDeviceType(type)); /** * Emits when a motor or sensor is attached to the Hub. * @event Hub#attach @@ -279,6 +297,7 @@ export class Hub extends EventEmitter { } } else { port.type = Consts.DeviceType.UNKNOWN; + port.modes = []; debug(`Port ${port.id} disconnected`); /** * Emits when an attached motor or sensor is detached from the Hub. diff --git a/src/lpf2hub.ts b/src/lpf2hub.ts index d8393d11..3e27f4f0 100644 --- a/src/lpf2hub.ts +++ b/src/lpf2hub.ts @@ -1,7 +1,7 @@ import { Peripheral } from "@abandonware/noble"; import { Hub } from "./hub"; -import { Port } from "./port"; +import { IPortMode, Port } from "./port"; import * as Consts from "./consts"; import { toBin, toHex } from "./utils"; @@ -294,8 +294,8 @@ export class LPF2Hub extends Hub { const hwVersion = LPF2Hub.decodeVersion(data.readInt32LE(7)); const swVersion = LPF2Hub.decodeVersion(data.readInt32LE(11)); modeInfoDebug(`Port ${toHex(data[3])}, hardware version ${hwVersion}, software version ${swVersion}`); - this._sendPortInformationRequest(data[3]); } + this._sendPortInformationRequest(data[3]); if (!port) { if (data[4] === 0x02) { @@ -362,31 +362,46 @@ export class LPF2Hub extends Hub { private _parseModeInformationResponse (data: Buffer) { - const port = toHex(data[3]); + const portHex = toHex(data[3]); const mode = data[4]; const type = data[5]; + const port = this._getPortForPortNumber(data[3]) as Port; + + if (!port) { + return false; + } + + port.modes[mode] = port.modes[mode] || {}; + switch (type) { case 0x00: // Mode Name - modeInfoDebug(`Port ${port}, mode ${mode}, name ${data.slice(6, data.length).toString()}`); + const name = data.slice(6, data.length).toString(); + port.modes[mode].name = name; + modeInfoDebug(`Port ${portHex}, mode ${mode}, name ${name}`); break; case 0x01: // RAW Range - modeInfoDebug(`Port ${port}, mode ${mode}, RAW min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`); + port.modes[mode].rawMin = data.readFloatLE(6); + modeInfoDebug(`Port ${portHex}, mode ${mode}, RAW min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`); break; case 0x02: // PCT Range - modeInfoDebug(`Port ${port}, mode ${mode}, PCT min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`); + modeInfoDebug(`Port ${portHex}, mode ${mode}, PCT min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`); break; case 0x03: // SI Range - modeInfoDebug(`Port ${port}, mode ${mode}, SI min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`); + modeInfoDebug(`Port ${portHex}, mode ${mode}, SI min ${data.readFloatLE(6)}, max ${data.readFloatLE(10)}`); break; case 0x04: // SI Symbol - modeInfoDebug(`Port ${port}, mode ${mode}, SI symbol ${data.slice(6, data.length).toString()}`); + const unit = data.slice(6, data.length).toString(); + port.modes[mode].unit = unit; + modeInfoDebug(`Port ${portHex}, mode ${mode}, SI symbol ${unit}`); break; case 0x80: // Value Format const numValues = data[6]; - const dataType = ["8bit", "16bit", "32bit", "float"][data[7]]; + const dataType = [Consts.ValueType.Int8, Consts.ValueType.Int16, Consts.ValueType.Int32, Consts.ValueType.Float][data[7]]; const totalFigures = data[8]; const decimals = data[9]; - modeInfoDebug(`Port ${port}, mode ${mode}, Value ${numValues} x ${dataType}, Decimal format ${totalFigures}.${decimals}`); + + port.modes[mode].valueType = dataType; + modeInfoDebug(`Port ${portHex}, mode ${mode}, Value ${numValues} x ${dataType}, Decimal format ${totalFigures}.${decimals}`); } } @@ -439,174 +454,183 @@ export class LPF2Hub extends Hub { return; } - if (port && port.connected) { - switch (port.type) { - case Consts.DeviceType.WEDO2_DISTANCE: { - let distance = data[4]; - if (data[5] === 1) { - distance = data[4] + 255; - } - /** - * Emits when a distance sensor is activated. - * @event LPF2Hub#distance - * @param {string} port - * @param {number} distance Distance, in millimeters. - */ - this.emit("distance", port.id, distance * 10); - break; - } - case Consts.DeviceType.BOOST_DISTANCE: { - - /** - * Emits when a color sensor is activated. - * @event LPF2Hub#color - * @param {string} port - * @param {Color} color - */ - if (data[4] <= 10) { - this.emit("color", port.id, data[4]); - } - - let distance = data[5]; - const partial = data[7]; - - if (partial > 0) { - distance += 1.0 / partial; - } - - distance = Math.floor(distance * 25.4) - 20; - - this.emit("distance", port.id, distance); - - /** - * A combined color and distance event, emits when the sensor is activated. - * @event LPF2Hub#colorAndDistance - * @param {string} port - * @param {Color} color - * @param {number} distance Distance, in millimeters. - */ - if (data[4] <= 10) { - this.emit("colorAndDistance", port.id, data[4], distance); - } - break; - } - case Consts.DeviceType.WEDO2_TILT: { - const tiltX = data.readInt8(4); - const tiltY = data.readInt8(5); - this._lastTiltX = tiltX; - this._lastTiltY = tiltY; - /** - * Emits when a tilt sensor is activated. - * @event LPF2Hub#tilt - * @param {string} port If the event is fired from the Move Hub or Control+ Hub's in-built tilt sensor, the special port "TILT" is used. - * @param {number} x - * @param {number} y - * @param {number} z (Only available when using a Control+ Hub) - */ - this.emit("tilt", port.id, this._lastTiltX, this._lastTiltY, this._lastTiltZ); - break; - } - case Consts.DeviceType.BOOST_TACHO_MOTOR: { - const rotation = data.readInt32LE(4); - /** - * Emits when a rotation sensor is activated. - * @event LPF2Hub#rotate - * @param {string} port - * @param {number} rotation - */ - this.emit("rotate", port.id, rotation); - break; - } - case Consts.DeviceType.BOOST_MOVE_HUB_MOTOR: { - const rotation = data.readInt32LE(4); - this.emit("rotate", port.id, rotation); - break; - } - case Consts.DeviceType.CONTROL_PLUS_LARGE_MOTOR: { - const rotation = data.readInt32LE(4); - this.emit("rotate", port.id, rotation); - break; - } - case Consts.DeviceType.CONTROL_PLUS_XLARGE_MOTOR: { - const rotation = data.readInt32LE(4); - this.emit("rotate", port.id, rotation); - break; - } - case Consts.DeviceType.CONTROL_PLUS_TILT: { - const tiltZ = data.readInt16LE(4); - const tiltY = data.readInt16LE(6); - const tiltX = data.readInt16LE(8); - this._lastTiltX = tiltX; - this._lastTiltY = tiltY; - this._lastTiltZ = tiltZ; - this.emit("tilt", "TILT", this._lastTiltX, this._lastTiltY, this._lastTiltZ); - break; - } - case Consts.DeviceType.CONTROL_PLUS_ACCELEROMETER: { - const accelX = Math.round((data.readInt16LE(4) / 28571) * 2000); - const accelY = Math.round((data.readInt16LE(6) / 28571) * 2000); - const accelZ = Math.round((data.readInt16LE(8) / 28571) * 2000); - /** - * Emits when accelerometer detects movement. Measured in DPS - degrees per second. - * @event LPF2Hub#accel - * @param {string} port - * @param {number} x - * @param {number} y - * @param {number} z - */ - this.emit("accel", "ACCEL", accelX, accelY, accelZ); - break; - } - case Consts.DeviceType.BOOST_TILT: { - const tiltX = data.readInt8(4); - const tiltY = data.readInt8(5); - this._lastTiltX = tiltX; - this._lastTiltY = tiltY; - this.emit("tilt", port.id, this._lastTiltX, this._lastTiltY, this._lastTiltZ); - break; - } - case Consts.DeviceType.POWERED_UP_REMOTE_BUTTON: { - switch (data[4]) { - case 0x01: { - this.emit("button", port.id, Consts.ButtonState.UP); - break; - } - case 0xff: { - this.emit("button", port.id, Consts.ButtonState.DOWN); - break; - } - case 0x7f: { - this.emit("button", port.id, Consts.ButtonState.STOP); - break; - } - case 0x00: { - this.emit("button", port.id, Consts.ButtonState.RELEASED); - break; - } - } - break; - } - case Consts.DeviceType.DUPLO_TRAIN_BASE_COLOR: { - if (data[4] <= 10) { - this.emit("color", port.id, data[4]); - } - break; - } - case Consts.DeviceType.DUPLO_TRAIN_BASE_SPEEDOMETER: { - /** - * Emits on a speed change. - * @event LPF2Hub#speed - * @param {string} port - * @param {number} speed - */ - const speed = data.readInt16LE(4); - this.emit("speed", port.id, speed); - break; + if (port.connected && port.mode !== null && port.modes[port.mode]) { + const mode = port.modes[port.mode]; + const values = []; + const signed = mode.rawMin < 0; + + for (let index = 4; index < data.length; index += Consts.VALUE_SIZE[mode.valueType]) { + switch (mode.valueType) { + case Consts.ValueType.Int8: + values.push(signed ? data.readInt8(index) : data.readUInt8(index)); + break; + case Consts.ValueType.Int16: + values.push(signed ? data.readInt16LE(index) : data.readUInt16LE(index)); + break; + case Consts.ValueType.Int32: + values.push(signed ? data.readInt32LE(index) : data.readUInt32LE(index)); + break; + case Consts.ValueType.Float: + values.push(data.readFloatLE(index)); + break; } } + this.emit("sensor", port.id, mode, values); + + this._emitSensorEvent(port, mode, values); } } - + private _emitSensorEvent(port: Port, mode: IPortMode, values: number[]) { + const modeToEvent: { [key: string]: string } = { + /** + * Emits when a color sensor is activated. + * @event LPF2Hub#color + * @param {string} port + * @param {Color} color Uses LWP3 color codes (0 to 10, -1 is no color). + */ + COLOR: "color", + /** + * Emits when a distance sensor is activated. + * @event LPF2Hub#distance + * @param {string} port + * @param {number} distance Distance indicator, from 10 to 0. + */ + PROX: "distance", + "LPF2-DETECT": "distance", + /** + * Emits when a count sensor is activated. + * @event LPF2Hub#count + * @param {string} port + * @param {number} count Times a obstacle got detected. + */ + COUNT: "count", + "LPF2-COUNT": "count", + /** + * Emits when a reflectivity sensor is activated. + * @event LPF2Hub#reflectivity + * @param {string} port + * @param {number} reflectivity Percentage of reflexion of the obstacle. + */ + REFLT: "reflectivity", + /** + * Emits when a luminosity sensor is activated. + * @event LPF2Hub#luminosity + * @param {string} port + * @param {number} luminosity Percentage of ambiant luminosity. + */ + AMBI: "luminosity", + /** + * Emits when an RGB color sensor is activated. + * @event LPF2Hub#colorRGB + * @param {string} port + * @param {number} red + * @param {number} green + * @param {number} blue + * + */ + RGB_I: "colorRGB", + // SPEC_1: "", // it emits multiple events + /** + * Emits when a motor power change. + * @event LPF2Hub#power + * @param {string} port + * @param {number} power Percentage of power used to rotate the motor. + */ + POWER: "power", + /** + * Emits when a motor speed changes. + * @event LPF2Hub#speed + * @param {string} port + * @param {number} speed Percentage of speed set to rotate the motor. + */ + SPEED: "speed", + /** + * Emits when a motor rotates. + * @event LPF2Hub#rotate + * @param {string} port + * @param {number} angle angle the motor rotated by. + */ + POS: "rotate", // POS for CONTROL+ GYRO is an angle event + /** + * Emits when a motor rotates. + * @event LPF2Hub#position + * @param {string} port + * @param {number} angle the position of the motor related to its absolute zero. + */ + APOS: "position", + /** + * Emits when a motor starts to rotate. + * @event LPF2Hub#load + * @param {string} port + * @param {number} load Percentage of load of the motor. + */ + LOAD: "load", + /** + * Emits when an angle (tilt, gyro) sensor is activated. + * @event LPF2Hub#angle + * @param {string} port + * @param {number} x + * @param {number} y + * @param {number} z (Unavailable from WeDo2 tilt sensor) + */ + ANGLE: "angle", + "LPF2-ANGLE": "angle", + /** + * Emits when a tilt sensor is activated. + * @event LPF2Hub#tilt + * @param {string} port + * @param {number} direction + */ + TILT: "tilt", + "LPF2-TILT": "tilt", + /** + * Emits when a orientation sensor is activated. + * @event LPF2Hub#orientation + * @param {string} port + * @param {number} orientation + */ + ORINT: "orientation", + /** + * Emits when a tilt sensor is activated. + * @event LPF2Hub#impact + * @param {string} port + * @param {number} direction + */ + IMPCT: "impact", + "LPF2-CRASH": "impact", + IMP: "impact", + /** + * Emits when an accelerometer sensor is activated. + * @event LPF2Hub#angle + * @param {string} port + * @param {number} x + * @param {number} y + * @param {number} z + */ + ACCEL: "acceleration", + ROT: "acceleration" + }; + + if (mode.name === "SPEC_1") { + this.emit("color", port.id, values[0]); + this.emit("distance", port.id, values[1]); + this.emit("reflect", port.id, values[3]); + /** + * A combined color and distance event, emits when the sensor is activated. + * @event LPF2Hub#colorAndDistance + * @param {string} port + * @param {Color} color Uses LWP3 color codes (0 to 10, -1 is no color). + * @param {number} distance Distance indicator, from 10 to 0. + */ + this.emit("colorAndDistance", port.id, values[0], values[1]); + } else if (mode.name === "POS" && port.type === Consts.DeviceType.CONTROL_PLUS_TILT) { + this.emit("angle", port.id, ...values); + } else if (modeToEvent[mode.name]) { + this.emit(modeToEvent[mode.name], port.id, ...values); + } else { + debug("Unhandle mode event", mode.name); + } + } } diff --git a/src/port.ts b/src/port.ts index 0a279cb2..90efc852 100644 --- a/src/port.ts +++ b/src/port.ts @@ -1,15 +1,21 @@ import * as Consts from "./consts"; +export interface IPortMode { + name: string; + unit: string; + rawMin: number; + valueType: Consts.ValueType; +} export class Port { - - public id: string; public value: number; public type: Consts.DeviceType; public connected: boolean = false; public busy: boolean = false; public finished: (() => void) | null = null; + public mode: number | null = null; + public modes: IPortMode[] = []; private _eventTimer: NodeJS.Timer | null = null;