signalk-racer 0.9.0 → 0.9.2
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 +35 -19
- package/ReleaseNotes.md +7 -0
- package/index.js +95 -3
- package/openapi.json +31 -0
- package/package.json +3 -2
- package/public/index.html +52 -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
|
|
16
|
-
| `navigation.racing
|
|
17
|
-
| `navigation.racing
|
|
18
|
-
| `navigation.racing
|
|
19
|
-
| `navigation.racing
|
|
20
|
-
| `navigation.racing
|
|
21
|
-
| `navigation.racing
|
|
22
|
-
| `navigation.racing
|
|
23
|
-
| `navigation.racing
|
|
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.
|
|
@@ -44,16 +45,15 @@ You can configure the plugin via the Signal K web interface or by editing `setti
|
|
|
44
45
|
|
|
45
46
|
## 🧠 Algorithm Descriptions
|
|
46
47
|
|
|
47
|
-
###
|
|
48
|
+
### Waypoint selection (`navigation.racing.startLinePort` and `navigation.racing.startLineStb`)
|
|
48
49
|
|
|
49
|
-
The `startLineStb` and `startLinePort` configurations are used to search resources for matching waypoints.
|
|
50
|
-
These waypoints can be modified in real time (e.g. moving or setting in Freeboard SK) and the algorithm with update to the new line.
|
|
50
|
+
The `startLineStb` and `startLinePort` configurations are used to search resources for matching waypoints. These waypoints can be modified in real time (e.g. moving or setting in Freeboard SK) and the algorithm with update to the new line.
|
|
51
51
|
|
|
52
|
-
###
|
|
52
|
+
### Bow position
|
|
53
53
|
|
|
54
54
|
If `navigation.headingTrue` is not available, then `navigation.courseOverGroundTrue` is used when determining the position of the bow relative to the GPS position.
|
|
55
55
|
|
|
56
|
-
###
|
|
56
|
+
### Distance to Start Line (`navigation.racing.distanceStartline`)
|
|
57
57
|
|
|
58
58
|
- Computes the **signed minimum distance** from the **bow** of the boat to the configured start line.
|
|
59
59
|
- Uses `sensors.gps.fromBow` and `sensors.gps.fromCenter` offsets to calculate the actual bow position.
|
|
@@ -63,26 +63,42 @@ If `navigation.headingTrue` is not available, then `navigation.courseOverGroundT
|
|
|
63
63
|
- **Negative** when the bow is **over the line (OCS)**
|
|
64
64
|
- **Positive** when the bow is **behind the line**
|
|
65
65
|
|
|
66
|
-
###
|
|
66
|
+
### Start Line Bias (`navigation.racing.stbLineBias`)
|
|
67
67
|
|
|
68
68
|
- Measures how far ahead (downwind) the **pin** end is relative to the **boat** end.
|
|
69
69
|
- The `environment.wind.directionTrue` is used as well as the calculated line length and bearing.
|
|
70
70
|
- A **positive value** means the **pin end is more downwind**, implying the **starboard (boat) end is favored** for upwind starts.
|
|
71
71
|
|
|
72
|
-
###
|
|
72
|
+
### Line Coordinates & Length
|
|
73
73
|
|
|
74
74
|
- The plugin reads Signal K `waypoints` resources for the names you configure.
|
|
75
75
|
- If both endpoints are found, it calculates:
|
|
76
76
|
- Line length (using `geolib.getPreciseDistance`)
|
|
77
77
|
- Line bearing (rhumb line from starboard to port)
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
### Next Leg
|
|
80
80
|
- If a course is active then the true bearing for the next leg is calculated as `navigation.racing.nextLegHeading`
|
|
81
81
|
- If the `environment.wind.directionTrue` is available, then the True Wind Angle for the next leg is also calculated as `navigation.racing.nextLegHeading`
|
|
82
82
|
- If the next mark of the active route is beyond the first mark, then the start line calculations are suspended.
|
|
83
83
|
|
|
84
84
|
---
|
|
85
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
|
+
|
|
101
|
+
|
|
86
102
|
## 🔄 Dependencies
|
|
87
103
|
|
|
88
104
|
- [geolib](https://github.com/manuelbieh/geolib): Used for all geospatial math, including distances, bearings, and projections.
|
package/ReleaseNotes.md
ADDED
package/index.js
CHANGED
|
@@ -178,6 +178,85 @@ module.exports = (app) => {
|
|
|
178
178
|
return bow;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
plugin.setStartLineEnd = async (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const end = req.params.end; // "port" or "stb"
|
|
184
|
+
if (end !== 'port' && end !== 'stb') {
|
|
185
|
+
res.status(400).send({error: 'End must be "port" or "stb"'});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let position = app.getSelfPath('navigation.position');
|
|
190
|
+
if (position)
|
|
191
|
+
position = position.value;
|
|
192
|
+
else {
|
|
193
|
+
res.status(500).send({error: 'No valid position available'});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const bow = bowPosition(position);
|
|
197
|
+
const startLine = state.startLine;
|
|
198
|
+
const waypointName = options[`startLine${end.charAt(0).toUpperCase() + end.slice(1)}`];
|
|
199
|
+
|
|
200
|
+
let waypointId = startLine ? startLine[end + 'Id'] : null;
|
|
201
|
+
if (!waypointId) {
|
|
202
|
+
// Get the ID of the last existing waypoint with the given name.
|
|
203
|
+
const waypoints = await app.resourcesApi.listResources('waypoints', {});
|
|
204
|
+
for (const [id, resource] of Object.entries(waypoints)) {
|
|
205
|
+
if (resource.name === waypointName) {
|
|
206
|
+
waypointId = id;
|
|
207
|
+
// don't break here as we want the last waypoint with the given name
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
app.debug(`POSITION to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(bow)}`);
|
|
213
|
+
|
|
214
|
+
const waypoint = {
|
|
215
|
+
name: waypointName,
|
|
216
|
+
feature: {
|
|
217
|
+
type: 'Feature',
|
|
218
|
+
geometry: {
|
|
219
|
+
type: 'Point',
|
|
220
|
+
coordinates: [bow.longitude, bow.latitude]
|
|
221
|
+
},
|
|
222
|
+
properties: {}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
app.debug(`waypoint: ${waypointId} -> ${end}/${waypointName} : ${JSON.stringify(waypoint)}`);
|
|
227
|
+
|
|
228
|
+
if (waypointId)
|
|
229
|
+
await app.resourcesApi.setResource('waypoints', waypointId, waypoint);
|
|
230
|
+
else
|
|
231
|
+
await app.resourcesApi.createResource('waypoints', waypoint);
|
|
232
|
+
|
|
233
|
+
res.json({
|
|
234
|
+
message: 'OK'
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (startLine)
|
|
238
|
+
{
|
|
239
|
+
const newStartLine = { ...state.startLine };
|
|
240
|
+
newStartLine[end + 'Id'] = waypointId;
|
|
241
|
+
newStartLine[end] = bow;
|
|
242
|
+
startLine.length = geolib.getPreciseDistance(newStartLine.port, newStartLine.stb, 0.1);
|
|
243
|
+
startLine.bearing = geolib.getRhumbLineBearing(newStartLine.stb, newStartLine.port);
|
|
244
|
+
state.startLine = newStartLine;
|
|
245
|
+
sendDeltas([
|
|
246
|
+
{ path: `navigation.racing.startLine${end.charAt(0).toUpperCase() + end.slice(1)}`, value: bow },
|
|
247
|
+
{ path: 'navigation.racing.startLineLength', value: newStartLine.length },
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
processPosition(position);
|
|
251
|
+
} else {
|
|
252
|
+
findLineAndThenProcess(position, true);
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
app.error('Failed to set start line end:', err);
|
|
256
|
+
res.status(500).json({ error: 'Failed to set start line end' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
181
260
|
function findLineAndThenProcess(position, alwaysFindLine = false) {
|
|
182
261
|
if (!state.startLine || alwaysFindLine) {
|
|
183
262
|
app.resourcesApi.listResources(
|
|
@@ -196,15 +275,20 @@ module.exports = (app) => {
|
|
|
196
275
|
app.debug(`WAYPOINT: ${key}, Value:`, JSON.stringify(value));
|
|
197
276
|
if (value.name === options.startLinePort) {
|
|
198
277
|
let pos = waypointToPosition(value);
|
|
199
|
-
if (pos)
|
|
278
|
+
if (pos) {
|
|
279
|
+
startLine.portId = key;
|
|
200
280
|
startLine.port = pos;
|
|
281
|
+
}
|
|
201
282
|
}
|
|
202
283
|
if (value.name === options.startLineStb) {
|
|
203
284
|
let pos = waypointToPosition(value);
|
|
204
|
-
if (pos)
|
|
285
|
+
if (pos) {
|
|
286
|
+
startLine.stbId = key;
|
|
205
287
|
startLine.stb = pos;
|
|
288
|
+
}
|
|
206
289
|
}
|
|
207
290
|
}
|
|
291
|
+
|
|
208
292
|
if (startLine.port && startLine.stb) {
|
|
209
293
|
startLine.length = geolib.getPreciseDistance(startLine.port, startLine.stb, 0.1);
|
|
210
294
|
startLine.bearing = geolib.getRhumbLineBearing(startLine.stb, startLine.port);
|
|
@@ -494,7 +578,15 @@ module.exports = (app) => {
|
|
|
494
578
|
unsubscribes.length = 0;
|
|
495
579
|
},
|
|
496
580
|
|
|
497
|
-
schema: () => racerSchema
|
|
581
|
+
schema: () => racerSchema,
|
|
582
|
+
|
|
583
|
+
registerWithRouter: (router) => {
|
|
584
|
+
router.get('/startline/port', (req, res) => res.json(state.startLine ? state.startLine.port : null));
|
|
585
|
+
router.get('/startline/stb', (req, res) => res.json(state.startLine ? state.startLine.stb : null));
|
|
586
|
+
router.put('/startline/:end', plugin.setStartLineEnd);
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
getOpenApi: () => require('./openapi.json'),
|
|
498
590
|
};
|
|
499
591
|
|
|
500
592
|
return plugin;
|
package/openapi.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
|
|
12
|
+
"paths": {
|
|
13
|
+
"/startline/port": {
|
|
14
|
+
"get": {
|
|
15
|
+
"summary": "Set the port end of the start line",
|
|
16
|
+
"responses": {
|
|
17
|
+
"200": { "description": "Port end set" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"/startline/stb": {
|
|
22
|
+
"put": {
|
|
23
|
+
"summary": "Set the starboard end of the start line",
|
|
24
|
+
"description": "Set the starboard end of the start line to the boats position plus the bow offset.",
|
|
25
|
+
"responses": {
|
|
26
|
+
"200": { "description": "Starboard end set" }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-racer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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,52 @@
|
|
|
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: 600px;
|
|
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
|
+
<button onclick="setEnd('port')">Set Port End (Pin)</button>
|
|
29
|
+
<button onclick="setEnd('stb')">Set Starboard End (Boat)</button>
|
|
30
|
+
<div id="status"></div>
|
|
31
|
+
|
|
32
|
+
<script>
|
|
33
|
+
async function setEnd(end) {
|
|
34
|
+
const url = `/plugins/signalk-racer/startline/${end}`;
|
|
35
|
+
document.getElementById('status').textContent = `Sending request to ${url}...`;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(url, { method: 'PUT' });
|
|
39
|
+
if (res.ok) {
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
document.getElementById('status').textContent = `✅ ${end.toUpperCase()} set at bow position.`;
|
|
42
|
+
} else {
|
|
43
|
+
const err = await res.text();
|
|
44
|
+
throw new Error(`Error ${res.status}: ${err}`);
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
document.getElementById('status').textContent = `❌ Failed: ${e.message}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|