CCCPaste

fission_controller

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