| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- /**
- * 蓝牙秤工具模块 - BT-YH
- * Service UUID: 49535343-FE7D-4AE5-8FA9-9FAFD205E455
- * Characteristic UUID: 49535343-1E4D-4BD9-BA61-23C647249616
- * 数据格式: ASCII 字符串, 正则 /=(\d+\.\d+)/ 提取重量(kg)
- */
- const DEVICE_NAME = 'BT-YH';
- const SERVICE_UUID = '49535343-FE7D-4AE5-8FA9-9FAFD205E455';
- const CHARACTERISTIC_UUID = '49535343-1E4D-4BD9-BA61-23C647249616';
- // ArrayBuffer 转 ASCII 字符串
- function ab2str(buffer) {
- const arr = new Uint8Array(buffer);
- let str = '';
- for (let i = 0; i < arr.length; i++) {
- str += String.fromCharCode(arr[i]);
- }
- return str;
- }
- // 从原始字符串中提取重量值
- function parseWeight(str) {
- const match = str.match(/=(\d+\.\d+)/);
- if (match) {
- const val = parseFloat(match[1]);
- if (!isNaN(val)) return val;
- }
- return null;
- }
- // UUID 统一转大写,兼容 iOS/Android 差异
- function normalizeUUID(uuid) {
- return uuid.toUpperCase();
- }
- class BluetoothScale {
- constructor() {
- this._deviceId = null;
- this._serviceId = null;
- this._characteristicId = null;
- this._buffer = ''; // 分包拼接缓冲
- this._lastWeight = null; // 上一次稳定值
- this._stableCount = 0; // 连续相同次数
- this._onWeight = null; // 稳定重量回调
- this._onStatus = null; // 状态变更回调
- this._onSelectDevice = null; // 多设备选择回调
- this._scanning = false;
- this._connected = false;
- }
- // 注册回调
- onWeight(fn) { this._onWeight = fn; }
- onStatus(fn) { this._onStatus = fn; }
- // 多设备时触发,参数为设备列表,调用方需 resolve(deviceId) 或 reject()
- onSelectDevice(fn) { this._onSelectDevice = fn; }
- _setStatus(msg) {
- console.log('[BT]', msg);
- if (this._onStatus) this._onStatus(msg);
- }
- // 处理收到的数据包
- _handleData(buffer) {
- const chunk = ab2str(buffer);
- this._buffer += chunk;
- // 避免缓冲区过大
- if (this._buffer.length > 200) {
- this._buffer = this._buffer.slice(-100);
- }
- const weight = parseWeight(this._buffer);
- if (weight !== null) {
- if (weight === this._lastWeight) {
- this._stableCount++;
- // 连续2次相同认为稳定
- if (this._stableCount >= 2 && this._onWeight) {
- this._onWeight(weight);
- }
- } else {
- this._lastWeight = weight;
- this._stableCount = 1;
- }
- }
- }
- // 初始化蓝牙适配器
- _openAdapter() {
- return new Promise((resolve, reject) => {
- wx.openBluetoothAdapter({
- success: resolve,
- fail: (err) => {
- if (err.errCode === 10001) {
- reject(new Error('请先开启手机蓝牙'));
- } else {
- reject(new Error('蓝牙初始化失败: ' + err.errMsg));
- }
- }
- });
- });
- }
- // 扫描目标设备,固定扫5秒收集所有 BT-YH,多台时交给调用方选择
- _scanDevice() {
- return new Promise((resolve, reject) => {
- this._scanning = true;
- this._setStatus('正在搜索蓝牙秤...');
- const foundDevices = []; // 收集所有找到的目标设备
- const done = () => {
- wx.stopBluetoothDevicesDiscovery();
- this._scanning = false;
- if (foundDevices.length === 0) {
- reject(new Error('未找到蓝牙秤,请确认秤已开机并在附近'));
- return;
- }
- if (foundDevices.length === 1) {
- // 只有一台,直接连
- resolve(foundDevices[0].deviceId);
- return;
- }
- // 多台,交给调用方弹选择框
- if (this._onSelectDevice) {
- this._onSelectDevice(foundDevices, (deviceId) => {
- if (deviceId) {
- resolve(deviceId);
- } else {
- reject(new Error('已取消选择'));
- }
- });
- } else {
- // 没注册回调时默认取第一台
- resolve(foundDevices[0].deviceId);
- }
- };
- // 5秒后结束扫描
- const timer = setTimeout(done, 5000);
- wx.startBluetoothDevicesDiscovery({
- services: [SERVICE_UUID],
- allowDuplicatesKey: false,
- success: () => {
- wx.onBluetoothDeviceFound((res) => {
- const devices = res.devices || [];
- for (const device of devices) {
- const name = (device.name || device.localName || '').trim();
- if (name === DEVICE_NAME) {
- // 去重后加入列表
- const exists = foundDevices.some(d => d.deviceId === device.deviceId);
- if (!exists) {
- foundDevices.push({
- deviceId: device.deviceId,
- name: name,
- rssi: device.RSSI || 0,
- });
- this._setStatus('已发现 ' + foundDevices.length + ' 台蓝牙秤...');
- }
- }
- }
- });
- },
- fail: (err) => {
- clearTimeout(timer);
- this._scanning = false;
- reject(new Error('启动扫描失败: ' + err.errMsg));
- }
- });
- });
- }
- // 连接设备
- _connect(deviceId) {
- return new Promise((resolve, reject) => {
- this._setStatus('正在连接蓝牙秤...');
- wx.createBLEConnection({
- deviceId,
- timeout: 10000,
- success: () => {
- this._deviceId = deviceId;
- this._connected = true;
- resolve();
- },
- fail: (err) => {
- reject(new Error('连接失败: ' + err.errMsg));
- }
- });
- });
- }
- // 获取服务列表,找到目标 Service
- _getService() {
- return new Promise((resolve, reject) => {
- // iOS 需要延迟一点再获取服务
- setTimeout(() => {
- wx.getBLEDeviceServices({
- deviceId: this._deviceId,
- success: (res) => {
- const target = res.services.find(
- s => normalizeUUID(s.uuid) === normalizeUUID(SERVICE_UUID)
- );
- if (target) {
- this._serviceId = target.uuid;
- resolve();
- } else {
- reject(new Error('未找到目标蓝牙服务'));
- }
- },
- fail: (err) => reject(new Error('获取服务失败: ' + err.errMsg))
- });
- }, 500);
- });
- }
- // 获取特征值,找到目标 Characteristic
- _getCharacteristic() {
- return new Promise((resolve, reject) => {
- wx.getBLEDeviceCharacteristics({
- deviceId: this._deviceId,
- serviceId: this._serviceId,
- success: (res) => {
- const target = res.characteristics.find(
- c => normalizeUUID(c.uuid) === normalizeUUID(CHARACTERISTIC_UUID)
- );
- if (target) {
- this._characteristicId = target.uuid;
- resolve();
- } else {
- // 未精确匹配时,取第一个支持 notify 的特征值
- const notifyChar = res.characteristics.find(
- c => c.properties && c.properties.notify
- );
- if (notifyChar) {
- this._characteristicId = notifyChar.uuid;
- resolve();
- } else {
- reject(new Error('未找到可订阅的特征值'));
- }
- }
- },
- fail: (err) => reject(new Error('获取特征值失败: ' + err.errMsg))
- });
- });
- }
- // 开启 notify 订阅
- _subscribeNotify() {
- return new Promise((resolve, reject) => {
- this._setStatus('正在订阅数据...');
- wx.notifyBLECharacteristicValueChange({
- deviceId: this._deviceId,
- serviceId: this._serviceId,
- characteristicId: this._characteristicId,
- state: true,
- success: () => {
- // 注册数据接收回调
- wx.onBLECharacteristicValueChange((res) => {
- if (
- res.deviceId === this._deviceId &&
- normalizeUUID(res.characteristicId) === normalizeUUID(this._characteristicId)
- ) {
- this._handleData(res.value);
- }
- });
- resolve();
- },
- fail: (err) => reject(new Error('订阅失败: ' + err.errMsg))
- });
- });
- }
- /**
- * 启动蓝牙秤连接
- * 返回 Promise,resolve 后可通过 onWeight 回调接收数据
- */
- async start() {
- try {
- this._buffer = '';
- this._lastWeight = null;
- this._stableCount = 0;
- await this._openAdapter();
- const deviceId = await this._scanDevice();
- await this._connect(deviceId);
- await this._getService();
- await this._getCharacteristic();
- await this._subscribeNotify();
- this._setStatus('已连接,等待读数...');
- } catch (err) {
- this._setStatus(err.message);
- this.stop();
- throw err;
- }
- }
- /**
- * 断开并清理
- */
- stop() {
- // 关闭 notify
- if (this._connected && this._deviceId && this._serviceId && this._characteristicId) {
- wx.notifyBLECharacteristicValueChange({
- deviceId: this._deviceId,
- serviceId: this._serviceId,
- characteristicId: this._characteristicId,
- state: false,
- fail: () => {}
- });
- }
- if (this._deviceId) {
- wx.closeBLEConnection({ deviceId: this._deviceId, fail: () => {} });
- }
- if (this._scanning) {
- wx.stopBluetoothDevicesDiscovery();
- }
- wx.offBLECharacteristicValueChange && wx.offBLECharacteristicValueChange();
- this._deviceId = null;
- this._serviceId = null;
- this._characteristicId = null;
- this._connected = false;
- this._scanning = false;
- this._buffer = '';
- this._lastWeight = null;
- this._stableCount = 0;
- }
- }
- export default BluetoothScale;
|