feat(lefu): 迁移乐福体脂秤 SDK,接入设备扫描连接与 WiFi 配网

- 新增 lefu 服务层(扫描/连接/断开/保活重连/测量回调/WiFi 配网),事件回调支持多监听
- connectedDevice 接入真实蓝牙扫描连接,修复断线重连 UI 未同步、蓝牙/定位开关误判
- connectedWifi 接入真实 WiFi 配网,补连接状态校验与退出配网模式
- 新增 5 张设备图标并调整应用入口

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
17792275749
2026-08-20 15:57:16 +08:00
co-authored by Claude Opus 4.7
parent feb494080c
commit f53f11e6e9
14 changed files with 901 additions and 71 deletions
+1 -1
View File
@@ -1,8 +1,8 @@
{
"pages": [
"pages/connectedDevice/connectedDevice",
"pages/login/login",
"pages/supplementPersonal/supplementPersonal",
"pages/connectedDevice/connectedDevice",
"pages/connectedWifi/connectedWifi",
"pages/home/home",
"pages/detailedReport/detailedReport",
+9 -1
View File
@@ -1,7 +1,15 @@
App<IAppOption>({
globalData: {},
onLaunch() {},
onLaunch() {
// 已登录则跳过登录页
const userInfo = wx.getStorageSync('userInfo') || null;
if (userInfo) {
wx.reLaunch({
url: '/pages/home/home'
})
}
},
getWxLoginCode(): Promise<string> {
return new Promise((resolve, reject) => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+11
View File
@@ -0,0 +1,11 @@
import type { LeFuConfig } from './types'
/** 乐福 SDK 配置 */
export 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',
}
+45
View File
@@ -0,0 +1,45 @@
import type { DeviceSetting } from './types'
/** 支持的设备型号配置列表(5款) */
export 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,
},
]
+10
View File
@@ -0,0 +1,10 @@
export { leFuService } from './service'
export type {
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
LeFuConfig,
DeviceSetting,
DeviceInfo,
} from './types'
+410
View File
@@ -0,0 +1,410 @@
import { LEFU_CONFIG } from './config'
import { DEVICE_SETTINGS } from './deviceSettings'
import type {
RawDevice,
LeFuPlugin,
ScannedDevice,
LeFuWifiItem,
ProgressData,
LockData,
LeFuConfig,
DeviceInfo,
} from './types'
/** 顶层加载插件(对齐 demorequirePlugin 必须在文件顶层调用) */
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<string, string> { 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<LeFuWifiItem[]> {
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<void> {
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<boolean> {
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()
+90
View File
@@ -0,0 +1,90 @@
/** ppScale-plugin 原始设备对象 */
export type RawDevice = Record<string, any>
/** 乐福 SDK 插件对象(运行时由 requirePlugin 注入) */
export type LeFuPlugin = Record<string, any>
/** 设备配置(来自 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
}
/** 设备固件信息(deviceInfo 事件返回) */
export interface DeviceInfo {
/** 固件版本号,格式:mcu.ble.wifi.res,如 "006.005.004.305" */
firmwareRevision: string
/** 设备型号,用于请求服务端版本接口 */
modelNumber: string
/** 设备序列号 */
serialNumber: string
[key: string]: any
}
@@ -1,21 +1,30 @@
/** 展示用设备项 */
import { leFuService } from '../../lefu/index'
import type { ScannedDevice } from '../../lefu/index'
import type { RawDevice } from '../../lefu/types'
/** 渲染用的设备项(RawDevice 含复杂对象,不进 setData */
interface DeviceItem {
name: string
connected: boolean
}
/** 页面状态 */
type DeviceStatus = 'idle' | 'searching' | 'found' | 'empty'
/** 假设备数据 */
const MOCK_DEVICES: DeviceItem[] = [
{ name: '乐福体脂秤 Pro', connected: false },
{ name: '乐福体脂秤 Air', connected: false },
{ name: '乐福体脂秤 Mini', connected: false },
]
/** 扫描定时器 */
let _searchTimer: number | null = null
/** 扫描到的原始设备缓存 */
let scannedDevices: ScannedDevice[] = []
/** 扫描 / 断开回调的取消订阅句柄 */
let unsubDevicesList: (() => void) | null = null
let unsubDisconnected: (() => void) | null = null
let unsubDeviceConnect: (() => void) | null = null
/** 连接流程的清理句柄 */
let connectCleanup: {
timer: number | null
unsubState: (() => void) | null
unsubConnect: (() => void) | null
} | null = null
/** 一次性防重入标记 */
let modalLocked = false
let autoConnectTried = false
Page({
data: {
@@ -23,34 +32,33 @@ Page({
deviceList: [] as DeviceItem[],
},
onHide() {
this._clearSearchTimer()
},
onUnload() {
this._clearSearchTimer()
},
_clearSearchTimer() {
if (_searchTimer !== null) {
clearTimeout(_searchTimer)
_searchTimer = null
onShow() {
// 从其他页面返回时同步连接状态
if (!leFuService.isConnected) {
this._markAllDisconnected()
}
},
/** 点击「开始搜索 / 重新搜索」:模拟扫描 */
onStartSearch() {
this._clearSearchTimer()
this.setData({ status: 'searching', deviceList: [] })
_searchTimer = setTimeout(() => {
this.setData({
status: 'found',
deviceList: MOCK_DEVICES.map(item => ({ ...item })),
})
}, 1500)
onHide() {
leFuService.stopScan()
},
/** 点击整张卡片:已连接 → 跳转配网页;未连接 → 走连接流程 */
onUnload() {
leFuService.stopScan()
this._clearSubscriptions()
this._clearConnectCleanup()
},
/** 点击「开始 / 重新搜索」 */
async onStartSearch() {
if (modalLocked) return
scannedDevices = []
autoConnectTried = false
this.setData({ status: 'searching', deviceList: [] })
await this._requestPermissionAndScan()
},
/** 点击设备卡片:已连接跳配网,未连接走连接流程 */
onTapCard(e: WechatMiniprogram.TouchEvent) {
const connected = e.currentTarget.dataset.connected as boolean
if (connected) {
@@ -60,24 +68,220 @@ Page({
this.onConnect(e)
},
/** 模拟连接指定设备 */
/** 手动连接指定设备 */
onConnect(e: WechatMiniprogram.TouchEvent) {
const index = e.currentTarget.dataset.index as number
const list = this.data.deviceList.map((item, i) => ({
...item,
connected: i === index,
}))
this.setData({ deviceList: list })
this._connectByIndex(index, false)
},
/** 断开当前连接 */
onDisconnect() {
const list = this.data.deviceList.map(item => ({ ...item, connected: false }))
this.setData({ deviceList: list })
this._markAllDisconnected()
leFuService.disconnect()
},
/** 查看帮助 */
onOpenHelp() {
wx.showToast({ title: '请确保设备已开机且在附近', icon: 'none' })
},
/** 将所有卡片置为未连接 */
_markAllDisconnected() {
this.setData({
deviceList: this.data.deviceList.map(item => ({ ...item, connected: false })),
})
},
/** 连接成功后把对应设备卡片标记为已连接(覆盖手动连接与自动重连) */
_markConnected() {
const cached = wx.getStorageSync('connectDeviceInfo') as Record<string, any> | null
const name = leFuService.deviceInfo?.modelNumber
|| cached?.scaleDeviceName
|| cached?.name
|| cached?.deviceName
if (!name) return
this.setData({
deviceList: this.data.deviceList.map(item => ({ ...item, connected: item.name === name })),
})
},
_clearSubscriptions() {
unsubDevicesList?.()
unsubDisconnected?.()
unsubDeviceConnect?.()
unsubDevicesList = null
unsubDisconnected = null
unsubDeviceConnect = null
},
_clearConnectCleanup() {
if (!connectCleanup) return
if (connectCleanup.timer !== null) clearTimeout(connectCleanup.timer)
connectCleanup.unsubState?.()
connectCleanup.unsubConnect?.()
connectCleanup = null
},
/** 申请位置权限后打开蓝牙适配器(蓝牙权限由 openBluetoothAdapter 自动触发) */
async _requestPermissionAndScan() {
try {
const { authSetting } = await wx.getSetting()
const setting = authSetting as Record<string, boolean | undefined>
if (!setting['scope.userLocation']) {
await wx.authorize({ scope: 'scope.userLocation' })
}
} catch {
this._showLocationGuide()
return
}
await this._openAdapterAndScan()
},
async _openAdapterAndScan() {
try {
await wx.openBluetoothAdapter()
} catch (err: any) {
console.error('[connectedDevice] openBluetoothAdapter 失败:', err)
this.setData({ status: 'idle' })
this._handleAdapterError()
return
}
this._startScan()
},
/** 区分蓝牙/定位服务开关,给出准确提示 */
_handleAdapterError() {
try {
const setting = wx.getSystemSetting()
if (!setting.bluetoothEnabled) {
this._showBluetoothOff()
return
}
if (!setting.locationEnabled) {
this._showLocationOff()
return
}
} catch {
// 基础库不支持 wx.getSystemSetting 时,退化为通用提示
}
wx.showToast({ title: '蓝牙初始化失败,请重试', icon: 'none' })
},
_startScan() {
this._clearSubscriptions()
unsubDevicesList = leFuService.onDevicesList((devices) => {
scannedDevices = devices
if (!devices.length) {
this.setData({ status: 'empty', deviceList: [] })
return
}
const connectedName = this.data.deviceList.find(d => d.connected)?.name
this.setData({
status: 'found',
deviceList: devices.map(d => ({ name: d.name, connected: d.name === connectedName })),
})
this._tryAutoConnect()
})
unsubDisconnected = leFuService.onDisconnected(() => {
this._markAllDisconnected()
})
unsubDeviceConnect = leFuService.onDeviceConnect(() => {
this._markConnected()
})
leFuService.startScan()
},
/** 命中缓存设备时自动连接一次 */
_tryAutoConnect() {
if (autoConnectTried) return
const cached = wx.getStorageSync('connectDeviceInfo') as RawDevice | null
if (!cached?.deviceId) return
const index = scannedDevices.findIndex(d => d.raw.deviceId === cached.deviceId)
if (index < 0) return
autoConnectTried = true
this._connectByIndex(index, true)
},
/** 统一连接流程(手动 / 自动共用),手动连接成功后跳配网 */
_connectByIndex(index: number, isAuto: boolean) {
const scanned = scannedDevices[index]
if (!scanned) return
wx.showLoading({ title: '连接中...', mask: true })
leFuService.stopScan()
leFuService.connect(scanned.raw)
this._clearConnectCleanup()
connectCleanup = { timer: null, unsubState: null, unsubConnect: null }
const finish = () => {
if (connectCleanup?.timer !== null) clearTimeout(connectCleanup.timer)
connectCleanup?.unsubState?.()
connectCleanup?.unsubConnect?.()
connectCleanup = null
wx.hideLoading()
}
connectCleanup.timer = setTimeout(() => {
finish()
wx.showToast({ title: '连接超时', icon: 'none' })
}, 15000)
connectCleanup.unsubState = leFuService.onConnectState((state) => {
if (state === leFuService.BLUE_STATE.CONNECTFAILED) {
finish()
wx.showToast({ title: '连接失败,请重试', icon: 'none' })
}
})
connectCleanup.unsubConnect = leFuService.onDeviceConnect(() => {
finish()
wx.setStorageSync('connectDeviceInfo', { ...scanned.raw, scaleDeviceName: scanned.raw.name })
if (!isAuto) {
wx.navigateTo({ url: '/pages/connectedWifi/connectedWifi' })
}
})
},
_showBluetoothOff() {
wx.showModal({
title: '蓝牙未开启',
content: '请先开启手机蓝牙后重试',
showCancel: false,
confirmText: '知道了',
success: () => this.setData({ status: 'idle' }),
})
},
_showLocationOff() {
wx.showModal({
title: '定位服务未开启',
content: '安卓手机搜索蓝牙设备需开启定位服务,请前往系统设置开启后重试。',
showCancel: false,
confirmText: '知道了',
})
},
_showLocationGuide() {
modalLocked = true
wx.showModal({
title: '需要授权',
content: '小程序需要位置权限才能搜索蓝牙设备,请前往设置开启。',
confirmText: '去开启',
cancelText: '取消',
success: (res) => {
modalLocked = false
this.setData({ status: 'idle' })
if (res.confirm) {
wx.openSetting({
success: () => this.onStartSearch(),
fail: () => this.setData({ status: 'idle' }),
})
}
},
fail: () => {
modalLocked = false
this.setData({ status: 'idle' })
},
})
},
})
@@ -1,3 +1,10 @@
import { leFuService } from '../../lefu/index'
/** 连接状态事件的取消订阅句柄 */
let unsubDisconnected: (() => void) | null = null
let unsubDeviceConnect: (() => void) | null = null
/** 渲染用的 WiFi 项(在 SSID 基础上追加连接状态) */
interface WifiItem {
ssid: string
connected: boolean
@@ -6,17 +13,6 @@ interface WifiItem {
type WifiStatus = 'idle' | 'searching' | 'list' | 'empty' | 'password' | 'configuring'
type BleStatus = 'connected' | 'failed' | 'connecting'
/** 假 WiFi 列表 */
const MOCK_WIFI: WifiItem[] = [
{ ssid: 'TP-LINK_5G_8A3C', connected: false },
{ ssid: 'Home_WiFi', connected: false },
{ ssid: 'Xiaomi_1234', connected: false },
]
/** 搜索 / 配网定时器 */
let _searchTimer: number | null = null
let _configTimer: number | null = null
Page({
data: {
status: 'idle' as WifiStatus,
@@ -30,31 +26,74 @@ Page({
},
onLoad() {
this._initDeviceInfo()
this._subscribeDisconnect()
this.onStartSearch()
},
onUnload() {
wx.hideLoading()
this._clearTimers()
unsubDisconnected?.()
unsubDeviceConnect?.()
unsubDisconnected = null
unsubDeviceConnect = null
leFuService.exitWifiConfig()
},
_clearTimers() {
if (_searchTimer !== null) { clearTimeout(_searchTimer); _searchTimer = null }
if (_configTimer !== null) { clearTimeout(_configTimer); _configTimer = null }
/** 订阅连接状态事件:断开 / 重连时同步状态到 UI */
_subscribeDisconnect() {
unsubDisconnected?.()
unsubDeviceConnect?.()
unsubDisconnected = leFuService.onDisconnected(() => {
this.setData({ bleStatus: 'failed' })
})
unsubDeviceConnect = leFuService.onDeviceConnect(() => {
this.setData({ bleStatus: 'connected' })
})
},
/** 模拟获取 WiFi 列表 */
onStartSearch() {
if (_searchTimer !== null) { clearTimeout(_searchTimer); _searchTimer = null }
/** 初始化设备信息:读取缓存设备名 + 同步蓝牙连接状态 */
_initDeviceInfo() {
const cached = wx.getStorageSync('connectDeviceInfo') as Record<string, any> | null
const deviceName = cached?.scaleDeviceName || cached?.name
if (deviceName) {
this.setData({ deviceName })
}
this.setData({ bleStatus: leFuService.isConnected ? 'connected' : 'failed' })
},
/** 校验设备连接状态:未连接则同步状态 + 提示,返回是否可继续操作 */
_ensureConnected(): boolean {
if (leFuService.isConnected) return true
this.setData({ bleStatus: 'failed' })
wx.showToast({ title: '设备已断开,请重新连接', icon: 'none' })
return false
},
/** 通过设备协议获取周围 WiFi 列表 */
async onStartSearch() {
if (!this._ensureConnected()) {
this.setData({ status: 'idle', wifiList: [] })
return
}
this.setData({ status: 'searching', wifiList: [] })
wx.showLoading({ title: '正在获取 WiFi 列表...', mask: true })
_searchTimer = setTimeout(() => {
try {
const list = await leFuService.getWifiList()
wx.hideLoading()
if (!list.length) {
this.setData({ status: 'empty', wifiList: [] })
return
}
this.setData({
status: 'list',
wifiList: MOCK_WIFI.map(item => ({ ...item })),
wifiList: list.map(item => ({ ssid: item.ssid, connected: false })),
})
}, 1200)
} catch {
wx.hideLoading()
this.setData({ status: 'empty', wifiList: [] })
wx.showToast({ title: '获取 WiFi 列表失败', icon: 'none' })
}
},
onRefresh() {
@@ -62,6 +101,7 @@ Page({
},
onTapWifi(e: WechatMiniprogram.TouchEvent) {
if (!this._ensureConnected()) return
const ssid = e.currentTarget.dataset.ssid as string
const target = this.data.wifiList.find(item => item.ssid === ssid)
if (!target || target.connected) return
@@ -80,13 +120,15 @@ Page({
this.setData({ status: 'list', selectedSsid: '', selectWifiPWD: '' })
},
/** 模拟配网 */
onConfirmPwd() {
/** 下发配网指令,成功后标记对应 WiFi 已连接 */
async onConfirmPwd() {
if (!this._ensureConnected()) return
const { selectedSsid, selectWifiPWD } = this.data
if (!selectedSsid) return
this.setData({ status: 'configuring' })
wx.showLoading({ title: '正在配网...', mask: true })
_configTimer = setTimeout(() => {
try {
await leFuService.configWifi(selectedSsid, selectWifiPWD)
wx.hideLoading()
this.setData({
status: 'list',
@@ -95,11 +137,21 @@ Page({
selectedSsid: '',
selectWifiPWD: '',
})
}, 1500)
wx.showToast({ title: '配网成功', icon: 'success' })
} catch (err: any) {
wx.hideLoading()
this.setData({ selectedSsid: '', selectWifiPWD: '' })
wx.showToast({ title: err?.message || '配网失败', icon: 'none' })
// 配网失败:延时 1.5s 后清空数据并重新获取 WiFi 列表
setTimeout(() => {
this.onStartSearch()
}, 1500)
}
},
onConfirm() {
if (!this.data.hasConnected) return
wx.navigateTo({ url: '/pages/supplementPersonal/supplementPersonal' })
if (!this._ensureConnected()) return
wx.navigateTo({ url: '/pages/equipment/equipment' })
},
})