640 lines
18 KiB
Lua
640 lines
18 KiB
Lua
-- Unified Pocket Control Center
|
|
-- Combines turtle control, GPS tracking, server management, and webbridge control
|
|
-- Communicates wirelessly with webbridge - NO direct HTTP calls
|
|
|
|
local CHANNEL_SEND = 100
|
|
local CHANNEL_RECEIVE = 101
|
|
local STATUS_CHANNEL = 102
|
|
local POCKET_CHANNEL = 103 -- Pocket <-> Webbridge communication
|
|
|
|
-- Find modem
|
|
local modem = peripheral.find("modem")
|
|
if not modem then
|
|
error("No wireless modem found!")
|
|
end
|
|
|
|
-- Equip modem if this is a pocket computer
|
|
if pocket then
|
|
pocket.equipBack()
|
|
modem = peripheral.find("modem")
|
|
end
|
|
|
|
modem.open(CHANNEL_RECEIVE)
|
|
modem.open(STATUS_CHANNEL)
|
|
modem.open(POCKET_CHANNEL)
|
|
|
|
local w, h = term.getSize()
|
|
|
|
-- State
|
|
local turtles = {}
|
|
local selectedTurtle = nil
|
|
local mode = "overview" -- overview, control, gps, webbridge, server
|
|
local myPosition = nil
|
|
local webbridgeStatus = nil
|
|
local serverStats = nil
|
|
local logMessages = {}
|
|
local maxLogs = 50
|
|
|
|
-- Button system
|
|
local buttons = {}
|
|
|
|
local function addLog(message, color)
|
|
table.insert(logMessages, 1, {
|
|
text = message,
|
|
time = os.epoch("utc"),
|
|
color = color or colors.white
|
|
})
|
|
if #logMessages > maxLogs then
|
|
table.remove(logMessages)
|
|
end
|
|
end
|
|
|
|
local function clearButtons()
|
|
buttons = {}
|
|
end
|
|
|
|
local function addButton(x, y, width, height, label, action, color)
|
|
table.insert(buttons, {
|
|
x = x, y = y, width = width, height = height,
|
|
label = label, action = action, color = color or colors.gray
|
|
})
|
|
end
|
|
|
|
local function drawButton(btn, highlight)
|
|
term.setCursorPos(btn.x, btn.y)
|
|
local bg = highlight and colors.white or btn.color
|
|
local fg = highlight and colors.black or colors.white
|
|
term.setBackgroundColor(bg)
|
|
term.setTextColor(fg)
|
|
|
|
local text = btn.label
|
|
local padding = math.floor((btn.width - #text) / 2)
|
|
|
|
for dy = 0, btn.height - 1 do
|
|
term.setCursorPos(btn.x, btn.y + dy)
|
|
if dy == math.floor(btn.height / 2) then
|
|
term.write(string.rep(" ", padding) .. text .. string.rep(" ", btn.width - padding - #text))
|
|
else
|
|
term.write(string.rep(" ", btn.width))
|
|
end
|
|
end
|
|
end
|
|
|
|
local function checkButton(x, y)
|
|
for _, btn in ipairs(buttons) do
|
|
if x >= btn.x and x < btn.x + btn.width and
|
|
y >= btn.y and y < btn.y + btn.height then
|
|
return btn
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Update my GPS position and send to webbridge
|
|
local function updateMyPosition()
|
|
local x, y, z = gps.locate(2)
|
|
if x then
|
|
myPosition = {x = math.floor(x), y = math.floor(y), z = math.floor(z)}
|
|
-- Send to webbridge (which will forward to server)
|
|
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "player_position",
|
|
playerID = os.getComputerID(),
|
|
label = os.getComputerLabel() or ("Pocket #" .. os.getComputerID()),
|
|
position = myPosition,
|
|
timestamp = os.epoch("utc")
|
|
})
|
|
return true
|
|
else
|
|
addLog("GPS: Failed to locate", colors.red)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Send command to turtle via webbridge
|
|
local function sendCommand(turtleID, command, param)
|
|
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "turtle_command",
|
|
turtleID = turtleID,
|
|
command = command,
|
|
param = param,
|
|
from = os.getComputerID()
|
|
})
|
|
addLog("Sent: " .. command .. " -> Turtle #" .. turtleID, colors.yellow)
|
|
end
|
|
|
|
-- Send webbridge control command
|
|
local function sendWebbridgeCommand(command)
|
|
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "webbridge_control",
|
|
command = command,
|
|
from = os.getComputerID()
|
|
})
|
|
addLog("Webbridge: " .. command, colors.cyan)
|
|
end
|
|
|
|
-- Request server stats via webbridge
|
|
local function fetchServerStats()
|
|
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "server_stats_request",
|
|
from = os.getComputerID()
|
|
})
|
|
end
|
|
|
|
-- Draw header
|
|
local function drawHeader()
|
|
term.setBackgroundColor(colors.blue)
|
|
term.setTextColor(colors.white)
|
|
term.setCursorPos(1, 1)
|
|
term.clearLine()
|
|
|
|
local title = "POCKET CTRL"
|
|
term.setCursorPos(2, 1)
|
|
term.write(title)
|
|
|
|
-- Show quick stats
|
|
term.setCursorPos(w - 10, 1)
|
|
term.write("T:" .. #turtles .. " ")
|
|
if myPosition then
|
|
term.write("GPS")
|
|
end
|
|
end
|
|
|
|
-- Draw mode tabs
|
|
local function drawTabs()
|
|
clearButtons()
|
|
local tabWidth = math.floor(w / 5)
|
|
local y = 2
|
|
|
|
-- Draw tab highlights
|
|
term.setCursorPos(1, y)
|
|
term.setBackgroundColor(colors.black)
|
|
term.clearLine()
|
|
|
|
local tabs = {
|
|
{name = "TRT", mode = "overview"},
|
|
{name = "CTL", mode = "control"},
|
|
{name = "GPS", mode = "gps"},
|
|
{name = "WEB", mode = "webbridge"},
|
|
{name = "SRV", mode = "server"}
|
|
}
|
|
|
|
for i, tab in ipairs(tabs) do
|
|
local x = (i - 1) * tabWidth + 1
|
|
addButton(x, y, tabWidth, 1, tab.name, function() mode = tab.mode end,
|
|
mode == tab.mode and colors.green or colors.gray)
|
|
|
|
term.setCursorPos(x, y)
|
|
if mode == tab.mode then
|
|
term.setBackgroundColor(colors.green)
|
|
term.setTextColor(colors.white)
|
|
else
|
|
term.setBackgroundColor(colors.gray)
|
|
term.setTextColor(colors.lightGray)
|
|
end
|
|
local padding = math.floor((tabWidth - #tab.name) / 2)
|
|
term.write(string.rep(" ", padding) .. tab.name .. string.rep(" ", tabWidth - padding - #tab.name))
|
|
end
|
|
end
|
|
|
|
-- Draw overview mode
|
|
local function drawOverview()
|
|
term.setBackgroundColor(colors.black)
|
|
local y = 4
|
|
|
|
term.setTextColor(colors.lime)
|
|
term.setCursorPos(2, y)
|
|
term.write("Online: " .. #turtles)
|
|
y = y + 1
|
|
|
|
if #turtles == 0 then
|
|
term.setTextColor(colors.gray)
|
|
term.setCursorPos(2, y + 1)
|
|
term.write("No turtles")
|
|
else
|
|
for i, turtle in ipairs(turtles) do
|
|
if y >= h - 3 then break end
|
|
|
|
term.setCursorPos(2, y)
|
|
term.setTextColor(colors.yellow)
|
|
term.write("#" .. turtle.turtleID)
|
|
|
|
term.setTextColor(colors.lightGray)
|
|
term.setCursorPos(8, y)
|
|
local stateText = (turtle.mode or "idle"):sub(1, 8)
|
|
term.write(stateText)
|
|
|
|
if turtle.position then
|
|
term.setCursorPos(17, y)
|
|
term.setTextColor(colors.gray)
|
|
term.write(string.format("%d,%d,%d", turtle.position.x, turtle.position.y, turtle.position.z))
|
|
end
|
|
|
|
y = y + 1
|
|
end
|
|
end
|
|
|
|
-- Add select button
|
|
local btnY = h - 1
|
|
addButton(2, btnY, 10, 1, "SELECT", function()
|
|
if #turtles > 0 then
|
|
selectedTurtle = (selectedTurtle or 0) % #turtles + 1
|
|
addLog("Selected #" .. turtles[selectedTurtle].turtleID, colors.lime)
|
|
end
|
|
end, colors.green)
|
|
|
|
drawButton(buttons[#buttons], false)
|
|
end
|
|
|
|
-- Draw control mode
|
|
local function drawControl()
|
|
term.setBackgroundColor(colors.black)
|
|
local y = 4
|
|
|
|
if not selectedTurtle or not turtles[selectedTurtle] then
|
|
term.setTextColor(colors.red)
|
|
term.setCursorPos(2, y)
|
|
term.write("No turtle selected")
|
|
term.setCursorPos(2, y + 1)
|
|
term.setTextColor(colors.gray)
|
|
term.write("SELECT from TURTLES")
|
|
return
|
|
end
|
|
|
|
local turtle = turtles[selectedTurtle]
|
|
term.setTextColor(colors.yellow)
|
|
term.setCursorPos(2, y)
|
|
term.write("Turtle #" .. turtle.turtleID)
|
|
y = y + 1
|
|
|
|
-- Movement buttons (compact layout)
|
|
local btnW = 4
|
|
local btnH = 1
|
|
local centerX = math.floor(w / 2) - 2
|
|
local centerY = 8
|
|
|
|
-- Movement controls
|
|
addButton(centerX, centerY - 2, btnW, btnH, "FWD", function()
|
|
sendCommand(turtle.turtleID, "forward")
|
|
end, colors.blue)
|
|
|
|
addButton(centerX, centerY + 2, btnW, btnH, "BCK", function()
|
|
sendCommand(turtle.turtleID, "back")
|
|
end, colors.blue)
|
|
|
|
addButton(centerX - btnW - 1, centerY, btnW, btnH, "LFT", function()
|
|
sendCommand(turtle.turtleID, "turnLeft")
|
|
end, colors.blue)
|
|
|
|
addButton(centerX + btnW + 1, centerY, btnW, btnH, "RGT", function()
|
|
sendCommand(turtle.turtleID, "turnRight")
|
|
end, colors.blue)
|
|
|
|
addButton(centerX, centerY, btnW, btnH, "UP", function()
|
|
sendCommand(turtle.turtleID, "up")
|
|
end, colors.green)
|
|
|
|
-- Action buttons (bottom rows)
|
|
local btnY = h - 5
|
|
addButton(2, btnY, 6, 1, "EXPLR", function()
|
|
sendCommand(turtle.turtleID, "explore")
|
|
end, colors.cyan)
|
|
|
|
addButton(9, btnY, 6, 1, "MINE", function()
|
|
sendCommand(turtle.turtleID, "mine")
|
|
end, colors.orange)
|
|
|
|
addButton(16, btnY, 6, 1, "HOME", function()
|
|
sendCommand(turtle.turtleID, "returnHome")
|
|
end, colors.yellow)
|
|
|
|
addButton(23, btnY, 6, 1, "STOP", function()
|
|
sendCommand(turtle.turtleID, "stop")
|
|
end, colors.red)
|
|
|
|
btnY = h - 3
|
|
addButton(2, btnY, 6, 1, "DOWN", function()
|
|
sendCommand(turtle.turtleID, "down")
|
|
end, colors.green)
|
|
|
|
addButton(9, btnY, 6, 1, "DIG", function()
|
|
sendCommand(turtle.turtleID, "dig")
|
|
end, colors.red)
|
|
|
|
for i = #buttons - 8, #buttons do
|
|
if buttons[i] then
|
|
drawButton(buttons[i], false)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draw GPS mode
|
|
local function drawGPS()
|
|
term.setBackgroundColor(colors.black)
|
|
local y = 4
|
|
|
|
term.setTextColor(colors.lime)
|
|
term.setCursorPos(2, y)
|
|
term.write("GPS Tracking")
|
|
y = y + 1
|
|
|
|
-- My position (compact)
|
|
term.setTextColor(colors.white)
|
|
term.setCursorPos(2, y)
|
|
if myPosition then
|
|
term.write(string.format("You: %d,%d,%d", myPosition.x, myPosition.y, myPosition.z))
|
|
else
|
|
term.setTextColor(colors.red)
|
|
term.write("GPS: Not available")
|
|
end
|
|
y = y + 2
|
|
|
|
-- Distance to selected turtle
|
|
if selectedTurtle and turtles[selectedTurtle] and myPosition then
|
|
local turtle = turtles[selectedTurtle]
|
|
if turtle.position then
|
|
local dx = turtle.position.x - myPosition.x
|
|
local dy = turtle.position.y - myPosition.y
|
|
local dz = turtle.position.z - myPosition.z
|
|
local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
|
|
|
|
term.setTextColor(colors.cyan)
|
|
term.setCursorPos(2, y)
|
|
term.write(string.format("Turtle #%d:", turtle.turtleID))
|
|
y = y + 1
|
|
term.setTextColor(colors.white)
|
|
term.setCursorPos(2, y)
|
|
term.write(string.format("Dist: %.1f blocks", dist))
|
|
y = y + 1
|
|
term.setTextColor(colors.gray)
|
|
term.setCursorPos(2, y)
|
|
term.write(string.format("Delta: %d,%d,%d", dx, dy, dz))
|
|
end
|
|
end
|
|
|
|
y = h - 2
|
|
addButton(2, y, 13, 1, "UPDATE GPS", function()
|
|
if updateMyPosition() then
|
|
addLog("GPS updated", colors.lime)
|
|
else
|
|
addLog("GPS failed", colors.red)
|
|
end
|
|
end, colors.green)
|
|
|
|
addButton(16, y, 13, 1, "GOTO TURTLE", function()
|
|
if selectedTurtle and turtles[selectedTurtle] and turtles[selectedTurtle].position then
|
|
local pos = turtles[selectedTurtle].position
|
|
addLog(string.format("At: %d,%d,%d", pos.x, pos.y, pos.z), colors.cyan)
|
|
end
|
|
end, colors.blue)
|
|
|
|
drawButton(buttons[#buttons-1], false)
|
|
drawButton(buttons[#buttons], false)
|
|
end
|
|
|
|
-- Draw webbridge mode
|
|
local function drawWebbridge()
|
|
term.setBackgroundColor(colors.black)
|
|
local y = 4
|
|
|
|
term.setTextColor(colors.lime)
|
|
term.setCursorPos(2, y)
|
|
term.write("Webbridge Control")
|
|
y = y + 1
|
|
|
|
if webbridgeStatus then
|
|
term.setTextColor(colors.white)
|
|
term.setCursorPos(2, y)
|
|
term.write("Status: ONLINE")
|
|
y = y + 1
|
|
term.setCursorPos(2, y)
|
|
term.write("MSG:" .. (webbridgeStatus.messages or 0) ..
|
|
" CMD:" .. (webbridgeStatus.commands or 0))
|
|
else
|
|
term.setTextColor(colors.gray)
|
|
term.setCursorPos(2, y)
|
|
term.write("Status: Unknown")
|
|
end
|
|
|
|
y = y + 2
|
|
|
|
-- Compact button layout
|
|
addButton(2, y, 13, 1, "PING", function()
|
|
sendWebbridgeCommand("ping")
|
|
end, colors.blue)
|
|
|
|
addButton(16, y, 13, 1, "RESTART", function()
|
|
sendWebbridgeCommand("restart")
|
|
end, colors.orange)
|
|
|
|
y = y + 2
|
|
addButton(2, y, 13, 1, "STATUS", function()
|
|
sendWebbridgeCommand("status")
|
|
end, colors.green)
|
|
|
|
addButton(16, y, 13, 1, "LOGS", function()
|
|
sendWebbridgeCommand("logs")
|
|
end, colors.cyan)
|
|
|
|
for i = #buttons - 3, #buttons do
|
|
if buttons[i] then
|
|
drawButton(buttons[i], false)
|
|
end
|
|
end
|
|
|
|
-- Show recent logs (compact)
|
|
y = y + 3
|
|
term.setTextColor(colors.gray)
|
|
term.setCursorPos(2, y)
|
|
term.write("Activity:")
|
|
y = y + 1
|
|
|
|
for i = 1, math.min(3, #logMessages) do
|
|
if y >= h - 1 then break end
|
|
local log = logMessages[i]
|
|
term.setTextColor(log.color)
|
|
term.setCursorPos(2, y)
|
|
local text = log.text
|
|
if #text > w - 3 then
|
|
text = text:sub(1, w - 6) .. "..."
|
|
end
|
|
term.write(text)
|
|
y = y + 1
|
|
end
|
|
end
|
|
|
|
-- Draw server mode
|
|
local function drawServer()
|
|
term.setBackgroundColor(colors.black)
|
|
local y = 4
|
|
|
|
term.setTextColor(colors.lime)
|
|
term.setCursorPos(2, y)
|
|
term.write("Server Interface")
|
|
y = y + 1
|
|
|
|
if serverStats then
|
|
term.setTextColor(colors.white)
|
|
term.setCursorPos(2, y)
|
|
term.write("Status: ONLINE")
|
|
y = y + 1
|
|
|
|
term.setCursorPos(2, y)
|
|
term.write("Turtles: " .. (serverStats.turtles or 0))
|
|
y = y + 1
|
|
term.setCursorPos(2, y)
|
|
term.write("Blocks: " .. (serverStats.blocks or 0))
|
|
else
|
|
term.setTextColor(colors.red)
|
|
term.setCursorPos(2, y)
|
|
term.write("Status: OFFLINE")
|
|
end
|
|
|
|
y = h - 2
|
|
|
|
addButton(2, y, 13, 1, "REFRESH", function()
|
|
fetchServerStats()
|
|
addLog("Stats refreshed", colors.lime)
|
|
end, colors.green)
|
|
|
|
addButton(16, y, 13, 1, "WEB INFO", function()
|
|
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
|
|
type = "web_interface_request",
|
|
from = os.getComputerID()
|
|
})
|
|
addLog("Requesting info", colors.cyan)
|
|
end, colors.blue)
|
|
|
|
drawButton(buttons[#buttons-1], false)
|
|
drawButton(buttons[#buttons], false)
|
|
end
|
|
|
|
-- Main draw function
|
|
local function draw()
|
|
term.setBackgroundColor(colors.black)
|
|
term.clear()
|
|
|
|
drawHeader()
|
|
drawTabs()
|
|
|
|
if mode == "overview" then
|
|
drawOverview()
|
|
elseif mode == "control" then
|
|
drawControl()
|
|
elseif mode == "gps" then
|
|
drawGPS()
|
|
elseif mode == "webbridge" then
|
|
drawWebbridge()
|
|
elseif mode == "server" then
|
|
drawServer()
|
|
end
|
|
|
|
-- Status bar (compact)
|
|
term.setBackgroundColor(colors.gray)
|
|
term.setCursorPos(1, h)
|
|
term.clearLine()
|
|
term.setTextColor(colors.white)
|
|
term.setCursorPos(2, h)
|
|
term.write(string.format("T:%d", #turtles))
|
|
if selectedTurtle and turtles[selectedTurtle] then
|
|
term.setCursorPos(8, h)
|
|
term.write("#" .. turtles[selectedTurtle].turtleID)
|
|
end
|
|
if myPosition then
|
|
term.setCursorPos(w - 5, h)
|
|
term.write("GPS")
|
|
end
|
|
end
|
|
|
|
-- Main loop
|
|
print("Pocket Control Center")
|
|
print("Initializing...")
|
|
|
|
-- Initial GPS update
|
|
updateMyPosition()
|
|
|
|
-- Start background tasks
|
|
parallel.waitForAny(
|
|
function()
|
|
-- GPS update loop
|
|
while true do
|
|
sleep(2)
|
|
updateMyPosition()
|
|
end
|
|
end,
|
|
|
|
function()
|
|
-- Server stats update loop
|
|
while true do
|
|
sleep(10)
|
|
fetchServerStats()
|
|
end
|
|
end,
|
|
|
|
function()
|
|
-- Message listener
|
|
while true do
|
|
local event, side, channel, replyChannel, message = os.pullEvent("modem_message")
|
|
|
|
if channel == STATUS_CHANNEL and type(message) == "table" then
|
|
if message.type == "status" then
|
|
-- Update turtle list
|
|
local found = false
|
|
for i, t in ipairs(turtles) do
|
|
if t.turtleID == message.turtleID then
|
|
turtles[i] = message
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
if not found then
|
|
table.insert(turtles, message)
|
|
addLog("Turtle #" .. message.turtleID .. " connected", colors.lime)
|
|
end
|
|
end
|
|
elseif channel == POCKET_CHANNEL and type(message) == "table" then
|
|
-- Handle responses from webbridge
|
|
if message.type == "webbridge_status" then
|
|
webbridgeStatus = message.data
|
|
addLog("Webbridge status received", colors.cyan)
|
|
elseif message.type == "server_stats" then
|
|
serverStats = message.data
|
|
addLog("Server stats received", colors.lime)
|
|
elseif message.type == "webbridge_log" then
|
|
addLog("[WB] " .. (message.text or ""), colors.lightGray)
|
|
elseif message.type == "command_ack" then
|
|
addLog("Command acknowledged", colors.green)
|
|
elseif message.type == "error" then
|
|
addLog("Error: " .. (message.error or "unknown"), colors.red)
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
|
|
function()
|
|
-- UI loop
|
|
draw()
|
|
while true do
|
|
local event, p1, p2, p3 = os.pullEvent()
|
|
|
|
if event == "mouse_click" or event == "mouse_up" then
|
|
local btn = checkButton(p2, p3)
|
|
if btn and btn.action then
|
|
btn.action()
|
|
end
|
|
draw()
|
|
elseif event == "char" then
|
|
if p1 == "q" then
|
|
break
|
|
elseif p1 == "r" then
|
|
draw()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
term.setBackgroundColor(colors.black)
|
|
term.setTextColor(colors.white)
|
|
term.clear()
|
|
term.setCursorPos(1, 1)
|
|
print("Pocket Control Center closed")
|