@hangtime/grip-connect 0.3.8 → 0.3.10

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
@@ -7,8 +7,7 @@ Force-Sensing Hangboards / Dynamometers / Plates / LED system boards used by cli
7
7
  the [Griptonite Motherboard](https://griptonite.io/shop/motherboard/), [Climbro](https://climbro.com/),
8
8
  [mySmartBoard](https://www.smartboard-climbing.com/), [Entralpi](https://entralpi.com/),
9
9
  [Tindeq Progressor](https://tindeq.com/) or
10
- [Weiheng WH-C06](https://weihengmanufacturer.com/products/wh-c06-bluetooth-300kg-hanging-scale/) also sold as
11
- [MAT Muscle Meter](https://www.matassessment.com/musclemeter).
10
+ [Weiheng WH-C06](https://weihengmanufacturer.com/products/wh-c06-bluetooth-300kg-hanging-scale/).
12
11
 
13
12
  And LED system boards from [Aurora Climbing](https://auroraclimbing.com/) like the
14
13
  [Kilter Board](https://settercloset.com/pages/the-kilter-board),
@@ -28,7 +27,7 @@ Learn more: [Docs](https://stevie-ray.github.io/hangtime-grip-connect/) -
28
27
  ## Try it out
29
28
 
30
29
  [Chart](https://grip-connect.vercel.app/) - [Flappy Bird](https://grip-connect-flappy-bird.vercel.app/) -
31
- [Kilter Board](https://grip-connect-kilter-board.vercel.app/)
30
+ [Kilter Board](https://grip-connect-kilter-board.vercel.app/?route=p1083r15p1117r15p1164r12p1185r12p1233r13p1282r13p1303r13p1372r13p1392r14p1505r15)
32
31
 
33
32
  ## Install
34
33
 
@@ -52,75 +51,85 @@ import { Motherboard, active, battery, connect, disconnect, info, notify, stream
52
51
  const motherboardButton = document.querySelector("#motherboard")
53
52
 
54
53
  motherboardButton.addEventListener("click", () => {
55
- connect(Motherboard, async () => {
56
- // Listen for stream notifications
57
- notify((data) => {
58
- // { massTotal: "0", massMax: "0", massAverage: "0", massLeft: "0", massCenter: "0", massRight: "0" }
59
- console.log(data)
60
- })
61
-
62
- // Check if device is being used
63
- active((value) => {
64
- console.log(value)
65
- })
66
-
67
- // Read battery + device info
68
- await battery(Motherboard)
69
- await info(Motherboard)
70
-
71
- // trigger LEDs
72
- // await led(device)
73
-
74
- // Start weight streaming (for a minute) remove parameter for a continues stream
75
- await stream(Motherboard, 60000)
76
-
77
- // Manualy tare the device when the stream is running
78
- // await tare(5000)
79
-
80
- // Manually call stop method if stream is continues
81
- // await stop(Motherboard)
82
-
83
- // Download data to CSV: format => timestamp, frame, battery, samples, masses
84
- // download()
85
-
86
- // Disconnect from device after we are done
87
- disconnect(Motherboard)
88
- })
54
+ connect(
55
+ Motherboard,
56
+ async () => {
57
+ // Listen for stream notifications
58
+ notify((data) => {
59
+ // { massTotal: "0", massMax: "0", massAverage: "0", massLeft: "0", massCenter: "0", massRight: "0" }
60
+ console.log(data)
61
+ })
62
+
63
+ // Check if device is being used
64
+ active((value) => {
65
+ console.log(value)
66
+ })
67
+
68
+ // Read battery + device info
69
+ await battery(Motherboard)
70
+ await info(Motherboard)
71
+
72
+ // trigger LEDs
73
+ // await led(device)
74
+
75
+ // Start weight streaming (for a minute) remove parameter for a continues stream
76
+ await stream(Motherboard, 60000)
77
+
78
+ // Manualy tare the device when the stream is running
79
+ // await tare(5000)
80
+
81
+ // Manually call stop method if stream is continues
82
+ // await stop(Motherboard)
83
+
84
+ // Download data as CSV, JSON, or XML (default: CSV) format => timestamp, frame, battery, samples, masses
85
+ // download('json')
86
+
87
+ // Disconnect from device after we are done
88
+ disconnect(Motherboard)
89
+ },
90
+ (error) => {
91
+ // Optinal custom error handeling
92
+ console.error(error.message)
93
+ },
94
+ )
89
95
  })
90
96
  ```
91
97
 
92
- ## Roadmap
98
+ ## Device support
93
99
 
94
- **Help wanted:** Do you own any of these devices? Use Google Chrome's Bluetooth Internals
100
+ - Griptonite - Motherboard
101
+ - ✅ Tindeq - Progressor
102
+ - ✅ Weiheng - WH-C06
103
+ - By default [watchAdvertisements](https://chromestatus.com/feature/5180688812736512) isn't supported . For Chrome,
104
+ enable it at `chrome://flags/#enable-experimental-web-platform-features`.
105
+ - ✅ Kilter Board
106
+ - ⏳ Entralpi (not verified)
107
+ - ➡️ Climbro
108
+ - ➡️ Smartboard Climbing - mySmartBoard
109
+
110
+ ## Features
111
+
112
+ **Help wanted:** Do you own any of the missing devices? Use Google Chrome's Bluetooth Internals
95
113
  `chrome://bluetooth-internals/#devices` and press `Start Scan` to look for your device, click on `Inspect` and share all
96
114
  available services with us.
97
115
 
98
- ### Device support
99
-
100
- - ✅ Griptonite Motherboard
101
- - ✅ Tindeq Progressor
102
- - Entralpi (not verified)
103
- - Kilterboard (see example)
104
- - ⏳ Weiheng WH-C06 / MAT Muscle Meter
105
- - Enable: `chrome://flags#enable-experimental-web-platform-features`
106
- - ➡️ Climbro
107
- - ➡️ mySmartBoard
108
-
109
- ### Features
110
-
111
- - ✅ Connect / Disconnect
112
- - ✅ Start / Stop data stream
113
- - ✅ Battery status
114
- - ✅ Read calibration
115
- - ✅ Device info: firmware / serial etc.
116
- - ✅ Check if device is connected
117
- - ✅ Check if device is being used
118
- - ✅ Peak / Average load
119
- - ✅️ Tare / unladen weight
120
- - ✅️ Download data to CVS
121
- - ➡️ Endurance
122
- - ➡️ Rate of Force Development: RFD
123
- - ➡️ Critical Force
116
+ | | Motherboard | Progressor | WH-C06 | Entralpi | Kilter Board | Climbro | mySmartBoard |
117
+ | --------------------------------------------------------------------------------------- | ----------- | ---------- | ------ | -------- | ------------ | ------- | ------------ |
118
+ | [Battery](https://stevie-ray.github.io/hangtime-grip-connect/api/battery.html) | | ✅ | | | | | |
119
+ | [Calibration](https://stevie-ray.github.io/hangtime-grip-connect/api/calibration.html) | | | | | | | |
120
+ | [Connect](https://stevie-ray.github.io/hangtime-grip-connect/api/connect.html) | ✅ | ✅ | ✅ | ✅ | ✅ | | |
121
+ | [Disconnect](https://stevie-ray.github.io/hangtime-grip-connect/api/disconnect.html) | ✅ | ✅ | ✅ | ✅ | ✅ | | |
122
+ | [Download](https://stevie-ray.github.io/hangtime-grip-connect/api/download.html) | ✅ | ✅ | | | | | |
123
+ | [Info](https://stevie-ray.github.io/hangtime-grip-connect/api/info.html) | ✅ | ✅ | | | | | |
124
+ | [isActive](https://stevie-ray.github.io/hangtime-grip-connect/api/is-active.html) | ✅ | ✅ | ✅ | ✅ | | | |
125
+ | [isConnected](https://stevie-ray.github.io/hangtime-grip-connect/api/is-connected.html) | ✅ | ✅ | ✅ | ✅ | ✅ | | |
126
+ | [Led](https://stevie-ray.github.io/hangtime-grip-connect/api/led.html) | ✅ | | | | ✅ | | |
127
+ | [Notify](https://stevie-ray.github.io/hangtime-grip-connect/api/notify.html) | ✅ | ✅ | ✅ | ✅ | | | |
128
+ | [Read](https://stevie-ray.github.io/hangtime-grip-connect/api/read.html) | ✅ | | | | | | |
129
+ | [Stop](https://stevie-ray.github.io/hangtime-grip-connect/api/stop.html) | | ✅ | | | | | |
130
+ | [Stream](https://stevie-ray.github.io/hangtime-grip-connect/api/stream.html) | | ✅ | | | | | |
131
+ | [Tare](https://stevie-ray.github.io/hangtime-grip-connect/api/tare.html) | | ✅ | ✅ | ✅ | | | |
132
+ | [Write](https://stevie-ray.github.io/hangtime-grip-connect/api/write.html) | | ✅ | | | | | |
124
133
 
125
134
  ## Development
126
135
 
@@ -146,7 +155,7 @@ A special thank you to:
146
155
  - [@1-max-1](https://github.com/1-max-1) for the docs on his Kilter Board
147
156
  [simulator](https://github.com/1-max-1/fake_kilter_board) that I coverted to
148
157
  [hangtime-arduino-kilterboard](https://github.com/Stevie-Ray/hangtime-arduino-kilterboard).
149
- - [sebws](https://github.com/sebw) for a [code sample](https://github.com/sebws/Crane) of the Weiheng WH-C06 App.
158
+ - [@sebws](https://github.com/sebw) for a [code sample](https://github.com/sebws/Crane) of the Weiheng WH-C06 App.
150
159
 
151
160
  ## Disclaimer
152
161
 
@@ -21,3 +21,15 @@ export declare enum KilterBoardPacket {
21
21
  /** If this packet is the only packet in the message, the byte gets set to 84 (T). Note that this takes priority over the other conditions. */
22
22
  V3_ONLY = 84
23
23
  }
24
+ /**
25
+ * Extracted from placement_roles database table.
26
+ */
27
+ export declare const KilterBoardPlacementRoles: {
28
+ id: number;
29
+ product_id: number;
30
+ position: number;
31
+ name: string;
32
+ full_name: string;
33
+ led_color: string;
34
+ screen_color: string;
35
+ }[];
@@ -22,3 +22,44 @@ export var KilterBoardPacket;
22
22
  /** If this packet is the only packet in the message, the byte gets set to 84 (T). Note that this takes priority over the other conditions. */
23
23
  KilterBoardPacket[KilterBoardPacket["V3_ONLY"] = 84] = "V3_ONLY";
24
24
  })(KilterBoardPacket || (KilterBoardPacket = {}));
25
+ /**
26
+ * Extracted from placement_roles database table.
27
+ */
28
+ export const KilterBoardPlacementRoles = [
29
+ {
30
+ id: 12,
31
+ product_id: 1,
32
+ position: 1,
33
+ name: "start",
34
+ full_name: "Start",
35
+ led_color: "00FF00",
36
+ screen_color: "00DD00",
37
+ },
38
+ {
39
+ id: 13,
40
+ product_id: 1,
41
+ position: 2,
42
+ name: "middle",
43
+ full_name: "Middle",
44
+ led_color: "00FFFF",
45
+ screen_color: "00FFFF",
46
+ },
47
+ {
48
+ id: 14,
49
+ product_id: 1,
50
+ position: 3,
51
+ name: "finish",
52
+ full_name: "Finish",
53
+ led_color: "FF00FF",
54
+ screen_color: "FF00FF",
55
+ },
56
+ {
57
+ id: 15,
58
+ product_id: 1,
59
+ position: 4,
60
+ name: "foot",
61
+ full_name: "Foot Only",
62
+ led_color: "FFA500",
63
+ screen_color: "FFA500",
64
+ },
65
+ ];
package/dist/connect.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Device } from "./types/devices";
2
2
  /**
3
3
  * Connects to a Bluetooth device.
4
4
  * @param {Device} board - The device to connect to.
5
- * @param {Function} onSuccess - Callback function to execute on successful connection.
5
+ * @param {Function} [onSuccess] - Optional callback function to execute on successful connection. Default logs success.
6
+ * @param {Function} [onError] - Optional callback function to execute on error. Default logs the error.
6
7
  */
7
- export declare const connect: (board: Device, onSuccess: () => void) => Promise<void>;
8
+ export declare const connect: (board: Device, onSuccess?: () => void, onError?: (error: Error) => void) => Promise<void>;
package/dist/connect.js CHANGED
@@ -9,7 +9,7 @@ const receiveBuffer = [];
9
9
  const onDisconnected = (event, board) => {
10
10
  board.device = undefined;
11
11
  const device = event.target;
12
- console.log(`Device ${device.name} is disconnected.`);
12
+ throw new Error(`Device ${device.name} is disconnected.`);
13
13
  };
14
14
  /**
15
15
  * Handles notifications received from a characteristic.
@@ -61,46 +61,40 @@ const handleNotifications = (event, board) => {
61
61
  * @param {Function} onSuccess - Callback function to execute on successful connection.
62
62
  */
63
63
  const onConnected = async (board, onSuccess) => {
64
- try {
65
- // Connect to GATT server and set up characteristics
66
- const services = await server.getPrimaryServices();
67
- if (!services || services.length === 0) {
68
- console.error("No services found");
69
- return;
70
- }
71
- for (const service of services) {
72
- const matchingService = board.services.find((boardService) => boardService.uuid === service.uuid);
73
- if (matchingService) {
74
- // Android bug: Introduce a delay before getting characteristics
75
- await new Promise((resolve) => setTimeout(resolve, 100));
76
- const characteristics = await service.getCharacteristics();
77
- for (const characteristic of matchingService.characteristics) {
78
- const matchingCharacteristic = characteristics.find((char) => char.uuid === characteristic.uuid);
79
- if (matchingCharacteristic) {
80
- const element = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid);
81
- if (element) {
82
- element.characteristic = matchingCharacteristic;
83
- // notify
84
- if (element.id === "rx") {
85
- matchingCharacteristic.startNotifications();
86
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event) => {
87
- handleNotifications(event, board);
88
- });
89
- }
64
+ // Connect to GATT server and set up characteristics
65
+ const services = await server.getPrimaryServices();
66
+ if (!services || services.length === 0) {
67
+ throw new Error("No services found");
68
+ }
69
+ for (const service of services) {
70
+ const matchingService = board.services.find((boardService) => boardService.uuid === service.uuid);
71
+ if (matchingService) {
72
+ // Android bug: Introduce a delay before getting characteristics
73
+ await new Promise((resolve) => setTimeout(resolve, 100));
74
+ const characteristics = await service.getCharacteristics();
75
+ for (const characteristic of matchingService.characteristics) {
76
+ const matchingCharacteristic = characteristics.find((char) => char.uuid === characteristic.uuid);
77
+ if (matchingCharacteristic) {
78
+ const element = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid);
79
+ if (element) {
80
+ element.characteristic = matchingCharacteristic;
81
+ // notify
82
+ if (element.id === "rx") {
83
+ matchingCharacteristic.startNotifications();
84
+ matchingCharacteristic.addEventListener("characteristicvaluechanged", (event) => {
85
+ handleNotifications(event, board);
86
+ });
90
87
  }
91
88
  }
92
- else {
93
- console.warn(`Characteristic ${characteristic.uuid} not found in service ${service.uuid}`);
94
- }
89
+ }
90
+ else {
91
+ throw new Error(`Characteristic ${characteristic.uuid} not found in service ${service.uuid}`);
95
92
  }
96
93
  }
97
94
  }
98
- // Call the onSuccess callback after successful connection and setup
99
- onSuccess();
100
- }
101
- catch (error) {
102
- console.error(error);
103
95
  }
96
+ // Call the onSuccess callback after successful connection and setup
97
+ onSuccess();
104
98
  };
105
99
  /**
106
100
  * Returns UUIDs of all services associated with the device.
@@ -113,9 +107,10 @@ const getAllServiceUUIDs = (device) => {
113
107
  /**
114
108
  * Connects to a Bluetooth device.
115
109
  * @param {Device} board - The device to connect to.
116
- * @param {Function} onSuccess - Callback function to execute on successful connection.
110
+ * @param {Function} [onSuccess] - Optional callback function to execute on successful connection. Default logs success.
111
+ * @param {Function} [onError] - Optional callback function to execute on error. Default logs the error.
117
112
  */
118
- export const connect = async (board, onSuccess) => {
113
+ export const connect = async (board, onSuccess = () => console.log("Connected successfully"), onError = (error) => console.error(error)) => {
119
114
  try {
120
115
  // Request device and set up connection
121
116
  const deviceServices = getAllServiceUUIDs(board);
@@ -128,8 +123,7 @@ export const connect = async (board, onSuccess) => {
128
123
  });
129
124
  board.device = device;
130
125
  if (!board.device.gatt) {
131
- console.error("GATT is not available on this device");
132
- return;
126
+ throw new Error("GATT is not available on this device");
133
127
  }
134
128
  board.device.addEventListener("gattserverdisconnected", (event) => {
135
129
  onDisconnected(event, board);
@@ -145,8 +139,17 @@ export const connect = async (board, onSuccess) => {
145
139
  handleWHC06Data(manufacturerData);
146
140
  }
147
141
  });
142
+ // When the companyIdentifier is provided we want to get manufacturerData using watchAdvertisements.
148
143
  if (optionalManufacturerData.length) {
149
- await board.device.watchAdvertisements();
144
+ // Receive events when the system receives an advertisement packet from a watched device.
145
+ // To use this function in Chrome: chrome://flags/#enable-experimental-web-platform-features has to be enabled.
146
+ // More info: https://chromestatus.com/feature/5180688812736512
147
+ if (typeof board.device.watchAdvertisements === "function") {
148
+ await board.device.watchAdvertisements();
149
+ }
150
+ else {
151
+ throw new Error("watchAdvertisements isn't supported. For Chrome, enable it at chrome://flags/#enable-experimental-web-platform-features.");
152
+ }
150
153
  }
151
154
  server = await board.device.gatt.connect();
152
155
  if (server.connected) {
@@ -154,6 +157,6 @@ export const connect = async (board, onSuccess) => {
154
157
  }
155
158
  }
156
159
  catch (error) {
157
- console.error(error);
160
+ onError(error);
158
161
  }
159
162
  };
@@ -5,6 +5,12 @@ import type { DownloadPacket } from "./types/download";
5
5
  export declare const DownloadPackets: DownloadPacket[];
6
6
  export declare const emptyDownloadPackets: () => void;
7
7
  /**
8
- * Exports the data as a CSV file.
8
+ * Exports the data in the specified format (CSV, JSON, XML) with a filename format:
9
+ * 'data-export-YYYY-MM-DD-HH-MM-SS.{format}'.
10
+ *
11
+ * @param {('csv' | 'json' | 'xml')} [format='csv'] - The format in which to download the data.
12
+ * Defaults to 'csv'. Accepted values are 'csv', 'json', and 'xml'.
13
+ *
14
+ * @returns {void} Initiates a download of the data in the specified format.
9
15
  */
10
- export declare const download: () => void;
16
+ export declare const download: (format?: "csv" | "json" | "xml") => void;
package/dist/download.js CHANGED
@@ -26,20 +26,76 @@ const packetsToCSV = (data) => {
26
26
  .join("\r\n");
27
27
  };
28
28
  /**
29
- * Exports the data as a CSV file.
29
+ * Converts an array of DownloadPacket objects to a JSON string.
30
+ * @param data - Array of DownloadPacket objects.
31
+ * @returns JSON string representation of the data.
32
+ */
33
+ const packetsToJSON = (data) => {
34
+ return JSON.stringify(data, null, 2); // Pretty print JSON with 2-space indentation
35
+ };
36
+ /**
37
+ * Converts an array of DownloadPacket objects to an XML string.
38
+ * @param data - Array of DownloadPacket objects.
39
+ * @returns XML string representation of the data.
40
+ */
41
+ const packetsToXML = (data) => {
42
+ const xmlPackets = data
43
+ .map((packet) => {
44
+ const samples = packet.samples.map((sample) => `<sample>${sample}</sample>`).join("");
45
+ const masses = packet.masses.map((mass) => `<mass>${mass}</mass>`).join("");
46
+ return `
47
+ <packet>
48
+ <received>${packet.received}</received>
49
+ <sampleNum>${packet.sampleNum}</sampleNum>
50
+ <battRaw>${packet.battRaw}</battRaw>
51
+ <samples>${samples}</samples>
52
+ <masses>${masses}</masses>
53
+ </packet>
54
+ `;
55
+ })
56
+ .join("");
57
+ return `<DownloadPackets>${xmlPackets}</DownloadPackets>`;
58
+ };
59
+ /**
60
+ * Exports the data in the specified format (CSV, JSON, XML) with a filename format:
61
+ * 'data-export-YYYY-MM-DD-HH-MM-SS.{format}'.
62
+ *
63
+ * @param {('csv' | 'json' | 'xml')} [format='csv'] - The format in which to download the data.
64
+ * Defaults to 'csv'. Accepted values are 'csv', 'json', and 'xml'.
65
+ *
66
+ * @returns {void} Initiates a download of the data in the specified format.
30
67
  */
31
- export const download = () => {
32
- // Generate CSV string from DownloadPackets array
33
- const csvContent = packetsToCSV(DownloadPackets);
34
- // Create a Blob object containing the CSV data
35
- const blob = new Blob([csvContent], { type: "text/csv" });
68
+ export const download = (format = "csv") => {
69
+ let content = "";
70
+ let mimeType = "";
71
+ let fileName = "";
72
+ if (format === "csv") {
73
+ content = packetsToCSV(DownloadPackets);
74
+ mimeType = "text/csv";
75
+ }
76
+ else if (format === "json") {
77
+ content = packetsToJSON(DownloadPackets);
78
+ mimeType = "application/json";
79
+ }
80
+ else if (format === "xml") {
81
+ content = packetsToXML(DownloadPackets);
82
+ mimeType = "application/xml";
83
+ }
84
+ const now = new Date();
85
+ // YYYY-MM-DD
86
+ const date = now.toISOString().split("T")[0];
87
+ // HH-MM-SS
88
+ const time = now.toTimeString().split(" ")[0].replace(/:/g, "-");
89
+ fileName = `data-export-${date}-${time}.${format}`;
90
+ // Create a Blob object containing the data
91
+ const blob = new Blob([content], { type: mimeType });
36
92
  // Create a URL for the Blob
37
93
  const url = window.URL.createObjectURL(blob);
38
94
  // Create a link element
39
95
  const link = document.createElement("a");
40
96
  // Set link attributes
41
97
  link.href = url;
42
- link.setAttribute("download", "data.csv");
98
+ link.setAttribute("download", fileName);
43
99
  // Append link to document body
44
100
  document.body.appendChild(link);
45
101
  // Programmatically click the link to trigger the download
package/dist/led.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { Device } from "./types/devices";
2
2
  declare class ClimbPlacement {
3
3
  position: number;
4
- role_id: string;
5
- constructor(position: number, role_id: string);
4
+ role_id: number;
5
+ constructor(position: number, role_id: number);
6
6
  }
7
7
  /**
8
8
  * Prepares byte arrays for transmission based on a list of climb placements.
package/dist/led.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { write } from "./write";
2
2
  import { isConnected } from "./is-connected";
3
3
  import { KilterBoard, Motherboard } from "./devices";
4
- import { KilterBoardPacket } from "./commands/kilterboard";
4
+ import { KilterBoardPacket, KilterBoardPlacementRoles } from "./commands/kilterboard";
5
5
  /**
6
6
  * Maximum length of the message body for byte wrapping.
7
7
  */
@@ -102,8 +102,11 @@ export function prepBytesV3(climbPlacementList) {
102
102
  resultArray.push(tempArray);
103
103
  tempArray = [KilterBoardPacket.V3_MIDDLE];
104
104
  }
105
- const ledColor = climbPlacement.role_id;
106
- const encodedPlacement = encodePlacement(climbPlacement.position, ledColor);
105
+ const role = KilterBoardPlacementRoles.find((placement) => placement.id === climbPlacement.role_id);
106
+ if (!role) {
107
+ throw new Error(`Role with id ${climbPlacement.role_id} not found in placement_roles`);
108
+ }
109
+ const encodedPlacement = encodePlacement(climbPlacement.position, role.led_color);
107
110
  tempArray.push(...encodedPlacement);
108
111
  }
109
112
  resultArray.push(tempArray);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hangtime/grip-connect",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
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 Griptonite Motherboard, Climbro, SmartBoard, Entralpi or Tindeq Progressor",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -23,7 +23,9 @@
23
23
  "motherboard",
24
24
  "tindeq",
25
25
  "progressor",
26
- "entralpi"
26
+ "entralpi",
27
+ "wh-c06",
28
+ "kilterboard"
27
29
  ],
28
30
  "bugs": {
29
31
  "url": "https://github.com/Stevie-Ray/hangtime-grip-connect/issues"
@@ -21,3 +21,44 @@ export enum KilterBoardPacket {
21
21
  /** If this packet is the only packet in the message, the byte gets set to 84 (T). Note that this takes priority over the other conditions. */
22
22
  V3_ONLY,
23
23
  }
24
+ /**
25
+ * Extracted from placement_roles database table.
26
+ */
27
+ export const KilterBoardPlacementRoles = [
28
+ {
29
+ id: 12,
30
+ product_id: 1,
31
+ position: 1,
32
+ name: "start",
33
+ full_name: "Start",
34
+ led_color: "00FF00",
35
+ screen_color: "00DD00",
36
+ },
37
+ {
38
+ id: 13,
39
+ product_id: 1,
40
+ position: 2,
41
+ name: "middle",
42
+ full_name: "Middle",
43
+ led_color: "00FFFF",
44
+ screen_color: "00FFFF",
45
+ },
46
+ {
47
+ id: 14,
48
+ product_id: 1,
49
+ position: 3,
50
+ name: "finish",
51
+ full_name: "Finish",
52
+ led_color: "FF00FF",
53
+ screen_color: "FF00FF",
54
+ },
55
+ {
56
+ id: 15,
57
+ product_id: 1,
58
+ position: 4,
59
+ name: "foot",
60
+ full_name: "Foot Only",
61
+ led_color: "FFA500",
62
+ screen_color: "FFA500",
63
+ },
64
+ ]
package/src/connect.ts CHANGED
@@ -12,7 +12,7 @@ const receiveBuffer: number[] = []
12
12
  const onDisconnected = (event: Event, board: Device): void => {
13
13
  board.device = undefined
14
14
  const device = event.target as BluetoothDevice
15
- console.log(`Device ${device.name} is disconnected.`)
15
+ throw new Error(`Device ${device.name} is disconnected.`)
16
16
  }
17
17
  /**
18
18
  * Handles notifications received from a characteristic.
@@ -62,52 +62,47 @@ const handleNotifications = (event: Event, board: Device): void => {
62
62
  * @param {Function} onSuccess - Callback function to execute on successful connection.
63
63
  */
64
64
  const onConnected = async (board: Device, onSuccess: () => void): Promise<void> => {
65
- try {
66
- // Connect to GATT server and set up characteristics
67
- const services: BluetoothRemoteGATTService[] = await server.getPrimaryServices()
65
+ // Connect to GATT server and set up characteristics
66
+ const services: BluetoothRemoteGATTService[] = await server.getPrimaryServices()
68
67
 
69
- if (!services || services.length === 0) {
70
- console.error("No services found")
71
- return
72
- }
68
+ if (!services || services.length === 0) {
69
+ throw new Error("No services found")
70
+ }
73
71
 
74
- for (const service of services) {
75
- const matchingService = board.services.find((boardService) => boardService.uuid === service.uuid)
72
+ for (const service of services) {
73
+ const matchingService = board.services.find((boardService) => boardService.uuid === service.uuid)
76
74
 
77
- if (matchingService) {
78
- // Android bug: Introduce a delay before getting characteristics
79
- await new Promise((resolve) => setTimeout(resolve, 100))
75
+ if (matchingService) {
76
+ // Android bug: Introduce a delay before getting characteristics
77
+ await new Promise((resolve) => setTimeout(resolve, 100))
80
78
 
81
- const characteristics = await service.getCharacteristics()
79
+ const characteristics = await service.getCharacteristics()
82
80
 
83
- for (const characteristic of matchingService.characteristics) {
84
- const matchingCharacteristic = characteristics.find((char) => char.uuid === characteristic.uuid)
81
+ for (const characteristic of matchingService.characteristics) {
82
+ const matchingCharacteristic = characteristics.find((char) => char.uuid === characteristic.uuid)
85
83
 
86
- if (matchingCharacteristic) {
87
- const element = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid)
88
- if (element) {
89
- element.characteristic = matchingCharacteristic
84
+ if (matchingCharacteristic) {
85
+ const element = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid)
86
+ if (element) {
87
+ element.characteristic = matchingCharacteristic
90
88
 
91
- // notify
92
- if (element.id === "rx") {
93
- matchingCharacteristic.startNotifications()
94
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event: Event) => {
95
- handleNotifications(event, board)
96
- })
97
- }
89
+ // notify
90
+ if (element.id === "rx") {
91
+ matchingCharacteristic.startNotifications()
92
+ matchingCharacteristic.addEventListener("characteristicvaluechanged", (event: Event) => {
93
+ handleNotifications(event, board)
94
+ })
98
95
  }
99
- } else {
100
- console.warn(`Characteristic ${characteristic.uuid} not found in service ${service.uuid}`)
101
96
  }
97
+ } else {
98
+ throw new Error(`Characteristic ${characteristic.uuid} not found in service ${service.uuid}`)
102
99
  }
103
100
  }
104
101
  }
105
-
106
- // Call the onSuccess callback after successful connection and setup
107
- onSuccess()
108
- } catch (error) {
109
- console.error(error)
110
102
  }
103
+
104
+ // Call the onSuccess callback after successful connection and setup
105
+ onSuccess()
111
106
  }
112
107
  /**
113
108
  * Returns UUIDs of all services associated with the device.
@@ -120,9 +115,14 @@ const getAllServiceUUIDs = (device: Device) => {
120
115
  /**
121
116
  * Connects to a Bluetooth device.
122
117
  * @param {Device} board - The device to connect to.
123
- * @param {Function} onSuccess - Callback function to execute on successful connection.
118
+ * @param {Function} [onSuccess] - Optional callback function to execute on successful connection. Default logs success.
119
+ * @param {Function} [onError] - Optional callback function to execute on error. Default logs the error.
124
120
  */
125
- export const connect = async (board: Device, onSuccess: () => void): Promise<void> => {
121
+ export const connect = async (
122
+ board: Device,
123
+ onSuccess: () => void = () => console.log("Connected successfully"),
124
+ onError: (error: Error) => void = (error) => console.error(error),
125
+ ): Promise<void> => {
126
126
  try {
127
127
  // Request device and set up connection
128
128
  const deviceServices = getAllServiceUUIDs(board)
@@ -141,8 +141,7 @@ export const connect = async (board: Device, onSuccess: () => void): Promise<voi
141
141
  board.device = device
142
142
 
143
143
  if (!board.device.gatt) {
144
- console.error("GATT is not available on this device")
145
- return
144
+ throw new Error("GATT is not available on this device")
146
145
  }
147
146
 
148
147
  board.device.addEventListener("gattserverdisconnected", (event) => {
@@ -162,8 +161,18 @@ export const connect = async (board: Device, onSuccess: () => void): Promise<voi
162
161
  }
163
162
  })
164
163
 
164
+ // When the companyIdentifier is provided we want to get manufacturerData using watchAdvertisements.
165
165
  if (optionalManufacturerData.length) {
166
- await board.device.watchAdvertisements()
166
+ // Receive events when the system receives an advertisement packet from a watched device.
167
+ // To use this function in Chrome: chrome://flags/#enable-experimental-web-platform-features has to be enabled.
168
+ // More info: https://chromestatus.com/feature/5180688812736512
169
+ if (typeof board.device.watchAdvertisements === "function") {
170
+ await board.device.watchAdvertisements()
171
+ } else {
172
+ throw new Error(
173
+ "watchAdvertisements isn't supported. For Chrome, enable it at chrome://flags/#enable-experimental-web-platform-features.",
174
+ )
175
+ }
167
176
  }
168
177
 
169
178
  server = await board.device.gatt.connect()
@@ -172,6 +181,6 @@ export const connect = async (board: Device, onSuccess: () => void): Promise<voi
172
181
  await onConnected(board, onSuccess)
173
182
  }
174
183
  } catch (error) {
175
- console.error(error)
184
+ onError(error as Error)
176
185
  }
177
186
  }
package/src/download.ts CHANGED
@@ -32,14 +32,74 @@ const packetsToCSV = (data: DownloadPacket[]): string => {
32
32
  }
33
33
 
34
34
  /**
35
- * Exports the data as a CSV file.
35
+ * Converts an array of DownloadPacket objects to a JSON string.
36
+ * @param data - Array of DownloadPacket objects.
37
+ * @returns JSON string representation of the data.
38
+ */
39
+ const packetsToJSON = (data: DownloadPacket[]): string => {
40
+ return JSON.stringify(data, null, 2) // Pretty print JSON with 2-space indentation
41
+ }
42
+
43
+ /**
44
+ * Converts an array of DownloadPacket objects to an XML string.
45
+ * @param data - Array of DownloadPacket objects.
46
+ * @returns XML string representation of the data.
47
+ */
48
+ const packetsToXML = (data: DownloadPacket[]): string => {
49
+ const xmlPackets = data
50
+ .map((packet) => {
51
+ const samples = packet.samples.map((sample) => `<sample>${sample}</sample>`).join("")
52
+ const masses = packet.masses.map((mass) => `<mass>${mass}</mass>`).join("")
53
+ return `
54
+ <packet>
55
+ <received>${packet.received}</received>
56
+ <sampleNum>${packet.sampleNum}</sampleNum>
57
+ <battRaw>${packet.battRaw}</battRaw>
58
+ <samples>${samples}</samples>
59
+ <masses>${masses}</masses>
60
+ </packet>
61
+ `
62
+ })
63
+ .join("")
64
+
65
+ return `<DownloadPackets>${xmlPackets}</DownloadPackets>`
66
+ }
67
+
68
+ /**
69
+ * Exports the data in the specified format (CSV, JSON, XML) with a filename format:
70
+ * 'data-export-YYYY-MM-DD-HH-MM-SS.{format}'.
71
+ *
72
+ * @param {('csv' | 'json' | 'xml')} [format='csv'] - The format in which to download the data.
73
+ * Defaults to 'csv'. Accepted values are 'csv', 'json', and 'xml'.
74
+ *
75
+ * @returns {void} Initiates a download of the data in the specified format.
36
76
  */
37
- export const download = (): void => {
38
- // Generate CSV string from DownloadPackets array
39
- const csvContent: string = packetsToCSV(DownloadPackets)
77
+ export const download = (format: "csv" | "json" | "xml" = "csv"): void => {
78
+ let content = ""
79
+ let mimeType = ""
80
+ let fileName = ""
81
+
82
+ if (format === "csv") {
83
+ content = packetsToCSV(DownloadPackets)
84
+ mimeType = "text/csv"
85
+ } else if (format === "json") {
86
+ content = packetsToJSON(DownloadPackets)
87
+ mimeType = "application/json"
88
+ } else if (format === "xml") {
89
+ content = packetsToXML(DownloadPackets)
90
+ mimeType = "application/xml"
91
+ }
92
+
93
+ const now = new Date()
94
+ // YYYY-MM-DD
95
+ const date = now.toISOString().split("T")[0]
96
+ // HH-MM-SS
97
+ const time = now.toTimeString().split(" ")[0].replace(/:/g, "-")
98
+
99
+ fileName = `data-export-${date}-${time}.${format}`
40
100
 
41
- // Create a Blob object containing the CSV data
42
- const blob = new Blob([csvContent], { type: "text/csv" })
101
+ // Create a Blob object containing the data
102
+ const blob = new Blob([content], { type: mimeType })
43
103
 
44
104
  // Create a URL for the Blob
45
105
  const url = window.URL.createObjectURL(blob)
@@ -49,7 +109,7 @@ export const download = (): void => {
49
109
 
50
110
  // Set link attributes
51
111
  link.href = url
52
- link.setAttribute("download", "data.csv")
112
+ link.setAttribute("download", fileName)
53
113
 
54
114
  // Append link to document body
55
115
  document.body.appendChild(link)
package/src/led.ts CHANGED
@@ -2,7 +2,8 @@ import type { Device } from "./types/devices"
2
2
  import { write } from "./write"
3
3
  import { isConnected } from "./is-connected"
4
4
  import { KilterBoard, Motherboard } from "./devices"
5
- import { KilterBoardPacket } from "./commands/kilterboard"
5
+ import { KilterBoardPacket, KilterBoardPlacementRoles } from "./commands/kilterboard"
6
+
6
7
  /**
7
8
  * Maximum length of the message body for byte wrapping.
8
9
  */
@@ -46,9 +47,9 @@ function wrapBytes(data: number[]) {
46
47
  }
47
48
  class ClimbPlacement {
48
49
  position: number
49
- role_id: string
50
+ role_id: number
50
51
 
51
- constructor(position: number, role_id: string) {
52
+ constructor(position: number, role_id: number) {
52
53
  this.position = position
53
54
  this.role_id = role_id
54
55
  }
@@ -109,10 +110,11 @@ export function prepBytesV3(climbPlacementList: ClimbPlacement[]) {
109
110
  resultArray.push(tempArray)
110
111
  tempArray = [KilterBoardPacket.V3_MIDDLE]
111
112
  }
112
-
113
- const ledColor = climbPlacement.role_id
114
-
115
- const encodedPlacement = encodePlacement(climbPlacement.position, ledColor)
113
+ const role = KilterBoardPlacementRoles.find((placement) => placement.id === climbPlacement.role_id)
114
+ if (!role) {
115
+ throw new Error(`Role with id ${climbPlacement.role_id} not found in placement_roles`)
116
+ }
117
+ const encodedPlacement = encodePlacement(climbPlacement.position, role.led_color)
116
118
  tempArray.push(...encodedPlacement)
117
119
  }
118
120