1201 lines
42 KiB
Lua
1201 lines
42 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 threshold for fuel conversion
|
|
min_saturation = 0.20, -- Below this, reactor produces less power
|
|
|
|
-- 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
|
|
|
|
-- ============================================================================
|
|
-- 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
|
|
local tempFactor = 1.0
|
|
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
|
|
reason = "temp " .. formatTemp(temp) .. " > critical, factor=" .. string.format("%.2f", tempFactor)
|
|
end
|
|
|
|
local outputRate = generationRate * tempFactor
|
|
|
|
if saturation > 0.8 then
|
|
outputRate = generationRate * 1.1 * tempFactor
|
|
if reason == "" then reason = "high saturation (110%)" end
|
|
elseif reason == "" then
|
|
reason = "normal operation"
|
|
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 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
|
|
|
|
mon.setCursorPos(1, y)
|
|
mon.setTextColor(colors.white)
|
|
mon.write("Field Strength: ")
|
|
mon.setTextColor(fieldColor)
|
|
mon.write(formatPercent(fieldPercent) .. " (Target: " .. formatPercent(CONFIG.target_field_strength) .. ")")
|
|
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))
|
|
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
|
|
|
|
mon.setCursorPos(1, y)
|
|
mon.setTextColor(colors.white)
|
|
mon.write("Fuel Used: ")
|
|
mon.setTextColor(colors.brown)
|
|
mon.write(formatPercent(fuelPercent))
|
|
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()
|