Files
bodyWeight/miniprogram/lefu/service.ts
T
17792275749andClaude Opus 4.7 ea52c495f6 [AI Generated]: feat(device): 配网/绑定后下发主用户到设备并兜底超时
- configWifi 加 20s 超时与 settled 互锁,避免回调不触发导致 loading 死转
- supplementPersonal 提交后、connectedWifi 二次配网确认后均下发主用户
- 设备成员唯一标识从 idCard 切换为 userId,清理 MemberDisplay.idCard

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:56:44 +08:00

443 lines
19 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 { get } from '../utils/request/index'
import type {
RawDevice,
LeFuPlugin,
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
LeFuConfig,
DeviceSetting,
DeviceMember,
DeviceInfo,
FirmwareVersion,
} from './types'
/** 顶层加载插件(对齐 demorequirePlugin 必须在文件顶层调用) */
const plugin = requirePlugin('ppScale-plugin') as LeFuPlugin
/** 乐福 SDK 配置 */
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',
}
/** 支持的设备型号配置列表(5款) */
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,
},
]
const TAG = '[LeFuService]'
class LeFuService {
private _activeProtocol: any = null
private _connectState = ''
private _deviceInfo: DeviceInfo | null = null
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
private _onDevicesListCb: ((devices: ScannedDevice[]) => void) | null = null
private _onConnectStateCb: ((state: string) => void) | null = null
private _onDeviceConnectCb: (() => void) | null = null
private _onProgressCb: ((data: ProgressData) => void) | null = null
private _onLockedCb: ((data: LockData) => void) | null = null
private _onDisconnectedCb: (() => void) | null = null
private _onDeviceInfoCb: ((info: DeviceInfo) => void) | null = null
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._onDevicesListCb = cb }
onConnectState(cb: (state: string) => void): void { this._onConnectStateCb = cb }
onDeviceConnect(cb: () => void): void { this._onDeviceConnectCb = cb }
onMeasuring(cb: (data: ProgressData) => void): void { this._onProgressCb = cb }
onLocked(cb: (data: LockData) => void): void { this._onLockedCb = cb }
onDisconnected(cb: () => void): void { this._onDisconnectedCb = cb }
onDeviceInfo(cb: (info: DeviceInfo) => void): void { this._onDeviceInfoCb = cb }
// ─── 扫描与连接 ───────────────────────────────
startScan(): void {
console.log(TAG, 'startScan()')
plugin.Blue.setDeviceSetting(DEVICE_SETTINGS)
this._subscribeBus()
plugin.Blue.start(DEVICE_SETTINGS.map(s => s.deviceName), false)
}
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._onDevicesListCb?.([])
return
}
const devices: ScannedDevice[] = devList.map((item: RawDevice) => ({
name: item.name || item.deviceName || '未知设备',
raw: item,
model: {},
}))
this._onDevicesListCb?.(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._onDeviceInfoCb?.(res)
})
plugin.bus.subscribe('deviceConnect', () => {
console.log(TAG, 'bus[deviceConnect]')
this._intentionalConnect = false
this._reconnectCount = 0
plugin.ScaleAction.startDataProgress(true)
this._activeProtocol = plugin.ScaleAction.getActiveProtocol()
this._connectState = plugin.BLUE_STATE.CONNECTSUCCESS
this._activeProtocol.codeUpdateMTU((mtuRes: any) => {
console.log(TAG, 'codeUpdateMTU:', mtuRes)
this._activeProtocol.codeSyncTime((syncRes: any) => {
console.log(TAG, 'codeSyncTime:', syncRes)
this._onDeviceConnectCb?.()
})
})
})
plugin.bus.subscribe('connectState', (res: string) => {
console.log(TAG, 'bus[connectState]:', res)
this._connectState = res
this._onConnectStateCb?.(res)
if (res === plugin.BLUE_STATE.CONNECTSUCCESS) {
this._startKeepAlive()
}
if (res === plugin.BLUE_STATE.CONNECTFAILED) {
this._stopKeepAlive()
if (!this._intentionalConnect) {
this._doReconnect()
}
}
})
plugin.bus.subscribe('progressData', (res: ProgressData) => {
this._onProgressCb?.(res)
})
plugin.bus.subscribe('lockData', (res: LockData) => {
this._onLockedCb?.(res)
})
plugin.bus.subscribe('deviceWillDisconnect', () => {
console.warn(TAG, 'bus[deviceWillDisconnect]intentionalConnect:', this._intentionalConnect)
this._activeProtocol = null
this._stopKeepAlive()
this._onDisconnectedCb?.()
if (!this._intentionalConnect) {
this._doReconnect()
}
})
}
connect(rawDevice: RawDevice): void {
console.log(TAG, 'connect():', rawDevice.name)
this._lastRawDevice = rawDevice
this._intentionalConnect = true
this._stopKeepAlive()
this._stopReconnectTimer()
this._activeProtocol = null
this._connectState = ''
this._deviceInfo = null
// 直连缓存设备时不走 startScan,需手动初始化插件设备配置和总线订阅
plugin.Blue.setDeviceSetting(DEVICE_SETTINGS)
this._subscribeBus()
plugin.Blue.stopBluetoothDevicesDiscovery()
// 0.0.25 新增:重新连接前必须清除协议缓存
plugin.Blue.clearProtocol()
plugin.Blue.createBLEConnection(rawDevice)
}
disconnect(): void {
console.log(TAG, 'disconnect()')
this._stopKeepAlive()
plugin.Blue.disconnect()
}
/** 读缓存设备信息,自动扫描匹配后连接,已连接或无缓存时直接返回 */
autoConnect(): void {
if (this.isConnected) return
const cached = wx.getStorageSync('connectDeviceInfo') as RawDevice | null
const targetDeviceId = cached?.deviceId as string | undefined
if (!targetDeviceId) return
let scanTimer: number | null = null
wx.openBluetoothAdapter({
success: () => {
scanTimer = setTimeout(() => {
this.stopScan()
}, 15000)
this.onDevicesList((devices) => {
const matched = devices.find(d => d.raw.deviceId === targetDeviceId)
if (!matched) return
if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null }
this.stopScan()
wx.setStorageSync('connectDeviceInfo', matched.raw)
this.connect(matched.raw)
})
this.startScan()
},
fail: () => {
if (scanTimer !== null) { clearTimeout(scanTimer); scanTimer = null }
},
})
}
stopScan(): void {
console.log(TAG, 'stopScan()')
this._stopReconnectTimer()
wx.stopBluetoothDevicesDiscovery()
this._onDevicesListCb = null
}
// ─── WiFi 配网 ────────────────────────────────
getWifiList(): Promise<LeFuWifiItem[]> {
console.log(TAG, 'getWifiList()')
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('[LeFu] 设备未连接,无法获取 WiFi 列表'))
return
}
const timer = setTimeout(() => reject(new Error('[LeFu] 获取 WiFi 列表超时')), 15000)
setTimeout(() => {
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
clearTimeout(timer)
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)
})
}
configWifi(ssid: string, password: string): Promise<void> {
const version = (this._deviceInfo?.modelNumber ?? '').split('-').length === 5 ? 'domain2' : 'domain1'
console.log(TAG, 'configWifi():', ssid, version)
console.log(TAG, 'configNetWork():',this._deviceInfo?.modelNumber, version)
return new Promise((resolve, reject) => {
if (!this._activeProtocol) {
reject(new Error('[LeFu] 设备未连接,无法配网'))
return
}
let settled = false
const timer = setTimeout(() => {
if (settled) return
settled = true
reject(new Error('[LeFu] 配网超时,请重试'))
}, 20000)
const callback = (res: number) => {
if (settled) return
settled = true
clearTimeout(timer)
console.log(TAG, 'configWifi 回调:', res)
res === 23 ? resolve() : reject(new Error(`[LeFu] 配网失败,错误码:${res}`))
}
if (version === 'domain1') {
console.log(TAG, 'domain1 dataConfigNetWork')
this._activeProtocol.dataConfigNetWork({ domain: LEFU_CONFIG.domain1, ssid, password }, callback)
} else {
console.log(TAG, 'domain2 dataConfigUserNetWork')
this._activeProtocol.dataConfigUserNetWork(
{ domain: LEFU_CONFIG.domain2, ssid, password, userName: 'apiUser', userPassword: '3acebb95eb49577e9c2a2082589b9bd6' },
callback,
)
}
})
}
checkWifiConfig(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('[LeFu] 设备未连接')); return }
this._activeProtocol.codeFetchWifiConfig((res: number | undefined | null) => {
resolve(res === 0x01)
})
})
}
// ─── OTA 升级 ─────────────────────────────────
checkAndUpgrade(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('[LeFu] 设备未连接')); return }
if (!this._deviceInfo) { reject(new Error('[LeFu] 固件信息未就绪')); return }
const { firmwareRevision, modelNumber } = this._deviceInfo
const [mcuVer, bleVer, wifiVer, resVer] = (firmwareRevision || '').split('.')
const isServerNewer = (dv: string, sv: string) => {
const dn = parseInt(dv, 10)
const sn = (sv || '').split('.').map(v => parseInt(v, 10)).reduce((a, v) => a * 1000 + v, 0)
return sn > dn
}
get<FirmwareVersion>('weighingScale/v2/firmware/version', { type: modelNumber }, { loading: false })
.then(res => {
const { mcuVersion, bleVersion, wifiVersion, resVersion } = res.result
const needUpdate = isServerNewer(mcuVer, mcuVersion) || isServerNewer(bleVer, bleVersion)
|| isServerNewer(wifiVer, wifiVersion) || isServerNewer(resVer, resVersion)
if (!needUpdate) { resolve(false); return }
this._activeProtocol.codeOtaUpdate((status: boolean) => {
status ? reject(new Error('[LeFu] OTA 升级失败')) : resolve(true)
})
})
.catch(reject)
})
}
// ─── 私有:保活与重连 ─────────────────────────
private _startKeepAlive(): void {
this._stopKeepAlive()
this._keepAliveTimer = setInterval(() => { this._activeProtocol?.sendKeepAliveCode?.() }, 15000)
}
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
}
private _doReconnect(): void {
if (this._reconnectCount >= this._maxReconnect) {
console.warn(TAG, '重连次数已达上限')
this._onDisconnectedCb?.()
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)
}
// ─── 成员同步 ─────────────────────────────────
syncMembersToDevice(members: DeviceMember[], onProgress?: (current: number, total: number) => void): Promise<void> {
return new Promise((resolve, reject) => {
if (!this._activeProtocol) { reject(new Error('[LeFu] 设备未连接')); return }
const mainUser = members.find(m => m.isSelf)
if (!mainUser) { reject(new Error('[LeFu] 缺少主用户')); return }
const clearTimer = setTimeout(() => reject(new Error('[LeFu] 清除设备成员超时')), 15000)
this._activeProtocol.codeClearDeviceData('01', (clearRes: number) => {
clearTimeout(clearTimer)
if (clearRes !== 0x00) { reject(new Error('[LeFu] 清除设备成员失败')); return }
let index = 0
let syncTimer: number | null = null
const resetTimer = () => {
if (syncTimer !== null) clearTimeout(syncTimer)
syncTimer = setTimeout(() => reject(new Error(`[LeFu] 第 ${index + 1} 个成员下发超时`)), 15000)
}
const syncNext = () => {
if (index >= members.length) {
if (syncTimer !== null) clearTimeout(syncTimer)
resolve()
return
}
const member = members[index]
onProgress?.(index + 1, members.length)
resetTimer()
this._activeProtocol.dataSyncUserInfo(
{
userID: mainUser.id, userName: member.name,
memberID: member.isSelf ? '' : member.id,
age: member.age, gender: member.gender, height: member.height,
isAthleteMode: 0, deviceHeaderIndex: index,
currentWeight: '', targetWeight: '', idealWeight: '', recentData: [],
},
(res: number) => { if (res === 0) index++; syncNext() },
)
}
syncNext()
})
})
}
}
export const lefuService = new LeFuService()