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 +7 -1
- package/index.js +236 -132
- package/package.json +1 -1
- package/public/index.html +55 -57
package/ReleaseNotes.md
CHANGED
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
app.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
const
|
|
190
|
-
app.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
198
|
-
|
|
199
|
-
const waypointConfig
|
|
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(`
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
274
|
-
if (!
|
|
275
|
-
|
|
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 =
|
|
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
|
-
|
|
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'});
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
333
|
-
{
|
|
413
|
+
{path: 'navigation.racing.timeToStart', value: state.timeToStart},
|
|
414
|
+
{path: 'navigation.racing.startTime', value: state.startTime}
|
|
334
415
|
]);
|
|
335
|
-
|
|
336
|
-
break;
|
|
416
|
+
return complete(callback, 200, 'sync timer: OK');
|
|
337
417
|
}
|
|
338
418
|
|
|
339
419
|
case 'set': {
|
|
340
|
-
const input =
|
|
420
|
+
const input = args.startTime;
|
|
341
421
|
if (!input) {
|
|
342
|
-
|
|
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
|
-
|
|
349
|
-
return;
|
|
427
|
+
return complete(callback, 400, 'reset timer: invalid startTime');
|
|
350
428
|
}
|
|
351
429
|
|
|
352
430
|
const now = new Date();
|
|
353
|
-
const
|
|
431
|
+
const secondsToGo = Math.floor((inputTime - now) / 1000);
|
|
354
432
|
|
|
355
|
-
if (
|
|
433
|
+
if (secondsToGo <= 0) {
|
|
356
434
|
state.timerRunning = false;
|
|
357
435
|
state.timeToStart = 0;
|
|
358
436
|
state.startTime = null;
|
|
359
437
|
sendDeltas([
|
|
360
|
-
{
|
|
361
|
-
{
|
|
438
|
+
{path: 'navigation.racing.timeToStart', value: 0},
|
|
439
|
+
{path: 'navigation.racing.startTime', value: null}
|
|
362
440
|
]);
|
|
363
|
-
|
|
364
|
-
return;
|
|
441
|
+
return complete(callback, 200, 'set timer: 0K');
|
|
365
442
|
}
|
|
366
443
|
|
|
367
444
|
state.timerRunning = true;
|
|
368
|
-
state.timeToStart =
|
|
445
|
+
state.timeToStart = secondsToGo;
|
|
369
446
|
state.startTime = inputTime.toISOString();
|
|
370
447
|
|
|
371
448
|
sendDeltas([
|
|
372
|
-
{
|
|
373
|
-
{
|
|
449
|
+
{path: 'navigation.racing.timeToStart', value: state.timeToStart},
|
|
450
|
+
{path: 'navigation.racing.startTime', value: state.startTime}
|
|
374
451
|
]);
|
|
375
452
|
|
|
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' });
|
|
386
|
-
break;
|
|
453
|
+
setTimer();
|
|
454
|
+
return complete(callback, 200, 'set timer: OK');
|
|
387
455
|
}
|
|
388
456
|
|
|
389
457
|
case 'adjust': {
|
|
390
|
-
const delta = Number(
|
|
458
|
+
const delta = Number(args.delta);
|
|
391
459
|
if (isNaN(delta) || state.timeToStart == null || !state.startTime) {
|
|
392
|
-
cb(null, {
|
|
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
|
-
{
|
|
405
|
-
{
|
|
472
|
+
{path: 'navigation.racing.timeToStart', value: state.timeToStart},
|
|
473
|
+
{path: 'navigation.racing.startTime', value: adjustedStart}
|
|
406
474
|
]);
|
|
407
|
-
|
|
408
|
-
break;
|
|
475
|
+
return complete(callback, 200, 'adjust timer: OK');
|
|
409
476
|
}
|
|
410
477
|
|
|
411
478
|
default : {
|
|
412
|
-
|
|
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
|
-
|
|
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
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>
|