-- 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 if fieldStrength < CONFIG.charge_field_target * 0.9 then setState(STATES.CHARGING, "field " .. fieldPct .. " dropped below threshold while running") 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 + 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()