UNPKG

boost-movehub

Version:

Connects with your Lego Boost Movehub vie Bluetooth Low Energy (BLE).

349 lines (320 loc) 10.3 kB
const { EventEmitter } = require("events"); const MessageFactory = require("./MessageFactory"); const MovehubPorts = require("./MovehubPorts"); const PortOutputCommandFeedback = require("./messages/PortOutputCommandFeedback"); const HubAttached = require("./messages/HubAttached"); const PortInputFormat = require("./messages/PortInputFormat"); const PortValueSingle = require("./messages/PortValueSingle"); const UnknownMessage = require("./messages/UnknownMessage"); const HubAction = require("./messages/HubAction"); const HubAlert = require("./messages/HubAlert"); const { toHexString } = require("./helpers"); const DEFAULT_OPTIONS = { logger: {}, neededDevices: [], reconnect: true }; const LEGO_CHARACTERISTIC = "000016241212efde1623785feabcd123"; // TODO: Add different Error classes for different error cases /** * Movehub implementation that handles all the sending and receiving of messages * between us and the Movehub device. */ module.exports = class Hub extends EventEmitter { /** * @param {Object} peripheral The Movehub data received by `noble`. * @param {Object} [options] Some options * @param {boolean} [options.reconnect=true] Set to true if we should try reconnect if connection gets lost. * @param {number[]} [options.neededDevices=[]] PortIds that have to register themselves, bevore the `hubConnected` event is triggered. * @param {Object} [options.logger={}] Logging Interface. Object containing methods like `error`, `info`, `log`, `silly` or `warn`. */ constructor(peripheral, options = DEFAULT_OPTIONS) { super(); this.peripheral = peripheral; this.logger = options.logger || {}; this.doReconnection = options.reconnect; this.neededDevices = options.neededDevices; this.ports = new MovehubPorts({ logger: this.logger }); this.connect(); } /** * MAC Address of connected Movehub. */ get address() { return this.peripheral.address; } /** * UUID of connected Movehub. */ get uuid() { return this.peripheral.uuid; } /** * Tries connecting to initialized Movehub. */ connect() { this._log("debug", `Trying to connect to peripheral #${this.uuid}.`); this.peripheral.connect(err => { if (err) { this._log("error", "Could not connect to peripheral."); /** * Fires when an error is received when connecting to the Movehub. * @event Hub#error * @param {Error} error */ this.emit("error", err); return; } this._log("debug", `Peripheral #${this.uuid} connected.`); this._startIntervalForRssi(); this._handleDisconnection(); this._doServiceDiscovery(); }); } /** * Sends disconnection signal and disconnects from Movehub. */ disconnect() { this.sendMessage(HubAction.build(HubAction.DISCONNECT)); } /** * Sends SwitchOff signal to Movehub. */ switchOff() { this.sendMessage(HubAction.build(HubAction.SWITCH_OFF_HUB)); } /** * Sends signal to immediately shut down Movehub. */ immediateShutdown() { this.sendMessage(HubAction.build(HubAction.IMMEDIATE_SHUTDOWN)); } /** * Sends given device message to connected Movehub. * * @param {DeviceMessage} msg Message to send. * @param {function} callback */ sendMessage(msg, callback = null) { this._log("debug", "Sending message", msg.toString(), msg.data); this.characteristic.write(msg.data, false, (...args) => { this._log("silly", "Callback from write", args); callback && callback(...args); }); } /** * This subscribes to all or specific Hub Alerts. * * @param {number[]} [filter] List of HubAlert to subscribe to. Default are all. */ activateAlerts(filter = null) { if (filter === null) { filter = [ (HubAlert.LOW_VOLTAGE = 0x01), (HubAlert.HIGH_CURRANT = 0x02), (HubAlert.LOG_SIGNAL_STRGENTH = 0x03), (HubAlert.OVER_POWER_CONDITION = 0x04) ]; } filter.forEach(alertType => { this.sendMessage(HubAlert.build(alertType, HubAlert.OP_ENABLE_UPDATES)); }); } /** * This unsubscribes from all or specifc Hub Alerts. * * @param {number[]} [filter] List of HubAlert to subscribe to. Default are all. */ deactivateAlerts(filter = null) { if (filter === null) { filter = [ (HubAlert.LOW_VOLTAGE = 0x01), (HubAlert.HIGH_CURRANT = 0x02), (HubAlert.LOG_SIGNAL_STRGENTH = 0x03), (HubAlert.OVER_POWER_CONDITION = 0x04) ]; } filter.forEach(alertType => { this.sendMessage(HubAlert.build(alertType, HubAlert.OP_DISABLE_UPDATES)); }); } _handleDisconnection() { this.peripheral.on("disconnect", () => { this._log("debug", `Peripheral #${this.uuid} disconnected.`); /** * Fires when a Movehub gets disconnected. * @event Hub#disconnect */ this.emit("disconnect"); if (this.noReconnect) { this.noReconnect = false; } else { this.reconnectInterval = setInterval(() => { if (this.peripheral.state === "disconnected") { this.connect(); } }, 1000); } }); } _startIntervalForRssi() { setInterval(() => { this.peripheral.updateRssi(); }, 1000); this.peripheral.on("rssiUpdate", rssi => { if (this.rssi !== rssi) { /** * Fires when new RSSI value is received from BLE device. * @event Hub#rssi * @param {number} rssi The received RSSI value. */ this.emit("rssi", rssi); this.rssi = rssi; } }); } _doServiceDiscovery() { this.peripheral.discoverAllServicesAndCharacteristics( (err, services, characteristics) => { if (err) { this._log( "error", "Error in services and characteristics discovery.", err ); this.emit("error", err); } services.forEach(service => { this._log("info", "Service found.", service.uuid); }); characteristics.forEach(c => { this._log("info", "Characteristic found.", c.uuid, c); if (c.uuid === LEGO_CHARACTERISTIC) { this.characteristic = c; c.on("data", data => this._receiveMessage(MessageFactory.create(data)) ); c.subscribe((err, data) => { if (err) { this._log("error", "Error in characteristic:", err); this.emit("error", err); } else { this._log("debug", "Received from characteristic:", data); } }); } }); } ); } /** * Called when a Movehub sends a message. * * @param {DeviceMessage} msg */ _receiveMessage(msg) { this._log("debug", "Parse data:", msg && msg.data); // TODO: Add timeout for not registering all needed devices in time. if (msg instanceof HubAttached) { this._log( "debug", "Message:", msg.portId, msg.eventType, msg.ioType, msg.ioMembers ); this.ports.registerFromMessage(msg); this._registerEmitterOnPort(msg.portId); if ( !this.connected && this.ports.builtInDevicesRegistered && this._allNeededDevicesRegistered ) { /** * Fires when a connection to the Move Hub is established. * @event Hub#connect */ this.emit("connect"); this.connected = true; } } else if (msg instanceof PortInputFormat) { this._log("debug", `Got answer for setup on ${toHexString(msg.portId)}`); const peripheral = this.ports.get(msg.portId); if (peripheral) { if (peripheral.receiveSubscriptionAck) { peripheral.receiveSubscriptionAck(msg); } else { this._log( "warn", `Undefined method .receiveSubscriptionAck for peripheral ${peripheral}` ); } } else { this._log( "warn", `Received message for unregistered port ${msg.portId}`, msg.toString() ); } } else if (msg instanceof PortValueSingle) { // this._log("debug", `Got value: ${msg.toString()}`); const peripheral = this.ports.get(msg.portId); if (peripheral) { if (peripheral.receiveValue) { peripheral.receiveValue(msg); } else { this._log( "warn", `Undefined method .receiveValue for peripheral ${peripheral}` ); } } else { this._log( "warn", `Received message for unregistered port ${msg.portId}`, msg.toString() ); } } else if (msg instanceof HubAlert) { this._log("info", "Got Alert:", msg.alertTypeToString()); /** * Fires on received Hub Alert. * @event Hub#hubAlert * @param value {string} String representation of HubAlert that happened. */ this.emit("hubAlert", msg.alertTypeToString()); } else if (msg instanceof PortOutputCommandFeedback) { this._log("info", "Got feedback:", msg.toString()); const data = msg.valuesForPorts; Object.keys(data).forEach(portId => { const peripheral = this.ports.get(portId); if (peripheral.receiveCommandFeedback) { peripheral.receiveCommandFeedback(data[portId]); } else { this._log( "warn", `Undefined method .receiveCommandFeedback for peripheral ${peripheral}` ); } }); } else if (msg instanceof UnknownMessage) { this._log("warn", `Unknown message ${msg.toString()}`); } else { this._log("debug", `Unprocessed message ${msg.toString()}`); } } _registerEmitterOnPort(portId) { const peripheral = this.ports.get(portId); if (peripheral.emitAs) { peripheral.on("value", value => this.emit(peripheral.emitAs, value)); } } get _allNeededDevicesRegistered() { return this.ports.containsAll(this.neededDevices); } _log(type, ...message) { this.logger[type] && this.logger[type]("[Hub]", new Date().toISOString(), ...message); } };