feat: add user deactivation and vendor BLE protocol

这个提交包含在:
Guoguo
2026-05-11 21:14:41 +08:00
父节点 c95d47e0c1
当前提交 583fcb7e3d
修改 16 个文件,包含 698 行新增52 行删除
+56
查看文件
@@ -61,6 +61,32 @@ function writeCommand(type, payload) {
})
}
function writeRawCommand(bytes) {
return new Promise(function (resolve, reject) {
if (!connection.isConnected()) {
reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' })
return
}
var chars = connection.getChars()
if (!chars.command) {
reject({ error_code: 0xFF, error_msg: 'command characteristic not found' })
return
}
wx.writeBLECharacteristicValue({
deviceId: connection.getDeviceId(),
serviceId: chars.command.serviceId,
characteristicId: chars.command.uuid,
value: protocol.bytesToBuffer(bytes),
success: function () {
resolve({ message: 'success', bytes: bytes })
},
fail: function () {
reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' })
}
})
})
}
function writeCommandWithRetry(type, payload, maxRetries) {
if (maxRetries === undefined) maxRetries = 3
var attempt = 0
@@ -122,6 +148,10 @@ function readDeviceInfo() {
}
function setParams(options) {
if (protocol.PROTOCOL_MODE === 'vendor_33') {
return writeRawCommand(protocol.buildVendorCommand33(options || {}))
}
var regionMask = options.region_mask || protocol.REGION.FULL_FACE
var wavelength = options.wavelength || protocol.WAVELENGTH.R
var brightness = options.brightness || 200
@@ -142,19 +172,41 @@ function setParams(options) {
}
function startTreatment(regionMask) {
if (protocol.PROTOCOL_MODE === 'vendor_33') {
return Promise.resolve({ message: 'vendor_33 uses setParams write as start', region_mask: regionMask })
}
var mask = regionMask || protocol.REGION.FULL_FACE
return writeCommandWithRetry(protocol.CMD.START, [mask])
}
function stopTreatment() {
if (protocol.PROTOCOL_MODE === 'vendor_33') {
return writeRawCommand(protocol.buildVendorCommand33({
region_mask: 0,
wavelength: protocol.WAVELENGTH.R,
brightness: 0,
current_gain: 0,
hold_time: 0
}))
}
return writeCommandWithRetry(protocol.CMD.STOP, [])
}
function queryStatus() {
if (protocol.PROTOCOL_MODE === 'vendor_33') {
return Promise.resolve({ message: 'query status not defined by vendor_33 protocol' })
}
return writeCommandWithRetry(protocol.CMD.QUERY_STATUS, [])
}
function bindDevice(userId, bindToken) {
if (protocol.PROTOCOL_MODE === 'vendor_33') {
setTimeout(function () {
connection.emit('bind_result', { success: true, skipped_ble_bind: true })
}, 0)
return Promise.resolve({ message: 'BLE bind not defined by vendor_33 protocol', user_id: userId, bind_token: bindToken })
}
var userBytes = protocol.uint32ToBytes(parseInt(userId, 10))
var tokenBytes = protocol.hexToBytes(bindToken)
var ts = Math.floor(Date.now() / 1000)
@@ -165,6 +217,9 @@ function bindDevice(userId, bindToken) {
}
function unbindDevice(userId) {
if (protocol.PROTOCOL_MODE === 'vendor_33') {
return Promise.resolve({ message: 'BLE unbind not defined by vendor_33 protocol', user_id: userId })
}
var userBytes = protocol.uint32ToBytes(parseInt(userId, 10))
var payload = [0x02].concat(userBytes)
return writeCommandWithRetry(protocol.CMD.UNBIND, payload)
@@ -175,6 +230,7 @@ module.exports = {
clearPendingAcks: clearPendingAcks,
writeCommand: writeCommand,
writeRawCommand: writeRawCommand,
writeCommandWithRetry: writeCommandWithRetry,
readDeviceInfo: readDeviceInfo,
+72 -43
查看文件
@@ -81,6 +81,17 @@ function handleNotification(frame) {
}
}
function handleValueChange(res) {
var frame = protocol.parseFrame(res.value)
if (frame) {
handleNotification(frame)
return
}
if (protocol.PROTOCOL_MODE === 'vendor_33') {
emit('status', protocol.parseVendorStatus(res.value))
}
}
// --- service/characteristic discovery ---
function discoverChars(deviceId, serviceId, group) {
@@ -118,10 +129,10 @@ function subscribeToNotifications(deviceId, serviceId) {
characteristicId: _chars.status.uuid,
state: true,
success: function () {
wx.onBLECharacteristicValueChange(function (res) {
var frame = protocol.parseFrame(res.value)
if (frame) handleNotification(frame)
})
if (wx.offBLECharacteristicValueChange) {
wx.offBLECharacteristicValueChange(handleValueChange)
}
wx.onBLECharacteristicValueChange(handleValueChange)
resolve()
},
fail: function () { resolve() }
@@ -129,6 +140,20 @@ function subscribeToNotifications(deviceId, serviceId) {
})
}
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,
@@ -216,7 +241,9 @@ function connect(deviceId, callbacks) {
timeout: 10000,
success: function () {
_connected = true
discoverServices(deviceId, callbacks)
requestMtu(deviceId).then(function () {
discoverServices(deviceId, callbacks)
})
},
fail: function () {
_connected = false
@@ -251,46 +278,48 @@ function attemptReconnect(deviceId, retriesLeft) {
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
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 })
}
}
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 () {
+3
查看文件
@@ -38,8 +38,11 @@ module.exports = {
unbindDevice: commands.unbindDevice,
// Protocol utilities
PROTOCOL_MODE: protocol.PROTOCOL_MODE,
buildFrame: protocol.buildFrame,
parseFrame: protocol.parseFrame,
buildVendorCommand33: protocol.buildVendorCommand33,
parseVendorStatus: protocol.parseVendorStatus,
parseStatusReport: protocol.parseStatusReport,
parseAck: protocol.parseAck,
parseTreatmentComplete: protocol.parseTreatmentComplete,
+67 -2
查看文件
@@ -1,5 +1,7 @@
// BLE protocol: frame encoding/decoding, checksum, constants
var PROTOCOL_MODE = 'vendor_33'
var SERVICE = {
DEVICE_INFO: 'FFE0',
DATA_COMM: 'FFE1',
@@ -8,8 +10,8 @@ var SERVICE = {
var CHAR = {
DEVICE_INFO: 'FFE3',
COMMAND: 'FFE4',
STATUS: 'FFE5',
COMMAND: 'FFE1',
STATUS: 'FFE4',
BOND_INFO: 'FFE6',
OTA_CONTROL: 'FFE7',
OTA_DATA: 'FFE8',
@@ -112,6 +114,18 @@ function xorChecksum(bytes) {
return result
}
function clampByte(value) {
var n = parseInt(value, 10)
if (isNaN(n)) n = 0
return Math.max(0, Math.min(255, n))
}
function clampUInt16(value) {
var n = parseInt(value, 10)
if (isNaN(n)) n = 0
return Math.max(0, Math.min(65535, n))
}
function uint32ToBytes(value) {
return [
(value >> 24) & 0xFF,
@@ -141,6 +155,54 @@ function bytesToHex(bytes) {
return hex.toUpperCase()
}
// --- vendor 33-byte command protocol ---
function buildVendorCommand33(options) {
options = options || {}
var regionMask = options.region_mask || REGION.FULL_FACE
var wavelength = options.wavelength || WAVELENGTH.R
var brightness = clampByte(options.brightness === undefined ? 200 : options.brightness)
var gain = clampUInt16(options.current_gain === undefined ? brightness : options.current_gain)
var durationMs = options.duration_ms || 600000
var holdSeconds = clampUInt16(options.hold_time === undefined ? Math.round(durationMs / 1000) : options.hold_time)
var regions = [
REGION.LEFT_CHEEK,
REGION.RIGHT_CHEEK,
REGION.FOREHEAD,
REGION.CHIN,
REGION.NOSE
]
var bytes = []
for (var i = 0; i < regions.length; i++) {
var enabled = (regionMask & regions[i]) !== 0
var red = 0
var infrared = 0
var uv = 0
var warmYellow = 0
if (enabled) {
if (wavelength === WAVELENGTH.R) red = brightness
if (wavelength === WAVELENGTH.IR) infrared = brightness
if (wavelength === WAVELENGTH.UV) uv = brightness
if (wavelength === WAVELENGTH.Y) warmYellow = brightness
}
bytes.push(red, infrared, uv, warmYellow, (gain >> 8) & 0xFF, gain & 0xFF)
}
bytes.push((holdSeconds >> 8) & 0xFF, holdSeconds & 0xFF)
bytes.push(xorChecksum(bytes))
return bytes
}
function parseVendorStatus(buffer) {
var bytes = bufferToBytes(buffer)
return {
mode_state: bytes.length > 0 ? bytes[0] : MODE_STATE.IDLE,
raw_bytes: bytes,
raw_hex: bytesToHex(bytes)
}
}
// --- frame encoding/decoding ---
function buildFrame(type, payload) {
@@ -240,6 +302,7 @@ function getModeStateName(code) {
}
module.exports = {
PROTOCOL_MODE: PROTOCOL_MODE,
SERVICE: SERVICE,
CHAR: CHAR,
CMD: CMD,
@@ -258,9 +321,11 @@ module.exports = {
bytesToUint32: bytesToUint32,
hexToBytes: hexToBytes,
bytesToHex: bytesToHex,
buildVendorCommand33: buildVendorCommand33,
buildFrame: buildFrame,
parseFrame: parseFrame,
parseVendorStatus: parseVendorStatus,
parseStatusReport: parseStatusReport,
parseAck: parseAck,
parseTreatmentComplete: parseTreatmentComplete,