Files
bodyWeight/miniprogram/lefu/service.ts
T

610 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { get } from '../utils/request/index'
import type {
RawDevice,
LeFuPlugin,
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
WifiVersion,
LeFuConfig,
DeviceSetting,
DeviceMember,
DeviceInfo,
FirmwareVersion,
} from './types'
/** 乐福 SDK 配置 */
const LEFU_CONFIG: LeFuConfig = {
key: 'lefudc91611833e18d94',
secret: 'eQ50JYimMp0X7uhNLj6D3lTEk4TUUvEG7dvaqKM9t1U=',
domain1: 'http://192.168.1.31:8889', // 测试环境(老版本 无鉴权)
domain2: 'http://192.168.1.31:8889/weight', // 测试环境(新版本 有鉴权)
// domain1: 'http://device.shuziweidao.com:80/gateway', // 正式环境
// domain2: 'http://device.shuziweidao.com:80/weight', // 正式环境
}
/** 支持的设备型号配置列表(5款) */
const DEVICE_SETTINGS: DeviceSetting[] = [
{
advLength: 999, calorieStatus: 0, createBy: null,
deviceAccuracyType: 2, deviceCalcuteType: 3, deviceConnectType: 2,
deviceFuncType: 223, deviceName: 'CF568_G', devicePowerType: 3,
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
id: 65, imgUrl: null, macAddressStart: 6, remark: null,
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
},
{
advLength: 999, calorieStatus: 0, createBy: null,
deviceAccuracyType: 2, deviceCalcuteType: 3, deviceConnectType: 2,
deviceFuncType: 223, deviceName: 'YX-B4-568-BW', devicePowerType: 3,
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
id: 65, imgUrl: null, macAddressStart: 6, remark: null,
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
},
{
advLength: 999, calorieStatus: 0, createBy: null,
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
deviceFuncType: 65759, deviceName: 'CF636_G', devicePowerType: 3,
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,2',
id: 216, imgUrl: null, macAddressStart: 6, remark: null,
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
},
{
advLength: 999, calorieStatus: 0, createBy: null,
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
deviceFuncType: 65759, deviceName: 'YX-B8-636-BW', devicePowerType: 3,
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,2',
id: 216, imgUrl: null, macAddressStart: 6, remark: null,
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
},
{
advLength: 999, calorieStatus: 0, createBy: null,
deviceAccuracyType: 2, deviceCalcuteType: 4, deviceConnectType: 2,
deviceFuncType: 223, deviceName: 'CF577', devicePowerType: 3,
deviceProtocolType: 3, deviceType: 1, deviceUnitType: '0,1,11',
id: 51, imgUrl: null, macAddressStart: 6, remark: null,
sign: 'FF', status: 0, uhStatus: 1, updateBy: null,
},
]
const TAG = '[LeFuService]'
class LeFuService {
private _plugin: LeFuPlugin | null = null
/** 设备协议对象,devicesModel 事件返回,用于 WiFi 配网等高级操作 */
private _activeProtocol: any = null
/** 重连次数 */
private _reconnectCount = 0
private readonly _maxReconnect = 3
private _reconnectTimer: number | null = null
/** 保活心跳定时器 */
private _keepAliveTimer: number | null = null
/** 连接后从 deviceInfo 事件获取的固件信息 */
private _deviceInfo: DeviceInfo | null = null
/** bus 事件是否已订阅(防止多次 startScan 重复 subscribe 累积) */
private _busSubscribed = false
/** 回调注册(页面通过 on* 方法注册,stopScan 时清空) */
private _onDevicesListCb: ((devices: ScannedDevice[]) => void) | null = null
private _onConnectStateCb: ((state: string) => void) | null = null
private _onDeviceConnectCb: (() => void) | null = null
private _onProgressCb: ((data: ProgressData) => void) | null = null
private _onLockedCb: ((data: LockData) => void) | null = null
private _onDisconnectedCb: (() => void) | null = null
private _onDeviceInfoCb: ((info: DeviceInfo) => void) | null = null
/** 在 app.ts onLaunch 中调用,加载 ppScale-plugin */
init(): void {
console.log(TAG, 'init() 开始加载插件')
this._plugin = requirePlugin('ppScale-plugin') as LeFuPlugin
console.log(TAG, 'init() 插件加载完成:', !!this._plugin)
}
/** 获取插件(未初始化时抛出) */
private get _p(): LeFuPlugin {
if (!this._plugin) throw new Error('[LeFu] 服务未初始化,请先调用 init()')
return this._plugin
}
/** 乐福配置(供体成分分析等外部使用) */
get config(): LeFuConfig {
return LEFU_CONFIG
}
// ─── 回调注册 ─────────────────────────────────
/** 注册设备列表回调(扫描到设备时触发) */
onDevicesList(cb: (devices: ScannedDevice[]) => void): void {
console.log(TAG, 'onDevicesList 回调已注册')
this._onDevicesListCb = cb
}
/** 注册蓝牙连接状态回调 */
onConnectState(cb: (state: string) => void): void {
console.log(TAG, 'onConnectState 回调已注册')
this._onConnectStateCb = cb
}
/** 注册设备连接就绪回调(activeProtocol 可用时触发) */
onDeviceConnect(cb: () => void): void {
console.log(TAG, 'onDeviceConnect 回调已注册')
this._onDeviceConnectCb = cb
}
/** 当前蓝牙连接状态(connectState bus 事件实时更新) */
private _connectState = ''
/** 设备是否已连接(activeProtocol 就绪) */
get isConnected(): boolean {
return !!this._activeProtocol
}
/** 当前蓝牙连接状态字符串 */
get connectState(): string {
return this._connectState
}
/** 插件 BLUE_STATE 常量对象(供页面 wxml 条件判断用) */
get BLUE_STATE(): Record<string, string> {
return this._plugin?.BLUE_STATE ?? {}
}
/** 连接后从 deviceInfo 事件获取的设备信息(含 modelNumber / serialNumber 等) */
get deviceInfo(): DeviceInfo | null {
return this._deviceInfo
}
/** 注册实时测量回调(测量过程中多次触发) */
onMeasuring(cb: (data: ProgressData) => void): void {
this._onProgressCb = cb
}
/** 注册测量完成回调(数据锁定,触发一次) */
onLocked(cb: (data: LockData) => void): void {
this._onLockedCb = cb
}
/** 注册设备断开回调 */
onDisconnected(cb: () => void): void {
this._onDisconnectedCb = cb
}
/** 注册 deviceInfo 就绪回调(设备名/固件信息可用时触发) */
onDeviceInfo(cb: (info: DeviceInfo) => void): void {
this._onDeviceInfoCb = cb
}
// ─── 扫描与连接 ───────────────────────────────
/**
* 开始蓝牙扫描
* 对齐 master:先 setDeviceSetting → start,再 subscribemaster searchDevice.js 顺序)
*/
startScan(): void {
console.log(TAG, 'startScan() 开始,busSubscribed:', this._busSubscribed)
const plugin = this._p
// ① 先设置设备配置并启动扫描(必须在 subscribe 之前,与 master 一致)
plugin.Blue.setDeviceSetting(DEVICE_SETTINGS)
const deviceNames = DEVICE_SETTINGS.map(s => s.deviceName)
console.log(TAG, 'plugin.Blue.start() 调用,设备名称:', deviceNames)
plugin.Blue.start(deviceNames, false)
// bus 订阅只注册一次,stopScan 时重置,避免多次搜索导致订阅累积
if (this._busSubscribed) {
console.log(TAG, 'bus 已订阅,跳过重复注册')
return
}
this._busSubscribed = true
// ② 启动后再订阅设备列表(master 也是 start 后才 subscribe
plugin.bus.subscribe('devicesList', (devList: RawDevice[]) => {
console.log(TAG, 'bus[devicesList] 触发,原始设备数:', devList?.length, devList?.map((d: RawDevice) => d.name || d.deviceName))
if (!devList || !devList.length) {
this._onDevicesListCb?.([])
return
}
// 直接用原始列表转换,不调用 getDeviceModelmaster 也是直接传 res
const devices: ScannedDevice[] = devList.map((item: RawDevice) => ({
name: item.name || item.deviceName || '未知设备',
raw: item,
model: {},
}))
console.log(TAG, '转换后设备列表:', devices.map(d => d.name))
this._onDevicesListCb?.(devices)
})
// 设备模型信息(含 mac 等,仅用于日志/调试)
plugin.bus.subscribe('devicesModel', (res: any) => {
console.log(TAG, 'bus[devicesModel] 触发,deviceMac:', res?.deviceMac)
})
// 连接成功后获取固件信息(用于后续 OTA 版本比对)
plugin.bus.subscribe('deviceInfo', (res: DeviceInfo) => {
console.log(TAG, 'bus[deviceInfo] 触发:',res, res?.firmwareRevision, res?.modelNumber)
this._deviceInfo = res
this._onDeviceInfoCb?.(res)
})
// 连接建立:从 ScaleAction 获取 activeProtocol,启动数据进度(对齐 master configureDevice.js
plugin.bus.subscribe('deviceConnect', () => {
console.log(TAG, 'bus[deviceConnect] 触发,获取 activeProtocol,启动数据进度')
this._reconnectCount = 0
plugin.ScaleAction.startDataProgress(true)
this._activeProtocol = plugin.ScaleAction.getActiveProtocol()
this._connectState = plugin.BLUE_STATE.CONNECTSUCCESS
this._onDeviceConnectCb?.()
})
// 连接状态变化:CONNECTSUCCESS 时启动保活,CONNECTFAILED 时重连
plugin.bus.subscribe('connectState', (res: string) => {
console.log(TAG, 'bus[connectState] 触发:', res)
this._connectState = res
this._onConnectStateCb?.(res)
if (res === plugin.BLUE_STATE.CONNECTSUCCESS) {
console.log(TAG, '连接成功,启动保活心跳')
this._startKeepAlive()
}
if (res === plugin.BLUE_STATE.CONNECTFAILED) {
console.warn(TAG, '连接失败,触发重连逻辑')
this._stopKeepAlive()
this._doReconnect()
}
})
// 实时测量数据
plugin.bus.subscribe('progressData', (res: ProgressData) => {
console.log(TAG, 'bus[progressData] 体重(g):', res?.weight)
this._onProgressCb?.(res)
})
// 测量锁定(完成)
plugin.bus.subscribe('lockData', (res: LockData) => {
console.log(TAG, 'bus[lockData] 测量完成,体重(g):', res?.weight, '阻抗:', res?.resistance)
this._onLockedCb?.(res)
})
// 设备自动断开
plugin.bus.subscribe('deviceWillDisconnect', () => {
console.warn(TAG, 'bus[deviceWillDisconnect] 设备断开,触发重连')
this._stopKeepAlive()
this._onDisconnectedCb?.()
this._doReconnect()
})
}
/**
* 主动连接指定设备(对齐 master:直接调 createBLEConnection,不判断 deviceConnectType
*/
connect(device: ScannedDevice): void {
console.log(TAG, 'connect() 设备:', device.name)
this._p.Blue.createBLEConnection(device.raw)
}
/** 主动断开当前连接 */
disconnect(): void {
console.log(TAG, 'disconnect() 主动断开')
this._stopKeepAlive()
this._p.Blue.disconnect()
}
/**
* 停止扫描(仅停止设备发现,不清连接态)
* connectedDevice onHide / onUnload 时调用
*/
stopScan(): void {
console.log(TAG, 'stopScan() 停止扫描')
this._stopReconnectTimer()
this._busSubscribed = false
wx.stopBluetoothDevicesDiscovery()
this._onDevicesListCb = null
this._onDeviceConnectCb = null
}
// ─── WiFi 配网 ────────────────────────────────
/**
* 获取设备上报的 WiFi 列表(需设备已连接)
* 内部延迟 2 秒后调用,15 秒超时
*/
getWifiList(): Promise<LeFuWifiItem[]> {
console.log(TAG, 'getWifiList() 开始')
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('[LeFu] 设备未连接,无法获取 WiFi 列表'))
return
}
const timer = setTimeout(() => {
reject(new Error('[LeFu] 获取 WiFi 列表超时'))
}, 15000)
// 延迟 2 秒,等待设备就绪后再请求
setTimeout(() => {
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
clearTimeout(timer)
console.log(TAG, 'getWifiList 原始结果:', res)
if (!res || res.length === 0) {
this._activeProtocol.dataExitWifiConfig(() => {})
resolve([])
return
}
const list: LeFuWifiItem[] = res.map((item: any) => ({
ssid: item.ssid || item.name || '',
signal: item.signal,
}))
console.log(TAG, 'getWifiList 解析结果:', list.map(w => w.ssid))
resolve(list)
})
}, 2000)
})
}
/**
* 将 WiFi 配置写入设备
* @param ssid WiFi 名称
* @param password WiFi 密码
* @param version 协议版本:domain1(无鉴权)| domain2(有鉴权)
* 返回码 23 = 配网成功
*/
configWifi(ssid: string, password: string, version: WifiVersion): Promise<void> {
console.log(TAG, 'configWifi() ssid:', ssid, 'version:', version)
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('[LeFu] 设备未连接,无法配网'))
return
}
const callback = (res: number) => {
console.log(TAG, 'configWifi 回调 res:', res)
if (res === 23) {
console.log(TAG, 'configWifi 成功')
resolve()
} else {
console.error(TAG, 'configWifi 失败,错误码:', res)
reject(new Error(`[LeFu] 配网失败,错误码:${res}`))
}
}
if (version === 'domain1') {
this._activeProtocol.dataConfigNetWork(
{ domain: LEFU_CONFIG.domain1, ssid, password },
callback,
)
} else {
this._activeProtocol.dataConfigUserNetWork(
{
domain: LEFU_CONFIG.domain2,
ssid,
password,
userName: 'apiUser',
userPassword: '3acebb95eb49577e9c2a2082589b9bd6',
},
callback,
)
}
})
}
/**
* 查询设备是否已完成配网
* 返回 true = 已配网(0x01),false = 未配网
*/
checkWifiConfig(): Promise<boolean> {
console.log(TAG, 'checkWifiConfig() 查询配网状态')
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('[LeFu] 设备未连接'))
return
}
this._activeProtocol.codeFetchWifiConfig((res: number | undefined | null) => {
console.log(TAG, 'checkWifiConfig 回调 res:', res, '已配网:', res === 0x01)
resolve(res === 0x01)
})
})
}
// ─── OTA 升级 ─────────────────────────────────
/**
* 检查固件版本并按需触发 OTA 升级
* @returns true = 已触发升级,false = 无需升级
* 调用时机由业务层决定,不自动触发
*/
checkAndUpgrade(): Promise<boolean> {
console.log(TAG, 'checkAndUpgrade() 开始检查固件版本')
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('[LeFu] 设备未连接'))
return
}
if (!this._deviceInfo) {
reject(new Error('[LeFu] 固件信息未就绪,请稍后重试'))
return
}
const { firmwareRevision, modelNumber } = this._deviceInfo
console.log(TAG, '当前固件:', firmwareRevision, '型号:', modelNumber)
const [mcuVer, bleVer, wifiVer, resVer] = (firmwareRevision || '').split('.')
// 设备单段版本(如 "006"vs 服务端 x.y.z 格式(如 "0.0.6")比对
const isServerNewer = (deviceVer: string, serverVer: string): boolean => {
const deviceNum = parseInt(deviceVer, 10)
const serverNum = (serverVer || '').split('.').map(v => parseInt(v, 10)).reduce((acc, v) => acc * 1000 + v, 0)
return serverNum > deviceNum
}
get<FirmwareVersion>('weighingScale/v2/firmware/version', { type: modelNumber }, { loading: false })
.then(res => {
const { mcuVersion, bleVersion, wifiVersion, resVersion } = res.result
console.log(TAG, '服务端固件版本:', { mcuVersion, bleVersion, wifiVersion, resVersion })
const needUpdate =
isServerNewer(mcuVer, mcuVersion) ||
isServerNewer(bleVer, bleVersion) ||
isServerNewer(wifiVer, wifiVersion) ||
isServerNewer(resVer, resVersion)
console.log(TAG, '需要 OTA 升级:', needUpdate)
if (!needUpdate) {
resolve(false)
return
}
// status: false = 成功,true = 失败
this._activeProtocol.codeOtaUpdate((status: boolean) => {
console.log(TAG, 'OTA 升级回调 status:', status)
if (!status) {
resolve(true)
} else {
reject(new Error('[LeFu] OTA 升级失败'))
}
})
})
.catch(reject)
})
}
// ─── 私有:保活与重连 ─────────────────────────
private _startKeepAlive(): void {
this._stopKeepAlive()
console.log(TAG, '_startKeepAlive 保活心跳启动')
this._keepAliveTimer = setInterval(() => {
this._activeProtocol?.sendKeepAliveCode?.()
}, 15000)
}
private _stopKeepAlive(): void {
if (this._keepAliveTimer !== null) {
clearInterval(this._keepAliveTimer)
this._keepAliveTimer = null
console.log(TAG, '_stopKeepAlive 保活心跳停止')
}
}
private _stopReconnectTimer(): void {
if (this._reconnectTimer !== null) {
clearTimeout(this._reconnectTimer)
this._reconnectTimer = null
}
this._reconnectCount = 0
}
/** 自动重连,最多 _maxReconnect 次,超限后等待用户手动重试 */
private _doReconnect(): void {
if (this._reconnectCount >= this._maxReconnect) {
console.warn(TAG, '重连次数已达上限:', this._maxReconnect)
return
}
this._reconnectCount++
console.log(TAG, `_doReconnect 第 ${this._reconnectCount} 次重连`)
this._stopReconnectTimer()
this._reconnectTimer = setTimeout(() => {
this._plugin?.Blue?.disconnect((res: any) => {
console.log(TAG, '重连 disconnect 回调:', res?.errCode)
if (res?.errCode === 0) {
this._plugin?.Blue?.startBluetoothDevicesDiscovery()
}
})
}, 2000)
}
// ─── 成员同步 ─────────────────────────────────
/**
* 全量将成员列表同步到设备
* 流程:清除设备成员数据 → 逐个下发(顺序执行)
* 每步 15 秒超时保护,失败时自动重试当前成员
* @param members 成员列表,必须包含且仅包含一个 isSelf=true 的主用户
*/
syncMembersToDevice(members: DeviceMember[]): Promise<void> {
console.log(TAG, 'syncMembersToDevice() 成员数:', members.length)
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('[LeFu] 设备未连接,无法同步成员'))
return
}
const mainUser = members.find(m => m.isSelf)
if (!mainUser) {
reject(new Error('[LeFu] 成员列表中缺少主用户(isSelf=true'))
return
}
// 第一步:清除设备上的所有成员数据("01" 仅清成员,保留历史)
const clearTimer = setTimeout(() => {
reject(new Error('[LeFu] 清除设备成员超时'))
}, 15000)
this._activeProtocol.codeClearDeviceData('01', (clearRes: number) => {
clearTimeout(clearTimer)
console.log(TAG, 'codeClearDeviceData 回调:', clearRes)
if (clearRes !== 0x00) {
reject(new Error('[LeFu] 清除设备成员失败'))
return
}
// 第二步:逐个下发成员(顺序执行,失败自动重试当前)
let index = 0
let syncTimer: number | null = null
const resetTimer = () => {
if (syncTimer !== null) clearTimeout(syncTimer)
syncTimer = setTimeout(() => {
reject(new Error(`[LeFu] 第 ${index + 1} 个成员下发超时`))
}, 15000)
}
const syncNext = () => {
if (index >= members.length) {
if (syncTimer !== null) clearTimeout(syncTimer)
console.log(TAG, 'syncMembersToDevice 全部同步完成')
resolve()
return
}
const member = members[index]
console.log(TAG, `下发第 ${index + 1}/${members.length} 个成员:`, member.name)
resetTimer()
this._activeProtocol.dataSyncUserInfo(
{
userID: mainUser.id,
userName: member.name,
// 主用户 memberID 为空,子用户用自己的 id
memberID: member.isSelf ? '' : member.id,
age: member.age,
gender: member.gender,
height: member.height,
isAthleteMode: 0,
deviceHeaderIndex: index,
currentWeight: '',
targetWeight: '',
idealWeight: '',
recentData: [],
},
(res: number) => {
console.log(TAG, `第 ${index + 1} 个成员同步回调:`, res)
if (res === 0) {
index++
}
// 成功进下一个,失败重试当前(index 不变)
syncNext()
},
)
}
syncNext()
})
})
}
}
/** 全局单例,整个小程序共享同一个蓝牙服务实例 */
export const lefuService = new LeFuService()