452 lines
15 KiB
JavaScript
452 lines
15 KiB
JavaScript
"use strict";
|
|
const common_vendor = require("../common/vendor.js");
|
|
let deviceId = "";
|
|
let serviceId = "";
|
|
let characteristicId = "";
|
|
let writeCharacteristicId = "";
|
|
let weightCallback = null;
|
|
const initBluetooth = () => {
|
|
return new Promise((resolve, reject) => {
|
|
common_vendor.index.openBluetoothAdapter({
|
|
success: () => {
|
|
console.log("Bluetooth adapter initialized");
|
|
resolve();
|
|
},
|
|
fail: (err) => {
|
|
console.error("Bluetooth init failed:", err);
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
const startDiscovery = (callback) => {
|
|
return new Promise((resolve) => {
|
|
const foundDevices = [];
|
|
common_vendor.index.startBluetoothDevicesDiscovery({
|
|
services: [],
|
|
success: () => {
|
|
console.log("Started discovery");
|
|
common_vendor.index.onBluetoothDeviceFound((res) => {
|
|
const devices = res.devices.filter((d) => d.name && (d.name.includes("Scale") || d.name.includes("秤") || d.name.includes("WXL")));
|
|
devices.forEach((device) => {
|
|
if (!foundDevices.find((d) => d.deviceId === device.deviceId)) {
|
|
foundDevices.push({
|
|
deviceId: device.deviceId,
|
|
name: device.name,
|
|
RSSI: device.RSSI || 0
|
|
});
|
|
}
|
|
});
|
|
callback([...foundDevices]);
|
|
});
|
|
setTimeout(() => {
|
|
common_vendor.index.stopBluetoothDevicesDiscovery();
|
|
resolve();
|
|
}, 1e4);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
const stopDiscovery = () => {
|
|
common_vendor.index.stopBluetoothDevicesDiscovery();
|
|
};
|
|
const connectDevice = (deviceIdParam) => {
|
|
return new Promise((resolve, reject) => {
|
|
deviceId = deviceIdParam;
|
|
common_vendor.index.createBLEConnection({
|
|
deviceId: deviceIdParam,
|
|
success: () => {
|
|
console.log("Connected to device:", deviceIdParam);
|
|
getBLEDeviceServices(deviceIdParam).then(() => resolve()).catch(reject);
|
|
},
|
|
fail: (err) => {
|
|
console.error("Connection failed:", err);
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
const getBLEDeviceServices = (deviceIdParam) => {
|
|
return new Promise((resolve, reject) => {
|
|
common_vendor.index.getBLEDeviceServices({
|
|
deviceId: deviceIdParam,
|
|
success: (res) => {
|
|
console.log("=== Available Services ===");
|
|
res.services.forEach((s, i) => {
|
|
console.log(`${i + 1}. UUID: ${s.uuid}, Primary: ${s.isPrimary}`);
|
|
});
|
|
const commonServiceUUIDs = [
|
|
"0000FFE0",
|
|
"0000ffe0",
|
|
"0000180a",
|
|
"00001800",
|
|
"0000ffe5",
|
|
"0000fff0",
|
|
"fff0",
|
|
"0000ffe3",
|
|
"ffe3",
|
|
"0000ffe1",
|
|
"ffe1"
|
|
];
|
|
let foundService = res.services.find(
|
|
(s) => commonServiceUUIDs.some((uuid) => s.uuid.toLowerCase().startsWith(uuid.toLowerCase()))
|
|
);
|
|
if (!foundService && res.services.length > 0) {
|
|
foundService = res.services.find((s) => s.isPrimary) || res.services.find((s) => s.uuid.length === 4 || s.uuid.length === 8) || res.services[0];
|
|
console.log("Using fallback service:", foundService.uuid);
|
|
}
|
|
if (foundService) {
|
|
serviceId = foundService.uuid;
|
|
console.log("Selected service:", serviceId);
|
|
getBLEDeviceCharacteristics(deviceIdParam, foundService.uuid).then(() => resolve()).catch(reject);
|
|
} else {
|
|
reject(new Error("Service not found"));
|
|
}
|
|
},
|
|
fail: reject
|
|
});
|
|
});
|
|
};
|
|
const getBLEDeviceCharacteristics = (deviceIdParam, serviceIdParam) => {
|
|
return new Promise((resolve, reject) => {
|
|
common_vendor.index.getBLEDeviceCharacteristics({
|
|
deviceId: deviceIdParam,
|
|
serviceId: serviceIdParam,
|
|
success: (res) => {
|
|
console.log("=== Available Characteristics ===");
|
|
res.characteristics.forEach((c, i) => {
|
|
console.log(`${i + 1}. UUID: ${c.uuid}`);
|
|
console.log(` - Properties: notify=${c.properties.notify}, read=${c.properties.read}, write=${c.properties.write}, writeWithoutResponse=${c.properties.writeWithoutResponse}`);
|
|
});
|
|
const commonCharUUIDs = [
|
|
"0000FFE1",
|
|
"0000ffe1",
|
|
"0000ffe2",
|
|
"0000ffe4",
|
|
"0000fff1",
|
|
"fff1",
|
|
"0000ffe3",
|
|
"ffe3",
|
|
"00002a00",
|
|
"00002a01",
|
|
"00002a04",
|
|
"00002a05"
|
|
];
|
|
let characteristic = res.characteristics.find(
|
|
(c) => commonCharUUIDs.some((uuid) => c.uuid.toLowerCase().startsWith(uuid.toLowerCase())) && (c.properties.notify || c.properties.read)
|
|
);
|
|
const writeChar = res.characteristics.find((c) => c.properties.write || c.properties.writeWithoutResponse);
|
|
if (writeChar) {
|
|
writeCharacteristicId = writeChar.uuid;
|
|
console.log("Found write characteristic:", writeCharacteristicId);
|
|
}
|
|
if (!characteristic) {
|
|
characteristic = res.characteristics.find((c) => c.properties.notify);
|
|
}
|
|
if (!characteristic) {
|
|
characteristic = res.characteristics.find((c) => c.properties.read);
|
|
}
|
|
if (!characteristic && res.characteristics.length > 0) {
|
|
characteristic = res.characteristics.find((c) => c.uuid.length === 4 || c.uuid.length === 8) || res.characteristics[0];
|
|
console.log("Using fallback characteristic:", characteristic.uuid);
|
|
}
|
|
if (characteristic) {
|
|
characteristicId = characteristic.uuid;
|
|
console.log("Selected characteristic for read/notify:", characteristicId);
|
|
console.log("Properties:", characteristic.properties);
|
|
sendInitCommands(deviceIdParam, serviceIdParam);
|
|
if (characteristic.properties.notify) {
|
|
notifyBLECharacteristicValueChange(deviceIdParam, serviceIdParam, characteristic.uuid).then(() => resolve()).catch(reject);
|
|
} else if (characteristic.properties.read) {
|
|
readBLECharacteristicValue(deviceIdParam, serviceIdParam, characteristic.uuid).then(() => {
|
|
setupPeriodicRead(deviceIdParam, serviceIdParam, characteristic.uuid);
|
|
resolve();
|
|
}).catch(reject);
|
|
} else {
|
|
resolve();
|
|
}
|
|
} else {
|
|
reject(new Error("Characteristic not found"));
|
|
}
|
|
},
|
|
fail: reject
|
|
});
|
|
});
|
|
};
|
|
const sendInitCommands = (deviceIdParam, serviceIdParam) => {
|
|
if (!writeCharacteristicId) {
|
|
console.log("No write characteristic found, skipping init commands");
|
|
return;
|
|
}
|
|
const initCommands = [
|
|
stringToBuffer("START"),
|
|
stringToBuffer("ON"),
|
|
stringToBuffer("WEIGH"),
|
|
stringToBuffer("GET"),
|
|
stringToBuffer("1"),
|
|
new Uint8Array([1]).buffer,
|
|
new Uint8Array([2]).buffer,
|
|
new Uint8Array([3]).buffer,
|
|
new Uint8Array([85]).buffer,
|
|
new Uint8Array([170, 85]).buffer,
|
|
new Uint8Array([254, 1]).buffer,
|
|
new Uint8Array([255, 1]).buffer,
|
|
new Uint8Array([0, 0, 0, 1]).buffer
|
|
];
|
|
initCommands.forEach((cmd, index) => {
|
|
setTimeout(() => {
|
|
writeBLECharacteristicValue(deviceIdParam, serviceIdParam, writeCharacteristicId, cmd);
|
|
}, index * 200);
|
|
});
|
|
};
|
|
const writeBLECharacteristicValue = (deviceIdParam, serviceIdParam, characteristicIdParam, value) => {
|
|
common_vendor.index.writeBLECharacteristicValue({
|
|
deviceId: deviceIdParam,
|
|
serviceId: serviceIdParam,
|
|
characteristicId: characteristicIdParam,
|
|
value,
|
|
success: () => {
|
|
console.log("Wrote command:", arrayBufferToHex(value));
|
|
},
|
|
fail: (err) => {
|
|
console.log("Write failed (may be normal for read-only chars):", err.errMsg);
|
|
}
|
|
});
|
|
};
|
|
const stringToBuffer = (str) => {
|
|
const encoder = new TextEncoder();
|
|
return encoder.encode(str).buffer;
|
|
};
|
|
let readInterval = null;
|
|
const setupPeriodicRead = (deviceIdParam, serviceIdParam, characteristicIdParam) => {
|
|
if (readInterval) {
|
|
clearInterval(readInterval);
|
|
}
|
|
readInterval = setInterval(() => {
|
|
common_vendor.index.readBLECharacteristicValue({
|
|
deviceId: deviceIdParam,
|
|
serviceId: serviceIdParam,
|
|
characteristicId: characteristicIdParam,
|
|
success: (res) => {
|
|
handleBLEData(res.value);
|
|
},
|
|
fail: (err) => {
|
|
console.log("Periodic read failed:", err.errMsg);
|
|
}
|
|
});
|
|
}, 300);
|
|
console.log("Started periodic read every 300ms");
|
|
};
|
|
const notifyBLECharacteristicValueChange = (deviceIdParam, serviceIdParam, characteristicIdParam) => {
|
|
return new Promise((resolve, reject) => {
|
|
common_vendor.index.notifyBLECharacteristicValueChange({
|
|
deviceId: deviceIdParam,
|
|
serviceId: serviceIdParam,
|
|
characteristicId: characteristicIdParam,
|
|
state: true,
|
|
success: () => {
|
|
console.log("Notify enabled for:", characteristicIdParam);
|
|
resolve();
|
|
},
|
|
fail: (err) => {
|
|
console.log("Notify failed, trying read mode:", err.errMsg);
|
|
readBLECharacteristicValue(deviceIdParam, serviceIdParam, characteristicIdParam).then(() => {
|
|
setupPeriodicRead(deviceIdParam, serviceIdParam, characteristicIdParam);
|
|
resolve();
|
|
}).catch(reject);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
const readBLECharacteristicValue = (deviceIdParam, serviceIdParam, characteristicIdParam) => {
|
|
return new Promise((resolve, reject) => {
|
|
common_vendor.index.readBLECharacteristicValue({
|
|
deviceId: deviceIdParam,
|
|
serviceId: serviceIdParam,
|
|
characteristicId: characteristicIdParam,
|
|
success: (res) => {
|
|
console.log("Read characteristic value received");
|
|
handleBLEData(res.value);
|
|
resolve();
|
|
},
|
|
fail: (err) => {
|
|
console.error("Read failed:", err);
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
const handleBLEData = (value) => {
|
|
console.log("=== BLE Data Received ===");
|
|
console.log("Data length:", value.byteLength, "bytes");
|
|
console.log("Hex:", arrayBufferToHex(value));
|
|
const asciiData = arrayBufferToString(value);
|
|
console.log("ASCII:", JSON.stringify(asciiData));
|
|
const weight = parseWeightData(value);
|
|
console.log("Parsed weight:", weight, "kg");
|
|
if (weight > 0 && weight < 1e3 && weightCallback) {
|
|
weightCallback(weight);
|
|
console.log("Weight sent to callback:", weight);
|
|
}
|
|
};
|
|
const listenWeightData = (callback) => {
|
|
weightCallback = callback;
|
|
console.log("Weight callback registered");
|
|
common_vendor.index.onBLECharacteristicValueChange((res) => {
|
|
console.log("BLE characteristic value changed event");
|
|
handleBLEData(res.value);
|
|
});
|
|
};
|
|
const parseWeightData = (buffer) => {
|
|
if (buffer.byteLength === 0) {
|
|
console.log("Empty buffer received");
|
|
return 0;
|
|
}
|
|
const dataView = new DataView(buffer);
|
|
const asciiData = arrayBufferToString(buffer);
|
|
const cleaned = asciiData.replace(/[\r\n\s\x00-\x1F]+/g, "");
|
|
console.log("Cleaned ASCII:", JSON.stringify(cleaned));
|
|
const textPatterns = [
|
|
/[+-]?\d+\.\d{1,3}\s*[kK][gG]?/,
|
|
/[+-]?\d+\.\d{1,3}/,
|
|
/[+-]?\d+\,\d{1,3}/,
|
|
/[+-]?\d+\.?\d*/
|
|
];
|
|
for (const pattern of textPatterns) {
|
|
const match = cleaned.match(pattern);
|
|
if (match) {
|
|
const numStr = match[0].replace(",", ".").replace(/[^0-9.-]/g, "");
|
|
const weight = parseFloat(numStr);
|
|
if (!isNaN(weight) && weight > 0 && weight < 1e3) {
|
|
console.log("Parsed from text:", weight);
|
|
return weight;
|
|
}
|
|
}
|
|
}
|
|
if (buffer.byteLength >= 2) {
|
|
const testValues = [];
|
|
if (buffer.byteLength >= 4) {
|
|
testValues.push(dataView.getInt32(0, true) / 1e3);
|
|
testValues.push(dataView.getInt32(0, false) / 1e3);
|
|
testValues.push(dataView.getFloat32(0, true));
|
|
testValues.push(dataView.getFloat32(0, false));
|
|
testValues.push(dataView.getUint32(0, true) / 1e3);
|
|
testValues.push(dataView.getUint32(0, false) / 1e3);
|
|
}
|
|
if (buffer.byteLength >= 2) {
|
|
testValues.push(dataView.getInt16(0, true) / 10);
|
|
testValues.push(dataView.getInt16(0, false) / 10);
|
|
testValues.push(dataView.getUint16(0, true) / 10);
|
|
testValues.push(dataView.getUint16(0, false) / 10);
|
|
testValues.push(dataView.getInt16(0, true) / 100);
|
|
testValues.push(dataView.getInt16(0, false) / 100);
|
|
testValues.push(dataView.getInt16(0, true) / 1e3);
|
|
testValues.push(dataView.getInt16(0, false) / 1e3);
|
|
}
|
|
if (buffer.byteLength >= 3) {
|
|
const byte1 = dataView.getUint8(0);
|
|
const byte2 = dataView.getUint8(1);
|
|
const byte3 = dataView.getUint8(2);
|
|
testValues.push((byte1 << 16 | byte2 << 8 | byte3) / 1e3);
|
|
testValues.push((byte3 << 16 | byte2 << 8 | byte1) / 1e3);
|
|
testValues.push((byte2 << 16 | byte1 << 8 | byte3) / 1e3);
|
|
}
|
|
console.log("Testing binary values:", testValues.filter((v) => v > 0 && v < 1e3));
|
|
for (const value of testValues) {
|
|
if (value > 0 && value < 1e3 && !isNaN(value)) {
|
|
console.log("Parsed from binary:", value);
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
if (buffer.byteLength >= 4) {
|
|
const bytes = new Uint8Array(buffer);
|
|
let bcdValue = "";
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
const high = bytes[i] >> 4 & 15;
|
|
const low = bytes[i] & 15;
|
|
if (high >= 0 && high <= 9) bcdValue += high.toString();
|
|
if (low >= 0 && low <= 9) bcdValue += low.toString();
|
|
}
|
|
console.log("BCD value:", bcdValue);
|
|
if (bcdValue.length >= 1) {
|
|
let weight = parseFloat(bcdValue) / 1e3;
|
|
if (weight > 0 && weight < 1e3) {
|
|
console.log("Parsed from BCD:", weight);
|
|
return weight;
|
|
}
|
|
const dotIndex = bcdValue.length - 3;
|
|
if (dotIndex > 0) {
|
|
const formatted = bcdValue.slice(0, dotIndex) + "." + bcdValue.slice(dotIndex);
|
|
weight = parseFloat(formatted);
|
|
if (weight > 0 && weight < 1e3) {
|
|
console.log("Parsed from BCD with decimal:", weight);
|
|
return weight;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
console.log("No valid weight found, returning 0");
|
|
return 0;
|
|
};
|
|
const arrayBufferToString = (buffer) => {
|
|
try {
|
|
const bytes = new Uint8Array(buffer);
|
|
let result = "";
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
if (bytes[i] >= 32 && bytes[i] <= 126) {
|
|
result += String.fromCharCode(bytes[i]);
|
|
} else if (bytes[i] === 10 || bytes[i] === 13) {
|
|
result += bytes[i] === 10 ? "\n" : "\r";
|
|
}
|
|
}
|
|
return result;
|
|
} catch {
|
|
return "";
|
|
}
|
|
};
|
|
const arrayBufferToHex = (buffer) => {
|
|
const bytes = new Uint8Array(buffer);
|
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(" ");
|
|
};
|
|
const disconnectDevice = () => {
|
|
return new Promise((resolve, reject) => {
|
|
if (readInterval) {
|
|
clearInterval(readInterval);
|
|
readInterval = null;
|
|
console.log("Stopped periodic read");
|
|
}
|
|
if (!deviceId) {
|
|
resolve();
|
|
return;
|
|
}
|
|
common_vendor.index.closeBLEConnection({
|
|
deviceId,
|
|
success: () => {
|
|
console.log("Disconnected successfully");
|
|
deviceId = "";
|
|
serviceId = "";
|
|
characteristicId = "";
|
|
writeCharacteristicId = "";
|
|
weightCallback = null;
|
|
resolve();
|
|
},
|
|
fail: (err) => {
|
|
console.log("Disconnect failed but clearing state:", err.errMsg);
|
|
deviceId = "";
|
|
serviceId = "";
|
|
characteristicId = "";
|
|
writeCharacteristicId = "";
|
|
weightCallback = null;
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
exports.connectDevice = connectDevice;
|
|
exports.disconnectDevice = disconnectDevice;
|
|
exports.initBluetooth = initBluetooth;
|
|
exports.listenWeightData = listenWeightData;
|
|
exports.startDiscovery = startDiscovery;
|
|
exports.stopDiscovery = stopDiscovery;
|