dedsudiyu 1 week ago
parent
commit
b47bc5e8fd

+ 328 - 0
pages_hazardousWasteRecycling/utils/bluetooth.js

@@ -0,0 +1,328 @@
+/**
+ * 蓝牙秤工具模块 - 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;

+ 96 - 1
pages_hazardousWasteRecycling/views/weighingRegistration/addPage.vue

@@ -56,6 +56,13 @@
 						<input class="input-p" type="digit" maxlength="10" @input="(val)=>handleInput(index,val)" v-model="item.forData6">
 						<input class="input-p" type="digit" maxlength="10" @input="(val)=>handleInput(index,val)" v-model="item.forData6">
 						<view class="add-button" @click="reductionAdd(2,index)">+</view>
 						<view class="add-button" @click="reductionAdd(2,index)">+</view>
 						<view class="unit-p">kg</view>
 						<view class="unit-p">kg</view>
+						<view class="bt-button" :class="btActiveIndex===index?'bt-button-active':''" @click="readScale(index)">
+							{{btActiveIndex===index?'读取中':'读秤'}}
+						</view>
+					</view>
+					<view class="bt-status-box" v-if="btActiveIndex===index&&btStatus">
+						<view class="bt-status-text">{{btStatus}}</view>
+						<view class="bt-cancel-text" @click="stopScale()">取消</view>
 					</view>
 					</view>
 				</view>
 				</view>
 				<view class="add-item-button" v-if="this.addForm.formData4.length < this.dialogOptionForList.length"
 				<view class="add-item-button" v-if="this.addForm.formData4.length < this.dialogOptionForList.length"
@@ -96,6 +103,7 @@
 		hwmsAppWasteOrderList,
 		hwmsAppWasteOrderList,
 		hwmsAppWasteOrderWasteDetail,
 		hwmsAppWasteOrderWasteDetail,
 	} from '@/pages_hazardousWasteRecycling/api/index.js'
 	} from '@/pages_hazardousWasteRecycling/api/index.js'
+	import BluetoothScale from '@/pages_hazardousWasteRecycling/utils/bluetooth.js'
 	export default {
 	export default {
 		data() {
 		data() {
 			return {
 			return {
@@ -118,11 +126,17 @@
 				dialogOptionList:[],
 				dialogOptionList:[],
 				dialogOptionForList:[],
 				dialogOptionForList:[],
 				checkType:false,
 				checkType:false,
+				btScale: null,
+				btStatus: '',
+				btActiveIndex: -1,
 			}
 			}
 		},
 		},
 		onLoad(option) {
 		onLoad(option) {
 			this.hwmsAppWasteOrderList(option.id);
 			this.hwmsAppWasteOrderList(option.id);
 		},
 		},
+		onUnload() {
+			this.stopScale();
+		},
 		onShow() {
 		onShow() {
 
 
 		},
 		},
@@ -475,6 +489,57 @@
           self.$set(self.addForm.formData4[i],'forList',JSON.parse(JSON.stringify(list)));
           self.$set(self.addForm.formData4[i],'forList',JSON.parse(JSON.stringify(list)));
         }
         }
       },
       },
+		// 点击读秤按钮
+		readScale(index) {
+			let self = this;
+			if (this.btActiveIndex === index) {
+				this.stopScale();
+				return;
+			}
+			this.stopScale();
+			this.btActiveIndex = index;
+			this.btStatus = '初始化蓝牙...';
+			const scale = new BluetoothScale();
+			this.btScale = scale;
+			scale.onStatus((msg) => {
+				self.btStatus = msg;
+			});
+			scale.onSelectDevice((devices, callback) => {
+				const itemList = devices.map((d, i) => {
+					const rssiText = d.rssi ? '  信号:' + d.rssi + 'dBm' : '';
+					return d.name + ' (' + (i + 1) + ')' + rssiText;
+				});
+				uni.showActionSheet({
+					title: '检测到多台蓝牙秤,请选择',
+					itemList: itemList,
+					itemColor: '#0183FA',
+					success: (res) => {
+						callback(devices[res.tapIndex].deviceId);
+					},
+					fail: () => {
+						callback(null);
+					}
+				});
+			});
+			scale.onWeight((weight) => {
+				self.$set(self.addForm.formData4[index], 'forData6', weight);
+				self.btStatus = '读取成功: ' + weight + ' kg';
+				self.stopScale();
+				uni.showToast({ title: '读取成功: ' + weight + ' kg', icon: 'none', duration: 2000 });
+			});
+			scale.start().catch((err) => {
+				uni.showToast({ title: err.message || '蓝牙读取失败', icon: 'none', duration: 2500 });
+			});
+		},
+		// 停止蓝牙读秤
+		stopScale() {
+			if (this.btScale) {
+				this.btScale.stop();
+				this.btScale = null;
+			}
+			this.btActiveIndex = -1;
+			this.btStatus = '';
+		},
 		},
 		},
 	}
 	}
 </script>
 </script>
@@ -623,6 +688,21 @@
 					line-height:60rpx;
 					line-height:60rpx;
 					font-size:30rpx;
 					font-size:30rpx;
 				}
 				}
+				.bt-button{
+					margin-left:16rpx;
+					padding:0 18rpx;
+					height:60rpx;
+					line-height:60rpx;
+					border:1rpx solid #0183FA;
+					color:#0183FA;
+					font-size:26rpx;
+					border-radius:6rpx;
+					white-space:nowrap;
+				}
+				.bt-button-active{
+					background-color:#0183FA;
+					color:#fff;
+				}
 				.input-text-p{
 				.input-text-p{
 					flex:1;
 					flex:1;
 					height:185rpx;
 					height:185rpx;
@@ -678,7 +758,22 @@
 					font-size:50rpx;
 					font-size:50rpx;
 				}
 				}
 			}
 			}
-			.add-item-button{
+			.bt-status-box{
+				display:flex;
+				align-items:center;
+				padding:0 20rpx 16rpx 56rpx;
+				.bt-status-text{
+					flex:1;
+					font-size:24rpx;
+					color:#999;
+				}
+				.bt-cancel-text{
+					font-size:24rpx;
+					color:#FF6A6A;
+					padding:0 10rpx;
+				}
+			}
+		.add-item-button{
 				width:588rpx;
 				width:588rpx;
 				line-height:80rpx;
 				line-height:80rpx;
 				height:80rpx;
 				height:80rpx;