Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --
- -- Control turbines of a active cooled Big Reactor (http://big-reactors.com/).
- --
- -- Author: kla_sch
- --
- -- History:
- -- v0.1, 2014-12-29:
- -- - first version
- --
- -- v0.2, 2015-01-02:
- -- - Big Reactor since 0.3.4A: Feature to disengage coil to
- -- startup faster (see config value useDisengageCoils).
- -- - minor bugfixes
- -- - some code improvments (thanks to wieselkatze)
- --
- -- Save as "startup"
- --
- -- Constant values (configuration)
- -- ===============================
- -- Maximum loop time of controller (default: 0.5s)
- local loopT=0.5
- -- Display loop time, of controller has been switched off (default: 1s).
- local displayT=1
- -- Modem channel to listen for status requests. If it is set to 0,
- -- the remote status requests are disabled.
- -- The sender sould simply send "BR_turbine_get_state" to this channel. The
- -- turbine replies with status informations to the replay channel.
- local stateRequestChannel = 32768 -- Broadcast channel.
- --
- -- Enable remote control.
- -- Set to "true" if you want to enable this feature.
- --
- local remoteControl = true
- --
- -- Use disengaged coil for faster speedup. (Big Reactor >= 0.3.4A).
- -- Set this to "false" if you want to disable this feature.
- --
- local useDisengageCoils = true
- --
- -- Internal values:
- -- ================
- -- File to save last known controller state:
- local saveFilename = "turbine_crtl.save"
- local Kp, Kd, Ki, errSum, errOld -- global PID controller values
- local tSpeed -- target speed
- local turbine -- wraped turbine
- local maxFRate -- maximum float rate of turbine
- local floatRateChanged = false -- flag: true on float rate change
- --
- -- Find the first connected big turbine and return the wraped handle.
- --
- -- If no turbine was found this function terminate the program.
- --
- -- Return:
- -- Handle of first connected turbine found.
- --
- local function getTurbineHandle()
- local pList = peripheral.getNames()
- local i, name
- for i, name in pairs(pList) do
- if peripheral.getType(name) == "BigReactors-Turbine" then
- return peripheral.wrap(name)
- end
- end
- error("No turbine connected: Exit program")
- end
- --
- -- Search for any modem and open it to recieve modem requests.
- --
- local function searchAndOpenModems()
- if stateRequestChannel <= 0 then
- return -- feature disabled
- end
- local pList = peripheral.getNames()
- local i, name
- for i, name in pairs(pList) do
- if peripheral.getType(name) == "modem" then
- peripheral.call(name, "open", stateRequestChannel)
- peripheral.call(name, "open", os.getComputerID())
- end
- end
- end
- --
- -- Saves current controller state
- --
- local function saveControllerState()
- local tmpFilename = saveFilename .. ".tmp"
- fs.delete(tmpFilename)
- local sFile = fs.open(tmpFilename, "w")
- if sFile == nil then
- error("cannot open status file for writing.")
- end
- sFile.writeLine("V0.1")
- sFile.writeLine(tostring(tSpeed))
- sFile.writeLine(tostring(loopT))
- sFile.writeLine(tostring(Kp))
- sFile.writeLine(tostring(errSum))
- sFile.writeLine(tostring(errOld))
- sFile.writeLine("EOF")
- sFile.close()
- fs.delete(saveFilename)
- fs.move(tmpFilename, saveFilename)
- end
- --
- -- Initialize basic PID controller values
- --
- local function initControllerValues()
- local Tn = loopT
- local Tv = loopT * 10
- Ki = Kp / Tn
- Kd = Kp * Tv
- end
- --
- -- Read number from file
- --
- -- Parameters:
- -- sFile - opened file to read from
- --
- -- Return
- -- the number of nil, if an error has occurred
- --
- local function readNumber(sFile)
- local s = sFile.readLine()
- if s == nil then
- return nil
- end
- return tonumber(s)
- end
- --
- -- Restore last known controller state
- --
- -- Returns:
- -- true, if the last saved state has successfully readed.
- local function restoreControllerState()
- local tmpFilename = saveFilename .. ".tmp"
- local sFile = fs.open(saveFilename, "r")
- if sFile == nil and fs.exists(tmpFilename) then
- fs.move(tmpFilename, saveFilename)
- sFile = fs.open(saveFilename)
- end
- if sFile == nil then
- return false -- cannot read any file
- end
- local version = sFile.readLine()
- if version == nil then
- sFile.close()
- return false -- empty file
- end
- if version ~= "V0.1" then
- sFile.close()
- return false -- unknown version
- end
- local tSpeedNum = readNumber(sFile)
- if tSpeedNum == nil then
- sFile.close()
- return false -- cannot read target speed
- end
- local loopTNum = readNumber(sFile)
- if loopTNum == nil then
- sFile.close()
- return false -- cannot read loop speed
- end
- local KpNum = readNumber(sFile)
- if KpNum == nil then
- sFile.close()
- return false -- cannot read Kp
- end
- local errSumNum = readNumber(sFile)
- if errSumNum == nil then
- sFile.close()
- return false -- cannot read error sum
- end
- local errOldNum = readNumber(sFile)
- if errOldNum == nil then
- sFile.close()
- return false -- cannot read last error
- end
- local eofStr = sFile.readLine()
- if eofStr == nil or eofStr ~= "EOF" then
- sFile.close()
- return false -- EOF marker not found. File corrupted?
- end
- sFile.close()
- -- Restore saved values
- tSpeed = tSpeedNum
- loopT = loopTNum
- Kp = KpNum
- errSum = errSumNum
- errOld = errOldNum
- initControllerValues()
- if tSpeed == 0 then
- turbine.setActive(false)
- else
- turbine.setActive(true)
- end
- return true
- end
- --
- -- Write text with colors, if possible (advance monitor)
- --
- -- Parameters:
- -- mon - handle of monitor
- -- color - text color
- -- text - text to write
- --
- local function writeColor(mon, color, text)
- if mon.isColor() then
- mon.setTextColor(color)
- end
- mon.write(text)
- if mon.isColor() then
- mon.setTextColor(colors.white)
- end
- end
- --
- -- Scale the monitor text size to needed size of output text.
- --
- -- This function try to scale the monitor text size, so that it is enoth for
- -- "optHeight" lines with "optWidth" characters. If it is not possible
- -- (text scale is not supported or the connected monitor is too small),
- -- it also accept "minHeight" lines and "minWidth" characters.
- --
- -- Parameters:
- -- mon - handle of monitor.
- -- minWidth - Minimum number of columns needed.
- -- optWidth - Optimal number of columns desired.
- -- minHeight - Minimum number of rows needed.
- -- optHeight - Optimal number of rows desired.
- --
- -- Return:
- -- Size of monitor after scaling: width, height.
- -- If the monitor is too small, it returns nul,nil.
- --
- local function scaleMonitor(mon, minWidth, optWidth, minHeight, optHeight)
- if mon.setTextScale ~= nil then
- mon.setTextScale(1)
- end
- local width, height = mon.getSize()
- if mon.setTextScale == nil then
- -- Scale not available
- if width < minWidth or height < minHeight then
- return nil, nil -- too small
- else
- return width, height
- end
- end
- if width < optWidth or height < optHeight then
- -- too small: try to scale down.
- mon.setTextScale(0.5)
- width, height = mon.getSize()
- if width < minWidth or height < minHeight then
- return nil, nil -- still too small
- end
- else
- -- Huge monitors? Try to scale up, if possible (max scale=5).
- local scale = math.min(width / optWidth, height / optHeight, 5)
- scale = math.floor(scale * 2) / 2 -- multiple of 0.5
- if scale > 1 then
- mon.setTextScale(scale)
- width, height = mon.getSize()
- end
- end
- return width, height
- end
- -- Display turbine status to a monitor
- --
- -- Parameters:
- -- mon - Wraped handle of monitor
- -- turbine - Wraped handle of turbine.
- -- tSpeed - Target speed.
- --
- local function displayStateOnMonitor(mon, turbine, tSpeed)
- -- scale it, if possible.
- local width, height = scaleMonitor(mon, 15, 16, 5, 5)
- if width == nil or height == nil then
- return -- Montitor is too small
- end
- mon.clear()
- mon.setCursorPos(1,1)
- mon.write("Turbine: ")
- if tSpeed == 0 then
- writeColor(mon, colors.red, "off")
- else
- writeColor(mon, colors.green, string.format("%d", tSpeed))
- if width > 16 then
- mon.write(" RPM")
- end
- end
- mon.setCursorPos(1,3)
- local speed = math.floor(turbine.getRotorSpeed()*10+0.5)/10
- mon.write("Speed: ")
- if (speed == tSpeed) then
- writeColor(mon, colors.green, speed)
- else
- writeColor(mon, colors.orange, speed)
- end
- if width > 16 then
- mon.write(" RPM")
- end
- local maxFlow = turbine.getFluidFlowRateMax()
- local actFlow = turbine.getFluidFlowRate()
- if width >= 16 then
- -- bigger monitor
- mon.setCursorPos(1,4)
- mon.write("MFlow: " .. string.format("%d", maxFlow) .. " mB/t")
- mon.setCursorPos(1,5)
- mon.write("AFlow: ")
- if actFlow < maxFlow then
- writeColor(mon, colors.red, string.format("%d", actFlow))
- else
- writeColor(mon, colors.green, string.format("%d", actFlow))
- end
- mon.write(" mB/t")
- else
- -- 1x1 monitor
- mon.setCursorPos(1,4)
- mon.write("Flow (act/max):")
- mon.setCursorPos(1,5)
- mon.write("(")
- if actFlow < maxFlow then
- writeColor(mon, colors.red, string.format("%d",actFlow))
- else
- writeColor(mon, colors.green, string.format("%d",actFlow))
- end
- mon.write("/")
- mon.write(string.format("%d", maxFlow))
- mon.write(" mB/t)")
- end
- end
- -- Display turbine status to any connected monitor and also to console.
- --
- -- Parameters:
- -- turbine - Wraped handle of turbine.
- -- tSpeed - Target speed.
- --
- function displayState(turbine, tSpeed)
- displayStateOnMonitor(term, turbine, tSpeed)
- term.setCursorPos(1,7)
- term.write("* Keys: [o]ff, [m]edium (900), [f]ast (1800)")
- term.setCursorPos(1,8)
- local pList = peripheral.getNames()
- local i, name
- for i, name in pairs(pList) do
- if peripheral.getType(name) == "monitor" then
- -- found monitor as peripheral
- displayStateOnMonitor(peripheral.wrap(name), turbine, tSpeed)
- end
- end
- end
- --
- -- Test the speedup time of the turbine.
- --
- -- Parameters:
- -- turbine - Wraped handle of turbine.
- -- loopT - Loop timer.
- -- tSpeed - Target speed
- local function testSpeedup(turbine, loopT, tSpeed)
- turbine.setFluidFlowRateMax(maxFRate)
- if turbine.setInductorEngaged then
- -- always engage coil
- turbine.setInductorEngaged(true)
- end
- local KpSum=0
- local nKp=0
- local oldSpeed=-1
- for i=0,5 do
- displayState(turbine, tSpeed)
- speed = turbine.getRotorSpeed()
- if oldSpeed >= 0 then
- KpSum = KpSum + (speed-oldSpeed)
- nKp = nKp + 1
- end
- oldSpeed = speed
- sleep(loopT)
- end
- if KpSum * loopT / nKp > 5 then
- -- Too fast: increase loop speed
- loopT = 5 * nKp / KpSum
- return 5, loopT
- else
- return (KpSum / nKp), loopT
- end
- end
- --
- -- Main program
- --
- sleep(2) -- wait for 2s
- -- wrap turbine
- turbine = getTurbineHandle()
- searchAndOpenModems() -- search and open any modem.
- if restoreControllerState() == false then
- -- restore of old values failed.
- tSpeed = 0
- Kp=0
- end
- maxFRate = turbine.getFluidFlowRateMaxMax()
- while true do
- displayState(turbine, tSpeed)
- if tSpeed ~= 0 then
- -- calculate PID controller
- local speed = turbine.getRotorSpeed()
- err = tSpeed - speed
- local errSumOld = errSum
- errSum = errSum + err
- if useDisengageCoils
- and floatRateChanged
- and err > 0
- and turbine.setInductorEngaged
- then
- -- Turbine startup: disengage coils
- turbine.setInductorEngaged(false)
- end
- if turbine.setInductorEngaged and err < 0 then
- -- speed too fast: engage coils
- turbine.setInductorEngaged(true)
- end
- local p = Kp * err
- local i = Ki * loopT * errSum
- local d = Kd * (err - errOld) * loopT
- if i < 0 or i > maxFRate then
- errSum=errSumOld -- error too heavy => reset to old value.
- i = Ki * loopT * errSum
- end
- local fRate = p + i + d
- errOld = err
- -- cut extreme flow rates.
- if fRate < 0 then
- fRate = 0
- elseif fRate > maxFRate then
- fRate = maxFRate
- end
- turbine.setFluidFlowRateMax(fRate)
- tID = os.startTimer(loopT) -- Wait for loopT secounds.
- else
- -- Turbine switched off:
- tID = os.startTimer(displayT) -- Wait for displayT secounds.
- end
- saveControllerState()
- floatRateChanged = false
- repeat
- -- Event loop
- local evt, p1, p2, p3, p4, p5 = os.pullEvent()
- if evt == "char" then
- -- character typed
- local oldTSpeed = tSpeed
- if p1 == "o" then -- off
- tSpeed = 0
- turbine.setActive(false)
- saveControllerState()
- elseif p1 == "m" then -- medium speed = 900 RPM
- turbine.setActive(true)
- tSpeed = 900
- floatRateChanged=true
- elseif p1 == "f" then -- fast speed = 1800 RPM
- turbine.setActive(true)
- tSpeed = 1800
- floatRateChanged=true
- end
- if turbine.setInductorEngaged then
- -- engage coil by default
- turbine.setInductorEngaged(true)
- end
- if (p1 == "m" or p1 == "f") and tSpeed ~= oldTSpeed then
- -- Initialize PID controller values
- if Kp == 0 then
- Kp, loopT = testSpeedup(turbine, loopT, tSpeed)
- end
- initControllerValues()
- errSum = 0
- errOld = 0
- saveControllerState()
- end
- elseif evt == "modem_message"
- and stateRequestChannel >= 0
- and stateRequestChannel == p2
- and tostring(p4) == "BR_turbine_get_state"
- then
- -- Send status informations
- local cLabel = os.getComputerLabel()
- if cLabel == nil then
- -- No label: use coputer number (ID) as tio create a name
- cLabel = "ComputerID:" .. tostring(os.getComputerID())
- end
- if turbine.getInductorEngaged ~= nil then
- inductorEngaged = turbine.getInductorEngaged()
- else
- inductorEngaged = nil
- end
- -- State structure
- local rState = {
- version = "Turbine V0.1", -- Version of data structure
- label = cLabel,
- computerId = os.getComputerID(),
- remoteControl = remoteControl,
- active = turbine.getActive(),
- tSpeed = tSpeed,
- energyStored = turbine.getEnergyStored(),
- rotorSpeed = turbine.getRotorSpeed(),
- inputAmount = turbine.getInputAmount(),
- inputType = turbine.getInputType(),
- outputAmount = turbine.getOutputAmount(),
- outputType = turbine.getOutputType(),
- fluidAmountMax = turbine.getFluidAmountMax(),
- fluidFlowRate = turbine.getFluidFlowRate(),
- fluidFlowRateMax = turbine.getFluidFlowRateMax(),
- fluidFlowRateMaxMax = turbine.getFluidFlowRateMaxMax(),
- energyProducedLastTick = turbine.getEnergyProducedLastTick(),
- inductorEngaged = inductorEngaged
- }
- peripheral.call(p1, "transmit", p3, stateRequestChannel,
- textutils.serialize(rState))
- elseif evt == "modem_message"
- and remoteControl
- and p2 == os.getComputerID()
- and string.sub(tostring(p4), 1, 21) == "BR_turbine_set_speed:"
- then
- -- Remote Request: Speed change
- local oldTSpeed = tSpeed
- tSpeed = tonumber(string.sub(tostring(p4), 22))
- if (tSpeed == 0) then
- turbine.setActive(false)
- saveControllerState()
- else
- turbine.setActive(true)
- floatRateChanged=true
- end
- if tSpeed ~= 0 and tSpeed ~= oldTSpeed then
- -- Initialize PID controller values
- if Kp == 0 then
- Kp = testSpeedup(turbine, loopT, tSpeed)
- end
- initControllerValues()
- errSum = 0
- errOld = 0
- saveControllerState()
- end
- if turbine.setInductorEngaged then
- -- engage coil by default
- turbine.setInductorEngaged(true)
- end
- elseif evt == "peripheral"
- and peripheral.getType(p1) == "modem"
- and stateRequestChannel >= 0
- then
- -- new modem connected
- peripheral.call(p1, "open", stateRequestChannel)
- peripheral.call(p1, "open", os.getComputerID())
- end
- -- exit loop on timer event or float rate change
- until (evt == "timer" and p1 == tID) or floatRateChanged
- end
- --
- -- EOF
- --
Add Comment
Please, Sign In to add comment