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 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 | 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 end | `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 end | `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` | |
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.
@@ -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, restartPlugin) => {
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.1",
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>