Fixes timeout during binding when targetDeviceId is set but exact string match fails due to encoding differences. Also skips devices with no name, and logs match result for every candidate.
437 行
13 KiB
JavaScript
437 行
13 KiB
JavaScript
// 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)
|
|
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 })
|
|
emit('battery', { battery: batteryPct, vbat: vbatMv })
|
|
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
|
|
}
|