feat: add user deactivation and vendor BLE protocol
这个提交包含在:
@@ -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,
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
在新工单中引用
屏蔽一个用户