UNPKG

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
<!DOCTYPE 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:&nbsp;<input id="lineLength" readonly style="width: 5em; text-align: center;">m</label> <label>Bias:&nbsp;<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>