signalk-racer
Version:
Signalk plugin to calculate values of interest to sail racers: such as Distance to Line; Next leg TWA.
309 lines (275 loc) • 11.6 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SignalK Racer Control</title>
<style>
.button-timer-command { background-color: #041b8e; color: white; }
.button-timer-adjust { background-color: #2196F3; color: white; }
button {
padding: 1em 1.5em;
font-size: 1em;
cursor: pointer;
border: none;
border-radius: 4px;
}
body {
font-family: sans-serif;
max-width: 100%;
margin: 3em auto;
text-align: center;
}
button {
padding: 1em 1em;
font-size: 1em;
cursor: pointer;
}
input[readonly] {
background: #eee;
border: 1px solid #ccc;
padding: 0.3em;
}
#status {
margin-top: 2em;
font-size: 1em;
color: green;
}
.port-set { background-color: #f88; color: black; }
.port-move { background-color: #a00; color: white; }
.port-tweak { background-color: #f44; color: white; }
.port-rotate { background-color: #a00; color: white; }
.stb-set { background-color: #8f8; color: black; }
.stb-move { background-color: #080; color: white; }
.stb-tweak { background-color: #4c4; color: white; }
.stb-rotate { background-color: #080; color: white; }
</style>
</head>
<body>
<script>
let socket;
let statusTimer;
let startTime;
function updateStatus(message) {
const status = document.getElementById('status');
status.textContent = message;
if (statusTimer) clearTimeout(statusTimer);
statusTimer = setTimeout(() => {
if (status.textContent === message) {
status.textContent = '-';
}
}, 10000);
}
function toRadians(degrees) {
return degrees ? degrees * (Math.PI / 180) : null;
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const socketUrl = protocol + '//' + window.location.host + '/signalk/v1/stream?subscribe=none';
socket = new WebSocket(socketUrl);
socket.onopen = () => {
updateStatus('✅ Connected to Signal K server');
socket.send(JSON.stringify({
context: "vessels.self",
subscribe: [
{
path: "navigation.racing.*",
policy: "instant"
}
]
}));
};
socket.onerror = (err) => updateStatus('❌ WebSocket error: ' + err.message);
socket.onclose = () => {
updateStatus('❌ Disconnected. Retrying in 5s...');
setTimeout(connectWebSocket, 5000);
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.updates) {
data.updates.forEach(update => {
if (update.values) {
update.values.forEach(value => {
if (value.path === "navigation.racing.startLineLength") {
document.getElementById("lineLength").value = value.value.toFixed(1);
} else if (value.path === "navigation.racing.stbLineBias") {
document.getElementById("lineBias").value = value.value.toFixed(1);
} else if (value.path === "navigation.racing.timeToStart") {
document.getElementById('timeToStart').textContent = formatTimeToMMSS(value.value);
} else if (value.path === "navigation.racing.startTime") {
if (!value.value) {
document.getElementById('startTime').value = "HH:MM:SS";
} else {
startTime = new Date(value.value); // handles ISO 8601 format
document.getElementById('startTime').value =
startTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
} else {
console.log("Received value: " + value.path + " = " + JSON.stringify(value.value));
}
});
}
});
}
};
}
function sendSetStartLine(end) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
updateStatus('❌ WebSocket not connected.');
return;
}
let message = JSON.stringify({
context: 'vessels.self',
put: {
path: 'navigation.racing.setStartLine',
value: { end, position : 'bow'}
}
});
console.log("sendSetStartLine: " + message);
socket.send(message);
updateStatus(`⏳ Sent request to set ${end}`);
}
function sendAdjustStartLine(end, delta, rotate) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
updateStatus('❌ WebSocket not connected.');
return;
}
const message = JSON.stringify({
context: 'vessels.self',
put: {
path: 'navigation.racing.setStartLine',
value: { end, delta, rotate : rotate ? toRadians(rotate) : null }
}
});
console.log("sendAdjustStartLine: " + message);
socket.send(message);
updateStatus(`⏳ Sent ${delta}m ${rotate}° request for ${end}`);
}
connectWebSocket();
function formatTimeToMMSS(seconds) {
const mm = Math.floor(seconds / 60);
const ss = Math.floor(seconds % 60);
return `${mm.toString().padStart(2, '0')}:${ss.toString().padStart(2, '0')}`;
}
function adjustStartTime(deltaSeconds) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
updateStatus('❌ WebSocket not connected.');
return;
}
socket.send(JSON.stringify({
context: 'vessels.self',
put: {
path: 'navigation.racing.setStartTime',
value: {
command: 'adjust',
delta: deltaSeconds
}
}
}));
updateStatus(`⏳ Sent delta timeToStart: ${deltaSeconds} sec`);
}
function setStartTime() {
if (!socket || socket.readyState !== WebSocket.OPEN) {
updateStatus('❌ WebSocket not connected.');
return;
}
const date = startTime ? new Date(startTime) : new Date();
const [hours, minutes, seconds] = document.getElementById('startTime').value.split(':').map(Number);
date.setHours(hours, minutes, seconds, 0);
socket.send(JSON.stringify({
context: 'vessels.self',
put: {
path: 'navigation.racing.setStartTime',
value: {
command: 'set',
startTime : date.toISOString(),
}
}
}));
updateStatus(`⏳ Sent start time: ${startTime}`);
}
function sendTimerCommand(command) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
updateStatus('❌ WebSocket not connected.');
return;
}
socket.send(JSON.stringify({
context: 'vessels.self',
put: {
path: 'navigation.racing.setStartTime',
value: { command }
}
}));
updateStatus(`⏳ Sent timer command: ${command}`);
}
setInterval(() => {
if (timeToStart !== null && timeToStart > 0) {
timeToStart--;
updateTimerDisplay();
}
}, 1000);
</script>
<h1>Adjust Start Line</h1>
<div style="display: flex; justify-content: center; align-items: center; gap: 3em; flex-wrap: wrap;">
<div style="display: grid; grid-template-columns: repeat(10, auto); grid-template-rows: repeat(3, auto); gap: 0.5em;">
<div></div>
<div></div>
<button class="port-rotate" onclick="sendAdjustStartLine('port', 0, 1)">+1°</button>
<div></div>
<div></div>
<div></div>
<div></div>
<button class="stb-rotate" onclick="sendAdjustStartLine('stb', 0, -1)">+1°</button>
<div></div>
<div></div>
<!--------->
<button class="port-move" onclick="sendAdjustStartLine('port', 10, 0)">+10m</button>
<button class="port-tweak" onclick="sendAdjustStartLine('port', 1, 0)">+1m</button>
<button class="port-set" onclick="sendSetStartLine('port')">Port<br/>(pin)</button>
<button class="port-tweak" onclick="sendAdjustStartLine('port', -1, 0)">-1m</button>
<button class="port-move" onclick="sendAdjustStartLine('port', -10, 0)">-10m</button>
<button class="stb-move" onclick="sendAdjustStartLine('stb', -10, 0)">-10m</button>
<button class="stb-tweak" onclick="sendAdjustStartLine('stb', -1, 0)">-1m</button>
<button class="stb-set" onclick="sendSetStartLine('stb')">Stbd<br/>(boat)</button>
<button class="stb-tweak" onclick="sendAdjustStartLine('stb', 1, 0)">+1m</button>
<button class="stb-move" onclick="sendAdjustStartLine('stb', +10, 0)">+10m</button>
<!--------->
<div></div>
<div></div>
<button class="port-rotate" onclick="sendAdjustStartLine('port', 0, -1)">-1°</button>
<div></div>
<div></div>
<div></div>
<div></div>
<button class="stb-rotate" onclick="sendAdjustStartLine('stb', 0, 1)">-1°</button>
<div></div>
<div></div>
</div>
</div>
<label>Length: <input id="lineLength" readonly style="width: 5em; text-align: center;">m</label>
<label>Bias: <input id="lineBias" readonly style="width: 5em; text-align: center;">m</label>
<!-- Timer Display -->
<div style="margin-top: 2em;">
<div id="timeToStart" style="font-size: 4em; font-weight: bold;">00:00</div>
</div>
<!-- Start Time Row -->
<div style="margin-top: 1em; display: flex; justify-content: center; align-items: center; gap: 1em;">
<button class="button-timer-adjust" onclick="adjustStartTime(-60)">-1 min</button>
<button class="button-timer-adjust" onclick="adjustStartTime(-1)">-1s</button>
<label for="startTime">Start at: <input id="startTime" style="font-size: 1.5em; width: 6em; text-align: center;"></label>
<button class="button-timer-adjust" onclick="adjustStartTime(1)">+1s</button>
<button class="button-timer-adjust" onclick="adjustStartTime(60)">+1 min</button>
</div>
<!-- Timer Control Buttons -->
<div style="margin-top: 1em;">
<button class="button-timer-command" onclick="sendTimerCommand('start')">Start</button>
<button class="button-timer-command" onclick="setStartTime()">Start At</button>
<button class="button-timer-command" onclick="sendTimerCommand('sync')">Sync</button>
<button class="button-timer-command" onclick="sendTimerCommand('reset')">Reset</button>
</div>
<div id="status">Connecting...</div>
</body>
</html>