feat: Enhance turtle command handling with legacy support and eval response endpoints
This commit is contained in:
206
server/server.js
206
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++;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user