524 lines
19 KiB
TypeScript
524 lines
19 KiB
TypeScript
import { get } from '../utils/request/index'
|
||
import type {
|
||
RawDevice,
|
||
LeFuPlugin,
|
||
ScannedDevice,
|
||
LeFuWifiItem,
|
||
ProgressData,
|
||
LockData,
|
||
WifiVersion,
|
||
LeFuConfig,
|
||
DeviceSetting,
|
||
DeviceMember,
|
||
DeviceInfo,
|
||
FirmwareVersion,
|
||
} from './types'
|
||
|
||
/** 乐福 SDK 配置 */
|
||
const LEFU_CONFIG: LeFuConfig = {
|
||
key: 'lefudc91611833e18d94',
|
||
secret: 'eQ50JYimMp0X7uhNLj6D3lTEk4TUUvEG7dvaqKM9t1U=',
|
||
domain1: 'http://192.168.1.31:8889', // 测试环境(老版本 无鉴权)
|
||
domain2: 'http://192.168.1.31: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,
|
||
},
|
||
]
|
||
|
||
class LeFuService {
|
||
private _plugin: LeFuPlugin | null = null
|
||
/** 设备协议对象,devicesModel 事件返回,用于 WiFi 配网等高级操作 */
|
||
private _activeProtocol: any = null
|
||
|
||
/** 重连次数 */
|
||
private _reconnectCount = 0
|
||
private readonly _maxReconnect = 3
|
||
private _reconnectTimer: number | null = null
|
||
|
||
/** 保活心跳定时器 */
|
||
private _keepAliveTimer: number | null = null
|
||
|
||
/** 连接后从 deviceInfo 事件获取的固件信息 */
|
||
private _deviceInfo: DeviceInfo | null = null
|
||
|
||
/** 回调注册(页面通过 on* 方法注册,stopScan 时清空) */
|
||
private _onDevicesListCb: ((devices: ScannedDevice[]) => void) | null = null
|
||
private _onConnectStateCb: ((state: string) => void) | null = null
|
||
private _onProgressCb: ((data: ProgressData) => void) | null = null
|
||
private _onLockedCb: ((data: LockData) => void) | null = null
|
||
private _onDisconnectedCb: (() => void) | null = null
|
||
|
||
/** 在 app.ts onLaunch 中调用,加载 ppScale-plugin */
|
||
init(): void {
|
||
this._plugin = requirePlugin('ppScale-plugin') as LeFuPlugin
|
||
}
|
||
|
||
/** 获取插件(未初始化时抛出) */
|
||
private get _p(): LeFuPlugin {
|
||
if (!this._plugin) throw new Error('[LeFu] 服务未初始化,请先调用 init()')
|
||
return this._plugin
|
||
}
|
||
|
||
/** 乐福配置(供体成分分析等外部使用) */
|
||
get config(): LeFuConfig {
|
||
return LEFU_CONFIG
|
||
}
|
||
|
||
// ─── 回调注册 ─────────────────────────────────
|
||
|
||
/** 注册设备列表回调(扫描到设备时触发) */
|
||
onDevicesList(cb: (devices: ScannedDevice[]) => void): void {
|
||
this._onDevicesListCb = cb
|
||
}
|
||
|
||
/** 注册蓝牙连接状态回调 */
|
||
onConnectState(cb: (state: string) => void): void {
|
||
this._onConnectStateCb = 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
|
||
}
|
||
|
||
// ─── 扫描与连接 ───────────────────────────────
|
||
|
||
/**
|
||
* 开始蓝牙扫描
|
||
* 先注册所有 bus 事件再调用 plugin.Blue.start()
|
||
* 页面 onShow 时调用
|
||
*/
|
||
startScan(): void {
|
||
const plugin = this._p
|
||
|
||
// 扫描到设备列表
|
||
plugin.bus.subscribe('devicesList', (devList: RawDevice[]) => {
|
||
// 广播秤(BleAdv)自动调用 setbroadcastDev,无需用户手动点击连接
|
||
const advDev = devList.find((item: RawDevice) => {
|
||
const model = plugin.Blue.getDeviceModel(item)
|
||
return model?.deviceConnectType === plugin.PPBluetoothDefine.PPDeviceConnectType.PPDeviceConnectTypeBleAdv
|
||
})
|
||
if (advDev) {
|
||
plugin.Blue.setbroadcastDev(advDev)
|
||
}
|
||
|
||
// 转换为展示层数据
|
||
const devices: ScannedDevice[] = devList.map((item: RawDevice) => ({
|
||
name: item.name || item.deviceName || '未知设备',
|
||
raw: item,
|
||
model: plugin.Blue.getDeviceModel(item) || {},
|
||
}))
|
||
this._onDevicesListCb?.(devices)
|
||
})
|
||
|
||
// 协议对象(持有 activeProtocol,WiFi 配网等高级操作依赖它)
|
||
plugin.bus.subscribe('devicesModel', (res: any) => {
|
||
this._activeProtocol = res
|
||
})
|
||
|
||
// 连接成功后获取固件信息(用于后续 OTA 版本比对)
|
||
plugin.bus.subscribe('deviceInfo', (res: DeviceInfo) => {
|
||
this._deviceInfo = res
|
||
})
|
||
|
||
// 连接成功
|
||
plugin.bus.subscribe('deviceConnect', () => {
|
||
this._reconnectCount = 0
|
||
plugin.ScaleAction.startDataProgress()
|
||
this._startKeepAlive()
|
||
})
|
||
|
||
// 连接状态变化
|
||
plugin.bus.subscribe('connectState', (res: string) => {
|
||
this._onConnectStateCb?.(res)
|
||
if (res === plugin.BLUE_STATE.CONNECTFAILED) {
|
||
this._stopKeepAlive()
|
||
this._doReconnect()
|
||
}
|
||
})
|
||
|
||
// 实时测量数据
|
||
plugin.bus.subscribe('progressData', (res: ProgressData) => {
|
||
this._onProgressCb?.(res)
|
||
})
|
||
|
||
// 测量锁定(完成)
|
||
plugin.bus.subscribe('lockData', (res: LockData) => {
|
||
this._onLockedCb?.(res)
|
||
})
|
||
|
||
// 设备自动断开
|
||
plugin.bus.subscribe('deviceWillDisconnect', () => {
|
||
this._stopKeepAlive()
|
||
this._onDisconnectedCb?.()
|
||
this._doReconnect()
|
||
})
|
||
|
||
// 开始扫描,传入设备配置过滤列表
|
||
plugin.Blue.start(DEVICE_SETTINGS)
|
||
}
|
||
|
||
/**
|
||
* 主动连接指定设备
|
||
* BleAdv 类型已在 devicesList 中自动处理,无需调用此方法
|
||
* BleConnect 类型需要用户点击后调用
|
||
*/
|
||
connect(device: ScannedDevice): void {
|
||
const connectType = this._p.PPBluetoothDefine.PPDeviceConnectType
|
||
if (device.model?.deviceConnectType === connectType.PPDeviceConnectTypeBleConnect) {
|
||
this._p.Blue.createBLEConnection(device.raw)
|
||
}
|
||
}
|
||
|
||
/** 主动断开当前连接 */
|
||
disconnect(): void {
|
||
this._stopKeepAlive()
|
||
this._p.Blue.disconnect()
|
||
}
|
||
|
||
/**
|
||
* 停止扫描,清理所有 bus 订阅与回调
|
||
* 页面 onUnload / onHide 时调用
|
||
*/
|
||
stopScan(): void {
|
||
this._stopKeepAlive()
|
||
this._stopReconnectTimer()
|
||
this._p.Blue.stop()
|
||
this._activeProtocol = null
|
||
this._deviceInfo = null
|
||
this._onDevicesListCb = null
|
||
this._onConnectStateCb = null
|
||
this._onProgressCb = null
|
||
this._onLockedCb = null
|
||
this._onDisconnectedCb = null
|
||
}
|
||
|
||
// ─── WiFi 配网 ────────────────────────────────
|
||
|
||
/**
|
||
* 获取设备上报的 WiFi 列表(需设备已连接)
|
||
* 内部延迟 2 秒后调用,15 秒超时
|
||
*/
|
||
getWifiList(): Promise<LeFuWifiItem[]> {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this._activeProtocol) {
|
||
reject(new Error('[LeFu] 设备未连接,无法获取 WiFi 列表'))
|
||
return
|
||
}
|
||
|
||
const timer = setTimeout(() => {
|
||
reject(new Error('[LeFu] 获取 WiFi 列表超时'))
|
||
}, 15000)
|
||
|
||
// 延迟 2 秒,等待设备就绪后再请求
|
||
setTimeout(() => {
|
||
this._activeProtocol.dataFindSurroundDevice((res: any[]) => {
|
||
clearTimeout(timer)
|
||
if (!res || res.length === 0) {
|
||
this._activeProtocol.dataExitWifiConfig(() => {})
|
||
resolve([])
|
||
return
|
||
}
|
||
const list: LeFuWifiItem[] = res.map((item: any) => ({
|
||
ssid: item.ssid || item.name || '',
|
||
signal: item.signal,
|
||
}))
|
||
resolve(list)
|
||
})
|
||
}, 2000)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 将 WiFi 配置写入设备
|
||
* @param ssid WiFi 名称
|
||
* @param password WiFi 密码
|
||
* @param version 协议版本:domain1(无鉴权)| domain2(有鉴权)
|
||
* 返回码 23 = 配网成功
|
||
*/
|
||
configWifi(ssid: string, password: string, version: WifiVersion): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this._activeProtocol) {
|
||
reject(new Error('[LeFu] 设备未连接,无法配网'))
|
||
return
|
||
}
|
||
|
||
const callback = (res: number) => {
|
||
if (res === 23) {
|
||
resolve()
|
||
} else {
|
||
reject(new Error(`[LeFu] 配网失败,错误码:${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,
|
||
)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 查询设备是否已完成配网
|
||
* 返回 true = 已配网(0x01),false = 未配网
|
||
*/
|
||
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 升级 ─────────────────────────────────
|
||
|
||
/**
|
||
* 检查固件版本并按需触发 OTA 升级
|
||
* @returns true = 已触发升级,false = 无需升级
|
||
* 调用时机由业务层决定,不自动触发
|
||
*/
|
||
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('.')
|
||
|
||
// 设备单段版本(如 "006")vs 服务端 x.y.z 格式(如 "0.0.6")比对
|
||
const isServerNewer = (deviceVer: string, serverVer: string): boolean => {
|
||
const deviceNum = parseInt(deviceVer, 10)
|
||
const serverNum = (serverVer || '').split('.').map(v => parseInt(v, 10)).reduce((acc, v) => acc * 1000 + v, 0)
|
||
return serverNum > deviceNum
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// status: false = 成功,true = 失败
|
||
this._activeProtocol.codeOtaUpdate((status: boolean) => {
|
||
if (!status) {
|
||
resolve(true)
|
||
} else {
|
||
reject(new Error('[LeFu] OTA 升级失败'))
|
||
}
|
||
})
|
||
})
|
||
.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
|
||
}
|
||
|
||
/** 自动重连,最多 _maxReconnect 次,超限后等待用户手动重试 */
|
||
private _doReconnect(): void {
|
||
if (this._reconnectCount >= this._maxReconnect) return
|
||
this._reconnectCount++
|
||
this._stopReconnectTimer()
|
||
this._reconnectTimer = setTimeout(() => {
|
||
this._plugin?.Blue?.disconnect((res: any) => {
|
||
if (res?.errCode === 0) {
|
||
this._plugin?.Blue?.startBluetoothDevicesDiscovery()
|
||
}
|
||
})
|
||
}, 2000)
|
||
}
|
||
|
||
// ─── 成员同步 ─────────────────────────────────
|
||
|
||
/**
|
||
* 全量将成员列表同步到设备
|
||
* 流程:清除设备成员数据 → 逐个下发(顺序执行)
|
||
* 每步 15 秒超时保护,失败时自动重试当前成员
|
||
* @param members 成员列表,必须包含且仅包含一个 isSelf=true 的主用户
|
||
*/
|
||
syncMembersToDevice(members: DeviceMember[]): 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] 成员列表中缺少主用户(isSelf=true)'))
|
||
return
|
||
}
|
||
|
||
// 第一步:清除设备上的所有成员数据("01" 仅清成员,保留历史)
|
||
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]
|
||
resetTimer()
|
||
|
||
this._activeProtocol.dataSyncUserInfo(
|
||
{
|
||
userID: mainUser.id,
|
||
userName: member.name,
|
||
// 主用户 memberID 为空,子用户用自己的 id
|
||
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++
|
||
}
|
||
// 成功进下一个,失败重试当前(index 不变)
|
||
syncNext()
|
||
},
|
||
)
|
||
}
|
||
|
||
syncNext()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
/** 全局单例,整个小程序共享同一个蓝牙服务实例 */
|
||
export const lefuService = new LeFuService()
|