CCCPaste

reactor_control.lua

-- reactor_control.lua
-- Mekanism Fission SCADA controller for Mekanism fission reactors using ComputerCraft
-- Features:
--  - Auto-detect one or more reactor peripherals
--  - Monitor dashboard rendering on attached monitor peripheral
--  - File logging of events and periodic stats to `scada_log.txt`
--  - Network status replies over rednet (if a modem is present)
--  - Per-reactor control via peripheral methods or redstone output
--  - Alarm handling and safe shutdown

local config = {
  control_mode = "auto",    -- "auto", "peripheral" or "redstone"
  peripheral_sides = nil,    -- if provided, a table mapping reactor names to sides, or nil for auto-discovery
  redstone_side_default = "left",   -- default redstone side for reactors without a peripheral control
  poll_interval = 2,        -- seconds between polls
  start_below_temp = 800,   -- start when measured temperature falls below this
  stop_above_temp = 1200,   -- stop when temperature rises above this
  safe_shutdown_temp = 1400,-- emergency shutdown if temperature > this
  log_to_term = true,
  log_file = "scada_log.txt",
  max_history = 1000,
}

local function term_write(...)
  if config.log_to_term then print(...) end
end

-- Utility: safe pcall wrapper for peripheral methods
local function safe_call(fn, ...)
  if not fn then return false, "nofn" end
  local ok, res = pcall(fn, ...)
  if not ok then return false, res end
  return true, res
end

-- Discover monitor peripheral (first available)
local function find_monitor()
  for _, name in ipairs(peripheral.getNames()) do
    local ok, ptype = pcall(peripheral.getType, name)
    if ok and ptype == "monitor" then
      local mon = peripheral.wrap(name)
      return mon, name
    end
  end
  return nil
end

-- Discover modem for rednet
local function find_modem()
  for _, name in ipairs(peripheral.getNames()) do
    local ok, ptype = pcall(peripheral.getType, name)
    if ok and (ptype == "modem" or ptype == "wireless_modem") then
      local modem = peripheral.wrap(name)
      return modem, name
    end
  end
  return nil
end

-- Heuristics for reactor peripheral detection
local reactor_method_signatures = {"getTemperature","getHeat","getReactorTemperature","getCasingTemperature","getCoreTemperature","getFissionTemperature"}
local function looks_like_reactor(name)
  local methods = peripheral.getMethods(name) or {}
  local joined = table.concat(methods, " ")
  for _, sig in ipairs(reactor_method_signatures) do
    if joined:match(sig) then return true end
  end
  -- fallback: presence of keywords
  if joined:match("Fuel") or joined:match("Active") or joined:match("Heat") then return true end
  return false
end

-- Scan and build list of reactors
local function scan_reactors()
  local reactors = {}
  for _, name in ipairs(peripheral.getNames()) do
    if looks_like_reactor(name) then
      local ok, p = pcall(peripheral.wrap, name)
      if ok and p then
        table.insert(reactors, {name = name, periph = p, control = "peripheral"})
      end
    end
  end
  -- If none found and config allows redstone, create a pseudo-reactor that uses redstone side
  if #reactors == 0 and config.control_mode ~= "peripheral" then
    -- support a single redstone-controlled reactor
    table.insert(reactors, {name = "local_redstone", periph = nil, control = "redstone", redstone_side = config.redstone_side_default})
  end
  return reactors
end

-- Probe a reactor peripheral for commonly available stats
local read_attempt_names = {
  temperature = {"getTemperature","getHeat","getReactorTemperature","getCasingTemperature","getCoreTemperature","getFissionTemperature"},
  active = {"getActive","isActive","getStatus","isRunning","isOnline"},
  power = {"getEnergy","getEnergyStored","getGeneration","getOutput"},
  fuel = {"getFuelAmount","getFuelRemaining","getFuelPercent","getFuel"}
}

local function probe_reactor_stats(periph)
  local stats = {}
  if not periph then return stats end
  -- temperature
  for _, name in ipairs(read_attempt_names.temperature) do
    if periph[name] then
      local ok, val = safe_call(periph[name])
      if ok and type(val) == "number" then stats.temperature = val; stats.temp_method = name; break end
    end
  end
  -- active
  for _, name in ipairs(read_attempt_names.active) do
    if periph[name] then
      local ok, val = safe_call(periph[name])
      if ok then stats.active = val; stats.active_method = name; break end
    end
  end
  -- power
  for _, name in ipairs(read_attempt_names.power) do
    if periph[name] then
      local ok, val = safe_call(periph[name])
      if ok and type(val) == "number" then stats.power = val; stats.power_method = name; break end
    end
  end
  -- fuel
  for _, name in ipairs(read_attempt_names.fuel) do
    if periph[name] then
      local ok, val = safe_call(periph[name])
      if ok and (type(val) == "number" or type(val) == "table") then stats.fuel = val; stats.fuel_method = name; break end
    end
  end
  return stats
end

-- Try to set reactor active via peripheral
local function set_reactor_active_peripheral(periph, active)
  if not periph then return false end
  local candidates = {"setActive","setRunning","activate","deactivate","setOnline","setEnabled","enable","disable","setPower"}
  for _, name in ipairs(candidates) do
    if periph[name] then
      local ok, res = pcall(periph[name], active)
      if ok then return true end
    end
  end
  return false
end

-- Redstone control
local function set_reactor_active_redstone(side, active)
  pcall(redstone.setOutput, side, active)
  return true
end

-- Simple logger
local function write_log_line(line)
  local ok, f = pcall(fs.open, config.log_file, "a")
  if ok and f then
    f.writeLine(line)
    f.close()
  else
    -- fallback: try native io if available
    pcall(print, "LOG FAIL: " .. tostring(line))
  end
end

local function log(msg)
  local timestamp = os.date("%Y-%m-%d %H:%M:%S")
  local line = string.format("[%s] %s", timestamp, tostring(msg))
  term_write(line)
  write_log_line(line)
end

-- Render a dashboard on a monitor peripheral
local function render_dashboard(mon, reactors, statuses)
  if not mon then return end
  local w,h = mon.getSize()
  mon.setTextScale(1)
  mon.setBackgroundColor(colors.black)
  mon.clear()
  mon.setCursorPos(1,1)
  mon.setTextColor(colors.white)
  mon.write("Mekanism Fission SCADA")
  mon.setCursorPos(1,2)
  mon.write(string.rep("-", math.max(1,w)))

  local row = 3
  for i, r in ipairs(reactors) do
    local s = statuses[r.name] or {}
    local line = string.format("%d) %s | T:%s | A:%s | P:%s | F:%s",
      i,
      r.name,
      tostring(s.temperature or "?") ,
      tostring(s.active or "?") ,
      tostring(s.power or "?") ,
      tostring(s.fuel or "?"))
    if row <= h then
      mon.setCursorPos(1,row)
      if s.temperature and s.temperature >= config.safe_shutdown_temp then
        mon.setTextColor(colors.red)
      else
        mon.setTextColor(colors.white)
      end
      mon.write(" " .. string.sub(line,1,w-1))
    end
    row = row + 1
  end
  -- footer
  if row <= h then
    mon.setCursorPos(1,row)
    mon.setTextColor(colors.gray)
    mon.write("Poll: " .. tostring(config.poll_interval) .. "s   Reactors: " .. tostring(#reactors))
  end
end

-- Main control loop
local function main()
  log("SCADA starting...")
  local monitor, monName = find_monitor()
  if monitor then log("Monitor found: " .. monName) end
  local modem, modemName = find_modem()
  if modem then
    log("Modem found: " .. modemName)
    -- open rednet on the modem side if necessary
    local ok = pcall(rednet.open)
    if not ok then pcall(rednet.open, modemName) end
    -- rednet messages will be polled in the main loop
  end

  local reactors = scan_reactors()
  if #reactors == 0 then log("No reactors discovered") end

  local statuses = {}
  local history = {}

  while true do
    -- handle rednet messages (non-blocking)
    if modem and rednet then
      local sender, message = rednet.receive(0)
      if sender and message then
        if type(message) == "string" then
          if message:upper() == "STATUS" then
            rednet.send(sender, "SCADA: OK")
          elseif message:upper() == "PING" then
            rednet.send(sender, "PONG")
          else
            rednet.send(sender, "UNKNOWN")
          end
        end
      end
    end

    -- refresh reactor list periodically
    reactors = scan_reactors()

    for _, r in ipairs(reactors) do
      local s = {}
      if r.periph then
        s = probe_reactor_stats(r.periph)
      end
      -- attach control metadata
      if r.control == "redstone" then s.redstone_side = r.redstone_side or config.redstone_side_default end
      statuses[r.name] = s

      -- Take action based on temperature thresholds
      local temp = tonumber(s.temperature)
      local active = s.active
      if temp then
        if temp >= config.safe_shutdown_temp then
          log(string.format("EMERGENCY: %s temp=%s >= safe_shutdown=%s -> forcing shutdown", r.name, tostring(temp), tostring(config.safe_shutdown_temp)))
          if r.periph then
            set_reactor_active_peripheral(r.periph, false)
          else
            set_reactor_active_redstone(s.redstone_side or config.redstone_side_default, false)
          end
        elseif temp >= config.stop_above_temp and active then
          log(string.format("Stopping %s: temp=%s >= stop=%s", r.name, tostring(temp), tostring(config.stop_above_temp)))
          if r.periph then set_reactor_active_peripheral(r.periph, false) else set_reactor_active_redstone(s.redstone_side or config.redstone_side_default, false) end
        elseif temp <= config.start_below_temp and not active then
          log(string.format("Starting %s: temp=%s <= start=%s", r.name, tostring(temp), tostring(config.start_below_temp)))
          if r.periph then set_reactor_active_peripheral(r.periph, true) else set_reactor_active_redstone(s.redstone_side or config.redstone_side_default, true) end
        end
      end

      -- record history entry
      table.insert(history, {time = os.time(), reactor = r.name, stats = s})
      if #history > config.max_history then table.remove(history,1) end

      -- alarms: beep if dangerously hot
      if monitor and s.temperature and s.temperature >= config.safe_shutdown_temp then
        term_write("ALARM: " .. r.name .. " temperature: " .. tostring(s.temperature))
      end
    end

    -- render dashboard
    render_dashboard(monitor, reactors, statuses)

    -- periodic logging of all statuses
    for name, s in pairs(statuses) do
      local line = string.format("STAT %s T=%s A=%s P=%s F=%s", name, tostring(s.temperature or "?"), tostring(s.active or "?"), tostring(s.power or "?"), tostring(s.fuel or "?"))
      write_log_line(line)
    end

    os.sleep(config.poll_interval)
  end
end

-- Run main
local ok, err = pcall(main)
if not ok then
  log("Unhandled error: " .. tostring(err))
end