From 585e7d1912f4f08bc1f7306018be5a4c63fca01a Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 27 Apr 2026 22:42:57 +0000 Subject: [PATCH] added rover state info tab with ros bridge integration --- .../__pycache__/ros_bridge.cpython-310.pyc | Bin 3978 -> 8821 bytes .../app/GUI functions/Navigation.js | 112 +++----- .../app/GUI functions/RoverStateDashboard.js | 237 +++++++++++++++++ .../app/GUI functions/pageConstants.js | 2 +- umdloop_gui_web/app/RoverStateDashboard.js | 234 +++++++++++++++++ umdloop_gui_web/ros_bridge.py | 245 +++++++++++++----- umdloop_gui_web/server.py | 5 + 7 files changed, 694 insertions(+), 141 deletions(-) create mode 100644 umdloop_gui_web/app/GUI functions/RoverStateDashboard.js create mode 100644 umdloop_gui_web/app/RoverStateDashboard.js diff --git a/umdloop_gui_web/__pycache__/ros_bridge.cpython-310.pyc b/umdloop_gui_web/__pycache__/ros_bridge.cpython-310.pyc index 38fc5145d1ce5cae9c46a5eba6fa46c429eaa65d..ac467a6674c1a8007f08067f85f34aed6138db4d 100644 GIT binary patch literal 8821 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 literal 3978 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 N+F(s<{(kMP{{w>};%Wc@ diff --git a/umdloop_gui_web/app/GUI functions/Navigation.js b/umdloop_gui_web/app/GUI functions/Navigation.js index 7aac68e..1cacbfe 100644 --- a/umdloop_gui_web/app/GUI functions/Navigation.js +++ b/umdloop_gui_web/app/GUI functions/Navigation.js @@ -1,6 +1,7 @@ "use client"; import React, { useEffect, useState } from "react"; +import RoverStateDashboard from "./RoverStateDashboard"; export default function Navigation({ selectedNavItem }) { const [running, setRunning] = useState(false); @@ -79,7 +80,6 @@ export default function Navigation({ selectedNavItem }) { useEffect(() => { if (selectedNavItem !== "Object Detection") return undefined; - fetchStatus(); const id = setInterval(fetchStatus, 1000); return () => clearInterval(id); @@ -92,51 +92,27 @@ export default function Navigation({ selectedNavItem }) { return (
-

{selectedNavItem} - Navigation Mode

- + {/* ── Object Detection ── */} {selectedNavItem === "Object Detection" && (
-
+
{running ? "RUNNING ✅" : "STOPPED ❌"}
- -
- PID: {pid ?? "—"} -
- +
PID: {pid ?? "—"}
{error &&
{error}
}
- - - + + +
@@ -146,51 +122,34 @@ export default function Navigation({ selectedNavItem }) {
)} + {/* ── Control Panel ── */} {selectedNavItem === "Control Panel" && (
-
+

Control Panel

- setLatitude(e.target.value)} placeholder="e.g. 38.4239116" style={{ width: "100%", padding: "10px 12px", borderRadius: "10px", border: "2px solid #1f1e1eff", background: "#3d3d3d", color: "white", outline: "none" }} /> + setLatitude(e.target.value)} placeholder="e.g. 38.4239116" + style={{ width: "100%", padding: "10px 12px", borderRadius: "10px", border: "2px solid #1f1e1eff", background: "#3d3d3d", color: "white", outline: "none" }} />
-
- setLongitude(e.target.value)} placeholder="e.g. -110.7849055" style={{ width: "100%", padding: "10px 12px", borderRadius: "10px", border: "2px solid #1f1e1eff", background: "#3d3d3d", color: "white", outline: "none" }} /> + setLongitude(e.target.value)} placeholder="e.g. -110.7849055" + style={{ width: "100%", padding: "10px 12px", borderRadius: "10px", border: "2px solid #1f1e1eff", background: "#3d3d3d", color: "white", outline: "none" }} />
Mode
- {["GNSS", "Object Detection", "Aruco Tag"].map((opt) => ( -
- - {pathPlanStatus ?
{pathPlanStatus}
: null} - {error ?
{error}
: null} + {pathPlanStatus &&
{pathPlanStatus}
} + {error &&
{error}
}
)} + + {/* ── Rover State */} + {selectedNavItem === "Rover State" && }
); } diff --git a/umdloop_gui_web/app/GUI functions/RoverStateDashboard.js b/umdloop_gui_web/app/GUI functions/RoverStateDashboard.js new file mode 100644 index 0000000..57646fd --- /dev/null +++ b/umdloop_gui_web/app/GUI functions/RoverStateDashboard.js @@ -0,0 +1,237 @@ +"use client"; + +import React, { useEffect, useState, useCallback } from "react"; +import { getApiBaseUrl } from "../config"; + +const fmt = (v, d = 3) => (v == null || isNaN(v) ? "—" : Number(v).toFixed(d)); +const fmtAge = (ts) => { + if (!ts) return "—"; + const s = (Date.now() - ts) / 1000; + if (s < 1.5) return "live"; + return `${s.toFixed(1)}s ago`; +}; +const isLive = (ts, maxMs = 2500) => ts && Date.now() - ts < maxMs; + +const gpsFixLabel = (status) => { + if (status == null) return "—"; + if (status < 0) return "No Fix"; + if (status === 0) return "GPS Fix"; + return "DGPS Fix"; +}; + +const diagColor = (level) => level === 0 ? "#4ade80" : level === 1 ? "#facc15" : "#f87171"; +const diagLabel = (level) => level === 0 ? "OK" : level === 1 ? "WARN" : level === 2 ? "ERR" : "STALE"; + +function StatusDot({ live }) { + return ( + + ); +} + +function Card({ title, live, children }) { + return ( +
+
+ + + {title} + + +
+ {children} +
+ ); +} + +function Row({ label, value, unit = "" }) { + return ( +
+ {label} + + {value ?? "—"}{unit && {unit}} + +
+ ); +} + + +function VelBar({ label, value, max = 2 }) { + const pct = Math.min(100, (Math.abs(value ?? 0) / max) * 100); + const neg = (value ?? 0) < 0; + return ( +
+
+ {label} + + {fmt(value, 3)} m/s + +
+
+
+
+
+ ); +} + + +export default function RoverStateDashboard() { + const [state, setState] = useState(null); + const [apiStatus, setApiStatus] = useState("connecting"); + const [lastPoll, setLastPoll] = useState(null); + const [errorMsg, setErrorMsg] = useState(""); + + const poll = useCallback(async () => { + try { + const res = await fetch(`${getApiBaseUrl()}/rover/state`); + const data = await res.json(); + if (data.ok) { + setState(data); + setApiStatus("ok"); + setLastPoll(Date.now()); + setErrorMsg(""); + } else { + setApiStatus("error"); + setErrorMsg(data.error || "Unknown error"); + } + } catch (e) { + setApiStatus("error"); + setErrorMsg("Flask backend unreachable"); + } + }, []); + + useEffect(() => { + poll(); + const id = setInterval(poll, 1000); + return () => clearInterval(id); + }, [poll]); + + const ok = apiStatus === "ok"; + const gps = state?.gps; + const imu = state?.imu; + const imuf = state?.imu_filtered; + const odom = state?.odom; + const joints = state?.joints ?? []; + const diags = state?.diagnostics ?? []; + const headingDeg = state?.heading ?? odom?.yaw_deg ?? null; + + return ( +
+
+
+

ROVER STATE

+
+ + + {ok ? "FLASK API CONNECTED" : errorMsg || "CONNECTING…"} + +
+
+ polled {fmtAge(lastPoll)} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0}> + {joints.length > 0 ? ( + joints.slice(0, 6).map((j) => ( + + )) + ) : ( + Waiting for /joint_states… + )} + + + 0}> + {diags.length === 0 ? ( + Waiting for /diagnostics… + ) : ( + <> + {diags.slice(0, 10).map((d, i) => ( +
+
+ {d.name} + {diagLabel(d.level)} +
+ {d.message && {d.message}} +
+ ))} + {diags.length > 10 && +{diags.length - 10} more} + + )} +
+ +
+
+ ); +} diff --git a/umdloop_gui_web/app/GUI functions/pageConstants.js b/umdloop_gui_web/app/GUI functions/pageConstants.js index 2f8a9ef..5709387 100644 --- a/umdloop_gui_web/app/GUI functions/pageConstants.js +++ b/umdloop_gui_web/app/GUI functions/pageConstants.js @@ -3,4 +3,4 @@ export const MODES = ["Operator", "Technician", "Drone", "Navigation", "Map"]; export const MODE_ICONS = ["camera.png", "sensor.png", "camera.png", "navigation.png", "map.png"]; export const SUBSYSTEMS = ["Drive", "Arm", "Science"]; -export const NAVIGATION_BUTTONS = ["Object Detection", "Control Panel", "Placeholder2"]; +export const NAVIGATION_BUTTONS = ["Object Detection", "Control Panel", "Rover State"]; diff --git a/umdloop_gui_web/app/RoverStateDashboard.js b/umdloop_gui_web/app/RoverStateDashboard.js new file mode 100644 index 0000000..e446b62 --- /dev/null +++ b/umdloop_gui_web/app/RoverStateDashboard.js @@ -0,0 +1,234 @@ +"use client"; + +import React, { useEffect, useState, useCallback } from "react"; +import { getApiBaseUrl } from "../config"; + +const fmt = (v, d = 3) => (v == null || isNaN(v) ? "—" : Number(v).toFixed(d)); +const fmtAge = (ts) => { + if (!ts) return "—"; + const s = (Date.now() - ts) / 1000; + if (s < 1.5) return "live"; + return `${s.toFixed(1)}s ago`; +}; +const isLive = (ts, maxMs = 2500) => ts && Date.now() - ts < maxMs; + +const gpsFixLabel = (status) => { + if (status == null) return "—"; + if (status < 0) return "No Fix"; + if (status === 0) return "GPS Fix"; + return "DGPS Fix"; +}; + +const diagColor = (level) => level === 0 ? "#4ade80" : level === 1 ? "#facc15" : "#f87171"; +const diagLabel = (level) => level === 0 ? "OK" : level === 1 ? "WARN" : level === 2 ? "ERR" : "STALE"; + +function StatusDot({ live }) { + return ( + + ); +} + +function Card({ title, live, children }) { + return ( +
+
+ + + {title} + + +
+ {children} +
+ ); +} + +function Row({ label, value, unit = "" }) { + return ( +
+ {label} + + {value ?? "—"}{unit && {unit}} + +
+ ); +} + + +function VelBar({ label, value, max = 2 }) { + const pct = Math.min(100, (Math.abs(value ?? 0) / max) * 100); + const neg = (value ?? 0) < 0; + return ( +
+
+ {label} + + {fmt(value, 3)} m/s + +
+
+
+
+
+ ); +} + + +export default function RoverStateDashboard() { + const [state, setState] = useState(null); + const [apiStatus, setApiStatus] = useState("connecting"); + const [lastPoll, setLastPoll] = useState(null); + const [errorMsg, setErrorMsg] = useState(""); + + const poll = useCallback(async () => { + try { + const res = await fetch(`${getApiBaseUrl()}/rover/state`); + const data = await res.json(); + if (data.ok) { + setState(data); + setApiStatus("ok"); + setLastPoll(Date.now()); + setErrorMsg(""); + } else { + setApiStatus("error"); + setErrorMsg(data.error || "Unknown error"); + } + } catch (e) { + setApiStatus("error"); + setErrorMsg("Flask backend unreachable"); + } + }, []); + + useEffect(() => { + poll(); + const id = setInterval(poll, 1000); + return () => clearInterval(id); + }, [poll]); + + const ok = apiStatus === "ok"; + const gps = state?.gps; + const imu = state?.imu; + const imuf = state?.imu_filtered; + const odom = state?.odom; + const joints = state?.joints ?? []; + const diags = state?.diagnostics ?? []; + const headingDeg = state?.heading ?? odom?.yaw_deg ?? null; + + return ( +
+
+
+

ROVER STATE

+
+ + + {ok ? "FLASK API CONNECTED" : errorMsg || "CONNECTING…"} + +
+
+ polled {fmtAge(lastPoll)} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0}> + {joints.length > 0 ? ( + joints.slice(0, 6).map((j) => ( + + )) + ) : ( + Waiting for /joint_states… + )} + + + 0}> + {diags.length === 0 ? ( + Waiting for /diagnostics… + ) : ( + <> + {diags.slice(0, 10).map((d, i) => ( +
+ {d.name} + {diagLabel(d.level)} +
+ ))} + {diags.length > 10 && +{diags.length - 10} more} + + )} +
+ +
+
+ ); +} diff --git a/umdloop_gui_web/ros_bridge.py b/umdloop_gui_web/ros_bridge.py index a938af8..777a1bf 100644 --- a/umdloop_gui_web/ros_bridge.py +++ b/umdloop_gui_web/ros_bridge.py @@ -5,58 +5,202 @@ from rclpy.node import Node from rclpy.action import ActionClient -from rclpy.qos import QoSProfile, DurabilityPolicy, ReliabilityPolicy +from rclpy.qos import QoSProfile, DurabilityPolicy, ReliabilityPolicy, HistoryPolicy, LivelinessPolicy -from std_msgs.msg import String +from std_msgs.msg import String, Float64 from msgs.action import NavigateToGPS -from sensor_msgs.msg import NavSatFix +from sensor_msgs.msg import NavSatFix, Imu, JointState +from nav_msgs.msg import Odometry +from diagnostic_msgs.msg import DiagnosticArray + +import math + + +def quat_to_rpy(q): + x, y, z, w = q.x, q.y, q.z, q.w + roll = math.degrees(math.atan2(2*(w*x + y*z), 1 - 2*(x*x + y*y))) + pitch = math.degrees(math.asin(max(-1.0, min(1.0, 2*(w*y - z*x))))) + yaw = math.degrees(math.atan2(2*(w*z + x*y), 1 - 2*(y*y + z*z))) + return roll, pitch, yaw + + +def safe_str(v): + if v is None: + return None + if isinstance(v, bytes): + return v.decode("utf-8", errors="replace") + return str(v) + +def safe_float(v): + try: + f = float(v) + return None if (f != f or f == float("inf") or f == float("-inf")) else f + except Exception: + return None class RosGpsClient(Node): def __init__(self): super().__init__("umdloop_gui_ros_bridge") - # Latest rover GPS fix (None until first message arrives) - self.rover_position = None - - # Action client (GNSS navigation) - self._client = ActionClient( - self, - NavigateToGPS, - "/navigate_to_gps" - ) + # Action client (GNSS navigation) + self._client = ActionClient(self, NavigateToGPS, "/navigate_to_gps") # QoS MUST match bt_navigator subscriber - qos = QoSProfile(depth=1) - qos.durability = DurabilityPolicy.TRANSIENT_LOCAL - qos.reliability = ReliabilityPolicy.RELIABLE + qos_latched = QoSProfile(depth=1) + qos_latched.durability = DurabilityPolicy.TRANSIENT_LOCAL + qos_latched.reliability = ReliabilityPolicy.RELIABLE + # nav mode publisher - self._nav_mode_pub = self.create_publisher( - String, - "/nav_mode", - qos + self._nav_mode_pub = self.create_publisher(String, "/nav_mode", qos_latched) + + self._state_lock = threading.Lock() + self._state = { + "gps": None, + "imu": None, + "imu_filtered": None, + "odom": None, + "joints": None, + "nav_mode": None, + "heading": None, + "diagnostics": [], + } + + sensor_qos = QoSProfile( + depth=5, + reliability=ReliabilityPolicy.BEST_EFFORT, + durability=DurabilityPolicy.VOLATILE, ) - - # GPS fix subscriber — best-effort sensor QoS - gps_qos = QoSProfile(depth=1) - gps_qos.reliability = ReliabilityPolicy.BEST_EFFORT - self.create_subscription( - NavSatFix, - "/gps/fix", - self._gps_callback, - gps_qos, + + reliable_qos = QoSProfile( + depth=5, + reliability=ReliabilityPolicy.RELIABLE, + durability=DurabilityPolicy.VOLATILE, ) - self.get_logger().info("RosGpsClient initialized") + self._state["nav_mode"] = "GNSS" + threading.Timer(0.5, lambda: self.publish_nav_mode("GNSS")).start() + + self.create_subscription(NavSatFix, "/gps/fix", self.gps, sensor_qos) + self.create_subscription(Imu, "/imu", self.imu, sensor_qos) + self.create_subscription(Imu, "/imu/filtered", self.imu_filt, sensor_qos) + self.create_subscription(Odometry, "/localization/odom", self.odom, reliable_qos) + self.create_subscription(JointState, "/joint_states", self.joints, reliable_qos) + self.create_subscription(String, "/nav_mode", self.nav_mode, qos_latched) + self.create_subscription(Float64, "/heading", self.heading, reliable_qos) + self.create_subscription(DiagnosticArray, "/diagnostics", self.diag, reliable_qos) + + + self.get_logger().info("RosGpsClient initialized with rover state subscribers") + - def _gps_callback(self, msg: NavSatFix): - if msg.latitude is not None and msg.longitude is not None: - self.rover_position = { - "latitude": msg.latitude, + def gps(self, msg): + with self._state_lock: + self._state["gps"] = { + "latitude": msg.latitude, "longitude": msg.longitude, + "altitude": msg.altitude, + "status": msg.status.status, + } + + def imu(self, msg): + with self._state_lock: + roll, pitch, yaw = quat_to_rpy(msg.orientation) + self._state["imu"] = { + "roll_deg": roll, + "pitch_deg": pitch, + "yaw_deg": yaw, + "accel_x": msg.linear_acceleration.x, + "accel_y": msg.linear_acceleration.y, + "accel_z": msg.linear_acceleration.z, + "gyro_x": msg.angular_velocity.x, + "gyro_y": msg.angular_velocity.y, + "gyro_z": msg.angular_velocity.z, + } + + def imu_filt(self, msg): + with self._state_lock: + roll, pitch, yaw = quat_to_rpy(msg.orientation) + self._state["imu_filtered"] = { + "roll_deg": roll, + "pitch_deg": pitch, + "yaw_deg": yaw, + "accel_x": msg.linear_acceleration.x, + "accel_y": msg.linear_acceleration.y, + "accel_z": msg.linear_acceleration.z, } + def odom(self, msg): + with self._state_lock: + p = msg.pose.pose.position + roll, pitch, yaw = quat_to_rpy(msg.pose.pose.orientation) + t = msg.twist.twist + lin_speed = math.hypot(t.linear.x, t.linear.y) + self._state["odom"] = { + "x": safe_float(p.x), "y": safe_float(p.y), "z": safe_float(p.z), + "roll_deg": safe_float(roll), + "pitch_deg": safe_float(pitch), + "yaw_deg": safe_float(yaw), + "linear_x": safe_float(t.linear.x), + "linear_y": safe_float(t.linear.y), + "linear_speed": safe_float(lin_speed), + "angular_z": safe_float(t.angular.z), + } + + def joints(self, msg): + def safe(arr, i): + if i >= len(arr): + return None + v = arr[i] + return None if (v != v or v == float("inf") or v == float("-inf")) else v + + with self._state_lock: + self._state["joints"] = [ + { + "name": safe_str(name), + "position": safe(msg.position, i), + "velocity": safe(msg.velocity, i), + "effort": safe(msg.effort, i), + } + for i, name in enumerate(msg.name) + ] + + def nav_mode(self, msg): + with self._state_lock: + self._state["nav_mode"] = safe_str(msg.data) + + def heading(self, msg): + with self._state_lock: + self._state["heading"] = safe_float(msg.data) + + def diag(self, msg): + def parse_level(v): + if isinstance(v, bytes): + return v[0] + if isinstance(v, str): + return ord(v) + return int(v) + + + with self._state_lock: + self._state["diagnostics"] = [ + { + "name": safe_str(s.name), + "level": parse_level(s.level), + "message": safe_str(s.message), + } + for s in msg.status + ] + + # -------------------------------------------------- + # Publish API + # -------------------------------------------------- + def get_rover_state(self): + with self._state_lock: + import copy + return copy.deepcopy(self._state) + # -------------------------------------------------- # Publish navigation mode # -------------------------------------------------- @@ -64,16 +208,13 @@ def publish_nav_mode(self, mode: str): msg = String() msg.data = mode self._nav_mode_pub.publish(msg) - self.get_logger().info(f"Published /nav_mode = '{mode}'") # -------------------------------------------------- # Send GNSS goal (blocking) # -------------------------------------------------- def send_gps_goal_blocking(self, lat, lon, tol=0.0, timeout_sec=60.0): - self.get_logger().info( - f"Sending GPS goal: lat={lat}, lon={lon}, tol={tol}" - ) + self.get_logger().info(f"Sending GPS goal: lat={lat}, lon={lon}, tol={tol}") if not self._client.wait_for_server(timeout_sec=2.0): return False, False, "navigate_to_gps action server not available" @@ -85,33 +226,23 @@ def send_gps_goal_blocking(self, lat, lon, tol=0.0, timeout_sec=60.0): # send goal async send_future = self._client.send_goal_async(goal) - rclpy.spin_until_future_complete( - self, - send_future, - timeout_sec=5.0 - ) + rclpy.spin_until_future_complete(self, send_future, timeout_sec=5.0) if not send_future.done(): return False, False, "Failed to send goal" goal_handle = send_future.result() - if not goal_handle.accepted: return False, False, "Goal rejected" # wait for result result_future = goal_handle.get_result_async() - rclpy.spin_until_future_complete( - self, - result_future, - timeout_sec=timeout_sec - ) + rclpy.spin_until_future_complete(self, result_future, timeout_sec=timeout_sec) if not result_future.done(): return True, False, "Timed out waiting for result" result = result_future.result().result - return True, bool(result.success), result.message @@ -127,15 +258,10 @@ def __init__(self): self._lock = threading.Lock() def start(self): - """ - Safe to call multiple times. - Prevents double rclpy.init() crashes. - """ with self._lock: - + if self.started and self.node is not None: return - # init ROS only if needed try: if not rclpy.ok(): @@ -147,13 +273,13 @@ def start(self): # create node once if self.node is None: self.node = RosGpsClient() - + # start spin thread once if self.thread is None or not self.thread.is_alive(): self.thread = threading.Thread( target=rclpy.spin, args=(self.node,), - daemon=True + daemon=True, ) self.thread.start() @@ -162,16 +288,15 @@ def start(self): def shutdown(self): with self._lock: - if self.node is not None: self.node.destroy_node() self.node = None - if rclpy.ok(): + if rclpy is not None and rclpy.ok(): rclpy.shutdown() self.started = False # global singleton -ros_context = RosContext() \ No newline at end of file +ros_context = RosContext() diff --git a/umdloop_gui_web/server.py b/umdloop_gui_web/server.py index e2fb016..0c7b057 100644 --- a/umdloop_gui_web/server.py +++ b/umdloop_gui_web/server.py @@ -304,6 +304,11 @@ def radio_status(): status = get_mikrotik_radio_status() return jsonify({"ok": True, **status}), 200 +@app.get("/rover/state") +def rover_state(): + ros_context.start() + state = ros_context.node.get_rover_state() + return jsonify({"ok": True, **state}), 200 @app.get("/object-detection/status") def status():