signalk-racer 0.9.1 → 0.9.3
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 +29 -11
- package/ReleaseNotes.md +9 -0
- package/index.js +123 -4
- package/openapi.json +83 -0
- package/package.json +3 -2
- package/public/index.html +53 -0
package/README.MD
CHANGED
|
@@ -10,17 +10,17 @@ GitHub repository: [https://github.com/gregw/signalk-racer](https://github.com/g
|
|
|
10
10
|
|
|
11
11
|
This plugin calculates and publishes the following Signal K paths:
|
|
12
12
|
|
|
13
|
-
| Path | Description
|
|
14
|
-
|
|
15
|
-
| `navigation.racing.`<br/>`distanceStartline` | Signed minimum distance from the bow to the start line
|
|
16
|
-
| `navigation.racing.`<br/>`startLineLength` | Total length of the start line
|
|
17
|
-
| `navigation.racing.`<br/>`stbLineBias` | Bias of the start line toward the starboard end
|
|
18
|
-
| `navigation.racing.`<br/>`startLinePort.latitude` | Latitude of the port (pin) end of the start line
|
|
19
|
-
| `navigation.racing.`<br/>`startLinePort.longitude` | Longitude of the port end
|
|
20
|
-
| `navigation.racing.`<br/>`startLineStb.latitude` | Latitude of the starboard (boat) end of the start line
|
|
21
|
-
| `navigation.racing.`<br/>`startLineStb.longitude` | Longitude of the starboard end
|
|
22
|
-
| `navigation.racing.`<br/>`nextLegHeading` | True heading for the next leg of the course
|
|
23
|
-
| `navigation.racing.`<br/>`nextLegTWA` | True Wind Angle for the next leg of the course
|
|
13
|
+
| Path | Description | Units | Schema |
|
|
14
|
+
|---------------------------------------------|---------------------------------------------------------|---------|--------|
|
|
15
|
+
| `navigation.racing.`<br/>`distanceStartline` | Signed minimum distance from the bow to the start line | `m` | Y |
|
|
16
|
+
| `navigation.racing.`<br/>`startLineLength` | Total length of the start line | `m` | |
|
|
17
|
+
| `navigation.racing.`<br/>`stbLineBias` | Bias of the start line toward the starboard end | `m` | |
|
|
18
|
+
| `navigation.racing.`<br/>`startLinePort.latitude` | Latitude of the port (pin) end of the start line | `deg` | Y |
|
|
19
|
+
| `navigation.racing.`<br/>`startLinePort.longitude` | Longitude of the port (pin) end of the start line | `deg` | Y |
|
|
20
|
+
| `navigation.racing.`<br/>`startLineStb.latitude` | Latitude of the starboard (boat) end of the start line | `deg` | Y |
|
|
21
|
+
| `navigation.racing.`<br/>`startLineStb.longitude` | Longitude of the starboard (boat) end of the start line | `deg` | Y |
|
|
22
|
+
| `navigation.racing.`<br/>`nextLegHeading` | True heading for the next leg of the course | `deg` | |
|
|
23
|
+
| `navigation.racing.`<br/>`nextLegTWA` | True Wind Angle for the next leg of the course | `deg` | |
|
|
24
24
|
|
|
25
25
|
These values can be displayed in KIP, Freeboard-SK, or other Signal K clients.
|
|
26
26
|
|
|
@@ -28,6 +28,7 @@ These values can be displayed in KIP, Freeboard-SK, or other Signal K clients.
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
## ⚙️ Configuration
|
|
32
33
|
|
|
33
34
|
You can configure the plugin via the Signal K web interface or by editing `settings.json` manually.
|
|
@@ -82,6 +83,23 @@ If `navigation.headingTrue` is not available, then `navigation.courseOverGroundT
|
|
|
82
83
|
|
|
83
84
|
---
|
|
84
85
|
|
|
86
|
+
## 🌐 Start Line REST API
|
|
87
|
+
|
|
88
|
+
The plugin provides a REST API that allows external clients (e.g. KIP dashboards, mobile apps, or scripts) to **set or query the start line ends**. This can be used to "ping" the boat or pin end by calling a simple HTTP POST with or without a position.
|
|
89
|
+
|
|
90
|
+
The plugin supports **partial line configuration**: you can set just one end (port or starboard), and it will store and emit that end's position immediately. Once both ends are defined, the full line geometry (length, bearing, bias) will be calculated.
|
|
91
|
+
|
|
92
|
+
### 📮 API Endpoints
|
|
93
|
+
|
|
94
|
+
| Method | Endpoint | Description |
|
|
95
|
+
|--------|-----------------------------------------|------------------------------------------------------------------------------------------------------|
|
|
96
|
+
| `GET` | `/plugins/signalk-racer/startline/port` | Get the last known **port (pin)** end position |
|
|
97
|
+
| `PUT` | `/plugins/signalk-racer/startline/port` | Set the **port (pin)** end of the start line to the current position of the boat plus the bow offset |
|
|
98
|
+
| `GET` | `/plugins/signalk-racer/startline/stb` | Get the last known **starboard (boat)** end position |
|
|
99
|
+
| `PUT` | `/plugins/signalk-racer/startline/stb` | Set the **starboard (boat)** end of the line to the current position of the boat plus the bow offset |
|
|
100
|
+
| `PUT` | `/plugins/signalk-racer/timeToStart` | Set the **timeToStart** in seconds and receive the current **distanceToLine** |
|
|
101
|
+
|
|
102
|
+
|
|
85
103
|
## 🔄 Dependencies
|
|
86
104
|
|
|
87
105
|
- [geolib](https://github.com/manuelbieh/geolib): Used for all geospatial math, including distances, bearings, and projections.
|
package/ReleaseNotes.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## 0.9.0 Initial Release
|
|
2
|
+
+ working DTL, Line Length, next TWA
|
|
3
|
+
## 0.9.1 Format Release
|
|
4
|
+
+ formating for the README.TXT
|
|
5
|
+
## 0.9.2 Set Start Line
|
|
6
|
+
+ Added API to set the start line
|
|
7
|
+
+ Added webapp to call start line API
|
|
8
|
+
## 0.9.3 KIP racer-timer integration
|
|
9
|
+
+ added timeToStart API
|
package/index.js
CHANGED
|
@@ -93,7 +93,7 @@ module.exports = (app) => {
|
|
|
93
93
|
const plugin = {
|
|
94
94
|
id: 'signalk-racer',
|
|
95
95
|
name: 'SignalK Racing Plugin',
|
|
96
|
-
start: (options
|
|
96
|
+
start: (options) => {
|
|
97
97
|
if (!options.startLinePort || !options.startLineStb) {
|
|
98
98
|
app.error('Missing waypoint names: startLinePort and/or startLineStb not configured.');
|
|
99
99
|
return;
|
|
@@ -178,6 +178,111 @@ module.exports = (app) => {
|
|
|
178
178
|
return bow;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
plugin.setTimeToStart = async (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const body = req.body;
|
|
184
|
+
if (!body || typeof body.timeToStart !== 'number') {
|
|
185
|
+
res.status(400).json({ error: 'Missing or invalid `timeToStart`' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const timeToStart = body.timeToStart; // value in seconds, or whatever unit you agree on
|
|
190
|
+
app.debug(`Setting timeToStart to ${timeToStart}`);
|
|
191
|
+
|
|
192
|
+
sendDeltas([
|
|
193
|
+
{path: 'navigation.racing.timeToStart', value: timeToStart},
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
res.json({
|
|
197
|
+
message: 'OK',
|
|
198
|
+
timeToStart,
|
|
199
|
+
distanceToLine: state.distanceToLine ? state.distanceToLine : null,
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
app.error('Failed to set time to start:', err);
|
|
203
|
+
res.status(500).json({ error: 'Failed to set time to start' });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
plugin.setStartLineEnd = async (req, res) => {
|
|
208
|
+
try {
|
|
209
|
+
const end = req.params.end; // "port" or "stb"
|
|
210
|
+
if (end !== 'port' && end !== 'stb') {
|
|
211
|
+
res.status(400).send({error: 'End must be "port" or "stb"'});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let position = app.getSelfPath('navigation.position');
|
|
216
|
+
if (position)
|
|
217
|
+
position = position.value;
|
|
218
|
+
else {
|
|
219
|
+
res.status(500).send({error: 'No valid position available'});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const bow = bowPosition(position);
|
|
223
|
+
const startLine = state.startLine;
|
|
224
|
+
const waypointName = options[`startLine${end.charAt(0).toUpperCase() + end.slice(1)}`];
|
|
225
|
+
|
|
226
|
+
let waypointId = startLine ? startLine[end + 'Id'] : null;
|
|
227
|
+
if (!waypointId) {
|
|
228
|
+
// Get the ID of the last existing waypoint with the given name.
|
|
229
|
+
const waypoints = await app.resourcesApi.listResources('waypoints', {});
|
|
230
|
+
for (const [id, resource] of Object.entries(waypoints)) {
|
|
231
|
+
if (resource.name === waypointName) {
|
|
232
|
+
waypointId = id;
|
|
233
|
+
// don't break here as we want the last waypoint with the given name
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
app.debug(`POSITION to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(bow)}`);
|
|
239
|
+
|
|
240
|
+
const waypoint = {
|
|
241
|
+
name: waypointName,
|
|
242
|
+
feature: {
|
|
243
|
+
type: 'Feature',
|
|
244
|
+
geometry: {
|
|
245
|
+
type: 'Point',
|
|
246
|
+
coordinates: [bow.longitude, bow.latitude]
|
|
247
|
+
},
|
|
248
|
+
properties: {}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
app.debug(`waypoint: ${waypointId} -> ${end}/${waypointName} : ${JSON.stringify(waypoint)}`);
|
|
253
|
+
|
|
254
|
+
if (waypointId)
|
|
255
|
+
await app.resourcesApi.setResource('waypoints', waypointId, waypoint);
|
|
256
|
+
else
|
|
257
|
+
await app.resourcesApi.createResource('waypoints', waypoint);
|
|
258
|
+
|
|
259
|
+
res.json({
|
|
260
|
+
message: 'OK'
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (startLine)
|
|
264
|
+
{
|
|
265
|
+
const newStartLine = { ...state.startLine };
|
|
266
|
+
newStartLine[end + 'Id'] = waypointId;
|
|
267
|
+
newStartLine[end] = bow;
|
|
268
|
+
startLine.length = geolib.getPreciseDistance(newStartLine.port, newStartLine.stb, 0.1);
|
|
269
|
+
startLine.bearing = geolib.getRhumbLineBearing(newStartLine.stb, newStartLine.port);
|
|
270
|
+
state.startLine = newStartLine;
|
|
271
|
+
sendDeltas([
|
|
272
|
+
{ path: `navigation.racing.startLine${end.charAt(0).toUpperCase() + end.slice(1)}`, value: bow },
|
|
273
|
+
{ path: 'navigation.racing.startLineLength', value: newStartLine.length },
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
processPosition(position);
|
|
277
|
+
} else {
|
|
278
|
+
findLineAndThenProcess(position, true);
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
app.error('Failed to set start line end:', err);
|
|
282
|
+
res.status(500).json({ error: 'Failed to set start line end' });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
181
286
|
function findLineAndThenProcess(position, alwaysFindLine = false) {
|
|
182
287
|
if (!state.startLine || alwaysFindLine) {
|
|
183
288
|
app.resourcesApi.listResources(
|
|
@@ -196,15 +301,20 @@ module.exports = (app) => {
|
|
|
196
301
|
app.debug(`WAYPOINT: ${key}, Value:`, JSON.stringify(value));
|
|
197
302
|
if (value.name === options.startLinePort) {
|
|
198
303
|
let pos = waypointToPosition(value);
|
|
199
|
-
if (pos)
|
|
304
|
+
if (pos) {
|
|
305
|
+
startLine.portId = key;
|
|
200
306
|
startLine.port = pos;
|
|
307
|
+
}
|
|
201
308
|
}
|
|
202
309
|
if (value.name === options.startLineStb) {
|
|
203
310
|
let pos = waypointToPosition(value);
|
|
204
|
-
if (pos)
|
|
311
|
+
if (pos) {
|
|
312
|
+
startLine.stbId = key;
|
|
205
313
|
startLine.stb = pos;
|
|
314
|
+
}
|
|
206
315
|
}
|
|
207
316
|
}
|
|
317
|
+
|
|
208
318
|
if (startLine.port && startLine.stb) {
|
|
209
319
|
startLine.length = geolib.getPreciseDistance(startLine.port, startLine.stb, 0.1);
|
|
210
320
|
startLine.bearing = geolib.getRhumbLineBearing(startLine.stb, startLine.port);
|
|
@@ -494,7 +604,16 @@ module.exports = (app) => {
|
|
|
494
604
|
unsubscribes.length = 0;
|
|
495
605
|
},
|
|
496
606
|
|
|
497
|
-
schema: () => racerSchema
|
|
607
|
+
schema: () => racerSchema,
|
|
608
|
+
|
|
609
|
+
registerWithRouter: (router) => {
|
|
610
|
+
router.get('/startline/port', (req, res) => res.json(state.startLine ? state.startLine.port : null));
|
|
611
|
+
router.get('/startline/stb', (req, res) => res.json(state.startLine ? state.startLine.stb : null));
|
|
612
|
+
router.put('/startline/:end', plugin.setStartLineEnd);
|
|
613
|
+
router.put('/timeToStart', plugin.setTimeToStart);
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
getOpenApi: () => require('./openapi.json'),
|
|
498
617
|
};
|
|
499
618
|
|
|
500
619
|
return plugin;
|
package/openapi.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"title": "Signal K Demo/Test Plugin OpenApi",
|
|
6
|
+
"license": {
|
|
7
|
+
"name": "Apache 2.0",
|
|
8
|
+
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"paths": {
|
|
12
|
+
"/startline/port": {
|
|
13
|
+
"get": {
|
|
14
|
+
"summary": "Set the port end of the start line",
|
|
15
|
+
"responses": {
|
|
16
|
+
"200": {
|
|
17
|
+
"description": "Port end set"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"/startline/stb": {
|
|
23
|
+
"put": {
|
|
24
|
+
"summary": "Set the starboard end of the start line",
|
|
25
|
+
"description": "Set the starboard end of the start line to the boats position plus the bow offset.",
|
|
26
|
+
"responses": {
|
|
27
|
+
"200": {
|
|
28
|
+
"description": "Starboard end set"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"/timeToStart": {
|
|
34
|
+
"put": {
|
|
35
|
+
"summary": "Set time to start",
|
|
36
|
+
"description": "Update the countdown timer value used for start line calculations.",
|
|
37
|
+
"requestBody": {
|
|
38
|
+
"required": true,
|
|
39
|
+
"content": {
|
|
40
|
+
"application/json": {
|
|
41
|
+
"schema": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"timeToStart": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"description": "Time to start in seconds"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"required": [
|
|
50
|
+
"timeToStart"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"responses": {
|
|
57
|
+
"200": {
|
|
58
|
+
"description": "Time to start set",
|
|
59
|
+
"content": {
|
|
60
|
+
"application/json": {
|
|
61
|
+
"schema": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {
|
|
64
|
+
"message": { "type": "string" },
|
|
65
|
+
"timeToStart": {
|
|
66
|
+
"type": "number",
|
|
67
|
+
"description": "Time to start in seconds"
|
|
68
|
+
},
|
|
69
|
+
"distanceToLine": {
|
|
70
|
+
"type": "number",
|
|
71
|
+
"description": "Distance to the line in meters"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"required": ["message", "timeToStart"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-racer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Signalk plugin to calculate values of interest to sail racers: such as Distance to Line; Next leg TWA.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"signalk-node-server-plugin",
|
|
15
|
-
"signalk-category-utility"
|
|
15
|
+
"signalk-category-utility",
|
|
16
|
+
"signalk-webapp"
|
|
16
17
|
],
|
|
17
18
|
"signalk-plugin-enabled-by-default": false,
|
|
18
19
|
"signalk": {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>SignalK Racer</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: sans-serif;
|
|
9
|
+
max-width: 100%;
|
|
10
|
+
margin: 3em auto;
|
|
11
|
+
text-align: center;
|
|
12
|
+
}
|
|
13
|
+
button {
|
|
14
|
+
padding: 1em 2em;
|
|
15
|
+
margin: 1em;
|
|
16
|
+
font-size: 1.2em;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
}
|
|
19
|
+
#status {
|
|
20
|
+
margin-top: 2em;
|
|
21
|
+
font-size: 1em;
|
|
22
|
+
color: green;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<h1>Set Start Line Ends</h1>
|
|
28
|
+
<div id="buttons">
|
|
29
|
+
<button onclick="setEnd('port')">Set Port End (Pin)</button><button onclick="setEnd('stb')">Set Starboard End (Boat)</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div id="status"></div>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
async function setEnd(end) {
|
|
35
|
+
const url = `/plugins/signalk-racer/startline/${end}`;
|
|
36
|
+
document.getElementById('status').textContent = `Sending request to ${url}...`;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(url, { method: 'PUT' });
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
const data = await res.json();
|
|
42
|
+
document.getElementById('status').textContent = `✅ ${end.toUpperCase()} set at bow position.`;
|
|
43
|
+
} else {
|
|
44
|
+
const err = await res.text();
|
|
45
|
+
throw new Error(`Error ${res.status}: ${err}`);
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
document.getElementById('status').textContent = `❌ Failed: ${e.message}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|