From e84ca4cfb943475261addf76353b1f1926b9b21a Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Fri, 20 Feb 2026 04:14:03 -0500 Subject: [PATCH] refactor: enhance WebSocket handling for bridge connections and command forwarding --- server/server.js | 164 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 153 insertions(+), 11 deletions(-) diff --git a/server/server.js b/server/server.js index ffba979..09b63bf 100644 --- a/server/server.js +++ b/server/server.js @@ -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);