feat(lefu): 迁移乐福体脂秤 SDK,接入设备扫描连接与 WiFi 配网

- 新增 lefu 服务层(扫描/连接/断开/保活重连/测量回调/WiFi 配网),事件回调支持多监听
- connectedDevice 接入真实蓝牙扫描连接,修复断线重连 UI 未同步、蓝牙/定位开关误判
- connectedWifi 接入真实 WiFi 配网,补连接状态校验与退出配网模式
- 新增 5 张设备图标并调整应用入口

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
17792275749
2026-08-20 15:57:16 +08:00
co-authored by Claude Opus 4.7
parent feb494080c
commit f53f11e6e9
14 changed files with 901 additions and 71 deletions
+11
View File
@@ -0,0 +1,11 @@
import type { LeFuConfig } from './types'
/** 乐福 SDK 配置 */
export const LEFU_CONFIG: LeFuConfig = {
key: 'lefudc91611833e18d94',
secret: 'eQ50JYimMp0X7uhNLj6D3lTEk4TUUvEG7dvaqKM9t1U=',
// domain1: 'http://10.10.10.10:8889',
// domain2: 'http://10.10.10.10:8889/weight',
domain1: 'http://device.shuziweidao.com:80/gateway',
domain2: 'http://device.shuziweidao.com:80/weight',
}
+45
View File
@@ -0,0 +1,45 @@
import type { DeviceSetting } from './types'
/** 支持的设备型号配置列表(5款) */
export 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,
},
]
+10
View File
@@ -0,0 +1,10 @@
export { leFuService } from './service'
export type {
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
LeFuConfig,
DeviceSetting,
DeviceInfo,
} from './types'
+410
View File
@@ -0,0 +1,410 @@
import { LEFU_CONFIG } from './config'
import { DEVICE_SETTINGS } from './deviceSettings'
import type {
RawDevice,
LeFuPlugin,
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
LeFuConfig,
DeviceInfo,
} from './types'
/** 顶层加载插件(对齐 demorequirePlugin 必须在文件顶层调用) */
const plugin = requirePlugin('ppScale-plugin') as LeFuPlugin
/** 统一日志前缀,便于定位问题 */
const TAG = '[LeFuService]'
/**
* 乐福体脂秤服务(单例)
* 只保留 SDK 底层必需能力:蓝牙扫描、连接、断开、测量回调、保活与重连。
* 业务层能力(WiFi 配网、用户同步、清空成员等)不在此维护。
*/
class LeFuService {
// ─── 内部状态 ───────────────────────────────
/** 当前激活的协议实例,非空表示已连接 */
private _activeProtocol: any = null
/** 当前蓝牙连接状态(取值来自 plugin.BLUE_STATE */
private _connectState = ''
/** 设备固件信息 */
private _deviceInfo: DeviceInfo | null = null
/** 总线是否已订阅(避免重复 subscribe) */
private _busSubscribed = false
/** 是否为用户主动发起的连接/断开(用于区分是否自动重连) */
private _intentionalConnect = false
/** 已重连次数 */
private _reconnectCount = 0
/** 最大自动重连次数 */
private readonly _maxReconnect = 3
/** 重连定时器 */
private _reconnectTimer: number | null = null
/** 保活心跳定时器 */
private _keepAliveTimer: number | null = null
/** 最近一次连接/扫描到的原始设备(用于断线重连) */
private _lastRawDevice: RawDevice | null = null
// ─── 事件回调(每个事件支持多个监听者,用 Set 存储) ───
private _onDevicesListCbs: Set<(devices: ScannedDevice[]) => void> = new Set()
private _onConnectStateCbs: Set<(state: string) => void> = new Set()
private _onDeviceConnectCbs: Set<() => void> = new Set()
private _onProgressCbs: Set<(data: ProgressData) => void> = new Set()
private _onLockedCbs: Set<(data: LockData) => void> = new Set()
private _onDisconnectedCbs: Set<() => void> = new Set()
private _onDeviceInfoCbs: Set<(info: DeviceInfo) => void> = new Set()
// ─── 只读状态访问器 ─────────────────────────
/** SDK 配置(key/secret 等) */
get config(): LeFuConfig { return LEFU_CONFIG }
/** 是否已连接(存在激活协议实例) */
get isConnected(): boolean { return !!this._activeProtocol }
/** 当前连接状态字符串 */
get connectState(): string { return this._connectState }
/** 蓝牙状态枚举(由插件提供,兜底为空对象) */
get BLUE_STATE(): Record<string, string> { return plugin.BLUE_STATE ?? {} }
/** 设备固件信息 */
get deviceInfo(): DeviceInfo | null { return this._deviceInfo }
// ─── 事件回调注册(均返回反注册函数) ───────
/** 订阅「扫描结果列表」事件 */
onDevicesList(cb: (devices: ScannedDevice[]) => void): () => void { this._onDevicesListCbs.add(cb); return () => { this._onDevicesListCbs.delete(cb) } }
/** 订阅「连接状态变化」事件 */
onConnectState(cb: (state: string) => void): () => void { this._onConnectStateCbs.add(cb); return () => { this._onConnectStateCbs.delete(cb) } }
/** 订阅「设备已连接」事件 */
onDeviceConnect(cb: () => void): () => void { this._onDeviceConnectCbs.add(cb); return () => { this._onDeviceConnectCbs.delete(cb) } }
/** 订阅「测量进度」事件 */
onMeasuring(cb: (data: ProgressData) => void): () => void { this._onProgressCbs.add(cb); return () => { this._onProgressCbs.delete(cb) } }
/** 订阅「测量锁定结果」事件 */
onLocked(cb: (data: LockData) => void): () => void { this._onLockedCbs.add(cb); return () => { this._onLockedCbs.delete(cb) } }
/** 订阅「断开连接」事件 */
onDisconnected(cb: () => void): () => void { this._onDisconnectedCbs.add(cb); return () => { this._onDisconnectedCbs.delete(cb) } }
/** 订阅「设备固件信息」事件 */
onDeviceInfo(cb: (info: DeviceInfo) => void): () => void { this._onDeviceInfoCbs.add(cb); return () => { this._onDeviceInfoCbs.delete(cb) } }
// ─── 扫描与连接 ─────────────────────────────
/**
* 开始扫描设备
* 初始化插件设备配置、订阅总线事件,然后启动蓝牙扫描。
*/
startScan(): void {
console.log(TAG, 'startScan()')
plugin.Blue.setDeviceSetting(DEVICE_SETTINGS)
this._subscribeBus()
plugin.Blue.start(DEVICE_SETTINGS.map(s => s.deviceName), false)
}
/**
* 订阅插件总线事件(幂等,只订阅一次)
* 将底层 SDK 事件桥接为服务层的回调。
*/
private _subscribeBus(): void {
if (this._busSubscribed) {
console.log(TAG, 'bus 已订阅,跳过')
return
}
this._busSubscribed = true
// 扫描结果列表:转换为展示层结构后回调
plugin.bus.subscribe('devicesList', (devList: RawDevice[]) => {
console.log(TAG, 'bus[devicesList],设备数:', devList?.length, devList)
if (!devList?.length) {
this._onDevicesListCbs.forEach(cb => cb([]))
return
}
const devices: ScannedDevice[] = devList.map((item: RawDevice) => ({
name: item.name || item.deviceName || '未知设备',
raw: item,
model: {},
}))
this._onDevicesListCbs.forEach(cb => cb(devices))
})
// 设备模型:仅打印日志,暂未对外暴露
plugin.bus.subscribe('devicesModel', (res: any) => {
console.log(TAG, 'bus[devicesModel] 完整数据:', JSON.stringify(res))
})
// 设备固件信息:缓存并回调
plugin.bus.subscribe('deviceInfo', (res: DeviceInfo) => {
console.log(TAG, 'bus[deviceInfo]:', res)
this._deviceInfo = res
this._onDeviceInfoCbs.forEach(cb => cb(res))
})
// 设备连接成功:重置重连计数,初始化协议实例并同步时间/MTU
plugin.bus.subscribe('deviceConnect', () => {
console.log(TAG, 'bus[deviceConnect]')
this._intentionalConnect = false
this._reconnectCount = 0
plugin.ScaleAction.startDataProgress(true)
this._activeProtocol = plugin.ScaleAction.getActiveProtocol()
if (!this._activeProtocol) {
console.error(TAG, 'getActiveProtocol() 返回空,无法完成连接初始化')
return
}
// 更新 MTU 后同步时间,全部完成再通知业务层「已连接」
this._activeProtocol.codeUpdateMTU((mtuRes: any) => {
console.log(TAG, 'codeUpdateMTU:', mtuRes)
this._activeProtocol.codeSyncTime((syncRes: any) => {
console.log(TAG, 'codeSyncTime:', syncRes)
this._onDeviceConnectCbs.forEach(cb => cb())
})
})
})
// 连接状态变化:驱动保活、重连与清理
plugin.bus.subscribe('connectState', (res: string) => {
console.log(TAG, 'bus[connectState]:', res)
this._connectState = res
this._onConnectStateCbs.forEach(cb => cb(res))
// 连接成功:启动保活心跳
if (res === plugin.BLUE_STATE.CONNECTSUCCESS) {
this._startKeepAlive()
return
}
// 连接失败:停止保活,尝试重连
if (res === plugin.BLUE_STATE.CONNECTFAILED) {
this._stopKeepAlive()
this._intentionalConnect = false
this._doReconnect()
return
}
// 蓝牙不可用:做完整清理
if (res === plugin.BLUE_STATE.UNAVAILABLE) {
this._stopKeepAlive()
this._stopReconnectTimer()
if (this._activeProtocol) {
this._activeProtocol = null
this._onDisconnectedCbs.forEach(cb => cb())
}
return
}
// 蓝牙就绪:如有缓存设备且非主动断开,尝试重连
if (res === plugin.BLUE_STATE.READY) {
if (this._intentionalConnect) {
this._intentionalConnect = false
} else if (this._lastRawDevice && !this._activeProtocol) {
this._stopReconnectTimer()
wx.openBluetoothAdapter({
success: () => this.connect(this._lastRawDevice!),
})
}
}
})
// 测量进度:透传给业务层
plugin.bus.subscribe('progressData', (res: ProgressData) => {
this._onProgressCbs.forEach(cb => cb(res))
})
// 测量锁定结果:透传给业务层
plugin.bus.subscribe('lockData', (res: LockData) => {
this._onLockedCbs.forEach(cb => cb(res))
})
// 即将断开:清理协议实例,若非主动断开则触发重连
plugin.bus.subscribe('deviceWillDisconnect', () => {
console.warn(TAG, 'bus[deviceWillDisconnect]intentionalConnect:', this._intentionalConnect)
this._stopKeepAlive()
this._activeProtocol = null
this._onDisconnectedCbs.forEach(cb => cb())
if (!this._intentionalConnect) {
this._doReconnect()
}
})
}
/**
* 连接指定设备
* 记录缓存设备、标记主动连接,清理旧状态后创建新的 BLE 连接。
*/
connect(rawDevice: RawDevice): void {
console.log(TAG, 'connect():', rawDevice.name)
this._lastRawDevice = rawDevice
this._intentionalConnect = true
this._stopKeepAlive()
this._stopReconnectTimer()
// 先断开旧连接(此时状态仍在),再清空内部状态
if (this.isConnected) {
plugin.Blue.disconnect()
}
this._activeProtocol = null
this._connectState = ''
this._deviceInfo = null
// 直连缓存设备时不走 startScan,需手动初始化插件设备配置和总线订阅
plugin.Blue.setDeviceSetting(DEVICE_SETTINGS)
this._subscribeBus()
plugin.Blue.stopBluetoothDevicesDiscovery()
plugin.Blue.clearProtocol()
plugin.Blue.createBLEConnection(rawDevice)
}
/**
* 主动断开连接
* 标记主动断开以阻止自动重连,并停止保活。
*/
disconnect(): void {
console.log(TAG, 'disconnect()')
this._intentionalConnect = true
this._stopKeepAlive()
plugin.Blue.disconnect()
}
/**
* 停止扫描
* 停止蓝牙设备发现、清除重连定时器与扫描回调。
*/
stopScan(): void {
console.log(TAG, 'stopScan()')
this._stopReconnectTimer()
wx.stopBluetoothDevicesDiscovery()
this._onDevicesListCbs.clear()
}
// ─── WiFi 配网 ─────────────────────────────
/**
* 获取周围 WiFi 列表
* 依赖已连接的设备协议实例,下发查询指令后返回附近热点。
*/
getWifiList(): Promise<LeFuWifiItem[]> {
console.log(TAG, 'getWifiList()')
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('设备未连接,无法获取 WiFi 列表'))
return
}
let timer: number | null = null
let delayTimer: number | null = null
// 15s 整体超时
timer = setTimeout(() => {
if (delayTimer !== null) { clearTimeout(delayTimer); delayTimer = null }
reject(new Error('获取 WiFi 列表超时'))
}, 15000)
// 延时 2s 后下发查询指令(等待设备进入配网模式)
delayTimer = setTimeout(() => {
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
if (timer !== null) { clearTimeout(timer); timer = null }
if (delayTimer !== null) { clearTimeout(delayTimer); delayTimer = null }
console.log(TAG, 'getWifiList 结果:', res)
if (!res?.length) {
this._activeProtocol.dataExitWifiConfig(() => { })
resolve([])
return
}
resolve(res.map((item: any) => ({ ssid: item.ssid || item.name || '', signal: item.signal })))
})
}, 2000)
})
}
/**
* 配置设备连接指定 WiFi
* 型号含 5 段走新协议 domain2,否则走老协议 domain1;成功返回码为 23。
*/
configWifi(ssid: string, password: string): Promise<void> {
const version = (this._deviceInfo?.modelNumber ?? '').split('-').length === 5 ? 'domain2' : 'domain1'
console.log(TAG, 'configWifi():', ssid, version, 'activeProtocol=', !!this._activeProtocol)
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('设备未连接,无法配网'))
return
}
let settled = false
const timer = setTimeout(() => {
if (settled) return
settled = true
reject(new Error('配网超时,请重试'))
}, 20000)
const callback = (res: number) => {
if (settled) return
settled = true
clearTimeout(timer)
res === 23 ? resolve() : reject(new Error(`配网失败,错误码:${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,
)
}
})
}
/**
* 查询设备是否已配置 WiFi
* 返回码 0x01 表示已配网。
*/
checkWifiConfig(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
this._activeProtocol.codeFetchWifiConfig((res: number | undefined | null) => {
resolve(res === 0x01)
})
})
}
/**
* 退出配网模式
* 配网流程结束(含页面退出)时调用,让设备退出 WiFi 配置状态。
*/
exitWifiConfig(): void {
if (!this._activeProtocol) return
this._activeProtocol.dataExitWifiConfig(() => {
console.log(TAG, '退出配网模式完成')
})
}
// ─── 私有:保活与重连 ───────────────────────
/** 启动保活心跳:文档建议每 10s 发送一次 keepAlive,防止设备主动断开 */
private _startKeepAlive(): void {
this._stopKeepAlive()
this._keepAliveTimer = setInterval(() => { this._activeProtocol?.sendKeepAliveCode?.() }, 10000)
}
/** 停止保活心跳定时器 */
private _stopKeepAlive(): void {
if (this._keepAliveTimer !== null) {
clearInterval(this._keepAliveTimer)
this._keepAliveTimer = null
}
}
/** 停止重连定时器并重置重连计数 */
private _stopReconnectTimer(): void {
if (this._reconnectTimer !== null) {
clearTimeout(this._reconnectTimer)
this._reconnectTimer = null
}
this._reconnectCount = 0
}
/**
* 执行自动重连
* 超过最大次数则放弃;否则延时 2s 后重建连接。
*/
private _doReconnect(): void {
if (this._reconnectCount >= this._maxReconnect) {
console.warn(TAG, '重连次数已达上限')
this._onDisconnectedCbs.forEach(cb => cb())
return
}
if (!this._lastRawDevice) {
console.warn(TAG, '无缓存设备,放弃重连')
return
}
this._reconnectCount++
console.log(TAG, `_doReconnect 第 ${this._reconnectCount}`)
this._stopReconnectTimer()
this._reconnectTimer = setTimeout(() => {
plugin.Blue.clearProtocol()
plugin.Blue.createBLEConnection(this._lastRawDevice!)
}, 2000)
}
}
/** 导出单例实例 */
export const leFuService = new LeFuService()
+90
View File
@@ -0,0 +1,90 @@
/** ppScale-plugin 原始设备对象 */
export type RawDevice = Record<string, any>
/** 乐福 SDK 插件对象(运行时由 requirePlugin 注入) */
export type LeFuPlugin = Record<string, any>
/** 设备配置(来自 master device.setting 列表) */
export interface DeviceSetting {
advLength: number
calorieStatus: number
deviceAccuracyType: number
deviceCalcuteType: number
deviceConnectType: number
deviceFuncType: number
deviceName: string
devicePowerType: number
deviceProtocolType: number
deviceType: number
deviceUnitType: string
id: number
macAddressStart: number
sign: string
status: number
uhStatus: number
createBy: null
imgUrl: null
remark: null
updateBy: null
}
/** 扫描到的设备(展示层使用) */
export interface ScannedDevice {
/** 设备名称 */
name: string
/** plugin 返回的原始设备对象,连接时传回给 SDK */
raw: RawDevice
/** 设备模型(含 deviceConnectType 等信息) */
model: Record<string, any>
}
/** WiFi 项 */
export interface LeFuWifiItem {
/** WiFi SSID */
ssid: string
/** 信号强度(部分设备上报) */
signal?: number
}
/** 测量进度数据(progressData 事件,测量过程中多次回调) */
export interface ProgressData {
/** 体重,单位:克 */
weight: number
/** 单位枚举值 */
unit: number
/** 是否正在心率测量 */
isHeartRating: boolean
}
/** 测量锁定数据(lockData 事件,测量完成时回调一次) */
export interface LockData {
/** 体重,单位:克 */
weight: number
/** 阻抗值(用于体成分分析) */
resistance: number
/** 心率(部分设备上报) */
heartRate?: number
[key: string]: any
}
/** 乐福服务配置 */
export interface LeFuConfig {
key: string
secret: string
/** 老协议域名(无鉴权) */
domain1: string
/** 新协议域名(有鉴权) */
domain2: string
}
/** 设备固件信息(deviceInfo 事件返回) */
export interface DeviceInfo {
/** 固件版本号,格式:mcu.ble.wifi.res,如 "006.005.004.305" */
firmwareRevision: string
/** 设备型号,用于请求服务端版本接口 */
modelNumber: string
/** 设备序列号 */
serialNumber: string
[key: string]: any
}