diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue
index f432045..20f574e 100644
--- a/meshchatx/src/frontend/components/map/MapPage.vue
+++ b/meshchatx/src/frontend/components/map/MapPage.vue
@@ -140,6 +140,102 @@
+
+
+
+
+
+
+
+
+
+ {{
+ selectedMarker.peer?.display_name ||
+ selectedMarker.telemetry.destination_hash.substring(0, 8)
+ }}
+
+
+ {{ selectedMarker.telemetry.destination_hash }}
+
+
+
+
+
+
+
+
+
+ Latitude
+
+
+ {{ selectedMarker.telemetry.telemetry.location.latitude.toFixed(6) }}
+
+
+
+
+ Longitude
+
+
+ {{ selectedMarker.telemetry.telemetry.location.longitude.toFixed(6) }}
+
+
+
+
+ Altitude
+
+
{{ selectedMarker.telemetry.telemetry.location.altitude.toFixed(1) }}m
+
+
+
Speed
+
{{ selectedMarker.telemetry.telemetry.location.speed.toFixed(1) }}km/h
+
+
+
+
+
Signal
+
+ RSSI: {{ selectedMarker.telemetry.physical_link.rssi }}
+ SNR: {{ selectedMarker.telemetry.physical_link.snr }}
+ Q: {{ selectedMarker.telemetry.physical_link.q }}%
+
+
+
+
+
+ Updated: {{ formatTimestamp(selectedMarker.telemetry.timestamp) }}
+
+
+
+
+
+
{
@@ -643,12 +770,19 @@ export default {
// Check screen size for mobile
this.checkScreenSize();
window.addEventListener("resize", this.checkScreenSize);
+
+ // Update info every few seconds
+ this.reloadInterval = setInterval(() => {
+ this.fetchTelemetryMarkers();
+ }, 30000);
},
beforeUnmount() {
+ if (this.reloadInterval) clearInterval(this.reloadInterval);
if (this.exportInterval) clearInterval(this.exportInterval);
if (this.searchTimeout) clearTimeout(this.searchTimeout);
document.removeEventListener("click", this.handleClickOutside);
window.removeEventListener("resize", this.checkScreenSize);
+ WebSocketConnection.off("message", this.onWebsocketMessage);
},
methods: {
async getConfig() {
@@ -733,6 +867,23 @@ export default {
}),
});
+ // setup telemetry markers
+ this.markerSource = new VectorSource();
+ this.markerLayer = new VectorLayer({
+ source: this.markerSource,
+ zIndex: 100,
+ });
+ this.map.addLayer(this.markerLayer);
+
+ this.map.on("click", (evt) => {
+ const feature = this.map.forEachFeatureAtPixel(evt.pixel, (f) => f);
+ if (feature && feature.get("telemetry")) {
+ this.onMarkerClick(feature);
+ } else {
+ this.selectedMarker = null;
+ }
+ });
+
this.currentCenter = [defaultLon, defaultLat];
this.currentZoom = defaultZoom;
@@ -1307,6 +1458,108 @@ export default {
checkScreenSize() {
this.isMobileScreen = window.innerWidth < 640;
},
+ async fetchPeers() {
+ try {
+ const response = await window.axios.get("/api/v1/lxmf/conversations");
+ const peers = {};
+ for (const conv of response.data.conversations) {
+ peers[conv.destination_hash] = conv;
+ }
+ this.peers = peers;
+ } catch (e) {
+ console.error("Failed to fetch peers", e);
+ }
+ },
+ async fetchTelemetryMarkers() {
+ try {
+ const response = await window.axios.get("/api/v1/telemetry/peers");
+ this.telemetryList = response.data.telemetry;
+ this.updateMarkers();
+ } catch (e) {
+ console.error("Failed to fetch telemetry", e);
+ }
+ },
+ updateMarkers() {
+ if (!this.markerSource) return;
+ this.markerSource.clear();
+
+ for (const t of this.telemetryList) {
+ const loc = t.telemetry?.location;
+ if (!loc || loc.latitude === undefined || loc.longitude === undefined) continue;
+
+ const peer = this.peers[t.destination_hash];
+ const displayName = peer?.display_name || t.destination_hash.substring(0, 8);
+
+ const feature = new Feature({
+ geometry: new Point(fromLonLat([loc.longitude, loc.latitude])),
+ telemetry: t,
+ peer: peer,
+ });
+
+ // Default style
+ let iconColor = "#3b82f6";
+ let bgColor = "#ffffff";
+
+ if (peer?.lxmf_user_icon) {
+ iconColor = peer.lxmf_user_icon.foreground_colour || iconColor;
+ bgColor = peer.lxmf_user_icon.background_colour || bgColor;
+ }
+
+ feature.setStyle(
+ new Style({
+ image: new CircleStyle({
+ radius: 8,
+ fill: new Fill({ color: bgColor }),
+ stroke: new Stroke({ color: iconColor, width: 2 }),
+ }),
+ text: new Text({
+ text: displayName,
+ offsetY: -15,
+ font: "bold 11px sans-serif",
+ fill: new Fill({ color: "#000" }),
+ stroke: new Stroke({ color: "#fff", width: 2 }),
+ }),
+ })
+ );
+
+ this.markerSource.addFeature(feature);
+ }
+ },
+ onMarkerClick(feature) {
+ this.selectedMarker = {
+ telemetry: feature.get("telemetry"),
+ peer: feature.get("peer"),
+ };
+ },
+ async onWebsocketMessage(message) {
+ const json = JSON.parse(message.data);
+ if (json.type === "lxmf.telemetry") {
+ // Find and update or add to telemetryList
+ const index = this.telemetryList.findIndex((t) => t.destination_hash === json.destination_hash);
+ const entry = {
+ destination_hash: json.destination_hash,
+ timestamp: json.timestamp,
+ telemetry: json.telemetry,
+ updated_at: new Date().toISOString(),
+ };
+
+ if (index !== -1) {
+ this.telemetryList.splice(index, 1, entry);
+ } else {
+ this.telemetryList.push(entry);
+ }
+ this.updateMarkers();
+ }
+ },
+ formatTimestamp(ts) {
+ return new Date(ts * 1000).toLocaleString();
+ },
+ openChat(hash) {
+ this.$router.push({
+ name: "messages",
+ params: { destinationHash: hash },
+ });
+ },
},
};