Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"leaflet": "^1.9.4",
"next": "15.1.6",
"next-runtime-env": "^3.3.0",
"ping": "^1.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.6.0",
Expand All @@ -26,6 +27,7 @@
"recharts": "^2.15.1",
"roslib": "^1.4.1",
"three": "^0.173.0",
"uuid": "^13.0.0",
"webrtc-adapter": "^9.0.1"
},
"devDependencies": {
Expand Down
68 changes: 59 additions & 9 deletions src/app/dashboard/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ const USERNAME = 'ubnt';
const PASSWORD = 'samitherover';
const baseStationIP = '192.168.0.2';

const hosts = ['192.168.0.2', '172.19.228.1']; // Add more hosts here as needed

const ping = require('ping');

// Ping configuration
const pingConfig = {
timeout: 1, // timeout set to 1 second
};

// Function to ping multiple hosts and return RTT times in dictionary format
async function pingHosts(hosts: string[]): Promise<{ [key: string]: number }> {
const results: { [key: string]: number } = {};

const pingPromises = hosts.map(async (host) => {
try {
const res = await ping.promise.probe(host, pingConfig);
if (res.alive) {
results[host] = parseFloat(res.time as string);
} else {
results[host] = -1;
}
} catch (error) {
results[host] = -1;
}
});

await Promise.all(pingPromises);
return results;
}

// Authenticates with the base station
async function authenticate() {
const response = await fetch(`http://${baseStationIP}/api/auth`, {
Expand Down Expand Up @@ -39,25 +69,45 @@ async function fetchStatus(cookie: string) {
}

export async function GET(request: Request) {
// Initialize response data with defaults
let uplinkCapacity = 0;
let downlinkCapacity = 0;
let uplinkThroughput = 0;
let downlinkThroughput = 0;
let baseStationError: string | null = null;

// Try to fetch base station data, but don't fail if it's unavailable
try {
const authStatus = await authenticate();


const status = await fetchStatus(authStatus.cookie);

// Extract only the needed fields for the frontend
const uplinkCapacity = status.wireless?.polling?.ucap ?? 0;
const downlinkCapacity = status.wireless?.polling?.dcap ?? 0;
const uplinkThroughput = status.wireless?.throughput?.tx ?? 0;
const downlinkThroughput = status.wireless?.throughput?.rx ?? 0;
uplinkCapacity = status.wireless?.polling?.ucap ?? 0;
downlinkCapacity = status.wireless?.polling?.dcap ?? 0;
uplinkThroughput = status.wireless?.throughput?.tx ?? 0;
downlinkThroughput = status.wireless?.throughput?.rx ?? 0;
} catch (error: any) {
baseStationError = error.message;
console.warn('Failed to fetch base station data:', error.message);
}

// Always ping hosts, regardless of base station status
try {
const pings = await pingHosts(hosts);

return Response.json({
uplinkCapacity,
downlinkCapacity,
uplinkThroughput,
downlinkThroughput,
pings,
baseStationError, // Include error info in response
});
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), { status: 500 });
console.error('Failed to ping hosts:', error);
return Response.json(
{ error: 'Failed to ping hosts: ' + error.message },
{ status: 500 }
);
}
}
}

21 changes: 19 additions & 2 deletions src/components/panels/MosaicDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import GoalSetterPanel from './GoalSetterPanel';
import GasSensor from './GasSensor';
import NetworkHealthTelemetryPanel from './NetworkHealthTelemetryPanel';
import VideoControls from './VideoControls';
import MotorStatusPanel from './MotorStatusPanel';
import { v4 as uuidv4 } from 'uuid';

type TileType =
| 'mapView'
Expand All @@ -26,7 +28,8 @@ type TileType =
| 'gasSensor'
| 'orientationDisplay'
| 'goalSetter'
| 'networkHealthMonitor';
| 'networkHealthMonitor'
| 'MotorStatusPanel';

type TileId = `${TileType}:${string}`;

Expand All @@ -39,6 +42,7 @@ const TILE_DISPLAY_NAMES: Record<TileType, string> = {
orientationDisplay: 'Rover Orientation',
goalSetter: 'Nav2',
networkHealthMonitor: 'Connection Health',
MotorStatusPanel: 'motor',
};

const ALL_TILE_TYPES: TileType[] = [
Expand All @@ -50,10 +54,11 @@ const ALL_TILE_TYPES: TileType[] = [
'waypointList',
'gasSensor',
'goalSetter',
'MotorStatusPanel',
];

function makeTileId(type: TileType): TileId {
const uid = crypto.randomUUID();
const uid = uuidv4();
return `${type}:${uid}`;
}

Expand Down Expand Up @@ -314,6 +319,18 @@ const MosaicDashboard: React.FC = () => {
<GoalSetterPanel />
</MosaicWindow>
);
case 'MotorStatusPanel':
return (
<MosaicWindow<TileId>
title={TILE_DISPLAY_NAMES[type]}
path={path}
additionalControls={
<Controls id={id} path={path} pendingAdd={pendingAdd} setPendingAdd={setPendingAdd} />
}
>
<MotorStatusPanel />
</MosaicWindow>
);

default:
return <div>Unknown tile</div>;
Expand Down
134 changes: 134 additions & 0 deletions src/components/panels/MotorStatusPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use client';

import React, { useEffect, useState } from 'react';
import ROSLIB from 'roslib';
import { useROS } from '@/ros/ROSContext';

type MotorStatus = {
velocity: number;
temperature: number;
output_current: number;
};

const MOTORS = {
fl: { label: 'FLeft', topic: '/frontLeft/status' },
fr: { label: 'FRight', topic: '/frontRight/status' },
rl: { label: 'BLeft', topic: '/backLeft/status' },
rr: { label: 'BRight', topic: '/backRight/status' },
} as const;

type MotorKey = keyof typeof MOTORS;

const MotorStatusPanel: React.FC = () => {
const { ros } = useROS();

const [motorStats, setMotorStats] = useState<
Record<MotorKey, MotorStatus | null>
>({
fl: null,
fr: null,
rl: null,
rr: null,
});

useEffect(() => {
if (!ros) return;

const unsubscribers = (
Object.entries(MOTORS) as [MotorKey, typeof MOTORS[MotorKey]][]
).map(([key, { topic }]) => {
const rosTopic = new ROSLIB.Topic({
ros,
name: topic,
messageType: 'ros_phoenix/msg/MotorStatus',
throttle_rate: 100,
});

const handler = (msg: any) => {
setMotorStats(prev => ({
...prev,
[key]: {
velocity: msg.velocity,
temperature: msg.temperature,
output_current: msg.output_current,
},
}));
};

rosTopic.subscribe(handler);
return () => rosTopic.unsubscribe(handler);
});

return () => unsubscribers.forEach(unsub => unsub());
}, [ros]);

return (
<div className="motor-panel">
<table className="motor-table">
<thead>
<tr>
<th>Motor</th>
<th>Vel.</th>
<th>Temp.</th>
<th>Curr.</th>
</tr>
</thead>
<tbody>
{(Object.entries(MOTORS) as [MotorKey, typeof MOTORS[MotorKey]][]).map(
([key, { label }]) => {
const stat = motorStats[key];

return (
<tr key={key}>
<td>{label}</td>
<td>{stat ? stat.velocity.toFixed(2) : '-'}</td>
<td>{stat ? `${stat.temperature.toFixed(2)}°C` : '-'}</td>
<td>{stat ? `${stat.output_current.toFixed(2)}A` : '-'}</td>
</tr>
);
}
)}
</tbody>
</table>

<style jsx>{`
.motor-panel {
background: #1e1e1e;
color: #f1f1f1;
height: 100%;
display: flex;
flex-direction: column;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: auto;
}

.motor-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}

.motor-table thead {
background: #2d2d2d;
border-bottom: 2px solid #444;
}

.motor-table th {
text-align: left;
font-weight: 600;

}

.motor-table td {
border-bottom: 1px solid #333;
}

.motor-table tbody tr:hover {
background-color: #262626;
}
`}</style>
</div>
);
};

export default MotorStatusPanel;
Loading