signalk-racer 0.9.12 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Signal K Racing Plugin
2
2
 
3
- This plugin adds sail racing-relevant data to a Signal K server. It focuses on real-time information useful during the start of a race, such as:
3
+ This plugin adds sail racing relevant data to a Signal K server. It focuses on real-time information useful during the start of a race, such as:
4
4
  + A server side race timer that can be displayed on multiple screens and be finely adjusted
5
5
  + A mechanism to set and adjust the start line using waypoints and REST commands
6
6
  + Calculation of the distance to the line.
@@ -26,9 +26,10 @@ This plugin calculates and publishes the following Signal K paths:
26
26
  | `navigation.racing.`<br/>`timeToStart` | Period of time until the race start | `s` | Y |
27
27
  | `navigation.racing.`<br/>`startTime` | The start time as an ISO timestamp | `rfc3339` | |
28
28
 
29
- These values can be displayed in KIP widgets, Freeboard-SK, or other Signal K clients.
29
+ These values can be displayed in KIP widgets, Freeboard-SK, or other Signal K clients.
30
+ There are dedicated widgets for the start timer and line adjustment since 3.5.0 of KIP
30
31
 
31
- ![KIP and Freeboard Screenshot](kip-freeboard-screenshot.png)
32
+ ![KIP and Freeboard Screenshot](racer-kip.png)
32
33
 
33
34
  There is also a webapp to adjust the line and timer.
34
35
 
@@ -95,7 +96,7 @@ If `navigation.headingTrue` is not available, then `navigation.courseOverGroundT
95
96
 
96
97
  ## 🌐 API Access
97
98
 
98
- This plugin uses WebSocket-based PUT requests to Signal K model paths.
99
+ This plugin uses the following WebSocket-based PUT requests to Signal K model paths.
99
100
 
100
101
  ### `navigation.racing.setStartLine`
101
102
 
@@ -133,13 +134,15 @@ Used to **start**, **sync**, **reset**, or **set a fixed start time**.
133
134
  ---
134
135
 
135
136
  ## 🧩 KIP Widgets
136
- KIP components for `racer-timer` and `racer-line` have been developed and will be available in a future release of KIP.
137
+ KIP components for `racer-timer` and `racer-line` have been developed and are available from release 3.5.0 of KIP.
137
138
 
138
139
  ## 🔄 Dependencies
139
140
 
140
141
  ### Plugins
142
+ - [KIP >= 3.5.0](https://github.com/mxtommy/Kip) for graphic widget support
143
+ - [FreeboardSK](https://github.com/SignalK/freeboard-sk#readme) for graphic display of the start line
141
144
  - [resources-provider](https://www.npmjs.com/package/@signalk/resources-provider) plugin with waypoints enabled
142
- - [course-provider](https://www.npmjs.com/package/@signalk/course-provider) plugin
145
+ - [course-provider](https://www.npmjs.com/package/@signalk/course-provider) plugin to enable the next leg calculations
143
146
 
144
147
  ### Libraries
145
148
  - `geolib`: for distance and bearing calculations.
@@ -151,7 +154,6 @@ KIP components for `racer-timer` and `racer-line` have been developed and will b
151
154
  - Add calculations for:
152
155
  - **Time to line** (using STW and heading or polar speeds)
153
156
  - **Laylines** and distance/time to them
154
- - Better integration with **Freeboard-SK** and **KIP** dashboards
155
157
 
156
158
  ---
157
159
  ## 📬 Feedback
package/ReleaseNotes.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 1.0.0 GA Release with KIP Widgets
2
+ + Fixed bias to be towards wind
3
+
4
+ ## 0.9.13 GA Release Candidate
5
+ + handle reverse routes
6
+ + Improve startAt date handling
7
+ + persist timer over plugin restart
8
+
1
9
  ## 0.9.12 No waypoints
2
10
  + improve no waypoint handling
3
11
  + fix create waypoint
package/index.js CHANGED
@@ -105,7 +105,7 @@ module.exports = (app) => {
105
105
  ]
106
106
  });
107
107
 
108
- // send multiple deltas, each in the form of { path, value, units?}
108
+ // send multiple deltas, each in the form of { path, value }
109
109
  function sendDeltas(updatesArray) {
110
110
  const timestamp = new Date().toISOString();
111
111
 
@@ -199,14 +199,12 @@ module.exports = (app) => {
199
199
  app.debug(`putStartLineEnd: ${end} ${JSON.stringify(position)}`);
200
200
 
201
201
  try {
202
- if (!end || !position || typeof position.latitude !== 'number' || typeof position.longitude !== 'number') {
202
+ if (!end || !position || typeof position.latitude !== 'number' || typeof position.longitude !== 'number')
203
203
  return complete(callback, 400, 'Failed to put start line end: !end || !position');
204
- }
205
204
 
206
205
  end = end.toLowerCase();
207
- if (end !== 'port' && end !== 'stb') {
206
+ if (end !== 'port' && end !== 'stb')
208
207
  return complete(callback, 400, 'Failed to put start line end: unknown end');
209
- }
210
208
 
211
209
  let startLine = state.startLine;
212
210
  if (startLine) {
@@ -427,9 +425,8 @@ module.exports = (app) => {
427
425
  }
428
426
 
429
427
  case 'sync': {
430
- if (!state.timerRunning || state.timeToStart == null) {
428
+ if (!state.timerRunning || state.timeToStart == null)
431
429
  return complete(callback, 500, 'sync timer: !running');
432
- }
433
430
 
434
431
  const rounded = Math.round(state.timeToStart / 60) * 60;
435
432
  state.timeToStart = rounded;
@@ -465,7 +462,7 @@ module.exports = (app) => {
465
462
  {path: 'navigation.racing.timeToStart', value: 0},
466
463
  {path: 'navigation.racing.startTime', value: null}
467
464
  ]);
468
- return complete(callback, 200, 'set timer: 0K');
465
+ return complete(callback, 200, 'set timer: Ignored negative start time');
469
466
  }
470
467
 
471
468
  state.timerRunning = true;
@@ -483,12 +480,17 @@ module.exports = (app) => {
483
480
 
484
481
  case 'adjust': {
485
482
  const delta = Number(args.delta);
486
- if (isNaN(delta) || state.timeToStart == null || !state.startTime) {
487
- cb(null, {state: 'FAILED', message: 'Cannot adjust: invalid state or delta'});
488
- return;
489
- }
483
+ if (isNaN(delta))
484
+ return complete(callback, 400, 'adjust timer: Cannot adjust invalid delta');
485
+
486
+ if (state.timeToStart == null)
487
+ return complete(callback, 400, 'adjust timer: Cannot adjust start time');
490
488
 
491
- state.timeToStart = Math.max(state.timeToStart + delta, 0);
489
+ const nextTimeToStart = state.timeToStart + delta;
490
+ if (nextTimeToStart <= 0)
491
+ return complete(callback, 200, 'adjust timer: Ignored negative start time');
492
+
493
+ state.timeToStart = nextTimeToStart;
492
494
 
493
495
  if (state.timerRunning) {
494
496
  let now = Date.now();
@@ -501,7 +503,9 @@ module.exports = (app) => {
501
503
  if (state.timerRunning && state.startTime) {
502
504
  deltas.push({path: 'navigation.racing.startTime', value: state.startTime });
503
505
  }
504
- setTimer();
506
+ if (delta % 60 !== 0)
507
+ setTimer();
508
+
505
509
  sendDeltas(deltas);
506
510
 
507
511
  return complete(callback, 200, 'adjust timer: OK');
@@ -512,8 +516,8 @@ module.exports = (app) => {
512
516
  }
513
517
  }
514
518
  } catch (err) {
515
- app.debug('startTimerCommand:', JSON.stringify(err));
516
- return complete(callback, 500, 'Failed to command timer: ' + JSON.stringify(err));
519
+ app.debug('startTimerCommand:', err);
520
+ return complete(callback, 500, 'Failed to command timer: ' + err.message);
517
521
  }
518
522
  }
519
523
 
@@ -577,6 +581,23 @@ module.exports = (app) => {
577
581
  }
578
582
  }
579
583
 
584
+ function isNearLineInRoute() {
585
+ // e.g. {
586
+ // "href":"/resources/routes/a12eefe7-8fd3-49f7-81af-ce1f3440d3ff",
587
+ // "name":"Course 1",
588
+ // "reverse":false,
589
+ // "pointIndex":1,
590
+ // "pointTotal":5
591
+ // }
592
+
593
+ const activeRoute = state.activeRoute;
594
+ if (!activeRoute)
595
+ return true;
596
+ if (activeRoute.pointIndex < 2)
597
+ return true;
598
+ return activeRoute.pointIndex + 2 >= activeRoute.pointTotal;
599
+ }
600
+
580
601
  function processPosition(position) {
581
602
  if (!position) {
582
603
  position = app.getSelfPath('navigation.position');
@@ -587,7 +608,7 @@ module.exports = (app) => {
587
608
  const startLine = state.startLine;
588
609
 
589
610
  app.debug(`processPosition ${JSON.stringify(position)} to ${JSON.stringify(startLine)}`);
590
- if (position && startLine && (!state.activeRoute || state.activeRoute.pointIndex < 2)) {
611
+ if (position && startLine && isNearLineInRoute()) {
591
612
  // handle the bow offset
592
613
  const bow = bowPosition(position);
593
614
 
@@ -636,6 +657,7 @@ module.exports = (app) => {
636
657
  state.distanceToLine = null;
637
658
  sendDeltas([
638
659
  {path: 'navigation.racing.distanceStartline', value: null},
660
+ {path: 'navigation.racing.startLineBias', value: null},
639
661
  ]);
640
662
  }
641
663
  }
@@ -652,19 +674,16 @@ module.exports = (app) => {
652
674
 
653
675
  let nextTwa = null;
654
676
  const activeRoute = state.activeRoute;
655
- if (activeRoute) {
656
- const nextLegHeading = activeRoute.nextLegHeading;
657
- if (nextLegHeading) {
658
- nextTwa = (540 + toDegrees(twd) - nextLegHeading) % 360 - 180;
659
- app.debug(`nextTwa: ${nextTwa}`);
660
- }
677
+ if (activeRoute && activeRoute.nextLegHeading) {
678
+ nextTwa = (540 + toDegrees(twd) - activeRoute.nextLegHeading) % 360 - 180;
679
+ app.debug(`nextTwa: ${nextTwa}`);
661
680
  }
662
681
 
663
682
  let bias = null;
664
683
  const startLine = state.startLine;
665
- if (startLine && (!activeRoute || activeRoute.pointIndex < 2)) {
684
+ if (startLine && isNearLineInRoute()) {
666
685
  app.debug(`processWind ${twd} to ${JSON.stringify(startLine)}`);
667
- bias = startLine.length * Math.cos(toRadians(startLine.bearing) - (twd + Math.PI));
686
+ bias = - startLine.length * Math.cos(toRadians(startLine.bearing) - twd);
668
687
  }
669
688
 
670
689
  sendDeltas([
@@ -674,16 +693,50 @@ module.exports = (app) => {
674
693
  }
675
694
 
676
695
  function processRoute(activeRoute) {
677
- if (activeRoute && (activeRoute.pointIndex + 1) < activeRoute.pointTotal) {
696
+ if (activeRoute) {
678
697
  // e.g. {"href":"/resources/routes/a12eefe7-8fd3-49f7-81af-ce1f3440d3ff","name":"Course 1","reverse":false,"pointIndex":1,"pointTotal":5}
698
+ const nextPointIndex = activeRoute.pointIndex + 1;
699
+ if (nextPointIndex < 0 || nextPointIndex === activeRoute.pointTotal) {
700
+ activeRoute.nextLegHeading = null;
701
+ sendDeltas([
702
+ {path: 'navigation.racing.nextLegHeading', value: null},
703
+ {path: 'navigation.racing.nextLegTrueWindAngle', value: null}
704
+ ]);
705
+ processPosition();
706
+ return;
707
+ }
708
+
679
709
  const routeId = activeRoute.href.split('/').pop(); // e.g. "a12eefe7-8fd3-49f7-81af-ce1f3440d3ff"
680
710
  app.debug('processRoute:' + routeId);
681
711
  app.resourcesApi.getResource('routes', routeId).then(route => {
682
712
  app.debug('found active route:', JSON.stringify(route));
683
- // e.g. {name:"Course 1",description:"Test race course",distance:3027,feature:{type:"Feature",geometry:{type:"LineString",coordinates:[[151.2789194160523,-33.80427227074185],[151.28230238234357,-33.813458268981435],[151.2758044350174,-33.80201284252691],[151.27864400784992,-33.801915364141514],[151.27982384226053,-33.80429179893497]]},properties:{coordinatesMeta:[{href:"/resources/waypoints/bb35d9d0-c04e-4721-89f2-a1d8e697ab81"},{href:"/resources/waypoints/65082ddd-6fa6-4538-9a01-49d5d795b0bb"},{href:"/resources/waypoints/0bbce508-67c6-4f03-8de0-fb0532f88917"},{href:"/resources/waypoints/602ee562-c6dc-42c3-a775-512597063ca4"},{href:"/resources/waypoints/2e3cf194-8835-498d-abaa-a5d2104550b3"}]},id:""},timestamp:"2025-06-09T22:22:54.739Z",$source:"resources-provider"}
713
+ // e.g. {
714
+ // name:"Course 1",
715
+ // description:"Test race course",
716
+ // distance:3027,
717
+ // feature:{
718
+ // type:"Feature",
719
+ // geometry:{
720
+ // type:"LineString",
721
+ // coordinates:[[151.27,-33.80],[151.28,-33.81],[151.27,-33.80],[151.27,-33.80],[151.27,-33.80]]},
722
+ // properties:{
723
+ // coordinatesMeta:[
724
+ // {href:"/resources/waypoints/bb35d9d0-c04e-4721-89f2-a1d8e697ab81"},
725
+ // {href:"/resources/waypoints/65082ddd-6fa6-4538-9a01-49d5d795b0bb"},
726
+ // {href:"/resources/waypoints/0bbce508-67c6-4f03-8de0-fb0532f88917"},
727
+ // {href:"/resources/waypoints/602ee562-c6dc-42c3-a775-512597063ca4"},
728
+ // {href:"/resources/waypoints/2e3cf194-8835-498d-abaa-a5d2104550b3"}
729
+ // ]
730
+ // },
731
+ // id:""
732
+ // },
733
+ // timestamp:"2025-06-09T22:22:54.739Z",
734
+ // $source:"resources-provider"}
684
735
  const coordinates = route.feature.geometry.coordinates;
685
- const [fromLongitude, fromLatitude] = coordinates[activeRoute.pointIndex];
686
- const [toLongitude, toLatitude] = coordinates[activeRoute.pointIndex + 1];
736
+ const fromIndex = activeRoute.reverse ? activeRoute.pointTotal - activeRoute.pointIndex - 1 : activeRoute.pointIndex;
737
+ const toIndex = activeRoute.reverse ? activeRoute.pointTotal - nextPointIndex - 1 : nextPointIndex;
738
+ const [fromLongitude, fromLatitude] = coordinates[fromIndex];
739
+ const [toLongitude, toLatitude] = coordinates[toIndex];
687
740
  app.debug(`from: {longitude:${fromLongitude}, latitude:${fromLatitude}}, to: {longitude:${toLongitude}, latitude:${toLatitude}}`);
688
741
  const nextLegHeading = geolib.getRhumbLineBearing(
689
742
  {latitude: fromLatitude, longitude: fromLongitude},
@@ -693,16 +746,20 @@ module.exports = (app) => {
693
746
  activeRoute.nextLegHeading = nextLegHeading;
694
747
  state.activeRoute = activeRoute;
695
748
  sendDeltas([
696
- {path: 'navigation.racing.nextLegHeading', value: toRadians(nextLegHeading)}
749
+ {path: 'navigation.racing.nextLegHeading', value: toRadians(nextLegHeading)},
750
+ {path: 'navigation.racing.nextLegTrueWindAngle', value: null}
697
751
  ]);
752
+ processWind();
698
753
  }).catch(err => {
699
754
  app.error(err);
700
755
  });
701
756
  } else {
702
757
  state.activeRoute = null;
703
758
  sendDeltas([
704
- {path: 'navigation.racing.nextLegHeading', value: null}
759
+ {path: 'navigation.racing.nextLegHeading', value: null},
760
+ {path: 'navigation.racing.nextLegTrueWindAngle', value: null}
705
761
  ]);
762
+ processPosition(null);
706
763
  }
707
764
  }
708
765
 
@@ -742,6 +799,23 @@ module.exports = (app) => {
742
799
  state.timeToStart = options.timer;
743
800
  state.startLine = getStartLine();
744
801
 
802
+ // Is the timer already started
803
+ {
804
+ const startTime = app.getSelfPath('navigation.racing.startTime');
805
+ if (startTime && startTime.value) {
806
+ state.startTime = startTime.value;
807
+ const now = Date.now();
808
+ const startAtTime = new Date(startTime.value);
809
+ if (startAtTime > now) {
810
+ state.timeToStart = Math.round((new Date(state.startTime) - now) / 1000);
811
+ state.timerRunning = true;
812
+ } else {
813
+ state.timeToStart = 0;
814
+ state.timerRunning = false;
815
+ }
816
+ }
817
+ }
818
+
745
819
  sendDeltas([{path: 'navigation.racing.timeToStart', value: state.timeToStart}]);
746
820
 
747
821
  // Subscribe to position updates.
@@ -762,7 +836,7 @@ module.exports = (app) => {
762
836
 
763
837
  (delta) => {
764
838
  app.debug('DELTA POSITIONS ' + JSON.stringify(delta));
765
- position = null;
839
+ let position = null;
766
840
  delta.updates.forEach((u) => {
767
841
  u.values.forEach((v) => {
768
842
  app.debug('DELTA POSITION ' + v.path + ' = ' + JSON.stringify(v.value));
@@ -863,8 +937,7 @@ module.exports = (app) => {
863
937
  app.debug("DELTA ACTIVE ROUTE: " + v.path + ' = ' + JSON.stringify(route));
864
938
  })
865
939
  });
866
- if (route)
867
- processRoute(route);
940
+ processRoute(route);
868
941
  }
869
942
  );
870
943
 
@@ -884,23 +957,25 @@ module.exports = (app) => {
884
957
  (delta) => {
885
958
  app.debug('DELTAS START LINE ENDS ' + JSON.stringify(delta));
886
959
  delta.updates.forEach((u) => {
887
- u.values.forEach((v) => {
888
- app.debug(`DELTA START LINE END from ${u.source.label}: ${v.path} = ${JSON.stringify(v.value)}`);
889
- if (u.source && u.source.label && u.source.label !== 'signalk-racer') {
890
- switch (v.path) {
891
- case 'navigation.racing.startLinePort' :
892
- putStartLineEnd('port', v.value, ignored => {
893
- }, false).then(ignored => {
894
- });
895
- break;
896
- case 'navigation.racing.startLineStb' :
897
- putStartLineEnd('port', v.value, ignored => {
898
- }, false).then(ignored => {
899
- });
900
- break;
960
+ if (u.values) {
961
+ u.values.forEach((v) => {
962
+ app.debug(`DELTA START LINE END from ${u.source.label}: ${v.path} = ${JSON.stringify(v.value)}`);
963
+ if (u.source && u.source.label && u.source.label !== 'signalk-racer') {
964
+ switch (v.path) {
965
+ case 'navigation.racing.startLinePort' :
966
+ putStartLineEnd('port', v.value, ignored => {
967
+ }, false).then(ignored => {
968
+ });
969
+ break;
970
+ case 'navigation.racing.startLineStb' :
971
+ putStartLineEnd('port', v.value, ignored => {
972
+ }, false).then(ignored => {
973
+ });
974
+ break;
975
+ }
901
976
  }
902
- }
903
- })
977
+ });
978
+ }
904
979
  });
905
980
  }
906
981
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-racer",
3
- "version": "0.9.12",
3
+ "version": "1.0.0",
4
4
  "description": "Signalk plugin to calculate values of interest to sail racers: such as Time to Start, Time of Start, Distance to Line; Next leg TWA.",
5
5
  "main": "index.js",
6
6
  "repository": {
package/public/index.html CHANGED
@@ -44,11 +44,9 @@
44
44
 
45
45
  .port-set { background-color: #f88; color: black; font-size: 1em;}
46
46
  .port-move { background-color: #a00; color: white; }
47
- .port-tweak { background-color: #f44; color: white; }
48
47
  .port-rotate { background-color: #a00; color: white; }
49
48
  .stb-set { background-color: #8f8; color: black; font-size: 1em; }
50
49
  .stb-move { background-color: #080; color: white;}
51
- .stb-tweak { background-color: #4c4; color: white; }
52
50
  .stb-rotate { background-color: #080; color: white; }
53
51
 
54
52
  </style>
@@ -92,9 +90,9 @@
92
90
  }));
93
91
  };
94
92
 
95
- socket.onerror = (err) => updateStatus('❌ WebSocket error: ' + err.message);
93
+ socket.onerror = (err) => updateStatus('WebSocket error: ' + err.message);
96
94
  socket.onclose = () => {
97
- updateStatus('❌ Disconnected. Retrying in 5s...');
95
+ updateStatus('Disconnected. Retrying in 5s...');
98
96
  setTimeout(connectWebSocket, 5000);
99
97
  };
100
98
 
@@ -142,7 +140,7 @@
142
140
 
143
141
  function sendSetStartLine(end) {
144
142
  if (!socket || socket.readyState !== WebSocket.OPEN) {
145
- updateStatus('❌ WebSocket not connected.');
143
+ updateStatus('WebSocket not connected.');
146
144
  return;
147
145
  }
148
146
  let message = JSON.stringify({
@@ -154,12 +152,12 @@
154
152
  });
155
153
  console.log("sendSetStartLine: " + message);
156
154
  socket.send(message);
157
- updateStatus(`⏳ Sent request to set ${end}`);
155
+ updateStatus(`Sent request to set ${end}`);
158
156
  }
159
157
 
160
158
  function sendAdjustStartLine(end, delta, rotate) {
161
159
  if (!socket || socket.readyState !== WebSocket.OPEN) {
162
- updateStatus('❌ WebSocket not connected.');
160
+ updateStatus('WebSocket not connected.');
163
161
  return;
164
162
  }
165
163
  const message = JSON.stringify({
@@ -171,7 +169,7 @@
171
169
  });
172
170
  console.log("sendAdjustStartLine: " + message);
173
171
  socket.send(message);
174
- updateStatus(`⏳ Sent ${delta}m ${rotate}° request for ${end}`);
172
+ updateStatus(`Sent ${delta}m ${rotate}° request for ${end}`);
175
173
  }
176
174
 
177
175
  connectWebSocket();
@@ -184,7 +182,7 @@
184
182
 
185
183
  function adjustStartTime(deltaSeconds) {
186
184
  if (!socket || socket.readyState !== WebSocket.OPEN) {
187
- updateStatus('❌ WebSocket not connected.');
185
+ updateStatus('WebSocket not connected.');
188
186
  return;
189
187
  }
190
188
  socket.send(JSON.stringify({
@@ -197,35 +195,45 @@
197
195
  }
198
196
  }
199
197
  }));
200
- updateStatus(`⏳ Sent delta timeToStart: ${deltaSeconds} sec`);
198
+ updateStatus(`Sent delta timeToStart: ${deltaSeconds} sec`);
201
199
  }
202
200
 
203
201
  function setStartTime() {
204
202
  if (!socket || socket.readyState !== WebSocket.OPEN) {
205
- updateStatus('❌ WebSocket not connected.');
203
+ updateStatus('WebSocket not connected.');
206
204
  return;
207
205
  }
208
206
 
209
- const date = startTime ? new Date(startTime) : new Date();
210
- const [hours, minutes, seconds] = document.getElementById('startTime').value.split(':').map(Number);
211
- date.setHours(hours, minutes, seconds, 0);
207
+ try {
208
+ const now = new Date();
209
+ const parts = document.getElementById('startTime').value.split(':').map(Number);
210
+ const hours = parts[0];
211
+ const minutes = parts[1];
212
+ const seconds = parts.length >= 3 ? parts[2] : 0;
213
+ const date = new Date(now);
214
+ date.setHours(hours, minutes, seconds, 0);
215
+ if (date <= now)
216
+ date.setDate(date.getDate() + 1);
212
217
 
213
- socket.send(JSON.stringify({
214
- context: 'vessels.self',
215
- put: {
216
- path: 'navigation.racing.setStartTime',
217
- value: {
218
- command: 'set',
219
- startTime : date.toISOString(),
218
+ socket.send(JSON.stringify({
219
+ context: 'vessels.self',
220
+ put: {
221
+ path: 'navigation.racing.setStartTime',
222
+ value: {
223
+ command: 'set',
224
+ startTime: date.toISOString(),
225
+ }
220
226
  }
221
- }
222
- }));
223
- updateStatus(`⏳ Sent start time: ${startTime}`);
227
+ }));
228
+ updateStatus(`Sent start time: ${startTime}`);
229
+ } catch (err) {
230
+ console.log('Could not set start at ', err);
231
+ }
224
232
  }
225
233
 
226
234
  function sendTimerCommand(command) {
227
235
  if (!socket || socket.readyState !== WebSocket.OPEN) {
228
- updateStatus('❌ WebSocket not connected.');
236
+ updateStatus('WebSocket not connected.');
229
237
  return;
230
238
  }
231
239
  socket.send(JSON.stringify({
@@ -235,16 +243,8 @@
235
243
  value: { command }
236
244
  }
237
245
  }));
238
- updateStatus(`⏳ Sent timer command: ${command}`);
246
+ updateStatus(`Sent timer command: ${command}`);
239
247
  }
240
-
241
- setInterval(() => {
242
- if (timeToStart !== null && timeToStart > 0) {
243
- timeToStart--;
244
- updateTimerDisplay();
245
- }
246
- }, 1000);
247
-
248
248
  </script>
249
249
 
250
250
  <div style="display: flex; justify-content: center; align-items: center; gap: 3em; flex-wrap: wrap;">
package/racer-kip.png ADDED
Binary file
Binary file