Compare commits

...

2 Commits

View File

@ -55,12 +55,6 @@ body {
}
.logo { color: var(--orange); font-weight: bold; letter-spacing: .15em; font-size: 13px; white-space: nowrap; }
#conn-bar { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 240px; }
#conn-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--dim); flex-shrink: 0; transition: background .3s;
}
#conn-dot.connected { background: var(--green); }
#conn-dot.error { background: var(--red); animation: blink 1s infinite; }
#ros-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--dim); flex-shrink: 0; transition: background .3s;
@ -232,8 +226,7 @@ body {
<div id="header">
<div class="logo">⚡ SUL-TEE TRACKER</div>
<div id="conn-bar">
<div id="conn-dot" title="Phone WS"></div>
<div id="ros-dot" title="Robot ROS"></div>
<div id="ros-dot" title="ROS Bridge"></div>
<input id="ws-input" type="text" value="ws://100.64.0.2:9090" placeholder="ws://host:port" />
<button class="hbtn" id="btn-connect">CONNECT</button>
<span id="conn-label">Not connected</span>
@ -371,10 +364,6 @@ body {
const MAX_TRAIL = 2000;
const MAX_LOG = 60;
const STALE_MS = 5000;
const RECONNECT_BASE = 2000;
const RECONNECT_MAX = 30000;
const RECONNECT_MUL = 1.5;
// ── State ─────────────────────────────────────────────────────────────────────
const phone = {
@ -393,13 +382,7 @@ let robotTrail = [];
let logEntries = [];
let followMode = true;
// WS (phone)
let ws = null;
let wsDelay = RECONNECT_BASE;
let wsTimer = null;
let wsTick = null;
// ROS (robot)
// ROS (robot + phone via rosbridge)
let ros = null;
// ── DOM ───────────────────────────────────────────────────────────────────────
@ -638,93 +621,36 @@ function addLogEntry(d, ts) {
).join('');
}
// ── WebSocket (phone GPS) ─────────────────────────────────────────────────────
function cancelWsReconn() {
if (wsTimer) { clearTimeout(wsTimer); wsTimer = null; }
if (wsTick) { clearInterval(wsTick); wsTick = null; }
}
function scheduleWsReconn(url) {
cancelWsReconn();
let secs = Math.round(wsDelay / 1000);
$('conn-label').textContent = `Retry in ${secs}s…`;
$('conn-label').style.color = '#6b7280';
wsTick = setInterval(() => { secs = Math.max(0, secs-1); if (secs>0) $('conn-label').textContent=`Retry in ${secs}s…`; }, 1000);
wsTimer = setTimeout(() => { wsTimer = null; doConnect(url, true); }, wsDelay);
wsDelay = Math.min(wsDelay * RECONNECT_MUL, RECONNECT_MAX);
}
function doConnect(url, isRetry) {
cancelWsReconn();
if (!isRetry) wsDelay = RECONNECT_BASE;
if (ws) { try { ws.close(); } catch(_) {} ws = null; }
$('conn-label').textContent = 'Connecting…';
$('conn-label').style.color = '#6b7280';
$('conn-dot').className = '';
try { ws = new WebSocket(url); }
catch(e) { $('conn-label').textContent = 'Bad URL'; $('conn-dot').className = 'error'; return; }
ws.onopen = () => {
wsDelay = RECONNECT_BASE; cancelWsReconn();
$('conn-dot').className = 'connected';
$('conn-label').textContent = 'WS connected';
$('conn-label').style.color = '#22c55e';
localStorage.setItem('sultee_ws_url', url);
};
ws.onclose = () => { $('conn-dot').className = ''; ws = null; scheduleWsReconn(url); };
ws.onerror = () => { $('conn-dot').className = 'error'; $('conn-label').style.color='#ef4444'; $('conn-label').textContent='WS error'; };
ws.onmessage = evt => {
let msg;
try { msg = JSON.parse(evt.data); } catch(_) { return; }
if (msg.type !== 'gps' || !msg.data) return;
const d = msg.data;
const ts = msg.timestamp || Math.floor(Date.now() / 1000);
phone.lat = d.latitude ?? phone.lat;
phone.lon = d.longitude ?? phone.lon;
phone.speed = d.speed ?? phone.speed;
phone.altitude = d.altitude ?? phone.altitude;
phone.course = d.course ?? phone.course;
phone.accuracy = d.horizontalAccuracy ?? phone.accuracy;
phone.fixes++;
phone.ts = Date.now();
if (phone.lat != null && phone.lon != null) {
updatePhoneMap(phone.lat, phone.lon, phone.course);
addLogEntry(d, ts);
}
$('status-last').textContent = 'Phone: ' + new Date(ts * 1000).toLocaleTimeString('en-US', { hour12: false });
$('last-msg').textContent = 'Phone: ' + new Date(ts * 1000).toLocaleTimeString('en-US', { hour12: false });
render();
};
}
// ── ROSLIB (robot GPS) ────────────────────────────────────────────────────────
// ── ROSLIB (phone + robot GPS via rosbridge) ──────────────────────────────────
function connectRos(url) {
if (ros) { try { ros.close(); } catch(_) {} }
$('conn-label').textContent = 'Connecting…';
$('conn-label').style.color = '#6b7280';
ros = new ROSLIB.Ros({ url });
ros.on('connection', () => {
$('ros-dot').className = 'connected';
setupRobotTopics();
$('conn-label').textContent = 'ROS connected';
$('conn-label').style.color = '#22c55e';
localStorage.setItem('sultee_ws_url', url);
setupRosTopics();
});
ros.on('error', () => {
$('ros-dot').className = 'error';
$('conn-label').style.color = '#ef4444';
$('conn-label').textContent = 'ROS error';
});
ros.on('error', () => { $('ros-dot').className = 'error'; });
ros.on('close', () => {
$('ros-dot').className = '';
$('conn-label').textContent = 'Disconnected';
$('conn-label').style.color = '#6b7280';
setTimeout(() => connectRos(url), 5000);
});
}
function setupRobotTopics() {
// /saltybot/phone/gps — sensor_msgs/NavSatFix published by the Pixel 5
// MQTT-to-ROS2 bridge (saltybot_phone package). No velocity topic.
function setupRosTopics() {
// /saltybot/phone/gps — sensor_msgs/NavSatFix from the Pixel 5 MQTT bridge
new ROSLIB.Topic({
ros, name: '/saltybot/phone/gps',
messageType: 'sensor_msgs/NavSatFix', throttle_rate: 500,
@ -742,6 +668,24 @@ function setupRobotTopics() {
$('last-msg').textContent = 'Robot: ' + new Date().toLocaleTimeString('en-US', { hour12: false });
render();
});
// /saltybot/ios/gps — sensor_msgs/NavSatFix from the iOS phone MQTT bridge
new ROSLIB.Topic({
ros, name: '/saltybot/ios/gps',
messageType: 'sensor_msgs/NavSatFix', throttle_rate: 500,
}).subscribe(msg => {
phone.lat = msg.latitude ?? phone.lat;
phone.lon = msg.longitude ?? phone.lon;
phone.altitude = msg.altitude ?? phone.altitude;
phone.fixes++;
phone.ts = Date.now();
if (phone.lat != null && phone.lon != null) {
updatePhoneMap(phone.lat, phone.lon, phone.course);
}
$('last-msg').textContent = 'Phone: ' + new Date().toLocaleTimeString('en-US', { hour12: false });
render();
});
}
// ── Stale refresh ─────────────────────────────────────────────────────────────
@ -751,12 +695,10 @@ setInterval(render, 1000);
// ── Controls ──────────────────────────────────────────────────────────────────
$('btn-connect').addEventListener('click', () => {
const url = $('ws-input').value.trim();
doConnect(url, false);
connectRos(url);
connectRos($('ws-input').value.trim());
});
$('ws-input').addEventListener('keydown', e => {
if (e.key === 'Enter') { const url = $('ws-input').value.trim(); doConnect(url, false); connectRos(url); }
if (e.key === 'Enter') connectRos($('ws-input').value.trim());
});
$('btn-follow').addEventListener('click', () => {
@ -793,7 +735,6 @@ map.on('dragstart', () => {
const saved = localStorage.getItem('sultee_ws_url') || 'ws://100.64.0.2:9090';
$('ws-input').value = saved;
drawCompass(null);
doConnect(saved, false);
connectRos(saved);
})();
</script>