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 +3 -3
- package/ReleaseNotes.md +27 -14
- package/index.js +232 -116
- package/package.json +1 -1
- package/public/index.html +59 -74
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
|
-
```
|
|
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
|
-
```
|
|
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.
|
|
2
|
-
+
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
+
|
|
7
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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": "
|
|
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
|
|
64
|
-
"shortName": "
|
|
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
|
|
73
|
-
"shortName": "
|
|
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
|
-
|
|
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;
|
|
@@ -396,28 +475,27 @@ module.exports = (app) => {
|
|
|
396
475
|
state.timeToStart += delta;
|
|
397
476
|
if (state.timeToStart < 0) state.timeToStart = 0;
|
|
398
477
|
|
|
399
|
-
|
|
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
|
-
|
|
486
|
+
complete(callback, 200, 'adjust timer: OK');
|
|
407
487
|
break;
|
|
408
488
|
}
|
|
409
489
|
|
|
410
490
|
default : {
|
|
411
|
-
|
|
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
|
-
|
|
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
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,11 +23,11 @@
|
|
|
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 {
|
|
24
|
-
padding: 1em
|
|
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:
|
|
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,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',
|
|
258
|
-
<button class="port-
|
|
259
|
-
<button class="port-
|
|
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', -
|
|
269
|
-
<button class="stb-
|
|
270
|
-
<button class="stb-
|
|
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
|
-
|
|
300
|
-
<div id="timeToStart" style="font-size:
|
|
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:
|
|
305
|
-
<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>
|
|
306
289
|
<button class="button-timer-adjust" onclick="adjustStartTime(-1)">-1s</button>
|
|
307
|
-
<label for="startTime">
|
|
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)">+
|
|
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:
|
|
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()">
|
|
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>
|