fastbee/yack-mini/miniprogram/pages/device/scan/scan.js
2026-09-27 15:17:17 +08:00

242 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 扫描设备页面
*
* 新测点链路下此处只负责「连接蓝牙设备」:
* 扫描 → 点击设备 → 询问设备名称(选填)→ 登记保存 → 交由常驻网关 gateway 接管 BLE 连接。
* 设备不再需要配置弹窗(产品/MQTT 凭证统一在“我的”页配置),
* 设备编号自动取自 BLE 广播名(无则退化为 BLE 地址归一化),
* 设备名称仅作本地展示标签(可选填,留空沿用上次或退化为广播名/编号),不参与上报身份。
*/
const ble = require('../../../services/ble.js');
const storage = require('../../../utils/storage.js');
const { gateway } = require('../../../services/gateway.js');
const { LIMITS } = require('../../../utils/constants.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(),
};
// 必须检查返回值:数量达上限时这里是失败的,继续往下会变成
// "提示已连接、实际没保存也没连"的静默失败
if (!storage.addDevice(deviceConfig)) {
wx.hideLoading();
wx.showToast({
title: `最多添加 ${LIMITS.MAX_DEVICE_COUNT} 个设备`,
icon: 'none',
duration: 2500,
});
return;
}
// 成为唯一启用设备(单 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<string|null>} 确认返回输入内容(可能为空串);取消/弹窗失败返回 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) : '';
},
});