diff --git a/de_react_auto b/de_react_auto new file mode 100644 index 0000000..039fd57 --- /dev/null +++ b/de_react_auto @@ -0,0 +1,1107 @@ +-- 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 = "flux_gate_0", -- Flux gate feeding containment field + output_gate_name = "flux_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 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 + + -- 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) + 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 + + -- 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 + local targetStrength = CONFIG.target_field_strength + + -- 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 + + -- SAFETY: Never return less than the drain rate (would cause field to drop) + -- Add 10% safety margin + local minimumInput = drainRate * 1.1 + + return math.max(minimumInput, math.floor(totalInput)) +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 + + -- If we're in emergency or cooling, limit output + if state == STATES.EMERGENCY then + return 0 + end + + if state == STATES.COOLING then + -- During cooling, output less than we generate to build saturation + return math.floor(generationRate * 0.5) + end + + if state == STATES.CHARGING or state == STATES.WARMING_UP then + -- During warmup, output slightly less than generation + return math.floor(generationRate * 0.8) + end + + -- Normal operation: output slightly more than generation to keep temp rising slowly + -- But cap it if temperature is getting high + local tempFactor = 1.0 + if temp > CONFIG.critical_temperature then + -- Reduce output as we approach max temp + local overTemp = temp - CONFIG.critical_temperature + local tempRange = CONFIG.max_temperature - CONFIG.critical_temperature + tempFactor = 1.0 - (overTemp / tempRange) * 0.5 + end + + -- Output rate based on generation and saturation + local outputRate = generationRate * tempFactor + + -- If saturation is high, we can output more + if saturation > 0.8 then + outputRate = generationRate * 1.1 * tempFactor + end + + return math.max(0, math.floor(outputRate)) +end + +-- ============================================================================ +-- STATE MACHINE LOGIC +-- ============================================================================ + +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 + state = STATES.EMERGENCY + 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 + + -- Check for emergency conditions first (always) + if reactorStatus ~= "cold" and reactorStatus ~= "cooling" then + if fieldStrength < CONFIG.min_field_strength then + state = STATES.EMERGENCY + return + end + if temp > CONFIG.max_temperature then + state = STATES.EMERGENCY + return + end + end + + -- Check for shutdown request + if shutdownRequested then + if reactorStatus == "cold" or reactorStatus == "cooling" then + state = STATES.SHUTDOWN + shutdownRequested = false + elseif state ~= STATES.EMERGENCY then + state = STATES.SHUTDOWN + end + return + end + + -- State transitions based on reactor status + if reactorStatus == "cold" or reactorStatus == "invalid" then + state = STATES.OFFLINE + return + end + + if reactorStatus == "cooling" then + if state == STATES.EMERGENCY or state == STATES.SHUTDOWN then + -- Keep current state during cooldown + return + end + state = STATES.COOLING + 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 + state = STATES.CHARGING + else + state = STATES.WARMING_UP + end + return + end + + if reactorStatus == "running" then + if fieldStrength < CONFIG.charge_field_target * 0.9 then + state = STATES.CHARGING + elseif temp > CONFIG.critical_temperature then + state = STATES.COOLING + else + state = STATES.RUNNING + end + return + end + + if reactorStatus == "stopping" then + state = STATES.SHUTDOWN + 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 + -- SAFETY: Prevent division by zero + local maxField = reactorInfo.maxFieldStrength or 1 + if maxField <= 0 then maxField = 1 end + + 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()) + outputRate = 0 + + elseif state == STATES.WARMING_UP then + -- WARMING UP: Maintain field, slowly ramp output + inputRate = calculateFieldInput() + 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, no action needed + inputRate = 0 + outputRate = 0 + end + + -- Apply the flow rates with rate limiting + -- Emergency/Error states bypass rate limiting for safety + local now = os.clock() + local isEmergency = (state == STATES.EMERGENCY or state == STATES.ERROR) + local ratesChanged = (inputRate ~= lastInputRate) or (outputRate ~= lastOutputRate) + local timeSinceUpdate = now - lastFluxUpdate + + -- Update flux gates if: emergency, OR (rates changed AND enough time passed) + if isEmergency 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.getFlowOverride() or 0 + end) + pcall(function() + outputFlow = outputGate.getFlowOverride() 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 + 2 + + -- Instructions + mon.setCursorPos(1, y) + mon.setTextColor(colors.gray) + mon.write("Press Q to shutdown, R to restart") +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.r then + -- Restart reactor + if state == STATES.OFFLINE or state == STATES.SHUTDOWN then + shutdownRequested = false + if reactor then + pcall(function() + reactor.chargeReactor() + end) + end + print("Charging reactor...") + 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.getFlowOverride() 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 R to restart 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()