Files
bodyWeight/miniprogram/lefu/service.ts
T
17792275749andClaude Opus 4.7 dbb2ab8423 fix: 修复蓝牙连接误报与 WiFi 列表获取问题,完善一键添加本人
- connectedDevice 忽略 connect 过程中的 UNAVAILABLE 抖动,避免误报蓝牙关闭
- getWifiList 加 settled 保护,onStartSearch 加防重入
- userManagement 一键添加本人(仅加后台、不下发设备)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-27 14:02:21 +08:00

577 lines
20 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 { LEFU_CONFIG } from './config'
import { DEVICE_SETTINGS } from './deviceSettings'
import { WIFI_CONFIG_STATE_TEXT } from './types'
import type {
RawDevice,
LeFuPlugin,
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
LeFuConfig,
DeviceInfo,
SyncUserData,
} 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
/**
* 最近一次连接/扫描到的原始设备(内存缓存)
* 用途:
* 1. 意外断线时自动重连(_doReconnect / connectState READY 分支)
* 2. 切后台回前台后自动重连(autoConnect
* 3. 业务层通过 lastRawDevice 判断「当前连接的是哪台设备」
* 生命周期:
* - connect() 时写入
* - disconnect() 主动断开时清空(用户主动断开,不再需要重连)
* - suspend() 切后台时保留(回前台 autoConnect 靠它重连)
* - 意外断开时保留(自动重连靠它)
*/
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 }
/** 最近一次连接/扫描到的原始设备 */
get lastRawDevice(): RawDevice | null { return this._lastRawDevice }
// ─── 事件回调注册(均返回反注册函数) ───────
/** 订阅「扫描结果列表」事件 */
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()')
this._intentionalConnect = true
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._lastRawDevice && !this._activeProtocol && this._onDevicesListCbs.size === 0) {
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()
if (this._activeProtocol) {
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)
}
/**
* 主动断开连接(用户主动操作:断开、退出登录、取消配对)
* - 断开蓝牙连接、停止保活
* - 清理 _activeProtocol(主动断开立即清,不依赖后台事件,避免残留)
* - 清空 _lastRawDevice(主动断开后不再需要自动重连,避免残留)
*/
disconnect(): void {
console.log(TAG, 'disconnect()')
this._intentionalConnect = true
this._stopKeepAlive()
plugin.Blue.disconnect()
if (this._activeProtocol) {
this._activeProtocol = null
this._onDisconnectedCbs.forEach(cb => cb())
}
this._lastRawDevice = null
}
/**
* 切后台挂起(小程序 onHide 时调用)
* 和 disconnect 的唯一区别:断开连接、清理 _activeProtocol,但【保留 _lastRawDevice】,
* 这样回前台时 autoConnect 才能用缓存的设备重连。
*/
suspend(): void {
console.log(TAG, 'suspend()')
this._intentionalConnect = true
this._stopKeepAlive()
plugin.Blue.disconnect()
if (this._activeProtocol) {
this._activeProtocol = null
this._onDisconnectedCbs.forEach(cb => cb())
}
}
/**
* 回前台自动重连(小程序 onShow 时调用)
* 用 suspend 保留的 _lastRawDevice 重连;已连接或无缓存设备则跳过。
*/
autoConnect(): void {
if (this.isConnected) return
if (!this._lastRawDevice) return
this.connect(this._lastRawDevice)
}
/**
* 停止扫描
* 停止蓝牙设备发现、清除重连定时器与扫描回调。
*/
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 settled = false
let timer: number | null = null
let delayTimer: number | null = null
const settle = (err: Error | null, data: LeFuWifiItem[] = []) => {
if (settled) return
settled = true
if (timer !== null) { clearTimeout(timer); timer = null }
if (delayTimer !== null) { clearTimeout(delayTimer); delayTimer = null }
err ? reject(err) : resolve(data)
}
// 15s 整体超时
timer = setTimeout(() => settle(new Error('获取 WiFi 列表超时')), 15000)
// 延时 2s 后下发查询指令(等待设备进入配网模式)
delayTimer = setTimeout(() => {
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
console.log(TAG, 'getWifiList 结果:', res)
if (!res?.length) {
this._activeProtocol.dataExitWifiConfig(() => { })
settle(null, [])
return
}
settle(null, 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)
if (res === 23) {
resolve()
return
}
reject(new Error(WIFI_CONFIG_STATE_TEXT[res] || `配网失败,错误码:${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, '退出配网模式完成')
})
}
// ─── 用户同步 ─────────────────────────────
/**
* 清空设备内的用户数据
* 下发清空指令,让设备清除已录入的所有成员。
*/
clearDeviceMembers(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
let settled = false
const settle = (err: Error | null) => {
if (settled) return
settled = true
clearTimeout(timer)
this._onDisconnectedCbs.delete(onDisconnect)
err ? reject(err) : resolve()
}
// 操作中设备断开:立即失败,不等超时
const onDisconnect = () => settle(new Error('设备已断开'))
this._onDisconnectedCbs.add(onDisconnect)
const timer = setTimeout(() => settle(new Error('请求超时')), 15000)
this._activeProtocol.codeClearDeviceData('01', (res: number) => {
settle(res === 0 ? null : new Error('请求失败'))
})
})
}
/**
* 同步成员到设备(只负责逐个下发)
* 参数列表由调用方组装好,本方法不做字段转换,onProgress 回调已完成的用户数。
*/
syncMembersToDevice(list: SyncUserData[], onProgress?: (current: number, total: number) => void): Promise<void> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
if (!list.length) { reject(new Error('无用户可同步')); return }
let settled = false
let index = 0
let syncTimer: number | null = null
const settle = (err: Error | null) => {
if (settled) return
settled = true
if (syncTimer !== null) clearTimeout(syncTimer)
this._onDisconnectedCbs.delete(onDisconnect)
err ? reject(err) : resolve()
}
// 操作中设备断开:立即失败,不等超时
const onDisconnect = () => settle(new Error('设备已断开'))
this._onDisconnectedCbs.add(onDisconnect)
const resetTimer = () => {
if (syncTimer !== null) clearTimeout(syncTimer)
syncTimer = setTimeout(() => settle(new Error('请求超时')), 15000)
}
const syncNext = () => {
if (settled) return
if (index >= list.length) {
settle(null)
return
}
resetTimer()
this._activeProtocol.dataSyncUserInfo(list[index], (res: number) => {
if (settled) return
if (res !== 0) {
settle(new Error('请求失败'))
return
}
index++
onProgress?.(index, list.length)
syncNext()
})
}
syncNext()
})
}
/**
* 获取设备端主用户的 userId 列表
* 未连接 reject;成功返回主用户 userId 数组,无主用户返回 null
*/
fetchDeviceUserIds(): Promise<string[] | null> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('设备未连接')); return }
this._activeProtocol.dataFetchUserID((res: any) => {
console.log(TAG, 'dataFetchUserID:', res)
resolve(res ?? null)
})
})
}
/**
* 读取设备端指定用户的历史测量数据(数据补传)
* 请求该用户的历史记录,结果通过回调返回
*/
fetchHistoryData(userId: string, cb?: (list: any[]) => void): void {
if (!this._activeProtocol) return
this._activeProtocol.dataFetchHistoryData({ userID: userId }, (res: any) => {
console.log(TAG, 'dataFetchHistoryData:', res)
cb?.(res ?? [])
})
}
// ─── 私有:保活与重连 ───────────────────────
/** 启动保活心跳:文档建议每 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(() => {
// 延时期间蓝牙可能已不可用,重连前再判断一次
if (this._connectState === plugin.BLUE_STATE.UNAVAILABLE) {
console.warn(TAG, '蓝牙不可用,放弃重连')
this._onDisconnectedCbs.forEach(cb => cb())
return
}
plugin.Blue.clearProtocol()
plugin.Blue.createBLEConnection(this._lastRawDevice!)
}, 2000)
}
}
/** 导出单例实例 */
export const leFuService = new LeFuService()