// BLE commands: writing commands and high-level device operations var protocol = require('./protocol') var connection = require('./connection') var _cmdSeq = 0 var _pendingAcks = {} function nextSeq() { _cmdSeq = (_cmdSeq + 1) % 256 return _cmdSeq } function getPendingAcks() { return _pendingAcks } function clearPendingAcks() { _pendingAcks = {} } // --- low-level write --- function writeCommand(type, payload) { return new Promise(function (resolve, reject) { if (!connection.isConnected()) { reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' }) return } var chars = connection.getChars() if (!chars.command) { reject({ error_code: 0xFF, error_msg: 'command characteristic not found' }) return } var seq = nextSeq() var payloadWithSeq = (payload || []).concat([seq]) var buffer = protocol.buildFrame(type, payloadWithSeq) _pendingAcks[seq] = { resolve: resolve, reject: reject } setTimeout(function () { if (_pendingAcks[seq]) { _pendingAcks[seq].reject({ error_code: 0xFF, error_msg: 'ACK timeout' }) delete _pendingAcks[seq] } }, 5000) var deviceId = connection.getDeviceId() wx.writeBLECharacteristicValue({ deviceId: deviceId, serviceId: chars.command.serviceId, characteristicId: chars.command.uuid, value: buffer, success: function () {}, fail: function () { delete _pendingAcks[seq] reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' }) } }) }) } function writeRawCommand(bytes) { return new Promise(function (resolve, reject) { if (!connection.isConnected()) { reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' }) return } var chars = connection.getChars() if (!chars.command) { reject({ error_code: 0xFF, error_msg: 'command characteristic not found' }) return } wx.writeBLECharacteristicValue({ deviceId: connection.getDeviceId(), serviceId: chars.command.serviceId, characteristicId: chars.command.uuid, value: protocol.bytesToBuffer(bytes), success: function () { resolve({ message: 'success', bytes: bytes }) }, fail: function () { reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' }) } }) }) } function writeCommandWithRetry(type, payload, maxRetries) { if (maxRetries === undefined) maxRetries = 3 var attempt = 0 function tryOnce() { attempt++ return writeCommand(type, payload).catch(function (err) { if (err && err.error_code === 0x0C) { return Promise.reject(err) } if (attempt >= maxRetries) { return Promise.reject(err) } return new Promise(function (resolve) { setTimeout(resolve, 500) }).then(function () { return tryOnce() }) }) } return tryOnce() } // --- high-level commands --- function readDeviceInfo() { return new Promise(function (resolve, reject) { var chars = connection.getChars() var deviceId = connection.getDeviceId() if (!connection.isConnected() || !chars.deviceInfo) { reject({ msg: '设备未连接或特征值未就绪' }) return } wx.readBLECharacteristicValue({ deviceId: deviceId, serviceId: chars.deviceInfo.serviceId, characteristicId: chars.deviceInfo.uuid, success: function () {}, fail: function () { reject({ msg: '读取设备信息失败' }) } }) var handler = function (res) { if (res.characteristicId.toUpperCase().indexOf(protocol.CHAR.DEVICE_INFO) !== -1) { wx.offBLECharacteristicValueChange(handler) var bytes = protocol.bufferToBytes(res.value) if (bytes.length >= 14) { resolve({ hw_version: (bytes[0] << 8) | bytes[1], fw_version: (bytes[2] << 8) | bytes[3], device_type: (bytes[4] << 8) | bytes[5], device_id: protocol.bytesToHex(bytes.slice(6, 14)) }) } else { reject({ msg: '设备信息格式错误' }) } } } wx.onBLECharacteristicValueChange(handler) }) } function setParams(options) { if (protocol.PROTOCOL_MODE === 'vendor_33') { return writeRawCommand(protocol.buildVendorCommand33(options || {})) } var regionMask = options.region_mask || protocol.REGION.FULL_FACE var wavelength = options.wavelength || protocol.WAVELENGTH.R var brightness = options.brightness || 200 var durationMs = options.duration_ms || 600000 var mode = options.mode !== undefined ? options.mode : protocol.TREAT_MODE.NORMAL var payload = [ regionMask, wavelength, brightness, protocol.uint32ToBytes(durationMs), mode ].reduce(function (a, b) { return a.concat(Array.isArray(b) ? b : [b]) }, []) return writeCommandWithRetry(protocol.CMD.SET_PARAMS, payload) } function startTreatment(regionMask) { if (protocol.PROTOCOL_MODE === 'vendor_33') { return Promise.resolve({ message: 'vendor_33 uses setParams write as start', region_mask: regionMask }) } var mask = regionMask || protocol.REGION.FULL_FACE return writeCommandWithRetry(protocol.CMD.START, [mask]) } function stopTreatment() { if (protocol.PROTOCOL_MODE === 'vendor_33') { return writeRawCommand(protocol.buildVendorCommand33({ region_mask: 0, wavelength: protocol.WAVELENGTH.R, brightness: 0, current_gain: 0, hold_time: 0 })) } return writeCommandWithRetry(protocol.CMD.STOP, []) } function queryStatus() { if (protocol.PROTOCOL_MODE === 'vendor_33') { return Promise.resolve({ message: 'query status not defined by vendor_33 protocol' }) } return writeCommandWithRetry(protocol.CMD.QUERY_STATUS, []) } function bindDevice(userId, bindToken) { if (protocol.PROTOCOL_MODE === 'vendor_33') { setTimeout(function () { connection.emit('bind_result', { success: true, skipped_ble_bind: true }) }, 0) return Promise.resolve({ message: 'BLE bind not defined by vendor_33 protocol', user_id: userId, bind_token: bindToken }) } var userBytes = protocol.uint32ToBytes(parseInt(userId, 10)) var tokenBytes = protocol.hexToBytes(bindToken) var ts = Math.floor(Date.now() / 1000) var tsBytes = protocol.uint32ToBytes(ts) var payload = [0x01].concat(userBytes).concat(tokenBytes).concat(tsBytes) return writeCommandWithRetry(protocol.CMD.BIND, payload) } function unbindDevice(userId) { if (protocol.PROTOCOL_MODE === 'vendor_33') { return Promise.resolve({ message: 'BLE unbind not defined by vendor_33 protocol', user_id: userId }) } var userBytes = protocol.uint32ToBytes(parseInt(userId, 10)) var payload = [0x02].concat(userBytes) return writeCommandWithRetry(protocol.CMD.UNBIND, payload) } module.exports = { getPendingAcks: getPendingAcks, clearPendingAcks: clearPendingAcks, writeCommand: writeCommand, writeRawCommand: writeRawCommand, writeCommandWithRetry: writeCommandWithRetry, readDeviceInfo: readDeviceInfo, setParams: setParams, startTreatment: startTreatment, stopTreatment: stopTreatment, queryStatus: queryStatus, bindDevice: bindDevice, unbindDevice: unbindDevice }