@hangtime/grip-connect 0.0.7 → 0.0.8

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
+ - Griptonte 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,36 @@ 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)
83
+ // recalibrate
84
+ await write(Motherboard, "uart", "tx", "", 0)
85
+ await write(Motherboard, "uart", "tx", "", 0)
86
+ await write(Motherboard, "uart", "tx", "", 1000)
66
87
 
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)
88
+ await write(Motherboard, "uart", "tx", "C3,0,0,0", 5000)
71
89
 
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)
90
+ // start stream
91
+ await write(Motherboard, "uart", "tx", "S20", 15000)
76
92
 
93
+ // end stream
94
+ await write(Motherboard, "uart", "tx", "", 0)
77
95
  // disconnect from device after we are done
78
96
  disconnect(Motherboard)
79
97
  })
80
98
  })
81
99
  ```
82
100
 
101
+ ## Credits
102
+
103
+ A special thank you to:
104
+
105
+ - [@CassimLadha](https://github.com/CassimLadha) for sharing insights on reading the Motherboards data.
106
+ - [@donaldharvey](https://github.com/donaldharvey) for a valuable example on connecting to the motherboard.
107
+
108
+ ## Disclamer
109
+
110
+ THIS SOFTWARE IS NOT OFFICIALY SUPPORTED, SUPPLIED OR MAINTAINED BY THE DEVICE MANUFACTURER. BY USING THE SOFTWARE YOU
111
+ ARE ACKNOWLEDGEING THIS AND UNDERSTAND THAT USING THIS SOFTWARE WILL INVALIDATE THE MANUFACTURERS WARRANTY.
112
+
83
113
  ## License
84
114
 
85
- MIT © [Stevie-Ray Hartog](https://github.com/Stevie-Ray)
115
+ BSD 2-Clause © [Stevie-Ray Hartog](https://github.com/Stevie-Ray)
package/build/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) {
@@ -1,2 +1,7 @@
1
1
  import { Device } from "./types";
2
2
  export declare const Motherboard: Device;
3
+ /**
4
+ * handleMotherboardData
5
+ * @param line
6
+ */
7
+ 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,83 @@ 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
+ const zeroCalib = calibration[0][2];
91
+ let sgn = 1;
92
+ let final = 0;
93
+ if (sample < zeroCalib) {
94
+ sgn = -1;
95
+ sample = 2 * zeroCalib - sample;
96
+ }
97
+ for (let i = 1; i < calibration.length; i++) {
98
+ const calibStart = calibration[i - 1][2];
99
+ const calibEnd = calibration[i][2];
100
+ if (sample < calibEnd) {
101
+ final =
102
+ calibration[i - 1][1] +
103
+ ((sample - calibStart) / (calibEnd - calibStart)) * (calibration[i][1] - calibration[i - 1][1]);
104
+ break;
105
+ }
106
+ }
107
+ return sgn * final;
108
+ };
109
+ /**
110
+ * handleMotherboardData
111
+ * @param line
112
+ */
113
+ export function handleMotherboardData(uuid, receivedString) {
114
+ const receivedTime = Date.now();
115
+ // Check if the line is entirely hex characters
116
+ const allHex = /^[0-9A-Fa-f]+$/g.test(receivedString);
117
+ // Decide if this is a streaming packet
118
+ if (allHex && receivedString.length === PACKET_LENGTH) {
119
+ // Base-16 decode the string: convert hex pairs to byte values
120
+ const bytes = Array.from({ length: receivedString.length / 2 }, (_, i) => Number(`0x${receivedString.substring(i * 2, i * 2 + 2)}`));
121
+ // Translate header into packet, number of samples from the packet length
122
+ const packet = {
123
+ received: receivedTime,
124
+ sampleNum: new DataView(new Uint8Array(bytes).buffer).getUint16(0, true),
125
+ battRaw: new DataView(new Uint8Array(bytes).buffer).getUint16(2, true),
126
+ samples: [],
127
+ masses: [],
128
+ };
129
+ for (let i = 0; i < NUM_SAMPLES; i++) {
130
+ const sampleStart = 4 + 3 * i;
131
+ packet.samples[i] = bytes[sampleStart] | (bytes[sampleStart + 1] << 8) | (bytes[sampleStart + 2] << 16);
132
+ if (packet.samples[i] >= 0x7fffff) {
133
+ packet.samples[i] -= 0x1000000;
134
+ }
135
+ // TODO: make sure device is calibrated
136
+ if (!CALIBRATION[0].length)
137
+ return;
138
+ packet.masses[i] = applyCalibration(packet.samples[i], CALIBRATION[i]);
139
+ }
140
+ const left = packet.masses[0];
141
+ const center = packet.masses[1];
142
+ const right = packet.masses[2];
143
+ notifyCallback({
144
+ uuid,
145
+ value: {
146
+ massTotal: Math.max(-1000, left + right + center).toFixed(3),
147
+ massLeft: Math.max(-1000, left).toFixed(3),
148
+ massRight: Math.max(-1000, right).toFixed(3),
149
+ massCentre: Math.max(-1000, center).toFixed(3),
150
+ },
151
+ });
152
+ }
153
+ else if ((receivedString.match(/,/g) || []).length === 3) {
154
+ // if the returned notification is a calibration string add them to the array
155
+ const parts = receivedString.split(",");
156
+ const numericParts = parts.map((x) => parseFloat(x));
157
+ CALIBRATION[numericParts[0]].push(numericParts.slice(1));
158
+ }
159
+ else {
160
+ // unhanded data
161
+ console.log(receivedString);
162
+ }
163
+ }
package/build/write.js CHANGED
@@ -10,8 +10,9 @@ export const write = (board, serviceId, characteristicId, message, duration = 0)
10
10
  const encoder = new TextEncoder();
11
11
  const characteristic = getCharacteristic(board, serviceId, characteristicId);
12
12
  if (characteristic) {
13
+ const value = message + "\n";
13
14
  characteristic
14
- .writeValue(encoder.encode(message))
15
+ .writeValue(encoder.encode(value))
15
16
  .then(() => {
16
17
  setTimeout(() => {
17
18
  resolve();
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.8",
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,7 @@
1
1
  import { Device } from "./types";
2
2
  export declare const Motherboard: Device;
3
+ /**
4
+ * handleMotherboardData
5
+ * @param line
6
+ */
7
+ 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,83 @@ 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
+ const zeroCalib = calibration[0][2];
91
+ let sgn = 1;
92
+ let final = 0;
93
+ if (sample < zeroCalib) {
94
+ sgn = -1;
95
+ sample = 2 * zeroCalib - sample;
96
+ }
97
+ for (let i = 1; i < calibration.length; i++) {
98
+ const calibStart = calibration[i - 1][2];
99
+ const calibEnd = calibration[i][2];
100
+ if (sample < calibEnd) {
101
+ final =
102
+ calibration[i - 1][1] +
103
+ ((sample - calibStart) / (calibEnd - calibStart)) * (calibration[i][1] - calibration[i - 1][1]);
104
+ break;
105
+ }
106
+ }
107
+ return sgn * final;
108
+ };
109
+ /**
110
+ * handleMotherboardData
111
+ * @param line
112
+ */
113
+ export function handleMotherboardData(uuid, receivedString) {
114
+ const receivedTime = Date.now();
115
+ // Check if the line is entirely hex characters
116
+ const allHex = /^[0-9A-Fa-f]+$/g.test(receivedString);
117
+ // Decide if this is a streaming packet
118
+ if (allHex && receivedString.length === PACKET_LENGTH) {
119
+ // Base-16 decode the string: convert hex pairs to byte values
120
+ const bytes = Array.from({ length: receivedString.length / 2 }, (_, i) => Number(`0x${receivedString.substring(i * 2, i * 2 + 2)}`));
121
+ // Translate header into packet, number of samples from the packet length
122
+ const packet = {
123
+ received: receivedTime,
124
+ sampleNum: new DataView(new Uint8Array(bytes).buffer).getUint16(0, true),
125
+ battRaw: new DataView(new Uint8Array(bytes).buffer).getUint16(2, true),
126
+ samples: [],
127
+ masses: [],
128
+ };
129
+ for (let i = 0; i < NUM_SAMPLES; i++) {
130
+ const sampleStart = 4 + 3 * i;
131
+ packet.samples[i] = bytes[sampleStart] | (bytes[sampleStart + 1] << 8) | (bytes[sampleStart + 2] << 16);
132
+ if (packet.samples[i] >= 0x7fffff) {
133
+ packet.samples[i] -= 0x1000000;
134
+ }
135
+ // TODO: make sure device is calibrated
136
+ if (!CALIBRATION[0].length)
137
+ return;
138
+ packet.masses[i] = applyCalibration(packet.samples[i], CALIBRATION[i]);
139
+ }
140
+ const left = packet.masses[0];
141
+ const center = packet.masses[1];
142
+ const right = packet.masses[2];
143
+ notifyCallback({
144
+ uuid,
145
+ value: {
146
+ massTotal: Math.max(-1000, left + right + center).toFixed(3),
147
+ massLeft: Math.max(-1000, left).toFixed(3),
148
+ massRight: Math.max(-1000, right).toFixed(3),
149
+ massCentre: Math.max(-1000, center).toFixed(3),
150
+ },
151
+ });
152
+ }
153
+ else if ((receivedString.match(/,/g) || []).length === 3) {
154
+ // if the returned notification is a calibration string add them to the array
155
+ const parts = receivedString.split(",");
156
+ const numericParts = parts.map((x) => parseFloat(x));
157
+ CALIBRATION[numericParts[0]].push(numericParts.slice(1));
158
+ }
159
+ else {
160
+ // unhanded data
161
+ console.log(receivedString);
162
+ }
163
+ }
@@ -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,104 @@ 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
+ const zeroCalib: number = calibration[0][2]
94
+ let sgn: number = 1
95
+ let final: number = 0
96
+
97
+ if (sample < zeroCalib) {
98
+ sgn = -1
99
+ sample = 2 * zeroCalib - sample
100
+ }
101
+
102
+ for (let i = 1; i < calibration.length; i++) {
103
+ const calibStart: number = calibration[i - 1][2]
104
+ const calibEnd: number = calibration[i][2]
105
+
106
+ if (sample < calibEnd) {
107
+ final =
108
+ calibration[i - 1][1] +
109
+ ((sample - calibStart) / (calibEnd - calibStart)) * (calibration[i][1] - calibration[i - 1][1])
110
+ break
111
+ }
112
+ }
113
+
114
+ return sgn * final
115
+ }
116
+
117
+ interface Packet {
118
+ received: number
119
+ sampleNum: number
120
+ battRaw: number
121
+ samples: number[]
122
+ masses: number[]
123
+ }
124
+
125
+ /**
126
+ * handleMotherboardData
127
+ * @param line
128
+ */
129
+ export function handleMotherboardData(uuid: string, receivedString: string): void {
130
+ const receivedTime: number = Date.now()
131
+
132
+ // Check if the line is entirely hex characters
133
+ const allHex: boolean = /^[0-9A-Fa-f]+$/g.test(receivedString)
134
+
135
+ // Decide if this is a streaming packet
136
+ if (allHex && receivedString.length === PACKET_LENGTH) {
137
+ // Base-16 decode the string: convert hex pairs to byte values
138
+ const bytes: number[] = Array.from({ length: receivedString.length / 2 }, (_, i) =>
139
+ Number(`0x${receivedString.substring(i * 2, i * 2 + 2)}`),
140
+ )
141
+
142
+ // Translate header into packet, number of samples from the packet length
143
+ const packet: Packet = {
144
+ received: receivedTime,
145
+ sampleNum: new DataView(new Uint8Array(bytes).buffer).getUint16(0, true),
146
+ battRaw: new DataView(new Uint8Array(bytes).buffer).getUint16(2, true),
147
+ samples: [],
148
+ masses: [],
149
+ }
150
+
151
+ for (let i = 0; i < NUM_SAMPLES; i++) {
152
+ const sampleStart: number = 4 + 3 * i
153
+ packet.samples[i] = bytes[sampleStart] | (bytes[sampleStart + 1] << 8) | (bytes[sampleStart + 2] << 16)
154
+
155
+ if (packet.samples[i] >= 0x7fffff) {
156
+ packet.samples[i] -= 0x1000000
157
+ }
158
+
159
+ // TODO: make sure device is calibrated
160
+ if (!CALIBRATION[0].length) return
161
+
162
+ packet.masses[i] = applyCalibration(packet.samples[i], CALIBRATION[i])
163
+ }
164
+
165
+ const left: number = packet.masses[0]
166
+ const center: number = packet.masses[1]
167
+ const right: number = packet.masses[2]
168
+
169
+ notifyCallback({
170
+ uuid,
171
+ value: {
172
+ massTotal: Math.max(-1000, left + right + center).toFixed(3),
173
+ massLeft: Math.max(-1000, left).toFixed(3),
174
+ massRight: Math.max(-1000, right).toFixed(3),
175
+ massCentre: Math.max(-1000, center).toFixed(3),
176
+ },
177
+ })
178
+ } else if ((receivedString.match(/,/g) || []).length === 3) {
179
+ // if the returned notification is a calibration string add them to the array
180
+ const parts: string[] = receivedString.split(",")
181
+ const numericParts: number[] = parts.map((x) => parseFloat(x))
182
+ ;(CALIBRATION[numericParts[0]] as number[][]).push(numericParts.slice(1))
183
+ } else {
184
+ // unhanded data
185
+ console.log(receivedString)
186
+ }
187
+ }
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/src/write.js CHANGED
@@ -10,8 +10,9 @@ export const write = (board, serviceId, characteristicId, message, duration = 0)
10
10
  const encoder = new TextEncoder();
11
11
  const characteristic = getCharacteristic(board, serviceId, characteristicId);
12
12
  if (characteristic) {
13
+ const value = message + "\n";
13
14
  characteristic
14
- .writeValue(encoder.encode(message))
15
+ .writeValue(encoder.encode(value))
15
16
  .then(() => {
16
17
  setTimeout(() => {
17
18
  resolve();
package/src/write.ts CHANGED
@@ -19,8 +19,9 @@ export const write = (
19
19
  const characteristic = getCharacteristic(board, serviceId, characteristicId)
20
20
 
21
21
  if (characteristic) {
22
+ const value = message + "\n"
22
23
  characteristic
23
- .writeValue(encoder.encode(message))
24
+ .writeValue(encoder.encode(value))
24
25
  .then(() => {
25
26
  setTimeout(() => {
26
27
  resolve()
package/tsconfig.json CHANGED
@@ -4,6 +4,6 @@
4
4
  "compilerOptions": {
5
5
  "tsBuildInfoFile": "node_modules/.cache/.tsbuildinfo",
6
6
  "types": ["web-bluetooth"],
7
- "outDir": "./build"
8
- }
7
+ "outDir": "./build",
8
+ },
9
9
  }