-- miner_startup.lua — Flicker-free UI + debug + “mostly-stupid” miner
-- Axis-only mine vectors. No area/volume planning. Controller drives vectors.
-- Requires: CC:Tweaked turtle with wireless/Ender modem.
-------------------- CONFIG --------------------
local PROTOCOL = "minesys:v1"
-- Home / logistics (edit to your base)
local HOME = { x=-924, y=99, z=-1093 }
local SHAFT_XZ = { x=-924, z=-1092 }
local CHEST_FUEL = { x=-924, y=98, z=-1091 }
local CHEST_DUMP = { x=-924, y=99, z=-1091 }
-- Fuel policy
local FUEL_MIN_BUFFER = 200
local FUEL_PER_STEP = 1
local REFUEL_TARGET = 3000
-- Timing
local HEARTBEAT_SEC = 2.0
local UI_REFRESH = 0.5
------------------------------------------------
-- Peripherals
local modem = peripheral.find("modem", function(_,p) return p.isWireless() end)
assert(modem, "No wireless modem")
rednet.open(peripheral.getName(modem))
-- UI window (double-buffered, no flicker)
local root = term.current()
local W,H = root.getSize()
local ui = window.create(root, 1, 1, W, H, true)
ui.setCursorBlink(false)
local function uiBegin()
ui.setVisible(false)
ui.setBackgroundColor(colors.black)
ui.setTextColor(colors.white)
ui.clear()
ui.setCursorPos(1,1)
end
local function uiEnd() ui.setVisible(true) end
-- State
local BOOT = os.epoch("utc")
local function now() return os.epoch("utc") end
local function uptime()
local s = math.floor((now()-BOOT)/1000)
local h = math.floor(s/3600); s=s-3600*h
local m = math.floor(s/60); s=s-60*m
return string.format("%02d:%02d:%02d", h,m,s)
end
local S = {
status="idle", extra=nil, facing="N",
pos=nil, lastFix=nil, fuel=turtle.getFuelLevel(),
abort=false, stop=false, uiOn=true, lastAck=nil
}
-- GPS helpers
local function gpsPos()
local x,y,z = gps.locate(5)
if x then
S.pos = { x = math.floor(x+0.5), y = math.floor(y+0.5), z = math.floor(z+0.5) }
S.lastFix = now()
end
return x,y,z
end
local function fixAge()
if not S.lastFix then return nil end
return math.floor((now()-S.lastFix)/1000)
end
-- Inventory stats
local function invStats()
local empty, fuelAvail = 0, 0
for i=1,16 do
local cnt = turtle.getItemCount(i)
if cnt==0 then empty = empty + 1
else
turtle.select(i)
if turtle.refuel(0) then fuelAvail = fuelAvail + cnt end
end
end
turtle.select(1)
return { freeSlots=empty, freePct=(empty/16)*100, fuelAvail=fuelAvail }
end
-- Heartbeat
local function hb()
S.fuel = turtle.getFuelLevel()
if not S.pos then gpsPos() end
rednet.broadcast({
t=now(), op="status", id=os.getComputerID(), label=os.getComputerLabel(),
pos=S.pos, fuel=S.fuel, facing=S.facing, status=S.status,
extra={ kind=S.extra and S.extra.kind or nil, data=S.extra, gps={fixAge=fixAge()} },
inv=invStats()
}, PROTOCOL)
end
local function report(status, extra) S.status=status or S.status; S.extra=extra; hb() end
-- UI
local spinner = {"|","/","-","\\"}; local sp=1
local function drawUI()
if not S.uiOn then return end
uiBegin()
local lbl = os.getComputerLabel() or ("id"..os.getComputerID())
ui.write(("Miner %s [%s] %s"):format(lbl, spinner[sp], uptime()))
sp = (sp % #spinner) + 1
local pos = S.pos and ("("..S.pos.x..","..S.pos.y..","..S.pos.z..")") or "(?, ?, ?)"
local fage = fixAge() and (fixAge().."s") or "no-fix"
ui.setCursorPos(1,3); ui.write(("Pos: %s GPS: %s"):format(pos, fage))
ui.setCursorPos(1,4); ui.write(("Fuel: %s"):format(S.fuel or "?"))
local inv = invStats()
ui.setCursorPos(1,5); ui.write(("Inv: free %d/16 filled %d%% fuelItems %d")
:format(inv.freeSlots, 100-math.floor(inv.freePct+0.5), inv.fuelAvail))
ui.setCursorPos(1,6); ui.write(("Facing: %s Status: %s"):format(S.facing, S.status or "?"))
if S.extra and S.extra.kind=="work" and S.extra.total and S.extra.step then
local pct = math.floor(S.extra.step*100/S.extra.total+0.5)
ui.setCursorPos(1,8); ui.write(("Work: %d/%d (%d%%)"):format(S.extra.step, S.extra.total, pct))
elseif S.extra and S.extra.kind then
ui.setCursorPos(1,8); ui.write(("Extra: %s"):format(S.extra.kind))
end
ui.setCursorPos(1,10); ui.write(("Abort:%s Stop:%s"):format(tostring(S.abort), tostring(S.stop)))
local mname = peripheral.getName(modem)
ui.setCursorPos(1,12); ui.write(("Modem: %s Rednet: open"):format(mname))
if S.lastAck then ui.setCursorPos(1,13); ui.write(("Last ACK: %s"):format(S.lastAck)) end
uiEnd()
end
-- Motion primitives
local DIRS={N=0,E=1,S=2,W=3}
local function setFacing(dir)
if not DIRS[dir] then return false,"bad dir" end
local cur=DIRS[S.facing] or 0; local tgt=DIRS[dir]
local diff=(tgt-cur)%4
if diff==1 then turtle.turnRight()
elseif diff==2 then turtle.turnRight(); turtle.turnRight()
elseif diff==3 then turtle.turnLeft() end
S.facing=dir; report("idle",{kind="face",dir=dir}); return true
end
local function digForward() while turtle.detect() do turtle.dig(); sleep(0.05) end end
local function stepForward()
local tries=0
digForward()
while not turtle.forward() do
digForward(); tries=tries+1
if tries>60 then return false,"blocked" end
sleep(0.05)
end
return true
end
local function stepUp()
while turtle.detectUp() do turtle.digUp(); sleep(0.05) end
local tries=0
while not turtle.up() do
while turtle.detectUp() do turtle.digUp(); sleep(0.05) end
tries=tries+1; if tries>60 then return false,"blocked up" end
sleep(0.05)
end
return true
end
local function stepDown()
while turtle.detectDown() do turtle.digDown(); sleep(0.05) end
local tries=0
while not turtle.down() do
while turtle.detectDown() do turtle.digDown(); sleep(0.05) end
tries=tries+1; if tries>60 then return false,"blocked down" end
sleep(0.05)
end
return true
end
local function moveAxis(axis, n)
if n==0 then return true end
local dir, steps = nil, math.abs(n)
if axis=="x" then dir = n>0 and "E" or "W"
elseif axis=="z" then dir = n>0 and "S" or "N"
elseif axis=="y" then
for i=1,steps do
if S.abort then return false,"abort" end
report("moving",{kind="move",axis="y",step=i,total=steps})
local ok,err = (n>0) and stepUp() or stepDown()
if not ok then return false,err end
hb()
end
return true
else return false,"bad axis" end
assert(setFacing(dir))
for i=1,steps do
if S.abort then return false,"abort" end
report("moving",{kind="move",axis=axis,step=i,total=steps})
local ok,err = stepForward()
if not ok then return false,err end
hb()
end
return true
end
-- Distance/fuel estimates to get home
local function distToHome()
if not S.pos then return 999999 end
local x,y,z = S.pos.x,S.pos.y,S.pos.z
local d1 = math.abs(x-SHAFT_XZ.x) + math.abs(z-SHAFT_XZ.z)
local d2 = math.abs(HOME.y - y)
local d3 = math.abs(HOME.z - (-1092))
return d1 + d2 + d3
end
local function fuelNeededToHome() return distToHome()*FUEL_PER_STEP + FUEL_MIN_BUFFER end
-- Routing + base ops
local function gotoPos(tx,ty,tz, order)
order = order or {"y","x","z"}
if not S.pos then gpsPos() end
if not S.pos then return false,"no gps" end
local dx,dy,dz = tx - S.pos.x, ty - S.pos.y, tz - S.pos.z
local map={x=dx,y=dy,z=dz}
for _,ax in ipairs(order) do
local ok,err = moveAxis(ax, map[ax]); if not ok then return false,err end
end
return true
end
local function faceTo(target)
if not S.pos then return false,"no gps" end
local dx, dz = target.x - S.pos.x, target.z - S.pos.z
if math.abs(dx)>math.abs(dz) then return setFacing(dx>0 and "E" or "W") else return setFacing(dz>0 and "S" or "N") end
end
local function returnToBase()
report("returning",{kind="service",stage="to_shaft"}); assert(gotoPos(SHAFT_XZ.x, S.pos.y, SHAFT_XZ.z, {"x","z"}))
report("returning",{kind="service",stage="shaft"}); assert(gotoPos(SHAFT_XZ.x, HOME.y, SHAFT_XZ.z, {"y"}))
report("returning",{kind="service",stage="home"}); assert(gotoPos(HOME.x, HOME.y, HOME.z, {"z"}))
return true
end
local function dumpAll()
assert(gotoPos(CHEST_DUMP.x, CHEST_DUMP.y, CHEST_DUMP.z, {"x","y","z"}))
faceTo({x=CHEST_DUMP.x+1, z=CHEST_DUMP.z})
for i=1,16 do
local d=turtle.getItemDetail(i)
if d then
turtle.select(i)
if turtle.refuel(0) then else turtle.drop() end
end
end
turtle.select(1); return true
end
local function refuelFromChest()
assert(gotoPos(CHEST_FUEL.x, CHEST_FUEL.y, CHEST_FUEL.z, {"x","y","z"}))
faceTo({x=CHEST_FUEL.x+1, z=CHEST_FUEL.z})
local pulls=0
while turtle.getFuelLevel() < REFUEL_TARGET and pulls<64 do
if turtle.suck() then
for i=1,16 do turtle.select(i); if turtle.refuel(0) then turtle.refuel() end end
turtle.select(1)
else break end
pulls=pulls+1
end
return true
end
local function serviceIfNeeded()
local need = fuelNeededToHome()
if S.fuel < need then
report("service",{kind="low_fuel", need=need, have=S.fuel})
assert(returnToBase()); assert(dumpAll()); assert(refuelFromChest()); assert(gotoPos(HOME.x, HOME.y, HOME.z))
report("idle",{kind="serviced"}); return true
end
local inv = invStats()
if inv.freeSlots <= 1 then
report("service",{kind="dump"})
assert(returnToBase()); assert(dumpAll()); assert(refuelFromChest()); assert(gotoPos(HOME.x, HOME.y, HOME.z))
report("idle",{kind="serviced"}); return true
end
return false
end
-- Mining (single axis only)
local function mineVector(axis, len, stopOnAir, stopOnDirt)
len = tonumber(len) or 0
if len==0 then return true end
serviceIfNeeded()
if axis=="y" then
local steps=math.abs(len)
for i=1,steps do
if S.abort then return false,"abort" end
report("ACTIVE",{kind="work",step=i,total=steps})
local ok,err = (len>0) and stepUp() or stepDown()
if not ok then return false,err end
hb(); serviceIfNeeded()
end
report("idle"); return true
end
local dir = (axis=="x") and ((len>0) and "E" or "W") or (axis=="z") and ((len>0) and "S" or "N") or nil
if not dir then return false,"bad axis" end
assert(setFacing(dir))
local steps = math.abs(len)
for i=1,steps do
if S.abort then return false,"abort" end
if stopOnAir or stopOnDirt then
local ok, data = turtle.inspect()
if ok and stopOnDirt and data and data.name and data.name:find("dirt") then report("STOPPED",{kind="halt_dirt"}); return true end
if (not ok) and stopOnAir then report("STOPPED",{kind="halt_air"}); return true end
end
report("ACTIVE",{kind="work",step=i,total=steps})
digForward()
local ok,err = stepForward()
if not ok then return false,err end
hb(); serviceIfNeeded()
end
report("idle"); return true
end
-- Debug commands
local function dbg_echo(msg) return true, ("echo: "..tostring(msg)) end
local function dbg_uptime() return true, uptime() end
local function dbg_toggle_ui(on) S.uiOn = on~=false; return true, "ui:"..tostring(S.uiOn) end
local function dbg_where() return true, S.pos end
local function dbg_gps()
local x,y,z = gps.locate(3)
local ids = {rednet.lookup("gps")}
local n=0 for _ in pairs(ids) do n=n+1 end
return x~=nil, {x=x,y=y,z=z, fixAge=fixAge(), beacons=n}
end
local function dbg_net()
local list={}
for _,n in ipairs(peripheral.getNames()) do table.insert(list, n..":"..peripheral.getType(n)) end
return true, {periph=list}
end
-- Threads
local function uiThread() while true do drawUI(); sleep(UI_REFRESH) end end
local function hbThread() while true do sleep(HEARTBEAT_SEC); hb() end end
local function rxThread()
while true do
local sid,msg,proto = rednet.receive(PROTOCOL)
if proto==PROTOCOL and type(msg)=="table" and msg.op=="cmd" then
local ok,err=false,nil
if msg.act=="face" then ok,err=setFacing(msg.dir)
elseif msg.act=="up" then ok,err=moveAxis("y", math.abs(msg.n or 1))
elseif msg.act=="down" then ok,err=moveAxis("y", -math.abs(msg.n or 1))
elseif msg.act=="mine_vector" then ok,err=mineVector(msg.axis, msg.len, msg.stopOnAir, msg.stopOnDirt)
elseif msg.act=="return_service" then ok,err=returnToBase()
elseif msg.act=="dump" then ok,err=dumpAll()
elseif msg.act=="refuel" then ok,err=refuelFromChest()
elseif msg.act=="set_facing_tag" then S.facing=msg.dir or S.facing; ok=true
elseif msg.act=="set_pos" then if msg.x and msg.y and msg.z then S.pos={x=msg.x,y=msg.y,z=msg.z}; ok=true else err="bad pos" end
elseif msg.act=="abort" then S.abort=true; ok=true
elseif msg.act=="resume" then S.abort=false; ok=true
elseif msg.act=="stop" then S.stop=true; ok=true
-- DEBUG:
elseif msg.act=="dbg_echo" then ok,err=dbg_echo(msg.msg)
elseif msg.act=="dbg_net" then ok,err=dbg_net()
elseif msg.act=="dbg_gps" then ok,err=dbg_gps()
elseif msg.act=="dbg_uptime" then ok,err=dbg_uptime()
elseif msg.act=="dbg_toggle_ui" then ok,err=dbg_toggle_ui(msg.on)
elseif msg.act=="dbg_where" then ok,err=dbg_where()
else err="unknown act" end
S.lastAck = (ok and "ok") or ("err:"..tostring(err))
rednet.send(sid,{op="ack", ok=ok, err=err, ref=msg.ref}, PROTOCOL)
hb()
end
end
end
report("idle",{kind="boot"})
parallel.waitForAny(uiThread, hbThread, rxThread)
Comments