Files
bodyWeight/miniprogram/lefu/service.ts
T

455 lines
16 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 type {
RawDevice,
LeFuPlugin,
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
WifiVersion,
LeFuConfig,
DeviceSetting,
DeviceMember,
} from './types'
/** 乐福 SDK 配置 */
const LEFU_CONFIG: LeFuConfig = {
key: 'lefudc91611833e18d94',
secret: 'eQ50JYimMp0X7uhNLj6D3lTEk4TUUvEG7dvaqKM9t1U=',
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
/** 回调注册(页面通过 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)
})
// 协议对象(持有 activeProtocolWiFi 配网等高级操作依赖它)
plugin.bus.subscribe('devicesModel', (res: any) => {
this._activeProtocol = 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._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)
})
})
}
// ─── 私有:保活与重连 ─────────────────────────
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()