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 cors from 'cors';
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import * as db from './database.js';
|
import * as db from './database.js';
|
||||||
|
import { Turtle } from './Turtle.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3001;
|
const PORT = 3001;
|
||||||
const WS_PORT = 3002;
|
const WS_PORT = 3002;
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json({ limit: '5mb' }));
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
db.initializeDatabase();
|
db.initializeDatabase();
|
||||||
@@ -23,11 +24,23 @@ console.log(` Loaded ${savedBlocks.length} world blocks`);
|
|||||||
|
|
||||||
// Store connected web clients and turtle data
|
// Store connected web clients and turtle data
|
||||||
const webClients = new Set();
|
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 worldBlocks = new Map(); // "x,y,z" -> {name, metadata, discoveredBy, timestamp}
|
||||||
const turtleHomes = new Map(); // turtleID -> {x, y, z} home position
|
const turtleHomes = new Map(); // turtleID -> {x, y, z} home position
|
||||||
const turtleConfig = new Map(); // turtleID -> {maxDistance, facing, etc}
|
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
|
// Load saved homes into memory
|
||||||
for (const home of savedHomes) {
|
for (const home of savedHomes) {
|
||||||
turtleHomes.set(home.turtle_id, { x: home.x, y: home.y, z: home.z });
|
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
|
// Cleanup stale turtles periodically
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
||||||
for (const [turtleID, turtle] of turtleData.entries()) {
|
for (const [turtleID, turtle] of turtles.entries()) {
|
||||||
if (now - turtle.lastUpdate > TURTLE_TIMEOUT) {
|
if (now - turtle.lastUpdate > TURTLE_TIMEOUT) {
|
||||||
console.log(`🔌 Turtle ${turtleID} timed out (last seen ${Math.floor((now - turtle.lastUpdate) / 1000)}s ago)`);
|
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++;
|
removedCount++;
|
||||||
|
|
||||||
// Notify web clients
|
|
||||||
broadcastToClients({
|
|
||||||
type: 'turtle_removed',
|
|
||||||
turtleID: turtleID
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +187,6 @@ function getBlockPosition(turtlePos, facing, direction) {
|
|||||||
} else if (direction === 'down') {
|
} else if (direction === 'down') {
|
||||||
pos.y -= 1;
|
pos.y -= 1;
|
||||||
} else if (direction === 'forward') {
|
} else if (direction === 'forward') {
|
||||||
// Calculate based on facing direction
|
|
||||||
if (facing === 0) pos.z -= 1; // North
|
if (facing === 0) pos.z -= 1; // North
|
||||||
else if (facing === 1) pos.x += 1; // East
|
else if (facing === 1) pos.x += 1; // East
|
||||||
else if (facing === 2) pos.z += 1; // South
|
else if (facing === 2) pos.z += 1; // South
|
||||||
|
|||||||
Reference in New Issue
Block a user