computercraft/de_react_auto
2025-12-09 23:25:34 +01:00

1268 lines
45 KiB
Lua

-- vi: ft=lua
-- ============================================================================
-- DRACONIC REACTOR CONTROLLER & OPTIMIZER
-- For ATM9 / Draconic Evolution
-- ============================================================================
-- ============================================================================
-- CONFIGURATION - EDIT THESE VALUES TO MATCH YOUR SETUP
-- ============================================================================
local CONFIG = {
-- =======================================================================
-- PERIPHERAL NAMES (REQUIRED)
-- =======================================================================
-- Run "peripherals" in the ComputerCraft terminal to find these names
-- All three must be set for the controller to work!
reactor_name = "draconic_reactor_0", -- The reactor itself
input_gate_name = "flow_gate_0", -- Flux gate feeding containment field
output_gate_name = "flow_gate_1", -- Flux gate for power output
monitor_name = "right", -- Monitor side or network name (optional)
-- =======================================================================
-- FAIL-SAFE: Hardcoded emergency field input (RF/t)
-- =======================================================================
-- This is used if we can't read reactor data - set HIGH ENOUGH to sustain field!
-- For a typical reactor, 5-50 million RF/t is safe for emergency containment
-- If your reactor is very large, increase this value!
emergency_field_input = 50000000, -- 50M RF/t fail-safe
-- =======================================================================
-- FIELD STRENGTH TARGET (this is the main optimization setting)
-- =======================================================================
-- CONSERVATIVE: 50% (0.50) - Very safe, lower efficiency
-- Pros: Large safety margin, slow temperature rise, forgiving
-- Cons: Less power output, uses more fuel per RF generated
--
-- BALANCED: 30% (0.30) - Good balance of safety and output
-- Pros: Decent output, still reasonably safe
-- Cons: Need to pay some attention
--
-- AGGRESSIVE: 15-20% (0.15-0.20) - Maximum efficiency
-- Pros: Maximum power output, best fuel efficiency
-- Cons: Less margin for error, requires fast computer response
--
-- EXTREME: 8-10% (0.08-0.10) - Danger zone, not recommended
-- Pros: Absolute maximum output
-- Cons: Very easy to explode, not worth the risk
-- =======================================================================
target_field_strength = 0.30,
-- Safety thresholds
min_field_strength = 0.20, -- Emergency shutdown below this (8%)
max_temperature = 8000, -- Emergency shutdown above this (Kelvin)
critical_temperature = 7500, -- Start reducing output at this temp
-- Saturation control for optimal fuel conversion
min_saturation = 0.20, -- Below this, reactor produces less power
target_saturation = 0.45, -- Target saturation for optimal output (lower = more power)
-- Fuel monitoring
fuel_warning_threshold = 0.85, -- Warn when fuel is 90% used
fuel_shutdown_threshold = 0.90, -- Auto-shutdown when fuel is 95% used
-- Update intervals (in seconds)
update_interval = 0.1, -- Main loop speed (faster = more responsive)
display_interval = 0.5, -- Monitor refresh rate
-- Field strength control
field_strength_tolerance = 0.01, -- Acceptable deviation from target (1%)
-- Startup settings
charge_field_target = 0.55, -- Charge field to this before starting (55%)
}
-- ============================================================================
-- STATE MACHINE STATES
-- ============================================================================
local STATES = {
OFFLINE = "OFFLINE", -- Reactor is off
CONNECTING = "CONNECTING", -- Finding peripherals
CHARGING = "CHARGING", -- Charging containment field
WARMING_UP = "WARMING UP", -- Initial temperature ramp
RUNNING = "RUNNING", -- Normal optimized operation
COOLING = "COOLING", -- Temperature too high, reducing output
EMERGENCY = "EMERGENCY", -- Emergency shutdown in progress
SHUTDOWN = "SHUTDOWN", -- Manual shutdown requested
ERROR = "ERROR", -- Peripheral error
}
-- ============================================================================
-- GLOBAL VARIABLES
-- ============================================================================
local state = STATES.OFFLINE
local reactor = nil
local inputGate = nil
local outputGate = nil
local monitor = nil
local lastDisplayUpdate = 0
local reactorInfo = {}
local shutdownRequested = false
local chargeRequested = false -- Track when user requests charging
local errorMessage = ""
local fieldErrors = {} -- Track specific field read errors
-- Retry rate limiting for peripheral reconnection
local lastPeripheralRetry = 0
local peripheralRetryDelay = 1 -- Start with 1 second, exponential backoff up to 30s
-- Flux gate rate limiting
local lastFluxUpdate = 0
local lastInputRate = -1
local lastOutputRate = -1
-- Fuel warning throttle
local lastFuelWarning = 0
-- ============================================================================
-- UTILITY FUNCTIONS
-- ============================================================================
local function formatNumber(num)
if num >= 1e12 then
return string.format("%.2fT", num / 1e12)
elseif num >= 1e9 then
return string.format("%.2fG", num / 1e9)
elseif num >= 1e6 then
return string.format("%.2fM", num / 1e6)
elseif num >= 1e3 then
return string.format("%.2fK", num / 1e3)
else
return string.format("%.2f", num)
end
end
local function formatPercent(decimal)
return string.format("%.2f%%", decimal * 100)
end
local function formatTemp(kelvin)
return string.format("%.0fK", kelvin)
end
local function clamp(value, min, max)
return math.max(min, math.min(max, value))
end
-- ============================================================================
-- PERIPHERAL DETECTION
-- ============================================================================
local function findPeripherals()
state = STATES.CONNECTING
errorMessage = ""
-- Validate configuration
if not CONFIG.reactor_name then
errorMessage = "CONFIG.reactor_name not set!"
state = STATES.ERROR
return false
end
if not CONFIG.input_gate_name then
errorMessage = "CONFIG.input_gate_name not set!"
state = STATES.ERROR
return false
end
if not CONFIG.output_gate_name then
errorMessage = "CONFIG.output_gate_name not set!"
state = STATES.ERROR
return false
end
-- Connect to reactor
reactor = peripheral.wrap(CONFIG.reactor_name)
if not reactor then
errorMessage = "Reactor not found: " .. CONFIG.reactor_name
state = STATES.ERROR
return false
end
-- Connect to input flux gate (feeds containment field)
inputGate = peripheral.wrap(CONFIG.input_gate_name)
if not inputGate then
errorMessage = "Input flux gate not found: " .. CONFIG.input_gate_name
state = STATES.ERROR
return false
end
-- Connect to output flux gate (power output)
outputGate = peripheral.wrap(CONFIG.output_gate_name)
if not outputGate then
errorMessage = "Output flux gate not found: " .. CONFIG.output_gate_name
state = STATES.ERROR
return false
end
-- Enable override mode on flux gates (required for setFlowOverride to work)
pcall(function() inputGate.setOverrideEnabled(true) end)
pcall(function() outputGate.setOverrideEnabled(true) end)
print("Flux gate overrides enabled")
-- Connect to monitor (optional)
monitor = peripheral.wrap(CONFIG.monitor_name)
if monitor then
monitor.setTextScale(0.5)
monitor.clear()
else
print("Warning: Monitor not found at '" .. CONFIG.monitor_name .. "', using terminal only")
end
return true
end
-- ============================================================================
-- REACTOR INFORMATION GATHERING
-- ============================================================================
local function updateReactorInfo()
if not reactor then return false end
-- Clear previous field errors
fieldErrors = {}
local success, err = pcall(function()
reactorInfo = reactor.getReactorInfo()
end)
if not success then
errorMessage = "Failed to read reactor: " .. tostring(err)
state = STATES.ERROR
return false
end
-- Validate critical fields and track errors
if not reactorInfo then
table.insert(fieldErrors, "reactorInfo is nil")
errorMessage = "Reactor returned no data"
state = STATES.ERROR
return false
end
-- Check each critical field
local criticalFields = {
"status", "temperature", "fieldStrength", "maxFieldStrength",
"fieldDrainRate", "energySaturation", "maxEnergySaturation",
"generationRate", "fuelConversion", "maxFuelConversion"
}
for _, field in ipairs(criticalFields) do
if reactorInfo[field] == nil then
table.insert(fieldErrors, field .. " = nil")
elseif type(reactorInfo[field]) == "number" and reactorInfo[field] ~= reactorInfo[field] then
-- NaN check (NaN ~= NaN is true)
table.insert(fieldErrors, field .. " = NaN")
end
end
-- Check for invalid numeric values
local numericFields = {
"temperature", "fieldStrength", "maxFieldStrength",
"fieldDrainRate", "energySaturation", "maxEnergySaturation",
"generationRate"
}
for _, field in ipairs(numericFields) do
local val = reactorInfo[field]
if val ~= nil and type(val) == "number" then
if val < 0 then
table.insert(fieldErrors, field .. " < 0 (" .. tostring(val) .. ")")
end
if val == math.huge or val == -math.huge then
table.insert(fieldErrors, field .. " = infinity")
end
end
end
-- -- Check for zero max values (would cause division by zero)
-- -- Only flag as error if reactor is not offline (these are expected to be 0 when cold)
-- local isOffline = reactorInfo.status == "cold" or reactorInfo.status == "invalid"
-- if not isOffline then
-- if reactorInfo.maxFieldStrength ~= nil and reactorInfo.maxFieldStrength <= 0 then
-- table.insert(fieldErrors, "maxFieldStrength <= 0")
-- end
-- if reactorInfo.maxEnergySaturation ~= nil and reactorInfo.maxEnergySaturation <= 0 then
-- table.insert(fieldErrors, "maxEnergySaturation <= 0")
-- end
-- end
-- Set error message if we have field errors
if #fieldErrors > 0 then
errorMessage = "Field errors: " .. #fieldErrors .. " issues"
else
errorMessage = ""
end
-- Check for critical field errors that require EMERGENCY state
-- These fields are essential for safe reactor control
local criticalForSafety = {"fieldStrength", "maxFieldStrength", "fieldDrainRate", "temperature"}
for _, field in ipairs(criticalForSafety) do
if reactorInfo[field] == nil then
errorMessage = "CRITICAL: " .. field .. " is nil!"
-- Only go to emergency if reactor is not offline
if reactorInfo.status ~= "cold" and reactorInfo.status ~= "invalid" then
state = STATES.EMERGENCY
return false
end
end
end
return true
end
-- Calculate required field input to maintain current field
local function calculateFieldInput()
-- SAFETY: If we can't read reactor data, return emergency high value
if not reactorInfo then
return CONFIG.emergency_field_input
end
-- SAFETY: If critical fields are missing, use emergency input
if not reactorInfo.fieldDrainRate or not reactorInfo.fieldStrength or not reactorInfo.maxFieldStrength then
return CONFIG.emergency_field_input
end
-- SAFETY: Prevent division by zero
if reactorInfo.maxFieldStrength <= 0 then
return CONFIG.emergency_field_input
end
local drainRate = reactorInfo.fieldDrainRate
local currentStrength = reactorInfo.fieldStrength / reactorInfo.maxFieldStrength
-- Use charge target during warmup/charging, operational target when running
local targetStrength = CONFIG.target_field_strength
if state == STATES.CHARGING or state == STATES.WARMING_UP then
targetStrength = CONFIG.charge_field_target
end
-- Base input = drain rate to maintain current level
local baseInput = drainRate
-- Add correction factor to move toward target
local strengthError = targetStrength - currentStrength
local correction = strengthError * reactorInfo.maxFieldStrength * 0.5
-- Total input needed
local totalInput = baseInput + correction
-- If field is above target, allow input to go very low (just above drain rate)
-- If field is below target, ensure minimum input to build field
local minimumInput
if currentStrength > targetStrength then
-- Above target - only maintain drain rate, let field drop
minimumInput = drainRate * 1.1
else
-- Below target - ensure some input even if drain is 0
minimumInput = math.max(drainRate * 1.1, 1000000)
end
local result = math.max(0, math.floor(math.max(minimumInput, totalInput)))
-- Verbose output
local fieldPct = string.format("%.1f%%", currentStrength * 100)
local targetPct = string.format("%.1f%%", targetStrength * 100)
if currentStrength < targetStrength - 0.01 then
print("[FIELD] " .. fieldPct .. " < " .. targetPct .. " target -> increasing input to " .. formatNumber(result) .. " RF/t (drain=" .. formatNumber(drainRate) .. ")")
elseif currentStrength > targetStrength + 0.01 then
print("[FIELD] " .. fieldPct .. " > " .. targetPct .. " target -> reducing input to " .. formatNumber(result) .. " RF/t (drain=" .. formatNumber(drainRate) .. ")")
else
print("[FIELD] " .. fieldPct .. " at target -> maintaining " .. formatNumber(result) .. " RF/t")
end
return result
end
-- Calculate safe output rate based on temperature and saturation
local function calculateOutputRate()
-- SAFETY: If we can't read reactor data, output nothing (conservative)
if not reactorInfo then return 0 end
-- SAFETY: If critical fields are missing, output nothing
if not reactorInfo.energySaturation or not reactorInfo.maxEnergySaturation then
return 0
end
-- SAFETY: Prevent division by zero
if reactorInfo.maxEnergySaturation <= 0 then
return 0
end
local temp = reactorInfo.temperature or 0
local saturation = reactorInfo.energySaturation / reactorInfo.maxEnergySaturation
local generationRate = reactorInfo.generationRate or 0
local result = 0
local reason = ""
-- If we're in emergency or cooling, limit output
if state == STATES.EMERGENCY then
result = 0
reason = "EMERGENCY state"
elseif state == STATES.COOLING then
result = math.floor(generationRate * 0.5)
reason = "COOLING - 50% of generation"
elseif state == STATES.CHARGING or state == STATES.WARMING_UP then
result = math.floor(generationRate * 0.8)
reason = state .. " - 80% of generation"
else
-- Normal operation with saturation targeting
local tempFactor = 1.0
local tempCritical = false
if temp > CONFIG.critical_temperature then
local overTemp = temp - CONFIG.critical_temperature
local tempRange = CONFIG.max_temperature - CONFIG.critical_temperature
tempFactor = 1.0 - (overTemp / tempRange) * 0.5
tempCritical = true
end
-- Saturation-based output adjustment
-- Target saturation for optimal fuel conversion (lower sat = more power)
local targetSat = CONFIG.target_saturation
local satError = saturation - targetSat -- positive = too high, negative = too low
-- Scale factor: adjust output by up to 50% based on saturation error
local satFactor = 1.0 + satError
satFactor = math.max(0.5, math.min(1.5, satFactor)) -- Clamp to 50%-150%
-- IMPORTANT: If temperature is critical, don't increase output to drain saturation
-- Temperature safety takes priority over saturation optimization
if tempCritical and satFactor > 1.0 then
satFactor = 1.0 -- Cap at 100%, don't try to drain saturation when hot
end
local outputRate = generationRate * tempFactor * satFactor
-- Build reason string
local satPct = string.format("%.0f%%", saturation * 100)
local targetPct = string.format("%.0f%%", targetSat * 100)
if tempCritical then
reason = "TEMP CRITICAL " .. formatTemp(temp) .. ", factor=" .. string.format("%.2f", tempFactor)
if satFactor < 1.0 then
reason = reason .. ", sat " .. satPct .. " low"
end
elseif satError > 0.05 then
reason = "sat " .. satPct .. " > " .. targetPct .. " target, +" .. string.format("%.0f%%", (satFactor - 1) * 100) .. " output"
elseif satError < -0.05 then
reason = "sat " .. satPct .. " < " .. targetPct .. " target, " .. string.format("%.0f%%", (satFactor - 1) * 100) .. " output"
else
reason = "sat " .. satPct .. " at target"
end
result = math.max(0, math.floor(outputRate))
end
if generationRate > 0 then
print("[OUTPUT] " .. formatNumber(result) .. " RF/t (" .. reason .. ")")
end
return result
end
-- ============================================================================
-- STATE MACHINE LOGIC
-- ============================================================================
local lastState = nil
local function setState(newState, reason)
if state ~= newState then
print("[STATE] " .. state .. " -> " .. newState .. " (" .. reason .. ")")
state = newState
end
end
local function processStateMachine()
if not reactorInfo or not reactorInfo.status then
-- SAFETY: Can't read reactor, go to emergency to maintain field
if state ~= STATES.OFFLINE and state ~= STATES.ERROR then
setState(STATES.EMERGENCY, "cannot read reactor data")
end
return
end
local reactorStatus = reactorInfo.status
local temp = reactorInfo.temperature or 0
-- SAFETY: Prevent division by zero
local maxField = reactorInfo.maxFieldStrength or 1
local maxSat = reactorInfo.maxEnergySaturation or 1
if maxField <= 0 then maxField = 1 end
if maxSat <= 0 then maxSat = 1 end
local fieldStrength = (reactorInfo.fieldStrength or 0) / maxField
local saturation = (reactorInfo.energySaturation or 0) / maxSat
local fieldPct = string.format("%.1f%%", fieldStrength * 100)
-- Check for emergency conditions first (always)
if reactorStatus ~= "cold" and reactorStatus ~= "cooling" then
if fieldStrength < CONFIG.min_field_strength then
setState(STATES.EMERGENCY, "field " .. fieldPct .. " below minimum " .. formatPercent(CONFIG.min_field_strength))
return
end
if temp > CONFIG.max_temperature then
setState(STATES.EMERGENCY, "temp " .. formatTemp(temp) .. " exceeds max " .. formatTemp(CONFIG.max_temperature))
return
end
end
-- Check fuel level
local maxFuel = reactorInfo.maxFuelConversion or 1
if maxFuel <= 0 then maxFuel = 1 end
local fuelUsed = (reactorInfo.fuelConversion or 0) / maxFuel
if fuelUsed >= CONFIG.fuel_shutdown_threshold then
-- Fuel nearly depleted - initiate shutdown
if state ~= STATES.SHUTDOWN and state ~= STATES.EMERGENCY then
print("[FUEL] CRITICAL: Fuel " .. formatPercent(fuelUsed) .. " depleted! Initiating shutdown...")
shutdownRequested = true
end
elseif fuelUsed >= CONFIG.fuel_warning_threshold then
-- Fuel low - warn user (throttle to every 5 seconds)
local now = os.clock()
if now - lastFuelWarning >= 5 then
lastFuelWarning = now
print("[FUEL] WARNING: Fuel " .. formatPercent(fuelUsed) .. " used - running low!")
end
end
-- Check for shutdown request
if shutdownRequested then
if reactorStatus == "cold" or reactorStatus == "cooling" then
setState(STATES.SHUTDOWN, "shutdown requested, reactor cooling")
shutdownRequested = false
elseif state ~= STATES.EMERGENCY then
setState(STATES.SHUTDOWN, "shutdown requested")
end
return
end
-- State transitions based on reactor status
if reactorStatus == "cold" or reactorStatus == "invalid" then
setState(STATES.OFFLINE, "reactor status: " .. reactorStatus)
return
end
-- Clear charge request flag once field is sufficiently charged
if chargeRequested then
if fieldStrength >= CONFIG.charge_field_target then
chargeRequested = false
print("[INFO] Field charged to " .. fieldPct .. ", charge request cleared")
end
end
if reactorStatus == "cooling" then
if state == STATES.EMERGENCY or state == STATES.SHUTDOWN then
-- Keep current state during cooldown
return
end
setState(STATES.COOLING, "reactor cooling down")
return
end
if reactorStatus == "warming_up" then
-- Use 0.9 multiplier for hysteresis (consistent with RUNNING state)
if fieldStrength < CONFIG.charge_field_target * 0.9 then
setState(STATES.CHARGING, "field " .. fieldPct .. " below charge target")
else
setState(STATES.WARMING_UP, "field " .. fieldPct .. " sufficient, warming up")
end
return
end
if reactorStatus == "running" then
local minThreshold = CONFIG.target_field_strength * 0.9 -- 90% of operational target (27% if target is 30%)
if fieldStrength < minThreshold then
setState(STATES.CHARGING, "field " .. fieldPct .. " < " .. formatPercent(minThreshold) .. " min threshold")
elseif temp > CONFIG.critical_temperature then
setState(STATES.COOLING, "temp " .. formatTemp(temp) .. " above critical " .. formatTemp(CONFIG.critical_temperature))
else
setState(STATES.RUNNING, "normal operation")
end
return
end
if reactorStatus == "stopping" then
setState(STATES.SHUTDOWN, "reactor stopping")
return
end
end
-- ============================================================================
-- REACTOR CONTROL
-- ============================================================================
local function controlReactor()
if not reactor or not inputGate or not outputGate then
return
end
if not reactorInfo or not reactorInfo.status then
return
end
local inputRate = 0
local outputRate = 0
if state == STATES.EMERGENCY then
-- EMERGENCY: Maximum field input, stop the reactor
-- SAFETY: Use 2x maxFieldStrength or our fail-safe constant, whichever is higher
local maxField = reactorInfo.maxFieldStrength or 0
inputRate = math.max(maxField * 2, CONFIG.emergency_field_input)
outputRate = 0
-- Try to stop the reactor
local stopSuccess, stopErr = pcall(function()
reactor.stopReactor()
end)
if not stopSuccess then
print("[WARNING] Failed to stop reactor: " .. tostring(stopErr))
end
elseif state == STATES.SHUTDOWN then
-- SHUTDOWN: Maintain field with safety margin, no output, let it cool
-- Use 1.5x normal input to account for increased drain during cooldown
inputRate = math.floor(calculateFieldInput() * 1.5)
outputRate = 0
-- Try to stop the reactor if running
if reactorInfo.status == "running" or reactorInfo.status == "warming_up" then
local stopSuccess, stopErr = pcall(function()
reactor.stopReactor()
end)
if not stopSuccess then
print("[WARNING] Failed to stop reactor: " .. tostring(stopErr))
end
end
elseif state == STATES.CHARGING then
-- CHARGING: High field input to charge up
local maxField = reactorInfo.maxFieldStrength or 0
if maxField > 0 then
-- Reactor has valid maxFieldStrength - calculate based on charge needed
local fieldStrength = (reactorInfo.fieldStrength or 0) / maxField
local chargeNeeded = CONFIG.charge_field_target - fieldStrength
-- Charge faster when further from target
inputRate = math.floor(maxField * math.max(0.1, chargeNeeded))
inputRate = math.max(inputRate, calculateFieldInput())
else
-- Reactor is cold/initializing - use emergency input to charge field
-- This provides enough power to establish the containment field
inputRate = CONFIG.emergency_field_input
end
outputRate = 0
elseif state == STATES.WARMING_UP then
-- WARMING UP: Maintain field, slowly ramp output
local maxField = reactorInfo.maxFieldStrength or 0
if maxField > 0 then
inputRate = calculateFieldInput()
else
-- maxFieldStrength not available yet - use emergency input
inputRate = CONFIG.emergency_field_input
end
outputRate = calculateOutputRate()
elseif state == STATES.RUNNING then
-- RUNNING: Normal optimized operation
inputRate = calculateFieldInput()
outputRate = calculateOutputRate()
elseif state == STATES.COOLING then
-- COOLING: Maintain field, reduce output
inputRate = calculateFieldInput()
outputRate = calculateOutputRate() -- Already reduced in the function
elseif state == STATES.ERROR then
-- ERROR: Peripheral issues - pump maximum into field to be safe!
inputRate = CONFIG.emergency_field_input
outputRate = 0
else
-- OFFLINE: Reactor is cold
if chargeRequested then
-- User requested charge - provide input flux to start charging the field
inputRate = CONFIG.emergency_field_input
outputRate = 0
print("[CHARGE] Providing " .. formatNumber(inputRate) .. " RF/t to charge field")
else
-- No action needed
inputRate = 0
outputRate = 0
end
end
-- Apply the flow rates with rate limiting
-- Emergency/Error/Charging states bypass rate limiting for safety
local now = os.clock()
local isUrgent = (state == STATES.EMERGENCY or state == STATES.ERROR or chargeRequested)
local ratesChanged = (inputRate ~= lastInputRate) or (outputRate ~= lastOutputRate)
local timeSinceUpdate = now - lastFluxUpdate
-- Update flux gates if: urgent, OR (rates changed AND enough time passed)
if isUrgent or (ratesChanged and timeSinceUpdate >= 0.5) then
lastFluxUpdate = now
lastInputRate = inputRate
lastOutputRate = outputRate
local inputSuccess = pcall(function()
inputGate.setFlowOverride(math.floor(inputRate))
end)
-- SAFETY: If we failed to set input gate, that's critical!
if not inputSuccess and state ~= STATES.OFFLINE then
errorMessage = "CRITICAL: Failed to set input flux gate!"
state = STATES.ERROR
end
local outputSuccess = pcall(function()
outputGate.setFlowOverride(math.floor(outputRate))
end)
-- Output gate failure is less critical but should be logged
if not outputSuccess and state ~= STATES.OFFLINE then
print("[WARNING] Failed to set output flux gate!")
end
end
end
-- ============================================================================
-- DISPLAY FUNCTIONS
-- ============================================================================
local function getStateColor(s)
local colors_map = {
[STATES.OFFLINE] = colors.gray,
[STATES.CONNECTING] = colors.yellow,
[STATES.CHARGING] = colors.blue,
[STATES.WARMING_UP] = colors.orange,
[STATES.RUNNING] = colors.lime,
[STATES.COOLING] = colors.cyan,
[STATES.EMERGENCY] = colors.red,
[STATES.SHUTDOWN] = colors.purple,
[STATES.ERROR] = colors.red,
}
return colors_map[s] or colors.white
end
local function drawProgressBar(mon, x, y, width, value, maxValue, barColor, bgColor)
local percent = clamp(value / maxValue, 0, 1)
local filledWidth = math.floor(width * percent)
mon.setCursorPos(x, y)
mon.setBackgroundColor(barColor)
mon.write(string.rep(" ", filledWidth))
mon.setBackgroundColor(bgColor or colors.gray)
mon.write(string.rep(" ", width - filledWidth))
mon.setBackgroundColor(colors.black)
end
local function updateDisplay()
if not monitor then
-- Fallback to terminal
term.clear()
term.setCursorPos(1, 1)
print("=== DRACONIC REACTOR CONTROLLER ===")
print("State: " .. state)
if reactorInfo and reactorInfo.status then
print("Reactor: " .. reactorInfo.status)
print("Temp: " .. formatTemp(reactorInfo.temperature or 0))
local fs = (reactorInfo.fieldStrength or 0) / (reactorInfo.maxFieldStrength or 1)
print("Field: " .. formatPercent(fs))
print("Gen: " .. formatNumber(reactorInfo.generationRate or 0) .. " RF/t")
end
if errorMessage ~= "" then
print("Error: " .. errorMessage)
end
-- Show field errors on terminal
if #fieldErrors > 0 then
print("")
print("--- FIELD READ ERRORS ---")
for i, err in ipairs(fieldErrors) do
print(" " .. i .. ". " .. err)
if i >= 5 then
print(" ... and " .. (#fieldErrors - 5) .. " more")
break
end
end
end
return
end
local mon = monitor
local w, h = mon.getSize()
mon.setBackgroundColor(colors.black)
mon.clear()
-- Header
mon.setCursorPos(1, 1)
mon.setTextColor(colors.cyan)
mon.write("=== DRACONIC REACTOR CONTROL ===")
-- State display (prominent)
mon.setCursorPos(1, 3)
mon.setTextColor(colors.white)
mon.write("OPTIMIZER STATE: ")
mon.setTextColor(getStateColor(state))
mon.write(state)
-- Error message if any
if errorMessage ~= "" then
mon.setCursorPos(1, 4)
mon.setTextColor(colors.red)
mon.write("! " .. errorMessage)
end
-- Show field errors on monitor
if #fieldErrors > 0 then
local errorY = 5
mon.setCursorPos(1, errorY)
mon.setTextColor(colors.orange)
mon.write("--- FIELD READ ERRORS ---")
errorY = errorY + 1
for i, err in ipairs(fieldErrors) do
mon.setCursorPos(1, errorY)
mon.setTextColor(colors.red)
mon.write(" " .. err)
errorY = errorY + 1
if i >= 4 then
mon.setCursorPos(1, errorY)
mon.write(" +" .. (#fieldErrors - 4) .. " more errors")
errorY = errorY + 1
break
end
end
end
if not reactorInfo or not reactorInfo.status then
local waitY = #fieldErrors > 0 and (7 + math.min(#fieldErrors, 5)) or 6
mon.setCursorPos(1, waitY)
mon.setTextColor(colors.yellow)
mon.write("Waiting for reactor data...")
return
end
local y = #fieldErrors > 0 and (7 + math.min(#fieldErrors, 5)) or 5
-- Reactor status
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Reactor Status: ")
mon.setTextColor(colors.yellow)
mon.write(reactorInfo.status or "unknown")
y = y + 2
-- Temperature
local temp = reactorInfo.temperature or 0
local tempColor = colors.lime
if temp > CONFIG.critical_temperature then
tempColor = colors.orange
end
if temp > CONFIG.max_temperature * 0.95 then
tempColor = colors.red
end
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Temperature: ")
mon.setTextColor(tempColor)
mon.write(formatTemp(temp) .. " / " .. formatTemp(CONFIG.max_temperature))
y = y + 1
-- Temperature bar
drawProgressBar(mon, 1, y, w - 2, temp, CONFIG.max_temperature, tempColor)
y = y + 2
-- Field Strength
local fieldStrength = reactorInfo.fieldStrength or 0
local maxField = reactorInfo.maxFieldStrength or 1
if maxField <= 0 then maxField = 1 end -- SAFETY: prevent division by zero
local fieldPercent = fieldStrength / maxField
local fieldColor = colors.lime
if fieldPercent < CONFIG.target_field_strength then
fieldColor = colors.yellow
end
if fieldPercent < CONFIG.min_field_strength * 2 then
fieldColor = colors.orange
end
if fieldPercent < CONFIG.min_field_strength then
fieldColor = colors.red
end
-- Show correct target based on state (55% during charging, 30% when running)
local fieldTarget = CONFIG.target_field_strength
if state == STATES.CHARGING or state == STATES.WARMING_UP then
fieldTarget = CONFIG.charge_field_target
end
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Field Strength: ")
mon.setTextColor(fieldColor)
mon.write(formatPercent(fieldPercent) .. " (Target: " .. formatPercent(fieldTarget) .. ")")
y = y + 1
-- Field bar
drawProgressBar(mon, 1, y, w - 2, fieldPercent, 1, fieldColor)
y = y + 2
-- Energy Saturation
local saturation = reactorInfo.energySaturation or 0
local maxSaturation = reactorInfo.maxEnergySaturation or 1
if maxSaturation <= 0 then maxSaturation = 1 end -- SAFETY: prevent division by zero
local satPercent = saturation / maxSaturation
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Saturation: ")
mon.setTextColor(colors.magenta)
mon.write(formatPercent(satPercent) .. " (Target: " .. formatPercent(CONFIG.target_saturation) .. ")")
y = y + 1
drawProgressBar(mon, 1, y, w - 2, satPercent, 1, colors.magenta)
y = y + 2
-- Fuel
local fuel = reactorInfo.fuelConversion or 0
local maxFuel = reactorInfo.maxFuelConversion or 1
if maxFuel <= 0 then maxFuel = 1 end -- SAFETY: prevent division by zero
local fuelPercent = fuel / maxFuel
-- Color fuel based on thresholds
local fuelColor = colors.lime
if fuelPercent >= CONFIG.fuel_shutdown_threshold then
fuelColor = colors.red
elseif fuelPercent >= CONFIG.fuel_warning_threshold then
fuelColor = colors.orange
end
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Fuel Used: ")
mon.setTextColor(fuelColor)
mon.write(formatPercent(fuelPercent) .. " (Shutdown: " .. formatPercent(CONFIG.fuel_shutdown_threshold) .. ")")
y = y + 1
drawProgressBar(mon, 1, y, w - 2, fuelPercent, 1, colors.brown, colors.green)
y = y + 2
-- Generation Rate
local genRate = reactorInfo.generationRate or 0
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Generation: ")
mon.setTextColor(colors.lime)
mon.write(formatNumber(genRate) .. " RF/t")
y = y + 1
-- Field Drain Rate
local drainRate = reactorInfo.fieldDrainRate or 0
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Field Drain: ")
mon.setTextColor(colors.orange)
mon.write(formatNumber(drainRate) .. " RF/t")
y = y + 2
-- Flux Gate Info
mon.setCursorPos(1, y)
mon.setTextColor(colors.cyan)
mon.write("--- FLUX GATES ---")
y = y + 1
local inputFlow = 0
local outputFlow = 0
pcall(function()
inputFlow = inputGate.getFlow() or 0
end)
pcall(function()
outputFlow = outputGate.getFlow() or 0
end)
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Input (Field): ")
mon.setTextColor(colors.blue)
mon.write(formatNumber(inputFlow) .. " RF/t")
y = y + 1
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Output (Power): ")
mon.setTextColor(colors.lime)
mon.write(formatNumber(outputFlow) .. " RF/t")
y = y + 1
-- Net power gain
local netPower = outputFlow - inputFlow
mon.setCursorPos(1, y)
mon.setTextColor(colors.white)
mon.write("Net Power: ")
if netPower >= 0 then
mon.setTextColor(colors.lime)
mon.write("+" .. formatNumber(netPower) .. " RF/t")
else
mon.setTextColor(colors.red)
mon.write(formatNumber(netPower) .. " RF/t")
end
y = y + 2
-- Instructions
mon.setCursorPos(1, y)
mon.setTextColor(colors.gray)
mon.write("Press Q to shutdown, C to charge")
end
-- ============================================================================
-- INPUT HANDLING
-- ============================================================================
local function handleInput()
while true do
local event, key = os.pullEvent("key")
if key == keys.q then
-- Request shutdown
shutdownRequested = true
print("Shutdown requested...")
elseif key == keys.c then
-- Charge reactor
if state == STATES.OFFLINE or state == STATES.SHUTDOWN then
shutdownRequested = false
chargeRequested = true -- Flag to provide input flux
if reactor then
pcall(function()
reactor.chargeReactor()
end)
end
print("Charging reactor... providing input flux")
end
elseif key == keys.x then
-- Force exit
return true
end
end
end
-- ============================================================================
-- MAIN LOOP
-- ============================================================================
local lastErrorPrint = 0
local lastFieldErrorCount = 0
local function mainLoop()
while true do
-- Update reactor info
if not updateReactorInfo() then
-- Try to reconnect with exponential backoff
local now = os.clock()
if now - lastPeripheralRetry >= peripheralRetryDelay then
lastPeripheralRetry = now
if findPeripherals() then
-- Success - reset backoff
peripheralRetryDelay = 1
print("[INFO] Peripherals reconnected")
else
-- Failure - increase backoff (max 30 seconds)
peripheralRetryDelay = math.min(peripheralRetryDelay * 2, 30)
print("[WARNING] Peripheral reconnect failed, retry in " .. peripheralRetryDelay .. "s")
end
end
else
-- Successfully reading reactor - reset backoff
if peripheralRetryDelay > 1 then
peripheralRetryDelay = 1
end
end
-- Print field errors to terminal when they change
local currentTime = os.clock()
if #fieldErrors > 0 and (#fieldErrors ~= lastFieldErrorCount or currentTime - lastErrorPrint > 5) then
lastFieldErrorCount = #fieldErrors
lastErrorPrint = currentTime
print("[" .. os.date("%H:%M:%S") .. "] Field read errors:")
for i, err in ipairs(fieldErrors) do
print(" - " .. err)
if i >= 5 then
print(" ... and " .. (#fieldErrors - 5) .. " more")
break
end
end
elseif #fieldErrors == 0 and lastFieldErrorCount > 0 then
lastFieldErrorCount = 0
print("[" .. os.date("%H:%M:%S") .. "] Field errors cleared")
end
-- Process state machine
processStateMachine()
-- Control reactor based on state
controlReactor()
-- Update display periodically
local currentTime = os.clock()
if currentTime - lastDisplayUpdate >= CONFIG.display_interval then
updateDisplay()
lastDisplayUpdate = currentTime
end
-- Wait for next cycle
sleep(CONFIG.update_interval)
end
end
-- ============================================================================
-- SAFETY FUNCTIONS
-- ============================================================================
-- Set fail-safe state on exit (input gate to max, output to 0)
local function emergencyExit()
print("\n!!! EMERGENCY EXIT - Setting fail-safe flux gate values !!!")
-- Calculate dynamic emergency input based on reactor info if available
local emergencyInput = CONFIG.emergency_field_input
if reactorInfo and reactorInfo.maxFieldStrength then
emergencyInput = math.max(reactorInfo.maxFieldStrength * 2, CONFIG.emergency_field_input)
end
if inputGate then
pcall(function()
inputGate.setFlowOverride(emergencyInput)
end)
print("Input gate set to " .. formatNumber(emergencyInput) .. " RF/t")
end
if outputGate then
pcall(function()
outputGate.setFlowOverride(0)
end)
print("Output gate set to 0 RF/t")
end
if monitor then
pcall(function()
monitor.setBackgroundColor(colors.red)
monitor.clear()
monitor.setCursorPos(1, 1)
monitor.setTextColor(colors.white)
monitor.write("!!! CONTROLLER STOPPED !!!")
monitor.setCursorPos(1, 3)
monitor.write("Input gate set to MAXIMUM")
monitor.setCursorPos(1, 4)
monitor.write("Restart controller ASAP!")
end)
end
end
-- Verify flux gates are likely configured correctly
local function verifyFluxGates()
if not inputGate or not outputGate or not reactor then
return false, "Peripherals not connected"
end
-- Try to get reactor info
local info = nil
pcall(function()
info = reactor.getReactorInfo()
end)
if not info or info.status == "cold" or info.status == "invalid" then
-- Reactor is off, can't verify
return true, "Reactor offline - please verify gates manually"
end
-- If reactor is running, check that input gate is feeding field
local inputFlow = 0
local drainRate = info.fieldDrainRate or 0
pcall(function()
inputFlow = inputGate.getFlow() or 0
end)
-- Warning if input seems too low for drain rate
if drainRate > 0 and inputFlow < drainRate * 0.5 then
return true, "WARNING: Input flow seems low for field drain rate!"
end
return true, "Flux gates appear correctly configured"
end
-- ============================================================================
-- STARTUP
-- ============================================================================
local function main()
print("=== DRACONIC REACTOR CONTROLLER ===")
print("Initializing...")
-- Find peripherals
if not findPeripherals() then
print("ERROR: " .. errorMessage)
print("")
print("To fix this:")
print("1. Run 'peripherals' to see available peripheral names")
print("2. Edit this file and set the names in the CONFIG section:")
print(" - reactor_name")
print(" - input_gate_name (flux gate TO the reactor field)")
print(" - output_gate_name (flux gate FROM the reactor)")
return
end
print("Peripherals connected!")
-- Verify flux gate configuration
local gatesOk, gateMsg = verifyFluxGates()
print("Flux gates: " .. gateMsg)
print("Starting control loop...")
print("Press Q to shutdown reactor")
print("Press C to charge reactor")
print("Press X to exit program")
print("")
print("!!! IMPORTANT: If this program stops unexpectedly,")
print(" input gate will be set to MAXIMUM for safety !!!")
print("")
state = STATES.OFFLINE
-- Run main loop and input handler in parallel
-- Wrap in pcall to catch any crashes and set fail-safe
local success, err = pcall(function()
parallel.waitForAny(mainLoop, handleInput)
end)
if not success then
print("\n!!! PROGRAM CRASHED: " .. tostring(err))
emergencyExit()
return
end
-- Normal cleanup - still set fail-safe values to be safe
emergencyExit()
print("\nController stopped safely.")
end
-- Wrap entire program to catch terminate events
local function safeMain()
local success, err = pcall(main)
if not success then
if err == "Terminated" then
print("\nProgram terminated by user")
else
print("\nUnexpected error: " .. tostring(err))
end
emergencyExit()
end
end
-- Run the program with safety wrapper
safeMain()