@hangtime/grip-connect 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,23 +2,42 @@
2
2
 
3
3
  **Force-Sensing Climbing Training**
4
4
 
5
- The objective of this project is to create a client that can establish connections with various Force-Sensing Hangboards
6
- / Plates used by climbers for strength measurement. Examples of such hangboards include the
5
+ The objective of this project is to create a Web Bluetooth API client that can establish connections with various
6
+ Force-Sensing Hangboards / Plates used by climbers for strength measurement. Examples of such hangboards include the
7
7
  [Motherboard](https://griptonite.io/shop/motherboard/), [Climbro](https://climbro.com/),
8
8
  [SmartBoard](https://www.smartboard-climbing.com/), [Entralpi](https://entralpi.com/) or
9
9
  [Tindeq Progressor](https://tindeq.com/)
10
10
 
11
+ [Try it out](https://grip-connect.vercel.app/) - [Docs](https://stevie-ray.github.io/hangtime-grip-connect/) -
12
+ [Browser Support](https://caniuse.com/web-bluetooth)
13
+
11
14
  ## Roadmap
12
15
 
13
- - ➡️ Connect with devices
14
- - Griptonte Motherboard
15
- - Tindeq Progressor
16
- - Entralpi
17
- - ➡️ Climbro
18
- - ➡️ SmartBoard
19
- - Read / Write / Notify using Bluetooth
20
- - ➡️ Calibrate Devices
21
- - ➡️ Output weight/force stream
16
+ - Griptonite Motherboard
17
+ - ✅️ Connect with devices
18
+ - ✅️ Read / Write / Notify using Bluetooth
19
+ - ➡️ Calibrate Devices
20
+ - ✅️ Output weight/force stream
21
+ - Tindeq Progressor
22
+ - ✅️ Connect with devices
23
+ - ✅️ Read / Write / Notify using Bluetooth
24
+ - ➡️ Calibrate Devices
25
+ - ➡️ Output weight/force stream
26
+ - ✅ Entralpi
27
+ - ✅️ Connect with devices
28
+ - ✅️ Read / Write / Notify using Bluetooth
29
+ - ➡️ Calibrate Devices
30
+ - ➡️ Output weight/force stream
31
+ - ➡️ Climbro
32
+ - ➡️ Connect with devices
33
+ - ➡️ Read / Write / Notify using Bluetooth
34
+ - ➡️ Calibrate Devices
35
+ - ➡️ Output weight/force stream
36
+ - ➡️ SmartBoard
37
+ - ➡️ Connect with devices
38
+ - ➡️ Read / Write / Notify using Bluetooth
39
+ - ➡️ Calibrate Devices
40
+ - ➡️ Output weight/force stream
22
41
 
23
42
  ## Development
24
43
 
@@ -61,25 +80,32 @@ motherboardButton.addEventListener("click", () => {
61
80
  await read(Motherboard, "device", "hardware", 1000)
62
81
  await read(Motherboard, "device", "firmware", 1000)
63
82
 
64
- // Calibrate?
65
- await write(Motherboard, "uart", "tx", "C", 10000)
66
-
67
- // Read stream?
68
- await write(Motherboard, "unknown", "01", "1", 2500)
69
- await write(Motherboard, "unknown", "02", "0", 2500)
70
- await write(Motherboard, "uart", "tx", "S30", 5000)
83
+ // read calibration (required before reading data)
84
+ await write(Motherboard, "uart", "tx", "C", 5000)
71
85
 
72
- // Read stream (2x)?
73
- await write(Motherboard, "unknown", "01", "0", 2500)
74
- await write(Motherboard, "unknown", "02", "1", 2500)
75
- await write(Motherboard, "uart", "tx", "S30", 5000)
86
+ // start stream
87
+ await write(Motherboard, "uart", "tx", "S30", 15000)
76
88
 
89
+ // end stream
90
+ await write(Motherboard, "uart", "tx", "", 0)
77
91
  // disconnect from device after we are done
78
92
  disconnect(Motherboard)
79
93
  })
80
94
  })
81
95
  ```
82
96
 
97
+ ## Credits
98
+
99
+ A special thank you to:
100
+
101
+ - [@CassimLadha](https://github.com/CassimLadha) for sharing insights on reading the Motherboards data.
102
+ - [@donaldharvey](https://github.com/donaldharvey) for a valuable example on connecting to the motherboard.
103
+
104
+ ## Disclamer
105
+
106
+ THIS SOFTWARE IS NOT OFFICIALY SUPPORTED, SUPPLIED OR MAINTAINED BY THE DEVICE MANUFACTURER. BY USING THE SOFTWARE YOU
107
+ ARE ACKNOWLEDGEING THIS AND UNDERSTAND THAT USING THIS SOFTWARE WILL INVALIDATE THE MANUFACTURERS WARRANTY.
108
+
83
109
  ## License
84
110
 
85
- MIT © [Stevie-Ray Hartog](https://github.com/Stevie-Ray)
111
+ BSD 2-Clause © [Stevie-Ray Hartog](https://github.com/Stevie-Ray)
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@hangtime/grip-connect",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A client that can establish connections with various Force-Sensing Hangboards/Plates used by climbers for strength measurement. Examples of such hangboards include the Motherboard, Climbro, SmartBoard, Entralpi or Tindeq Progressor",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
7
7
  "build": "tsc --build"
8
8
  },
9
9
  "author": "Stevie-Ray Hartog <mail@stevie-ray.nl>",
10
- "license": "MIT",
10
+ "license": "BSD-2-Clause",
11
11
  "devDependencies": {},
12
12
  "repository": {
13
13
  "type": "git",
package/src/connect.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { notifyCallback } from "./notify";
2
+ import { handleMotherboardData } from "./devices/moterboard";
2
3
  let server;
4
+ const receiveBuffer = [];
3
5
  /**
4
6
  * onDisconnected
5
7
  * @param board
@@ -17,62 +19,38 @@ const onDisconnected = (event, board) => {
17
19
  */
18
20
  const handleNotifications = (event, board) => {
19
21
  const characteristic = event.target;
20
- const receivedData = new Uint8Array(characteristic.value.buffer);
21
- // Create an array to store the parsed decimal values
22
- const decimalArray = [];
23
- // Iterate through each byte and convert to decimal
24
- for (let i = 0; i < receivedData.length; i++) {
25
- decimalArray.push(receivedData[i]);
26
- }
27
- // Convert the decimal array to a string representation
28
- const receivedString = String.fromCharCode(...decimalArray);
29
- if (board.name === "Motherboard") {
30
- // Split the string into pairs of characters
31
- const hexPairs = receivedString.match(/.{1,2}/g);
32
- // Convert each hexadecimal pair to decimal
33
- const parsedDecimalArray = hexPairs?.map((hexPair) => parseInt(hexPair, 16));
34
- // Handle different types of data
35
- if (characteristic.value.byteLength === 20) {
36
- const elementKeys = [
37
- "frames",
38
- "cycle",
39
- "unknown",
40
- "eleven",
41
- "dynamic1",
42
- "pressure1",
43
- "left",
44
- "dynamic2",
45
- "pressure2",
46
- "right",
47
- ];
48
- const dataObject = {};
49
- if (parsedDecimalArray) {
50
- elementKeys.forEach((key, index) => {
51
- dataObject[key] = parsedDecimalArray[index];
52
- });
22
+ const value = characteristic.value;
23
+ if (value) {
24
+ if (board.name === "Motherboard") {
25
+ if (value) {
26
+ for (let i = 0; i < value.byteLength; i++) {
27
+ receiveBuffer.push(value.getUint8(i));
28
+ }
29
+ let idx;
30
+ while ((idx = receiveBuffer.indexOf(10)) >= 0) {
31
+ const line = receiveBuffer.splice(0, idx + 1).slice(0, -1); // Combine and remove LF
32
+ if (line.length > 0 && line[line.length - 1] === 13)
33
+ line.pop(); // Remove CR
34
+ const decoder = new TextDecoder("utf-8");
35
+ const receivedString = decoder.decode(new Uint8Array(line));
36
+ handleMotherboardData(characteristic.uuid, receivedString);
37
+ }
53
38
  }
39
+ }
40
+ else if (board.name === "ENTRALPI") {
41
+ // TODO: handle Entralpi notify
42
+ // characteristic.value!.getInt16(0) / 100;
54
43
  if (notifyCallback) {
55
- notifyCallback({ uuid: characteristic.uuid, value: dataObject });
44
+ notifyCallback({ uuid: characteristic.uuid, value: value });
56
45
  }
57
46
  }
58
- else if (characteristic.value.byteLength === 14) {
59
- // TODO: handle 14 byte data
60
- // notifyCallback({ uuid: characteristic.uuid, value: characteristic.value!.getInt8(0) / 100 })
61
- }
62
- }
63
- else if (board.name === "ENTRALPI") {
64
- // TODO: handle Entralpi notify
65
- // characteristic.value!.getInt16(0) / 100;
66
- if (notifyCallback) {
67
- notifyCallback({ uuid: characteristic.uuid, value: receivedString });
47
+ else if (board.name === "Tindeq") {
48
+ // TODO: handle Tindeq notify
68
49
  }
69
- }
70
- else if (board.name === "Tindeq") {
71
- // TODO: handle Tindeq notify
72
- }
73
- else {
74
- if (notifyCallback) {
75
- notifyCallback({ uuid: characteristic.uuid, value: receivedString });
50
+ else {
51
+ if (notifyCallback) {
52
+ notifyCallback({ uuid: characteristic.uuid, value: value });
53
+ }
76
54
  }
77
55
  }
78
56
  };
@@ -153,7 +131,7 @@ export const connect = async (board, onSuccess) => {
153
131
  }
154
132
  const device = await navigator.bluetooth.requestDevice({
155
133
  filters: filters,
156
- optionalServices: deviceServices
134
+ optionalServices: deviceServices,
157
135
  });
158
136
  board.device = device;
159
137
  if (!board.device.gatt) {
package/src/connect.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { Device } from "./devices/types"
2
2
  import { notifyCallback } from "./notify"
3
+ import { handleMotherboardData } from "./devices/moterboard"
3
4
 
4
5
  let server: BluetoothRemoteGATTServer
6
+ const receiveBuffer: number[] = []
5
7
 
6
8
  /**
7
9
  * onDisconnected
@@ -20,61 +22,35 @@ const onDisconnected = (event: Event, board: Device): void => {
20
22
  */
21
23
  const handleNotifications = (event: Event, board: Device): void => {
22
24
  const characteristic = event.target as BluetoothRemoteGATTCharacteristic
23
- const receivedData = new Uint8Array(characteristic.value!.buffer)
24
- // Create an array to store the parsed decimal values
25
- const decimalArray: number[] = []
25
+ const value = characteristic.value
26
+ if (value) {
27
+ if (board.name === "Motherboard") {
28
+ if (value) {
29
+ for (let i = 0; i < value.byteLength; i++) {
30
+ receiveBuffer.push(value.getUint8(i))
31
+ }
26
32
 
27
- // Iterate through each byte and convert to decimal
28
- for (let i = 0; i < receivedData.length; i++) {
29
- decimalArray.push(receivedData[i])
30
- }
31
- // Convert the decimal array to a string representation
32
- const receivedString: string = String.fromCharCode(...decimalArray)
33
-
34
- if (board.name === "Motherboard") {
35
- // Split the string into pairs of characters
36
- const hexPairs: RegExpMatchArray | null = receivedString.match(/.{1,2}/g)
37
- // Convert each hexadecimal pair to decimal
38
- const parsedDecimalArray: number[] | undefined = hexPairs?.map((hexPair) => parseInt(hexPair, 16))
39
- // Handle different types of data
40
- if (characteristic.value!.byteLength === 20) {
41
- const elementKeys = [
42
- "frames",
43
- "cycle",
44
- "unknown",
45
- "eleven",
46
- "dynamic1",
47
- "pressure1",
48
- "left",
49
- "dynamic2",
50
- "pressure2",
51
- "right",
52
- ]
53
- const dataObject: { [key: string]: number } = {}
54
-
55
- if (parsedDecimalArray) {
56
- elementKeys.forEach((key: string, index: number) => {
57
- dataObject[key] = parsedDecimalArray[index]
58
- })
33
+ let idx: number
34
+ while ((idx = receiveBuffer.indexOf(10)) >= 0) {
35
+ const line = receiveBuffer.splice(0, idx + 1).slice(0, -1) // Combine and remove LF
36
+ if (line.length > 0 && line[line.length - 1] === 13) line.pop() // Remove CR
37
+ const decoder = new TextDecoder("utf-8")
38
+ const receivedString = decoder.decode(new Uint8Array(line))
39
+ handleMotherboardData(characteristic.uuid, receivedString)
40
+ }
59
41
  }
42
+ } else if (board.name === "ENTRALPI") {
43
+ // TODO: handle Entralpi notify
44
+ // characteristic.value!.getInt16(0) / 100;
60
45
  if (notifyCallback) {
61
- notifyCallback({ uuid: characteristic.uuid, value: dataObject })
46
+ notifyCallback({ uuid: characteristic.uuid, value: value })
47
+ }
48
+ } else if (board.name === "Tindeq") {
49
+ // TODO: handle Tindeq notify
50
+ } else {
51
+ if (notifyCallback) {
52
+ notifyCallback({ uuid: characteristic.uuid, value: value })
62
53
  }
63
- } else if (characteristic.value!.byteLength === 14) {
64
- // TODO: handle 14 byte data
65
- // notifyCallback({ uuid: characteristic.uuid, value: characteristic.value!.getInt8(0) / 100 })
66
- }
67
- } else if (board.name === "ENTRALPI") {
68
- // TODO: handle Entralpi notify
69
- // characteristic.value!.getInt16(0) / 100;
70
- if (notifyCallback) {
71
- notifyCallback({ uuid: characteristic.uuid, value: receivedString })
72
- }
73
- } else if (board.name === "Tindeq") {
74
- // TODO: handle Tindeq notify
75
- } else {
76
- if (notifyCallback) {
77
- notifyCallback({ uuid: characteristic.uuid, value: receivedString })
78
54
  }
79
55
  }
80
56
  }
@@ -97,7 +73,7 @@ const onConnected = async (board: Device, onSuccess: () => void): Promise<void>
97
73
 
98
74
  if (matchingService) {
99
75
  // Android bug: Introduce a delay before getting characteristics
100
- await new Promise((resolve) => setTimeout(resolve, 100));
76
+ await new Promise((resolve) => setTimeout(resolve, 100))
101
77
 
102
78
  const characteristics = await service.getCharacteristics()
103
79
 
@@ -166,7 +142,7 @@ export const connect = async (board: Device, onSuccess: () => void): Promise<voi
166
142
 
167
143
  const device = await navigator.bluetooth.requestDevice({
168
144
  filters: filters,
169
- optionalServices: deviceServices
145
+ optionalServices: deviceServices,
170
146
  })
171
147
 
172
148
  board.device = device
@@ -181,7 +157,7 @@ export const connect = async (board: Device, onSuccess: () => void): Promise<voi
181
157
  board.device.addEventListener("gattserverdisconnected", (event) => onDisconnected(event, board))
182
158
 
183
159
  if (server.connected) {
184
- await onConnected(board, onSuccess);
160
+ await onConnected(board, onSuccess)
185
161
  }
186
162
  } catch (error) {
187
163
  console.error(error)
@@ -1,2 +1,8 @@
1
1
  import { Device } from "./types";
2
2
  export declare const Motherboard: Device;
3
+ /**
4
+ * handleMotherboardData
5
+ * @param uuid - Unique identifier
6
+ * @param receivedString - Received data string
7
+ */
8
+ export declare function handleMotherboardData(uuid: string, receivedString: string): void;
@@ -1,3 +1,7 @@
1
+ import { notifyCallback } from "../notify";
2
+ const PACKET_LENGTH = 32;
3
+ const NUM_SAMPLES = 3;
4
+ const CALIBRATION = [[], [], [], []];
1
5
  export const Motherboard = {
2
6
  name: "Motherboard",
3
7
  companyId: 0x2a29,
@@ -77,3 +81,102 @@ export const Motherboard = {
77
81
  },
78
82
  ],
79
83
  };
84
+ /**
85
+ * applyCalibration
86
+ * @param sample
87
+ * @param calibration
88
+ */
89
+ const applyCalibration = (sample, calibration) => {
90
+ // Extract the calibrated value for the zero point
91
+ const zeroCalibration = calibration[0][2];
92
+ // Initialize sign as positive
93
+ let sign = 1;
94
+ // Initialize the final calibrated value
95
+ let final = 0;
96
+ // If the sample value is less than the zero calibration point
97
+ if (sample < zeroCalibration) {
98
+ // Change the sign to negative
99
+ sign = -1;
100
+ // Reflect the sample around the zero calibration point
101
+ sample = 2 * zeroCalibration - sample;
102
+ }
103
+ // Iterate through the calibration data
104
+ for (let i = 1; i < calibration.length; i++) {
105
+ // Extract the lower and upper bounds of the current calibration range
106
+ const calibrationStart = calibration[i - 1][2];
107
+ const calibrationEnd = calibration[i][2];
108
+ // If the sample value is within the current calibration range
109
+ if (sample < calibrationEnd) {
110
+ // Interpolate to get the calibrated value within the range
111
+ final =
112
+ calibration[i - 1][1] +
113
+ ((sample - calibrationStart) / (calibrationEnd - calibrationStart)) * (calibration[i][1] - calibration[i - 1][1]);
114
+ break;
115
+ }
116
+ }
117
+ // Return the calibrated value with the appropriate sign (positive/negative)
118
+ return sign * final;
119
+ };
120
+ /**
121
+ * handleMotherboardData
122
+ * @param uuid - Unique identifier
123
+ * @param receivedString - Received data string
124
+ */
125
+ export function handleMotherboardData(uuid, receivedString) {
126
+ const receivedTime = Date.now();
127
+ // Check if the line is entirely hex characters
128
+ const isAllHex = /^[0-9A-Fa-f]+$/g.test(receivedString);
129
+ // Handle streaming packet
130
+ if (isAllHex && receivedString.length === PACKET_LENGTH) {
131
+ // Base-16 decode the string: convert hex pairs to byte values
132
+ const bytes = Array.from({ length: receivedString.length / 2 }, (_, i) => Number(`0x${receivedString.substring(i * 2, i * 2 + 2)}`));
133
+ // Translate header into packet, number of samples from the packet length
134
+ const packet = {
135
+ received: receivedTime,
136
+ sampleNum: new DataView(new Uint8Array(bytes).buffer).getUint16(0, true),
137
+ battRaw: new DataView(new Uint8Array(bytes).buffer).getUint16(2, true),
138
+ samples: [],
139
+ masses: [],
140
+ };
141
+ const dataView = new DataView(new Uint8Array(bytes).buffer);
142
+ for (let i = 0; i < NUM_SAMPLES; i++) {
143
+ const sampleStart = 4 + 3 * i;
144
+ // Use DataView to read the 24-bit unsigned integer
145
+ const rawValue = dataView.getUint8(sampleStart) |
146
+ (dataView.getUint8(sampleStart + 1) << 8) |
147
+ (dataView.getUint8(sampleStart + 2) << 16);
148
+ // Ensure unsigned 32-bit integer
149
+ packet.samples[i] = rawValue >>> 0;
150
+ if (packet.samples[i] >= 0x7fffff) {
151
+ packet.samples[i] -= 0x1000000;
152
+ }
153
+ // TODO: make sure device is calibrated
154
+ if (!CALIBRATION[0].length)
155
+ return;
156
+ packet.masses[i] = applyCalibration(packet.samples[i], CALIBRATION[i]);
157
+ }
158
+ const left = packet.masses[0];
159
+ const center = packet.masses[1];
160
+ const right = packet.masses[2];
161
+ notifyCallback({
162
+ uuid,
163
+ value: {
164
+ massTotal: Math.max(-1000, left + right + center).toFixed(3),
165
+ massLeft: Math.max(-1000, left).toFixed(3),
166
+ massRight: Math.max(-1000, right).toFixed(3),
167
+ massCenter: Math.max(-1000, center).toFixed(3),
168
+ },
169
+ });
170
+ }
171
+ else if ((receivedString.match(/,/g) || []).length === 3) {
172
+ console.log(receivedString);
173
+ // if the returned notification is a calibration string add them to the array
174
+ const parts = receivedString.split(",");
175
+ const numericParts = parts.map((x) => parseFloat(x));
176
+ CALIBRATION[numericParts[0]].push(numericParts.slice(1));
177
+ }
178
+ else {
179
+ // unhanded data
180
+ console.log(receivedString);
181
+ }
182
+ }
@@ -1,4 +1,9 @@
1
1
  import { Device } from "./types"
2
+ import { notifyCallback } from "../notify"
3
+
4
+ const PACKET_LENGTH: number = 32
5
+ const NUM_SAMPLES: number = 3
6
+ const CALIBRATION = [[], [], [], []]
2
7
 
3
8
  export const Motherboard: Device = {
4
9
  name: "Motherboard",
@@ -79,3 +84,128 @@ export const Motherboard: Device = {
79
84
  },
80
85
  ],
81
86
  }
87
+ /**
88
+ * applyCalibration
89
+ * @param sample
90
+ * @param calibration
91
+ */
92
+ const applyCalibration = (sample: number, calibration: number[][]): number => {
93
+ // Extract the calibrated value for the zero point
94
+ const zeroCalibration: number = calibration[0][2];
95
+
96
+ // Initialize sign as positive
97
+ let sign: number = 1;
98
+
99
+ // Initialize the final calibrated value
100
+ let final: number = 0;
101
+
102
+ // If the sample value is less than the zero calibration point
103
+ if (sample < zeroCalibration) {
104
+ // Change the sign to negative
105
+ sign = -1;
106
+
107
+ // Reflect the sample around the zero calibration point
108
+ sample = 2 * zeroCalibration - sample;
109
+ }
110
+
111
+ // Iterate through the calibration data
112
+ for (let i = 1; i < calibration.length; i++) {
113
+ // Extract the lower and upper bounds of the current calibration range
114
+ const calibrationStart: number = calibration[i - 1][2];
115
+ const calibrationEnd: number = calibration[i][2];
116
+
117
+ // If the sample value is within the current calibration range
118
+ if (sample < calibrationEnd) {
119
+ // Interpolate to get the calibrated value within the range
120
+ final =
121
+ calibration[i - 1][1] +
122
+ ((sample - calibrationStart) / (calibrationEnd - calibrationStart)) * (calibration[i][1] - calibration[i - 1][1]);
123
+ break;
124
+ }
125
+ }
126
+ // Return the calibrated value with the appropriate sign (positive/negative)
127
+ return sign * final;
128
+ }
129
+
130
+ interface Packet {
131
+ received: number
132
+ sampleNum: number
133
+ battRaw: number
134
+ samples: number[]
135
+ masses: number[]
136
+ }
137
+
138
+ /**
139
+ * handleMotherboardData
140
+ * @param uuid - Unique identifier
141
+ * @param receivedString - Received data string
142
+ */
143
+ export function handleMotherboardData(uuid: string, receivedString: string): void {
144
+ const receivedTime: number = Date.now()
145
+
146
+ // Check if the line is entirely hex characters
147
+ const isAllHex: boolean = /^[0-9A-Fa-f]+$/g.test(receivedString);
148
+
149
+ // Handle streaming packet
150
+ if (isAllHex && receivedString.length === PACKET_LENGTH) {
151
+ // Base-16 decode the string: convert hex pairs to byte values
152
+ const bytes: number[] = Array.from({ length: receivedString.length / 2 }, (_, i) =>
153
+ Number(`0x${receivedString.substring(i * 2, i * 2 + 2)}`),
154
+ )
155
+
156
+ // Translate header into packet, number of samples from the packet length
157
+ const packet: Packet = {
158
+ received: receivedTime,
159
+ sampleNum: new DataView(new Uint8Array(bytes).buffer).getUint16(0, true),
160
+ battRaw: new DataView(new Uint8Array(bytes).buffer).getUint16(2, true),
161
+ samples: [],
162
+ masses: [],
163
+ }
164
+
165
+ const dataView = new DataView(new Uint8Array(bytes).buffer);
166
+
167
+ for (let i = 0; i < NUM_SAMPLES; i++) {
168
+ const sampleStart: number = 4 + 3 * i
169
+ // Use DataView to read the 24-bit unsigned integer
170
+ const rawValue = dataView.getUint8(sampleStart) |
171
+ (dataView.getUint8(sampleStart + 1) << 8) |
172
+ (dataView.getUint8(sampleStart + 2) << 16);
173
+
174
+ // Ensure unsigned 32-bit integer
175
+ packet.samples[i] = rawValue >>> 0;
176
+
177
+ if (packet.samples[i] >= 0x7fffff) {
178
+ packet.samples[i] -= 0x1000000;
179
+ }
180
+
181
+ // TODO: make sure device is calibrated
182
+ if (!CALIBRATION[0].length) return
183
+ packet.masses[i] = applyCalibration(packet.samples[i], CALIBRATION[i])
184
+ }
185
+
186
+ const left: number = packet.masses[0]
187
+ const center: number = packet.masses[1]
188
+ const right: number = packet.masses[2]
189
+
190
+ notifyCallback({
191
+ uuid,
192
+ value: {
193
+ massTotal: Math.max(-1000, left + right + center).toFixed(3),
194
+ massLeft: Math.max(-1000, left).toFixed(3),
195
+ massRight: Math.max(-1000, right).toFixed(3),
196
+ massCenter: Math.max(-1000, center).toFixed(3),
197
+ },
198
+ })
199
+ } else if ((receivedString.match(/,/g) || []).length === 3) {
200
+ console.log(receivedString)
201
+ // if the returned notification is a calibration string add them to the array
202
+ const parts: string[] = receivedString.split(",")
203
+ const numericParts: number[] = parts.map((x) => parseFloat(x))
204
+ ;(CALIBRATION[numericParts[0]] as number[][]).push(numericParts.slice(1))
205
+ } else {
206
+ // unhanded data
207
+ console.log(receivedString)
208
+ }
209
+ }
210
+
211
+
package/src/read.ts CHANGED
@@ -6,7 +6,12 @@ import { getCharacteristic } from "./characteristic"
6
6
  * read
7
7
  * @param characteristic
8
8
  */
9
- export const read = (board: Device, serviceId: string, characteristicId: string, duration: number = 0): Promise<void> => {
9
+ export const read = (
10
+ board: Device,
11
+ serviceId: string,
12
+ characteristicId: string,
13
+ duration: number = 0,
14
+ ): Promise<void> => {
10
15
  return new Promise((resolve, reject) => {
11
16
  if (board.device?.gatt?.connected) {
12
17
  const characteristic = getCharacteristic(board, serviceId, characteristicId)
package/tsconfig.json CHANGED
@@ -3,7 +3,5 @@
3
3
  "include": ["src"],
4
4
  "compilerOptions": {
5
5
  "tsBuildInfoFile": "node_modules/.cache/.tsbuildinfo",
6
- "types": ["web-bluetooth"],
7
- "outDir": "./build"
8
- }
6
+ },
9
7
  }
@@ -1,9 +0,0 @@
1
- /// <reference types="web-bluetooth" />
2
- import { Device } from "./devices/types";
3
- /**
4
- * getCharacteristic
5
- * @param board
6
- * @param serviceId
7
- * @param characteristicId
8
- */
9
- export declare const getCharacteristic: (board: Device, serviceId: string, characteristicId: string) => BluetoothRemoteGATTCharacteristic | undefined;
@@ -1,15 +0,0 @@
1
- /**
2
- * getCharacteristic
3
- * @param board
4
- * @param serviceId
5
- * @param characteristicId
6
- */
7
- export const getCharacteristic = (board, serviceId, characteristicId) => {
8
- const boardService = board.services.find((service) => service.id === serviceId);
9
- if (boardService) {
10
- const boardCharacteristic = boardService.characteristics.find((characteristic) => characteristic.id === characteristicId);
11
- if (boardCharacteristic) {
12
- return boardCharacteristic.characteristic;
13
- }
14
- }
15
- };
@@ -1,7 +0,0 @@
1
- import { Device } from "./devices/types";
2
- /**
3
- * Connect to the BluetoothDevice
4
- * @param device
5
- * @param onSuccess
6
- */
7
- export declare const connect: (board: Device, onSuccess: () => void) => Promise<void>;
package/build/connect.js DELETED
@@ -1,172 +0,0 @@
1
- import { notifyCallback } from "./notify";
2
- let server;
3
- /**
4
- * onDisconnected
5
- * @param board
6
- * @param event
7
- */
8
- const onDisconnected = (event, board) => {
9
- board.device = undefined;
10
- const device = event.target;
11
- console.log(`Device ${device.name} is disconnected.`);
12
- };
13
- /**
14
- * handleNotifications
15
- * @param event
16
- * @param onNotify
17
- */
18
- const handleNotifications = (event, board) => {
19
- const characteristic = event.target;
20
- const receivedData = new Uint8Array(characteristic.value.buffer);
21
- // Create an array to store the parsed decimal values
22
- const decimalArray = [];
23
- // Iterate through each byte and convert to decimal
24
- for (let i = 0; i < receivedData.length; i++) {
25
- decimalArray.push(receivedData[i]);
26
- }
27
- // Convert the decimal array to a string representation
28
- const receivedString = String.fromCharCode(...decimalArray);
29
- if (board.name === "Motherboard") {
30
- // Split the string into pairs of characters
31
- const hexPairs = receivedString.match(/.{1,2}/g);
32
- // Convert each hexadecimal pair to decimal
33
- const parsedDecimalArray = hexPairs?.map((hexPair) => parseInt(hexPair, 16));
34
- // Handle different types of data
35
- if (characteristic.value.byteLength === 20) {
36
- const elementKeys = [
37
- "frames",
38
- "cycle",
39
- "unknown",
40
- "eleven",
41
- "dynamic1",
42
- "pressure1",
43
- "left",
44
- "dynamic2",
45
- "pressure2",
46
- "right",
47
- ];
48
- const dataObject = {};
49
- if (parsedDecimalArray) {
50
- elementKeys.forEach((key, index) => {
51
- dataObject[key] = parsedDecimalArray[index];
52
- });
53
- }
54
- if (notifyCallback) {
55
- notifyCallback({ uuid: characteristic.uuid, value: dataObject });
56
- }
57
- }
58
- else if (characteristic.value.byteLength === 14) {
59
- // TODO: handle 14 byte data
60
- // notifyCallback({ uuid: characteristic.uuid, value: characteristic.value!.getInt8(0) / 100 })
61
- }
62
- }
63
- else if (board.name === "ENTRALPI") {
64
- // TODO: handle Entralpi notify
65
- // characteristic.value!.getInt16(0) / 100;
66
- if (notifyCallback) {
67
- notifyCallback({ uuid: characteristic.uuid, value: receivedString });
68
- }
69
- }
70
- else if (board.name === "Tindeq") {
71
- // TODO: handle Tindeq notify
72
- }
73
- else {
74
- if (notifyCallback) {
75
- notifyCallback({ uuid: characteristic.uuid, value: receivedString });
76
- }
77
- }
78
- };
79
- /**
80
- * onConnected
81
- * @param event
82
- * @param board
83
- */
84
- const onConnected = async (board, onSuccess) => {
85
- try {
86
- const services = await server?.getPrimaryServices();
87
- if (!services || services.length === 0) {
88
- console.error("No services found");
89
- return;
90
- }
91
- for (const service of services) {
92
- const matchingService = board.services.find((boardService) => boardService.uuid === service.uuid);
93
- if (matchingService) {
94
- // Android bug: Introduce a delay before getting characteristics
95
- await new Promise((resolve) => setTimeout(resolve, 100));
96
- const characteristics = await service.getCharacteristics();
97
- for (const characteristic of matchingService.characteristics) {
98
- const matchingCharacteristic = characteristics.find((char) => char.uuid === characteristic.uuid);
99
- if (matchingCharacteristic) {
100
- const element = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid);
101
- if (element) {
102
- element.characteristic = matchingCharacteristic;
103
- // notify
104
- if (element.id === "rx") {
105
- matchingCharacteristic.startNotifications();
106
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event) => handleNotifications(event, board));
107
- }
108
- }
109
- }
110
- else {
111
- console.warn(`Characteristic ${characteristic.uuid} not found in service ${service.uuid}`);
112
- }
113
- }
114
- }
115
- }
116
- // Call the onSuccess callback after successful connection and setup
117
- onSuccess();
118
- }
119
- catch (error) {
120
- console.error(error);
121
- }
122
- };
123
- /**
124
- * Return all service UUIDs
125
- * @param device
126
- */
127
- function getAllServiceUUIDs(device) {
128
- return device.services.map((service) => service.uuid);
129
- }
130
- /**
131
- * Connect to the BluetoothDevice
132
- * @param device
133
- * @param onSuccess
134
- */
135
- export const connect = async (board, onSuccess) => {
136
- try {
137
- const deviceServices = getAllServiceUUIDs(board);
138
- // setup filter list
139
- const filters = [];
140
- if (board.name) {
141
- filters.push({
142
- name: board.name,
143
- });
144
- }
145
- if (board.companyId) {
146
- filters.push({
147
- manufacturerData: [
148
- {
149
- companyIdentifier: board.companyId,
150
- },
151
- ],
152
- });
153
- }
154
- const device = await navigator.bluetooth.requestDevice({
155
- filters: filters,
156
- optionalServices: deviceServices
157
- });
158
- board.device = device;
159
- if (!board.device.gatt) {
160
- console.error("GATT is not available on this device");
161
- return;
162
- }
163
- server = await board.device?.gatt?.connect();
164
- board.device.addEventListener("gattserverdisconnected", (event) => onDisconnected(event, board));
165
- if (server.connected) {
166
- await onConnected(board, onSuccess);
167
- }
168
- }
169
- catch (error) {
170
- console.error(error);
171
- }
172
- };
@@ -1,2 +0,0 @@
1
- import { Device } from "./types";
2
- export declare const Entralpi: Device;
@@ -1,52 +0,0 @@
1
- export const Entralpi = {
2
- name: "ENTRALPI",
3
- services: [
4
- {
5
- name: "Device Information",
6
- id: "device",
7
- uuid: "0000180a-0000-1000-8000-00805f9b34fb",
8
- characteristics: [],
9
- },
10
- {
11
- name: "Battery Service",
12
- id: "battery",
13
- uuid: "0000180f-0000-1000-8000-00805f9b34fb",
14
- characteristics: [],
15
- },
16
- {
17
- name: "Generic Attribute",
18
- id: "attribute",
19
- uuid: "00001801-0000-1000-8000-00805f9b34fb",
20
- characteristics: [],
21
- },
22
- {
23
- name: "UART ISSC Transparent Service",
24
- id: "uart",
25
- uuid: "0000fff0-0000-1000-8000-00805f9b34fb",
26
- characteristics: [
27
- {
28
- name: "TX",
29
- id: "tx",
30
- uuid: "0000fff5-0000-1000-8000-00805f9b34fb",
31
- },
32
- {
33
- name: "RX",
34
- id: "rx",
35
- uuid: "0000fff4-0000-1000-8000-00805f9b34fb",
36
- },
37
- ],
38
- },
39
- {
40
- name: "Weight Scale",
41
- id: "weight",
42
- uuid: "0000181d-0000-1000-8000-00805f9b34fb",
43
- characteristics: [],
44
- },
45
- {
46
- name: "Generic Access",
47
- id: "access",
48
- uuid: "00001800-0000-1000-8000-00805f9b34fb",
49
- characteristics: [],
50
- },
51
- ],
52
- };
@@ -1,3 +0,0 @@
1
- export { Motherboard } from "./moterboard";
2
- export { Entralpi } from "./entralpi";
3
- export { Tindeq } from "./tindeq";
@@ -1,3 +0,0 @@
1
- export { Motherboard } from "./moterboard";
2
- export { Entralpi } from "./entralpi";
3
- export { Tindeq } from "./tindeq";
@@ -1,2 +0,0 @@
1
- import { Device } from "./types";
2
- export declare const Motherboard: Device;
@@ -1,79 +0,0 @@
1
- export const Motherboard = {
2
- name: "Motherboard",
3
- companyId: 0x2a29,
4
- services: [
5
- {
6
- name: "Device Information",
7
- id: "device",
8
- uuid: "0000180a-0000-1000-8000-00805f9b34fb",
9
- characteristics: [
10
- // {
11
- // name: 'Serial Number (Blocked)',
12
- // id: 'serial'
13
- // uuid: '00002a25-0000-1000-8000-00805f9b34fb'
14
- // },
15
- {
16
- name: "Firmware Revision",
17
- id: "firmware",
18
- uuid: "00002a26-0000-1000-8000-00805f9b34fb",
19
- },
20
- {
21
- name: "Hardware Revision",
22
- id: "hardware",
23
- uuid: "00002a27-0000-1000-8000-00805f9b34fb",
24
- },
25
- {
26
- name: "Manufacturer Name",
27
- id: "manufacturer",
28
- uuid: "00002a29-0000-1000-8000-00805f9b34fb",
29
- },
30
- ],
31
- },
32
- {
33
- name: "Battery Service",
34
- id: "battery",
35
- uuid: "0000180f-0000-1000-8000-00805f9b34fb",
36
- characteristics: [
37
- {
38
- name: "Battery Level",
39
- id: "level",
40
- uuid: "00002a19-0000-1000-8000-00805f9b34fb",
41
- },
42
- ],
43
- },
44
- {
45
- name: "Unknown Service",
46
- id: "unknown",
47
- uuid: "10ababcd-15e1-28ff-de13-725bea03b127",
48
- characteristics: [
49
- {
50
- name: "Unknown 01",
51
- id: "01",
52
- uuid: "10ab1524-15e1-28ff-de13-725bea03b127",
53
- },
54
- {
55
- name: "Unknown 02",
56
- id: "02",
57
- uuid: "10ab1525-15e1-28ff-de13-725bea03b127",
58
- },
59
- ],
60
- },
61
- {
62
- name: "UART Nordic Service",
63
- id: "uart",
64
- uuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
65
- characteristics: [
66
- {
67
- name: "TX",
68
- id: "tx",
69
- uuid: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
70
- },
71
- {
72
- name: "RX",
73
- id: "rx",
74
- uuid: "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
75
- },
76
- ],
77
- },
78
- ],
79
- };
@@ -1,17 +0,0 @@
1
- import { Device } from "./types";
2
- export declare const Tindeq: Device;
3
- export declare const Commands: {
4
- TARE_SCALE: number;
5
- START_MEASURING: number;
6
- STOP_MEASURING: number;
7
- GET_APP_VERSION: number;
8
- GET_ERROR_INFO: number;
9
- CLEAR_ERR_INFO: number;
10
- GET_BATTERY_LEVEL: number;
11
- SLEEP: number;
12
- };
13
- export declare const NotificationTypes: {
14
- COMMAND_RESPONSE: number;
15
- WEIGHT_MEASURE: number;
16
- LOW_BATTERY_WARNING: number;
17
- };
@@ -1,37 +0,0 @@
1
- export const Tindeq = {
2
- name: "Tindeq",
3
- services: [
4
- {
5
- name: "Progressor Service",
6
- id: "progressor",
7
- uuid: "7e4e1701-1ea6-40c9-9dcc-13d34ffead57",
8
- characteristics: [
9
- {
10
- name: "Write",
11
- id: "tx",
12
- uuid: "7e4e1703-1ea6-40c9-9dcc-13d34ffead57",
13
- },
14
- {
15
- name: "Notify",
16
- id: "rx",
17
- uuid: "7e4e1702-1ea6-40c9-9dcc-13d34ffead57",
18
- },
19
- ],
20
- },
21
- ],
22
- };
23
- export const Commands = {
24
- TARE_SCALE: 0x64,
25
- START_MEASURING: 0x65,
26
- STOP_MEASURING: 0x66,
27
- GET_APP_VERSION: 0x6b,
28
- GET_ERROR_INFO: 0x6c,
29
- CLEAR_ERR_INFO: 0x6d,
30
- GET_BATTERY_LEVEL: 0x6f,
31
- SLEEP: 0x6e,
32
- };
33
- export const NotificationTypes = {
34
- COMMAND_RESPONSE: 0,
35
- WEIGHT_MEASURE: 1,
36
- LOW_BATTERY_WARNING: 2,
37
- };
@@ -1,20 +0,0 @@
1
- /// <reference types="web-bluetooth" />
2
- interface Characteristic {
3
- name: string;
4
- id: string;
5
- uuid: string;
6
- characteristic?: BluetoothRemoteGATTCharacteristic;
7
- }
8
- interface Service {
9
- name: string;
10
- id: string;
11
- uuid: string;
12
- characteristics: Characteristic[];
13
- }
14
- export interface Device {
15
- name: string;
16
- companyId?: number;
17
- services: Service[];
18
- device?: BluetoothDevice;
19
- }
20
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,6 +0,0 @@
1
- import { Device } from "./devices/types";
2
- /**
3
- * disconnect
4
- * @param board
5
- */
6
- export declare const disconnect: (board: Device) => void;
@@ -1,11 +0,0 @@
1
- /**
2
- * disconnect
3
- * @param board
4
- */
5
- export const disconnect = (board) => {
6
- if (!board.device)
7
- return;
8
- if (board.device.gatt?.connected) {
9
- board.device.gatt?.disconnect();
10
- }
11
- };
package/build/index.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export { Motherboard, Entralpi, Tindeq } from "./devices/index";
2
- export { connect } from "./connect";
3
- export { disconnect } from "./disconnect";
4
- export { notify } from "./notify";
5
- export { read } from "./read";
6
- export { write } from "./write";
package/build/index.js DELETED
@@ -1,6 +0,0 @@
1
- export { Motherboard, Entralpi, Tindeq } from "./devices/index";
2
- export { connect } from "./connect";
3
- export { disconnect } from "./disconnect";
4
- export { notify } from "./notify";
5
- export { read } from "./read";
6
- export { write } from "./write";
package/build/notify.d.ts DELETED
@@ -1,4 +0,0 @@
1
- type NotifyCallback = (data: object) => void;
2
- export declare let notifyCallback: NotifyCallback;
3
- export declare const notify: (callback: NotifyCallback) => void;
4
- export {};
package/build/notify.js DELETED
@@ -1,6 +0,0 @@
1
- // Initialize the callback variable
2
- export let notifyCallback;
3
- // Export a function to set the callback
4
- export const notify = (callback) => {
5
- notifyCallback = callback;
6
- };
package/build/read.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { Device } from "./devices/types";
2
- /**
3
- * read
4
- * @param characteristic
5
- */
6
- export declare const read: (board: Device, serviceId: string, characteristicId: string, duration?: number) => Promise<void>;
package/build/read.js DELETED
@@ -1,44 +0,0 @@
1
- import { notifyCallback } from "./notify";
2
- import { getCharacteristic } from "./characteristic";
3
- /**
4
- * read
5
- * @param characteristic
6
- */
7
- export const read = (board, serviceId, characteristicId, duration = 0) => {
8
- return new Promise((resolve, reject) => {
9
- if (board.device?.gatt?.connected) {
10
- const characteristic = getCharacteristic(board, serviceId, characteristicId);
11
- if (characteristic) {
12
- characteristic
13
- .readValue()
14
- .then((value) => {
15
- let decodedValue;
16
- const decoder = new TextDecoder("utf-8");
17
- switch (characteristicId) {
18
- case "level":
19
- decodedValue = value.getUint8(0);
20
- break;
21
- default:
22
- decodedValue = decoder.decode(value);
23
- break;
24
- }
25
- if (notifyCallback) {
26
- notifyCallback({ uuid: characteristic.uuid, value: decodedValue });
27
- }
28
- setTimeout(() => {
29
- resolve();
30
- }, duration);
31
- })
32
- .catch((error) => {
33
- reject(error);
34
- });
35
- }
36
- else {
37
- reject(new Error("Characteristic is undefined"));
38
- }
39
- }
40
- else {
41
- reject(new Error("Device is not connected"));
42
- }
43
- });
44
- };
package/build/write.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import { Device } from "./devices/types";
2
- /**
3
- * write
4
- * @param characteristic
5
- * @param message
6
- */
7
- export declare const write: (board: Device, serviceId: string, characteristicId: string, message: string, duration?: number) => Promise<void>;
package/build/write.js DELETED
@@ -1,32 +0,0 @@
1
- import { getCharacteristic } from "./characteristic";
2
- /**
3
- * write
4
- * @param characteristic
5
- * @param message
6
- */
7
- export const write = (board, serviceId, characteristicId, message, duration = 0) => {
8
- return new Promise((resolve, reject) => {
9
- if (board.device?.gatt?.connected) {
10
- const encoder = new TextEncoder();
11
- const characteristic = getCharacteristic(board, serviceId, characteristicId);
12
- if (characteristic) {
13
- characteristic
14
- .writeValue(encoder.encode(message))
15
- .then(() => {
16
- setTimeout(() => {
17
- resolve();
18
- }, duration);
19
- })
20
- .catch((error) => {
21
- reject(error);
22
- });
23
- }
24
- else {
25
- reject(new Error("Characteristics is undefined"));
26
- }
27
- }
28
- else {
29
- reject(new Error("Device is not connected"));
30
- }
31
- });
32
- };