CCCPaste

fission_agent

-- fission_agent.lua
-- per-reactor agent for Mekanism fission (CC:T)
-- needs: ldb2, rnc2
-- hard safety: SCRAM >= 1400 C, never start >= 1000 C

os.loadAPI("ldb2")
os.loadAPI("rnc2")

local PROG  = "fission-agent"
local MY_ID = rnc2.getId(PROG)
print("my id:", MY_ID)

-- timings
local HEARTBEAT_EVERY    = 1.0   -- seconds
local CONTROLLER_TIMEOUT = 5.0   -- seconds

-- temps (hard rules)
local HARD_SCRAM_TEMP  = 1400    -- >= this -> scram, unoverrideable
local SAFE_START_TEMP  = 1000    -- must be lower than this to activate/start

-- "soft" safety defaults (controller may disable these)
local SOFT_MIN_COOLANT = 5       -- %  if coolant below -> soft scram
local SOFT_MAX_WASTE   = 95      -- %  if waste above -> soft scram
local SOFT_MAX_HOT     = 95      -- %  if heated-coolant above -> soft scram

-- find the fission reactor logic adapter
local reactor = peripheral.find("fissionReactorLogicAdapter")
if not reactor then
  error("fission_agent: no fissionReactorLogicAdapter found")
end

-- state
local controllerId       = nil
local controllerLastSeen = 0
local softAutoScram      = true  -- controller can toggle this, but NOT the 1400C rule
local lastHeartbeat      = 0

-- helpers -------------------------------------------------

local function safe(fn, ...)
  if not fn then return nil end
  local ok, res = pcall(fn, ...)
  if ok then return res else return nil end
end

local function scram()
  -- fission has scram()
  if reactor.scram then
    reactor.scram()
  end
  -- also zero burn to be sure
  if reactor.setBurnRate then
    reactor.setBurnRate(0)
  end
end

local function canStartNow()
  local temp = safe(reactor.getTemperature) or 0
  return temp < SAFE_START_TEMP
end

local function start()
  if not canStartNow() then
    -- too hot, ignore start
    return false
  end
  if reactor.activate then
    reactor.activate()
  end
  return true
end

local function setBurn(b)
  b = tonumber(b) or 0
  if not canStartNow() then
    -- refuse to set high burn while hot, but do allow 0
    if b > 0 then
      return false, "too hot"
    end
  end
  local maxB = safe(reactor.getMaxBurnRate) or b
  if b > maxB then b = maxB end
  if b < 0 then b = 0 end
  if reactor.setBurnRate then
    reactor.setBurnRate(b)
  end
  return true, b
end

-- telemetry using ONLY your methods -----------------------
local function readTelemetry()
  local temp       = safe(reactor.getTemperature) or 0
  local burn       = safe(reactor.getBurnRate) or 0
  local actualBurn = safe(reactor.getActualBurnRate) or burn
  local coolPct    = safe(reactor.getCoolantFilledPercentage) or 0
  local hotPct     = safe(reactor.getHeatedCoolantFilledPercentage) or 0
  local fuelPct    = safe(reactor.getFuelFilledPercentage) or 0
  local wastePct   = safe(reactor.getWasteFilledPercentage) or 0
  local heating    = safe(reactor.getHeatingRate) or 0
  local status     = safe(reactor.getStatus) or "unknown"
  local formed     = safe(reactor.isFormed) or false
  local forced     = safe(reactor.isForceDisabled) or false

  return {
    id                   = MY_ID,
    temp                 = temp,
    burn                 = burn,
    actualBurn           = actualBurn,
    coolantPct           = coolPct,
    heatedCoolantPct     = hotPct,
    fuelPct              = fuelPct,
    wastePct             = wastePct,
    heatingRate          = heating,
    status               = status,
    formed               = formed,
    forceDisabled        = forced,
    softAutoScram        = softAutoScram,
    time                 = os.epoch("utc"),
  }
end

-- decide soft-unsafe from telemetry
local function isSoftUnsafe(telem)
  if not softAutoScram then return false end
  if telem.coolantPct and telem.coolantPct > 0 and telem.coolantPct < SOFT_MIN_COOLANT then
    return true
  end
  if telem.wastePct and telem.wastePct >= SOFT_MAX_WASTE then
    return true
  end
  if telem.heatedCoolantPct and telem.heatedCoolantPct >= SOFT_MAX_HOT then
    return true
  end
  return false
end

-- message handling -----------------------------------------
local function handleMessage(pkt)
  if rnc2.autoPong(PROG, pkt) then return end

  local b = pkt.body or {}
  local t = b.type

  -- controller discovery
  if t == "discover" or t == "controller-hello" then
    controllerId       = pkt.from
    controllerLastSeen = os.clock()
    if b.disableSoftScram ~= nil then
      softAutoScram = not b.disableSoftScram
    end
    -- answer
    rnc2.send(PROG, pkt.from, {
      type  = "reactor-hello",
      id    = MY_ID,
      role  = "fission-reactor",
      telem = readTelemetry(),
      ver   = "fission_agent/1.1"
    })
    return
  end

  -- controller heartbeat
  if t == "controller-hb" then
    controllerId       = pkt.from
    controllerLastSeen = os.clock()
    if b.disableSoftScram ~= nil then
      softAutoScram = not b.disableSoftScram
    end
    return
  end

  -- direct commands
  if t == "cmd" then
    controllerId       = pkt.from
    controllerLastSeen = os.clock()

    if b.cmd == "on" then
      local ok = start()
      rnc2.send(PROG, pkt.from, { type="ack", cmd="on", ok=ok, reason= ok and nil or "temp>=1000" })

    elseif b.cmd == "off" then
      scram()
      rnc2.send(PROG, pkt.from, { type="ack", cmd="off", ok=true })

    elseif b.cmd == "setBurn" and b.value then
      local ok, valOrReason = setBurn(b.value)
      if ok then
        rnc2.send(PROG, pkt.from, { type="ack", cmd="setBurn", ok=true, burn=valOrReason })
      else
        rnc2.send(PROG, pkt.from, { type="ack", cmd="setBurn", ok=false, reason=valOrReason })
      end

    elseif b.cmd == "setSoftScram" and b.value ~= nil then
      softAutoScram = b.value and true or false
      rnc2.send(PROG, pkt.from, { type="ack", cmd="setSoftScram", ok=true, softAutoScram=softAutoScram })
    end
  end
end

-- main loop ------------------------------------------------
print("fission_agent started, id=" .. MY_ID)

while true do
  local now = os.clock()

  -- controller timeout -> re-enable soft scram
  if controllerId and (now - controllerLastSeen) > CONTROLLER_TIMEOUT then
    controllerId  = nil
    softAutoScram = true
    print("controller lost -> soft auto-scram re-enabled")
  end

  -- get telemetry (once per tick)
  local telem = readTelemetry()

  -- HARD safety (cannot be disabled)
  if telem.temp and telem.temp >= HARD_SCRAM_TEMP then
    if telem.status ~= "stopped" then
      print(("HARD SCRAM at %.1f C"):format(telem.temp))
      scram()
    end
  else
    -- SOFT safety (controller can disable)
    if isSoftUnsafe(telem) then
      if telem.status ~= "stopped" then
        print("SOFT SCRAM (coolant/waste/hot)")
        scram()
      end
    end
  end

  -- Heartbeat (1/s)
  if (now - lastHeartbeat) >= HEARTBEAT_EVERY then
    lastHeartbeat = now
    local hb = {
      type  = "hb",
      id    = MY_ID,
      role  = "fission-reactor",
      telem = telem
    }
    if controllerId then
      rnc2.send(PROG, controllerId, hb)
    else
      rnc2.broadcast(PROG, hb)
    end
  end

  -- non-blocking recv
  local pkt = rnc2.recv(PROG, 0.2)
  if pkt then
    handleMessage(pkt)
  end
end