refactor: enhance WebSocket handling for bridge connections and command forwarding

This commit is contained in:
MayaTheShy
2026-02-20 04:14:03 -05:00
parent b8cd239597
commit e84ca4cfb9

View File

@@ -7,7 +7,6 @@ import { Turtle } from './Turtle.js';
const app = express();
const PORT = 3001;
const WS_PORT = 3002;
app.use(cors());
app.use(express.json({ limit: '5mb' }));
@@ -99,13 +98,9 @@ function getOrCreateTurtle(turtleID) {
// ---- Event handlers ----
// Forward eval commands to webbridge via pending command queue
// Forward eval commands to webbridge via WebSocket (or fallback to poll queue)
turtle.on('sendCommand', (command) => {
// Queue eval command for webbridge to poll
turtle.pendingCommands.push({
...command,
timestamp: Date.now(),
});
pushCommandToBridge(turtleID, command);
});
// Store discovered blocks
@@ -201,15 +196,162 @@ function getBlockPosition(turtlePos, facing, direction) {
// Create HTTP server
const server = createServer(app);
// WebSocket server for web clients
const wss = new WebSocketServer({ port: WS_PORT });
// WebSocket server for web clients AND bridge connections
const wss = new WebSocketServer({ server });
// Track bridge connections separately
const bridgeClients = new Set();
console.log(`🚀 Turtle Control Server starting...`);
console.log(`📡 HTTP Server: http://localhost:${PORT}`);
console.log(`🔌 WebSocket Server: ws://localhost:${WS_PORT}`);
console.log(`🔌 WebSocket Server: ws://localhost:${PORT}/ws`);
/**
* Push a command to the webbridge for a specific turtle.
* If a bridge is connected via WebSocket, send instantly.
* Otherwise, queue for HTTP polling (fallback).
*/
function pushCommandToBridge(turtleID, command) {
let sent = false;
for (const bridge of bridgeClients) {
if (bridge.readyState === 1) { // OPEN
bridge.send(JSON.stringify({
type: 'command',
turtleID,
command,
}));
sent = true;
}
}
if (!sent) {
// Fallback: queue for HTTP polling (backward compatible)
const turtle = turtles.get(turtleID);
if (turtle) {
turtle.pendingCommands.push({ ...command, timestamp: Date.now() });
}
}
}
// WebSocket connection handler
wss.on('connection', (ws) => {
wss.on('connection', (ws, req) => {
const url = req.url || '';
// ---- Bridge WebSocket connection ----
if (url.startsWith('/ws/bridge')) {
console.log('🌉 Webbridge connected via WebSocket');
bridgeClients.add(ws);
// Send list of known turtle IDs so bridge knows who to listen for
ws.send(JSON.stringify({
type: 'init',
turtleIDs: Array.from(turtles.keys()),
}));
ws.on('message', (raw) => {
try {
const data = JSON.parse(raw);
if (data.type === 'status') {
// Turtle status update forwarded from bridge
const turtleID = data.turtleID;
if (!turtleID) return;
const turtle = getOrCreateTurtle(turtleID);
turtle.updateFromStatus(data);
// Store home position if provided
if (data.homePosition) {
turtleHomes.set(turtleID, data.homePosition);
db.saveTurtleHome(turtleID, data.homePosition);
}
} else if (data.type === 'eval_response') {
// Eval response from turtle via bridge
const turtle = turtles.get(data.turtleID);
if (turtle) {
const handled = turtle.handleResponse(data.uuid, data.result, data.error);
if (handled) {
console.log(`📩 Eval response T#${data.turtleID} uuid:${(data.uuid || '').substring(0, 8)} - resolved`);
}
}
} else if (data.type === 'event') {
// Real-time events (inventory, peripheral)
const turtle = turtles.get(data.turtleID);
if (turtle) {
turtle.handleEvent(data.eventType, data.message);
}
} else if (data.type === 'request_home') {
// Turtle requesting home position
const home = turtleHomes.get(data.turtleID);
ws.send(JSON.stringify({
type: 'home_position',
turtleID: data.turtleID,
homePosition: home || null,
}));
} else if (data.type === 'set_home') {
// Turtle setting home position
const pos = data.position;
if (pos && data.turtleID) {
turtleHomes.set(data.turtleID, pos);
db.saveTurtleHome(data.turtleID, pos);
const turtle = turtles.get(data.turtleID);
if (turtle) turtle.homePosition = pos;
ws.send(JSON.stringify({
type: 'home_set_confirm',
turtleID: data.turtleID,
homePosition: pos,
}));
}
} else if (data.type === 'blocks_discovered') {
// Batch block discovery from turtle
if (data.blocks && Array.isArray(data.blocks)) {
for (const block of data.blocks) {
storeBlock(block.x, block.y, block.z, { name: block.name, metadata: block.metadata || 0 }, data.turtleID || block.discoveredBy);
}
broadcastToClients({
type: 'blocks_discovered',
blocks: data.blocks.map(b => ({
x: b.x, y: b.y, z: b.z,
name: b.name, metadata: b.metadata || 0,
discoveredBy: data.turtleID || b.discoveredBy,
timestamp: Date.now(),
})),
});
}
} else if (data.type === 'player_update') {
// Player position from pocket computer
if (data.playerID && data.position) {
db.savePlayerPosition(data.playerID, data.position);
broadcastToClients({
type: 'player_update',
player: { id: data.playerID, ...data.position },
});
}
}
} catch (error) {
console.error('❌ Bridge WS message error:', error.message);
}
});
ws.on('close', () => {
console.log('🌉 Webbridge disconnected');
bridgeClients.delete(ws);
});
ws.on('error', (error) => {
console.error('❌ Bridge WS error:', error);
bridgeClients.delete(ws);
});
return;
}
// ---- Web client WebSocket connection ----
console.log('🌐 New web client connected');
webClients.add(ws);