diff --git a/server/server.js b/server/server.js index 382824a..f36820e 100644 --- a/server/server.js +++ b/server/server.js @@ -364,14 +364,14 @@ app.get('/api/turtle/:id/commands', (req, res) => { app.post('/api/turtle/:id/commands/ack', (req, res) => { try { const turtleID = parseInt(req.params.id); + const turtle = turtles.get(turtleID); - if (turtleData.has(turtleID)) { - const turtle = turtleData.get(turtleID); - const clearedCount = (turtle.pendingCommands || []).length; + if (turtle) { + const clearedCount = (turtle.pendingLegacyCommands || []).length; if (clearedCount > 0) { console.log(`✅ Turtle ${turtleID} acknowledged ${clearedCount} command(s)`); - turtle.pendingCommands = []; + turtle.pendingLegacyCommands = []; } res.json({ success: true, cleared: clearedCount }); @@ -387,7 +387,7 @@ app.post('/api/turtle/:id/commands/ack', (req, res) => { // Get all turtles app.get('/api/turtles', (req, res) => { res.json({ - turtles: Array.from(turtleData.values()) + turtles: Array.from(turtles.values()).map(t => t.toJSON()) }); }); @@ -407,16 +407,15 @@ app.post('/api/turtle/:id/home', (req, res) => { // Persist to database db.saveTurtleHome(turtleID, position); - // Update turtle data - if (turtleData.has(turtleID)) { - const turtle = turtleData.get(turtleID); + // Update turtle instance + const turtle = turtles.get(turtleID); + if (turtle) { turtle.homePosition = position; - turtleData.set(turtleID, turtle); // Broadcast update broadcastToClients({ type: 'turtle_update', - turtle: turtle + turtle: turtle.toJSON() }); } @@ -486,17 +485,24 @@ app.post('/api/turtle/:id/command', (req, res) => { try { const turtleID = parseInt(req.params.id); const { command, param } = req.body; + const turtle = turtles.get(turtleID); - 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); + if (turtle) { + // Check for state change commands + if (command === 'set_state' || command === 'setState') { + const stateName = param?.state || param; + const stateData = param?.data || {}; + turtle.setState(stateName, stateData); + console.log(`📤 State set for turtle ${turtleID}: ${stateName}`); + } else { + // Legacy command - queue for webbridge + turtle.pendingLegacyCommands.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' }); @@ -507,6 +513,141 @@ app.post('/api/turtle/:id/command', (req, res) => { } }); +// ========== EVAL PROTOCOL ENDPOINTS ========== + +// Receive eval response from webbridge (turtle -> webbridge -> server) +app.post('/api/turtle/eval-response', (req, res) => { + try { + const { turtleID, uuid, result, error } = req.body; + + if (!turtleID || !uuid) { + return res.status(400).json({ error: 'Missing turtleID or uuid' }); + } + + const turtle = turtles.get(turtleID); + if (turtle) { + const handled = turtle.handleResponse(uuid, result, error); + if (handled) { + console.log(`📩 Eval response from T#${turtleID} uuid:${uuid.substring(0, 8)} - resolved`); + } else { + console.log(`📩 Eval response from T#${turtleID} uuid:${uuid.substring(0, 8)} - no matching pending command`); + } + } else { + console.log(`📩 Eval response from unknown turtle ${turtleID}`); + } + + res.json({ success: true }); + } catch (error) { + console.error('❌ Error processing eval response:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Set turtle state (state machine control) +app.post('/api/turtle/:id/state', (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { state, data } = req.body; + + if (!state) { + return res.status(400).json({ error: 'Missing state name' }); + } + + const turtle = turtles.get(turtleID); + if (turtle) { + turtle.setState(state, data || {}); + console.log(`🔄 State set for turtle ${turtleID}: ${state}`); + res.json({ success: true, state: turtle.stateName, description: turtle.stateDescription }); + } else { + res.status(404).json({ error: 'Turtle not found' }); + } + } catch (error) { + console.error('❌ Error setting state:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Get turtle state +app.get('/api/turtle/:id/state', (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const turtle = turtles.get(turtleID); + + if (turtle) { + res.json({ + state: turtle.stateName, + description: turtle.stateDescription, + turtleID, + }); + } else { + res.status(404).json({ error: 'Turtle not found' }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Execute Lua code on a turtle directly (for debugging/admin) +app.post('/api/turtle/:id/exec', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { code, timeout } = req.body; + + if (!code) { + return res.status(400).json({ error: 'Missing code' }); + } + + const turtle = turtles.get(turtleID); + if (!turtle) { + return res.status(404).json({ error: 'Turtle not found' }); + } + + const result = await turtle.exec(code, timeout || 30000); + res.json({ success: true, result }); + } catch (error) { + console.error(`❌ Exec error for turtle ${req.params.id}:`, error.message); + res.json({ success: false, error: error.message }); + } +}); + +// Bulk blocks discovery (from webbridge batch uploads) +app.post('/api/world/blocks/batch', (req, res) => { + try { + const { blocks, turtleID } = req.body; + + if (!Array.isArray(blocks)) { + return res.status(400).json({ error: 'blocks must be an array' }); + } + + let stored = 0; + for (const block of blocks) { + if (block.x !== undefined && block.y !== undefined && block.z !== undefined && block.name) { + storeBlock(block.x, block.y, block.z, { name: block.name, metadata: block.metadata || 0 }, turtleID || block.discoveredBy); + stored++; + } + } + + // Broadcast to web clients + if (stored > 0) { + broadcastToClients({ + type: 'blocks_discovered', + blocks: blocks.filter(b => b.x !== undefined && b.name).map(b => ({ + x: b.x, y: b.y, z: b.z, + name: b.name, + metadata: b.metadata || 0, + discoveredBy: turtleID || b.discoveredBy, + timestamp: Date.now(), + })), + }); + } + + res.json({ success: true, stored }); + } catch (error) { + console.error('❌ Error batch storing blocks:', error); + res.status(500).json({ error: error.message }); + } +}); + // ========== PATH RECORDING ENDPOINTS ========== // Save a recorded path @@ -835,8 +976,8 @@ app.post('/api/mining-areas/:areaId/close', (req, res) => { app.get('/api/stats', (req, res) => { try { const stats = { - activeTurtles: turtleData.size, - turtles: turtleData.size, + activeTurtles: turtles.size, + turtles: turtles.size, totalBlocks: worldBlocks.size, blocks: worldBlocks.size, savedHomes: turtleHomes.size, @@ -1028,14 +1169,19 @@ app.post('/api/groups/:groupId/command', (req, res) => { let successCount = 0; for (const member of members) { - if (turtleData.has(member.turtle_id)) { - const turtle = turtleData.get(member.turtle_id); - turtle.pendingCommands = turtle.pendingCommands || []; - turtle.pendingCommands.push({ - command, - param, - timestamp: Date.now() - }); + const turtle = turtles.get(member.turtle_id); + if (turtle) { + if (command === 'set_state' || command === 'setState') { + const stateName = param?.state || param; + const stateData = param?.data || {}; + turtle.setState(stateName, stateData); + } else { + turtle.pendingLegacyCommands.push({ + command, + param, + timestamp: Date.now() + }); + } successCount++; } }