文件
jw-beauty/miniprogram/services/ble/connection.js
T
Guoguo 2eb38195f1 fix: address critical security and data integrity issues from cross-audit
- Add expire_time > NOW() filter to findActive() preventing stale subscriptions
- Add express-rate-limit on login endpoints (user: 10/15min, admin: 5/15min)
- Add production guard for default admin credentials
- Fix BLE bindDevice userId encoding (uint32 instead of hexToBytes on numeric)
- Wrap adminCreate in transaction to prevent race condition
- Add settings cache invalidation after admin saves
- Read trial_days from settings instead of hardcoding 7
- Fix double JSON.stringify in commandDao.finish call
- Cancel stale pending bindings before creating new ones
- Reduce token refresh grace period from 3 days to 1 day
- Fix subscribe-success to fetch expiry from server (correct for renewals)
- Add keep-alive name property to DashboardView and SettingsView
- Fix BLE disconnect() to preserve listener registrations across reconnects
2026-05-05 02:33:25 -07:00

344 行
9.9 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
}
}
// --- 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()
if (uuid.indexOf(CHAR.DEVICE_INFO) !== -1) _chars.deviceInfo = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.COMMAND) !== -1) _chars.command = { uuid: c.uuid, serviceId: serviceId }
if (uuid.indexOf(CHAR.STATUS) !== -1) _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 () {
wx.onBLECharacteristicValueChange(function (res) {
var frame = protocol.parseFrame(res.value)
if (frame) handleNotification(frame)
})
resolve()
},
fail: function () { resolve() }
})
})
}
function discoverServices(deviceId, callbacks) {
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
} else if (uuid.indexOf(SERVICE.OTA) !== -1) {
serviceMap.ota = 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 () {
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) {
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.indexOf('HOX') !== -1 || localName.indexOf('HOX') !== -1 ||
name.indexOf('LIGHTMASK') !== -1 || localName.indexOf('LIGHTMASK') !== -1) {
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) {
_deviceId = deviceId
_chars = {}
wx.createBLEConnection({
deviceId: deviceId,
timeout: 10000,
success: function () {
_connected = true
discoverServices(deviceId, callbacks)
},
fail: function () {
_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 = {}
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
}