[AI Generated]: feat(*): 新增 lefu 蓝牙服务层、mock 业务数据层及 request 请求封装,接入各页面

This commit is contained in:
17792275749
2026-05-13 16:18:14 +08:00
parent 38d6f0867a
commit 3f12e34afd
18 changed files with 1222 additions and 540 deletions
+11
View File
@@ -0,0 +1,11 @@
export { lefuService } from './service'
export type {
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
WifiVersion,
LeFuConfig,
DeviceSetting,
DeviceMember,
} from './types'
+454
View File
@@ -0,0 +1,454 @@
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()
+97
View File
@@ -0,0 +1,97 @@
/** ppScale-plugin 原始设备对象 */
export type RawDevice = Record<string, any>
/** 乐福 SDK 插件对象(运行时由 requirePlugin 注入) */
export type LeFuPlugin = Record<string, any>
/** WiFi 配网协议版本 */
export type WifiVersion = 'domain1' | 'domain2'
/** 设备配置(来自 master device.setting 列表) */
export interface DeviceSetting {
advLength: number
calorieStatus: number
deviceAccuracyType: number
deviceCalcuteType: number
deviceConnectType: number
deviceFuncType: number
deviceName: string
devicePowerType: number
deviceProtocolType: number
deviceType: number
deviceUnitType: string
id: number
macAddressStart: number
sign: string
status: number
uhStatus: number
createBy: null
imgUrl: null
remark: null
updateBy: null
}
/** 扫描到的设备(展示层使用) */
export interface ScannedDevice {
/** 设备名称 */
name: string
/** plugin 返回的原始设备对象,连接时传回给 SDK */
raw: RawDevice
/** 设备模型(含 deviceConnectType 等信息) */
model: Record<string, any>
}
/** WiFi 项 */
export interface LeFuWifiItem {
/** WiFi SSID */
ssid: string
/** 信号强度(部分设备上报) */
signal?: number
}
/** 测量进度数据(progressData 事件,测量过程中多次回调) */
export interface ProgressData {
/** 体重,单位:克 */
weight: number
/** 单位枚举值 */
unit: number
/** 是否正在心率测量 */
isHeartRating: boolean
}
/** 测量锁定数据(lockData 事件,测量完成时回调一次) */
export interface LockData {
/** 体重,单位:克 */
weight: number
/** 阻抗值(用于体成分分析) */
resistance: number
/** 心率(部分设备上报) */
heartRate?: number
[key: string]: any
}
/** 乐福服务配置 */
export interface LeFuConfig {
key: string
secret: string
/** 老协议域名(无鉴权) */
domain1: string
/** 新协议域名(有鉴权) */
domain2: string
}
/** 下发给设备的成员信息(lefu 层使用,业务层转换后传入) */
export interface DeviceMember {
/** 成员唯一 ID */
id: string
/** 姓名 */
name: string
/** 性别:1=男,0=女 */
gender: 0 | 1
/** 年龄 */
age: number
/** 身高(cm) */
height: number
/** 是否本人(主用户) */
isSelf: boolean
}