516 lines
18 KiB
TypeScript
516 lines
18 KiB
TypeScript
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'
|
||
|
||
/** 顶层加载插件(对齐 demo,requirePlugin 必须在文件顶层调用) */
|
||
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()
|