- 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
344 行
9.9 KiB
JavaScript
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
|
|
}
|