/** * 设备详情页 - 实时数据 */ const ble = require('../../../services/ble.js'); const mqtt = require('../../../services/mqtt-client.js'); const storage = require('../../../utils/storage.js'); const auth = require('../../../utils/auth.js'); const app = getApp(); Page({ data: { device: null, bleConnected: false, mqttConnected: false, connecting: false, mqttConnecting: false, // 数据列表 dataList: [], maxDataCount: 50, // 统计 stats: { received: 0, uploaded: 0, failed: 0, }, }, onLoad(options) { const sn = options.sn; if (!sn) { wx.showToast({ title: '设备信息缺失', icon: 'none' }); setTimeout(() => wx.navigateBack(), 500); return; } // 从全局或存储获取设备信息 let device = app.globalData.currentDevice; if (!device || device.serialNumber !== sn) { device = storage.getDevice(sn); } if (!device) { wx.showToast({ title: '设备不存在', icon: 'none' }); setTimeout(() => wx.navigateBack(), 500); return; } this.setData({ device }); // 监听 BLE 状态变化 ble.bleService.onStateChange((state) => { const connected = state === ble.BLE_STATE.READY || state === ble.BLE_STATE.CONNECTED; this.setData({ bleConnected: connected }); }); // 监听 BLE 数据 ble.bleService.onData((data) => { this._handleBleData(data); }); // 监听 MQTT 状态变化 mqtt.mqttClient.onStateChange((state) => { const connected = state === mqtt.MQTT_STATE.CONNECTED; this.setData({ mqttConnected: connected }); }); }, onUnload() { // 清除回调,防止页面销毁后 setData 报错 ble.bleService.clearCallbacks(); mqtt.mqttClient.clearCallbacks(); // 断开连接 ble.bleService.disconnect(); mqtt.mqttClient.disconnect(); }, /** * 处理 BLE 数据 */ _handleBleData(data) { const now = new Date(); const time = this._formatTime(now); // 使用唯一 ID 标识数据项(防止重复数据更新错乱) const dataItem = { id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, hex: data.hex, time: time, uploaded: false, }; let dataList = this.data.dataList; dataList.unshift(dataItem); // 限制数量 if (dataList.length > this.data.maxDataCount) { dataList = dataList.slice(0, this.data.maxDataCount); } this.setData({ dataList, 'stats.received': this.data.stats.received + 1, }); // 如果 MQTT 已连接且设备已启用,自动上报 if (this.data.mqttConnected && this.data.device.enabled) { this._uploadData(data.hex, dataItem.id); } else { // MQTT 未连接,缓存数据(离线缓存) storage.cachePendingData({ deviceId: this.data.device.serialNumber, productId: this.data.device.productId, hex: data.hex, dataId: dataItem.id, }); } }, /** * 上报数据到 yack * @param {string} hexData - 十六进制数据 * @param {string} [dataId] - 数据项 ID(用于更新列表状态) * @throws 上报失败时抛出异常 */ async _uploadData(hexData, dataId) { const device = this.data.device; if (!device) return; const topic = auth.buildPropertyPostTopic(device.productId, device.serialNumber); try { await mqtt.mqttClient.publish(topic, hexData); console.log('[Detail] 上报成功:', hexData); // 更新状态 this.setData({ 'stats.uploaded': this.data.stats.uploaded + 1, }); // 更新 dataList 中的状态(按唯一 ID 查找) if (dataId) { const dataList = this.data.dataList; const index = dataList.findIndex(d => d.id === dataId); if (index >= 0) { this.setData({ [`dataList[${index}].uploaded`]: true, }); } } } catch (err) { console.error('[Detail] 上报失败:', err); this.setData({ 'stats.failed': this.data.stats.failed + 1, }); // 抛出异常,供调用方统计失败 throw err; } }, /** * 连接/断开 BLE */ async handleConnect() { const { device, bleConnected } = this.data; if (!device) return; if (bleConnected) { // 断开连接 await ble.bleService.disconnect(); this.setData({ bleConnected: false }); return; } this.setData({ connecting: true }); try { // 初始化蓝牙 await ble.bleService.initBluetooth(); // 连接设备 await ble.bleService.connectDevice(device.deviceId, device.serviceUUID); this.setData({ bleConnected: true }); wx.showToast({ title: 'BLE 已连接', icon: 'success' }); } catch (err) { console.error('[Detail] BLE 连接失败:', err); wx.showToast({ title: err.message || 'BLE 连接失败', icon: 'none', }); } finally { this.setData({ connecting: false }); } }, /** * 连接/断开 MQTT */ async handleMqttConnect() { const { device, mqttConnected } = this.data; if (!device) return; if (mqttConnected) { // 断开连接 await mqtt.mqttClient.disconnect(); this.setData({ mqttConnected: false }); return; } this.setData({ mqttConnecting: true }); try { // 获取 MQTT 凭证 const credentials = auth.getMqttCredentialsFromDevice(device); if (!credentials) { throw new Error('获取 MQTT 凭证失败'); } // 连接 MQTT await mqtt.mqttClient.connect(credentials); this.setData({ mqttConnected: true }); wx.showToast({ title: 'MQTT 已连接', icon: 'success' }); // 上传缓存的离线数据 this._uploadPendingData(); } catch (err) { console.error('[Detail] MQTT 连接失败:', err); wx.showToast({ title: err.message || 'MQTT 连接失败', icon: 'none', }); } finally { this.setData({ mqttConnecting: false }); } }, /** * 上传缓存的离线数据 */ async _uploadPendingData() { const pendingList = storage.getPendingData(); if (pendingList.length === 0) return; console.log('[Detail] 开始上传离线缓存数据:', pendingList.length, '条'); const device = this.data.device; let uploaded = 0; let failed = 0; for (const item of pendingList) { // 只上传当前设备的数据 if (item.deviceId === device.serialNumber) { try { const topic = auth.buildPropertyPostTopic(device.productId, device.serialNumber); await mqtt.mqttClient.publish(topic, item.hex); uploaded++; } catch (err) { console.error('[Detail] 离线数据上传失败:', item.hex, err); failed++; } } } // 清除已处理的缓存数据 storage.clearPendingData(); if (uploaded > 0 || failed > 0) { console.log('[Detail] 离线数据上传完成:', uploaded, '成功,', failed, '失败'); } }, /** * 上报单条数据 */ async handleUploadSingle(e) { const index = e.currentTarget.dataset.index; const data = this.data.dataList[index]; if (!data || data.uploaded) return; if (!this.data.mqttConnected) { wx.showToast({ title: 'MQTT 未连接', icon: 'none' }); return; } try { await this._uploadData(data.hex, data.id); } catch (err) { wx.showToast({ title: err.message || '上报失败', icon: 'none' }); } }, /** * 全部上报 */ async handleUploadAll() { const { dataList, mqttConnected } = this.data; if (!mqttConnected || dataList.length === 0) return; wx.showLoading({ title: '上报中...', mask: true }); let uploaded = 0; let failed = 0; for (const data of dataList) { if (!data.uploaded) { try { await this._uploadData(data.hex, data.id); uploaded++; } catch (err) { failed++; } } } wx.hideLoading(); wx.showToast({ title: `上报完成: ${uploaded} 成功, ${failed} 失败`, icon: 'none', }); }, /** * 清空数据 */ handleClearData() { wx.showModal({ title: '清空数据', content: '确定清空所有接收到的数据吗?', success: (res) => { if (res.confirm) { this.setData({ dataList: [], stats: { received: 0, uploaded: 0, failed: 0 }, }); } }, }); }, /** * 格式化时间 */ _formatTime(date) { const h = date.getHours().toString().padStart(2, '0'); const m = date.getMinutes().toString().padStart(2, '0'); const s = date.getSeconds().toString().padStart(2, '0'); return `${h}:${m}:${s}`; }, });