Files
bodyWeight/miniprogram/lefu/service.ts
T

516 lines
18 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
/** 最近一次连接/扫描到的原始设备(用于断线重连) */
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()
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)
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)
})
})
}
// ─── 私有:保活与重连 ───────────────────────
/** 启动保活心跳:文档建议每 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()