signalk-racer 0.9.7 → 0.9.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/ReleaseNotes.md CHANGED
@@ -1,4 +1,10 @@
1
- ## 0.9.7
1
+ ## 0.9.9 Working with KIP prototypes
2
+ + Fixed API handlers to use return status rather than callback
3
+
4
+ ## 0.9.8
5
+ + Minor updates to webapp UI
6
+
7
+ ## 0.9.7
2
8
  + Cleaned up webapp UI
3
9
  + reset to seconds when adjusting time
4
10
 
package/index.js CHANGED
@@ -15,6 +15,16 @@ module.exports = (app) => {
15
15
  title: 'The start line port end (pin) waypoint name',
16
16
  default: 'startPin'
17
17
  },
18
+ updateStartLineWaypoint: {
19
+ type: 'boolean',
20
+ title: 'Update the start line waypoints if the startLine is updated',
21
+ default: 'true'
22
+ },
23
+ createStartLineWaypoint: {
24
+ type: 'boolean',
25
+ title: 'Create the start line waypoints if the startLine is updated and the waypoint does not exist',
26
+ default: 'true'
27
+ },
18
28
  period: {
19
29
  type: 'number',
20
30
  title: 'The subscription period in microseconds used for position and wind deltas',
@@ -60,7 +70,7 @@ module.exports = (app) => {
60
70
  "value": {
61
71
  "units": "rad",
62
72
  "description": "Heading of the next leg of the course",
63
- "displayName": "Heading of the next leg of the course",
73
+ "displayName": "Next Heading",
64
74
  "shortName": "NextHDG",
65
75
  }
66
76
  },
@@ -69,7 +79,7 @@ module.exports = (app) => {
69
79
  "value": {
70
80
  "units": "rad",
71
81
  "description": "TWA on the next leg of the course",
72
- "displayName": "TWA on the next leg of the course",
82
+ "displayName": "Next TWA",
73
83
  "shortName": "NextTWA",
74
84
  }
75
85
  },
@@ -124,6 +134,17 @@ module.exports = (app) => {
124
134
  return degrees ? degrees * (Math.PI / 180) : null;
125
135
  }
126
136
 
137
+ function toOtherEnd(end) {
138
+ switch (end) {
139
+ case 'port' :
140
+ return 'stb';
141
+ case 'stb' :
142
+ return 'port';
143
+ default:
144
+ return null;
145
+ }
146
+ }
147
+
127
148
  function bowPosition(position) {
128
149
  let headingTrue = app.getSelfPath("navigation.headingTrue");
129
150
  if (!headingTrue)
@@ -150,59 +171,78 @@ module.exports = (app) => {
150
171
  return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
151
172
  }
152
173
 
153
- async function setStartLine(context, path, value, cb) {
174
+ function complete(callback, statusCode, message, details) {
175
+ const result = {state: statusCode === 200 ? 'SUCCESS' : 'FAILURE', statusCode, message, details};
176
+ app.debug(JSON.stringify(result));
177
+ return result;
178
+ }
179
+
180
+ // Put an absolute position
181
+ async function putStartLineEnd(end, position, callback, updateWaypoint) {
182
+ app.debug(`putStartLineEnd: ${end} ${JSON.stringify(position)}`);
183
+
154
184
  try {
155
- app.debug('setStartLine:', JSON.stringify(value));
156
- if (!value || !value.end || typeof value.end !== 'string') {
157
- app.error('Failed to set start line: !value.end');
158
- cb(null, {state: 'FAILED'});
159
- }
160
- const end = value.end.toLowerCase();
161
- if (end !== 'port' && end !== 'stb') {
162
- app.error('Failed to set start line: unknown end');
163
- cb(null, {state: 'FAILED'});
185
+ if (!end || !position || typeof position.latitude !== 'number' || typeof position.longitude !== 'number') {
186
+ return complete(callback, 400, 'Failed to put start line end: !end || !position');
164
187
  }
165
188
 
166
- // Get the position either as arg or of the current bow postion
167
- let position = value.position;
168
- if (position === 'bow') {
169
- position = app.getSelfPath('navigation.position');
170
- if (position)
171
- position = bowPosition(position.value);
189
+ end = end.toLowerCase();
190
+ if (end !== 'port' && end !== 'stb') {
191
+ return complete(callback, 400, 'Failed to put start line end: unknown end');
172
192
  }
173
193
 
174
- app.debug(`setStartLine[${end}] = ${JSON.stringify(position)} before translation/rotation`);
175
- const startLine = state.startLine;
176
-
177
- // Do any rotations or length changes
178
- if (startLine && (value.delta || value.rotate)) {
179
- app.debug(`setStartLine[${end}] translate ${value.delta} and rotate ${value.rotate} of ${JSON.stringify(startLine)}`);
180
- const fixedEnd = startLine[end === 'port' ? 'stb' : 'port'];
181
- const movingEnd = position ? position : startLine[end];
182
- app.debug(`fixedEnd: ${JSON.stringify(fixedEnd)} movingEnd: ${JSON.stringify(movingEnd)}`);
183
-
184
- const initialBearing = geolib.getRhumbLineBearing(fixedEnd, movingEnd);
185
- const currentDistance = geolib.getDistance(fixedEnd, movingEnd);
186
- app.debug(`initialBearing: ${initialBearing} currentDistance: ${currentDistance}`);
194
+ let startLine = state.startLine;
195
+ if (startLine) {
196
+ // We have a line, check if this end is the same?
197
+ if (startLine[end].latitude === position.latitude && startLine[end].longitude === position.longitude) {
198
+ return complete(callback, 304, 'Put start line end: unchanged line end');
199
+ }
200
+ } else {
201
+ // We don't have a line, so check if this is just an update for one end of the line?
202
+ const thisEnd = app.getSelfPath(`navigation.racing.startLine${camelCase(end)}`);
203
+ if (thisEnd && thisEnd.value.lat === position.latitude && thisEnd.value.longitude === position.longitude) {
204
+ return complete(callback, 304, 'Put start line end: unchanged end');
205
+ }
187
206
 
188
- const newDistance = value.delta && typeof value.delta === 'number' ? (currentDistance + value.delta) : currentDistance;
189
- const newBearing = value.rotate && typeof value.rotate === 'number'? (initialBearing + toDegrees(value.rotate)) : initialBearing;
190
- app.debug(`newDistance: ${newDistance} newBearing: ${newBearing}`);
207
+ // If we now have both ends, we have a line!
208
+ const otherEnd = toOtherEnd(end);
209
+ const otherEndPosition = app.getSelfPath(`navigation.racing.startLine${camelCase(otherEnd)}`);
210
+ if (otherEndPosition && otherEndPosition.value) {
211
+ startLine = {};
212
+ startLine[end] = position;
213
+ startLine[otherEnd] = otherEnd.value;
214
+ }
215
+ }
191
216
 
192
- const newPosition = geolib.computeDestinationPoint(fixedEnd, newDistance, newBearing);
193
- app.debug(`Moved/Rotated ${end} from ${JSON.stringify(position)} to ${JSON.stringify(newPosition)}`);
194
- position = newPosition;
217
+ // If we have a startLine, we can send deltas for this end and the length
218
+ if (startLine) {
219
+ app.debug('update existing start line');
220
+ const newStartLine = {...state.startLine};
221
+ newStartLine[end] = position;
222
+ startLine.length = geolib.getPreciseDistance(newStartLine.port, newStartLine.stb, 0.1);
223
+ startLine.bearing = geolib.getRhumbLineBearing(newStartLine.stb, newStartLine.port);
224
+ state.startLine = newStartLine;
225
+ sendDeltas([
226
+ {path: `navigation.racing.startLine${camelCase(end)}`, value: position},
227
+ {path: 'navigation.racing.startLineLength', value: newStartLine.length},
228
+ ]);
229
+ } else {
230
+ // otherwise we can only send the delta for this end
231
+ sendDeltas([
232
+ {path: `navigation.racing.startLine${camelCase(end)}`, value: position},
233
+ ]);
195
234
  }
196
235
 
197
- if (position) {
198
- app.debug('position:' + JSON.stringify(position));
199
- const waypointConfig =`startLine${camelCase(end)}`;
236
+ // Set/create the waypoint if we are configured to do so
237
+ if (state.options.updateStartLineWaypoint || updateWaypoint) {
238
+ const waypointConfig = `startLine${camelCase(end)}`;
200
239
  app.debug(`waypointConfig: ${waypointConfig}`);
201
240
  const waypointName = state.options[waypointConfig];
202
241
  app.debug(`waypointName: ${waypointName}`);
203
242
  let waypointId = startLine ? startLine[end + 'Id'] : null;
204
243
  app.debug(`waypointId: ${waypointId}`);
205
244
 
245
+ // If we have not cached the Id of the line end, then
206
246
  if (!waypointId) {
207
247
  // Get the ID of the last existing waypoint with the given name.
208
248
  const waypoints = await app.resourcesApi.listResources('waypoints', {});
@@ -215,7 +255,7 @@ module.exports = (app) => {
215
255
  app.debug(`waypointId: ${waypointId}`);
216
256
  }
217
257
 
218
- app.debug(`POSITION to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(position)}`);
258
+ app.debug(`Startline waypoint to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(position)}`);
219
259
 
220
260
  const waypoint = {
221
261
  name: waypointName,
@@ -226,6 +266,11 @@ module.exports = (app) => {
226
266
  coordinates: [position.longitude, position.latitude]
227
267
  },
228
268
  properties: {}
269
+ },
270
+ type: end === 'port' ? 'start-pin' : 'start-boat',
271
+ position: {
272
+ latitude: position.latitude,
273
+ longitude: position.longitude
229
274
  }
230
275
  }
231
276
 
@@ -233,52 +278,99 @@ module.exports = (app) => {
233
278
 
234
279
  if (waypointId)
235
280
  await app.resourcesApi.setResource('waypoints', waypointId, waypoint);
236
- else
281
+ else if (state.options.createStartLineWaypoint)
237
282
  await app.resourcesApi.createResource('waypoints', waypoint);
283
+ else
284
+ app.debug('startline waypoint not created');
285
+ } else {
286
+ app.debug('startline waypoint not updated');
238
287
  }
239
288
 
240
- cb(null, {
241
- state: 'COMPLETED',
242
- result: {
243
- distanceToStart: state.distanceToLine ? state.distanceToLine : null,
244
- }
245
- });
246
-
247
- if (startLine) {
248
- const newStartLine = {...state.startLine};
249
- newStartLine[end + 'Id'] = waypointId;
250
- newStartLine[end] = position;
251
- startLine.length = geolib.getPreciseDistance(newStartLine.port, newStartLine.stb, 0.1);
252
- startLine.bearing = geolib.getRhumbLineBearing(newStartLine.stb, newStartLine.port);
253
- state.startLine = newStartLine;
254
- sendDeltas([
255
- {path: `navigation.racing.startLine${end.charAt(0).toUpperCase() + end.slice(1)}`, value: position},
256
- {path: 'navigation.racing.startLineLength', value: newStartLine.length},
257
- ]);
258
289
 
290
+ // If we have a startLine, then process position against the new line
291
+ if (startLine)
259
292
  processPosition(null);
260
- } else {
261
- findLineAndThenProcess(null, true);
293
+ // Complete the handler successfully
294
+ return complete(callback, 200, 'Put start line end: OK');
295
+ } catch (err) {
296
+ return complete(callback, 500, 'Put start line end: Failed ' + JSON.stringify(err));
297
+ }
298
+ }
299
+
300
+ async function setStartLine(context, path, args, callback) {
301
+ try {
302
+ app.debug('setStartLine:', JSON.stringify(args));
303
+ if (!args || !args.end || typeof args.end !== 'string') {
304
+ return complete(callback, 400, 'Failed to set the startLine: !end');
305
+ }
306
+ const end = args.end.toLowerCase();
307
+ if (end !== 'port' && end !== 'stb') {
308
+ return complete(callback, 400, 'Failed to set the startLine: unknown end');
309
+ }
310
+
311
+ // Get the position either as arg or as the current bow position
312
+ let position = args.position;
313
+ if (position === 'bow' || !position && !args.delta && !args.rotate ) {
314
+ position = app.getSelfPath('navigation.position');
315
+ app.debug('bowPosition:' + JSON.stringify(position));
316
+ if (position)
317
+ position = bowPosition(position.value);
318
+ }
319
+
320
+ app.debug(`setStartLine[${end}] = ${JSON.stringify(position)} before translation/rotation`);
321
+ const startLine = state.startLine;
322
+
323
+ // Do any rotations or length changes
324
+ if (startLine && (args.delta || args.rotate)) {
325
+ app.debug(`setStartLine[${end}] translate ${args.delta} and rotate ${args.rotate} of ${JSON.stringify(startLine)}`);
326
+ const otherEnd = startLine[toOtherEnd(end)];
327
+ const thisEnd = position ? position : startLine[end];
328
+ app.debug(`otherEnd(fixed): ${JSON.stringify(otherEnd)} thisEnd(moving): ${JSON.stringify(thisEnd)}`);
329
+
330
+ const initialBearing = geolib.getRhumbLineBearing(otherEnd, thisEnd);
331
+ const currentDistance = geolib.getDistance(otherEnd, thisEnd);
332
+ app.debug(`initialBearing: ${initialBearing} currentDistance: ${currentDistance}`);
333
+
334
+ const newDistance = args.delta && typeof args.delta === 'number' ? (currentDistance + args.delta) : currentDistance;
335
+ const newBearing = args.rotate && typeof args.rotate === 'number' ? (initialBearing + toDegrees(args.rotate)) : initialBearing;
336
+ app.debug(`newDistance: ${newDistance} newBearing: ${newBearing}`);
337
+
338
+ const newPosition = geolib.computeDestinationPoint(otherEnd, newDistance, newBearing);
339
+ app.debug(`Moved/Rotated ${end} from ${JSON.stringify(position)} to ${JSON.stringify(newPosition)}`);
340
+ position = newPosition;
262
341
  }
342
+
343
+ if (position)
344
+ return putStartLineEnd(end, position, callback, args.updateWaypoint === true);
345
+ return complete(callback, 500, 'Failed to set the startLine: No position');
263
346
  } catch (err) {
264
- app.error('Failed to set start line end:', JSON.stringify(err));
265
- cb(null, {state: 'FAILED'});
266
- throw err;
347
+ return complete(callback, 500, 'Failed to set the startLine: ' + JSON.stringify(err));
267
348
  }
268
349
  }
269
350
 
270
- async function startTimeCommand(context, path, value, cb) {
351
+ function setTimer() {
352
+ if (state.timerInterval)
353
+ clearInterval(state.timerInterval);
354
+ state.timerInterval = setInterval(() => {
355
+ if (!state.timerRunning || state.timeToStart <= 0) {
356
+ clearInterval(state.timerInterval);
357
+ return;
358
+ }
359
+ state.timeToStart--;
360
+ sendDeltas([{path: 'navigation.racing.timeToStart', value: state.timeToStart}]);
361
+ }, 1000);
362
+ }
363
+
364
+ async function startTimeCommand(context, path, args, callback) {
271
365
  // start / reset / sync
272
366
  try {
273
- app.debug('startTimerCommand:', JSON.stringify(value));
274
- if (!value || !value.command || typeof value.command !== 'string') {
275
- app.error('Failed to command start timer: ' + JSON.stringify(value));
276
- cb(null, {state: 'FAILED'});
367
+ app.debug('startTimerCommand:', JSON.stringify(args));
368
+ if (!args || !args.command || typeof args.command !== 'string') {
369
+ return complete(callback, 400, 'Failed to command timer: no command');
277
370
  }
278
- const command = value.command.toLowerCase();
371
+ const command = args.command.toLowerCase();
279
372
 
280
- switch (command)
281
- {
373
+ switch (command) {
282
374
  case 'start': {
283
375
  const now = new Date();
284
376
  const timeToStart = state.timeToStart ?? state.options.timer ?? 300;
@@ -293,17 +385,8 @@ module.exports = (app) => {
293
385
  {path: 'navigation.racing.timeToStart', value: timeToStart}
294
386
  ]);
295
387
 
296
- if (state.timerInterval) clearInterval(state.timerInterval);
297
- state.timerInterval = setInterval(() => {
298
- if (!state.timerRunning || state.timeToStart <= 0) {
299
- clearInterval(state.timerInterval);
300
- return;
301
- }
302
- state.timeToStart--;
303
- sendDeltas([{path: 'navigation.racing.timeToStart', value: state.timeToStart}]);
304
- }, 1000);
305
- cb(null, {state: 'COMPLETED'});
306
- break;
388
+ setTimer();
389
+ return complete(callback, 200, 'start timer: OK');
307
390
  }
308
391
  case 'reset': {
309
392
  state.timerRunning = false;
@@ -314,14 +397,12 @@ module.exports = (app) => {
314
397
  {path: 'navigation.racing.timeToStart', value: state.timeToStart},
315
398
  {path: 'navigation.racing.startTime', value: null}
316
399
  ]);
317
- cb(null, {state: 'COMPLETED'});
318
- break;
400
+ return complete(callback, 200, 'reset timer: OK');
319
401
  }
320
402
 
321
403
  case 'sync': {
322
404
  if (!state.timerRunning || state.timeToStart == null) {
323
- cb(null, { state: 'FAILED', message: 'Timer not running' });
324
- return;
405
+ return complete(callback, 500, 'sync timer: !running');
325
406
  }
326
407
 
327
408
  const rounded = Math.round(state.timeToStart / 60) * 60;
@@ -329,67 +410,54 @@ module.exports = (app) => {
329
410
  state.startTime = new Date(Date.now() + rounded * 1000).toISOString();
330
411
 
331
412
  sendDeltas([
332
- { path: 'navigation.racing.timeToStart', value: state.timeToStart },
333
- { path: 'navigation.racing.startTime', value: state.startTime }
413
+ {path: 'navigation.racing.timeToStart', value: state.timeToStart},
414
+ {path: 'navigation.racing.startTime', value: state.startTime}
334
415
  ]);
335
- cb(null, { state: 'COMPLETED' });
336
- break;
416
+ return complete(callback, 200, 'sync timer: OK');
337
417
  }
338
418
 
339
419
  case 'set': {
340
- const input = value.startTime;
420
+ const input = args.startTime;
341
421
  if (!input) {
342
- cb(null, { state: 'FAILED', message: 'Missing startTime' });
343
- return;
422
+ return complete(callback, 400, 'reset timer: !startTime');
344
423
  }
345
424
 
346
425
  const inputTime = new Date(input);
347
426
  if (isNaN(inputTime)) {
348
- cb(null, { state: 'FAILED', message: 'Invalid startTime format' });
349
- return;
427
+ return complete(callback, 400, 'reset timer: invalid startTime');
350
428
  }
351
429
 
352
430
  const now = new Date();
353
- const diff = Math.floor((inputTime - now) / 1000);
431
+ const secondsToGo = Math.floor((inputTime - now) / 1000);
354
432
 
355
- if (diff <= 0) {
433
+ if (secondsToGo <= 0) {
356
434
  state.timerRunning = false;
357
435
  state.timeToStart = 0;
358
436
  state.startTime = null;
359
437
  sendDeltas([
360
- { path: 'navigation.racing.timeToStart', value: 0 },
361
- { path: 'navigation.racing.startTime', value: null }
438
+ {path: 'navigation.racing.timeToStart', value: 0},
439
+ {path: 'navigation.racing.startTime', value: null}
362
440
  ]);
363
- cb(null, { state: 'COMPLETED' });
364
- return;
441
+ return complete(callback, 200, 'set timer: 0K');
365
442
  }
366
443
 
367
444
  state.timerRunning = true;
368
- state.timeToStart = diff;
445
+ state.timeToStart = secondsToGo;
369
446
  state.startTime = inputTime.toISOString();
370
447
 
371
448
  sendDeltas([
372
- { path: 'navigation.racing.timeToStart', value: state.timeToStart },
373
- { path: 'navigation.racing.startTime', value: state.startTime }
449
+ {path: 'navigation.racing.timeToStart', value: state.timeToStart},
450
+ {path: 'navigation.racing.startTime', value: state.startTime}
374
451
  ]);
375
452
 
376
- if (state.timerInterval) clearInterval(state.timerInterval);
377
- state.timerInterval = setInterval(() => {
378
- if (!state.timerRunning || state.timeToStart <= 0) {
379
- clearInterval(state.timerInterval);
380
- return;
381
- }
382
- state.timeToStart--;
383
- sendDeltas([{ path: 'navigation.racing.timeToStart', value: state.timeToStart }]);
384
- }, 1000);
385
- cb(null, { state: 'COMPLETED' });
386
- break;
453
+ setTimer();
454
+ return complete(callback, 200, 'set timer: OK');
387
455
  }
388
456
 
389
457
  case 'adjust': {
390
- const delta = Number(value.delta);
458
+ const delta = Number(args.delta);
391
459
  if (isNaN(delta) || state.timeToStart == null || !state.startTime) {
392
- cb(null, { state: 'FAILED', message: 'Cannot adjust: invalid state or delta' });
460
+ cb(null, {state: 'FAILED', message: 'Cannot adjust: invalid state or delta'});
393
461
  return;
394
462
  }
395
463
 
@@ -401,23 +469,18 @@ module.exports = (app) => {
401
469
  state.startTime = adjustedStart;
402
470
 
403
471
  sendDeltas([
404
- { path: 'navigation.racing.timeToStart', value: state.timeToStart },
405
- { path: 'navigation.racing.startTime', value: adjustedStart }
472
+ {path: 'navigation.racing.timeToStart', value: state.timeToStart},
473
+ {path: 'navigation.racing.startTime', value: adjustedStart}
406
474
  ]);
407
- cb(null, { state: 'COMPLETED' });
408
- break;
475
+ return complete(callback, 200, 'adjust timer: OK');
409
476
  }
410
477
 
411
478
  default : {
412
- app.error('Unknown command to start timer: ' + command);
413
- cb(null, {state: 'FAILED'});
414
- return;
479
+ return complete(callback, 400, 'Failed to command timer: unknown command');
415
480
  }
416
481
  }
417
482
  } catch (err) {
418
- app.error('Failed to set start line end:', JSON.stringify(err));
419
- cb(null, {state: 'FAILED'});
420
- throw err;
483
+ return complete(callback, 500, 'Failed to command timer: ' + JSON.stringify(err));
421
484
  }
422
485
  }
423
486
 
@@ -536,8 +599,7 @@ module.exports = (app) => {
536
599
  sendDeltas([
537
600
  {path: 'navigation.racing.distanceStartline', value: distanceToLine},
538
601
  ]);
539
- }
540
- else if (state.distanceToLine) {
602
+ } else if (state.distanceToLine) {
541
603
  state.distanceToLine = null;
542
604
  sendDeltas([
543
605
  {path: 'navigation.racing.distanceStartline', value: null},
@@ -728,7 +790,7 @@ module.exports = (app) => {
728
790
  });
729
791
  processWind(twd);
730
792
  }
731
- )
793
+ );
732
794
 
733
795
  // Subscribe to Active route.
734
796
  app.subscriptionmanager.subscribe(
@@ -759,6 +821,48 @@ module.exports = (app) => {
759
821
  }
760
822
  );
761
823
 
824
+ // Subscribe to racing start line to see changes from any other plugin.
825
+ app.subscriptionmanager.subscribe(
826
+ {
827
+ context: 'vessels.' + app.selfId,
828
+ subscribe: [
829
+ {path: 'navigation.racing.startLinePort', policy: 'instant'},
830
+ {path: 'navigation.racing.startLineStb', policy: 'instant'},
831
+ ]
832
+ },
833
+ unsubscribes,
834
+ (subscriptionError) => {
835
+ app.error('Error:' + subscriptionError)
836
+ },
837
+ (delta) => {
838
+ app.debug('DELTAS START LINE ENDS ' + JSON.stringify(delta));
839
+ delta.updates.forEach((u) => {
840
+ u.values.forEach((v) => {
841
+ app.debug(`DELTA START LINE END from ${u.source.label}: ${v.path} = ${JSON.stringify(v.value)}`);
842
+ if (u.source && u.source.label && u.source.label !== 'signalk-racer') {
843
+ switch (v.path) {
844
+ case 'navigation.racing.startLinePort' :
845
+ putStartLineEnd('port', v.value, ignored => {
846
+ }, false).then(ignored => {
847
+ });
848
+ break;
849
+ case 'navigation.racing.startLineStb' :
850
+ putStartLineEnd('port', v.value, ignored => {
851
+ }, false).then(ignored => {
852
+ });
853
+ break;
854
+ }
855
+ }
856
+ })
857
+ });
858
+ }
859
+ );
860
+
861
+ // register to the standard path, so we can implement a direct put
862
+ app.registerPutHandler('vessels.self', 'navigation.racing.startLinePort', (ctx, path, value, callback) => putStartLineEnd('port', value, callback));
863
+ app.registerPutHandler('vessels.self', 'navigation.racing.startLineStb', (ctx, path, value, callback) => putStartLineEnd('stb', value, callback));
864
+
865
+ // register to API paths for this plugin
762
866
  app.registerPutHandler('vessels.self', 'navigation.racing.setStartLine', setStartLine);
763
867
  app.registerPutHandler('vessels.self', 'navigation.racing.setStartTime', startTimeCommand);
764
868
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-racer",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
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": {
package/public/index.html CHANGED
@@ -4,10 +4,16 @@
4
4
  <meta charset="UTF-8">
5
5
  <title>SignalK Racer Control</title>
6
6
  <style>
7
- .button-timer-command { background-color: #041b8e; color: white; }
7
+
8
+ body {
9
+ background-color: black;
10
+ color: white; /* optional for visibility */
11
+ }
12
+
13
+ .button-timer-command { background-color: #041b8e; color: white; font-size: 1em; font-weight: bold; }
8
14
  .button-timer-adjust { background-color: #2196F3; color: white; }
9
15
  button {
10
- padding: 1em 1.5em;
16
+ padding: 1em 1em;
11
17
  font-size: 1em;
12
18
  cursor: pointer;
13
19
  border: none;
@@ -17,7 +23,7 @@
17
23
  body {
18
24
  font-family: sans-serif;
19
25
  max-width: 100%;
20
- margin: 3em auto;
26
+ margin: 1em auto;
21
27
  text-align: center;
22
28
  }
23
29
  button {
@@ -31,17 +37,17 @@
31
37
  padding: 0.3em;
32
38
  }
33
39
  #status {
34
- margin-top: 2em;
40
+ margin-top: 1em;
35
41
  font-size: 1em;
36
42
  color: green;
37
43
  }
38
44
 
39
- .port-set { background-color: #f88; color: black; }
45
+ .port-set { background-color: #f88; color: black; font-size: 1em;}
40
46
  .port-move { background-color: #a00; color: white; }
41
47
  .port-tweak { background-color: #f44; color: white; }
42
48
  .port-rotate { background-color: #a00; color: white; }
43
- .stb-set { background-color: #8f8; color: black; }
44
- .stb-move { background-color: #080; color: white; }
49
+ .stb-set { background-color: #8f8; color: black; font-size: 1em; }
50
+ .stb-move { background-color: #080; color: white;}
45
51
  .stb-tweak { background-color: #4c4; color: white; }
46
52
  .stb-rotate { background-color: #080; color: white; }
47
53
 
@@ -98,26 +104,34 @@
98
104
  data.updates.forEach(update => {
99
105
  if (update.values) {
100
106
  update.values.forEach(value => {
101
- if (value.path === "navigation.racing.startLineLength") {
102
- document.getElementById("lineLength").value = value.value.toFixed(1);
103
- } else if (value.path === "navigation.racing.stbLineBias") {
104
- document.getElementById("lineBias").value = value.value.toFixed(1);
105
- } else if (value.path === "navigation.racing.timeToStart") {
106
- document.getElementById('timeToStart').textContent = formatTimeToMMSS(value.value);
107
- } else if (value.path === "navigation.racing.startTime") {
108
- if (!value.value) {
109
- document.getElementById('startTime').value = "HH:MM:SS";
110
- } else {
111
- startTime = new Date(value.value); // handles ISO 8601 format
112
- document.getElementById('startTime').value =
113
- startTime.toLocaleTimeString([], {
114
- hour: '2-digit',
115
- minute: '2-digit',
116
- second: '2-digit'
117
- });
107
+ switch (value.path) {
108
+ case "navigation.racing.startLineLength" :
109
+ // document.getElementById("lineLength").value = value.value.toFixed(1);
110
+ break;
111
+ case "navigation.racing.stbLineBias" :
112
+ // document.getElementById("lineBias").value = value.value.toFixed(1);
113
+ break;
114
+ case "navigation.racing.timeToStart":
115
+ let timer = document.getElementById('timeToStart');
116
+ if (timer) timer.textContent = formatTimeToMMSS(value.value);
117
+ break;
118
+ case "navigation.racing.startTime": {
119
+ if (!value.value) {
120
+ document.getElementById('startTime').value = "HH:MM:SS";
121
+ } else {
122
+ startTime = new Date(value.value); // handles ISO 8601 format
123
+ document.getElementById('startTime').value =
124
+ startTime.toLocaleTimeString([], {
125
+ hour: '2-digit',
126
+ minute: '2-digit',
127
+ second: '2-digit'
128
+ });
129
+ }
130
+ break;
118
131
  }
119
- } else {
120
- console.log("Received value: " + value.path + " = " + JSON.stringify(value.value));
132
+ default:
133
+ console.log("Received value: " + value.path + " = " + JSON.stringify(value.value));
134
+ break;
121
135
  }
122
136
  });
123
137
  }
@@ -233,71 +247,55 @@
233
247
 
234
248
  </script>
235
249
 
236
- <h1>Adjust Start Line</h1>
237
250
  <div style="display: flex; justify-content: center; align-items: center; gap: 3em; flex-wrap: wrap;">
238
- <div style="display: grid; grid-template-columns: repeat(10, auto); grid-template-rows: repeat(3, auto); gap: 0.5em;">
239
- <div></div>
251
+ <div style="display: grid; grid-template-columns: repeat(6, auto); grid-template-rows: repeat(3, auto); gap: 0.5em;">
240
252
  <div></div>
241
253
  <button class="port-rotate" onclick="sendAdjustStartLine('port', 0, 1)">+1°</button>
242
254
  <div></div>
243
- <div></div>
244
255
 
245
- <div></div>
246
256
  <div></div>
247
257
  <button class="stb-rotate" onclick="sendAdjustStartLine('stb', 0, -1)">+1°</button>
248
258
  <div></div>
249
- <div></div>
250
259
 
251
260
  <!--------->
252
261
 
253
- <button class="port-move" onclick="sendAdjustStartLine('port', 10, 0)">+10m</button>
254
- <button class="port-tweak" onclick="sendAdjustStartLine('port', 1, 0)">+1m</button>
255
- <button class="port-set" onclick="sendSetStartLine('port')">Port<br/>(pin)</button>
256
- <button class="port-tweak" onclick="sendAdjustStartLine('port', -1, 0)">-1m</button>
257
- <button class="port-move" onclick="sendAdjustStartLine('port', -10, 0)">-10m</button>
262
+ <button class="port-move" onclick="sendAdjustStartLine('port', 5, 0)">+5m</button>
263
+ <button class="port-set" onclick="sendSetStartLine('port')">Port</button>
264
+ <button class="port-move" onclick="sendAdjustStartLine('port', -5, 0)">-5m</button>
258
265
 
259
- <button class="stb-move" onclick="sendAdjustStartLine('stb', -10, 0)">-10m</button>
260
- <button class="stb-tweak" onclick="sendAdjustStartLine('stb', -1, 0)">-1m</button>
261
- <button class="stb-set" onclick="sendSetStartLine('stb')">Stbd<br/>(boat)</button>
262
- <button class="stb-tweak" onclick="sendAdjustStartLine('stb', 1, 0)">+1m</button>
263
- <button class="stb-move" onclick="sendAdjustStartLine('stb', +10, 0)">+10m</button>
266
+ <button class="stb-move" onclick="sendAdjustStartLine('stb', -5, 0)">-5m</button>
267
+ <button class="stb-set" onclick="sendSetStartLine('stb')">Stbd</button>
268
+ <button class="stb-move" onclick="sendAdjustStartLine('stb', 5, 0)">+5m</button>
264
269
 
265
270
  <!--------->
266
271
 
267
- <div></div>
268
272
  <div></div>
269
273
  <button class="port-rotate" onclick="sendAdjustStartLine('port', 0, -1)">-1°</button>
270
274
  <div></div>
271
275
  <div></div>
272
- <div></div>
273
- <div></div>
274
276
  <button class="stb-rotate" onclick="sendAdjustStartLine('stb', 0, 1)">-1°</button>
275
277
  <div></div>
276
- <div></div>
277
278
  </div>
278
279
  </div>
279
280
 
280
- <label>Length:&nbsp;<input id="lineLength" readonly style="width: 5em; text-align: center;">m</label>
281
- <label>Bias:&nbsp;<input id="lineBias" readonly style="width: 5em; text-align: center;">m</label>
282
-
283
281
  <!-- Timer Display -->
284
- <div style="margin-top: 2em;">
282
+ <!-- div style="margin-top: 2em;">
285
283
  <div id="timeToStart" style="font-size: 4em; font-weight: bold;">00:00</div>
286
- </div>
284
+ </div -->
287
285
 
288
286
  <!-- Start Time Row -->
289
- <div style="margin-top: 1em; display: flex; justify-content: center; align-items: center; gap: 1em;">
290
- <button class="button-timer-adjust" onclick="adjustStartTime(-60)">-1 min</button>
287
+ <div style="margin-top: 1em; display: flex; justify-content: center; align-items: center; gap: 0.5em;">
288
+ <button class="button-timer-adjust" onclick="adjustStartTime(-60)">-1min</button>
291
289
  <button class="button-timer-adjust" onclick="adjustStartTime(-1)">-1s</button>
292
- <label for="startTime">Start at: <input id="startTime" style="font-size: 1.5em; width: 6em; text-align: center;"></label>
290
+ <label for="startTime">At: <input id="startTime" style="font-size: 1.5em; width: 6em; text-align: center;"></label>
293
291
  <button class="button-timer-adjust" onclick="adjustStartTime(1)">+1s</button>
294
- <button class="button-timer-adjust" onclick="adjustStartTime(60)">+1 min</button>
292
+ <button class="button-timer-adjust" onclick="adjustStartTime(60)">+1min</button>
295
293
  </div>
296
294
 
297
295
  <!-- Timer Control Buttons -->
298
- <div style="margin-top: 1em;">
296
+ <div style="margin-top: 0.5em;">
299
297
  <button class="button-timer-command" onclick="sendTimerCommand('start')">Start</button>
300
- <button class="button-timer-command" onclick="setStartTime()">Start At</button>
298
+ <button class="button-timer-command" onclick="setStartTime()">At</button>
301
299
  <button class="button-timer-command" onclick="sendTimerCommand('sync')">Sync</button>
302
300
  <button class="button-timer-command" onclick="sendTimerCommand('reset')">Reset</button>
303
301
  </div>