feat: Enhance turtle instance management with improved state handling and event forwarding
This commit is contained in:
103
server/server.js
103
server/server.js
@@ -3,13 +3,14 @@ import { WebSocketServer } from 'ws';
|
||||
import cors from 'cors';
|
||||
import { createServer } from 'http';
|
||||
import * as db from './database.js';
|
||||
import { Turtle } from './Turtle.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = 3001;
|
||||
const WS_PORT = 3002;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: '5mb' }));
|
||||
|
||||
// Initialize database
|
||||
db.initializeDatabase();
|
||||
@@ -23,11 +24,23 @@ console.log(` Loaded ${savedBlocks.length} world blocks`);
|
||||
|
||||
// Store connected web clients and turtle data
|
||||
const webClients = new Set();
|
||||
const turtleData = new Map(); // turtleID -> turtle state
|
||||
const turtles = new Map(); // turtleID -> Turtle instance
|
||||
const worldBlocks = new Map(); // "x,y,z" -> {name, metadata, discoveredBy, timestamp}
|
||||
const turtleHomes = new Map(); // turtleID -> {x, y, z} home position
|
||||
const turtleConfig = new Map(); // turtleID -> {maxDistance, facing, etc}
|
||||
|
||||
// Legacy compat: turtleData getter (returns serialized Turtle data)
|
||||
const turtleData = {
|
||||
has(id) { return turtles.has(id); },
|
||||
get(id) { const t = turtles.get(id); return t ? t.toJSON() : undefined; },
|
||||
set(id, _val) { /* no-op, use getOrCreateTurtle */ },
|
||||
delete(id) { return turtles.delete(id); },
|
||||
get size() { return turtles.size; },
|
||||
entries() { return Array.from(turtles.entries()).map(([id, t]) => [id, t.toJSON()])[Symbol.iterator](); },
|
||||
values() { return Array.from(turtles.values()).map(t => t.toJSON())[Symbol.iterator](); },
|
||||
keys() { return turtles.keys(); },
|
||||
};
|
||||
|
||||
// Load saved homes into memory
|
||||
for (const home of savedHomes) {
|
||||
turtleHomes.set(home.turtle_id, { x: home.x, y: home.y, z: home.z });
|
||||
@@ -57,22 +70,91 @@ function broadcastToClients(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Turtle Instance Management ==========
|
||||
|
||||
/**
|
||||
* Get or create a Turtle instance for the given ID.
|
||||
* Wires up event handlers for the state machine and command forwarding.
|
||||
*/
|
||||
function getOrCreateTurtle(turtleID) {
|
||||
if (turtles.has(turtleID)) {
|
||||
return turtles.get(turtleID);
|
||||
}
|
||||
|
||||
console.log(`✨ Creating Turtle instance for #${turtleID}`);
|
||||
const turtle = new Turtle(turtleID, {
|
||||
worldBlocks,
|
||||
broadcastToClients,
|
||||
storeBlock,
|
||||
db,
|
||||
});
|
||||
|
||||
// Restore home position from DB
|
||||
const savedHome = turtleHomes.get(turtleID);
|
||||
if (savedHome) {
|
||||
turtle.homePosition = savedHome;
|
||||
}
|
||||
|
||||
// ---- Event handlers ----
|
||||
|
||||
// Forward eval commands to webbridge via pending command queue
|
||||
turtle.on('sendCommand', (command) => {
|
||||
// Queue eval command for webbridge to poll
|
||||
turtle.pendingLegacyCommands.push({
|
||||
...command,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
// Store discovered blocks
|
||||
turtle.on('blocksDiscovered', (blocks) => {
|
||||
for (const block of blocks) {
|
||||
storeBlock(block.x, block.y, block.z, { name: block.name, metadata: block.metadata }, block.discoveredBy);
|
||||
}
|
||||
// Broadcast blocks to web clients
|
||||
broadcastToClients({
|
||||
type: 'blocks_discovered',
|
||||
blocks: blocks.map(b => ({
|
||||
x: b.x, y: b.y, z: b.z,
|
||||
name: b.name,
|
||||
metadata: b.metadata,
|
||||
discoveredBy: b.discoveredBy,
|
||||
timestamp: Date.now(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
// Broadcast turtle updates to web clients
|
||||
turtle.on('update', (data) => {
|
||||
broadcastToClients({
|
||||
type: 'turtle_update',
|
||||
turtle: data,
|
||||
});
|
||||
});
|
||||
|
||||
// Handle turtle disconnect
|
||||
turtle.on('disconnect', (id) => {
|
||||
broadcastToClients({
|
||||
type: 'turtle_removed',
|
||||
turtleID: id,
|
||||
});
|
||||
});
|
||||
|
||||
turtles.set(turtleID, turtle);
|
||||
return turtle;
|
||||
}
|
||||
|
||||
// Cleanup stale turtles periodically
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
let removedCount = 0;
|
||||
|
||||
for (const [turtleID, turtle] of turtleData.entries()) {
|
||||
for (const [turtleID, turtle] of turtles.entries()) {
|
||||
if (now - turtle.lastUpdate > TURTLE_TIMEOUT) {
|
||||
console.log(`🔌 Turtle ${turtleID} timed out (last seen ${Math.floor((now - turtle.lastUpdate) / 1000)}s ago)`);
|
||||
turtleData.delete(turtleID);
|
||||
turtle.disconnect();
|
||||
turtles.delete(turtleID);
|
||||
removedCount++;
|
||||
|
||||
// Notify web clients
|
||||
broadcastToClients({
|
||||
type: 'turtle_removed',
|
||||
turtleID: turtleID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +187,6 @@ function getBlockPosition(turtlePos, facing, direction) {
|
||||
} else if (direction === 'down') {
|
||||
pos.y -= 1;
|
||||
} else if (direction === 'forward') {
|
||||
// Calculate based on facing direction
|
||||
if (facing === 0) pos.z -= 1; // North
|
||||
else if (facing === 1) pos.x += 1; // East
|
||||
else if (facing === 2) pos.z += 1; // South
|
||||
|
||||
Reference in New Issue
Block a user