feat: Implement Pocket Control Center with turtle control, GPS tracking, and server management
This commit is contained in:
623
pocketcontrol.lua
Normal file
623
pocketcontrol.lua
Normal file
@@ -0,0 +1,623 @@
|
||||
-- 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(),
|
||||
position = myPosition,
|
||||
timestamp = os.epoch("utc")
|
||||
})
|
||||
return true
|
||||
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 CONTROL CENTER"
|
||||
term.setCursorPos(math.floor((w - #title) / 2) + 1, 1)
|
||||
term.write(title)
|
||||
|
||||
-- Show mode indicator
|
||||
term.setCursorPos(w - 10, 1)
|
||||
local modeText = mode:upper():sub(1, 10)
|
||||
term.write(modeText)
|
||||
end
|
||||
|
||||
-- Draw mode tabs
|
||||
local function drawTabs()
|
||||
clearButtons()
|
||||
local tabWidth = math.floor(w / 5)
|
||||
local y = 2
|
||||
|
||||
addButton(1, y, tabWidth, 1, "TURTLES", function() mode = "overview" end,
|
||||
mode == "overview" and colors.green or colors.gray)
|
||||
addButton(tabWidth + 1, y, tabWidth, 1, "CONTROL", function() mode = "control" end,
|
||||
mode == "control" and colors.green or colors.gray)
|
||||
addButton(tabWidth * 2 + 1, y, tabWidth, 1, "GPS", function() mode = "gps" end,
|
||||
mode == "gps" and colors.green or colors.gray)
|
||||
addButton(tabWidth * 3 + 1, y, tabWidth, 1, "BRIDGE", function() mode = "webbridge" end,
|
||||
mode == "webbridge" and colors.green or colors.gray)
|
||||
addButton(tabWidth * 4 + 1, y, w - tabWidth * 4, 1, "SERVER", function() mode = "server" end,
|
||||
mode == "server" and colors.green or colors.gray)
|
||||
|
||||
for _, btn in ipairs(buttons) do
|
||||
drawButton(btn, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw overview mode
|
||||
local function drawOverview()
|
||||
term.setBackgroundColor(colors.black)
|
||||
local y = 4
|
||||
|
||||
term.setTextColor(colors.white)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Turtles Online: " .. #turtles)
|
||||
y = y + 2
|
||||
|
||||
if #turtles == 0 then
|
||||
term.setTextColor(colors.gray)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("No turtles detected")
|
||||
else
|
||||
for i, turtle in ipairs(turtles) do
|
||||
if y >= h - 2 then break end
|
||||
|
||||
term.setCursorPos(2, y)
|
||||
term.setTextColor(colors.yellow)
|
||||
term.write(string.format("Turtle #%-3d", turtle.turtleID))
|
||||
|
||||
term.setTextColor(colors.lightGray)
|
||||
term.setCursorPos(15, y)
|
||||
term.write(turtle.mode or "idle")
|
||||
|
||||
if turtle.position then
|
||||
term.setCursorPos(25, 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 buttons
|
||||
local btnY = h - 1
|
||||
addButton(2, btnY, 12, 1, "SELECT", function()
|
||||
if #turtles > 0 then
|
||||
selectedTurtle = (selectedTurtle or 0) % #turtles + 1
|
||||
addLog("Selected Turtle #" .. 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("Go to TURTLES and SELECT")
|
||||
return
|
||||
end
|
||||
|
||||
local turtle = turtles[selectedTurtle]
|
||||
term.setTextColor(colors.yellow)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Controlling Turtle #" .. turtle.turtleID)
|
||||
y = y + 2
|
||||
|
||||
-- Movement buttons
|
||||
local btnSize = 5
|
||||
local centerX = math.floor(w / 2)
|
||||
local centerY = 10
|
||||
|
||||
addButton(centerX - btnSize / 2, centerY - btnSize - 1, btnSize, 2, "FWD", function()
|
||||
sendCommand(turtle.turtleID, "forward")
|
||||
end, colors.blue)
|
||||
|
||||
addButton(centerX - btnSize / 2, centerY + 2, btnSize, 2, "BACK", function()
|
||||
sendCommand(turtle.turtleID, "back")
|
||||
end, colors.blue)
|
||||
|
||||
addButton(centerX - btnSize - 2, centerY, btnSize, 2, "LEFT", function()
|
||||
sendCommand(turtle.turtleID, "turnLeft")
|
||||
end, colors.blue)
|
||||
|
||||
addButton(centerX + 2, centerY, btnSize, 2, "RIGHT", function()
|
||||
sendCommand(turtle.turtleID, "turnRight")
|
||||
end, colors.blue)
|
||||
|
||||
addButton(centerX - btnSize / 2, centerY - 1, btnSize, 2, "UP", function()
|
||||
sendCommand(turtle.turtleID, "up")
|
||||
end, colors.green)
|
||||
|
||||
addButton(2, centerY + 5, 7, 2, "DOWN", function()
|
||||
sendCommand(turtle.turtleID, "down")
|
||||
end, colors.green)
|
||||
|
||||
addButton(10, centerY + 5, 7, 2, "DIG", function()
|
||||
sendCommand(turtle.turtleID, "dig")
|
||||
end, colors.red)
|
||||
|
||||
addButton(18, centerY + 5, 7, 2, "MINE", function()
|
||||
sendCommand(turtle.turtleID, "mineStart")
|
||||
end, colors.orange)
|
||||
|
||||
addButton(26, centerY + 5, 7, 2, "HOME", function()
|
||||
sendCommand(turtle.turtleID, "returnHome")
|
||||
end, colors.yellow)
|
||||
|
||||
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.yellow)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("GPS TRACKING")
|
||||
y = y + 2
|
||||
|
||||
-- My position
|
||||
term.setTextColor(colors.lime)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Your Position:")
|
||||
y = y + 1
|
||||
|
||||
if myPosition then
|
||||
term.setTextColor(colors.white)
|
||||
term.setCursorPos(4, y)
|
||||
term.write(string.format("X: %d Y: %d Z: %d", myPosition.x, myPosition.y, myPosition.z))
|
||||
else
|
||||
term.setTextColor(colors.red)
|
||||
term.setCursorPos(4, y)
|
||||
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 distance:", turtle.turtleID))
|
||||
y = y + 1
|
||||
term.setTextColor(colors.white)
|
||||
term.setCursorPos(4, y)
|
||||
term.write(string.format("%.1f blocks", dist))
|
||||
y = y + 1
|
||||
term.setCursorPos(4, y)
|
||||
term.setTextColor(colors.gray)
|
||||
term.write(string.format("Delta: %d,%d,%d", dx, dy, dz))
|
||||
end
|
||||
end
|
||||
|
||||
y = y + 2
|
||||
addButton(2, y, 15, 2, "UPDATE GPS", function()
|
||||
term.setTextColor(colors.yellow)
|
||||
term.setCursorPos(2, h - 2)
|
||||
term.write("Locating...")
|
||||
if updateMyPosition() then
|
||||
addLog("GPS updated", colors.lime)
|
||||
else
|
||||
addLog("GPS failed", colors.red)
|
||||
end
|
||||
end, colors.green)
|
||||
|
||||
addButton(18, y, 15, 2, "GOTO TURTLE", function()
|
||||
if selectedTurtle and turtles[selectedTurtle] and turtles[selectedTurtle].position then
|
||||
local pos = turtles[selectedTurtle].position
|
||||
addLog(string.format("Turtle 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.yellow)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("WEBBRIDGE CONTROL")
|
||||
y = y + 2
|
||||
|
||||
if webbridgeStatus then
|
||||
term.setTextColor(colors.lime)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Status: ONLINE")
|
||||
y = y + 1
|
||||
term.setTextColor(colors.white)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Messages: " .. (webbridgeStatus.messages or 0))
|
||||
y = y + 1
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Commands: " .. (webbridgeStatus.commands or 0))
|
||||
else
|
||||
term.setTextColor(colors.gray)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Status: Unknown")
|
||||
end
|
||||
|
||||
y = y + 2
|
||||
|
||||
addButton(2, y, 15, 2, "PING", function()
|
||||
sendWebbridgeCommand("ping")
|
||||
end, colors.blue)
|
||||
|
||||
addButton(18, y, 15, 2, "RESTART", function()
|
||||
sendWebbridgeCommand("restart")
|
||||
end, colors.orange)
|
||||
|
||||
y = y + 3
|
||||
addButton(2, y, 15, 2, "GET STATUS", function()
|
||||
sendWebbridgeCommand("status")
|
||||
end, colors.green)
|
||||
|
||||
addButton(18, y, 15, 2, "VIEW 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
|
||||
y = y + 4
|
||||
term.setTextColor(colors.gray)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Recent Activity:")
|
||||
y = y + 1
|
||||
|
||||
for i = 1, math.min(5, #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.yellow)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("SERVER INTERFACE")
|
||||
y = y + 2
|
||||
|
||||
if serverStats then
|
||||
term.setTextColor(colors.lime)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Server: ONLINE")
|
||||
y = y + 1
|
||||
|
||||
term.setTextColor(colors.white)
|
||||
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))
|
||||
y = y + 1
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Uptime: " .. (serverStats.uptime or "?"))
|
||||
else
|
||||
term.setTextColor(colors.red)
|
||||
term.setCursorPos(2, y)
|
||||
term.write("Server: OFFLINE")
|
||||
end
|
||||
|
||||
y = y + 2
|
||||
|
||||
addButton(2, y, 15, 2, "REFRESH", function()
|
||||
term.setTextColor(colors.yellow)
|
||||
term.setCursorPos(2, h - 2)
|
||||
term.write("Fetching...")
|
||||
fetchServerStats()
|
||||
addLog("Server stats refreshed", colors.lime)
|
||||
end, colors.green)
|
||||
|
||||
addButton(18, y, 15, 2, "VIEW API", function()
|
||||
modem.transmit(POCKET_CHANNEL, CHANNEL_RECEIVE, {
|
||||
type = "web_interface_request",
|
||||
from = os.getComputerID()
|
||||
})
|
||||
addLog("Requesting web interface 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
|
||||
term.setBackgroundColor(colors.gray)
|
||||
term.setCursorPos(1, h)
|
||||
term.clearLine()
|
||||
term.setTextColor(colors.white)
|
||||
term.setCursorPos(2, h)
|
||||
term.write(string.format("Online:%d", #turtles))
|
||||
if selectedTurtle and turtles[selectedTurtle] then
|
||||
term.setCursorPos(15, h)
|
||||
term.write("T#" .. turtles[selectedTurtle].turtleID)
|
||||
end
|
||||
if myPosition then
|
||||
term.setCursorPos(w - 10, h)
|
||||
term.write("GPS:OK")
|
||||
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(5)
|
||||
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")
|
||||
Reference in New Issue
Block a user