local peripheralNames = {
reactor = "draconic_reactor_1",
inGate = "flow_gate_3",
outGate = "flow_gate_2",
monitor = "right"
}
local autoStop = 0.1
local setfield = 0.05
local setTemp = 8000.4
local fieldCoe = {0.08, 0.03, 0.09}
local tempCoe = {0.05, 0.09, 0.1}
local energyCoe = {0.045, 0.11, 0.1}
local intgTime = 15
local TICK = 0.05
local FIELD_INTERVAL = math.floor(0.3 / TICK + 0.5)
local TEMP_INTERVAL = math.floor(0.8 / TICK + 0.5)
-- Safety thresholds
local MAX_TEMP = 8300
local MIN_FIELD_RATE = 0.02 -- 2%
local reactor, inGate, outGate, monitor = nil, nil, nil, nil
local curIn, curOut = 0, 0
local avgOut = 0
local fieldDeltaList = {}
local tempDeltaList = {}
local energyDeltaList = {}
local state = "idle" -- idle, check, attach, charge, active, error
local buttonState = "stop" -- start/stop text toggling on GUI
local running = false -- whether PID control is active / we have 'started' reactor control
-- PID internal trackers
local field_it, field_eI, field_lastDelta = 0, 0.0, 0.0
local temp_it, temp_eI, temp_lastDelta = 0, 0.0, 0.0
local energy_it, energy_eI, energy_lastDelta = 0, 0.0, 0.0
-- counters for tick-based updates
local tickCounter = 0
local fieldCounter = 0
local tempCounter = 0
-- monitor layout (3x2 proportion)
local mon_w, mon_h = nil, nil
local seg_w, seg_h = nil, nil
-------------------------------------------------------------------------------
-- Helper: safe call - check method exists before calling
-------------------------------------------------------------------------------
local function hasMethod(obj, name)
if not obj then return false end
return type(obj[name]) == "function"
end
local function safeCall(obj, name, ...)
if not obj then return nil, "nil object" end
local f = obj[name]
if type(f) ~= "function" then
return nil, "method_missing:" .. tostring(name)
end
return pcall(f, obj, ...)
end
-------------------------------------------------------------------------------
-- Peripheral detection helpers
-------------------------------------------------------------------------------
local function tryWrap(name)
if name and name ~= "" then
local ok, res = pcall(peripheral.wrap, name)
if ok and res then return res end
end
return nil
end
local function findReactorPeripheral()
for n, t in pairs(peripheral.getNames()) do
local p = peripheral.wrap(t)
if p and type(p.getReactorInfo) == "function" then
return t, p
end
end
return nil, nil
end
local function findGatePeripheral(exclude)
for n, t in pairs(peripheral.getNames()) do
if t ~= exclude then
local p = peripheral.wrap(t)
if p then
-- attempt to find a method that looks like flow control
if (type(p.setFlow) == "function")
or (type(p.setFlowOverride) == "function")
or (type(p.setFlowRate) == "function")
or (type(p.setOverrideEnabled) == "function") then
return t, p
end
end
end
end
return nil, nil
end
local function findMonitorPeripheral()
for n, t in pairs(peripheral.getNames()) do
local p = peripheral.wrap(t)
if p and type(p.clear) == "function" and type(p.setCursorPos) == "function" then
return t, p
end
end
return nil, nil
end
-------------------------------------------------------------------------------
-- Peripheral assignment / self-check
-------------------------------------------------------------------------------
local function detectPeripherals()
-- attempt to wrap via explicit names first
reactor = tryWrap(peripheralNames.reactor)
inGate = tryWrap(peripheralNames.inGate)
outGate = tryWrap(peripheralNames.outGate)
monitor = tryWrap(peripheralNames.monitor)
-- auto-detect any that are still nil
if not reactor then
local name, p = findReactorPeripheral()
if p then
reactor = p
peripheralNames.reactor = name
end
end
if not inGate then
local name, p = findGatePeripheral(peripheralNames.outGate)
if p then
inGate = p
peripheralNames.inGate = name
end
end
if not outGate then
local name, p = findGatePeripheral(peripheralNames.inGate)
if p then
outGate = p
peripheralNames.outGate = name
end
end
if not monitor then
local name, p = findMonitorPeripheral()
if p then
monitor = p
peripheralNames.monitor = name
end
end
end
local function checkAPIs()
-- return true if basic APIs exist
if not reactor then return false, "no reactor" end
if not hasMethod(reactor, "getReactorInfo") then return false, "reactor.getReactorInfo missing" end
-- reactor methods may vary, but check some common ones
if not (hasMethod(reactor, "activateReactor") and hasMethod(reactor, "stopReactor") and hasMethod(reactor, "chargeReactor")) then
-- still allow if some present: require at least getReactorInfo
-- but prefer to have activate/stop/charge
end
if not inGate or not outGate then return false, "missing gates" end
-- check flow methods on gates
local inOk = hasMethod(inGate, "setFlow") or hasMethod(inGate, "setFlowOverride") or hasMethod(inGate, "setFlowRate") or hasMethod(inGate, "setOverrideEnabled")
local outOk = hasMethod(outGate, "setFlow") or hasMethod(outGate, "setFlowOverride") or hasMethod(outGate, "setFlowRate") or hasMethod(outGate, "setOverrideEnabled")
if not (inOk and outOk) then return false, "gate methods missing" end
if monitor then
local monOk = hasMethod(monitor, "clear") and hasMethod(monitor, "setCursorPos") and hasMethod(monitor, "write")
if not monOk then return false, "monitor methods missing" end
end
return true, "ok"
end
-------------------------------------------------------------------------------
-- Flow control wrappers (tolerant to different gate APIs)
-------------------------------------------------------------------------------
local function setIn(value)
if not inGate then return false end
if value < 0 then value = 0 end
-- clamp to a large max
if value > 64000000 then value = 64000000 end
curIn = value
-- try several possible methods
if hasMethod(inGate, "setFlowOverride") then
pcall(inGate.setFlowOverride, inGate, value)
return true
elseif hasMethod(inGate, "setFlow") then
pcall(inGate.setFlow, inGate, value)
return true
elseif hasMethod(inGate, "setFlowRate") then
pcall(inGate.setFlowRate, inGate, value)
return true
elseif hasMethod(inGate, "setOverrideEnabled") then
-- set override, then set flow maybe via setFlow or similar
pcall(inGate.setOverrideEnabled, inGate, true)
if hasMethod(inGate, "setFlow") then pcall(inGate.setFlow, inGate, value) end
return true
else
return false
end
end
local function setOut(value)
if not outGate then return false end
if value < 0 then value = 0 end
if value > 64000000 then value = 64000000 end
curOut = value
if hasMethod(outGate, "setFlowOverride") then
pcall(outGate.setFlowOverride, outGate, value)
return true
elseif hasMethod(outGate, "setFlow") then
pcall(outGate.setFlow, outGate, value)
return true
elseif hasMethod(outGate, "setFlowRate") then
pcall(outGate.setFlowRate, outGate, value)
return true
elseif hasMethod(outGate, "setOverrideEnabled") then
pcall(outGate.setOverrideEnabled, outGate, true)
if hasMethod(outGate, "setFlow") then pcall(outGate.setFlow, outGate, value) end
return true
else
return false
end
end
-------------------------------------------------------------------------------
-- Reactor info helpers (wrap original names)
-------------------------------------------------------------------------------
local function getInfo()
if not reactor then return nil end
local ok, info = pcall(reactor.getReactorInfo, reactor)
if ok and type(info) == "table" then return info end
return nil
end
local function status()
local info = getInfo()
if not info then return nil end
return info.status
end
local function Temp()
local info = getInfo()
if not info then return 0 end
return info.temperature or 0
end
local function Field()
local info = getInfo()
if not info then return 0 end
return info.fieldStrength or 0
end
local function maxField()
local info = getInfo()
if not info then return 1 end
return info.maxFieldStrength or 1
end
local function FieldRate()
local info = getInfo()
if not info then return 0 end
if info.maxFieldStrength and info.maxFieldStrength > 0 then
return (info.fieldStrength or 0) / info.maxFieldStrength
end
return 0
end
local function FieldDrain()
local info = getInfo()
if not info then return 0 end
return info.fieldDrainRate or 0
end
local function Energy()
local info = getInfo()
if not info then return 0 end
return info.energySaturation or 0
end
local function EnergyRate()
local info = getInfo()
if not info then return 0 end
if info.maxEnergySaturation and info.maxEnergySaturation > 0 then
return (info.energySaturation or 0) / info.maxEnergySaturation
end
return 0
end
local function FuelRate()
local info = getInfo()
if not info then return 0 end
if info.maxFuelConversion and info.maxFuelConversion > 0 then
return (info.fuelConversion or 0) / info.maxFuelConversion
end
return 0
end
local function eV()
local info = getInfo()
if not info then return 0 end
return info.generationRate or 0
end
local function startReactor()
if not reactor then return false end
if hasMethod(reactor, "activateReactor") then
pcall(reactor.activateReactor, reactor)
return true
end
return false
end
local function stopReactor()
if not reactor then return false end
if hasMethod(reactor, "stopReactor") then
pcall(reactor.stopReactor, reactor)
return true
end
return false
end
local function chargeReactor()
if not reactor then return false end
if hasMethod(reactor, "chargeReactor") then
pcall(reactor.chargeReactor, reactor)
return true
end
return false
end
-------------------------------------------------------------------------------
-- Initialize reactor for activation (charging, override enabling)
-------------------------------------------------------------------------------
local function initializeReactor()
if inGate and hasMethod(inGate, "setOverrideEnabled") then
pcall(inGate.setOverrideEnabled, inGate, true)
end
if outGate and hasMethod(outGate, "setOverrideEnabled") then
pcall(outGate.setOverrideEnabled, outGate, true)
end
chargeReactor()
setOut(0)
setIn(64000000)
-- try to activate if thresholds satisfied
for i = 1, 200 do -- timeout loop (200*0.05 = 10s)
local t = Temp()
local f = Field()
local e = Energy()
if t >= 2000 and f >= 0.49 and e >= 0.49 then
startReactor()
if status() == "running" then return true end
end
os.sleep(TICK)
end
return false
end
-------------------------------------------------------------------------------
-- PID update functions (called at their respective intervals)
-------------------------------------------------------------------------------
local function sum(tb)
local s = 0.0
for _, v in ipairs(tb) do s = s + v end
return s
end
local function len(tb)
return #tb
end
local function updateFieldPID()
-- fieldDelta relative to maxField
local fieldDelta = maxField() * setfield - Field()
local eP = fieldDelta * fieldCoe[1]
-- only integrate if eP + drain >= 0 (mimic original logic)
if eP + FieldDrain() >= 0 then
table.insert(fieldDeltaList, fieldDelta)
if field_it >= 3 and #fieldDeltaList >= 2 then
field_eI = sum(fieldDeltaList) * fieldCoe[2]
local last = fieldDeltaList[#fieldDeltaList - 1] or fieldDeltaList[#fieldDeltaList]
local eD = (fieldDelta - last) * fieldCoe[3]
local eT = eP + field_eI + eD
setIn(eT + FieldDrain())
field_lastDelta = fieldDelta
else
-- accumulate until integral starts
if field_it <= intgTime then field_it = field_it + 1 end
-- still compute proportional-only
local eT = eP + field_eI
setIn(eT + FieldDrain())
field_lastDelta = fieldDelta
table.insert(fieldDeltaList, fieldDelta)
end
-- limit integral history
if #fieldDeltaList > intgTime then
while #fieldDeltaList > intgTime do table.remove(fieldDeltaList, 1) end
end
else
-- if not allowed to integrate, still set minimal override
setIn(0)
end
end
local function updateTempEnergyPID()
-- Temperature PID driving target energy delta
local tDelta = setTemp - Temp()
local eP1 = tDelta * tempCoe[1]
table.insert(tempDeltaList, tDelta)
if temp_it >= 3 and #tempDeltaList >= 2 then
temp_eI = sum(tempDeltaList) * tempCoe[2]
local eD1 = (tDelta - (tempDeltaList[#tempDeltaList - 1] or tempDeltaList[#tempDeltaList])) * tempCoe[3]
-- eT1 total temperature PID output
local eT1 = eP1 + temp_eI + eD1
local eDelta = eT1 * 100
-- now energy PID (use eDelta as input)
local eP2 = eDelta * energyCoe[1]
if eP2 + eV() >= 0 then
table.insert(energyDeltaList, eDelta)
if energy_it >= 3 and #energyDeltaList >= 2 then
energy_eI = sum(energyDeltaList) * energyCoe[2]
local eD2 = (eDelta - (energyDeltaList[#energyDeltaList - 1] or energyDeltaList[#energyDeltaList])) * energyCoe[3]
local eT2 = eP2 + energy_eI + eD2
setOut(eV() + eT2)
else
-- still use proportional-only
setOut(eV() + eP2)
end
end
-- trim histories
if #tempDeltaList > intgTime then
while #tempDeltaList > intgTime do table.remove(tempDeltaList, 1) end
end
if #energyDeltaList > intgTime then
while #energyDeltaList > intgTime do table.remove(energyDeltaList, 1) end
end
else
-- initial accumulation: still set baseline outputs
temp_it = math.min(temp_it + 1, intgTime + 1)
energy_it = math.min(energy_it + 1, intgTime + 1)
-- set conservative out to current generation (no extra)
setOut(eV())
end
end
-------------------------------------------------------------------------------
-- GUI drawing (monitor proportional to 3x2)
-------------------------------------------------------------------------------
local function initMonitorLayout()
if not monitor then return end
local ok, w, h = pcall(monitor.getSize, monitor)
if ok and w and h then
mon_w, mon_h = w, h
else
-- fallback
mon_w, mon_h = 51, 19
end
seg_w = math.floor(mon_w / 3)
seg_h = math.floor(mon_h / 2)
end
local function monClear()
if not monitor then return end
pcall(monitor.clear, monitor)
pcall(monitor.setCursorPos, monitor, 1, 1)
end
local function drawCentered(x, y, text)
if not monitor then return end
local txt = tostring(text)
local len = #txt
local col = math.max(1, math.floor((seg_w - len) / 2))
pcall(monitor.setCursorPos, monitor, (x - 1) * seg_w + col, (y - 1) * seg_h + 1)
pcall(monitor.write, monitor, txt)
end
local function drawGUI()
if not monitor then return end
monClear()
-- Title
pcall(monitor.setCursorPos, monitor, 2, 1)
pcall(monitor.write, monitor, "Draconic Reactor Control")
-- Status region (left-top segment)
local st = ("State: %s"):format(state:upper())
pcall(monitor.setCursorPos, monitor, 1, 3)
pcall(monitor.write, monitor, st)
-- Data region (middle-top)
pcall(monitor.setCursorPos, monitor, seg_w + 2, 3)
local fRate = FieldRate() * 100
local eRate = EnergyRate() * 100
local fuelPct = (1 - FuelRate()) * 100
pcall(monitor.write, monitor, ("Field: %.2f%%"):format(fRate))
pcall(monitor.setCursorPos, monitor, seg_w + 2, 4)
pcall(monitor.write, monitor, ("Energy: %.2f%%"):format(eRate))
pcall(monitor.setCursorPos, monitor, seg_w + 2, 5)
pcall(monitor.write, monitor, ("Fuel Left: %.2f%%"):format(fuelPct))
-- Temp region (right-top)
pcall(monitor.setCursorPos, monitor, 2 * seg_w + 2, 3)
pcall(monitor.write, monitor, ("TEMP: %d C"):format(Temp()))
pcall(monitor.setCursorPos, monitor, 2 * seg_w + 2, 4)
pcall(monitor.write, monitor, ("Out: %.2f RF/t"):format(curOut - curIn))
-- Center: big Field percent
pcall(monitor.setCursorPos, monitor, seg_w + 2, seg_h - 2)
pcall(monitor.write, monitor, ("Field Strength: %.2f%%"):format(FieldRate() * 100))
-- Bottom area: button
local btnY = mon_h - 2
pcall(monitor.setCursorPos, monitor, 2, btnY)
local btnText = (running and "[ STOP REACTOR ]" or "[ START REACTOR ]")
pcall(monitor.write, monitor, btnText)
-- If error state, show warning
if state == "error" then
pcall(monitor.setCursorPos, monitor, 2, btnY - 2)
pcall(monitor.write, monitor, "!!! ERROR / SAFETY STOPPED !!!")
end
end
-------------------------------------------------------------------------------
-- Touch handling: check if touch inside button area, toggle start/stop
-- Uses proportional layout (bottom-left segment area)
-------------------------------------------------------------------------------
local function handleMonitorTouch(x, y)
if not monitor or not mon_w or not mon_h then return end
local btnY = mon_h - 2
-- simplify: if y >= btnY and x between 1..mon_w, toggle
if y >= btnY then
if state ~= "error" then
if running then
-- stop reactor control: stop PIDs, try to stop reactor
running = false
-- stop reactor and zero outputs
stopReactor()
setOut(0)
setIn(64000000)
else
running = true
-- attempt initialize/attach
initializeReactor()
end
else
-- if in error, clicking does nothing (could be extended to reset)
end
end
end
-------------------------------------------------------------------------------
-- Emergency supervise (single-step check inside main loop)
-------------------------------------------------------------------------------
local function emergencyCheck()
if state == "active" and (Temp() >= MAX_TEMP or FieldRate() <= MIN_FIELD_RATE) then
-- immediate protective actions
setOut(0)
setIn(64000000)
pcall(stopReactor)
state = "error"
running = false
end
end
-------------------------------------------------------------------------------
-- State machine handlers
-------------------------------------------------------------------------------
local function handleIdle()
-- waiting for peripherals
detectPeripherals()
local ok, msg = checkAPIs()
if ok then
state = "check"
else
state = "idle"
end
end
local function handleCheck()
local ok, msg = checkAPIs()
if ok then
state = "attach"
else
state = "idle"
end
end
local function handleAttach()
-- try to attach to existing reactor state (charge, activate if needed)
-- keep previous running flag
if initializeReactor() then
-- if reactor is running already, accept control
if status() == "running" then
state = "active"
running = true
else
-- if not running but charged, go to charge and then active
state = "charge"
end
else
-- initialization failed, wait a bit and retry
state = "charge"
end
end
local function handleCharge()
-- charge until thresholds reached then try activate
chargeReactor()
setOut(0)
setIn(64000000)
local t = Temp()
local f = Field()
local e = Energy()
if t >= 2000 and f >= 0.49 and e >= 0.49 then
startReactor()
if status() == "running" then
state = "active"
running = true
else
-- keep charging / waiting
state = "charge"
end
else
state = "charge"
end
end
local function handleActive()
emergencyCheck()
if state == "error" then return end
-- when running flag false, interpret as user stop
if not running then
-- stop PIDs and attempt to stop reactor
stopReactor()
state = "idle"
return
end
-- update PID based on tick counters handled in main loop
end
local function handleError()
-- show error and keep outputs safe
setOut(0)
setIn(64000000)
-- wait until user reconnects peripherals and manually restart by clicking Start
end
-------------------------------------------------------------------------------
-- MAIN loop: uses os.startTimer + os.pullEvent for precise tick & non-blocking touch
-------------------------------------------------------------------------------
local function main()
-- initial detection
detectPeripherals()
initMonitorLayout()
monClear()
if monitor then
pcall(monitor.setCursorPos, monitor, 1, 1)
pcall(monitor.write, monitor, "Detecting peripherals...")
end
-- start a repeating timer for ticks
local timerId = os.startTimer(TICK)
while true do
local ev = { os.pullEvent() }
if ev[1] == "timer" and ev[2] == timerId then
-- schedule next tick
timerId = os.startTimer(TICK)
tickCounter = tickCounter + 1
fieldCounter = fieldCounter + 1
tempCounter = tempCounter + 1
-- refresh detection occasionally (every 1s)
if tickCounter % math.floor(1 / TICK) == 0 then
detectPeripherals()
initMonitorLayout()
end
-- run state machine dispatcher
if state == "idle" then handleIdle()
elseif state == "check" then handleCheck()
elseif state == "attach" then handleAttach()
elseif state == "charge" then handleCharge()
elseif state == "active" then handleActive()
elseif state == "error" then handleError()
end
-- PID updates on their own intervals (only if running and state active)
if running and state == "active" then
if fieldCounter >= FIELD_INTERVAL then
updateFieldPID()
fieldCounter = 0
end
if tempCounter >= TEMP_INTERVAL then
updateTempEnergyPID()
tempCounter = 0
end
-- avgOut sample (simple)
avgOut = curOut - curIn
end
-- emergency check each tick
emergencyCheck()
-- draw GUI each tick (cheap)
pcall(drawGUI)
elseif ev[1] == "monitor_touch" then
-- monitor touch event: params vary but typically (side, x, y)
local side = ev[2]
local tx = ev[3]
local ty = ev[4]
-- ensure event belongs to our monitor (if monitor.name matches 'side')
if monitor and peripheralNames.monitor ~= "" then
if side == peripheralNames.monitor then
handleMonitorTouch(tx, ty)
end
else
-- if monitor auto-detected, we cannot get side easily; still handle by touch
handleMonitorTouch(tx, ty)
end
elseif ev[1] == "terminate" then
-- user requested stop; cleanup
setOut(0)
setIn(64000000)
stopReactor()
break
else
-- ignore other events
end
end
end
-------------------------------------------------------------------------------
-- Entry point
-------------------------------------------------------------------------------
local ok, msg = pcall(main)
if not ok then
-- best effort safe shutdown
pcall(setOut, 0)
pcall(setIn, 64000000)
pcall(stopReactor)
-- print error
print("Script terminated with error:", msg)
end