import { LEFU_CONFIG } from './config' import { DEVICE_SETTINGS } from './deviceSettings' import type { RawDevice, LeFuPlugin, ScannedDevice, LeFuWifiItem, ProgressData, LockData, LeFuConfig, DeviceInfo, } 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 { 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 { 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 { 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 { 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()