@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 +15 -6
- package/dist/commands/kilterboard.d.ts +12 -0
- package/dist/commands/kilterboard.js +41 -0
- package/dist/data/entralpi.js +3 -0
- package/dist/data/motherboard.js +3 -0
- package/dist/data/progressor.js +3 -0
- package/dist/data/wh-c06.js +3 -0
- package/dist/download.d.ts +8 -2
- package/dist/download.js +63 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/is-active.d.ts +37 -0
- package/dist/is-active.js +49 -0
- package/dist/led.d.ts +2 -2
- package/dist/led.js +6 -3
- package/package.json +4 -2
- package/src/commands/kilterboard.ts +41 -0
- package/src/data/entralpi.ts +4 -0
- package/src/data/motherboard.ts +4 -0
- package/src/data/progressor.ts +4 -0
- package/src/data/wh-c06.ts +4 -0
- package/src/download.ts +67 -7
- package/src/index.ts +1 -0
- package/src/is-active.ts +58 -0
- package/src/led.ts +9 -7
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
|
|
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
|
|
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
|
+
];
|
package/dist/data/entralpi.js
CHANGED
|
@@ -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,
|
package/dist/data/motherboard.js
CHANGED
|
@@ -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),
|
package/dist/data/progressor.js
CHANGED
|
@@ -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,
|
package/dist/data/wh-c06.js
CHANGED
|
@@ -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,
|
package/dist/download.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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",
|
|
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:
|
|
5
|
-
constructor(position: number, role_id:
|
|
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
|
|
106
|
-
|
|
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.
|
|
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
|
+
]
|
package/src/data/entralpi.ts
CHANGED
|
@@ -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,
|
package/src/data/motherboard.ts
CHANGED
|
@@ -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),
|
package/src/data/progressor.ts
CHANGED
|
@@ -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,
|
package/src/data/wh-c06.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
const blob = new Blob([
|
|
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",
|
|
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
|
package/src/is-active.ts
ADDED
|
@@ -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:
|
|
50
|
+
role_id: number
|
|
50
51
|
|
|
51
|
-
constructor(position: number, role_id:
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|