From 268005daf8de802aa345754e8d3e25e6f441f262 Mon Sep 17 00:00:00 2001 From: Umar Khan <1khanumar@gmail.com> Date: Fri, 13 Feb 2026 02:10:45 -0500 Subject: [PATCH 1/2] working app component with mock data --- frontend/.env.example | 4 + frontend/src/App.tsx | 288 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 frontend/.env.example diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..20d3844 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,4 @@ +# WebSocket Configuration +# URL for the backend WebSocket endpoint +# Default: ws://localhost:8000/ws +VITE_WS_URL=ws://localhost:8000/ws diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5fd374b..0289883 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,290 @@ +import { useEffect } from 'react'; +import { useWebSocket } from './hooks/useWebSocket'; +import { JointName, JOINT_NAMES } from './types/telemetry'; + function App() { + // Get WebSocket URL from environment variable or use default + const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws'; + const { telemetry, isConnected, error, connectionState } = useWebSocket(wsUrl); + + // Log telemetry data to console for debugging + useEffect(() => { + if (telemetry) { + console.log('Telemetry Update:', telemetry); + } + }, [telemetry]); + + // Format uptime as HH:MM:SS + const formatUptime = (seconds: number): string => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }; + + // Get color class based on connection state + const getConnectionColor = () => { + switch (connectionState) { + case 'connected': + return 'text-green-400'; + case 'connecting': + return 'text-yellow-400'; + case 'error': + return 'text-red-400'; + case 'disconnected': + return 'text-red-400'; + default: + return 'text-gray-400'; + } + }; + + // Get color class based on system health status + const getHealthColor = (status: string) => { + switch (status) { + case 'healthy': + return 'text-green-400'; + case 'degraded': + return 'text-yellow-400'; + case 'critical': + return 'text-red-400'; + default: + return 'text-gray-400'; + } + }; + + // Get color class based on motor status + const getMotorStatusColor = (status: string) => { + switch (status) { + case 'ok': + return 'text-green-400'; + case 'warning': + return 'text-yellow-400'; + case 'error': + return 'text-red-400'; + case 'offline': + return 'text-gray-400'; + default: + return 'text-gray-400'; + } + }; + + // Get battery level color based on percentage + const getBatteryColor = (percentage: number) => { + if (percentage > 50) return 'text-green-400'; + if (percentage > 20) return 'text-yellow-400'; + return 'text-red-400'; + }; + return (
-

Exoskeleton Dashboard

-

- Real-time telemetry dashboard - WebSocket connection coming soon. -

+ {/* Header */} +
+

Exoskeleton Telemetry Dashboard

+ + {/* Connection Status */} +
+ Status: + + {connectionState.toUpperCase()} + {connectionState === 'connecting' && ( + ā— + )} + +
+ + {/* Error Message */} + {error && ( +
+ Error: {error} +
+ )} +
+ + {/* Telemetry Data Display */} + {!isConnected && !telemetry && ( +
+ {connectionState === 'connecting' && ( +
+
⟳
+

Connecting to WebSocket server...

+
+ )} + {connectionState === 'disconnected' && ( +
+
āœ•
+

Disconnected from server

+
+ )} + {connectionState === 'error' && ( +
+
⚠
+

Connection error - please check backend server

+
+ )} +
+ )} + + {telemetry && ( +
+ {/* System Status Section */} +
+

System Status

+ +
+
+ Health: + + {telemetry.system.health_status.toUpperCase()} + +
+ +
+ Emergency Stop: + + {telemetry.system.emergency_stop ? '🚨 ACTIVE' : 'Inactive'} + +
+ +
+ Uptime: + {formatUptime(telemetry.system.uptime_seconds)} +
+ +
+ Sequence: + #{telemetry.sequence} +
+ + {telemetry.system.error_messages.length > 0 && ( +
+
Active Errors:
+
    + {telemetry.system.error_messages.map((msg, idx) => ( +
  • {msg}
  • + ))} +
+
+ )} +
+
+ + {/* Power Section */} +
+

Power System

+ +
+
+ Battery: + + {telemetry.power.battery_percentage.toFixed(1)}% + +
+ + {/* Battery Visual Indicator */} +
+
50 + ? 'bg-green-500' + : telemetry.power.battery_percentage > 20 + ? 'bg-yellow-500' + : 'bg-red-500' + }`} + style={{ width: `${telemetry.power.battery_percentage}%` }} + /> +
+ +
+ Voltage: + {telemetry.power.battery_voltage.toFixed(2)} V +
+ +
+ Current Draw: + {telemetry.power.current_draw.toFixed(2)} A +
+
+
+ + {/* Joints Section */} +
+

Joint Telemetry

+ +
+ {JOINT_NAMES.map((jointName: JointName) => { + const joint = telemetry.joints[jointName]; + return ( +
+

+ {jointName.replace('_', ' ')} +

+
+
+ Position: + {joint.position.toFixed(3)} rad +
+
+ Velocity: + {joint.velocity.toFixed(3)} rad/s +
+
+ Torque: + {joint.torque.toFixed(2)} Nm +
+
+
+ ); + })} +
+
+ + {/* Motors Section */} +
+

Motor Status

+ +
+ {JOINT_NAMES.map((jointName: JointName) => { + const motor = telemetry.motors[jointName]; + return ( +
+

+ {jointName.replace('_', ' ')} +

+
+
+ Status: + + {motor.status.toUpperCase()} + +
+
+ Temperature: + 60 ? 'text-red-400' : motor.temperature > 50 ? 'text-yellow-400' : ''}`}> + {motor.temperature.toFixed(1)} °C + +
+
+ Current: + {motor.current.toFixed(2)} A +
+
+
+ ); + })} +
+
+
+ )} + + {/* Footer Info */} +
+

Connected to: {wsUrl}

+ {telemetry && ( +

+ Last update: {new Date(telemetry.timestamp).toLocaleTimeString()} +

+ )} +
); } From 5bebd5ac1eeefb38de1728cd2f4d5c93ceff013b Mon Sep 17 00:00:00 2001 From: Umar Khan <1khanumar@gmail.com> Date: Wed, 25 Feb 2026 16:52:04 -0500 Subject: [PATCH 2/2] feat: Add real-time power data integration with INA228 sensors - Add power_serial_bridge.py for MCU serial communication - Update backend to receive and use real power data via POST /power/update - Modify data_collector to use real power when available, fallback to mock --- backend/app/data_collector.py | 30 +++++++- backend/app/main.py | 23 ++++++ scripts/power_serial_bridge.py | 123 +++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100755 scripts/power_serial_bridge.py diff --git a/backend/app/data_collector.py b/backend/app/data_collector.py index 00e9bd9..7550c5c 100644 --- a/backend/app/data_collector.py +++ b/backend/app/data_collector.py @@ -359,9 +359,35 @@ def _generate_sensors(self, joints: JointsData) -> SensorsData: def _generate_power(self, current_time: float, motors: MotorsData) -> PowerData: """Generate power system data.""" - # Battery depletes over time (~0.01%/sec), wraps to 100 at 20% + # Try to get real power data from MCU + from app.main import real_power_data + + if real_power_data and real_power_data.get('healthy'): + # Use REAL data from Power MCU + battery_voltage = real_power_data['voltage'] + current_draw = real_power_data['current'] + + # Calculate battery percentage from voltage (simple linear approximation) + # TODO: Adjust V_MAX and V_MIN based on actual battery + V_MAX = 26.0 # Adjust for your battery + V_MIN = 20.0 # Adjust for your battery + + if battery_voltage >= V_MAX: + battery_percentage = 100.0 + elif battery_voltage <= V_MIN: + battery_percentage = 0.0 + else: + battery_percentage = ((battery_voltage - V_MIN) / (V_MAX - V_MIN)) * 100.0 + + return PowerData( + battery_percentage=battery_percentage, + battery_voltage=battery_voltage, + current_draw=current_draw, + ) + + # Fallback to MOCK data if no real data available elapsed = current_time - self._start_time - depletion = elapsed * 0.01 + depletion = elapsed * 0.01 self._battery_percentage = 100.0 - depletion if self._battery_percentage <= 20.0: diff --git a/backend/app/main.py b/backend/app/main.py index 0086903..d6c85b8 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,6 +2,7 @@ from fastapi import FastAPI, WebSocket from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel from app.websocket import websocket_endpoint @@ -19,6 +20,20 @@ version="0.1.0", ) +# Global variable to store real power data from MCU +real_power_data = None + +# Pydantic model for power data +class PowerUpdate(BaseModel): + voltage: float + current: float + power: float + healthy: int + voltage2: float + current2: float + power2: float + healthy2: int + # Configure CORS for frontend development # Using allow_origins=["*"] ensures WebSocket upgrades are not blocked by # missing or mismatched Origin headers (e.g. wscat, Postman, etc.). @@ -41,6 +56,14 @@ async def health_check(): return {"status": "healthy", "service": "exoskeleton-telemetry"} +@app.post("/power/update") +async def update_power(data: PowerUpdate): + """Receive real-time power data from MCU via serial bridge""" + global real_power_data + real_power_data = data.dict() + return {"status": "ok"} + + @app.websocket("/ws") async def ws(websocket: WebSocket): """WebSocket endpoint for streaming telemetry data.""" diff --git a/scripts/power_serial_bridge.py b/scripts/power_serial_bridge.py new file mode 100755 index 0000000..33c01f3 --- /dev/null +++ b/scripts/power_serial_bridge.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Serial-to-Backend Bridge for Power MCU Data +Reads power telemetry from STM32 via UART and updates the backend. +""" + +import serial +import json +import time +import sys +import requests + +class PowerSerialBridge: + def __init__(self, serial_port: str, backend_url: str = "http://localhost:8000", baudrate: int = 115200): + self.serial_port = serial_port + self.baudrate = baudrate + self.backend_url = backend_url + self.ser = None + + def connect_serial(self): + """Connect to serial port""" + try: + self.ser = serial.Serial( + port=self.serial_port, + baudrate=self.baudrate, + timeout=1.0 + ) + print(f"āœ“ Connected to {self.serial_port} at {self.baudrate} baud") + time.sleep(2) # Wait for MCU to stabilize + return True + except serial.SerialException as e: + print(f"āœ— Failed to connect to {self.serial_port}: {e}") + return False + + def read_and_parse(self): + """Read one line from serial and parse JSON""" + if not self.ser or not self.ser.is_open: + return None + + try: + line = self.ser.readline().decode('utf-8').strip() + if line: + data = json.loads(line) + return data + except json.JSONDecodeError: + # Silently ignore JSON errors (common on startup) + return None + except UnicodeDecodeError: + # Silently ignore unicode errors (common on startup) + return None + except Exception as e: + print(f"āœ— Read error: {e}") + return None + + def update_backend(self, power_data): + """Send power data to backend via HTTP POST""" + try: + response = requests.post( + f"{self.backend_url}/power/update", + json=power_data, + timeout=0.5 + ) + return response.status_code == 200 + except requests.RequestException: + # Backend not available - continue silently + return False + + def run(self): + """Main loop - read serial and update backend""" + if not self.connect_serial(): + return + + print(f"Reading power data and forwarding to {self.backend_url}") + print("Press Ctrl+C to stop\n") + + update_count = 0 + error_count = 0 + + try: + while True: + power_data = self.read_and_parse() + if power_data: + update_count += 1 + + # Display formatted output + print(f"[{update_count}] Sensor1: {power_data['voltage']:.2f}V {power_data['current']:.3f}A {power_data['power']:.2f}W [H:{power_data['healthy']}] | " + f"Sensor2: {power_data['voltage2']:.2f}V {power_data['current2']:.3f}A {power_data['power2']:.2f}W [H:{power_data['healthy2']}]", end='') + + # Update backend + if self.update_backend(power_data): + print(" → Backend āœ“") + else: + error_count += 1 + if error_count % 10 == 1: # Only print occasionally to avoid spam + print(" → Backend āœ— (not running?)") + else: + print() + + time.sleep(0.01) + + except KeyboardInterrupt: + print(f"\n\nāœ“ Stopped by user") + print(f"Total updates: {update_count}") + finally: + if self.ser: + self.ser.close() + print("āœ“ Serial port closed") + +def main(): + # Parse command line arguments + port = sys.argv[1] if len(sys.argv) > 1 else '/dev/ttyUSB0' + backend = sys.argv[2] if len(sys.argv) > 2 else 'http://localhost:8000' + + print(f"Power MCU Serial Bridge") + print(f"Port: {port}") + print(f"Backend: {backend}") + print(f"Baud: 115200\n") + + bridge = PowerSerialBridge(serial_port=port, backend_url=backend, baudrate=115200) + bridge.run() + +if __name__ == '__main__': + main()