681 lines
26 KiB
Lua
681 lines
26 KiB
Lua
-- Web Bridge Dashboard for Turtle System
|
|
-- Beautiful visual interface for 2x3 monitor setup
|
|
-- Forwards turtle status updates to the web server
|
|
|
|
local SERVER_URL = "http://10.10.10.6:4200" -- Change to your server address
|
|
local CHANNEL_RECEIVE = 101
|
|
local STATUS_CHANNEL = 102
|
|
local COMMAND_CHANNEL = 100
|
|
local POCKET_CHANNEL = 103 -- Pocket computer communication
|
|
|
|
-- Find peripherals
|
|
local modem = peripheral.find("modem")
|
|
local monitor = peripheral.find("monitor")
|
|
|
|
if not modem then
|
|
error("No wireless modem found!")
|
|
end
|
|
|
|
-- Check if we have a monitor for dashboard
|
|
local hasMonitor = monitor ~= nil
|
|
|
|
if hasMonitor then
|
|
monitor.setTextScale(0.5)
|
|
end
|
|
|
|
modem.open(CHANNEL_RECEIVE)
|
|
modem.open(STATUS_CHANNEL)
|
|
modem.open(POCKET_CHANNEL)
|
|
|
|
print("Webbridge Channel Configuration:")
|
|
print(" CHANNEL_RECEIVE: " .. CHANNEL_RECEIVE)
|
|
print(" STATUS_CHANNEL: " .. STATUS_CHANNEL)
|
|
print(" COMMAND_CHANNEL: " .. COMMAND_CHANNEL .. " (transmit only)")
|
|
print(" POCKET_CHANNEL: " .. POCKET_CHANNEL)
|
|
|
|
-- Track turtles and stats
|
|
local turtles = {}
|
|
local stats = {
|
|
messagesReceived = 0,
|
|
commandsSent = 0,
|
|
serverUpdates = 0,
|
|
errors = 0,
|
|
startTime = os.epoch("utc")
|
|
}
|
|
local activityLog = {}
|
|
|
|
-- Dashboard drawing functions (only if monitor available)
|
|
local function centerText(y, text, fg, bg)
|
|
if not hasMonitor then return end
|
|
local w = monitor.getSize()
|
|
monitor.setCursorPos(math.floor((w - #text) / 2) + 1, y)
|
|
monitor.setTextColor(fg or colors.white)
|
|
monitor.setBackgroundColor(bg or colors.black)
|
|
monitor.write(text)
|
|
end
|
|
|
|
local function drawBox(x, y, width, height, color)
|
|
if not hasMonitor then return end
|
|
monitor.setBackgroundColor(color)
|
|
for dy = 0, height - 1 do
|
|
monitor.setCursorPos(x, y + dy)
|
|
monitor.write(string.rep(" ", width))
|
|
end
|
|
end
|
|
|
|
local function formatTime(ms)
|
|
local seconds = math.floor(ms / 1000)
|
|
local minutes = math.floor(seconds / 60)
|
|
local hours = math.floor(minutes / 60)
|
|
|
|
if hours > 0 then
|
|
return string.format("%dh %dm", hours, minutes % 60)
|
|
elseif minutes > 0 then
|
|
return string.format("%dm %ds", minutes, seconds % 60)
|
|
else
|
|
return string.format("%ds", seconds)
|
|
end
|
|
end
|
|
|
|
local function getStatusColor(turtle)
|
|
if not turtle.lastSeen then return colors.red end
|
|
|
|
local timeSince = os.epoch("utc") - turtle.lastSeen
|
|
if timeSince < 10000 then
|
|
return colors.lime
|
|
elseif timeSince < 30000 then
|
|
return colors.yellow
|
|
else
|
|
return colors.red
|
|
end
|
|
end
|
|
|
|
local function drawDashboard()
|
|
if not hasMonitor then return end
|
|
|
|
local w, h = monitor.getSize()
|
|
monitor.setBackgroundColor(colors.black)
|
|
monitor.clear()
|
|
|
|
-- Count turtles
|
|
local turtleCount = 0
|
|
for _ in pairs(turtles) do
|
|
turtleCount = turtleCount + 1
|
|
end
|
|
|
|
-- Simple header
|
|
monitor.setBackgroundColor(colors.blue)
|
|
monitor.setTextColor(colors.white)
|
|
monitor.setCursorPos(1, 1)
|
|
monitor.clearLine()
|
|
centerText(1, "TURTLE BRIDGE - " .. turtleCount .. " ONLINE", colors.white, colors.blue)
|
|
|
|
-- Status line
|
|
monitor.setBackgroundColor(colors.gray)
|
|
monitor.setCursorPos(1, 2)
|
|
monitor.clearLine()
|
|
monitor.setTextColor(colors.white)
|
|
monitor.setCursorPos(2, 2)
|
|
monitor.write("MSG: " .. stats.messagesReceived .. " | CMD: " .. stats.commandsSent .. " | ERR: " .. stats.errors)
|
|
|
|
-- Turtle list - much larger, starts at line 4
|
|
monitor.setBackgroundColor(colors.black)
|
|
local startY = 4
|
|
|
|
if turtleCount == 0 then
|
|
monitor.setTextColor(colors.gray)
|
|
monitor.setCursorPos(2, startY)
|
|
monitor.write("No turtles connected. Waiting for signals...")
|
|
else
|
|
-- Show all turtles in a compact list
|
|
local y = startY
|
|
local count = 0
|
|
for id, turtle in pairs(turtles) do
|
|
if y >= h - 5 then break end -- Leave room for activity log
|
|
|
|
local statusColor = getStatusColor(turtle)
|
|
local timeSince = 0
|
|
if turtle.lastSeen then
|
|
local currentTime = os.epoch("utc") or 0
|
|
timeSince = math.floor((currentTime - turtle.lastSeen) / 1000)
|
|
end
|
|
|
|
monitor.setCursorPos(2, y)
|
|
monitor.setTextColor(statusColor)
|
|
monitor.write("\7")
|
|
|
|
monitor.setTextColor(colors.white)
|
|
monitor.write(string.format(" Turtle #%-3d", id))
|
|
|
|
monitor.setTextColor(colors.lightGray)
|
|
local state = (turtle.mode or "IDLE"):upper()
|
|
if #state > 12 then state = state:sub(1, 12) end
|
|
monitor.write(" " .. state)
|
|
|
|
-- Position on same line
|
|
if turtle.position then
|
|
monitor.setTextColor(colors.gray)
|
|
monitor.write(string.format(" [%d,%d,%d]",
|
|
turtle.position.x or 0,
|
|
turtle.position.y or 0,
|
|
turtle.position.z or 0))
|
|
end
|
|
|
|
-- Last seen
|
|
monitor.setTextColor(colors.gray)
|
|
monitor.write(string.format(" %ds", timeSince or 0))
|
|
|
|
y = y + 1
|
|
count = count + 1
|
|
end
|
|
end
|
|
|
|
-- Activity log (last 5 lines of screen)
|
|
local logY = h - 4
|
|
monitor.setBackgroundColor(colors.gray)
|
|
monitor.setCursorPos(1, logY)
|
|
monitor.clearLine()
|
|
monitor.setTextColor(colors.yellow)
|
|
monitor.setCursorPos(2, logY)
|
|
monitor.write("ACTIVITY LOG")
|
|
|
|
logY = logY + 1
|
|
monitor.setBackgroundColor(colors.black)
|
|
|
|
-- Show last 3 log entries
|
|
for i = 1, math.min(3, #activityLog) do
|
|
local entry = activityLog[i]
|
|
monitor.setCursorPos(1, logY)
|
|
monitor.clearLine()
|
|
|
|
monitor.setTextColor(colors.gray)
|
|
local time = os.date("%H:%M:%S", (entry.time or 0) / 1000)
|
|
monitor.write(time .. " ")
|
|
|
|
monitor.setTextColor(entry.color or colors.white)
|
|
local maxWidth = w - 10
|
|
local text = entry.text
|
|
if #text > maxWidth then
|
|
text = text:sub(1, maxWidth - 3) .. "..."
|
|
end
|
|
monitor.write(text)
|
|
|
|
logY = logY + 1
|
|
end
|
|
end
|
|
|
|
local function addLog(text, color)
|
|
table.insert(activityLog, 1, {text = text, color = color or colors.white, time = os.epoch("utc")})
|
|
if #activityLog > 35 then
|
|
table.remove(activityLog)
|
|
end
|
|
|
|
-- Also print to console if no monitor
|
|
if not hasMonitor then
|
|
print(text)
|
|
end
|
|
end
|
|
|
|
|
|
-- Function to send data to web server
|
|
local function sendToServer(data)
|
|
local success, result = pcall(function()
|
|
local jsonData = textutils.serializeJSON(data)
|
|
|
|
local response = http.post(
|
|
SERVER_URL .. "/api/turtle/update",
|
|
jsonData,
|
|
{["Content-Type"] = "application/json"}
|
|
)
|
|
|
|
if response then
|
|
response.close()
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end)
|
|
|
|
return success and result
|
|
end
|
|
|
|
-- Function to poll for commands from server
|
|
local function pollCommands(turtleID)
|
|
local success, result = pcall(function()
|
|
local response = http.get(SERVER_URL .. "/api/turtle/" .. turtleID .. "/commands")
|
|
|
|
if response then
|
|
local content = response.readAll()
|
|
response.close()
|
|
|
|
local data = textutils.unserializeJSON(content)
|
|
if data and data.commands then
|
|
return data.commands
|
|
end
|
|
end
|
|
|
|
return {}
|
|
end)
|
|
|
|
if success then
|
|
return result
|
|
else
|
|
stats.errors = stats.errors + 1
|
|
return {}
|
|
end
|
|
end
|
|
|
|
-- Function to acknowledge commands were sent
|
|
local function acknowledgeCommands(turtleID)
|
|
local success = pcall(function()
|
|
local response = http.post(
|
|
SERVER_URL .. "/api/turtle/" .. turtleID .. "/commands/ack",
|
|
"{}",
|
|
{["Content-Type"] = "application/json"}
|
|
)
|
|
|
|
if response then
|
|
response.close()
|
|
return true
|
|
end
|
|
return false
|
|
end)
|
|
|
|
return success
|
|
end
|
|
|
|
-- Initial setup
|
|
if hasMonitor then
|
|
drawDashboard()
|
|
addLog("System initialized with monitor", colors.lime)
|
|
else
|
|
print("Web Bridge Started (No monitor)")
|
|
print("Listening for turtle updates...")
|
|
print("Server: " .. SERVER_URL)
|
|
end
|
|
|
|
addLog("Listening on channels " .. STATUS_CHANNEL .. ", " .. CHANNEL_RECEIVE .. ", " .. POCKET_CHANNEL, colors.lightBlue)
|
|
|
|
-- Debug: Print what channels are actually open
|
|
print("Opened channels:")
|
|
for i = 0, 65535 do
|
|
if modem.isOpen(i) then
|
|
print(" Channel " .. i .. " is OPEN")
|
|
end
|
|
end
|
|
|
|
-- Start polling timer
|
|
local POLL_INTERVAL = 2 -- Poll every 2 seconds (reduced frequency for better reliability)
|
|
os.startTimer(POLL_INTERVAL)
|
|
|
|
-- Track which turtles we've sent commands to recently
|
|
local recentCommandSends = {}
|
|
|
|
-- Main loop
|
|
local lastRefresh = os.epoch("utc")
|
|
print("🎧 Starting main event loop...")
|
|
while true do
|
|
local event, side, channel, replyChannel, message, distance = os.pullEvent()
|
|
|
|
-- Log ALL modem messages for debugging
|
|
if event == "modem_message" then
|
|
print("🔔 MODEM MESSAGE RECEIVED!")
|
|
print(" Event: " .. event)
|
|
print(" Channel: " .. channel)
|
|
print(" Expected channels: " .. STATUS_CHANNEL .. " (status), " .. CHANNEL_RECEIVE .. " (receive), " .. POCKET_CHANNEL .. " (pocket)")
|
|
end
|
|
|
|
if event == "timer" then
|
|
-- Poll for commands for all known turtles
|
|
for turtleID, turtleData in pairs(turtles) do
|
|
local commands = pollCommands(turtleID)
|
|
|
|
-- Only send commands if we got some
|
|
if #commands > 0 then
|
|
addLog("Received " .. #commands .. " command(s) for Turtle #" .. turtleID, colors.cyan)
|
|
|
|
-- Forward commands back to turtle
|
|
for _, cmd in ipairs(commands) do
|
|
stats.commandsSent = stats.commandsSent + 1
|
|
addLog(" CMD: " .. cmd.command .. " -> Turtle #" .. turtleID, colors.yellow)
|
|
|
|
local commandPacket = {
|
|
command = cmd.command,
|
|
param = cmd.param,
|
|
target = turtleID
|
|
}
|
|
|
|
print("📡 Transmitting command to Turtle #" .. turtleID)
|
|
print(" Channel: " .. COMMAND_CHANNEL)
|
|
print(" Command: " .. cmd.command)
|
|
print(" Target: " .. turtleID)
|
|
print(" Packet: " .. textutils.serialize(commandPacket))
|
|
|
|
-- Send command multiple times for reliability
|
|
for i = 1, 3 do
|
|
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, commandPacket)
|
|
print(" Transmission " .. i .. "/3 sent")
|
|
os.sleep(0.05) -- Small delay between retransmissions
|
|
end
|
|
|
|
-- Debug: show what we're sending
|
|
if not hasMonitor then
|
|
print(" ✅ Sent 3 times on channel " .. COMMAND_CHANNEL)
|
|
end
|
|
end
|
|
|
|
-- Acknowledge that we sent the commands
|
|
-- Wait longer to ensure turtle has received them
|
|
os.sleep(1.5) -- Give turtle time to receive and process
|
|
if acknowledgeCommands(turtleID) then
|
|
addLog(" ACK: Commands acknowledged", colors.lime)
|
|
else
|
|
addLog(" WARN: Failed to acknowledge", colors.orange)
|
|
end
|
|
|
|
-- Mark that we sent commands to this turtle
|
|
recentCommandSends[turtleID] = os.epoch("utc")
|
|
end
|
|
end
|
|
|
|
-- Clean up old command send timestamps (older than 10 seconds)
|
|
local now = os.epoch("utc")
|
|
for turtleID, timestamp in pairs(recentCommandSends) do
|
|
if now - timestamp > 10000 then
|
|
recentCommandSends[turtleID] = nil
|
|
end
|
|
end
|
|
|
|
-- Restart timer
|
|
os.startTimer(POLL_INTERVAL)
|
|
|
|
-- Refresh display if we have a monitor
|
|
if hasMonitor then
|
|
local now = os.epoch("utc")
|
|
if now - lastRefresh > 2000 then
|
|
drawDashboard()
|
|
lastRefresh = now
|
|
end
|
|
end
|
|
|
|
elseif event == "modem_message" then
|
|
-- Only process messages on our channels
|
|
if channel == STATUS_CHANNEL or channel == CHANNEL_RECEIVE then
|
|
stats.messagesReceived = stats.messagesReceived + 1
|
|
|
|
if type(message) == "table" then
|
|
-- Debug: log what type of message we got
|
|
print("📨 Received message on channel " .. channel)
|
|
if message.type then
|
|
print(" Type: " .. message.type)
|
|
end
|
|
if message.turtleID then
|
|
print(" From turtle: " .. message.turtleID)
|
|
end
|
|
|
|
if message.type == "status" then
|
|
local turtleID = message.turtleID
|
|
|
|
print("✅ Processing status update from Turtle #" .. turtleID)
|
|
|
|
-- Update turtle data
|
|
turtles[turtleID] = message
|
|
turtles[turtleID].lastSeen = os.epoch("utc")
|
|
|
|
-- Count turtles
|
|
local count = 0
|
|
for _ in pairs(turtles) do
|
|
count = count + 1
|
|
end
|
|
print(" Total turtles tracked: " .. count)
|
|
|
|
addLog("Turtle #" .. turtleID .. " - " .. (message.mode or "status"), colors.lightBlue)
|
|
|
|
-- Forward to web server
|
|
local success = sendToServer(message)
|
|
if success then
|
|
stats.serverUpdates = stats.serverUpdates + 1
|
|
addLog(" -> Forwarded to server", colors.lime)
|
|
else
|
|
stats.errors = stats.errors + 1
|
|
addLog(" -> Server error", colors.red)
|
|
end
|
|
|
|
elseif message.type == "request_home" then
|
|
-- Turtle requesting its home position from server
|
|
local turtleID = message.turtleID
|
|
addLog("Turtle #" .. turtleID .. " requesting home", colors.cyan)
|
|
|
|
local success, result = pcall(function()
|
|
local response = http.get(SERVER_URL .. "/api/turtle/" .. turtleID .. "/home")
|
|
if response then
|
|
local content = response.readAll()
|
|
response.close()
|
|
local data = textutils.unserializeJSON(content)
|
|
return data
|
|
end
|
|
return nil
|
|
end)
|
|
|
|
if success and result then
|
|
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "home_position",
|
|
turtleID = turtleID,
|
|
homePosition = result.homePosition
|
|
})
|
|
addLog(" -> Sent home position", colors.lime)
|
|
else
|
|
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "home_position",
|
|
turtleID = turtleID,
|
|
homePosition = nil
|
|
})
|
|
addLog(" -> No home on server", colors.yellow)
|
|
end
|
|
|
|
elseif message.type == "set_home" then
|
|
-- Turtle setting its home position
|
|
local turtleID = message.turtleID
|
|
local position = message.position
|
|
addLog("Turtle #" .. turtleID .. " setting home", colors.cyan)
|
|
|
|
local success, result = pcall(function()
|
|
local response = http.post(
|
|
SERVER_URL .. "/api/turtle/" .. turtleID .. "/home",
|
|
textutils.serializeJSON({position = position}),
|
|
{["Content-Type"] = "application/json"}
|
|
)
|
|
if response then
|
|
local content = response.readAll()
|
|
response.close()
|
|
local data = textutils.unserializeJSON(content)
|
|
return data
|
|
end
|
|
return nil
|
|
end)
|
|
|
|
if success and result and result.success then
|
|
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "home_set_confirm",
|
|
turtleID = turtleID,
|
|
homePosition = result.homePosition
|
|
})
|
|
addLog(" -> Home saved to server", colors.lime)
|
|
else
|
|
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "home_set_confirm",
|
|
turtleID = turtleID,
|
|
homePosition = position
|
|
})
|
|
addLog(" -> Server error, local only", colors.red)
|
|
end
|
|
|
|
elseif message.type == "blocks_discovered" then
|
|
-- Turtle discovered new blocks - forward to server
|
|
local turtleID = message.turtleID
|
|
local blocks = message.blocks or {}
|
|
|
|
if #blocks > 0 then
|
|
print("📦 Turtle #" .. turtleID .. " discovered " .. #blocks .. " blocks")
|
|
|
|
-- Send each block to the server
|
|
for _, block in ipairs(blocks) do
|
|
local success = pcall(function()
|
|
local response = http.post(
|
|
SERVER_URL .. "/api/world/blocks",
|
|
textutils.serializeJSON({
|
|
x = block.x,
|
|
y = block.y,
|
|
z = block.z,
|
|
blockName = block.name,
|
|
metadata = block.metadata or 0,
|
|
discoveredBy = turtleID
|
|
}),
|
|
{["Content-Type"] = "application/json"}
|
|
)
|
|
if response then
|
|
response.close()
|
|
end
|
|
end)
|
|
|
|
if not success then
|
|
stats.errors = stats.errors + 1
|
|
end
|
|
end
|
|
|
|
addLog(" -> Sent " .. #blocks .. " blocks to server", colors.lime)
|
|
end
|
|
end
|
|
end
|
|
elseif channel == POCKET_CHANNEL then
|
|
-- Handle pocket computer requests
|
|
stats.messagesReceived = stats.messagesReceived + 1
|
|
|
|
if type(message) == "table" and message.from then
|
|
local pocketID = message.from
|
|
|
|
if message.type == "turtle_command" then
|
|
-- Forward command to turtle
|
|
local turtleID = message.turtleID
|
|
addLog("Pocket #" .. pocketID .. " -> Turtle #" .. turtleID .. ": " .. message.command, colors.magenta)
|
|
|
|
modem.transmit(COMMAND_CHANNEL, CHANNEL_RECEIVE, {
|
|
command = message.command,
|
|
param = message.param,
|
|
target = turtleID
|
|
})
|
|
|
|
-- Send acknowledgment
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "command_ack",
|
|
to = pocketID
|
|
})
|
|
|
|
elseif message.type == "player_position" then
|
|
-- Forward player position to server
|
|
addLog("Pocket #" .. pocketID .. " GPS update", colors.cyan)
|
|
|
|
local success = pcall(function()
|
|
http.post(
|
|
SERVER_URL .. "/api/player/update",
|
|
textutils.serializeJSON({
|
|
playerID = message.playerID,
|
|
position = message.position,
|
|
timestamp = message.timestamp
|
|
}),
|
|
{["Content-Type"] = "application/json"}
|
|
)
|
|
end)
|
|
|
|
if not success then
|
|
addLog(" -> Failed to send player position", colors.red)
|
|
end
|
|
|
|
elseif message.type == "server_stats_request" then
|
|
-- Fetch server stats and send to pocket
|
|
addLog("Pocket #" .. pocketID .. " requesting stats", colors.yellow)
|
|
|
|
local success, result = pcall(function()
|
|
local response = http.get(SERVER_URL .. "/api/stats")
|
|
if response then
|
|
local content = response.readAll()
|
|
response.close()
|
|
return textutils.unserializeJSON(content)
|
|
end
|
|
return nil
|
|
end)
|
|
|
|
if success and result then
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "server_stats",
|
|
to = pocketID,
|
|
data = result
|
|
})
|
|
else
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "error",
|
|
to = pocketID,
|
|
error = "Failed to fetch server stats"
|
|
})
|
|
end
|
|
|
|
elseif message.type == "webbridge_control" then
|
|
-- Handle webbridge control commands
|
|
addLog("Pocket #" .. pocketID .. " control: " .. message.command, colors.orange)
|
|
|
|
if message.command == "ping" then
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "webbridge_log",
|
|
to = pocketID,
|
|
text = "Pong! Webbridge online"
|
|
})
|
|
elseif message.command == "status" then
|
|
local turtleCount = 0
|
|
for _ in pairs(turtles) do
|
|
turtleCount = turtleCount + 1
|
|
end
|
|
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "webbridge_status",
|
|
to = pocketID,
|
|
data = {
|
|
messages = stats.messagesReceived,
|
|
commands = stats.commandsSent,
|
|
turtles = turtleCount,
|
|
uptime = os.epoch("utc") - stats.startTime
|
|
}
|
|
})
|
|
elseif message.command == "restart" then
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "webbridge_log",
|
|
to = pocketID,
|
|
text = "Restarting webbridge..."
|
|
})
|
|
sleep(1)
|
|
os.reboot()
|
|
elseif message.command == "logs" then
|
|
-- Send recent log entries
|
|
for i = math.max(1, #activityLog - 5), #activityLog do
|
|
if activityLog[i] then
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "webbridge_log",
|
|
to = pocketID,
|
|
text = activityLog[i].text
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
elseif message.type == "web_interface_request" then
|
|
-- Send web interface info
|
|
modem.transmit(POCKET_CHANNEL, POCKET_CHANNEL, {
|
|
type = "webbridge_log",
|
|
to = pocketID,
|
|
text = "Web: " .. SERVER_URL
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|