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

378 lines 16.8 kB
import { Device } from "../device.model.js"; /** * Represents a Griptonite Motherboard device. * {@link https://griptonite.io} */ export class Motherboard extends Device { /** * Length of the packet received from the device. * @type {number} * @static * @readonly * @constant */ static packetLength = 32; /** * Number of samples contained in the data packet. * @type {number} * @static * @readonly * @constant */ static samplesNumber = 3; /** * Buffer to store received data from the device. * @type {number[]} * @private */ receiveBuffer = []; /** * Calibration data for each sensor of the device. * @type {number[][][]} * @private */ calibrationData = [[], [], [], []]; constructor() { super({ filters: [{ name: "Motherboard" }], services: [ { name: "Device Information", id: "device", uuid: "0000180a-0000-1000-8000-00805f9b34fb", characteristics: [ // { // name: 'Serial Number String (Blocked)', // id: 'serial' // uuid: '00002a25-0000-1000-8000-00805f9b34fb' // }, { name: "Firmware Revision String", id: "firmware", uuid: "00002a26-0000-1000-8000-00805f9b34fb", }, { name: "Hardware Revision String", id: "hardware", uuid: "00002a27-0000-1000-8000-00805f9b34fb", }, { name: "Manufacturer Name String", id: "manufacturer", uuid: "00002a29-0000-1000-8000-00805f9b34fb", }, ], }, { name: "Battery Service", id: "battery", uuid: "0000180f-0000-1000-8000-00805f9b34fb", characteristics: [ { name: "Battery Level", id: "level", uuid: "00002a19-0000-1000-8000-00805f9b34fb", }, ], }, { name: "LED Service", id: "led", uuid: "10ababcd-15e1-28ff-de13-725bea03b127", characteristics: [ { name: "Red LED", id: "red", uuid: "10ab1524-15e1-28ff-de13-725bea03b127", }, { name: "Green LED", id: "green", uuid: "10ab1525-15e1-28ff-de13-725bea03b127", }, ], }, { name: "UART Nordic Service", id: "uart", uuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e", characteristics: [ { name: "TX", id: "tx", uuid: "6e400002-b5a3-f393-e0a9-e50e24dcca9e", }, { name: "RX", id: "rx", uuid: "6e400003-b5a3-f393-e0a9-e50e24dcca9e", }, ], }, ], commands: { GET_SERIAL: "#", START_WEIGHT_MEAS: "S30", STOP_WEIGHT_MEAS: "", // All commands will stop the data stream. GET_CALIBRATION: "C", SLEEP: 0, GET_TEXT: "T", DEBUG_STREAM: "D", }, }); } /** * Applies calibration to a sample value. * @param {number} sample - The sample value to calibrate. * @param {number[][]} calibration - The calibration data. * @returns {number} The calibrated sample value. */ applyCalibration = (sample, calibration) => { // Extract the calibrated value for the zero point const zeroCalibration = calibration[0][2]; // Initialize sign as positive let sign = 1; // Initialize the final calibrated value let final = 0; // If the sample value is less than the zero calibration point if (sample < zeroCalibration) { // Change the sign to negative sign = -1; // Reflect the sample around the zero calibration point sample = /* 2 * zeroCalibration */ -sample; } // Iterate through the calibration data for (let i = 1; i < calibration.length; i++) { // Extract the lower and upper bounds of the current calibration range const calibrationStart = calibration[i - 1][2]; const calibrationEnd = calibration[i][2]; // If the sample value is within the current calibration range if (sample < calibrationEnd) { // Interpolate to get the calibrated value within the range final = calibration[i - 1][1] + ((sample - calibrationStart) / (calibrationEnd - calibrationStart)) * (calibration[i][1] - calibration[i - 1][1]); break; } } // Return the calibrated value with the appropriate sign (positive/negative) return sign * final; }; /** * Retrieves battery or voltage information from the device. * @returns {Promise<string | undefined>} A Promise that resolves with the battery or voltage information, */ battery = async () => { return await this.read("battery", "level", 250); }; /** * Writes a command to get calibration data from the device. * @returns {Promise<void>} A Promise that resolves when the command is successfully sent. */ calibration = async () => { await this.write("uart", "tx", this.commands.GET_CALIBRATION, 2500, (data) => { console.log(data); }); }; /** * Retrieves firmware version from the device. * @returns {Promise<string>} A Promise that resolves with the firmware version, */ firmware = async () => { return await this.read("device", "firmware", 250); }; /** * Handles data received from the Motherboard device. Processes hex-encoded streaming packets * to extract samples, calibrate masses, and update running averages of mass data. * If the received data is not a valid hex packet, it returns the unprocessed data. * * @param {DataView} value - The notification event. */ handleNotifications = (value) => { if (value) { // Update timestamp this.updateTimestamp(); if (value.buffer) { for (let i = 0; i < value.byteLength; i++) { this.receiveBuffer.push(value.getUint8(i)); } let idx; while ((idx = this.receiveBuffer.indexOf(10)) >= 0) { const line = this.receiveBuffer.splice(0, idx + 1).slice(0, -1); // Combine and remove LF if (line.length > 0 && line[line.length - 1] === 13) line.pop(); // Remove CR const decoder = new TextDecoder("utf-8"); const receivedData = decoder.decode(new Uint8Array(line)); const receivedTime = Date.now(); // Check if the line is entirely hex characters const isAllHex = /^[0-9A-Fa-f]+$/g.test(receivedData); // Handle streaming packet if (isAllHex && receivedData.length === Motherboard.packetLength) { // Base-16 decode the string: convert hex pairs to byte values const bytes = Array.from({ length: receivedData.length / 2 }, (_, i) => Number(`0x${receivedData.substring(i * 2, i * 2 + 2)}`)); // Translate header into packet, number of samples from the packet length const packet = { received: receivedTime, sampleNum: new DataView(new Uint8Array(bytes).buffer).getUint16(0, true), battRaw: new DataView(new Uint8Array(bytes).buffer).getUint16(2, true), samples: [], masses: [], }; const dataView = new DataView(new Uint8Array(bytes).buffer); for (let i = 0; i < Motherboard.samplesNumber; i++) { const sampleStart = 4 + 3 * i; // Use DataView to read the 24-bit unsigned integer const rawValue = dataView.getUint8(sampleStart) | (dataView.getUint8(sampleStart + 1) << 8) | (dataView.getUint8(sampleStart + 2) << 16); // Ensure unsigned 32-bit integer packet.samples[i] = rawValue >>> 0; if (packet.samples[i] >= 0x7fffff) { packet.samples[i] -= 0x1000000; } packet.masses[i] = this.applyCalibration(packet.samples[i], this.calibrationData[i]); } // invert center and right values packet.masses[1] *= -1; packet.masses[2] *= -1; // Add data to downloadable Array this.downloadPackets.push({ received: packet.received, sampleNum: packet.battRaw, battRaw: packet.received, samples: [...packet.samples], masses: [...packet.masses], }); let left = packet.masses[0]; let center = packet.masses[1]; let right = packet.masses[2]; // Tare correction left -= this.applyTare(left); center -= this.applyTare(center); right -= this.applyTare(right); this.massMax = Math.max(Number(this.massMax), Math.max(-1000, left + center + right)).toFixed(1); // Update running sum and count const currentMassTotal = Math.max(-1000, left + center + right); 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(center); // Notify with weight data this.notifyCallback({ massTotal: Math.max(-1000, left + center + right).toFixed(1), massMax: this.massMax, massAverage: this.massAverage, massLeft: Math.max(-1000, packet.masses[0]).toFixed(1), massCenter: Math.max(-1000, packet.masses[1]).toFixed(1), massRight: Math.max(-1000, packet.masses[2]).toFixed(1), }); } else if (this.writeLast === this.commands.GET_CALIBRATION) { // check data integrity if ((receivedData.match(/,/g) || []).length === 3) { const parts = receivedData.split(","); const numericParts = parts.map((x) => parseFloat(x)); this.calibrationData[numericParts[0]].push(numericParts.slice(1)); } } else { // unhandled data this.writeCallback(receivedData); } } } } }; /** * Retrieves hardware version from the device. * @returns {Promise<string>} A Promise that resolves with the hardware version, */ hardware = async () => { return await this.read("device", "hardware", 250); }; /** * Sets the LED color based on a single color option. Defaults to turning the LEDs off if no configuration is provided. * @param {"green" | "red" | "orange"} [config] - Optional color or array of climb placements for the LEDs. Ignored if placements are provided. * @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard. */ led = async (config) => { if (this.isConnected()) { const colorMapping = { green: [[0x00], [0x01]], red: [[0x01], [0x00]], orange: [[0x01], [0x01]], off: [[0x00], [0x00]], }; // Default to "off" color if config is not set or not found in colorMapping const color = typeof config === "string" && colorMapping[config] ? config : "off"; const [redValue, greenValue] = colorMapping[color]; await this.write("led", "red", new Uint8Array(redValue)); await this.write("led", "green", new Uint8Array(greenValue), 1250); } return undefined; }; /** * Retrieves manufacturer information from the device. * @returns {Promise<string>} A Promise that resolves with the manufacturer information, */ manufacturer = async () => { return await this.read("device", "manufacturer", 250); }; /** * Retrieves serial number from the device. * @returns {Promise<string>} A Promise that resolves with the serial number, */ serial = async () => { let response = undefined; await this.write("uart", "tx", this.commands.GET_SERIAL, 250, (data) => { response = data; }); return response; }; /** * Stops the data stream on the specified device. * @returns {Promise<void>} A promise that resolves when the stream is stopped. */ stop = async () => { await this.write("uart", "tx", this.commands.STOP_WEIGHT_MEAS, 0); }; /** * Starts streaming data from the specified device. * @param {number} [duration=0] - The duration of the stream in milliseconds. If set to 0, stream will continue indefinitely. * @returns {Promise<void>} A promise that resolves when the streaming operation is completed. */ stream = async (duration = 0) => { // Reset download packets this.downloadPackets.length = 0; // Read calibration data if not already available if (!this.calibrationData[0].length) { await this.calibration(); } // Start streaming data await this.write("uart", "tx", this.commands.START_WEIGHT_MEAS, duration); // Stop streaming if duration is set if (duration !== 0) { await this.stop(); } }; /** * Retrieves the entire 320 bytes of non-volatile memory from the device. * * The memory consists of 10 segments, each 32 bytes long. If any segment was previously written, * the corresponding data will appear in the response. Unused portions of the memory are * padded with whitespace. * * @returns {Promise<string>} A Promise that resolves with the 320-byte memory content as a string, */ text = async () => { let response = undefined; await this.write("uart", "tx", this.commands.GET_TEXT, 250, (data) => { response = data; }); return response; }; } //# sourceMappingURL=motherboard.model.js.map