signalk-racer 0.9.3 → 0.9.5

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/ReleaseNotes.md CHANGED
@@ -6,4 +6,8 @@
6
6
  + Added API to set the start line
7
7
  + Added webapp to call start line API
8
8
  ## 0.9.3 KIP racer-timer integration
9
- + added timeToStart API
9
+ + added timeToStart API
10
+ ## 0.9.4 Trivial rename
11
+ + Renamed README.md for npmjs
12
+ ## 0.9.5 Restructured
13
+ + Restructured the plugin to avoid initialization problem in new server
package/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ const geolib = require("geolib");
1
2
  module.exports = (app) => {
3
+ const state = {};
2
4
  const geolib = require('geolib')
3
5
  const racerSchema = {
4
6
  title: 'Set System Time with sudo',
@@ -39,13 +41,14 @@ module.exports = (app) => {
39
41
  }
40
42
  }
41
43
 
44
+ const unsubscribes = [];
45
+
42
46
  // Update meta data for non standard paths
43
- const timestamp = new Date().toISOString();
44
47
  app.handleMessage('vessels.self', {
45
48
  context: "vessels.self",
46
49
  updates: [
47
50
  {
48
- timestamp: timestamp,
51
+ timestamp: new Date().toISOString(),
49
52
  "meta": [
50
53
  {
51
54
  "path": "navigation.racing.startLineLength",
@@ -88,391 +91,389 @@ module.exports = (app) => {
88
91
  ]
89
92
  });
90
93
 
91
- const unsubscribes = [];
94
+ // send multiple deltas, each in the form of { path, value, units?}
95
+ function sendDeltas(updatesArray) {
96
+ const timestamp = new Date().toISOString();
92
97
 
93
- const plugin = {
94
- id: 'signalk-racer',
95
- name: 'SignalK Racing Plugin',
96
- start: (options) => {
97
- if (!options.startLinePort || !options.startLineStb) {
98
- app.error('Missing waypoint names: startLinePort and/or startLineStb not configured.');
99
- return;
100
- }
101
- app.debug('startLinePort:' + options.startLinePort);
102
- app.debug('startLineStb:' + options.startLineStb);
103
- app.debug('app.selfId:' + app.selfId);
98
+ const delta = {
99
+ context: 'vessels.self',
100
+ updates: [
101
+ {
102
+ source: {label: 'signalk-racer'},
103
+ timestamp: timestamp,
104
+ values: updatesArray.map(({path, value}) => ({
105
+ path,
106
+ value
107
+ }))
108
+ }
109
+ ]
110
+ };
104
111
 
105
- let fromBow = app.getSelfPath('sensors.gps.fromBow');
106
- let fromCenter = app.getSelfPath('sensors.gps.fromCenter');
107
- const state = {
108
- wayPointsScanned: false,
109
- gpsFromBow: fromBow ? fromBow.value : null,
110
- gpsFromCenter: fromCenter ? fromCenter.value : null,
111
- }
112
+ app.debug('Sending deltas:', JSON.stringify(delta));
113
+ app.handleMessage('vessels.self', delta);
114
+ }
112
115
 
113
- // send multiple deltas, each in the form of { path, value, units?}
114
- function sendDeltas(updatesArray) {
115
- const timestamp = new Date().toISOString();
116
+ function waypointToPosition(waypoint) {
117
+ if (
118
+ waypoint &&
119
+ waypoint.feature &&
120
+ waypoint.feature.geometry &&
121
+ waypoint.feature.geometry.type === 'Point' &&
122
+ Array.isArray(waypoint.feature.geometry.coordinates) &&
123
+ waypoint.feature.geometry.coordinates.length === 2
124
+ ) {
125
+ const [longitude, latitude] = waypoint.feature.geometry.coordinates;
126
+ return {latitude, longitude};
127
+ } else {
128
+ return null;
129
+ }
130
+ }
116
131
 
117
- const delta = {
118
- context: 'vessels.self',
119
- updates: [
120
- {
121
- source: {label: 'signalk-racer'},
122
- timestamp: timestamp,
123
- values: updatesArray.map(({path, value}) => ({
124
- path,
125
- value
126
- }))
127
- }
128
- ]
129
- };
132
+ function toDegrees(rad) {
133
+ return rad ? rad * (180 / Math.PI) : null;
134
+ }
130
135
 
131
- app.debug('Sending deltas:', JSON.stringify(delta));
132
- app.handleMessage('vessels.self', delta);
133
- }
136
+ function toRadians(degrees) {
137
+ return degrees ? degrees * (Math.PI / 180) : null;
138
+ }
134
139
 
135
- function waypointToPosition(waypoint) {
136
- if (
137
- waypoint &&
138
- waypoint.feature &&
139
- waypoint.feature.geometry &&
140
- waypoint.feature.geometry.type === 'Point' &&
141
- Array.isArray(waypoint.feature.geometry.coordinates) &&
142
- waypoint.feature.geometry.coordinates.length === 2
143
- ) {
144
- const [longitude, latitude] = waypoint.feature.geometry.coordinates;
145
- return {latitude, longitude};
146
- } else {
147
- return null;
148
- }
149
- }
140
+ function bowPosition(position) {
141
+ let headingTrue = app.getSelfPath("navigation.headingTrue");
142
+ if (!headingTrue)
143
+ headingTrue = app.getSelfPath("navigation.courseOverGroundTrue");
144
+ if (!headingTrue || (!state.gpsFromBow && !state.gpsFromCenter))
145
+ return position
146
+
147
+ headingTrue = toDegrees(headingTrue.value);
148
+ app.debug('headingTrue:' + headingTrue);
149
+ app.debug('position:' + JSON.stringify(position));
150
+ app.debug('gpsFromBow:' + JSON.stringify(state.gpsFromBow));
151
+ app.debug('gpsFromCenter:' + JSON.stringify(state.gpsFromCenter));
152
+
153
+ let bow = position;
154
+ if (state.gpsFromBow)
155
+ bow = geolib.computeDestinationPoint(bow, state.gpsFromBow, headingTrue);
156
+ if (state.gpsFromCenter)
157
+ bow = geolib.computeDestinationPoint(bow, state.gpsFromCenter, headingTrue + 90);
158
+ app.debug('bowPosition:' + JSON.stringify(bow));
159
+ return bow;
160
+ }
150
161
 
151
- function toDegrees(rad) {
152
- return rad ? rad * (180 / Math.PI) : null;
162
+ async function setTimeToStart(req, res) {
163
+ try {
164
+ const body = req.body;
165
+ if (!body || typeof body.timeToStart !== 'number') {
166
+ res.status(400).json({ error: 'Missing or invalid `timeToStart`' });
167
+ return;
153
168
  }
154
169
 
155
- function toRadians(degrees) {
156
- return degrees ? degrees * (Math.PI / 180) : null;
157
- }
170
+ const timeToStart = body.timeToStart; // value in seconds, or whatever unit you agree on
171
+ app.debug(`Setting timeToStart to ${timeToStart}`);
172
+
173
+ sendDeltas([
174
+ {path: 'navigation.racing.timeToStart', value: timeToStart},
175
+ ]);
176
+
177
+ res.json({
178
+ message: 'OK',
179
+ timeToStart,
180
+ distanceToLine: state.distanceToLine ? state.distanceToLine : null,
181
+ });
182
+ } catch (err) {
183
+ app.error('Failed to set time to start:', err);
184
+ res.status(500).json({ error: 'Failed to set time to start' });
185
+ }
186
+ }
158
187
 
159
- function bowPosition(position) {
160
- let headingTrue = app.getSelfPath("navigation.headingTrue");
161
- if (!headingTrue)
162
- headingTrue = app.getSelfPath("navigation.courseOverGroundTrue");
163
- if (!headingTrue || (!state.gpsFromBow && !state.gpsFromCenter))
164
- return position
165
-
166
- headingTrue = toDegrees(headingTrue.value);
167
- app.debug('headingTrue:' + headingTrue);
168
- app.debug('position:' + JSON.stringify(position));
169
- app.debug('gpsFromBow:' + JSON.stringify(state.gpsFromBow));
170
- app.debug('gpsFromCenter:' + JSON.stringify(state.gpsFromCenter));
171
-
172
- let bow = position;
173
- if (state.gpsFromBow)
174
- bow = geolib.computeDestinationPoint(bow, state.gpsFromBow, headingTrue);
175
- if (state.gpsFromCenter)
176
- bow = geolib.computeDestinationPoint(bow, state.gpsFromCenter, headingTrue + 90);
177
- app.debug('bowPosition:' + JSON.stringify(bow));
178
- return bow;
188
+ async function setStartLineEnd(req, res) {
189
+ try {
190
+ const end = req.params.end; // "port" or "stb"
191
+ if (end !== 'port' && end !== 'stb') {
192
+ res.status(400).send({error: 'End must be "port" or "stb"'});
193
+ return;
179
194
  }
180
195
 
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;
196
+ let position = app.getSelfPath('navigation.position');
197
+ if (position)
198
+ position = position.value;
199
+ else {
200
+ res.status(500).send({error: 'No valid position available'});
201
+ return;
202
+ }
203
+ const bow = bowPosition(position);
204
+ const startLine = state.startLine;
205
+ const waypointName = state.options[`startLine${end.charAt(0).toUpperCase() + end.slice(1)}`];
206
+
207
+ let waypointId = startLine ? startLine[end + 'Id'] : null;
208
+ if (!waypointId) {
209
+ // Get the ID of the last existing waypoint with the given name.
210
+ const waypoints = await app.resourcesApi.listResources('waypoints', {});
211
+ for (const [id, resource] of Object.entries(waypoints)) {
212
+ if (resource.name === waypointName) {
213
+ waypointId = id;
214
+ // don't break here as we want the last waypoint with the given name
187
215
  }
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
216
  }
205
217
  }
206
218
 
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
- }
219
+ app.debug(`POSITION to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(bow)}`);
214
220
 
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
- }
221
+ const waypoint = {
222
+ name: waypointName,
223
+ feature: {
224
+ type: 'Feature',
225
+ geometry: {
226
+ type: 'Point',
227
+ coordinates: [bow.longitude, bow.latitude]
228
+ },
229
+ properties: {}
230
+ }
231
+ }
251
232
 
252
- app.debug(`waypoint: ${waypointId} -> ${end}/${waypointName} : ${JSON.stringify(waypoint)}`);
233
+ app.debug(`waypoint: ${waypointId} -> ${end}/${waypointName} : ${JSON.stringify(waypoint)}`);
253
234
 
254
- if (waypointId)
255
- await app.resourcesApi.setResource('waypoints', waypointId, waypoint);
256
- else
257
- await app.resourcesApi.createResource('waypoints', waypoint);
235
+ if (waypointId)
236
+ await app.resourcesApi.setResource('waypoints', waypointId, waypoint);
237
+ else
238
+ await app.resourcesApi.createResource('waypoints', waypoint);
258
239
 
259
- res.json({
260
- message: 'OK'
261
- });
240
+ res.json({
241
+ message: 'OK'
242
+ });
262
243
 
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
- ]);
244
+ if (startLine)
245
+ {
246
+ const newStartLine = { ...state.startLine };
247
+ newStartLine[end + 'Id'] = waypointId;
248
+ newStartLine[end] = bow;
249
+ startLine.length = geolib.getPreciseDistance(newStartLine.port, newStartLine.stb, 0.1);
250
+ startLine.bearing = geolib.getRhumbLineBearing(newStartLine.stb, newStartLine.port);
251
+ state.startLine = newStartLine;
252
+ sendDeltas([
253
+ { path: `navigation.racing.startLine${end.charAt(0).toUpperCase() + end.slice(1)}`, value: bow },
254
+ { path: 'navigation.racing.startLineLength', value: newStartLine.length },
255
+ ]);
275
256
 
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
- }
257
+ processPosition(position);
258
+ } else {
259
+ findLineAndThenProcess(position, true);
284
260
  }
261
+ } catch (err) {
262
+ app.error('Failed to set start line end:', err);
263
+ res.status(500).json({ error: 'Failed to set start line end' });
264
+ }
265
+ }
285
266
 
286
- function findLineAndThenProcess(position, alwaysFindLine = false) {
287
- if (!state.startLine || alwaysFindLine) {
288
- app.resourcesApi.listResources(
289
- 'waypoints',
290
- {}
291
- ).then(data => {
292
- state.wayPointsScanned = true;
293
-
294
- const startLine = {
295
- stb: null,
296
- port: null,
297
- length: null,
298
- bearing: null,
267
+ function findLineAndThenProcess(position, alwaysFindLine = false) {
268
+ if (!state.startLine || alwaysFindLine) {
269
+ app.resourcesApi.listResources(
270
+ 'waypoints',
271
+ {}
272
+ ).then(data => {
273
+ state.wayPointsScanned = true;
274
+
275
+ const startLine = {
276
+ stb: null,
277
+ port: null,
278
+ length: null,
279
+ bearing: null,
280
+ }
281
+ for (const [key, value] of Object.entries(data)) {
282
+ app.debug(`WAYPOINT: ${key}, Value:`, JSON.stringify(value));
283
+ if (value.name === state.options.startLinePort) {
284
+ let pos = waypointToPosition(value);
285
+ if (pos) {
286
+ startLine.portId = key;
287
+ startLine.port = pos;
299
288
  }
300
- for (const [key, value] of Object.entries(data)) {
301
- app.debug(`WAYPOINT: ${key}, Value:`, JSON.stringify(value));
302
- if (value.name === options.startLinePort) {
303
- let pos = waypointToPosition(value);
304
- if (pos) {
305
- startLine.portId = key;
306
- startLine.port = pos;
307
- }
308
- }
309
- if (value.name === options.startLineStb) {
310
- let pos = waypointToPosition(value);
311
- if (pos) {
312
- startLine.stbId = key;
313
- startLine.stb = pos;
314
- }
315
- }
289
+ }
290
+ if (value.name === state.options.startLineStb) {
291
+ let pos = waypointToPosition(value);
292
+ if (pos) {
293
+ startLine.stbId = key;
294
+ startLine.stb = pos;
316
295
  }
296
+ }
297
+ }
317
298
 
318
- if (startLine.port && startLine.stb) {
319
- startLine.length = geolib.getPreciseDistance(startLine.port, startLine.stb, 0.1);
320
- startLine.bearing = geolib.getRhumbLineBearing(startLine.stb, startLine.port);
299
+ if (startLine.port && startLine.stb) {
300
+ startLine.length = geolib.getPreciseDistance(startLine.port, startLine.stb, 0.1);
301
+ startLine.bearing = geolib.getRhumbLineBearing(startLine.stb, startLine.port);
321
302
 
322
- app.debug(`STARTLINE: startLinePort: ${JSON.stringify(startLine)}`);
323
- state.startLine = startLine;
324
- } else {
325
- app.debug(`STARTLINE: undefined`);
326
- state.startLine = null;
327
- }
303
+ app.debug(`STARTLINE: startLinePort: ${JSON.stringify(startLine)}`);
304
+ state.startLine = startLine;
305
+ } else {
306
+ app.debug(`STARTLINE: undefined`);
307
+ state.startLine = null;
308
+ }
328
309
 
329
- sendDeltas([
330
- {path: 'navigation.racing.startLinePort', value: startLine.port},
331
- {path: 'navigation.racing.startLineStb', value: startLine.stb},
332
- {path: 'navigation.racing.startLineLength', value: startLine.length},
333
- ]);
310
+ sendDeltas([
311
+ {path: 'navigation.racing.startLinePort', value: startLine.port},
312
+ {path: 'navigation.racing.startLineStb', value: startLine.stb},
313
+ {path: 'navigation.racing.startLineLength', value: startLine.length},
314
+ ]);
334
315
 
335
- processPosition(position);
336
- processWind()
316
+ processPosition(position);
317
+ processWind()
337
318
 
338
- }).catch(err => {
339
- app.error(err);
340
- });
341
- } else {
342
- processPosition(position);
343
- }
344
- }
319
+ }).catch(err => {
320
+ app.error(err);
321
+ });
322
+ } else {
323
+ processPosition(position);
324
+ }
325
+ }
345
326
 
346
- function processPosition(position) {
347
- if (!position) {
348
- position = app.getSelfPath('navigation.position');
349
- app.debug('POSITION:' + JSON.stringify(position));
350
- position = position ? position.value : null;
351
- }
327
+ function processPosition(position) {
328
+ if (!position) {
329
+ position = app.getSelfPath('navigation.position');
330
+ app.debug('POSITION:' + JSON.stringify(position));
331
+ position = position ? position.value : null;
332
+ }
352
333
 
353
- const startLine = state.startLine;
354
-
355
- app.debug(`processPosition ${JSON.stringify(position)} to ${JSON.stringify(startLine)}`);
356
- if (position && startLine && (!state.activeRoute || state.activeRoute.pointIndex < 2)) {
357
- // handle the bow offset
358
- const bow = bowPosition(position);
359
-
360
- // Get the distance to each end of the line
361
- const toPort = geolib.getPreciseDistance(bow, startLine.port, 0.1);
362
- const toStb = geolib.getPreciseDistance(bow, startLine.stb, 0.1);
363
-
364
- const closest = toPort < toStb ? startLine.port : startLine.stb;
365
- let toEnd;
366
- let ocs;
367
- let angle;
368
- if (closest === state.startLine.port) {
369
- toEnd = toPort;
370
- app.debug('closest to Port(pin):' + toPort);
371
- const toBowBearing = geolib.getRhumbLineBearing(startLine.port, bow);
372
- app.debug('toBowBearing:' + toBowBearing);
373
- angle = toBowBearing - (startLine.bearing + 180) % 360;
374
- } else {
375
- toEnd = toStb;
376
- app.debug('closest to Stb(boat):' + toStb);
377
- const toBowBearing = geolib.getRhumbLineBearing(startLine.stb, bow);
378
- app.debug('toBowBearing:' + toBowBearing);
379
- angle = startLine.bearing - toBowBearing;
380
- }
381
- angle = ((angle + 180) % 360 + 360) % 360 - 180;
382
- app.debug('angle:' + angle);
383
- app.debug('toEnd:' + toEnd);
384
- ocs = angle < 0;
385
- app.debug('ocs:' + ocs);
386
-
387
- let absAngle = Math.abs(angle);
388
- const farFromLine = absAngle > 135;
389
- app.debug('farFromLine:' + farFromLine);
390
- const perpendicularToLine = toEnd * Math.sin(toRadians(absAngle));
391
- app.debug('perpendicularToLine:' + perpendicularToLine);
392
- let toLine = farFromLine ? Math.sqrt(toEnd * toEnd - perpendicularToLine * perpendicularToLine) : perpendicularToLine;
393
- toLine = Math.round(10 * toLine) / 10;
394
- app.debug('toLine:' + toLine);
395
- const distanceToLine = ocs ? -toLine : toLine;
396
- app.debug('distanceToLine:' + distanceToLine);
397
- state.distanceToLine = distanceToLine;
398
- sendDeltas([
399
- {path: 'navigation.racing.distanceStartline', value: distanceToLine},
400
- ]);
401
- }
402
- else if (state.distanceToLine) {
403
- state.distanceToLine = null;
404
- sendDeltas([
405
- {path: 'navigation.racing.distanceStartline', value: null},
406
- ]);
407
- }
334
+ const startLine = state.startLine;
335
+
336
+ app.debug(`processPosition ${JSON.stringify(position)} to ${JSON.stringify(startLine)}`);
337
+ if (position && startLine && (!state.activeRoute || state.activeRoute.pointIndex < 2)) {
338
+ // handle the bow offset
339
+ const bow = bowPosition(position);
340
+
341
+ // Get the distance to each end of the line
342
+ const toPort = geolib.getPreciseDistance(bow, startLine.port, 0.1);
343
+ const toStb = geolib.getPreciseDistance(bow, startLine.stb, 0.1);
344
+
345
+ const closest = toPort < toStb ? startLine.port : startLine.stb;
346
+ let toEnd;
347
+ let ocs;
348
+ let angle;
349
+ if (closest === state.startLine.port) {
350
+ toEnd = toPort;
351
+ app.debug('closest to Port(pin):' + toPort);
352
+ const toBowBearing = geolib.getRhumbLineBearing(startLine.port, bow);
353
+ app.debug('toBowBearing:' + toBowBearing);
354
+ angle = toBowBearing - (startLine.bearing + 180) % 360;
355
+ } else {
356
+ toEnd = toStb;
357
+ app.debug('closest to Stb(boat):' + toStb);
358
+ const toBowBearing = geolib.getRhumbLineBearing(startLine.stb, bow);
359
+ app.debug('toBowBearing:' + toBowBearing);
360
+ angle = startLine.bearing - toBowBearing;
408
361
  }
362
+ angle = ((angle + 180) % 360 + 360) % 360 - 180;
363
+ app.debug('angle:' + angle);
364
+ app.debug('toEnd:' + toEnd);
365
+ ocs = angle < 0;
366
+ app.debug('ocs:' + ocs);
367
+
368
+ let absAngle = Math.abs(angle);
369
+ const farFromLine = absAngle > 135;
370
+ app.debug('farFromLine:' + farFromLine);
371
+ const perpendicularToLine = toEnd * Math.sin(toRadians(absAngle));
372
+ app.debug('perpendicularToLine:' + perpendicularToLine);
373
+ let toLine = farFromLine ? Math.sqrt(toEnd * toEnd - perpendicularToLine * perpendicularToLine) : perpendicularToLine;
374
+ toLine = Math.round(10 * toLine) / 10;
375
+ app.debug('toLine:' + toLine);
376
+ const distanceToLine = ocs ? -toLine : toLine;
377
+ app.debug('distanceToLine:' + distanceToLine);
378
+ state.distanceToLine = distanceToLine;
379
+ sendDeltas([
380
+ {path: 'navigation.racing.distanceStartline', value: distanceToLine},
381
+ ]);
382
+ }
383
+ else if (state.distanceToLine) {
384
+ state.distanceToLine = null;
385
+ sendDeltas([
386
+ {path: 'navigation.racing.distanceStartline', value: null},
387
+ ]);
388
+ }
389
+ }
409
390
 
410
- function processWind(twd) {
411
- if (!twd) {
412
- twd = app.getSelfPath('environment.wind.directionTrue');
413
- app.debug('TWD:' + JSON.stringify(twd));
414
- twd = twd ? twd.value : null;
415
- }
391
+ function processWind(twd) {
392
+ if (!twd) {
393
+ twd = app.getSelfPath('environment.wind.directionTrue');
394
+ app.debug('TWD:' + JSON.stringify(twd));
395
+ twd = twd ? twd.value : null;
396
+ }
416
397
 
417
- if (!twd)
418
- return;
398
+ if (!twd)
399
+ return;
419
400
 
420
- let nextTwa = null;
421
- const activeRoute = state.activeRoute;
422
- if (activeRoute) {
423
- const nextLegHeading = activeRoute.nextLegHeading;
424
- if (nextLegHeading) {
425
- nextTwa = (540 + toDegrees(twd) - nextLegHeading) % 360 - 180;
426
- app.debug(`nextTwa: ${nextTwa}`);
427
- }
428
- }
401
+ let nextTwa = null;
402
+ const activeRoute = state.activeRoute;
403
+ if (activeRoute) {
404
+ const nextLegHeading = activeRoute.nextLegHeading;
405
+ if (nextLegHeading) {
406
+ nextTwa = (540 + toDegrees(twd) - nextLegHeading) % 360 - 180;
407
+ app.debug(`nextTwa: ${nextTwa}`);
408
+ }
409
+ }
429
410
 
430
- let bias = null;
431
- const startLine = state.startLine;
432
- if (startLine && (!activeRoute || activeRoute.pointIndex < 2)) {
433
- app.debug(`processWind ${twd} to ${JSON.stringify(startLine)}`);
434
- bias = startLine.length * Math.cos(toRadians(startLine.bearing) - (twd + Math.PI));
435
- }
411
+ let bias = null;
412
+ const startLine = state.startLine;
413
+ if (startLine && (!activeRoute || activeRoute.pointIndex < 2)) {
414
+ app.debug(`processWind ${twd} to ${JSON.stringify(startLine)}`);
415
+ bias = startLine.length * Math.cos(toRadians(startLine.bearing) - (twd + Math.PI));
416
+ }
436
417
 
418
+ sendDeltas([
419
+ {path: 'navigation.racing.stbLineBias', value: bias},
420
+ {path: 'navigation.racing.nextLegTrueWindAngle', value: toRadians(nextTwa)},
421
+ ]);
422
+ }
423
+
424
+ function processRoute(activeRoute) {
425
+ if (activeRoute && (activeRoute.pointIndex + 1) < activeRoute.pointTotal) {
426
+ // e.g. {"href":"/resources/routes/a12eefe7-8fd3-49f7-81af-ce1f3440d3ff","name":"Course 1","reverse":false,"pointIndex":1,"pointTotal":5}
427
+ const routeId = activeRoute.href.split('/').pop(); // e.g. "a12eefe7-8fd3-49f7-81af-ce1f3440d3ff"
428
+ app.debug('processRoute:' + routeId);
429
+ app.resourcesApi.getResource('routes', routeId).then(route => {
430
+ app.debug('found active route:', JSON.stringify(route));
431
+ // e.g. {name:"Course 1",description:"Test race course",distance:3027,feature:{type:"Feature",geometry:{type:"LineString",coordinates:[[151.2789194160523,-33.80427227074185],[151.28230238234357,-33.813458268981435],[151.2758044350174,-33.80201284252691],[151.27864400784992,-33.801915364141514],[151.27982384226053,-33.80429179893497]]},properties:{coordinatesMeta:[{href:"/resources/waypoints/bb35d9d0-c04e-4721-89f2-a1d8e697ab81"},{href:"/resources/waypoints/65082ddd-6fa6-4538-9a01-49d5d795b0bb"},{href:"/resources/waypoints/0bbce508-67c6-4f03-8de0-fb0532f88917"},{href:"/resources/waypoints/602ee562-c6dc-42c3-a775-512597063ca4"},{href:"/resources/waypoints/2e3cf194-8835-498d-abaa-a5d2104550b3"}]},id:""},timestamp:"2025-06-09T22:22:54.739Z",$source:"resources-provider"}
432
+ const coordinates = route.feature.geometry.coordinates;
433
+ const [fromLongitude, fromLatitude] = coordinates[activeRoute.pointIndex];
434
+ const [toLongitude, toLatitude] = coordinates[activeRoute.pointIndex + 1];
435
+ app.debug(`from: {longitude:${fromLongitude}, latitude:${fromLatitude}}, to: {longitude:${toLongitude}, latitude:${toLatitude}}`);
436
+ const nextLegHeading = geolib.getRhumbLineBearing({
437
+ latitude: fromLatitude,
438
+ longitude: fromLongitude
439
+ }, {latitude: toLatitude, longitude: toLongitude});
440
+ app.debug(`nextLegHeading: ${nextLegHeading}`);
441
+
442
+ activeRoute.nextLegHeading = nextLegHeading;
443
+ state.activeRoute = activeRoute;
437
444
  sendDeltas([
438
- {path: 'navigation.racing.stbLineBias', value: bias},
439
- {path: 'navigation.racing.nextLegTrueWindAngle', value: toRadians(nextTwa)},
445
+ {path: 'navigation.racing.nextLegHeading', value: toRadians(nextLegHeading)}
440
446
  ]);
441
- }
447
+ }).catch(err => {
448
+ app.error(err);
449
+ });
450
+ } else {
451
+ state.activeRoute = null;
452
+ sendDeltas([
453
+ {path: 'navigation.racing.nextLegHeading', value: null}
454
+ ]);
455
+ }
456
+ }
442
457
 
443
- function processRoute(activeRoute) {
444
- if (activeRoute && (activeRoute.pointIndex + 1) < activeRoute.pointTotal) {
445
- // e.g. {"href":"/resources/routes/a12eefe7-8fd3-49f7-81af-ce1f3440d3ff","name":"Course 1","reverse":false,"pointIndex":1,"pointTotal":5}
446
- const routeId = activeRoute.href.split('/').pop(); // e.g. "a12eefe7-8fd3-49f7-81af-ce1f3440d3ff"
447
- app.debug('processRoute:' + routeId);
448
- app.resourcesApi.getResource('routes', routeId).then(route => {
449
- app.debug('found active route:', JSON.stringify(route));
450
- // e.g. {name:"Course 1",description:"Test race course",distance:3027,feature:{type:"Feature",geometry:{type:"LineString",coordinates:[[151.2789194160523,-33.80427227074185],[151.28230238234357,-33.813458268981435],[151.2758044350174,-33.80201284252691],[151.27864400784992,-33.801915364141514],[151.27982384226053,-33.80429179893497]]},properties:{coordinatesMeta:[{href:"/resources/waypoints/bb35d9d0-c04e-4721-89f2-a1d8e697ab81"},{href:"/resources/waypoints/65082ddd-6fa6-4538-9a01-49d5d795b0bb"},{href:"/resources/waypoints/0bbce508-67c6-4f03-8de0-fb0532f88917"},{href:"/resources/waypoints/602ee562-c6dc-42c3-a775-512597063ca4"},{href:"/resources/waypoints/2e3cf194-8835-498d-abaa-a5d2104550b3"}]},id:""},timestamp:"2025-06-09T22:22:54.739Z",$source:"resources-provider"}
451
- const coordinates = route.feature.geometry.coordinates;
452
- const [fromLongitude, fromLatitude] = coordinates[activeRoute.pointIndex];
453
- const [toLongitude, toLatitude] = coordinates[activeRoute.pointIndex + 1];
454
- app.debug(`from: {longitude:${fromLongitude}, latitude:${fromLatitude}}, to: {longitude:${toLongitude}, latitude:${toLatitude}}`);
455
- const nextLegHeading = geolib.getRhumbLineBearing({
456
- latitude: fromLatitude,
457
- longitude: fromLongitude
458
- }, {latitude: toLatitude, longitude: toLongitude});
459
- app.debug(`nextLegHeading: ${nextLegHeading}`);
460
-
461
- activeRoute.nextLegHeading = nextLegHeading;
462
- state.activeRoute = activeRoute;
463
- sendDeltas([
464
- {path: 'navigation.racing.nextLegHeading', value: toRadians(nextLegHeading)}
465
- ]);
466
- }).catch(err => {
467
- app.error(err);
468
- });
469
- } else {
470
- state.activeRoute = null;
471
- sendDeltas([
472
- {path: 'navigation.racing.nextLegHeading', value: null}
473
- ]);
474
- }
458
+ // Return the plugin
459
+ return {
460
+ id: 'signalk-racer',
461
+ name: 'SignalK Racing Plugin',
462
+ start: (options) => {
463
+ if (!options.startLinePort || !options.startLineStb) {
464
+ app.error('Missing waypoint names: startLinePort and/or startLineStb not configured.');
465
+ return;
475
466
  }
467
+ app.debug('startLinePort:' + options.startLinePort);
468
+ app.debug('startLineStb:' + options.startLineStb);
469
+ app.debug('app.selfId:' + app.selfId);
470
+
471
+ let fromBow = app.getSelfPath('sensors.gps.fromBow');
472
+ let fromCenter = app.getSelfPath('sensors.gps.fromCenter');
473
+ state.options = options;
474
+ state.wayPointsScanned = false;
475
+ state.gpsFromBow = fromBow ? fromBow.value : null;
476
+ state.gpsFromCenter = fromCenter ? fromCenter.value : null;
476
477
 
477
478
  // Subscribe to position updates.
478
479
  app.subscriptionmanager.subscribe(
@@ -596,7 +597,7 @@ module.exports = (app) => {
596
597
  if (route)
597
598
  processRoute(route);
598
599
  }
599
- )
600
+ );
600
601
  },
601
602
 
602
603
  stop: () => {
@@ -609,13 +610,11 @@ module.exports = (app) => {
609
610
  registerWithRouter: (router) => {
610
611
  router.get('/startline/port', (req, res) => res.json(state.startLine ? state.startLine.port : null));
611
612
  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);
613
+ router.put('/startline/:end', setStartLineEnd);
614
+ router.put('/timeToStart', setTimeToStart);
614
615
  },
615
616
 
616
617
  getOpenApi: () => require('./openapi.json'),
617
618
  };
618
-
619
- return plugin;
620
619
  };
621
620
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-racer",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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": {
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "signalk-plugin-enabled-by-default": false,
19
19
  "signalk": {
20
- "appIcon": "./racer72x72.png",
20
+ "appIcon": "assets/signalk-racer-72x72.png",
21
21
  "displayName": "Signalk Racer WebApp"
22
22
  },
23
23
  "author": "gregw@mortbay.com",
File without changes