This commit is contained in:
2026-06-02 15:28:25 +08:00
commit b271b84f70
249 changed files with 42240 additions and 0 deletions
+703
View File
@@ -0,0 +1,703 @@
/**
* 蓝牙称重设备服务模块
* 功能:
* - 自动连接已配置的称重设备
* - 主动读取重量:发送 'R' 命令,设备返回一次重量数据(readWeight方法)
* - 实时监听并解析重量数据(支持ASCII/二进制协议)
* - 发送控制命令(去皮、校准、单位切换等)
* - 连接状态管理与事件通知
*
* 使用方法:
* import scaleService from '@/utils/BluetoothScaleService.js';
* await scaleService.autoConnect();
*
* // 主动读取重量(发送一次命令,返回一次数据)
* const result = await scaleService.readWeight();
* console.log(result.raw); // 十六进制原始数据
* console.log(result.rawAscii); // ASCII原始数据
* console.log(result.value); // 解析后的重量值
*
* // 监听重量变化(设备主动推送模式)
* scaleService.onWeightChange(this.handleWeightChange);
*/
import bluetoothManager from './BluetoothManager.js';
class BluetoothScaleService {
constructor() {
this.isConnected = false;
this.deviceId = null;
this.deviceName = '';
this.serviceId = null;
this.notifyCharId = null;
this.writeCharId = null;
// 主动读取等待状态(发送读取命令后等待设备返回数据)
this.pendingRead = null; // { resolve, reject, timer }
this.readTimeout = 5000; // 读取超时时间(ms)
// 回调函数
this.callbacks = {
onWeightChange: null,
onConnectionChange: null,
onError: null,
onLog: null
};
// 当前重量数据
this.currentWeight = {
value: 0,
unit: 'kg',
stable: false,
raw: null,
timestamp: null
};
// 数据协议类型
this.protocolType = 'auto'; // auto, ascii, binary
// 预设的称重设备协议
this.protocols = {
// 通用ASCII协议(直接解析数字字符串)
ascii: {
pattern: /^([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?$/,
parser: (str) => {
const match = str.match(/([+-]?\d+\.?\d*)/);
const unitMatch = str.match(/(kg|g|lb|oz)/i);
return {
value: match ? parseFloat(match[1]) : null,
unit: unitMatch ? unitMatch[1].toLowerCase() : 'kg'
};
}
},
// 简易二进制协议(A12等常见电子秤)
A12: {
// 协议格式: 0x05 + 整数部分 + 小数部分 + 单位
parser: (buffer) => {
if (!buffer || buffer.length < 5) return null;
const data = new Uint8Array(buffer);
const sign = (data[0] & 0x80) ? -1 : 1;
const stable = (data[0] & 0x40) !== 0;
const decimal = (data[0] & 0x30) >> 4;
const weight = ((data[1] << 8) | data[2]) & 0x3FFF;
const value = sign * weight / Math.pow(10, decimal);
const units = ['kg', 'g', 'lb', 'oz'];
const unitIndex = data[3] & 0x03;
return {
value: value,
unit: units[unitIndex] || 'kg',
stable: stable
};
}
},
// 赛多利斯协议(示例)
Sartorius: {
parser: (buffer) => {
if (!buffer || buffer.length < 6) return null;
const data = new Uint8Array(buffer);
const value = ((data[2] << 16) | (data[3] << 8) | data[4]) / 1000;
return {
value: value,
unit: 'kg',
stable: data[1] === 0x01
};
}
},
// 梅特勒托利多协议(示例)
Mettler: {
parser: (buffer) => {
if (!buffer || buffer.length < 8) return null;
const data = new Uint8Array(buffer);
const value = parseFloat(String.fromCharCode.apply(null, data.slice(1, 7)));
return {
value: value,
unit: 'kg',
stable: data[7] === 0x0D
};
}
}
};
// 常用命令
this.commands = {
READ_WEIGHT: [0x52], // 'R' - 读取重量命令(ASCII 'R'
TARE: [0x1B, 0x54], // 去皮命令
CALIBRATE_1KG: [0x1B, 0x43, 0x01], // 1kg校准
CALIBRATE_2KG: [0x1B, 0x43, 0x02], // 2kg校准
CALIBRATE_5KG: [0x1B, 0x43, 0x03], // 5kg校准
UNIT_KG: [0x1B, 0x55, 0x01], // 切换kg
UNIT_G: [0x1B, 0x55, 0x02], // 切换g
UNIT_LB: [0x1B, 0x55, 0x03], // 切换lb
ZERO: [0x1B, 0x5A], // 零位校准
RESET: [0x1B, 0x52] // 复位
};
// 初始化监听器
this._initListeners();
}
/**
* 初始化蓝牙事件监听
*/
_initListeners() {
// 连接状态变化
bluetoothManager.setConnectionChangeCallback((deviceId, connected) => {
if (deviceId === this.deviceId) {
this.isConnected = connected;
if (!connected) {
this._log('设备连接断开');
}
if (this.callbacks.onConnectionChange) {
this.callbacks.onConnectionChange(connected);
}
}
});
// 数据接收
bluetoothManager.setDataReceivedCallback((deviceId, value) => {
if (deviceId === this.deviceId) {
this._handleDataReceived(value);
}
});
}
/**
* 处理接收到的数据
*/
_handleDataReceived(buffer) {
try {
// 如果有等待中的主动读取请求,优先处理
if (this.pendingRead) {
const pending = this.pendingRead;
this.pendingRead = null;
clearTimeout(pending.timer);
const rawHex = this._bufferToHex(buffer);
const rawAscii = this._bufferToString(buffer);
this._log(`读取响应 - HEX: ${rawHex}, ASCII: ${rawAscii}`);
// 解析自定义协议
const weight = this._parseCustom(buffer);
pending.resolve({
raw: rawHex,
rawAscii: rawAscii,
...weight
});
return;
}
let weight = null;
// 根据协议类型解析数据
if (this.protocolType === 'auto') {
// 自动检测协议类型
weight = this._autoParse(buffer);
} else if (this.protocolType === 'ascii') {
weight = this._parseAscii(buffer);
} else {
// 二进制协议
weight = this._parseBinary(buffer);
}
if (weight) {
this.currentWeight = {
...weight,
raw: this._bufferToHex(buffer),
timestamp: new Date().toISOString()
};
this._log(`重量: ${weight.value} ${weight.unit} ${weight.stable ? '(稳定)' : '(变化中)'}`);
if (this.callbacks.onWeightChange) {
this.callbacks.onWeightChange(this.currentWeight);
}
}
} catch (error) {
this._log(`数据解析错误: ${error.message}`, 'error');
}
}
/**
* 自动检测并解析数据格式
*/
_autoParse(buffer) {
// 先尝试ASCII解析
const asciiResult = this._parseAscii(buffer);
if (asciiResult) {
return asciiResult;
}
// 尝试二进制协议解析
return this._parseBinary(buffer);
}
/**
* 解析ASCII格式数据
*/
_parseAscii(buffer) {
try {
const text = this._bufferToString(buffer);
const trimmed = text.trim();
// 匹配常见的重量格式
// 例如: "1.23 kg", "2.500kg", "+5.00 KG", "-0.50 kg"
const match = trimmed.match(/^([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?$/i);
if (match) {
return {
value: parseFloat(match[1]),
unit: match[2] ? match[2].toLowerCase() : 'kg',
stable: true
};
}
// 匹配带状态位的格式
// 例如: "ST,1.23kg" 或 "US,2.50kg"
const statusMatch = trimmed.match(/^(ST|US|OL),?([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?$/i);
if (statusMatch) {
return {
value: parseFloat(statusMatch[2]),
unit: statusMatch[3] ? statusMatch[3].toLowerCase() : 'kg',
stable: statusMatch[1].toUpperCase() === 'ST'
};
}
return null;
} catch (error) {
return null;
}
}
/**
* 解析二进制格式数据
*/
_parseBinary(buffer) {
// 遍历所有预设的二进制协议进行尝试
for (const [name, protocol] of Object.entries(this.protocols)) {
if (name === 'ascii') continue;
try {
const result = protocol.parser(buffer);
if (result && typeof result.value === 'number' && !isNaN(result.value)) {
return result;
}
} catch (e) {
// 继续尝试下一个协议
}
}
// 默认二进制解析(A12格式)
return this.protocols.A12.parser(buffer);
}
/**
* 自定义协议解析(主动读取模式使用)
*
* 【重要】此方法用于 readWeight() 主动读取模式,当发送 'R' 命令后
* 设备返回数据时会调用此方法解析。
*
* 当前实现:先尝试自动检测(ASCII/二进制),如果都匹配不上则返回原始数据
* 如果你的设备有特殊协议格式,请取消下方注释并修改解析逻辑
*
* @param {ArrayBuffer} buffer 设备返回的原始数据
* @returns {{ value: number, unit: string, stable: boolean, rawHex: string, rawAscii: string }}
*/
_parseCustom(buffer) {
const data = new Uint8Array(buffer);
const hexStr = this._bufferToHex(buffer);
const asciiStr = this._bufferToString(buffer);
// ============================================================
// 【方式一】先尝试通用自动解析(支持常见ASCII和二进制协议)
// 大部分蓝牙秤可以直接用这个,不需要改下面的代码
// ============================================================
const autoResult = this._autoParse(buffer);
if (autoResult && autoResult.value !== null && !isNaN(autoResult.value)) {
this._log(`[自定义协议] 自动解析成功: ${autoResult.value} ${autoResult.unit}`);
return {
...autoResult,
rawHex: hexStr,
rawAscii: asciiStr.trim()
};
}
// ============================================================
// 【方式二】如果你的设备有特殊协议格式,在这里添加解析逻辑
// 取消注释并根据实际数据格式修改
// ============================================================
// --- 示例1: 十六进制帧格式 ---
// 假设设备返回 6 字节: [帧头0xAA][命令0x52][数据高位][数据低位][小数位][校验]
// 例: AA 52 00 7B 02 D1 → 重量 = 0x007B / 10^2 = 1.23 kg
//
// if (data.length >= 6 && data[0] === 0xAA && data[1] === 0x52) {
// const weightRaw = (data[2] << 8) | data[3];
// const decimal = data[4]; // 小数位数
// const value = weightRaw / Math.pow(10, decimal);
// this._log(`[自定义协议] 十六进制解析成功: ${value}`);
// return { value, unit: 'kg', stable: true, rawHex: hexStr, rawAscii: asciiStr.trim() };
// }
// --- 示例2: ASCII带前缀格式 ---
// 假设设备返回: "W:1.23kg\r\n" 或 "+00123g\r\n"
//
// const match = asciiStr.trim().match(/W:([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?/i);
// if (match) {
// this._log(`[自定义协议] ASCII解析成功: ${match[1]}`);
// return { value: parseFloat(match[1]), unit: (match[2] || 'kg').toLowerCase(), stable: true, rawHex: hexStr, rawAscii: asciiStr.trim() };
// }
// ============================================================
// 以上都没有匹配到 → 返回原始数据(value 为 null
// 页面拿到后可以显示原始数据方便调试
// ============================================================
this._log(`[自定义协议] 未识别的格式 - HEX: ${hexStr}, ASCII: ${asciiStr.trim()}`);
return {
value: null,
unit: 'kg',
stable: false,
rawHex: hexStr,
rawAscii: asciiStr.trim()
};
}
/**
* Buffer转16进制字符串
*/
_bufferToHex(buffer) {
const data = new Uint8Array(buffer);
return Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ');
}
/**
* Buffer转字符串
*/
_bufferToString(buffer) {
const data = new Uint8Array(buffer);
return String.fromCharCode.apply(null, data);
}
/**
* 设置协议类型
*/
setProtocol(protocol) {
if (protocol === 'auto' || protocol === 'ascii' || this.protocols[protocol]) {
this.protocolType = protocol;
this._log(`协议已切换: ${protocol}`);
}
}
/**
* 获取当前配置
*/
getConfig() {
return {
deviceId: this.deviceId,
deviceName: this.deviceName,
serviceId: this.serviceId,
notifyCharId: this.notifyCharId,
writeCharId: this.writeCharId,
protocol: this.protocolType,
isConnected: this.isConnected
};
}
/**
* 从存储加载配置
*/
loadConfig() {
const scaleConfig = uni.getStorageSync('scaleConfig');
if (scaleConfig) {
this.deviceId = scaleConfig.deviceId || null;
this.deviceName = scaleConfig.deviceName || '';
this.serviceId = scaleConfig.serviceId || null;
this.notifyCharId = scaleConfig.characteristicId || null;
this.writeCharId = scaleConfig.writeCharacteristicId || null;
this._log('配置已加载');
}
return this.getConfig();
}
/**
* 自动连接已配置的设备
*/
async autoConnect() {
this.loadConfig();
if (!this.deviceId || !this.serviceId) {
throw new Error('未配置称重设备,请先在设置中配置');
}
return this.connect();
}
/**
* 连接指定设备
*/
async connect() {
if (!this.deviceId) {
throw new Error('缺少设备ID');
}
try {
this._log('正在初始化蓝牙...');
await bluetoothManager.init();
this._log(`正在连接设备: ${this.deviceName || this.deviceId}`);
const result = await bluetoothManager.connectKnownDevice(
this.deviceId,
this.serviceId,
this.writeCharId,
this.notifyCharId
);
// 更新配置
this.serviceId = result.serviceId;
this.notifyCharId = result.notifyCharId;
this.writeCharId = result.writeCharId;
this.isConnected = true;
this._log('连接成功');
// 通知连接状态
if (this.callbacks.onConnectionChange) {
this.callbacks.onConnectionChange(true);
}
return true;
} catch (error) {
this._log(`连接失败: ${error.message}`, 'error');
this.isConnected = false;
if (this.callbacks.onError) {
this.callbacks.onError(error);
}
throw error;
}
}
/**
* 断开连接
*/
async disconnect() {
if (this.deviceId) {
await bluetoothManager.disconnectDevice(this.deviceId);
}
this.isConnected = false;
this._log('已断开连接');
if (this.callbacks.onConnectionChange) {
this.callbacks.onConnectionChange(false);
}
}
/**
* 发送命令
*/
async sendCommand(command) {
if (!this.isConnected) {
throw new Error('设备未连接');
}
let cmdData;
if (Array.isArray(command)) {
cmdData = new Uint8Array(command);
} else if (this.commands[command]) {
cmdData = new Uint8Array(this.commands[command]);
} else {
throw new Error('未知的命令');
}
this._log(`发送命令: ${Array.from(cmdData).map(b => b.toString(16).padStart(2, '0')).join(' ')}`);
try {
await bluetoothManager.writeData(this.deviceId, cmdData.buffer);
return true;
} catch (error) {
this._log(`命令发送失败: ${error.message}`, 'error');
throw error;
}
}
/**
* 主动读取重量
* 向设备发送 'R' 命令,等待设备返回重量数据
* 一次发送一次返回,不会持续读取
*
* @param {number} [timeout=5000] 等待设备响应超时时间(ms)
* @returns {Promise<{raw: string, rawAscii: string, value: number|null, unit: string, stable: boolean}>}
*
* 返回值说明:
* - raw: 设备返回的十六进制字符串,例如 "aa 52 00 7b 02 d1"
* - rawAscii: 设备返回的ASCII字符串,例如 "1.23kg"
* - value: 解析后的重量数值(需在 _parseCustom 中实现解析逻辑,默认为 null)
* - unit: 单位(默认 'kg'
* - stable: 是否稳定(默认 false
*
* 使用示例:
* const result = await scaleService.readWeight();
* console.log(result.raw); // "aa 52 00 7b 02 d1"
* console.log(result.rawAscii); // "1.23kg"
* console.log(result.value); // null(需实现 _parseCustom 后才有值)
*/
async readWeight(timeout) {
if (!this.isConnected) {
throw new Error('设备未连接');
}
if (this.pendingRead) {
throw new Error('已有读取请求等待中,请勿重复发送');
}
const actualTimeout = timeout || this.readTimeout;
return new Promise(async (resolve, reject) => {
// 设置超时定时器
const timer = setTimeout(() => {
this.pendingRead = null;
reject(new Error(`读取重量超时(${actualTimeout}ms),设备未返回数据`));
}, actualTimeout);
// 注册等待回调
this.pendingRead = { resolve, reject, timer };
try {
// 发送读取命令 'R'
await this.sendCommand('READ_WEIGHT');
this._log('已发送读取重量命令,等待设备响应...');
} catch (error) {
// 发送失败,清除等待状态
clearTimeout(timer);
this.pendingRead = null;
reject(error);
}
});
}
/**
* 去皮(归零)
*/
async tare() {
return this.sendCommand('TARE');
}
/**
* 砝码校准
* @param {number} weight 校准重量(默认1kg
*/
async calibrate(weight = 1) {
let command;
if (weight <= 1) {
command = 'CALIBRATE_1KG';
} else if (weight <= 2) {
command = 'CALIBRATE_2KG';
} else {
command = 'CALIBRATE_5KG';
}
return this.sendCommand(command);
}
/**
* 切换单位
* @param {string} unit kg, g, lb
*/
async changeUnit(unit) {
const unitMap = {
'kg': 'UNIT_KG',
'g': 'UNIT_G',
'lb': 'UNIT_LB'
};
const command = unitMap[unit];
if (!command) {
throw new Error('不支持的单位');
}
return this.sendCommand(command);
}
/**
* 获取当前重量
*/
getCurrentWeight() {
return { ...this.currentWeight };
}
/**
* 获取实时重量(带稳定性等待)
*/
async getStableWeight(timeout = 10000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkWeight = () => {
if (this.currentWeight.stable) {
resolve(this.currentWeight);
} else if (Date.now() - startTime > timeout) {
reject(new Error('等待重量稳定超时'));
} else {
setTimeout(checkWeight, 100);
}
};
checkWeight();
});
}
/**
* 发送自定义命令
*/
async sendRawCommand(commandArray) {
return this.sendCommand(commandArray);
}
/**
* 注册重量变化回调
*/
onWeightChange(callback) {
this.callbacks.onWeightChange = callback;
}
/**
* 取消重量变化回调
*/
offWeightChange() {
this.callbacks.onWeightChange = null;
}
/**
* 注册连接状态回调
*/
onConnectionChange(callback) {
this.callbacks.onConnectionChange = callback;
}
/**
* 取消连接状态回调
*/
offConnectionChange() {
this.callbacks.onConnectionChange = null;
}
/**
* 注册错误回调
*/
onError(callback) {
this.callbacks.onError = callback;
}
/**
* 取消错误回调
*/
offError() {
this.callbacks.onError = null;
}
/**
* 记录日志
*/
_log(message, level = 'info') {
console[level === 'error' ? 'error' : 'log'](`[称重服务] ${message}`);
if (this.callbacks.onLog) {
this.callbacks.onLog(message, level);
}
}
}
export default new BluetoothScaleService();