-- controller.lua
-- multi-reactor monitor/controller for rnc3 + fission_agent
-- listens on BOTH controller and agent ports
-- logs to debug.log
os.loadAPI("rnc3")
-- config ---------------------------------------------------
local CTRL_PROG = "fission-controller"
local AGENT_PROG = "fission-agent" -- this must match the program name in the agent
local DISCOVER_EVERY = 3
local REDRAW_EVERY = 0.4
local OFFLINE_AFTER = 10
-- ids / ports ----------------------------------------------
local CTRL_ID = rnc3.getId(CTRL_PROG)
local CTRL_PORT = rnc3.getPort(CTRL_PROG)
local AGENT_PORT = rnc3.getPort(AGENT_PROG)
-- logging --------------------------------------------------
local function log(msg)
local h = fs.open("debug.log", "a")
h.writeLine(("[%d] %s"):format(os.epoch("utc"), msg))
h.close()
end
log("=== controller start ===")
log("CTRL_ID=" .. CTRL_ID)
log("CTRL_PORT=" .. tostring(CTRL_PORT))
log("AGENT_PORT=" .. tostring(AGENT_PORT))
-- open BOTH ports on all modems (rnc3 already opened CTRL_PORT, but we force AGENT_PORT too)
do
local ms = { peripheral.find("modem") }
for _, m in ipairs(ms) do
if not m.isOpen(AGENT_PORT) then
m.open(AGENT_PORT)
log("opened AGENT_PORT on modem " .. peripheral.getName(m))
end
if not m.isOpen(CTRL_PORT) then
m.open(CTRL_PORT)
log("opened CTRL_PORT on modem " .. peripheral.getName(m))
end
end
end
-- monitor --------------------------------------------------
local mon = peripheral.find("monitor")
if not mon then
log("ERROR: no monitor attached")
error("no monitor attached")
end
mon.setTextScale(1)
mon.setBackgroundColor(colors.black)
mon.setTextColor(colors.white)
mon.clear()
-- state: reactors[id] = {id=..., telem=..., lastSeen=..., row=...}
local reactors = {}
local lastDiscover = 0
local lastRedraw = 0
-- helpers --------------------------------------------------
local function shortId(id, max)
max = max or 12
if #id <= max then return id end
return id:sub(1, max-3) .. "..."
end
local function draw()
local w, h = mon.getSize()
mon.setBackgroundColor(colors.black)
mon.setTextColor(colors.white)
mon.clear()
mon.setCursorPos(1,1)
mon.write("FISSION CTRL")
local list = {}
for _, r in pairs(reactors) do
table.insert(list, r)
end
table.sort(list, function(a,b) return a.id < b.id end)
local row = 2
for _, r in ipairs(list) do
if row > h then break end
local online = (os.clock() - r.lastSeen) < OFFLINE_AFTER
local t = r.telem or {}
local temp = t.temp or 0
local burn = t.burn or 0
local status = t.status or "?"
r.row = row
mon.setCursorPos(1, row)
if online then mon.setTextColor(colors.white) else mon.setTextColor(colors.gray) end
local isStopped = (status == "stopped" or status == "not ready")
local btnText = isStopped and "[ON]" or "[OFF]"
local idPart = shortId(r.id, math.floor(w * 0.35))
local tempPart = (" T:%d"):format(temp)
local burnPart = (" B:%d"):format(burn)
local text = idPart .. tempPart .. burnPart
local maxText = w - #btnText
if #text > maxText then
text = text:sub(1, maxText)
end
mon.write(text)
local btnX = w - #btnText + 1
if btnX < 1 then btnX = 1 end
mon.setCursorPos(btnX, row)
if isStopped then mon.setTextColor(colors.lime) else mon.setTextColor(colors.red) end
mon.write(btnText)
row = row + 1
end
end
local function reactorAtY(y)
for _, r in pairs(reactors) do
if r.row == y then return r end
end
return nil
end
local function sendDiscover()
-- send ON AGENT PORT (i.e. from controller prog, but to agents)
rnc3.send(CTRL_PROG, "*", {
type = "discover",
disableSoftScram = true
})
log("sent discover")
end
local function sendControllerHB()
for id, _ in pairs(reactors) do
rnc3.send(CTRL_PROG, id, {
type = "controller-hb",
disableSoftScram = true
})
end
end
-- packet handler -------------------------------------------
local function handlePacket(pkt, viaPort)
local b = pkt.body or {}
local t = b.type
log("got pkt from " .. tostring(pkt.from) .. " viaPort=" .. tostring(viaPort) .. " type=" .. tostring(t))
if t == "reactor-hello" or t == "hb" then
local rid = pkt.from
if not reactors[rid] then
reactors[rid] = { id = rid }
log("new reactor: " .. rid)
end
reactors[rid].lastSeen = os.clock()
reactors[rid].telem = b.telem or reactors[rid].telem
-- keep them warm with controller-hb
rnc3.send(CTRL_PROG, rid, {
type = "controller-hb",
disableSoftScram = true
})
end
end
-- main loop ------------------------------------------------
while true do
local now = os.clock()
if now - lastDiscover >= DISCOVER_EVERY then
sendDiscover()
lastDiscover = now
end
if now - lastRedraw >= REDRAW_EVERY then
draw()
lastRedraw = now
end
local ev, p1, p2, p3, p4, p5 = os.pullEvent()
if ev == "modem_message" then
local channel = p2
local data = p4
-- we only care if it's on OUR controller port OR on the agent port
if channel == CTRL_PORT or channel == AGENT_PORT then
log("modem_message on channel " .. channel .. ", len=" .. (data and #tostring(data) or 0))
local ok, pkt = pcall(textutils.unserialize, data)
if ok and type(pkt) == "table" then
-- make sure it's actually meant for us (to = * or to = our id)
local me = CTRL_ID
if (not pkt.to) or pkt.to == "*" or pkt.to == me then
handlePacket(pkt, channel)
else
log("pkt ignored: to=" .. tostring(pkt.to))
end
else
log("failed to unserialize modem message")
end
end
elseif ev == "monitor_touch" then
local side, x, y = p1, p2, p3
log(("monitor_touch x=%d y=%d"):format(x,y))
local r = reactorAtY(y)
if r then
local telem = r.telem or {}
local status = telem.status or "unknown"
if status == "stopped" or status == "not ready" then
log("sending ON to " .. r.id)
rnc3.send(CTRL_PROG, r.id, {
type = "cmd",
cmd = "on"
})
else
log("sending OFF to " .. r.id)
rnc3.send(CTRL_PROG, r.id, {
type = "cmd",
cmd = "off"
})
end
lastRedraw = 0
end
elseif ev == "terminate" then
log("terminated by user")
mon.clear()
mon.setCursorPos(1,1)
mon.write("CTRL stopped")
break
end
-- send controller-hb each loop (cheap)
sendControllerHB()
end