-- 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