-- controller_startup.lua
-- Two-display controller: LEFT 4x4 list + MAP on the other monitor.
-- Wireless Ender modem required. Supports pocket RPC and goto().
-- Map shows controller, GPS nodes, and turtles with high-contrast colors.
-- ================= CONFIG =================
local CONFIG = {
repl = false, -- set true to enable inline REPL
controllerPos = { x = -732, y = 66, z = -322 },
gpsNodes = {
{x=-715,y=68,z=-327},
{x=-732,y=67,z=-344},
{x=-762,y=69,z=-329},
{x=-745,y=66,z=-312},
{x=-725,y=72,z=-300},
},
map = {
origin = { x = -769, z = -351 }, -- top-left world X/Z at map cell (1,1)
scale = 1, -- blocks per pixel
follow = false,
}
}
-- ==========================================
local PROTOCOL = "minesys:v1" -- miners: status/acks/cmds
local CTL_PROTO = "minesys:ctl" -- controller RPC (goto, clear, etc.)
-- ---------- Peripherals ----------
local modem = peripheral.find("modem", function(_,p) return p.isWireless() end)
if not modem then error("No wireless modem") end
rednet.open(peripheral.getName(modem))
pcall(function() rednet.host(CTL_PROTO, "miner_controller") end)
-- find two monitors; use the 4x4 merged as LIST, the other as MAP
local mons = { peripheral.find("monitor") }
if #mons < 2 then error("Need two monitors attached") end
local function pickMonitors()
local listMon, mapMon
for _,m in ipairs(mons) do
m.setTextScale(0.5)
local w,h = m.getSize()
if w==4 and h==4 then listMon = m else mapMon = m end
end
if not listMon then
table.sort(mons, function(a,b) local aw,ah=a.getSize(); local bw,bh=b.getSize(); return aw*ah < bw*bh end)
listMon, mapMon = mons[1], mons[2]
end
return listMon, mapMon
end
local monList, monMap = pickMonitors()
-- ---------- State ----------
local function now() return os.epoch("utc") end
local turtles = {} -- id-> {id,label,pos,fuel,facing,status,freeSlots,freePct,tSeen}
local function count(t) local n=0 for _ in pairs(t) do n=n+1 end return n end
local function upsertStatus(m)
local key = m.id or m.label
if not key then return end
local t = turtles[key] or {}
t.id = m.id or t.id
t.label = m.label or t.label or ("t"..tostring(m.id or ""))
t.pos = m.pos or t.pos
t.fuel = m.fuel or t.fuel
t.facing = m.facing or t.facing
t.status = m.status or t.status or "?"
if m.extra and m.extra.inv then
t.freeSlots = m.extra.inv.freeSlots
t.freePct = m.extra.inv.freePct
end
t.tSeen = now()
turtles[key] = t
end
-- ---------- Commands to miners ----------
local function sendCmd(target, tbl)
tbl.op="cmd"; tbl.ref=math.random(1,1e9)
local tgtID=nil
for _,t in pairs(turtles) do
if tostring(t.id)==tostring(target) or t.label==target then tgtID=t.id break end
end
if not tgtID then return false,"unknown target" end
rednet.send(tgtID, tbl, PROTOCOL)
local tm=os.startTimer(3)
while true do
local ev,a,b,c = os.pullEvent()
if ev=="rednet_message" then
local sid,msg,proto=a,b,c
if proto==PROTOCOL and type(msg)=="table" and msg.op=="ack" and msg.ref==tbl.ref then
return msg.ok, msg.err
end
elseif ev=="timer" and a==tm then return false,"ack timeout" end
end
end
local function face(lbl,dir) return sendCmd(lbl,{act="face",dir=dir}) end
local function forward(lbl,n) return sendCmd(lbl,{act="forward",n=n}) end
local function up(lbl,n) return sendCmd(lbl,{act="up",n=n}) end
local function down(lbl,n) return sendCmd(lbl,{act="down",n=n}) end
local function clearArea(lbl,L,W,H,dir) return sendCmd(lbl,{act="clear_area",length=L,width=W,height=H,dir=dir}) end
local function setFacingTag(lbl,dir) return sendCmd(lbl,{act="set_facing_tag",dir=dir}) end
-- Move along one axis only
local function moveAxis(lbl, axis, delta)
if delta==0 then return true end
if axis=="x" then
if delta>0 then assert(face(lbl,"E")) else assert(face(lbl,"W")) end
return forward(lbl, math.abs(delta))
elseif axis=="z" then
if delta>0 then assert(face(lbl,"S")) else assert(face(lbl,"N")) end
return forward(lbl, math.abs(delta))
elseif axis=="y" then
if delta>0 then return up(lbl, math.abs(delta)) else return down(lbl, math.abs(-delta)) end
else
return false,"bad axis"
end
end
-- goto composed of axis-only moves in selected order
local function gotoXYZ(tgtLabel, tx, ty, tz, order)
local t = nil
for _,v in pairs(turtles) do if v.label==tgtLabel then t=v break end end
if not t or not t.pos then return false,"unknown turtle or position" end
local cx, cy, cz = t.pos.x, t.pos.y, t.pos.z
local dx, dy, dz = tx-cx, ty-cy, tz-cz
order = order or {"y","x","z"}
local map = {x=dx,y=dy,z=dz}
for _,ax in ipairs(order) do
local ok,err = moveAxis(tgtLabel, ax, map[ax])
if not ok then return false,err end
end
return true
end
-- ---------- LIST (left 4x4) ----------
local function drawList()
local m = monList
m.setBackgroundColor(colors.black); m.setTextColor(colors.white); m.clear()
m.setCursorPos(1,1); m.write("T:"..count(turtles))
-- Very compact 4x4: each turtle uses one row: Lbl St Fuel %Free
-- Position is shown briefly in row 4 as the most recent turtle seen.
local y = 2
local lastPosTxt = nil
for _,t in pairs(turtles) do
if y>3 then break end
m.setCursorPos(1,y)
local lab = (t.label or "?"):sub(1,4)
local st = (t.status or "?"):sub(1,4)
local fu = t.fuel and tostring(t.fuel) or "?"
local fp = t.freePct and string.format("%2d", math.floor(t.freePct+0.5)) or "--"
local line = string.format("%-4s%-4s", lab, st)
m.write(line)
local w,_ = m.getSize()
m.setCursorPos(w-3,y); m.write(string.format("%3s", fu)) -- fuel right
m.setCursorPos(w-1,y); m.write(fp) -- % free
if t.pos then lastPosTxt = ("x% d z% d"):format(t.pos.x, t.pos.z) end
y = y + 1
end
if lastPosTxt then
m.setCursorPos(1,4); m.write(lastPosTxt:sub(1,4)) -- hint of position due to tight space
end
end
-- ---------- MAP (top-down XZ) ----------
local function worldToMap(x,z)
local w,h = monMap.getSize()
local ox, oz = CONFIG.map.origin.x, CONFIG.map.origin.z
local s = CONFIG.map.scale
local col = math.floor((x - ox)/s) + 1
local row = math.floor((z - oz)/s) + 1
if col < 1 or row < 1 or col > w or row > h then return nil,nil end
return col,row
end
local function maybeFollow()
if not CONFIG.map.follow then return end
for _,t in pairs(turtles) do
if t.pos then
CONFIG.map.origin.x = t.pos.x - 10*CONFIG.map.scale
CONFIG.map.origin.z = t.pos.z - 6*CONFIG.map.scale
return
end
end
end
local function drawMap()
maybeFollow()
local m = monMap
m.setBackgroundColor(colors.black); m.clear()
local w,h = m.getSize()
-- High-contrast border and grid
m.setTextColor(colors.lightGray)
for x=1,w do m.setCursorPos(x,1); m.write("─"); m.setCursorPos(x,h); m.write("─") end
for y=1,h do m.setCursorPos(1,y); m.write("│"); m.setCursorPos(w,y); m.write("│") end
m.setCursorPos(1,1); m.write("┌"); m.setCursorPos(w,1); m.write("┐")
m.setCursorPos(1,h); m.write("└"); m.setCursorPos(w,h); m.write("┘")
-- faint grid points every 4 cells
for r=2,h-1 do
for c=2,w-1 do
if ((c-2) % 4 == 0) and ((r-2) % 4 == 0) then
m.setCursorPos(c,r); m.setTextColor(colors.gray); m.write(".")
end
end
end
-- Legend
m.setCursorPos(3,1); m.setTextColor(colors.white); m.write("MAP C/G/T")
-- Controller
local cx,cz = CONFIG.controllerPos.x, CONFIG.controllerPos.z
local mc, mr = worldToMap(cx, cz)
if mc then m.setCursorPos(mc,mr); m.setTextColor(colors.cyan); m.write("C") end
-- GPS nodes
for _,g in ipairs(CONFIG.gpsNodes) do
local gc,gr = worldToMap(g.x, g.z)
if gc then m.setCursorPos(gc,gr); m.setTextColor(colors.magenta); m.write("G") end
end
-- Turtles (lime good, yellow low space, red near full)
for _,t in pairs(turtles) do
if t.pos then
local tc,tr = worldToMap(t.pos.x, t.pos.z)
if tc then
local col = colors.lime
if t.freePct then
if t.freePct < 10 then col = colors.red
elseif t.freePct < 30 then col = colors.yellow
else col = colors.lime end
end
m.setCursorPos(tc,tr); m.setTextColor(col); m.write("T")
end
end
end
end
-- ---------- Controller RPC server (for pocket 'goto', etc.) ----------
local function ctlServer()
while true do
local sid, msg, proto = rednet.receive(CTL_PROTO)
if proto==CTL_PROTO and type(msg)=="table" and msg.op=="req" then
local ok, err
if msg.act=="goto" then
ok, err = gotoXYZ(msg.label, msg.x, msg.y, msg.z, msg.order)
elseif msg.act=="clear" then
ok, err = clearArea(msg.label, msg.L, msg.W, msg.H, msg.dir)
elseif msg.act=="face" then
ok, err = face(msg.label, msg.dir)
elseif msg.act=="fwd" then
ok, err = forward(msg.label, msg.n)
elseif msg.act=="up" then
ok, err = up(msg.label, msg.n)
elseif msg.act=="dn" then
ok, err = down(msg.label, msg.n)
else
ok, err = false, "unknown act"
end
rednet.send(sid, {op="resp", ref=msg.ref, ok=ok, err=err}, CTL_PROTO)
end
end
end
-- ---------- Networking + UI ----------
local function netUiThread()
while true do
local id,msg,proto = rednet.receive(PROTOCOL, 0.2)
if id and proto==PROTOCOL and type(msg)=="table" and msg.op=="status" then
upsertStatus(msg)
end
drawList()
drawMap()
end
end
-- Optional inline REPL for manual driving
local function replThread()
term.setCursorPos(1,1); print("Controller REPL. help for cmds.")
while true do
io.write("> ")
local line = read()
local a,b,c,d,e,f = line:match("^(%S+)%s*(%S*)%s*(%S*)%s*(%S*)%s*(%S*)%s*(%S*)")
if a=="help" then
print("fac <label> <N|E|S|W>")
print("fwd <label> <n>")
print("up <label> <n>")
print("dn <label> <n>")
print("clear <label> <L> <W> <H> <N|E|S|W>")
print("tagface <label> <N|E|S|W>")
print("goto <label> <x> <y> <z> [order yxz|xyz]")
elseif a=="fac" then print(face(b,c))
elseif a=="fwd" then print(forward(b,tonumber(c)))
elseif a=="up" then print(up(b,tonumber(c)))
elseif a=="dn" then print(down(b,tonumber(c)))
elseif a=="clear" then print(clearArea(b,tonumber(c),tonumber(d),tonumber(e),f))
elseif a=="tagface" then print(setFacingTag(b,c))
elseif a=="goto" then
local order = (f and #f==3) and {f:sub(1,1),f:sub(2,2),f:sub(3,3)} or nil
print(gotoXYZ(b, tonumber(c), tonumber(d), tonumber(e), order))
else print("unknown") end
end
end
if CONFIG.repl then
parallel.waitForAny(netUiThread, ctlServer, replThread)
else
parallel.waitForAny(netUiThread, ctlServer)
end
Comments