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