@hangtime/grip-connect 0.3.7 → 0.3.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
@@ -28,7 +28,7 @@ Learn more: [Docs](https://stevie-ray.github.io/hangtime-grip-connect/) -
28
28
  ## Try it out
29
29
 
30
30
  [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/)
31
+ [Kilter Board](https://grip-connect-kilter-board.vercel.app/?route=p1083r15p1117r15p1164r12p1185r12p1233r13p1282r13p1303r13p1372r13p1392r14p1505r15)
32
32
 
33
33
  ## Install
34
34
 
@@ -47,7 +47,7 @@ Simply importing the utilities you need from `@hangtime/grip-connect`.
47
47
  ```
48
48
 
49
49
  ```js
50
- import { Motherboard, battery, connect, disconnect, info, notify, stream } from "@hangtime/grip-connect"
50
+ import { Motherboard, active, battery, connect, disconnect, info, notify, stream } from "@hangtime/grip-connect"
51
51
 
52
52
  const motherboardButton = document.querySelector("#motherboard")
53
53
 
@@ -59,10 +59,18 @@ motherboardButton.addEventListener("click", () => {
59
59
  console.log(data)
60
60
  })
61
61
 
62
+ // Check if device is being used
63
+ active((value) => {
64
+ console.log(value)
65
+ })
66
+
62
67
  // Read battery + device info
63
68
  await battery(Motherboard)
64
69
  await info(Motherboard)
65
70
 
71
+ // trigger LEDs
72
+ // await led(device)
73
+
66
74
  // Start weight streaming (for a minute) remove parameter for a continues stream
67
75
  await stream(Motherboard, 60000)
68
76
 
@@ -72,8 +80,8 @@ motherboardButton.addEventListener("click", () => {
72
80
  // Manually call stop method if stream is continues
73
81
  // await stop(Motherboard)
74
82
 
75
- // Download data to CSV: format => timestamp, frame, battery, samples, masses
76
- // download()
83
+ // Download data as CSV, JSON, or XML (default: CSV) format => timestamp, frame, battery, samples, masses
84
+ // download('json')
77
85
 
78
86
  // Disconnect from device after we are done
79
87
  disconnect(Motherboard)
@@ -106,9 +114,10 @@ available services with us.
106
114
  - ✅ Read calibration
107
115
  - ✅ Device info: firmware / serial etc.
108
116
  - ✅ Check if device is connected
117
+ - ✅ Check if device is being used
109
118
  - ✅ Peak / Average load
110
119
  - ✅️ Tare / unladen weight
111
- - ✅️ Download data to CVS
120
+ - ✅️ Download data (CSV, JSON, XML)
112
121
  - ➡️ Endurance
113
122
  - ➡️ Rate of Force Development: RFD
114
123
  - ➡️ Critical Force
@@ -137,7 +146,7 @@ A special thank you to:
137
146
  - [@1-max-1](https://github.com/1-max-1) for the docs on his Kilter Board
138
147
  [simulator](https://github.com/1-max-1/fake_kilter_board) that I coverted to
139
148
  [hangtime-arduino-kilterboard](https://github.com/Stevie-Ray/hangtime-arduino-kilterboard).
140
- - [sebws](https://github.com/sebw) for a [code sample](https://github.com/sebws/Crane) of the Weiheng WH-C06 App.
149
+ - [@sebws](https://github.com/sebw) for a [code sample](https://github.com/sebws/Crane) of the Weiheng WH-C06 App.
141
150
 
142
151
  ## Disclaimer
143
152
 
@@ -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
+ ];
@@ -1,4 +1,5 @@
1
1
  import { notifyCallback } from "./../notify";
2
+ import { checkActivity } from "./../is-active";
2
3
  import { applyTare } from "./../tare";
3
4
  // Constants
4
5
  let MASS_MAX = "0";
@@ -21,6 +22,8 @@ export const handleEntralpiData = (receivedData) => {
21
22
  DATAPOINT_COUNT++;
22
23
  // Calculate the average dynamically
23
24
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1);
25
+ // Check if device is being used
26
+ checkActivity(numericData);
24
27
  // Notify with weight data
25
28
  notifyCallback({
26
29
  massMax: MASS_MAX,
@@ -1,6 +1,7 @@
1
1
  import { notifyCallback } from "./../notify";
2
2
  import { applyTare } from "./../tare";
3
3
  import { MotherboardCommands } from "./../commands";
4
+ import { checkActivity } from "./../is-active";
4
5
  import { lastWrite } from "./../write";
5
6
  import { DownloadPackets } from "./../download";
6
7
  // Constants
@@ -108,6 +109,8 @@ export const handleMotherboardData = (receivedData) => {
108
109
  DATAPOINT_COUNT++;
109
110
  // Calculate the average dynamically
110
111
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1);
112
+ // Check if device is being used
113
+ checkActivity(center);
111
114
  // Notify with weight data
112
115
  notifyCallback({
113
116
  massTotal: Math.max(-1000, left + center + right).toFixed(1),
@@ -1,5 +1,6 @@
1
1
  import { notifyCallback } from "./../notify";
2
2
  import { applyTare } from "./../tare";
3
+ import { checkActivity } from "./../is-active";
3
4
  import { ProgressorCommands, ProgressorResponses } from "./../commands/progressor";
4
5
  import { lastWrite } from "./../write";
5
6
  import struct from "./../struct";
@@ -39,6 +40,8 @@ export const handleProgressorData = (data) => {
39
40
  DATAPOINT_COUNT++;
40
41
  // Calculate the average dynamically
41
42
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1);
43
+ // Check if device is being used
44
+ checkActivity(weight);
42
45
  notifyCallback({
43
46
  massMax: MASS_MAX,
44
47
  massAverage: MASS_AVERAGE,
@@ -1,3 +1,4 @@
1
+ import { checkActivity } from "./../is-active";
1
2
  import { notifyCallback } from "./../notify";
2
3
  import { applyTare } from "./../tare";
3
4
  // Constants
@@ -26,6 +27,8 @@ export const handleWHC06Data = (data) => {
26
27
  DATAPOINT_COUNT++;
27
28
  // Calculate the average dynamically
28
29
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1);
30
+ // Check if device is being used
31
+ checkActivity(numericData);
29
32
  // Notify with weight data
30
33
  notifyCallback({
31
34
  massMax: MASS_MAX,
@@ -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/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { calibration } from "./calibration";
4
4
  export { download } from "./download";
5
5
  export { connect } from "./connect";
6
6
  export { disconnect } from "./disconnect";
7
+ export { active, isActive } from "./is-active";
7
8
  export { isConnected } from "./is-connected";
8
9
  export { info } from "./info";
9
10
  export { led } from "./led";
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { download } from "./download";
9
9
  // Export connection related functions
10
10
  export { connect } from "./connect";
11
11
  export { disconnect } from "./disconnect";
12
+ export { active, isActive } from "./is-active";
12
13
  export { isConnected } from "./is-connected";
13
14
  // Export information retrieval function
14
15
  export { info } from "./info";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Type definition for the callback function that is called when the activity status changes.
3
+ * @param {boolean} value - The new activity status (true if active, false if not).
4
+ */
5
+ type IsActiveCallback = (value: boolean) => void;
6
+ /**
7
+ * Indicates whether the device is currently active.
8
+ * @type {boolean}
9
+ */
10
+ export declare let isActive: boolean;
11
+ /**
12
+ * Sets the callback function to be called when the activity status changes.
13
+ *
14
+ * This function allows you to specify a callback that will be invoked whenever
15
+ * the activity status changes, indicating whether the device is currently active.
16
+ *
17
+ * @param {IsActiveCallback} callback - The callback function to be set. This function
18
+ * receives a boolean value indicating the new activity status.
19
+ * @returns {void}
20
+ */
21
+ export declare const active: (callback: IsActiveCallback) => void;
22
+ /**
23
+ * Checks if a dynamic value is active based on a threshold and duration.
24
+ *
25
+ * This function assesses whether a given dynamic value surpasses a specified threshold
26
+ * and remains active for a specified duration. If the activity status changes from
27
+ * the previous state, the callback function is called with the updated activity status.
28
+ *
29
+ * @param {number} input - The dynamic value to check for activity status.
30
+ * @param {number} [threshold=2.5] - The threshold value to determine if the input is considered active.
31
+ * Defaults to 2.5 if not provided.
32
+ * @param {number} [duration=1000] - The duration (in milliseconds) to monitor the input for activity.
33
+ * Defaults to 1000 milliseconds if not provided.
34
+ * @returns {Promise<void>} A promise that resolves once the activity check is complete.
35
+ */
36
+ export declare const checkActivity: (input: number, threshold?: number, duration?: number) => Promise<void>;
37
+ export {};
@@ -0,0 +1,49 @@
1
+ let activeCallback;
2
+ /**
3
+ * Indicates whether the device is currently active.
4
+ * @type {boolean}
5
+ */
6
+ export let isActive = false;
7
+ /**
8
+ * Sets the callback function to be called when the activity status changes.
9
+ *
10
+ * This function allows you to specify a callback that will be invoked whenever
11
+ * the activity status changes, indicating whether the device is currently active.
12
+ *
13
+ * @param {IsActiveCallback} callback - The callback function to be set. This function
14
+ * receives a boolean value indicating the new activity status.
15
+ * @returns {void}
16
+ */
17
+ export const active = (callback) => {
18
+ activeCallback = callback;
19
+ };
20
+ /**
21
+ * Checks if a dynamic value is active based on a threshold and duration.
22
+ *
23
+ * This function assesses whether a given dynamic value surpasses a specified threshold
24
+ * and remains active for a specified duration. If the activity status changes from
25
+ * the previous state, the callback function is called with the updated activity status.
26
+ *
27
+ * @param {number} input - The dynamic value to check for activity status.
28
+ * @param {number} [threshold=2.5] - The threshold value to determine if the input is considered active.
29
+ * Defaults to 2.5 if not provided.
30
+ * @param {number} [duration=1000] - The duration (in milliseconds) to monitor the input for activity.
31
+ * Defaults to 1000 milliseconds if not provided.
32
+ * @returns {Promise<void>} A promise that resolves once the activity check is complete.
33
+ */
34
+ export const checkActivity = (input, threshold = 2.5, duration = 1000) => {
35
+ return new Promise((resolve) => {
36
+ // Check the activity status after the specified duration
37
+ setTimeout(() => {
38
+ // Determine the activity status based on the threshold
39
+ const activeNow = input > threshold;
40
+ if (isActive !== activeNow) {
41
+ isActive = activeNow;
42
+ if (activeCallback) {
43
+ activeCallback(activeNow);
44
+ }
45
+ }
46
+ resolve();
47
+ }, duration);
48
+ });
49
+ };
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.7",
3
+ "version": "0.3.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 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
+ ]
@@ -1,4 +1,5 @@
1
1
  import { notifyCallback } from "./../notify"
2
+ import { checkActivity } from "./../is-active"
2
3
  import { applyTare } from "./../tare"
3
4
 
4
5
  // Constants
@@ -28,6 +29,9 @@ export const handleEntralpiData = (receivedData: string): void => {
28
29
  // Calculate the average dynamically
29
30
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
30
31
 
32
+ // Check if device is being used
33
+ checkActivity(numericData)
34
+
31
35
  // Notify with weight data
32
36
  notifyCallback({
33
37
  massMax: MASS_MAX,
@@ -1,6 +1,7 @@
1
1
  import { notifyCallback } from "./../notify"
2
2
  import { applyTare } from "./../tare"
3
3
  import { MotherboardCommands } from "./../commands"
4
+ import { checkActivity } from "./../is-active"
4
5
  import { lastWrite } from "./../write"
5
6
  import { DownloadPackets } from "./../download"
6
7
  import type { DownloadPacket } from "./../types/download"
@@ -131,6 +132,9 @@ export const handleMotherboardData = (receivedData: string): void => {
131
132
  // Calculate the average dynamically
132
133
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
133
134
 
135
+ // Check if device is being used
136
+ checkActivity(center)
137
+
134
138
  // Notify with weight data
135
139
  notifyCallback({
136
140
  massTotal: Math.max(-1000, left + center + right).toFixed(1),
@@ -1,5 +1,6 @@
1
1
  import { notifyCallback } from "./../notify"
2
2
  import { applyTare } from "./../tare"
3
+ import { checkActivity } from "./../is-active"
3
4
  import { ProgressorCommands, ProgressorResponses } from "./../commands/progressor"
4
5
  import { lastWrite } from "./../write"
5
6
  import struct from "./../struct"
@@ -43,6 +44,9 @@ export const handleProgressorData = (data: DataView): void => {
43
44
  // Calculate the average dynamically
44
45
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
45
46
 
47
+ // Check if device is being used
48
+ checkActivity(weight)
49
+
46
50
  notifyCallback({
47
51
  massMax: MASS_MAX,
48
52
  massAverage: MASS_AVERAGE,
@@ -1,3 +1,4 @@
1
+ import { checkActivity } from "./../is-active"
1
2
  import { notifyCallback } from "./../notify"
2
3
  import { applyTare } from "./../tare"
3
4
 
@@ -34,6 +35,9 @@ export const handleWHC06Data = (data: DataView): void => {
34
35
  // Calculate the average dynamically
35
36
  MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
36
37
 
38
+ // Check if device is being used
39
+ checkActivity(numericData)
40
+
37
41
  // Notify with weight data
38
42
  notifyCallback({
39
43
  massMax: MASS_MAX,
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/index.ts CHANGED
@@ -13,6 +13,7 @@ export { download } from "./download"
13
13
  // Export connection related functions
14
14
  export { connect } from "./connect"
15
15
  export { disconnect } from "./disconnect"
16
+ export { active, isActive } from "./is-active"
16
17
  export { isConnected } from "./is-connected"
17
18
 
18
19
  // Export information retrieval function
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Type definition for the callback function that is called when the activity status changes.
3
+ * @param {boolean} value - The new activity status (true if active, false if not).
4
+ */
5
+ type IsActiveCallback = (value: boolean) => void
6
+
7
+ let activeCallback: IsActiveCallback | undefined
8
+
9
+ /**
10
+ * Indicates whether the device is currently active.
11
+ * @type {boolean}
12
+ */
13
+ export let isActive = false
14
+
15
+ /**
16
+ * Sets the callback function to be called when the activity status changes.
17
+ *
18
+ * This function allows you to specify a callback that will be invoked whenever
19
+ * the activity status changes, indicating whether the device is currently active.
20
+ *
21
+ * @param {IsActiveCallback} callback - The callback function to be set. This function
22
+ * receives a boolean value indicating the new activity status.
23
+ * @returns {void}
24
+ */
25
+ export const active = (callback: IsActiveCallback): void => {
26
+ activeCallback = callback
27
+ }
28
+
29
+ /**
30
+ * Checks if a dynamic value is active based on a threshold and duration.
31
+ *
32
+ * This function assesses whether a given dynamic value surpasses a specified threshold
33
+ * and remains active for a specified duration. If the activity status changes from
34
+ * the previous state, the callback function is called with the updated activity status.
35
+ *
36
+ * @param {number} input - The dynamic value to check for activity status.
37
+ * @param {number} [threshold=2.5] - The threshold value to determine if the input is considered active.
38
+ * Defaults to 2.5 if not provided.
39
+ * @param {number} [duration=1000] - The duration (in milliseconds) to monitor the input for activity.
40
+ * Defaults to 1000 milliseconds if not provided.
41
+ * @returns {Promise<void>} A promise that resolves once the activity check is complete.
42
+ */
43
+ export const checkActivity = (input: number, threshold = 2.5, duration = 1000): Promise<void> => {
44
+ return new Promise((resolve) => {
45
+ // Check the activity status after the specified duration
46
+ setTimeout(() => {
47
+ // Determine the activity status based on the threshold
48
+ const activeNow = input > threshold
49
+ if (isActive !== activeNow) {
50
+ isActive = activeNow
51
+ if (activeCallback) {
52
+ activeCallback(activeNow)
53
+ }
54
+ }
55
+ resolve()
56
+ }, duration)
57
+ })
58
+ }
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