- Added index.html for the main entry point of the client application. - Created package.json for client dependencies and scripts. - Implemented App component with view controls and layout. - Added CSS styles for the application and components. - Developed ControlPanel component for turtle management and commands. - Created Map3D component for 3D visualization of turtles. - Established Zustand store for state management of turtles and WebSocket connection. - Set up server with Express and WebSocket for handling turtle updates and commands. - Added REST API endpoints for turtle status updates and command handling. - Implemented start.sh script for setting up and running the application.
192 lines
5.1 KiB
JavaScript
192 lines
5.1 KiB
JavaScript
import express from 'express';
|
|
import { WebSocketServer } from 'ws';
|
|
import cors from 'cors';
|
|
import { createServer } from 'http';
|
|
|
|
const app = express();
|
|
const PORT = 3001;
|
|
const WS_PORT = 3002;
|
|
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Store connected web clients and turtle data
|
|
const webClients = new Set();
|
|
const turtleData = new Map(); // turtleID -> turtle state
|
|
|
|
// Create HTTP server
|
|
const server = createServer(app);
|
|
|
|
// WebSocket server for web clients
|
|
const wss = new WebSocketServer({ port: WS_PORT });
|
|
|
|
console.log(`🚀 Turtle Control Server starting...`);
|
|
console.log(`📡 HTTP Server: http://localhost:${PORT}`);
|
|
console.log(`🔌 WebSocket Server: ws://localhost:${WS_PORT}`);
|
|
|
|
// WebSocket connection handler
|
|
wss.on('connection', (ws) => {
|
|
console.log('🌐 New web client connected');
|
|
webClients.add(ws);
|
|
|
|
// Send current turtle data to new client
|
|
ws.send(JSON.stringify({
|
|
type: 'initial_state',
|
|
turtles: Array.from(turtleData.values())
|
|
}));
|
|
|
|
ws.on('message', (message) => {
|
|
try {
|
|
const data = JSON.parse(message);
|
|
console.log('📨 Received from web client:', data);
|
|
|
|
// Handle commands from web client
|
|
if (data.type === 'command') {
|
|
// Store command for turtle to poll
|
|
const turtleID = data.turtleID;
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
turtle.pendingCommands = turtle.pendingCommands || [];
|
|
turtle.pendingCommands.push({
|
|
command: data.command,
|
|
param: data.param,
|
|
timestamp: Date.now()
|
|
});
|
|
console.log(`📤 Command queued for turtle ${turtleID}:`, data.command);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error processing web client message:', error);
|
|
}
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
console.log('👋 Web client disconnected');
|
|
webClients.delete(ws);
|
|
});
|
|
|
|
ws.on('error', (error) => {
|
|
console.error('❌ WebSocket error:', error);
|
|
});
|
|
});
|
|
|
|
// Broadcast to all web clients
|
|
function broadcastToClients(data) {
|
|
const message = JSON.stringify(data);
|
|
webClients.forEach((client) => {
|
|
if (client.readyState === 1) { // OPEN
|
|
client.send(message);
|
|
}
|
|
});
|
|
}
|
|
|
|
// REST API endpoint for turtles to update their status
|
|
app.post('/api/turtle/update', (req, res) => {
|
|
try {
|
|
const turtleUpdate = req.body;
|
|
const turtleID = turtleUpdate.turtleID;
|
|
|
|
console.log(`🐢 Status update from Turtle ${turtleID}`);
|
|
|
|
// Update turtle data
|
|
if (!turtleData.has(turtleID)) {
|
|
console.log(`✨ New turtle registered: ${turtleID}`);
|
|
}
|
|
|
|
turtleData.set(turtleID, {
|
|
...turtleUpdate,
|
|
lastUpdate: Date.now()
|
|
});
|
|
|
|
// Broadcast to web clients
|
|
broadcastToClients({
|
|
type: 'turtle_update',
|
|
turtle: turtleData.get(turtleID)
|
|
});
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('❌ Error processing turtle update:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Endpoint for turtles to poll for commands
|
|
app.get('/api/turtle/:id/commands', (req, res) => {
|
|
try {
|
|
const turtleID = parseInt(req.params.id);
|
|
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
const commands = turtle.pendingCommands || [];
|
|
|
|
// Clear pending commands
|
|
turtle.pendingCommands = [];
|
|
|
|
res.json({ commands });
|
|
} else {
|
|
res.json({ commands: [] });
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error getting commands:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get all turtles
|
|
app.get('/api/turtles', (req, res) => {
|
|
res.json({
|
|
turtles: Array.from(turtleData.values())
|
|
});
|
|
});
|
|
|
|
// Send command to turtle
|
|
app.post('/api/turtle/:id/command', (req, res) => {
|
|
try {
|
|
const turtleID = parseInt(req.params.id);
|
|
const { command, param } = req.body;
|
|
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
turtle.pendingCommands = turtle.pendingCommands || [];
|
|
turtle.pendingCommands.push({
|
|
command,
|
|
param,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
console.log(`📤 Command queued for turtle ${turtleID}:`, command);
|
|
res.json({ success: true });
|
|
} else {
|
|
res.status(404).json({ error: 'Turtle not found' });
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error sending command:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Cleanup stale turtles (haven't updated in 30 seconds)
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
const timeout = 30000; // 30 seconds
|
|
|
|
for (const [turtleID, turtle] of turtleData.entries()) {
|
|
if (now - turtle.lastUpdate > timeout) {
|
|
console.log(`🗑️ Removing stale turtle: ${turtleID}`);
|
|
turtleData.delete(turtleID);
|
|
|
|
broadcastToClients({
|
|
type: 'turtle_disconnected',
|
|
turtleID
|
|
});
|
|
}
|
|
}
|
|
}, 10000); // Check every 10 seconds
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`✅ Server ready!`);
|
|
console.log(`\nConfigured turtles to send updates to:`);
|
|
console.log(` http://localhost:${PORT}/api/turtle/update`);
|
|
});
|