feat: iOS phone GPS via rosbridge topic /saltybot/ios/gps (Issue #681) #722
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user