文件
jw-beauty/miniprogram/services/ble/connection.js
T
Guoguo 5bce7060ac fix: BLE scan match use indexOf instead of exact equals
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.
2026-06-05 22:38:07 -07:00

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
}