UNPKG

obniz

Version:

obniz sdk for javascript

909 lines (908 loc) 32.9 kB
/** * @packageDocumentation * @module Parts.Linking */ /* ------------------------------------------------------------------ * node-linking - device.js * * Copyright (c) 2017-2019, Futomi Hatano, All rights reserved. * Released under the MIT license * Date: 2019-11-03 * ---------------------------------------------------------------- */ 'use strict'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const advertising_1 = __importDefault(require("./advertising")); const service_1 = __importDefault(require("./service")); class LinkingDevice { constructor(peripheral) { this.PRIMARY_SERVICE_UUID = 'b3b3690150d34044808d50835b13a6cd'; this.WRITE_CHARACTERRISTIC_UUID = 'b3b3910150d34044808d50835b13a6cd'; this.INDICATE_CHARACTERRISTIC_UUID = 'b3b3910250d34044808d50835b13a6cd'; this.info = {}; this.connected = false; this.onconnect = null; this.onconnectprogress = null; this.ondisconnect = null; this.onnotify = null; this.services = { deviceName: null, led: null, vibration: null, button: null, gyroscope: null, accelerometer: null, orientation: null, battery: null, temperature: null, humidity: null, pressure: null, openclose: null, human: null, move: null, illuminance: null, }; this._service = null; this._div_packet_queue = []; this._LinkingService = new service_1.default(); this._onresponse = null; this._write_response_timeout = 30000; // msec this._generic_access_service = { SERVICE_UUID: '1800', service: null, device_name: { CHARACTERRISTIC_UUID: '2a00', char: null, }, }; this.advertisement = advertising_1.default.parse(peripheral); this._peripheral = peripheral; } /** * @deprecated * @param setting */ connect(setting) { return this.connectWait(setting); } async connectWait(setting) { if (this.connected === true) { throw new Error('The device has been already connected.'); } let onprogress = this.onconnectprogress; if (!this._isFunction(this.onconnectprogress)) { onprogress = () => { // do nothing. }; } const peripheral = this._peripheral; onprogress({ step: 1, desc: 'CONNECTING' }); try { peripheral.ondisconnect = async () => { await this._cleanWait(); if (this._isFunction(this.ondisconnect)) { this.ondisconnect({ wasClean: false }); } }; await peripheral.connectWait(setting); onprogress({ step: 2, desc: 'CONNECTION_ESTABLISHED' }); onprogress({ step: 3, desc: 'GETTING_CHARACTERISTICS' }); await this._getServicesAndChars(); onprogress({ step: 4, desc: 'SUBSCRIBING' }); await this._subscribeForIndicateWait(); onprogress({ step: 5, desc: 'GETTING_DEVICE_INFOMATION' }); let res; res = await this.write('GET_DEVICE_INFORMATION'); this.info.id = ''; if ('deviceId' in res.data) { this.info.id = res.data.deviceId; } this.info.uid = ''; if ('deviceUid' in res.data) { this.info.uid = res.data.deviceUid; } this.info.services = {}; if ('serviceList' in res.data) { res.data.serviceList.forEach((o) => { this.info.services[o.name] = o.id; }); } this.info.capabilities = {}; if ('deviceCapability' in res.data) { res.data.deviceCapability.forEach((o) => { this.info.capabilities[o.name] = o.id; }); } this.info.exsensors = {}; if ('exSensorType' in res.data) { res.data.exSensorType.forEach((o) => { this.info.exsensors[o.name] = o.id; }); } onprogress({ step: 6, desc: 'GETTING_NOTIFY_CATEGORIES' }); res = await this._writeConfirmNotifyCategory(); this.info.notifyCategories = {}; if (res) { if ('notifyCategory' in res.data) { res.data.notifyCategory.forEach((o) => { this.info.notifyCategories[o.name] = o.id; }); } } onprogress({ step: 7, desc: 'GETTING_SETTING_INFORMATION' }); res = await this._writeGetSettingInformation(); this.info.settings = {}; if (res) { if ('settingInformationData' in res.data) { res.data.settingInformationData.forEach((o) => { this.info.settings[o.name] = o; }); } } onprogress({ step: 8, desc: 'GETTING_LED_COLOR_NAMES' }); res = await this._writeGetSettingName('LEDColorName'); if (res) { this.info.settings.LED.colors = res.data.settingNameData; } onprogress({ step: 9, desc: 'GETTING_LED_PATTERN_NAMES' }); res = await this._writeGetSettingName('LEDPatternName'); if (res) { this.info.settings.LED.patterns = res.data.settingNameData; } onprogress({ step: 10, desc: 'GETTING_VIBRATION_PATTERN_NAMES' }); res = await this._writeGetSettingName('VibrationPatternName'); if (res) { this.info.settings.Vibration.patterns = res.data.settingNameData; } onprogress({ step: 11, desc: 'GETTING_BEEP_PATTERN_NAMES' }); res = await this._writeGetSettingName('BeepPatternName'); if (res) { this.info.settings.Beep.patterns = res.data.settingNameData; } this._LinkingService.setDeviceInfo(this.info); this._initServices(); this.connected = true; if (this._isFunction(this.onconnect)) { this.onconnect(); } onprogress({ step: 12, desc: 'COMPLETED' }); } catch (e) { onprogress({ step: 0, desc: 'FAILED' }); await this._cleanWait(); throw e; } } _wait(msec) { const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, msec); }); return promise; } _writeConfirmNotifyCategory() { const promise = new Promise((resolve, reject) => { if (!('PeripheralDeviceNotification' in this.info.services)) { resolve(null); return; } this.write('CONFIRM_NOTIFY_CATEGORY') .then((res) => { resolve(res); }) .catch((error) => { reject(error); }); }); return promise; } _writeGetSettingInformation() { const promise = new Promise((resolve, reject) => { if (!('PeripheralDeviceSettingOperation' in this.info.services)) { resolve(null); return; } this.write('GET_SETTING_INFORMATION') .then((res) => { resolve(res); }) .catch((error) => { reject(error); }); }); return promise; } _writeGetSettingName(name) { const promise = new Promise((resolve, reject) => { if (!('PeripheralDeviceSettingOperation' in this.info.services)) { resolve(null); return; } const s = this.info.settings; if (name === 'LEDColorName') { if (!('LED' in s && s.LED.colorMax)) { resolve(null); return; } } else if (name === 'LEDPatternName') { if (!('LED' in s && s.LED.patternMax)) { resolve(null); return; } } else if (name === 'VibrationPatternName') { if (!('Vibration' in s && s.Vibration.patternMax)) { resolve(null); return; } } else if (name === 'BeepPatternName') { if (!('Beep' in s && s.Beep.patternMax)) { resolve(null); return; } } this.write('GET_SETTING_NAME', { SettingNameType: name }) .then((res) => { resolve(res); }) .catch((error) => { reject(error); }); }); return promise; } _isFunction(o) { return o && typeof o === 'function' ? true : false; } _getServicesAndChars() { const peripheral = this._peripheral; const service = peripheral.getService(this.PRIMARY_SERVICE_UUID); if (!service) { throw new Error('No service was found'); } const write_char = service.getCharacteristic(this.WRITE_CHARACTERRISTIC_UUID); const indicate_char = service.getCharacteristic(this.INDICATE_CHARACTERRISTIC_UUID); if (!(write_char && indicate_char)) { throw new Error('No characteristic was found'); } this.char_write = write_char; this.char_indicate = indicate_char; this._service = service; const ga_service = peripheral.getService(this._generic_access_service.SERVICE_UUID); if (ga_service) { const ga_char = ga_service.getCharacteristic(this._generic_access_service.device_name.CHARACTERRISTIC_UUID); if (ga_service && ga_char) { this._generic_access_service.service = ga_service; this._generic_access_service.device_name.char = ga_char; } } } /** * @deprecated */ _subscribeForIndicate() { return this._subscribeForIndicateWait(); } async _subscribeForIndicateWait() { await this.char_indicate.registerNotifyWait((data) => { this._receivedPacket(Buffer.from(data)); }); } _receivedPacket(buf) { // console.log("receive raw packet ", buf); const new_buf = Buffer.alloc(buf.length - 1); buf.copy(new_buf, 0, 1, buf.length); this._div_packet_queue.push(new_buf); if (this._isExecutedPacket(buf)) { const header_byte = Buffer.from([buf.readUInt8(0)]); this._div_packet_queue.unshift(header_byte); const total_buf = Buffer.concat(this._div_packet_queue); this._receivedIndicate(total_buf); this._div_packet_queue = []; } } _isExecutedPacket(buf) { const ph = buf.readUInt8(0); return ph & 0x00000001 ? true : false; } _receivedIndicate(buf) { const parsed = this._LinkingService.parseResponse(buf); // console.log("linking buf parse", buf, JSON.stringify(parsed, null, 2)); if (!parsed) { return; } if (parsed.messageName.match(/_RESP$/)) { if (this._onresponse) { this._onresponse(parsed); } } else { // All notifications if (this._isFunction(this.onnotify)) { this.onnotify(parsed); } // Button if (parsed.serviceId === 2) { // PeripheralDeviceOperation Service if (parsed.messageId === 0) { // NOTIFY_PD_OPERATION // let f = this.services['button']['onnotify']; let f = null; if (this.services.button) { if (this._isFunction(this.services.button.onnotify)) { f = this.services.button.onnotify; } } if (f) { let button = null; parsed.parameters.forEach((p) => { if (p.parameterId === 2) { // ButtonId button = { buttonId: p.buttonId, buttonName: p.buttonName, }; } }); f(button); } } // Sensors } else if (parsed.serviceId === 3) { // PeripheralDeviceSensorInformation Service if (parsed.messageId === 4) { // NOTIFY_PD_SENSOR_INFO let sensor_type = -1; let res = {}; parsed.parameters.forEach((p) => { const pid = p.parameterId; if (pid === 2) { // SensorType sensor_type = p.sensorTypeCode; } else { if (sensor_type.toString().match(/^(0|1|2)$/)) { // Gyroscope, Accelerometer, Orientation if (pid === 4) { // X_value res.x = p.xValue; } else if (pid === 5) { // Y_value res.y = p.yValue; } else if (pid === 6) { // Z_value res.z = p.zValue; } } else if (sensor_type === 3) { // Battery res = { charge: p.charge, level: p.level, }; } else if (sensor_type === 4) { // Temperature res.temperature = p.temperature; } else if (sensor_type === 5) { // Humidity res.humidity = p.humidity; } else if (sensor_type === 6) { // Aire pressure res.pressure = p.pressure; } else if (sensor_type === 7) { // Opening and closing res.openclose = p.openclose; } else if (sensor_type === 8) { // Human detection res.human = p.human; } else if (sensor_type === 9) { // Move res.move = p.move; } else if (sensor_type === 0x0a) { // Illuminance res.illuminance = p.illuminance; } } }); let f = null; if (sensor_type === 0) { if (this.services.gyroscope) { f = this.services.gyroscope.onnotify; } } else if (sensor_type === 1) { if (this.services.accelerometer) { f = this.services.accelerometer.onnotify; } } else if (sensor_type === 2) { if (this.services.orientation) { f = this.services.orientation.onnotify; } } else if (sensor_type === 3) { if (this.services.battery) { f = this.services.battery.onnotify; } } else if (sensor_type === 4) { if (this.services.temperature) { f = this.services.temperature.onnotify; } } else if (sensor_type === 5) { if (this.services.humidity) { f = this.services.humidity.onnotify; } } else if (sensor_type === 6) { if (this.services.pressure) { f = this.services.pressure.onnotify; } } else if (sensor_type === 7) { if (this.services.openclose) { f = this.services.openclose.onnotify; } } else if (sensor_type === 8) { if (this.services.human) { f = this.services.human.onnotify; } } else if (sensor_type === 9) { if (this.services.move) { f = this.services.move.onnotify; } } else if (sensor_type === 0x0a) { if (this.services.illuminance) { f = this.services.illuminance.onnotify; } } if (this._isFunction(f)) { f(res); } } } } } /** * @deprecated */ disconnect() { return this.disconnectWait(); } async disconnectWait() { if (this._peripheral) { if (this._peripheral.connected) { await this._peripheral.disconnectWait(); // ondisconnect will call } else { await this._cleanWait(); } } else { await this._cleanWait(); } } /** * @deprecated */ _clean() { return this._cleanWait(); } async _cleanWait() { const p = this._peripheral; if (!p) { return; } if (this.char_indicate) { await this.char_indicate.unregisterNotifyWait(); } // p.removeAllListeners(); this.connected = false; this._service = null; this.char_write = null; this.char_indicate = null; this._div_packet_queue = []; this._onresponse = null; if (p.connected) { await p.disconnectWait(); } } write(message_name, params) { return this.writeWait(message_name, params); } async writeWait(message_name, params) { const buf = this._LinkingService.createRequest(message_name, params); if (!buf) { throw new Error('The specified parameters are invalid.'); } const timer = setTimeout(() => { this._onresponse = null; throw new Error('Timeout'); }, this._write_response_timeout); const waitResponse = new Promise((resolve, reject) => { this._onresponse = (res) => { if (res.messageName === message_name + '_RESP') { this._onresponse = null; clearTimeout(timer); const data = this._margeResponsePrameters(res); if (data) { res.data = data; resolve(res); } else { throw new Error('Unknown response'); } } }; }); // console.log("linking write ", buf, message_name, JSON.stringify(params, null, 2)); await this.char_write.writeWait(buf, true); return await waitResponse; } _margeResponsePrameters(res) { if (!res) { return null; } const parameters = res.parameters; if (parameters && Array.isArray(parameters) && parameters.length > 0) { const data = {}; parameters.forEach((p) => { for (const k in p) { if (!k.match(/^(name|parameterId)$/)) { data[k] = p[k]; } } }); return data; } else { return null; } } _initServices() { const device_name = this._peripheral.localName || ''; // Device Name if (this._generic_access_service.device_name.char) { this.services.deviceName = { get: this._deviceNameSetWait.bind(this), set: this._deviceNameSetWait.bind(this), }; } // Button if ('Button' in this.info.exsensors || device_name.match(/^(Linking Board01|BLEAD-LK-TSH)/)) { this.services.button = { onnotify: null, }; } // LED if ('LED' in this.info.settings) { const o = this.info.settings.LED; if (o.colors && o.colors.length > 0 && o.patterns && o.patterns.length > 0) { const colors = {}; for (let i = 0; i < o.colors.length; i++) { colors[o.colors[i]] = i + 1; } const patterns = {}; for (let i = 0; i < o.patterns.length; i++) { patterns[o.patterns[i]] = i + 1; } this.services.led = { colors, patterns, turnOn: this._ledTurnOn.bind(this), turnOff: this._ledTurnOff.bind(this), }; } } // Vibration if ('Vibration' in this.info.settings) { const o = this.info.settings.Vibration; if (o.patterns && o.patterns.length > 0) { const patterns = {}; for (let i = 0; i < o.patterns.length; i++) { patterns[o.patterns[i]] = i + 1; } this.services.vibration = { patterns, turnOn: this._vibrationTurnOn.bind(this), turnOff: this._vibrationTurnOff.bind(this), }; } } // Gyroscope if ('Gyroscope' in this.info.capabilities) { this.services.gyroscope = this._createSensorServiceObject(0x00); } // Accelerometer if ('Accelerometer' in this.info.capabilities) { this.services.accelerometer = this._createSensorServiceObject(0x01); } // Orientation if ('Orientation' in this.info.capabilities) { this.services.orientation = this._createSensorServiceObject(0x02); } // Battery if ('Battery' in this.info.capabilities) { this.services.battery = this._createSensorServiceObject(0x03); } // Temperature if ('Temperature' in this.info.capabilities) { this.services.temperature = this._createSensorServiceObject(0x04); } // Humidity if ('Humidity' in this.info.capabilities) { this.services.humidity = this._createSensorServiceObject(0x05); } // Atmospheric pressure if ('Atmospheric pressure' in this.info.capabilities) { this.services.pressure = this._createSensorServiceObject(0x06); } // Opening and closing if ('Opening and closing' in this.info.exsensors) { this.services.openclose = this._createSensorServiceObject(0x07); } // Human detection if ('Human detection' in this.info.exsensors) { this.services.human = this._createSensorServiceObject(0x08); } // Move if ('Move' in this.info.exsensors) { this.services.move = this._createSensorServiceObject(0x09); } // Illuminance if ('Illuminance' in this.info.exsensors) { this.services.illuminance = this._createSensorServiceObject(0x0a); } } /** * @deprecated */ _deviceNameGet() { return this._deviceNameGetWait(); } async _deviceNameGetWait() { const char = this._generic_access_service.device_name .char; const data = await char.readWait(); return { deviceName: Buffer.from(data).toString('utf8'), }; } /** * @deprecated * @param name */ _deviceNameSet(name) { return this._deviceNameSetWait(name); } async _deviceNameSetWait(name) { if (!name) { throw new Error('Device name is required.'); } else if (typeof name !== 'string') { throw new Error('Device name must be a string.'); } else if (name.length > 32) { throw new Error('Device name is too long. The length must be in the range 1 to 32.'); } const buf = Buffer.from(name, 'utf8'); const char = this._generic_access_service.device_name .char; await char.writeWait(buf, false); } _ledTurnOn(color, pattern, duration) { let color_number = 1; if (color) { const colors = this.services.led.colors; if (typeof color === 'number') { for (const name in colors) { if (colors[name] === color) { color_number = color; break; } } } else if (typeof color === 'string') { if (color in colors) { color_number = colors[color]; } } } let pattern_number = 2; if (pattern) { const patterns = this.services.led.patterns; if (typeof pattern === 'number') { for (const name in patterns) { if (patterns[name] === pattern) { pattern_number = pattern; break; } } } else if (typeof pattern === 'string') { if (pattern in patterns) { pattern_number = patterns[pattern]; } } } if (!duration || typeof duration !== 'number' || duration % 1 !== 0) { duration = 5; } const promise = new Promise((resolve, reject) => { this.write('SELECT_SETTING_INFORMATION', { SettingInformationRequest: { requestName: 'START_DEMONSTRATION', }, SettingInformationData: [ { settingName: 'LED', colorNumber: color_number, patternNumber: pattern_number, duration, }, ], }) .then((res) => { resolve(res.data); }) .catch((error) => { reject(error); }); }); return promise; } _ledTurnOff() { const promise = new Promise((resolve, reject) => { this.write('SELECT_SETTING_INFORMATION', { SettingInformationRequest: { requestName: 'STOP_DEMONSTRATION', }, }) .then((res) => { resolve(res.data); }) .catch((error) => { reject(error); }); }); return promise; } _vibrationTurnOn(pattern, duration) { let pattern_number = 2; if (pattern) { const patterns = this.services.vibration.patterns; if (typeof pattern === 'number') { for (const name in patterns) { if (patterns[name] === pattern) { pattern_number = pattern; break; } } } else if (typeof pattern === 'string') { if (pattern in patterns) { pattern_number = patterns[pattern]; } } } if (!duration || typeof duration !== 'number' || duration % 1 !== 0) { duration = 5; } const promise = new Promise((resolve, reject) => { this.write('SELECT_SETTING_INFORMATION', { SettingInformationRequest: { requestName: 'START_DEMONSTRATION', }, SettingInformationData: [ { settingName: 'Vibration', patternNumber: pattern_number, duration, }, ], }) .then((res) => { resolve(res.data); }) .catch((error) => { reject(error); }); }); return promise; } _vibrationTurnOff() { const promise = new Promise((resolve, reject) => { this.write('SELECT_SETTING_INFORMATION', { SettingInformationRequest: { requestName: 'STOP_DEMONSTRATION', }, }) .then((res) => { resolve(res.data); }) .catch((error) => { reject(error); }); }); return promise; } _createSensorServiceObject(sensor_type) { return { onnotify: null, start: () => { return this._setNotifySensorInfo(sensor_type, 1); }, stop: () => { return this._setNotifySensorInfo(sensor_type, 0); }, get: () => { return this._getSensorInfo(sensor_type); }, }; } _setNotifySensorInfo(sensor_type, status) { const promise = new Promise((resolve, reject) => { this.write('SET_NOTIFY_SENSOR_INFO', { SensorType: sensor_type, Status: status, }) .then((res) => { resolve(res.data); }) .catch((error) => { reject(error); }); }); return promise; } _getSensorInfo(sensor_type) { const promise = new Promise((resolve, reject) => { this.write('GET_SENSOR_INFO', { SensorType: sensor_type, }) .then((res) => { if (sensor_type.toString().match(/^(0|1|2)$/)) { // Gyroscope, Accelerometer, Orientation const d = res.data; const data = { x: d.xValue, y: d.yValue, z: d.zValue, }; resolve(data); } else { resolve(res.data); } }) .catch((error) => { reject(error); }); }); return promise; } } exports.default = LinkingDevice;