// BLE connection: scan, connect, disconnect, reconnect logic var protocol = require('./protocol') var SERVICE = protocol.SERVICE var CHAR = protocol.CHAR // Shared state — accessed by commands.js via getters/setters var _deviceId = null var _connected = false var _chars = {} var _autoReconnect = true var _reconnecting = false // Event emitter — shared across modules var _listeners = {} function on(event, callback) { if (!_listeners[event]) _listeners[event] = [] _listeners[event].push(callback) } function off(event, callback) { if (!_listeners[event]) return if (callback) { _listeners[event] = _listeners[event].filter(function (cb) { return cb !== callback }) } else { _listeners[event] = [] } } function emit(event, data) { if (!_listeners[event]) return _listeners[event].forEach(function (cb) { try { cb(data) } catch (e) { console.error('ble emit error:', e) } }) } // --- state accessors (used by commands.js) --- function getDeviceId() { return _deviceId } function isConnected() { return _connected && _deviceId !== null } function getChars() { return _chars } // --- notification handling --- function handleNotification(frame) { var pendingAcks = require('./commands').getPendingAcks() switch (frame.type) { case protocol.NOTIFY.STATUS_REPORT: emit('status', protocol.parseStatusReport(frame.payload)) break case protocol.NOTIFY.ACK: var ack = protocol.parseAck(frame.payload) emit('ack', ack) if (pendingAcks[ack.seq]) { if (ack.error_code === 0) { pendingAcks[ack.seq].resolve(ack) } else { pendingAcks[ack.seq].reject(ack) } delete pendingAcks[ack.seq] } break case protocol.NOTIFY.TREATMENT_COMPLETE: emit('treatment_complete', protocol.parseTreatmentComplete(frame.payload)) break case protocol.NOTIFY.EXCEPTION: emit('exception', protocol.parseException(frame.payload)) break case protocol.NOTIFY.BIND_SUCCESS: emit('bind_result', { success: frame.payload[0] === 0x00 }) break } } function handleValueChange(res) { if (protocol.PROTOCOL_MODE === 'vendor_33') { var parsed = protocol.parseVendorStatus(res.value) console.log('[BLE] FFE4 收到', protocol.bufferToBytes(res.value).length + '字节', parsed.type, '| raw:', parsed.raw_hex) emit('vendor_raw', parsed) if (parsed.type === 'heartbeat') { emit('heartbeat', { value: parsed.heartbeat }) return } if (parsed.type === 'adc') { var vbatMv = parsed.vbat var batteryPct = Math.min(100, Math.max(0, Math.round((vbatMv - 3000) / (4200 - 3000) * 100))) if (!parsed.checksum_ok) { console.warn('[BLE] ADC checksum mismatch, dropping') return } emit('adc', { pd: parsed.pd, vbat: vbatMv, battery: batteryPct, fitting: parsed.fitting, run_state: parsed.run_state }) emit('battery', { battery: batteryPct, vbat: vbatMv }) if (parsed.fitting !== -1) { emit('fitting', { fitting: parsed.fitting }) } if (parsed.run_state !== -1) { emit('run_state', { run_state: parsed.run_state }) } return } if (parsed.type === 'params' && parsed.ios) { var anyActive = false for (var i = 0; i < parsed.ios.length; i++) { if (parsed.ios[i].red || parsed.ios[i].infrared || parsed.ios[i].uv || parsed.ios[i].warm_yellow) { anyActive = true break } } emit('status', { mode_state: anyActive ? protocol.MODE_STATE.ACTIVE : protocol.MODE_STATE.IDLE, region_mask: protocol.REGION.FULL_FACE, wavelength: 0, brightness: 0, remaining_ms: parsed.hold_time > 0 ? parsed.hold_time * 1000 : 0, error_code: 0, battery: 0, temperature: 0, bind_status: 1, subscription: 1 }) } return } var frame = protocol.parseFrame(res.value) if (frame) { handleNotification(frame) } } // --- service/characteristic discovery --- function discoverChars(deviceId, serviceId, group) { return new Promise(function (resolve) { wx.getBLEDeviceCharacteristics({ deviceId: deviceId, serviceId: serviceId, success: function (res) { var chars = res.characteristics || [] for (var i = 0; i < chars.length; i++) { var c = chars[i] var uuid = c.uuid.toUpperCase() var props = c.properties || {} if (uuid.indexOf(CHAR.DEVICE_INFO) !== -1) _chars.deviceInfo = { uuid: c.uuid, serviceId: serviceId } if (uuid.indexOf(CHAR.COMMAND) !== -1 && (props.write || props.writeNoResponse)) { _chars.command = { uuid: c.uuid, serviceId: serviceId } } if (uuid.indexOf(CHAR.STATUS) !== -1 && props.notify) { _chars.status = { uuid: c.uuid, serviceId: serviceId } } if (uuid.indexOf(CHAR.BOND_INFO) !== -1) _chars.bondInfo = { uuid: c.uuid, serviceId: serviceId } if (uuid.indexOf(CHAR.OTA_CONTROL) !== -1) _chars.otaControl = { uuid: c.uuid, serviceId: serviceId } if (uuid.indexOf(CHAR.OTA_DATA) !== -1) _chars.otaData = { uuid: c.uuid, serviceId: serviceId } if (uuid.indexOf(CHAR.OTA_STATUS) !== -1) _chars.otaStatus = { uuid: c.uuid, serviceId: serviceId } } resolve() }, fail: function () { resolve() } }) }) } function subscribeToNotifications(deviceId, serviceId) { return new Promise(function (resolve) { if (!_chars.status) { resolve(); return } wx.notifyBLECharacteristicValueChange({ deviceId: deviceId, serviceId: _chars.status.serviceId, characteristicId: _chars.status.uuid, state: true, success: function () { if (wx.offBLECharacteristicValueChange) { wx.offBLECharacteristicValueChange(handleValueChange) } wx.onBLECharacteristicValueChange(handleValueChange) resolve() }, fail: function () { resolve() } }) }) } function requestMtu(deviceId) { return new Promise(function (resolve) { if (!wx.setBLEMTU) { resolve() return } wx.setBLEMTU({ deviceId: deviceId, mtu: 64, complete: function () { resolve() } }) }) } function discoverServices(deviceId, callbacks) { wx.getBLEDeviceServices({ deviceId: deviceId, success: function (res) { var services = res.services console.log('[BLE] 发现服务数量:', services.length) var serviceMap = {} for (var i = 0; i < services.length; i++) { var uuid = services[i].uuid.toUpperCase() console.log('[BLE] 服务:', uuid) if (uuid.indexOf(SERVICE.DEVICE_INFO) !== -1) { serviceMap.deviceInfo = services[i].uuid } else if (uuid.indexOf(SERVICE.DATA_COMM) !== -1) { serviceMap.dataComm = services[i].uuid } else if (uuid.indexOf(SERVICE.OTA) !== -1) { serviceMap.ota = services[i].uuid } } console.log('[BLE] 匹配服务:', JSON.stringify(serviceMap)) var tasks = [] if (serviceMap.deviceInfo) { tasks.push(discoverChars(deviceId, serviceMap.deviceInfo, 'deviceInfo')) } if (serviceMap.dataComm) { tasks.push(discoverChars(deviceId, serviceMap.dataComm, 'dataComm')) } Promise.all(tasks).then(function () { subscribeToNotifications(deviceId, serviceMap.dataComm).then(function () { if (callbacks.onConnected) callbacks.onConnected({ deviceId: deviceId }) }) }) }, fail: function () { if (callbacks.onError) callbacks.onError({ msg: '服务发现失败' }) } }) } // --- scan & connect --- function startScan(callbacks) { var targetName = callbacks.targetDeviceId ? ('JW_' + callbacks.targetDeviceId).toUpperCase() : '' wx.openBluetoothAdapter({ success: function () { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, success: function () { wx.offBluetoothDeviceFound() wx.onBluetoothDeviceFound(function (res) { var devices = res.devices || [] for (var i = 0; i < devices.length; i++) { var d = devices[i] var name = (d.name || '').toUpperCase() var localName = (d.localName || '').toUpperCase() if (!name && !localName) continue console.log('[BLE] 发现设备:', d.name, '| localName:', d.localName, '| deviceId:', d.deviceId) var matched = false if (targetName) { matched = name.indexOf(targetName) !== -1 || localName.indexOf(targetName) !== -1 } else { matched = localName.indexOf('JW_') !== -1 || name.indexOf('JW_') !== -1 || name.indexOf('HOX') !== -1 || localName.indexOf('HOX') !== -1 || name.indexOf('LIGHTMASK') !== -1 || localName.indexOf('LIGHTMASK') !== -1 || name.indexOf('SIMPLE PERIPHERAL') !== -1 || localName.indexOf('SIMPLE PERIPHERAL') !== -1 } console.log('[BLE] 匹配结果:', matched, targetName ? '(精确:' + targetName + ')' : '(模糊)') if (matched) { wx.stopBluetoothDevicesDiscovery({}) if (callbacks.onFound) callbacks.onFound(d) connect(d.deviceId, callbacks) return } } }) }, fail: function () { if (callbacks.onError) callbacks.onError({ msg: '扫描失败' }) } }) }, fail: function () { if (callbacks.onError) callbacks.onError({ msg: '请开启蓝牙' }) } }) } function stopScan() { wx.stopBluetoothDevicesDiscovery({}) wx.offBluetoothDeviceFound() } function connect(deviceId, callbacks) { console.log('[BLE] 开始连接:', deviceId) _deviceId = deviceId _chars = {} wx.createBLEConnection({ deviceId: deviceId, timeout: 10000, success: function () { console.log('[BLE] 连接成功:', deviceId) _connected = true requestMtu(deviceId).then(function () { discoverServices(deviceId, callbacks) }) }, fail: function (err) { console.log('[BLE] 连接失败:', deviceId, err) _connected = false if (callbacks.onError) callbacks.onError({ msg: '连接失败' }) } }) } function disconnect() { _autoReconnect = false _reconnecting = false if (_deviceId) { wx.closeBLEConnection({ deviceId: _deviceId }) _deviceId = null } _connected = false _chars = {} var commands = require('./commands') commands.clearPendingAcks() // Only emit disconnected event; do not clear _listeners so that // subscribers (other modules) retain their registrations across reconnects. emit('disconnect_cleanup', null) wx.closeBluetoothAdapter({}) } // --- reconnect --- function attemptReconnect(deviceId, retriesLeft) { wx.createBLEConnection({ deviceId: deviceId, timeout: 10000, success: function () { _connected = true _chars = {} requestMtu(deviceId).then(function () { wx.getBLEDeviceServices({ deviceId: deviceId, success: function (res) { var services = res.services var serviceMap = {} for (var i = 0; i < services.length; i++) { var uuid = services[i].uuid.toUpperCase() if (uuid.indexOf(SERVICE.DEVICE_INFO) !== -1) { serviceMap.deviceInfo = services[i].uuid } else if (uuid.indexOf(SERVICE.DATA_COMM) !== -1) { serviceMap.dataComm = services[i].uuid } } var tasks = [] if (serviceMap.deviceInfo) { tasks.push(discoverChars(deviceId, serviceMap.deviceInfo, 'deviceInfo')) } if (serviceMap.dataComm) { tasks.push(discoverChars(deviceId, serviceMap.dataComm, 'dataComm')) } Promise.all(tasks).then(function () { var serviceId = serviceMap.dataComm || null return subscribeToNotifications(deviceId, serviceId) }).then(function () { _reconnecting = false emit('reconnected', { deviceId: deviceId }) }) }, fail: function () { if (retriesLeft > 1) { setTimeout(function () { attemptReconnect(deviceId, retriesLeft - 1) }, 2000) } else { _reconnecting = false emit('reconnect_failed', { deviceId: deviceId }) } } }) }) }, fail: function () { if (retriesLeft > 1) { setTimeout(function () { attemptReconnect(deviceId, retriesLeft - 1) }, 2000) } else { _reconnecting = false emit('reconnect_failed', { deviceId: deviceId }) } } }) } function setAutoReconnect(enabled) { _autoReconnect = enabled } // --- BLE connection state change listener --- wx.onBLEConnectionStateChange(function (res) { if (!res.connected) { _connected = false emit('disconnected', { deviceId: res.deviceId }) if (_autoReconnect && !_reconnecting && _deviceId) { _reconnecting = true setTimeout(function () { attemptReconnect(_deviceId, 3) }, 2000) } } }) module.exports = { on: on, off: off, emit: emit, getDeviceId: getDeviceId, isConnected: isConnected, getChars: getChars, startScan: startScan, stopScan: stopScan, connect: connect, disconnect: disconnect, attemptReconnect: attemptReconnect, setAutoReconnect: setAutoReconnect }