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()