diff --git a/miniprogram/app.json b/miniprogram/app.json index 99c2c69..e1b8129 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -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", diff --git a/miniprogram/app.ts b/miniprogram/app.ts index 5c89327..ce0dc32 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -1,7 +1,15 @@ App({ globalData: {}, - onLaunch() {}, + onLaunch() { + // 已登录则跳过登录页 + const userInfo = wx.getStorageSync('userInfo') || null; + if (userInfo) { + wx.reLaunch({ + url: '/pages/home/home' + }) + } + }, getWxLoginCode(): Promise { return new Promise((resolve, reject) => { diff --git a/miniprogram/images/device/CF568_G.png b/miniprogram/images/device/CF568_G.png new file mode 100644 index 0000000..0af20bb Binary files /dev/null and b/miniprogram/images/device/CF568_G.png differ diff --git a/miniprogram/images/device/CF577.png b/miniprogram/images/device/CF577.png new file mode 100644 index 0000000..128435b Binary files /dev/null and b/miniprogram/images/device/CF577.png differ diff --git a/miniprogram/images/device/CF636_G.png b/miniprogram/images/device/CF636_G.png new file mode 100644 index 0000000..f77d79f Binary files /dev/null and b/miniprogram/images/device/CF636_G.png differ diff --git a/miniprogram/images/device/YX-B4-568-BW.png b/miniprogram/images/device/YX-B4-568-BW.png new file mode 100644 index 0000000..0af20bb Binary files /dev/null and b/miniprogram/images/device/YX-B4-568-BW.png differ diff --git a/miniprogram/images/device/YX-B8-636-BW.png b/miniprogram/images/device/YX-B8-636-BW.png new file mode 100644 index 0000000..f77d79f Binary files /dev/null and b/miniprogram/images/device/YX-B8-636-BW.png differ diff --git a/miniprogram/lefu/config.ts b/miniprogram/lefu/config.ts new file mode 100644 index 0000000..e860741 --- /dev/null +++ b/miniprogram/lefu/config.ts @@ -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', +} diff --git a/miniprogram/lefu/deviceSettings.ts b/miniprogram/lefu/deviceSettings.ts new file mode 100644 index 0000000..c742f62 --- /dev/null +++ b/miniprogram/lefu/deviceSettings.ts @@ -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, + }, +] diff --git a/miniprogram/lefu/index.ts b/miniprogram/lefu/index.ts new file mode 100644 index 0000000..86df503 --- /dev/null +++ b/miniprogram/lefu/index.ts @@ -0,0 +1,10 @@ +export { leFuService } from './service' +export type { + ScannedDevice, + LeFuWifiItem, + ProgressData, + LockData, + LeFuConfig, + DeviceSetting, + DeviceInfo, +} from './types' diff --git a/miniprogram/lefu/service.ts b/miniprogram/lefu/service.ts new file mode 100644 index 0000000..10428be --- /dev/null +++ b/miniprogram/lefu/service.ts @@ -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' + +/** 顶层加载插件(对齐 demo,requirePlugin 必须在文件顶层调用) */ +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 { 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 { + 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 { + 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 { + 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() diff --git a/miniprogram/lefu/types.ts b/miniprogram/lefu/types.ts new file mode 100644 index 0000000..7f0caf5 --- /dev/null +++ b/miniprogram/lefu/types.ts @@ -0,0 +1,90 @@ +/** ppScale-plugin 原始设备对象 */ +export type RawDevice = Record + +/** 乐福 SDK 插件对象(运行时由 requirePlugin 注入) */ +export type LeFuPlugin = Record + + +/** 设备配置(来自 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 +} + +/** 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 +} diff --git a/miniprogram/pages/connectedDevice/connectedDevice.ts b/miniprogram/pages/connectedDevice/connectedDevice.ts index 0eae258..90112d3 100644 --- a/miniprogram/pages/connectedDevice/connectedDevice.ts +++ b/miniprogram/pages/connectedDevice/connectedDevice.ts @@ -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 | 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 + 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' }) + }, + }) + }, }) diff --git a/miniprogram/pages/connectedWifi/connectedWifi.ts b/miniprogram/pages/connectedWifi/connectedWifi.ts index c2299d3..a9e52d1 100644 --- a/miniprogram/pages/connectedWifi/connectedWifi.ts +++ b/miniprogram/pages/connectedWifi/connectedWifi.ts @@ -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 | 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' }) }, })