signalk-racer 0.9.7 → 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/ReleaseNotes.md +4 -1
- package/index.js +223 -108
- package/package.json +1 -1
- package/public/index.html +55 -57
package/ReleaseNotes.md
CHANGED
package/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
module.exports = (app) => {
|
|
2
3
|
const state = {};
|
|
3
4
|
const geolib = require('geolib')
|
|
@@ -15,6 +16,16 @@ module.exports = (app) => {
|
|
|
15
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
31
|
title: 'The subscription period in microseconds used for position and wind deltas',
|
|
@@ -60,7 +71,7 @@ module.exports = (app) => {
|
|
|
60
71
|
"value": {
|
|
61
72
|
"units": "rad",
|
|
62
73
|
"description": "Heading of the next leg of the course",
|
|
63
|
-
"displayName": "Heading
|
|
74
|
+
"displayName": "Next Heading",
|
|
64
75
|
"shortName": "NextHDG",
|
|
65
76
|
}
|
|
66
77
|
},
|
|
@@ -69,7 +80,7 @@ module.exports = (app) => {
|
|
|
69
80
|
"value": {
|
|
70
81
|
"units": "rad",
|
|
71
82
|
"description": "TWA on the next leg of the course",
|
|
72
|
-
"displayName": "TWA
|
|
83
|
+
"displayName": "Next TWA",
|
|
73
84
|
"shortName": "NextTWA",
|
|
74
85
|
}
|
|
75
86
|
},
|
|
@@ -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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
const
|
|
190
|
-
app.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
198
|
-
|
|
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(`
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
274
|
-
if (!
|
|
275
|
-
|
|
276
|
-
|
|
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 =
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
+
complete(callback, 200, 'sync timer: OK');
|
|
336
423
|
break;
|
|
337
424
|
}
|
|
338
425
|
|
|
339
426
|
case 'set': {
|
|
340
|
-
const input =
|
|
427
|
+
const input = args.startTime;
|
|
341
428
|
if (!input) {
|
|
342
|
-
|
|
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
|
-
|
|
435
|
+
complete(callback, 400, 'reset timer: invalid startTime');
|
|
349
436
|
return;
|
|
350
437
|
}
|
|
351
438
|
|
|
352
439
|
const now = new Date();
|
|
353
|
-
const
|
|
440
|
+
const secondsToGo = Math.floor((inputTime - now) / 1000);
|
|
354
441
|
|
|
355
|
-
if (
|
|
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
|
-
|
|
450
|
+
complete(callback, 200, 'set timer: 0K');
|
|
364
451
|
return;
|
|
365
452
|
}
|
|
366
453
|
|
|
367
454
|
state.timerRunning = true;
|
|
368
|
-
state.timeToStart =
|
|
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
|
-
|
|
377
|
-
|
|
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(
|
|
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;
|
|
@@ -404,21 +483,19 @@ module.exports = (app) => {
|
|
|
404
483
|
{ path: 'navigation.racing.timeToStart', value: state.timeToStart },
|
|
405
484
|
{ path: 'navigation.racing.startTime', value: adjustedStart }
|
|
406
485
|
]);
|
|
407
|
-
|
|
486
|
+
complete(callback, 200, 'adjust timer: OK');
|
|
408
487
|
break;
|
|
409
488
|
}
|
|
410
489
|
|
|
411
490
|
default : {
|
|
412
|
-
|
|
413
|
-
cb(null, {state: 'FAILED'});
|
|
491
|
+
complete(callback, 400, 'Failed to command timer: unknown command');
|
|
414
492
|
return;
|
|
415
493
|
}
|
|
416
494
|
}
|
|
417
495
|
} catch (err) {
|
|
418
|
-
|
|
419
|
-
cb(null, {state: 'FAILED'});
|
|
420
|
-
throw err;
|
|
496
|
+
complete(callback, 500, 'Failed to command timer: ' + JSON.stringify(err));
|
|
421
497
|
}
|
|
498
|
+
return { state: 'PENDING' }
|
|
422
499
|
}
|
|
423
500
|
|
|
424
501
|
function findLineAndThenProcess(position, alwaysFindLine = false) {
|
|
@@ -728,7 +805,7 @@ module.exports = (app) => {
|
|
|
728
805
|
});
|
|
729
806
|
processWind(twd);
|
|
730
807
|
}
|
|
731
|
-
)
|
|
808
|
+
);
|
|
732
809
|
|
|
733
810
|
// Subscribe to Active route.
|
|
734
811
|
app.subscriptionmanager.subscribe(
|
|
@@ -759,6 +836,44 @@ module.exports = (app) => {
|
|
|
759
836
|
}
|
|
760
837
|
);
|
|
761
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
|
|
762
877
|
app.registerPutHandler('vessels.self', 'navigation.racing.setStartLine', setStartLine);
|
|
763
878
|
app.registerPutHandler('vessels.self', 'navigation.racing.setStartTime', startTimeCommand);
|
|
764
879
|
},
|
package/package.json
CHANGED
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
document.getElementById('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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(
|
|
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',
|
|
254
|
-
<button class="port-
|
|
255
|
-
<button class="port-
|
|
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', -
|
|
260
|
-
<button class="stb-
|
|
261
|
-
<button class="stb-
|
|
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: <input id="lineLength" readonly style="width: 5em; text-align: center;">m</label>
|
|
281
|
-
<label>Bias: <input id="lineBias" readonly style="width: 5em; text-align: center;">m</label>
|
|
282
|
-
|
|
283
281
|
<!-- Timer Display -->
|
|
284
|
-
|
|
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:
|
|
290
|
-
<button class="button-timer-adjust" onclick="adjustStartTime(-60)">-
|
|
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">
|
|
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)">+
|
|
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:
|
|
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()">
|
|
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>
|