/** * 扫描设备页面 * * 新测点链路下此处只负责「连接蓝牙设备」: * 扫描 → 点击设备 → 询问设备名称(选填)→ 登记保存 → 交由常驻网关 gateway 接管 BLE 连接。 * 设备不再需要配置弹窗(产品/MQTT 凭证统一在“我的”页配置), * 设备编号自动取自 BLE 广播名(无则退化为 BLE 地址归一化), * 设备名称仅作本地展示标签(可选填,留空沿用上次或退化为广播名/编号),不参与上报身份。 */ const ble = require('../../../services/ble.js'); const storage = require('../../../utils/storage.js'); const { gateway } = require('../../../services/gateway.js'); Page({ data: { scanning: false, connecting: false, scannedDevices: [], statusText: '准备扫描', statusHint: '点击下方按钮开始扫描 BLE 设备', }, onLoad() { // 暂停正在跑的网关,避免常驻会话与扫描抢占同一 BLE 目标; // 返回首页后 index.onShow → gateway.ensureStarted() 会自动恢复。 gateway.stop().then(() => this._initBluetooth()); }, onUnload() { // 停止扫描 if (this.data.scanning) { ble.bleService.stopScan(); } }, /** * 初始化蓝牙 */ async _initBluetooth() { try { await ble.bleService.initBluetooth(); this.setData({ statusText: '蓝牙已就绪', statusHint: '点击下方按钮开始扫描 BLE 设备', }); } catch (err) { console.error('[Scan] 初始化蓝牙失败:', err); this.setData({ statusText: '蓝牙初始化失败', statusHint: err.message || '请确保已开启蓝牙', }); wx.showModal({ title: '蓝牙错误', content: err.message || '请确保已开启蓝牙权限', showCancel: false, success: () => { wx.navigateBack(); }, }); } }, /** * 开始扫描 */ async handleScan() { if (this.data.scanning) return; this.setData({ scanning: true, scannedDevices: [], statusText: '正在扫描...', statusHint: '请确保设备已开机且在附近', }); try { const devices = await ble.bleService.scanDevices(); this.setData({ scanning: false, scannedDevices: devices, statusText: devices.length > 0 ? `发现 ${devices.length} 个设备` : '扫描完成', statusHint: devices.length > 0 ? '点击设备连接,名称可选填' : '未发现设备,请确保设备已开机', }); } catch (err) { console.error('[Scan] 扫描失败:', err); this.setData({ scanning: false, statusText: '扫描失败', statusHint: err.message || '请重试', }); } }, /** * 停止扫描 */ async handleStopScan() { await ble.bleService.stopScan(); this.setData({ scanning: false, statusText: '已停止扫描', }); }, /** * 点击设备 - 询问设备名称(选填)后登记并交由网关连接 * * 名称只是本地展示标签,serialNumber(上报身份)仍由 `_buildSerialNumber` 决定, * 所以留空也能直接连;这里用二次确认弹窗替代「点一下就连上」, * 便于顺手命名,也让误触有反悔余地。 */ async handleSelectDevice(e) { if (this.data.connecting) return; const index = e.currentTarget.dataset.index; const device = this.data.scannedDevices[index]; if (!device) return; const serialNumber = this._buildSerialNumber(device); if (!serialNumber) { wx.showToast({ title: '无法识别该设备', icon: 'none' }); return; } const existing = storage.getDevice(serialNumber); const inputName = await this._askDeviceName(device, serialNumber, existing); // 取消(或弹窗未能弹出)则不连接,否则用户没有退出路径 if (inputName === null) return; this.setData({ connecting: true }); wx.showLoading({ title: '连接中...', mask: true }); try { // 登记设备(同名按 serialNumber 覆盖刷新 BLE 地址与信号) const deviceConfig = { deviceId: device.deviceId, serialNumber: serialNumber, name: this._resolveDeviceName(inputName, existing, device.name, serialNumber), bleName: device.name, RSSI: device.RSSI, enabled: true, connected: true, createTime: Date.now(), }; storage.addDevice(deviceConfig); // 成为唯一启用设备(单 enabled 不变量) storage.activateDevice(serialNumber); const target = storage.getDevice(serialNumber); const hasMqtt = !!storage.getMqttConfig(); wx.hideLoading(); wx.showToast({ title: hasMqtt ? '已连接设备' : '已连接,请在"我的"配置MQTT', icon: 'none', duration: hasMqtt ? 1500 : 2500, }); // 由常驻网关接管 BLE 连接与采集 gateway.start(target || deviceConfig); wx.navigateBack(); } catch (err) { wx.hideLoading(); console.error('[Scan] 连接设备失败:', err); wx.showToast({ title: err.message || '连接失败', icon: 'none', }); } finally { this.setData({ connecting: false }); } }, /** * 弹窗询问设备名称(选填) * @returns {Promise} 确认返回输入内容(可能为空串);取消/弹窗失败返回 null */ _askDeviceName(device, serialNumber, existing) { const fallback = this._resolveDeviceName('', existing, device.name, serialNumber); const customized = this._isCustomizedName(existing); return new Promise((resolve) => { wx.showModal({ title: '设置设备名称', editable: true, // 已自定义过则回填,方便改名;仍是广播名/编号说明用户没命名过,留空不干扰 content: customized ? existing.name : '', placeholderText: `选填,留空用:${fallback}`, confirmText: '连接', cancelText: '取消', success: (res) => resolve(res.confirm ? (res.content || '').trim() : null), // 弹窗都没弹出来时不静默连接,按取消处理,避免「点一下就莫名连上了」 fail: () => resolve(null), }); }); }, /** * 解析最终展示名称:本次输入 > 既有名称 > BLE 广播名 > 设备编号 * @returns {string} */ _resolveDeviceName(inputName, existing, bleName, serialNumber) { return (inputName || '').trim() || (existing && existing.name) || (bleName || '').trim() || serialNumber; }, /** * 判断既有名称是否为用户自定义(而非广播名/编号的自动兜底值) * @returns {boolean} */ _isCustomizedName(existing) { const name = existing && existing.name; if (!name) return false; return name !== existing.bleName && name !== existing.serialNumber; }, /** * 生成设备编号:优先 BLE 广播名,无名称时退化为 BLE 地址归一化(去冒号取后12位) * @param {Object} device - 扫描到的 BLE 设备 * @returns {string} */ _buildSerialNumber(device) { const rawName = (device.name || device.localName || '').trim(); if (rawName) { return rawName; } const rawId = (device.deviceId || '').replace(/[:\-\s]/g, '').toUpperCase(); return rawId ? rawId.slice(-12) : ''; }, });