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) => {
|
app.post('/api/turtle/:id/commands/ack', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const turtleID = parseInt(req.params.id);
|
const turtleID = parseInt(req.params.id);
|
||||||
|
const turtle = turtles.get(turtleID);
|
||||||
|
|
||||||
if (turtleData.has(turtleID)) {
|
if (turtle) {
|
||||||
const turtle = turtleData.get(turtleID);
|
const clearedCount = (turtle.pendingLegacyCommands || []).length;
|
||||||
const clearedCount = (turtle.pendingCommands || []).length;
|
|
||||||
|
|
||||||
if (clearedCount > 0) {
|
if (clearedCount > 0) {
|
||||||
console.log(`✅ Turtle ${turtleID} acknowledged ${clearedCount} command(s)`);
|
console.log(`✅ Turtle ${turtleID} acknowledged ${clearedCount} command(s)`);
|
||||||
turtle.pendingCommands = [];
|
turtle.pendingLegacyCommands = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, cleared: clearedCount });
|
res.json({ success: true, cleared: clearedCount });
|
||||||
@@ -387,7 +387,7 @@ app.post('/api/turtle/:id/commands/ack', (req, res) => {
|
|||||||
// Get all turtles
|
// Get all turtles
|
||||||
app.get('/api/turtles', (req, res) => {
|
app.get('/api/turtles', (req, res) => {
|
||||||
res.json({
|
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
|
// Persist to database
|
||||||
db.saveTurtleHome(turtleID, position);
|
db.saveTurtleHome(turtleID, position);
|
||||||
|
|
||||||
// Update turtle data
|
// Update turtle instance
|
||||||
if (turtleData.has(turtleID)) {
|
const turtle = turtles.get(turtleID);
|
||||||
const turtle = turtleData.get(turtleID);
|
if (turtle) {
|
||||||
turtle.homePosition = position;
|
turtle.homePosition = position;
|
||||||
turtleData.set(turtleID, turtle);
|
|
||||||
|
|
||||||
// Broadcast update
|
// Broadcast update
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'turtle_update',
|
type: 'turtle_update',
|
||||||
turtle: turtle
|
turtle: turtle.toJSON()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,17 +485,24 @@ app.post('/api/turtle/:id/command', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const turtleID = parseInt(req.params.id);
|
const turtleID = parseInt(req.params.id);
|
||||||
const { command, param } = req.body;
|
const { command, param } = req.body;
|
||||||
|
const turtle = turtles.get(turtleID);
|
||||||
|
|
||||||
if (turtleData.has(turtleID)) {
|
if (turtle) {
|
||||||
const turtle = turtleData.get(turtleID);
|
// Check for state change commands
|
||||||
turtle.pendingCommands = turtle.pendingCommands || [];
|
if (command === 'set_state' || command === 'setState') {
|
||||||
turtle.pendingCommands.push({
|
const stateName = param?.state || param;
|
||||||
command,
|
const stateData = param?.data || {};
|
||||||
param,
|
turtle.setState(stateName, stateData);
|
||||||
timestamp: Date.now()
|
console.log(`📤 State set for turtle ${turtleID}: ${stateName}`);
|
||||||
});
|
} else {
|
||||||
|
// Legacy command - queue for webbridge
|
||||||
console.log(`📤 Command queued for turtle ${turtleID}:`, command);
|
turtle.pendingLegacyCommands.push({
|
||||||
|
command,
|
||||||
|
param,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
console.log(`📤 Command queued for turtle ${turtleID}:`, command);
|
||||||
|
}
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ error: 'Turtle not found' });
|
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 ==========
|
// ========== PATH RECORDING ENDPOINTS ==========
|
||||||
|
|
||||||
// Save a recorded path
|
// Save a recorded path
|
||||||
@@ -835,8 +976,8 @@ app.post('/api/mining-areas/:areaId/close', (req, res) => {
|
|||||||
app.get('/api/stats', (req, res) => {
|
app.get('/api/stats', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const stats = {
|
const stats = {
|
||||||
activeTurtles: turtleData.size,
|
activeTurtles: turtles.size,
|
||||||
turtles: turtleData.size,
|
turtles: turtles.size,
|
||||||
totalBlocks: worldBlocks.size,
|
totalBlocks: worldBlocks.size,
|
||||||
blocks: worldBlocks.size,
|
blocks: worldBlocks.size,
|
||||||
savedHomes: turtleHomes.size,
|
savedHomes: turtleHomes.size,
|
||||||
@@ -1028,14 +1169,19 @@ app.post('/api/groups/:groupId/command', (req, res) => {
|
|||||||
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
if (turtleData.has(member.turtle_id)) {
|
const turtle = turtles.get(member.turtle_id);
|
||||||
const turtle = turtleData.get(member.turtle_id);
|
if (turtle) {
|
||||||
turtle.pendingCommands = turtle.pendingCommands || [];
|
if (command === 'set_state' || command === 'setState') {
|
||||||
turtle.pendingCommands.push({
|
const stateName = param?.state || param;
|
||||||
command,
|
const stateData = param?.data || {};
|
||||||
param,
|
turtle.setState(stateName, stateData);
|
||||||
timestamp: Date.now()
|
} else {
|
||||||
});
|
turtle.pendingLegacyCommands.push({
|
||||||
|
command,
|
||||||
|
param,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
successCount++;
|
successCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user