From 200224f0d6bbd2cad3dd63c9e79cbaa8227af57c Mon Sep 17 00:00:00 2001 From: lambda Date: Mon, 15 Sep 2025 00:25:14 +0300 Subject: [PATCH] Connect HR using core class --- index.html | 1 + js/device/capabilities/control.js | 2 +- js/device/capabilities/core.js | 2 +- js/device/capabilities/cyclingPower.js | 2 +- js/device/capabilities/index.js | 6 +- js/device/capabilities/sensors.js | 4 +- js/device/index.js | 8 +- js/main.js | 113 +++++++++++++++++++++---- 8 files changed, 112 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index 9fed44b..2df752e 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ + diff --git a/js/device/capabilities/control.js b/js/device/capabilities/control.js index 20b9425..2861772 100644 --- a/js/device/capabilities/control.js +++ b/js/device/capabilities/control.js @@ -1,4 +1,4 @@ -import { deepFreeze, isFiniteNumber } from "../../utils"; +import { deepFreeze, isFiniteNumber } from "../../utils/index.js"; import { CyclingPowerCapabilities } from "./cyclingPower.js"; export class Control { diff --git a/js/device/capabilities/core.js b/js/device/capabilities/core.js index 0a3a010..7463056 100644 --- a/js/device/capabilities/core.js +++ b/js/device/capabilities/core.js @@ -1,4 +1,4 @@ -import { deepFreeze } from "../../utils"; +import { deepFreeze } from "../../utils/index.js"; export class Core { constructor({ diff --git a/js/device/capabilities/cyclingPower.js b/js/device/capabilities/cyclingPower.js index 26443d0..c7008d5 100644 --- a/js/device/capabilities/cyclingPower.js +++ b/js/device/capabilities/cyclingPower.js @@ -1,4 +1,4 @@ -import { deepFreeze } from "../../utils"; +import { deepFreeze } from "../../utils/index.js"; export class CyclingPowerCapabilities { constructor({ controlOpcodes = [], supportsCrankTorque = false } = {}) { diff --git a/js/device/capabilities/index.js b/js/device/capabilities/index.js index 7433834..e1cc785 100644 --- a/js/device/capabilities/index.js +++ b/js/device/capabilities/index.js @@ -1,5 +1,5 @@ -import { Control } from "./control"; -import { Sensors } from "./sensors"; -import { Core } from "./core"; +import { Control } from "./control.js"; +import { Sensors } from "./sensors.js"; +import { Core } from "./core.js"; export { Control, Sensors, Core }; diff --git a/js/device/capabilities/sensors.js b/js/device/capabilities/sensors.js index b1de6d2..c2f1e8d 100644 --- a/js/device/capabilities/sensors.js +++ b/js/device/capabilities/sensors.js @@ -1,5 +1,5 @@ -import { deepFreeze, isFiniteNumber } from "../../utils"; -import { validateCadenceQuality } from "../cadence"; +import { deepFreeze, isFiniteNumber } from "../../utils/index.js"; +import { validateCadenceQuality } from "../cadence.js"; export class Sensors { constructor({ diff --git a/js/device/index.js b/js/device/index.js index 9a3b639..b34c7b2 100644 --- a/js/device/index.js +++ b/js/device/index.js @@ -1,5 +1,5 @@ -import { Control, Sensors, Core } from "./capabilities"; -import { deepFreeze } from "../utils"; +import { Control, Sensors, Core } from "./capabilities/index.js"; +import { deepFreeze } from "../utils/index.js"; const DeviceType = Object.freeze({ HEART_RATE: "heart_rate", @@ -8,7 +8,7 @@ const DeviceType = Object.freeze({ UNKNOWN: "unknown", }); -export class Device { +class Device { constructor({ id, name, @@ -64,3 +64,5 @@ export class Device { }), ); } + +export { Device, Core }; diff --git a/js/main.js b/js/main.js index 7ac5a42..33d4839 100644 --- a/js/main.js +++ b/js/main.js @@ -1,24 +1,107 @@ +import { Core } from "./device/index.js"; + const startButton = document.getElementById("start"); +const hrButton = document.getElementById("hr"); -console.log(navigator.bluetooth); +hrButton.addEventListener("click", async () => { + const hrOptions = { + // Show only devices that advertise the Heart Rate service (0x180D) + filters: [{ services: [0x180d] }], -const devices = await navigator.bluetooth.getDevices(); + // After the user picks a device we also want to read the Device + // Information service (optional, no extra prompt) + optionalServices: [0x180a], // Device Information + }; + + 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); + } + + try { + const device = await navigator.bluetooth.requestDevice(hrOptions); + const server = await device.gatt.connect(); + + const partialCore = new Core({ + supportsBattery: true, + supportsManufacturerData: true, + gattServices: [ + 0x180a, // Device Information + 0x180d, // Heart Rate + ], + gattCharacteristics: [ + 0x2a29, // Manufacturer Name + 0x2a24, // Model Number + 0x2a25, // Serial number + 0x2a26, // Firmware version + 0x2a37, // Heart Rate Measurement + ], + }); + + const info = await server.getPrimaryService(partialCore.gattServices[0]); + const [manufacturer, model, firmwareVersion] = await Promise.all([ + info.getCharacteristic(0x2a29).then((c) => c.readValue()), + info.getCharacteristic(0x2a24).then((c) => c.readValue()), + info.getCharacteristic(0x2a26).then((c) => c.readValue()), + //info.getCharacteristic(0x2a25).then((c) => c.readValue()), + ]); -devices.forEach(async (device) => { - const gattServer = await device.gatt.connect(); - const primaryService = await gattServer.getPrimaryService(0x1800); - const MANUFACTURER_UUID = 0x2a29; - const MODEL_UUID = 0x2a0a; - const char = await primaryService.getCharacteristic(MODEL_UUID); - const val = await char.readValue(); - //const char2 = await primaryService.getCharacteristic(MODEL_UUID); - //const val2 = await char2.readValue(); - const decoder = new TextDecoder("utf-8"); - const manufacturer = decoder.decode(val); - //const model = decoder.decode(val2); - console.log("Manufacturer:", manufacturer); + // const [infoService, hrService] = await Promise.all([ + // server.getPrimaryService(0x180a), // Device Information + // server.getPrimaryService(0x180d), // Heart Rate + // ]); + + // const [manufVal, modelVal, hrChar] = await Promise.all([ + // infoService.getCharacteristic(0x2a29).then((c) => c.readValue()), // Manufacturer Name (0x2A29) + // infoService.getCharacteristic(0x2a24).then((c) => c.readValue()), // Model Number (0x2A24) + // hrService + // .getCharacteristic(0x2a37) + // .then((c) => c.startNotifications().then(() => c)), // Heart Rate Measurement + // ]); + + const dec = new TextDecoder("utf-8"); + + const core = partialCore.merge({ + manufacturer: dec.decode(manufacturer), + model: dec.decode(model), + firmwareVersion: dec.decode(firmwareVersion), + //serial: dec.decode(serial), + }); + + console.log(core); + + // hrChar.addEventListener("characteristicvaluechanged", (ev) => + // console.log("❤️", parseHeartRate(ev.target.value), "bpm"), + // ); + } catch (error) { + console.error(error); + } }); +const devices = await navigator.bluetooth.getDevices(); +console.log("CONNECTED", devices); + +// devices.forEach(async (device) => { +// const gattServer = await device.gatt.connect(); +// const primaryService = await gattServer.getPrimaryService(0x1800); +// const MANUFACTURER_UUID = 0x2a29; +// const MODEL_UUID = 0x2a0a; +// const char = await primaryService.getCharacteristic(MODEL_UUID); +// const val = await char.readValue(); +// //const char2 = await primaryService.getCharacteristic(MODEL_UUID); +// //const val2 = await char2.readValue(); +// const decoder = new TextDecoder("utf-8"); +// const manufacturer = decoder.decode(val); +// //const model = decoder.decode(val2); +// console.log("Manufacturer:", manufacturer); +// }); + startButton.addEventListener("click", async () => { try { const devices = await navigator.bluetooth.getDevices();