signalk-racer 0.9.6 → 0.9.8

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
@@ -93,7 +93,7 @@ This plugin uses WebSocket-based PUT requests to Signal K model paths.
93
93
  Used to **set** or **adjust** either end of the start line.
94
94
 
95
95
  #### Payload:
96
- ```json
96
+ ```
97
97
  {
98
98
  "end": "port" | "stb",
99
99
  "position": "bow" | { "latitude": ..., "longitude": ... },
@@ -114,7 +114,7 @@ Used to **set** or **adjust** either end of the start line.
114
114
  Used to **start**, **sync**, **reset**, or **set a fixed start time**.
115
115
 
116
116
  #### Payload:
117
- ```json
117
+ ```
118
118
  {
119
119
  "command": "start" | "reset" | "sync" | "adjust" | "set",
120
120
  "delta": 30,
@@ -141,4 +141,4 @@ Used to **start**, **sync**, **reset**, or **set a fixed start time**.
141
141
  ## 📬 Feedback
142
142
 
143
143
  Bug reports and suggestions are welcome at
144
- [https://github.com/gregw/signalk-racer](https://github.com/gregw/signalk-racer)
144
+ [https://github.com/gregw/signalk-racer](https://github.com/gregw/signalk-racer)
package/ReleaseNotes.md CHANGED
@@ -1,18 +1,31 @@
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
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
1
+ ## 0.9.8
2
+ + Minor updates to webapp UI
3
+
4
+ ## 0.9.7
5
+ + Cleaned up webapp UI
6
+ + reset to seconds when adjusting time
7
+
14
8
  ## 0.9.6 V2 API and Timer
15
9
  + Added new API V2 methods
16
10
  + Removed V1 methods
17
11
  + Webapp uses websocket
18
- + Server side timer
12
+ + Server side timer
13
+
14
+ ## 0.9.5 Restructured
15
+ + Restructured the plugin to avoid initialization problem in new server
16
+
17
+ ## 0.9.4 Trivial rename
18
+ + Renamed README.md for npmjs
19
+
20
+ ## 0.9.3 KIP racer-timer integration
21
+ + added timeToStart API
22
+
23
+ ## 0.9.2 Set Start Line
24
+ + Added API to set the start line
25
+ + Added webapp to call start line API
26
+
27
+ ## 0.9.1 Format Release
28
+ + formating for the README.TXT
29
+
30
+ ## 0.9.0 Initial Release
31
+ + working DTL, Line Length, next TWA
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  module.exports = (app) => {
2
3
  const state = {};
3
4
  const geolib = require('geolib')
@@ -7,22 +8,32 @@ module.exports = (app) => {
7
8
  properties: {
8
9
  startLineStb: {
9
10
  type: 'string',
10
- title: 'Line starboard waypoint name',
11
+ title: 'The start line starboard end (boat) waypoint name',
11
12
  default: 'startBoat'
12
13
  },
13
14
  startLinePort: {
14
15
  type: 'string',
15
- title: 'Line port waypoint name',
16
+ title: 'The start line port end (pin) waypoint name',
16
17
  default: 'startPin'
17
18
  },
19
+ updateStartLineWaypoint: {
20
+ type: 'boolean',
21
+ title: 'Update the start line waypoints if the startLine is updated',
22
+ default: 'true'
23
+ },
24
+ createStartLineWaypoint: {
25
+ type: 'boolean',
26
+ title: 'Create the start line waypoints if the startLine is updated and the waypoint does not exist',
27
+ default: 'true'
28
+ },
18
29
  period: {
19
30
  type: 'number',
20
- title: 'Update period in micro seconds',
31
+ title: 'The subscription period in microseconds used for position and wind deltas',
21
32
  default: 1000
22
33
  },
23
34
  timer: {
24
35
  type: 'number',
25
- title: 'Timer default period in seconds',
36
+ title: 'Start Timer default initial period in seconds',
26
37
  default: 300
27
38
  },
28
39
  }
@@ -52,7 +63,7 @@ module.exports = (app) => {
52
63
  "units": "m",
53
64
  "description": "Bias of the start line for the starboard end",
54
65
  "displayName": "Bias of the start line for the starboard end",
55
- "shortName": "SLB"
66
+ "shortName": "Bias"
56
67
  }
57
68
  },
58
69
  {
@@ -60,8 +71,8 @@ module.exports = (app) => {
60
71
  "value": {
61
72
  "units": "rad",
62
73
  "description": "Heading of the next leg of the course",
63
- "displayName": "Heading of the next leg of the course",
64
- "shortName": "NHDG",
74
+ "displayName": "Next Heading",
75
+ "shortName": "NextHDG",
65
76
  }
66
77
  },
67
78
  {
@@ -69,8 +80,8 @@ module.exports = (app) => {
69
80
  "value": {
70
81
  "units": "rad",
71
82
  "description": "TWA on the next leg of the course",
72
- "displayName": "TWA on the next leg of the course",
73
- "shortName": "NTWA",
83
+ "displayName": "Next TWA",
84
+ "shortName": "NextTWA",
74
85
  }
75
86
  },
76
87
  ]
@@ -124,6 +135,14 @@ module.exports = (app) => {
124
135
  return degrees ? degrees * (Math.PI / 180) : null;
125
136
  }
126
137
 
138
+ function toOtherEnd(end) {
139
+ switch(end) {
140
+ case 'port' : return 'stb';
141
+ case 'stb' : return 'port';
142
+ default: return null;
143
+ }
144
+ }
145
+
127
146
  function bowPosition(position) {
128
147
  let headingTrue = app.getSelfPath("navigation.headingTrue");
129
148
  if (!headingTrue)
@@ -150,52 +169,74 @@ module.exports = (app) => {
150
169
  return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
151
170
  }
152
171
 
153
- async function setStartLine(context, path, value, cb) {
172
+ function complete(callback, statusCode, message, details) {
173
+ const result = {state: 'COMPLETED', statusCode, message, details};
174
+ app.debug(JSON.stringify(result));
175
+ callback(result);
176
+ }
177
+
178
+ // Put an absolute position
179
+ async function putStartLineEnd(end, position, callback, updateWaypoint) {
180
+ app.debug(`putStartLineEnd: ${end} ${JSON.stringify(position)}`);
181
+
154
182
  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'});
183
+ if (!end || !position || typeof position.latitude !== 'number' || typeof position.longitude !== 'number' ) {
184
+ complete(callback, 400, 'Failed to put start line end: !end || !position');
185
+ return;
164
186
  }
165
187
 
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);
188
+ end = end.toLowerCase();
189
+ if (end !== 'port' && end !== 'stb') {
190
+ complete(callback, 400, 'Failed to put start line end: unknown end');
191
+ return;
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
+ complete(callback, 304, 'Put start line end: unchanged line end');
199
+ return;
200
+ }
201
+ } else {
202
+ // We don't have a line, so check if this is just an update for one end of the line?
203
+ const thisEnd = app.getSelfPath(`navigation.racing.startLine${camelCase(end)}`);
204
+ if (thisEnd && thisEnd.value.lat === position.latitude && thisEnd.value.longitude === position.longitude) {
205
+ complete(callback, 304, 'Put start line end: unchanged end');
206
+ return;
207
+ }
187
208
 
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}`);
209
+ // If we now have both ends, we have a line!
210
+ const otherEnd = toOtherEnd(end);
211
+ const otherEndPosition = app.getSelfPath(`navigation.racing.startLine${camelCase(otherEnd)}`);
212
+ if (otherEndPosition && otherEndPosition.value) {
213
+ startLine = {};
214
+ startLine[end] = position;
215
+ startLine[otherEnd] = otherEnd.value;
216
+ }
217
+ }
191
218
 
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;
219
+ // If we have a startLine, we can send deltas for this end and the length
220
+ if (startLine) {
221
+ app.debug('update existing start line');
222
+ const newStartLine = {...state.startLine};
223
+ newStartLine[end] = position;
224
+ startLine.length = geolib.getPreciseDistance(newStartLine.port, newStartLine.stb, 0.1);
225
+ startLine.bearing = geolib.getRhumbLineBearing(newStartLine.stb, newStartLine.port);
226
+ state.startLine = newStartLine;
227
+ sendDeltas([
228
+ {path: `navigation.racing.startLine${camelCase(end)}`, value: position},
229
+ {path: 'navigation.racing.startLineLength', value: newStartLine.length},
230
+ ]);
231
+ } else {
232
+ // otherwise we can only send the delta for this end
233
+ sendDeltas([
234
+ {path: `navigation.racing.startLine${camelCase(end)}`, value: position},
235
+ ]);
195
236
  }
196
237
 
197
- if (position) {
198
- app.debug('position:' + JSON.stringify(position));
238
+ // Set/create the waypoint if we are configured to do so
239
+ if (state.options.updateStartLineWaypoint || updateWaypoint) {
199
240
  const waypointConfig =`startLine${camelCase(end)}`;
200
241
  app.debug(`waypointConfig: ${waypointConfig}`);
201
242
  const waypointName = state.options[waypointConfig];
@@ -203,6 +244,7 @@ module.exports = (app) => {
203
244
  let waypointId = startLine ? startLine[end + 'Id'] : null;
204
245
  app.debug(`waypointId: ${waypointId}`);
205
246
 
247
+ // If we have not cached the Id of the line end, then
206
248
  if (!waypointId) {
207
249
  // Get the ID of the last existing waypoint with the given name.
208
250
  const waypoints = await app.resourcesApi.listResources('waypoints', {});
@@ -215,7 +257,7 @@ module.exports = (app) => {
215
257
  app.debug(`waypointId: ${waypointId}`);
216
258
  }
217
259
 
218
- app.debug(`POSITION to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(position)}`);
260
+ app.debug(`Startline waypoint to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(position)}`);
219
261
 
220
262
  const waypoint = {
221
263
  name: waypointName,
@@ -233,49 +275,102 @@ module.exports = (app) => {
233
275
 
234
276
  if (waypointId)
235
277
  await app.resourcesApi.setResource('waypoints', waypointId, waypoint);
236
- else
278
+ else if (state.options.createStartLineWaypoint)
237
279
  await app.resourcesApi.createResource('waypoints', waypoint);
280
+ else
281
+ app.debug('startline waypoint not created');
282
+ } else {
283
+ app.debug('startline waypoint not updated');
238
284
  }
239
285
 
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
- ]);
286
+ // Complete the handler successfully
287
+ complete(callback, 200, 'Put start line end: OK');
258
288
 
289
+ // If we have a startLine, then process position against the new line
290
+ if (startLine)
259
291
  processPosition(null);
260
- } else {
261
- findLineAndThenProcess(null, true);
292
+ } catch (err) {
293
+ complete(callback, 500, 'Put start line end: Failed ' + JSON.stringify(err));
294
+ }
295
+
296
+ return { state: 'PENDING' }
297
+ }
298
+
299
+ async function setStartLine(context, path, args, callback) {
300
+ try {
301
+ app.debug('setStartLine:', JSON.stringify(args));
302
+ if (!args || !args.end || typeof args.end !== 'string') {
303
+ complete(callback, 400, 'Failed to set the startLine: !end');
304
+ return;
305
+ }
306
+ const end = args.end.toLowerCase();
307
+ if (end !== 'port' && end !== 'stb') {
308
+ complete(callback, 400, 'Failed to set the startLine: unknown end');
309
+ return;
310
+ }
311
+
312
+ // Get the position either as arg or as the current bow position
313
+ let position = args.position;
314
+ if (position === 'bow') {
315
+ position = app.getSelfPath('navigation.position');
316
+ if (position)
317
+ position = bowPosition(position.value);
262
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;
341
+ }
342
+
343
+ if (position)
344
+ return putStartLineEnd(end, position, callback, args.updateWaypoint === true);
345
+ 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
+ complete(callback, 500, 'Failed to set the startLine: ' + JSON.stringify(err));
267
348
  }
349
+ return { state: 'PENDING' }
268
350
  }
269
351
 
270
- async function startTimeCommand(context, path, value, cb) {
352
+ function setTimer() {
353
+ if (state.timerInterval)
354
+ clearInterval(state.timerInterval);
355
+ state.timerInterval = setInterval(() => {
356
+ if (!state.timerRunning || state.timeToStart <= 0) {
357
+ clearInterval(state.timerInterval);
358
+ return;
359
+ }
360
+ state.timeToStart--;
361
+ sendDeltas([{ path: 'navigation.racing.timeToStart', value: state.timeToStart }]);
362
+ }, 1000);
363
+ }
364
+
365
+ async function startTimeCommand(context, path, args, callback) {
271
366
  // start / reset / sync
272
367
  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'});
368
+ app.debug('startTimerCommand:', JSON.stringify(args));
369
+ if (!args || !args.command || typeof args.command !== 'string') {
370
+ complete(callback, 400, 'Failed to command timer: no command');
371
+ return;
277
372
  }
278
- const command = value.command.toLowerCase();
373
+ const command = args.command.toLowerCase();
279
374
 
280
375
  switch (command)
281
376
  {
@@ -293,16 +388,8 @@ module.exports = (app) => {
293
388
  {path: 'navigation.racing.timeToStart', value: timeToStart}
294
389
  ]);
295
390
 
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'});
391
+ setTimer();
392
+ complete(callback, 200, 'start timer: OK');
306
393
  break;
307
394
  }
308
395
  case 'reset': {
@@ -314,13 +401,13 @@ module.exports = (app) => {
314
401
  {path: 'navigation.racing.timeToStart', value: state.timeToStart},
315
402
  {path: 'navigation.racing.startTime', value: null}
316
403
  ]);
317
- cb(null, {state: 'COMPLETED'});
404
+ complete(callback, 200, 'reset timer: OK');
318
405
  break;
319
406
  }
320
407
 
321
408
  case 'sync': {
322
409
  if (!state.timerRunning || state.timeToStart == null) {
323
- cb(null, { state: 'FAILED', message: 'Timer not running' });
410
+ complete(callback, 500, 'sync timer: !running');
324
411
  return;
325
412
  }
326
413
 
@@ -332,27 +419,27 @@ module.exports = (app) => {
332
419
  { path: 'navigation.racing.timeToStart', value: state.timeToStart },
333
420
  { path: 'navigation.racing.startTime', value: state.startTime }
334
421
  ]);
335
- cb(null, { state: 'COMPLETED' });
422
+ complete(callback, 200, 'sync timer: OK');
336
423
  break;
337
424
  }
338
425
 
339
426
  case 'set': {
340
- const input = value.startTime;
427
+ const input = args.startTime;
341
428
  if (!input) {
342
- cb(null, { state: 'FAILED', message: 'Missing startTime' });
429
+ complete(callback, 400, 'reset timer: !startTime');
343
430
  return;
344
431
  }
345
432
 
346
433
  const inputTime = new Date(input);
347
434
  if (isNaN(inputTime)) {
348
- cb(null, { state: 'FAILED', message: 'Invalid startTime format' });
435
+ complete(callback, 400, 'reset timer: invalid startTime');
349
436
  return;
350
437
  }
351
438
 
352
439
  const now = new Date();
353
- const diff = Math.floor((inputTime - now) / 1000);
440
+ const secondsToGo = Math.floor((inputTime - now) / 1000);
354
441
 
355
- if (diff <= 0) {
442
+ if (secondsToGo <= 0) {
356
443
  state.timerRunning = false;
357
444
  state.timeToStart = 0;
358
445
  state.startTime = null;
@@ -360,12 +447,12 @@ module.exports = (app) => {
360
447
  { path: 'navigation.racing.timeToStart', value: 0 },
361
448
  { path: 'navigation.racing.startTime', value: null }
362
449
  ]);
363
- cb(null, { state: 'COMPLETED' });
450
+ complete(callback, 200, 'set timer: 0K');
364
451
  return;
365
452
  }
366
453
 
367
454
  state.timerRunning = true;
368
- state.timeToStart = diff;
455
+ state.timeToStart = secondsToGo;
369
456
  state.startTime = inputTime.toISOString();
370
457
 
371
458
  sendDeltas([
@@ -373,21 +460,13 @@ module.exports = (app) => {
373
460
  { path: 'navigation.racing.startTime', value: state.startTime }
374
461
  ]);
375
462
 
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' });
463
+ setTimer();
464
+ complete(callback, 200, 'set timer: OK');
386
465
  break;
387
466
  }
388
467
 
389
468
  case 'adjust': {
390
- const delta = Number(value.delta);
469
+ const delta = Number(args.delta);
391
470
  if (isNaN(delta) || state.timeToStart == null || !state.startTime) {
392
471
  cb(null, { state: 'FAILED', message: 'Cannot adjust: invalid state or delta' });
393
472
  return;
@@ -396,28 +475,27 @@ module.exports = (app) => {
396
475
  state.timeToStart += delta;
397
476
  if (state.timeToStart < 0) state.timeToStart = 0;
398
477
 
399
- const adjustedStart = new Date(Date.now() + state.timeToStart * 1000).toISOString();
478
+ let now = Date.now();
479
+ const adjustedStart = new Date(now - (now % 1000) + state.timeToStart * 1000).toISOString();
400
480
  state.startTime = adjustedStart;
401
481
 
402
482
  sendDeltas([
403
483
  { path: 'navigation.racing.timeToStart', value: state.timeToStart },
404
484
  { path: 'navigation.racing.startTime', value: adjustedStart }
405
485
  ]);
406
- cb(null, { state: 'COMPLETED' });
486
+ complete(callback, 200, 'adjust timer: OK');
407
487
  break;
408
488
  }
409
489
 
410
490
  default : {
411
- app.error('Unknown command to start timer: ' + command);
412
- cb(null, {state: 'FAILED'});
491
+ complete(callback, 400, 'Failed to command timer: unknown command');
413
492
  return;
414
493
  }
415
494
  }
416
495
  } catch (err) {
417
- app.error('Failed to set start line end:', JSON.stringify(err));
418
- cb(null, {state: 'FAILED'});
419
- throw err;
496
+ complete(callback, 500, 'Failed to command timer: ' + JSON.stringify(err));
420
497
  }
498
+ return { state: 'PENDING' }
421
499
  }
422
500
 
423
501
  function findLineAndThenProcess(position, alwaysFindLine = false) {
@@ -727,7 +805,7 @@ module.exports = (app) => {
727
805
  });
728
806
  processWind(twd);
729
807
  }
730
- )
808
+ );
731
809
 
732
810
  // Subscribe to Active route.
733
811
  app.subscriptionmanager.subscribe(
@@ -758,6 +836,44 @@ module.exports = (app) => {
758
836
  }
759
837
  );
760
838
 
839
+ // Subscribe to racing start line to see changes from any other plugin.
840
+ app.subscriptionmanager.subscribe(
841
+ {
842
+ context: 'vessels.' + app.selfId,
843
+ subscribe: [
844
+ { path: 'navigation.racing.startLinePort', policy: 'instant'},
845
+ { path: 'navigation.racing.startLineStb', policy: 'instant'},
846
+ ]
847
+ },
848
+ unsubscribes,
849
+ (subscriptionError) => {
850
+ app.error('Error:' + subscriptionError)
851
+ },
852
+ (delta) => {
853
+ app.debug('DELTAS START LINE ENDS ' + JSON.stringify(delta));
854
+ delta.updates.forEach((u) => {
855
+ u.values.forEach((v) => {
856
+ app.debug(`DELTA START LINE END from ${u.source.label}: ${v.path} = ${JSON.stringify(v.value)}`);
857
+ if (u.source && u.source.label && u.source.label !== 'signalk-racer') {
858
+ switch (v.path) {
859
+ case 'navigation.racing.startLinePort' :
860
+ putStartLineEnd('port', v.value, ignored => {}, false).then(ignored =>{});
861
+ break;
862
+ case 'navigation.racing.startLineStb' :
863
+ putStartLineEnd('port', v.value, ignored => {}, false).then(ignored =>{});
864
+ break;
865
+ }
866
+ }
867
+ })
868
+ });
869
+ }
870
+ );
871
+
872
+ // register to the standard path, so we can implement a direct put
873
+ app.registerPutHandler('vessels.self', 'navigation.racing.startLinePort', (ctx, path, value, callback) => putStartLineEnd('port', value, callback));
874
+ app.registerPutHandler('vessels.self', 'navigation.racing.startLineStb', (ctx, path, value, callback) => putStartLineEnd('stb', value, callback));
875
+
876
+ // register to API paths for this plugin
761
877
  app.registerPutHandler('vessels.self', 'navigation.racing.setStartLine', setStartLine);
762
878
  app.registerPutHandler('vessels.self', 'navigation.racing.setStartTime', startTimeCommand);
763
879
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-racer",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
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,11 +23,11 @@
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 {
24
- padding: 1em 1.5em;
30
+ padding: 1em 1em;
25
31
  font-size: 1em;
26
32
  cursor: pointer;
27
33
  }
@@ -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,89 +247,60 @@
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
-
239
-
240
- <div style="display: grid; grid-template-columns: repeat(11, auto); grid-template-rows: repeat(3, auto); gap: 0.5em;">
241
- <div></div>
251
+ <div style="display: grid; grid-template-columns: repeat(6, auto); grid-template-rows: repeat(3, auto); gap: 0.5em;">
242
252
  <div></div>
243
253
  <button class="port-rotate" onclick="sendAdjustStartLine('port', 0, 1)">+1°</button>
244
- <div></div>
245
- <div></div>
246
-
247
254
  <div></div>
248
255
 
249
- <div></div>
250
256
  <div></div>
251
257
  <button class="stb-rotate" onclick="sendAdjustStartLine('stb', 0, -1)">+1°</button>
252
258
  <div></div>
253
- <div></div>
254
259
 
255
260
  <!--------->
256
261
 
257
- <button class="port-move" onclick="sendAdjustStartLine('port', 10, 0)">+10m</button>
258
- <button class="port-tweak" onclick="sendAdjustStartLine('port', 1, 0)">+1m</button>
259
- <button class="port-set" onclick="sendSetStartLine('port')">Port<br/>(pin)</button>
260
- <button class="port-tweak" onclick="sendAdjustStartLine('port', -1, 0)">-1m</button>
261
- <button class="port-move" onclick="sendAdjustStartLine('port', -10, 0)">-10m</button>
262
-
263
- <div style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
264
- ---------------<br/>
265
- <label><input id="lineLength" readonly style="width: 5em; text-align: center;">(m)</label>
266
- </div>
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>
267
265
 
268
- <button class="stb-move" onclick="sendAdjustStartLine('stb', -10, 0)">-10m</button>
269
- <button class="stb-tweak" onclick="sendAdjustStartLine('stb', -1, 0)">-1m</button>
270
- <button class="stb-set" onclick="sendSetStartLine('stb')">Starboard<br/>(boat)</button>
271
- <button class="stb-tweak" onclick="sendAdjustStartLine('stb', 1, 0)">+1m</button>
272
- <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>
273
269
 
274
270
  <!--------->
275
271
 
276
- <div></div>
277
272
  <div></div>
278
273
  <button class="port-rotate" onclick="sendAdjustStartLine('port', 0, -1)">-1°</button>
279
- <div></div>
280
- <div></div>
281
-
282
- <div style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
283
- <label>Bias: <input id="lineBias" readonly style="width: 5em; text-align: center;">(m)</label>
284
- </div>
285
-
286
274
  <div></div>
287
275
  <div></div>
288
276
  <button class="stb-rotate" onclick="sendAdjustStartLine('stb', 0, 1)">-1°</button>
289
277
  <div></div>
290
- <div></div>
291
278
  </div>
292
-
293
-
294
279
  </div>
295
280
 
296
- <div id="status">Connecting...</div>
297
-
298
281
  <!-- Timer Display -->
299
- <div style="margin-top: 2em;">
300
- <div id="timeToStart" style="font-size: 3em; font-weight: bold;">00:00</div>
301
- </div>
282
+ <!-- div style="margin-top: 2em;">
283
+ <div id="timeToStart" style="font-size: 4em; font-weight: bold;">00:00</div>
284
+ </div -->
302
285
 
303
286
  <!-- Start Time Row -->
304
- <div style="margin-top: 1em; display: flex; justify-content: center; align-items: center; gap: 1em;">
305
- <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>
306
289
  <button class="button-timer-adjust" onclick="adjustStartTime(-1)">-1s</button>
307
- <label for="startTime">Start at: <input id="startTime" style="font-size: 1.5em; width: 10em; text-align: center;"></label>
290
+ <label for="startTime">At: <input id="startTime" style="font-size: 1.5em; width: 6em; text-align: center;"></label>
308
291
  <button class="button-timer-adjust" onclick="adjustStartTime(1)">+1s</button>
309
- <button class="button-timer-adjust" onclick="adjustStartTime(60)">+1 min</button>
292
+ <button class="button-timer-adjust" onclick="adjustStartTime(60)">+1min</button>
310
293
  </div>
311
294
 
312
295
  <!-- Timer Control Buttons -->
313
- <div style="margin-top: 1em;">
296
+ <div style="margin-top: 0.5em;">
314
297
  <button class="button-timer-command" onclick="sendTimerCommand('start')">Start</button>
315
- <button class="button-timer-command" onclick="setStartTime()">Start At</button>
298
+ <button class="button-timer-command" onclick="setStartTime()">At</button>
316
299
  <button class="button-timer-command" onclick="sendTimerCommand('sync')">Sync</button>
317
300
  <button class="button-timer-command" onclick="sendTimerCommand('reset')">Reset</button>
318
301
  </div>
319
302
 
303
+ <div id="status">Connecting...</div>
304
+
320
305
  </body>
321
306
  </html>