|
|
// 1️⃣ Request a HR‑monitor device
|
|
|
const hrOptions = {
|
|
|
// Show only devices that advertise the Heart Rate service (0x180D)
|
|
|
filters: [{ services: [0x180d] }],
|
|
|
|
|
|
// After the user picks a device we also want to read the Device
|
|
|
// Information service (optional, no extra prompt)
|
|
|
optionalServices: [0x180a], // Device Information
|
|
|
};
|
|
|
|
|
|
navigator.bluetooth
|
|
|
.requestDevice(hrOptions)
|
|
|
.then((device) => {
|
|
|
console.log("✅ Selected:", device.name);
|
|
|
// 2️⃣ Connect to the GATT server
|
|
|
return device.gatt.connect();
|
|
|
})
|
|
|
.then((server) => {
|
|
|
// 3️⃣ Get the Heart Rate service
|
|
|
return server.getPrimaryService(0x180d);
|
|
|
})
|
|
|
.then((hrService) => {
|
|
|
// 4️⃣ Get the Heart Rate Measurement characteristic (0x2A37)
|
|
|
return hrService.getCharacteristic(0x2a37);
|
|
|
})
|
|
|
.then((hrChar) => {
|
|
|
// 5️⃣ Enable notifications – the monitor will push new readings
|
|
|
return hrChar.startNotifications().then(() => hrChar);
|
|
|
})
|
|
|
.then((hrChar) => {
|
|
|
console.log("🔔 Listening for heart‑rate measurements...");
|
|
|
hrChar.addEventListener("characteristicvaluechanged", (ev) => {
|
|
|
const value = ev.target.value; // DataView
|
|
|
const heartRate = parseHeartRate(value);
|
|
|
console.log("❤️ Heart Rate:", heartRate, "bpm");
|
|
|
});
|
|
|
})
|
|
|
.catch((err) => console.error("❌ Bluetooth error:", err));
|
|
|
|
|
|
/**
|
|
|
* Parse the Heart Rate Measurement characteristic (Bluetooth SIG spec).
|
|
|
* Returns the BPM as a Number.
|
|
|
*/
|
|
|
function parseHeartRate(dataView) {
|
|
|
// Flags are in the first byte
|
|
|
const flags = dataView.getUint8(0);
|
|
|
const hrFormatUint16 = flags & 0x01; // 0 = 8‑bit, 1 = 16‑bit
|
|
|
|
|
|
if (hrFormatUint16) {
|
|
|
return dataView.getUint16(1, /*littleEndian=*/ true);
|
|
|
}
|
|
|
return dataView.getUint8(1);
|
|
|
}
|
|
|
|
|
|
// After the HR connection succeeds, you can also fetch the Device Info service:
|
|
|
navigator.bluetooth
|
|
|
.requestDevice(hrOptions) // reuse the same options
|
|
|
.then((d) => d.gatt.connect())
|
|
|
.then((server) =>
|
|
|
Promise.all([
|
|
|
server.getPrimaryService(0x180a), // Device Information
|
|
|
server.getPrimaryService(0x180d), // Heart Rate
|
|
|
]),
|
|
|
)
|
|
|
.then(([infoService, hrService]) =>
|
|
|
Promise.all([
|
|
|
// Manufacturer Name (0x2A29)
|
|
|
infoService.getCharacteristic(0x2a29).then((c) => c.readValue()),
|
|
|
// Model Number (0x2A24)
|
|
|
infoService.getCharacteristic(0x2a24).then((c) => c.readValue()),
|
|
|
// Heart Rate Measurement (as before)
|
|
|
hrService
|
|
|
.getCharacteristic(0x2a37)
|
|
|
.then((c) => c.startNotifications().then(() => c)),
|
|
|
]),
|
|
|
)
|
|
|
.then(([manufVal, modelVal, hrChar]) => {
|
|
|
const dec = new TextDecoder("utf-8");
|
|
|
console.log("🏭 Manufacturer:", dec.decode(manufVal));
|
|
|
console.log("📦 Model:", dec.decode(modelVal));
|
|
|
|
|
|
hrChar.addEventListener("characteristicvaluechanged", (ev) =>
|
|
|
console.log("❤️", parseHeartRate(ev.target.value), "bpm"),
|
|
|
);
|
|
|
})
|
|
|
.catch(console.error);
|