From 6d047ca50c46101d79af17ebe9c88a600d8f854b Mon Sep 17 00:00:00 2001 From: sl-webui Date: Fri, 3 Apr 2026 22:34:44 -0400 Subject: [PATCH] feat(gps-map): add phone/user GPS as second marker on robot GPS map Subscribes to saltybot/phone/gps (JSON: {ts, lat, lon, alt_m, accuracy_m, speed_ms, bearing_deg, provider}) and renders a blue Leaflet marker + blue breadcrumb trail alongside the robot's orange/cyan marker. Status bar now shows PHONE badge with stale detection. Sidebar adds phone lat/lon/speed/accuracy/provider section. Clear button resets both trails. Co-Authored-By: Claude Sonnet 4.6 --- ui/gps_map_panel.html | 28 ++++++++++- ui/gps_map_panel.js | 109 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 124 insertions(+), 13 deletions(-) diff --git a/ui/gps_map_panel.html b/ui/gps_map_panel.html index d9b65af..36b5d43 100644 --- a/ui/gps_map_panel.html +++ b/ui/gps_map_panel.html @@ -24,11 +24,14 @@
- GPS FIX + ROBOT FIX STALE VELOCITY STALE + + PHONE + NO DATA Awaiting data…
@@ -94,6 +97,26 @@ + +
+
📱 Phone / User
+
+
LAT
+
LON
+
+
+
+
SPEED
+
+
+
+
ACCURACY
+
+
+
+
SRC
+
+
Trail log
@@ -105,7 +128,8 @@
FIXES0 TRAIL PTS0 - MSGS0 + ROBOT MSGS0 + PHONE MSGS0 No data
diff --git a/ui/gps_map_panel.js b/ui/gps_map_panel.js index 89eea4c..be42988 100644 --- a/ui/gps_map_panel.js +++ b/ui/gps_map_panel.js @@ -2,13 +2,15 @@ * gps_map_panel.js — Robot GPS Live Map (Issue #709 companion) * * Subscribes via rosbridge to: - * saltybot/gps/fix std_msgs/String JSON — {lat, lon, alt, stat, t} - * saltybot/gps/vel std_msgs/String JSON — {spd, hdg, t} + * saltybot/gps/fix std_msgs/String JSON — {lat, lon, alt, stat, t} + * saltybot/gps/vel std_msgs/String JSON — {spd, hdg, t} + * saltybot/phone/gps std_msgs/String JSON — {ts, lat, lon, alt_m, accuracy_m, speed_ms, bearing_deg, provider} * * Renders a Leaflet map with: - * - Live robot position marker with heading arrow - * - Orange breadcrumb trail polyline (up to MAX_TRAIL points) - * - Sidebar: speed, altitude, heading compass, fix status, coordinates + * - Live robot position marker (cyan/orange) with heading arrow + * - Live phone/user position marker (blue) with bearing arrow + * - Orange robot trail + blue phone trail (up to MAX_TRAIL points each) + * - Sidebar: speed, altitude, heading compass, fix status, coordinates, phone position * - FOLLOW mode (auto-pan) toggled by button or manual drag * - Stale detection at 5 s */ @@ -35,7 +37,15 @@ const gps = { tsfix: 0, tsvel: 0, }; +const phone = { + lat: null, lon: null, alt: null, + spd: null, bearing: null, + accuracy: null, provider: null, + msgs: 0, ts: 0, +}; + let trail = []; // [{lat, lon}] +let phoneTrail = []; // [{lat, lon}] let logEntries = []; let followMode = true; @@ -74,11 +84,35 @@ function makeIcon(hdg) { }); } +// Robot marker (cyan body, orange heading arrow) let marker = null; let polyline = L.polyline([], { color: '#f97316', weight: 2.5, opacity: .8, lineJoin: 'round', }).addTo(map); +// Phone/user marker (blue body, blue bearing arrow) +function makePhoneIcon(bearing) { + const rot = (bearing != null && bearing >= 0) ? bearing : 0; + return L.divIcon({ + className: '', + html: ` + + + + + + `, + iconSize: [24, 32], + iconAnchor: [12, 17], + }); +} + +let phoneMarker = null; +let phonePolyline = L.polyline([], { + color: '#3b82f6', weight: 2, opacity: .7, lineJoin: 'round', +}).addTo(map); + // ── Canvas compass ──────────────────────────────────────────────────────────── function drawCompass(hdg) { @@ -135,10 +169,13 @@ function render() { const now = Date.now(); // Stale badges - const fixStale = now - gps.tsfix > STALE_MS; - const velStale = now - gps.tsvel > STALE_MS; - setSysBadge('badge-fix', fixStale ? 'stale' : 'ok', fixStale ? 'STALE' : 'LIVE'); - setSysBadge('badge-vel', velStale ? 'stale' : 'ok', velStale ? 'STALE' : 'LIVE'); + const fixStale = now - gps.tsfix > STALE_MS; + const velStale = now - gps.tsvel > STALE_MS; + const phoneStale = now - phone.ts > STALE_MS; + setSysBadge('badge-fix', fixStale ? 'stale' : 'ok', fixStale ? 'STALE' : 'LIVE'); + setSysBadge('badge-vel', velStale ? 'stale' : 'ok', velStale ? 'STALE' : 'LIVE'); + setSysBadge('badge-phone', phone.ts === 0 ? 'stale' : phoneStale ? 'stale' : 'ok', + phone.ts === 0 ? 'NO DATA' : phoneStale ? 'STALE' : 'LIVE'); // Speed const spdKmh = gps.spd != null ? gps.spd : null; @@ -171,6 +208,15 @@ function render() { // Coordinates $('val-lat').textContent = gps.lat != null ? gps.lat.toFixed(6) : '—'; $('val-lon').textContent = gps.lon != null ? gps.lon.toFixed(6) : '—'; + + // Phone position + $('phone-lat').textContent = phone.lat != null ? phone.lat.toFixed(6) : '—'; + $('phone-lon').textContent = phone.lon != null ? phone.lon.toFixed(6) : '—'; + const phoneSpdMs = phone.spd; + $('phone-spd').textContent = phoneSpdMs != null ? (phoneSpdMs * 3.6).toFixed(1) + ' km/h' : '—'; + $('phone-acc').textContent = phone.accuracy != null ? phone.accuracy.toFixed(1) + ' m' : '—'; + $('phone-provider').textContent = phone.provider || '—'; + $('bb-phone').textContent = phone.msgs; } function updateMap(lat, lon, hdg) { @@ -190,6 +236,21 @@ function updateMap(lat, lon, hdg) { polyline.setLatLngs(trail.map(p => [p.lat, p.lon])); } +function updatePhoneMap(lat, lon, bearing) { + const icon = makePhoneIcon(bearing); + + if (!phoneMarker) { + phoneMarker = L.marker([lat, lon], { icon }).addTo(map); + } else { + phoneMarker.setIcon(icon); + phoneMarker.setLatLng([lat, lon]); + } + + phoneTrail.push({ lat, lon }); + if (phoneTrail.length > MAX_TRAIL) phoneTrail.shift(); + phonePolyline.setLatLngs(phoneTrail.map(p => [p.lat, p.lon])); +} + function addLog(lat, lon, spd, t) { const time = new Date((t || Date.now() / 1000) * 1000) .toLocaleTimeString('en-US', { hour12: false }); @@ -297,6 +358,30 @@ function setupTopics() { render(); } catch(_) {} }); + + // saltybot/phone/gps — {ts, lat, lon, alt_m, accuracy_m, speed_ms, bearing_deg, provider} + new ROSLIB.Topic({ + ros, name: 'saltybot/phone/gps', + messageType: 'std_msgs/String', throttle_rate: 500, + }).subscribe(msg => { + try { + const d = JSON.parse(msg.data); + phone.lat = d.lat ?? phone.lat; + phone.lon = d.lon ?? phone.lon; + phone.alt = d.alt_m ?? phone.alt; + phone.accuracy = d.accuracy_m ?? phone.accuracy; + phone.spd = d.speed_ms ?? phone.spd; + phone.bearing = d.bearing_deg ?? phone.bearing; + phone.provider = d.provider ?? phone.provider; + phone.msgs++; + phone.ts = Date.now(); + + if (phone.lat != null && phone.lon != null) { + updatePhoneMap(phone.lat, phone.lon, phone.bearing); + } + render(); + } catch(_) {} + }); } // ── Stale checker (1 Hz) ────────────────────────────────────────────────────── @@ -318,9 +403,11 @@ $('btn-follow').addEventListener('click', () => { }); $('btn-clear').addEventListener('click', () => { - trail = []; logEntries = []; gps.fixes = 0; - if (marker) { marker.remove(); marker = null; } + trail = []; phoneTrail = []; logEntries = []; gps.fixes = 0; + if (marker) { marker.remove(); marker = null; } + if (phoneMarker) { phoneMarker.remove(); phoneMarker = null; } polyline.setLatLngs([]); + phonePolyline.setLatLngs([]); $('trail-log').innerHTML = ''; render(); });