UNPKG

@hangtime/grip-connect

Version:

Griptonite Motherboard, Tindeq Progressor, PitchSix Force Board, WHC-06, Entralpi, Climbro, mySmartBoard: Bluetooth API Force-Sensing strength analysis for climbers

195 lines (172 loc) 6.82 kB
import { Device } from "../device.model.js" import type { IWHC06 } from "../../interfaces/device/wh-c06.interface.js" /** * Represents a Weiheng - WH-C06 (or MAT Muscle Meter) device. * To use this device enable: `chrome://flags/#enable-experimental-web-platform-features`. * {@link https://googlechrome.github.io/samples/web-bluetooth/scan.html| Web Bluetooth} * {@link https://weihengmanufacturer.com} */ export class WHC06 extends Device implements IWHC06 { /** * Offset for the byte location in the manufacturer data to extract the weight. * @type {number} * @static * @readonly * @constant */ private static readonly weightOffset: number = 10 /** * Company identifier for WH-C06, also used by 'TomTom International BV': https://www.bluetooth.com/specifications/assigned-numbers/ * @type {number} * @static * @readonly * @constant */ private static readonly manufacturerId: number = 256 /** * To track disconnection timeout. * @type {number|null} * @private */ private advertisementTimeout: ReturnType<typeof setTimeout> | null = null /** * The limit in seconds when timeout is triggered * @type {number} * @private * @readonly */ private readonly advertisementTimeoutTime: number = 10 // /** // * Offset for the byte location in the manufacturer data to determine weight stability. // * @type {number} // * @static // * @readonly // * @constant // */ // private static readonly stableOffset: number = 14 constructor() { super({ filters: [ { // name: "IF_B7", manufacturerData: [ { companyIdentifier: 0x0100, // 256 }, ], }, ], services: [], }) } /** * Connects to a Bluetooth device. * @param {Function} [onSuccess] - Optional callback function to execute on successful connection. Default logs success. * @param {Function} [onError] - Optional callback function to execute on error. Default logs the error. */ override connect = async ( onSuccess: () => void = () => console.log("Connected successfully"), onError: (error: Error) => void = (error) => console.error(error), ): Promise<void> => { try { // Only data matching the optionalManufacturerData parameter to requestDevice is included in the advertisement event: https://github.com/WebBluetoothCG/web-bluetooth/issues/598 const optionalManufacturerData = this.filters.flatMap( (filter) => filter.manufacturerData?.map((data) => data.companyIdentifier) || [], ) const bluetooth = await this.getBluetooth() this.bluetooth = await bluetooth.requestDevice({ filters: this.filters, optionalManufacturerData, }) if (!this.bluetooth.gatt) { throw new Error("GATT is not available on this device") } // Update timestamp this.updateTimestamp() // Device has no services / characteristics, so we directly call onSuccess onSuccess() this.bluetooth.addEventListener("advertisementreceived", (event) => { const data = event.manufacturerData.get(WHC06.manufacturerId) if (data) { // Handle recieved data const weight = (data.getUint8(WHC06.weightOffset) << 8) | data.getUint8(WHC06.weightOffset + 1) // const stable = (data.getUint8(STABLE_OFFSET) & 0xf0) >> 4 // const unit = data.getUint8(STABLE_OFFSET) & 0x0f const receivedTime: number = Date.now() const receivedData = weight / 100 const numericData = receivedData - this.applyTare(receivedData) * -1 // Add data to downloadable Array this.downloadPackets.push({ received: receivedTime, sampleNum: this.dataPointCount, battRaw: 0, samples: [numericData], masses: [numericData], }) // Update massMax this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1) // Update running sum and count const currentMassTotal = Math.max(-1000, numericData) this.massTotalSum += currentMassTotal this.dataPointCount++ // Calculate the average dynamically this.massAverage = (this.massTotalSum / this.dataPointCount).toFixed(1) // Check if device is being used this.activityCheck(numericData) // Notify with weight data this.notifyCallback({ massMax: this.massMax, massAverage: this.massAverage, massTotal: Math.max(-1000, numericData).toFixed(1), }) } // Reset "still advertising" counter this.resetAdvertisementTimeout() }) // When the companyIdentifier is provided we want to get manufacturerData using watchAdvertisements. if (optionalManufacturerData.length) { // Receive events when the system receives an advertisement packet from a watched device. // To use this function in Chrome: chrome://flags/#enable-experimental-web-platform-features has to be enabled. // More info: https://chromestatus.com/feature/5180688812736512 if (typeof this.bluetooth.watchAdvertisements === "function") { await this.bluetooth.watchAdvertisements() } else { throw new Error( "watchAdvertisements isn't supported. For Chrome, enable it at chrome://flags/#enable-experimental-web-platform-features.", ) } } } catch (error) { onError(error as Error) } } /** * Custom check if a Bluetooth device is connected. * For the WH-C06 device, the `gatt.connected` property remains `false` even after the device is connected. * @returns {boolean} A boolean indicating whether the device is connected. */ override isConnected = (): boolean => { return !!this.bluetooth } /** * Resets the timeout that checks if the device is still advertising. */ private resetAdvertisementTimeout = (): void => { // Clear the previous timeout if (this.advertisementTimeout) { clearTimeout(this.advertisementTimeout) } // Set a new timeout to stop tracking if no advertisement is received this.advertisementTimeout = globalThis.setTimeout(() => { // Mimic a disconnect const disconnectedEvent = new Event("gattserverdisconnected") Object.defineProperty(disconnectedEvent, "target", { value: this.bluetooth, writable: false, }) // Print error to the console console.error(`No advertisement received for ${this.advertisementTimeoutTime} seconds, stopping tracking..`) this.onDisconnected(disconnectedEvent) }, this.advertisementTimeoutTime * 1000) // 10 seconds } }