[Controller]
- refactored - added IPC for RUN, PAUSE, STOP, STIRR ON/OFF [TIMER] - fixed default minimum wait time - added duration to elapse git-svn-id: http://moon:8086/svn/projects/HendiControl@277 fda53097-d464-4ada-af97-ba876c37ca34
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import signal
|
||||
import json
|
||||
from controller import Controller
|
||||
from controller import Controller, commands
|
||||
from mass import Mass
|
||||
from stirrer import Stirrer
|
||||
#from pololu1376 import Pololu1376
|
||||
@@ -49,20 +49,31 @@ def results_plot(self):
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
stirr_isOn = False
|
||||
|
||||
def handler(signum, frame):
|
||||
global stirr_isOn
|
||||
print('Signal handler called with signal', signum)
|
||||
|
||||
if signum == signal.SIGINT:
|
||||
ablauf.stop()
|
||||
ablauf.command(commands.ABORT)
|
||||
if signum == signal.SIGSTOP:
|
||||
ablauf.pause()
|
||||
if signum == signal.SIGCONT:
|
||||
ablauf.cont()
|
||||
ablauf.command(commands.PAUSE)
|
||||
if signum == signal.SIGUSR1:
|
||||
ablauf.command(commands.RUN)
|
||||
if signum == signal.SIGHUP:
|
||||
if stirr_isOn:
|
||||
ablauf.command(commands.STIRR_OFF)
|
||||
stirr_isOn = False
|
||||
else:
|
||||
ablauf.command(commands.STIRR_ON)
|
||||
stirr_isOn = True
|
||||
|
||||
# Set the signal handler and a 5-second alarm
|
||||
signal.signal(signal.SIGABRT, handler)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
signal.signal(signal.SIGCONT, handler)
|
||||
signal.signal(signal.SIGUSR1, handler)
|
||||
signal.signal(signal.SIGHUP, handler)
|
||||
|
||||
fp = open("brewpi.json")
|
||||
configJson = json.load(fp)
|
||||
@@ -77,7 +88,7 @@ if __name__ == '__main__':
|
||||
fp = open("Rezept-001.json")
|
||||
recipeJson = json.load(fp)
|
||||
|
||||
ablauf.start(recipeJson)
|
||||
ablauf.setRecipe(recipeJson)
|
||||
ablauf.wait_finished()
|
||||
|
||||
results_plot(ablauf)
|
||||
|
||||
+117
-108
@@ -7,24 +7,24 @@ import time
|
||||
import threading
|
||||
from kalman import Kalman
|
||||
from timer import Timer, TimerManager, TimerListener
|
||||
import queue
|
||||
|
||||
|
||||
ovenStates = Enum('ovenStates', 'NOP HEAT HOLD')
|
||||
|
||||
ovenStates = Enum('ovenStates', 'IDLE HEAT HOLD')
|
||||
states = Enum('controllerStates', 'IDLE NORMAL PAUSE')
|
||||
commands = Enum('controllerCommands', 'RUN PAUSE ABORT STIRR_OFF STIRR_ON')
|
||||
|
||||
class Controller(TimerListener):
|
||||
|
||||
def __init__(self, params, plant, stirrer):
|
||||
print(json.dumps({'Controller': params}, indent=4, sort_keys=True))
|
||||
self.sim_warp_factor = params['sim_warp_factor']
|
||||
self.ovenState = ovenStates.NOP
|
||||
self.ovenState = ovenStates.IDLE
|
||||
self.plant = plant
|
||||
self.stirrer = stirrer
|
||||
self.dt = params['dt']
|
||||
self.params = params
|
||||
self.pid_hold = Pid()
|
||||
self.pid_rate = Pid()
|
||||
self.timer_ist = 0
|
||||
self.sensorTime_v = np.empty(0)
|
||||
self.theta_raw_v = np.empty(0)
|
||||
self.theta_v = np.empty(0)
|
||||
@@ -50,16 +50,26 @@ class Controller(TimerListener):
|
||||
self.kalman = Kalman(params['kalman'])
|
||||
self.sensor_dt = params['kalman']['dt']
|
||||
self.timerMgr = TimerManager()
|
||||
self.processTimer = Timer("Process", self)
|
||||
self.rastTimer = Timer("Rast", self)
|
||||
self.sensorTimer = Timer("Sensor", self)
|
||||
self.plantTimer = Timer("Plant", self)
|
||||
self.reportTimer = Timer("Report", self)
|
||||
self.timerMgr.registerTimer(self.processTimer)
|
||||
self.timerMgr.registerTimer(self.rastTimer)
|
||||
self.timerMgr.registerTimer(self.sensorTimer)
|
||||
self.timerMgr.registerTimer(self.plantTimer)
|
||||
self.timerMgr.registerTimer(self.reportTimer)
|
||||
self.rast_running = False
|
||||
self.power_hold_offset = 200
|
||||
self.power_heat_offset = 1500
|
||||
self.queue = queue.Queue()
|
||||
self.thread = threading.Thread(target=self.main)
|
||||
self.rast_done = False
|
||||
self.receipe_done = False
|
||||
self.thread.start()
|
||||
|
||||
def command(self, cmd):
|
||||
self.queue.put_nowait(cmd)
|
||||
|
||||
def onTimer(self, timer):
|
||||
if timer == self.sensorTimer:
|
||||
@@ -83,60 +93,59 @@ class Controller(TimerListener):
|
||||
self.theta_k_v = np.append(self.theta_k_v, self.theta_ist_k)
|
||||
self.heatrate_k_v = np.append(self.heatrate_k_v, self.heatrate_ist_k)
|
||||
|
||||
if timer == self.plantTimer:
|
||||
elif timer == self.plantTimer:
|
||||
# Process plant
|
||||
self.plant.process()
|
||||
|
||||
if timer == self.reportTimer:
|
||||
# Process stirrer
|
||||
self.stirrer.process()
|
||||
|
||||
elif timer == self.reportTimer:
|
||||
self.report()
|
||||
|
||||
elif timer == self.processTimer:
|
||||
rast = self.rasten[self.rast_index]
|
||||
if rast is not None:
|
||||
self.ctrl_theta_soll = rast['temp']
|
||||
self.ctrl_heatrate_soll = rast['heatRate']
|
||||
self.rast_timer_soll = 60 * rast['time']
|
||||
isFinished = self.rast(rast)
|
||||
if isFinished:
|
||||
if not self.rastTimer.isActive and not self.rast_done:
|
||||
self.log ("Start rast timer with {} min.".format(self.rast_timer_soll/60))
|
||||
self.rastTimer.start(self.rast_timer_soll / self.sim_warp_factor, mode='oneshot')
|
||||
self.rast_done = True
|
||||
else:
|
||||
self.receipe_done = True
|
||||
|
||||
elif timer == self.rastTimer:
|
||||
rast = self.rasten[self.rast_index]
|
||||
if not rast["waitForUser"]:
|
||||
self.rast_index += 1
|
||||
self.rast_done = False
|
||||
self.ovenState = ovenStates.IDLE
|
||||
|
||||
self.log("Rast timer elapsed")
|
||||
|
||||
def log(self, s):
|
||||
now = time.time()
|
||||
print("{:.2f}: {}".format(now, s))
|
||||
|
||||
def report(self):
|
||||
if self.rast_running:
|
||||
self.log("Temp SOLL {} °C".format(self.ctrl_theta_soll))
|
||||
self.log("Temp IST = {:0.1f} °C".format(self.ctrl_theta))
|
||||
self.log("Heatrate SOLL = {:0.1f} °C/min.".format(self.ctrl_heatrate_soll))
|
||||
self.log("Heatrate IST = {:0.1f} °C/min.".format(self.ctrl_heatrate))
|
||||
self.log("Power = {:0.0f} W".format(self.power_soll))
|
||||
if self.timer_ist > 0:
|
||||
self.log("Rast remaining : {:0.1f} min.".format(self.timer_ist/60))
|
||||
self.log("Temp SOLL {} °C".format(self.ctrl_theta_soll))
|
||||
self.log("Temp IST = {:0.1f} °C".format(self.ctrl_theta))
|
||||
self.log("Heatrate SOLL = {:0.1f} °C/min.".format(self.ctrl_heatrate_soll))
|
||||
self.log("Heatrate IST = {:0.1f} °C/min.".format(self.ctrl_heatrate))
|
||||
self.log("Power = {:0.0f} W".format(self.power_soll))
|
||||
if self.rastTimer.isActive:
|
||||
self.log("Rast remaining : {:0.1f} min.".format(self.rastTimer.elapseDuration / 60))
|
||||
|
||||
|
||||
def start(self, recipe):
|
||||
self.receipe = recipe
|
||||
def setRecipe(self, recipe):
|
||||
print(json.dumps({recipe['Name'] : recipe}, indent=4, sort_keys=True))
|
||||
|
||||
self.sensorTimer.start(self.sensor_dt/self.sim_warp_factor)
|
||||
self.plantTimer.start(1/self.sim_warp_factor)
|
||||
self.reportTimer.start(self.report_interval)
|
||||
self.rastTimer.start(self.dt/self.sim_warp_factor)
|
||||
self.rast_running = False
|
||||
|
||||
self.thread = threading.Thread(target=self.receipe_run, args=(recipe,))
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self.thread != None:
|
||||
self.receipe = None
|
||||
self.thread.join()
|
||||
self.thread = None
|
||||
|
||||
def pause(self):
|
||||
if self.thread != None:
|
||||
self.ovenState = ovenStates.HOLD
|
||||
|
||||
def cont(self):
|
||||
if self.thread != None:
|
||||
self.ovenState = ovenStates.NOP
|
||||
|
||||
def wait_finished(self):
|
||||
self.receipe_sema.acquire()
|
||||
pass
|
||||
|
||||
def receipe_run(self, recipe):
|
||||
self.receipe = recipe
|
||||
self.rasten = recipe['Rasten']
|
||||
self.rasten.append(None)
|
||||
|
||||
# Stirrer
|
||||
self.stirrSpeedHeat = recipe['stirrSpeedHeat']
|
||||
@@ -144,74 +153,73 @@ class Controller(TimerListener):
|
||||
self.stirrDutyRast = recipe['stirrDutyRast']
|
||||
self.stirrCycleTime = recipe['stirrCycleTime']
|
||||
|
||||
# Stirrer and plant
|
||||
self.stirrer.activate()
|
||||
self.plant.activate(True)
|
||||
def wait_finished(self):
|
||||
self.receipe_sema.acquire()
|
||||
pass
|
||||
|
||||
self.stirrer.setCycleTime(self.stirrCycleTime)
|
||||
def main(self):
|
||||
self.sensorTimer.start(self.sensor_dt/self.sim_warp_factor)
|
||||
self.plantTimer.start(1/self.sim_warp_factor)
|
||||
|
||||
self.time = 0
|
||||
rasten = recipe['Rasten']
|
||||
rasten.append(None)
|
||||
rast_count = 0
|
||||
self.timer_ist = 0
|
||||
while(self.receipe is not None):
|
||||
run = True
|
||||
while(run and not self.receipe_done):
|
||||
self.timerMgr.process()
|
||||
if self.rastTimer.isElapsed():
|
||||
rast = rasten[rast_count]
|
||||
if rast is not None:
|
||||
isFinished = self.rast(rast)
|
||||
if isFinished:
|
||||
rast_count += 1
|
||||
self.timer_ist = 0
|
||||
else:
|
||||
break
|
||||
wait = self.timerMgr.getMinTimeout(1.0/self.sim_warp_factor)
|
||||
try:
|
||||
msg = self.queue.get(block=True, timeout=wait)
|
||||
print ("Received: ", msg)
|
||||
if msg == commands.PAUSE:
|
||||
pass
|
||||
|
||||
waut = self.timerMgr.getMinTimeout()
|
||||
time.sleep(waut)
|
||||
elif msg == commands.RUN:
|
||||
if not self.processTimer.isActive:
|
||||
self.time = 0
|
||||
self.rast_index = 0
|
||||
self.reportTimer.start(self.report_interval)
|
||||
self.processTimer.start(self.dt / self.sim_warp_factor)
|
||||
|
||||
# Stirrer and plant
|
||||
self.stirrer.activate()
|
||||
self.plant.activate(True)
|
||||
self.stirrer.setCycleTime(self.stirrCycleTime)
|
||||
self.receipe_done = False
|
||||
else:
|
||||
self.rast_index += 1
|
||||
self.rast_done = False
|
||||
self.ovenState = ovenStates.IDLE
|
||||
|
||||
elif msg == commands.ABORT:
|
||||
run = False
|
||||
|
||||
elif msg == commands.STIRR_ON:
|
||||
self.stirrer.activate()
|
||||
|
||||
elif msg == commands.STIRR_OFF:
|
||||
self.stirrer.deactivate()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Stirrer and plant
|
||||
self.stirrer.deactivate()
|
||||
self.plant.activate(False)
|
||||
|
||||
self.receipe_sema.release()
|
||||
|
||||
def rast(self, rast):
|
||||
|
||||
# Temperature
|
||||
self.ctrl_theta_soll = rast['temp']
|
||||
self.ctrl_theta = self.theta_ist_k
|
||||
self.ctrl_heatrate_soll = rast['heatRate']
|
||||
self.ctrl_heatrate = self.heatrate_ist_k
|
||||
|
||||
# Timer
|
||||
timer_soll = 60*rast['time']
|
||||
|
||||
# ------------------------------
|
||||
# The loop
|
||||
# ------------------------------
|
||||
ovenStateNext = self.ovenState
|
||||
|
||||
# -----------------------------------------
|
||||
self.stirrer.process()
|
||||
|
||||
ctrl_theta = self.ctrl_theta
|
||||
ctrl_heatrate = self.ctrl_heatrate
|
||||
|
||||
ctrl_theta_err = self.ctrl_theta_soll - ctrl_theta
|
||||
ctrl_heatrate_err = self.ctrl_heatrate_soll - ctrl_heatrate
|
||||
|
||||
if self.ovenState == ovenStates.NOP:
|
||||
if ctrl_theta < self.ctrl_theta_soll:
|
||||
ovenStateNext = ovenStates.HEAT
|
||||
|
||||
if self.ovenState == ovenStates.HEAT:
|
||||
if (ctrl_theta + 1.0) >= self.ctrl_theta_soll:
|
||||
ovenStateNext = ovenStates.HOLD
|
||||
self.log("Set rast timer to {:0.1f} min.".format(timer_soll/60))
|
||||
self.timer_ist = timer_soll
|
||||
|
||||
if self.ovenState != ovenStates.NOP:
|
||||
if self.ovenState != ovenStates.IDLE:
|
||||
self.pid_hold.process(self.dt, self.params['Hold']['Pid'], ctrl_theta_err)
|
||||
self.pid_rate.process(self.dt, self.params['Heat']['Pid'], ctrl_heatrate_err)
|
||||
|
||||
@@ -221,49 +229,50 @@ class Controller(TimerListener):
|
||||
power_hold = max(self.params['P_min'], min(self.params['P_max'], power_hold))
|
||||
power_heat = max(self.params['P_min'], min(self.params['P_max'], power_heat))
|
||||
|
||||
power_soll = min(power_hold, power_hold)
|
||||
self.power_soll = power_hold
|
||||
if self.ovenState == ovenStates.HEAT:
|
||||
power_soll = min(power_heat, power_heat)
|
||||
self.power_soll = power_heat
|
||||
|
||||
self.plant.setPower(power_soll)
|
||||
power_ist = self.plant.getPower()
|
||||
self.plant.setPower(self.power_soll)
|
||||
|
||||
self.time_v = np.append(self.time_v, self.time/60)
|
||||
self.theta_v = np.append(self.theta_v, ctrl_theta)
|
||||
self.heatrate_v = np.append(self.heatrate_v, ctrl_heatrate)
|
||||
self.power_v = np.append(self.power_v, power_soll)
|
||||
self.power_v = np.append(self.power_v, self.power_soll)
|
||||
self.power_hold_v = np.append(self.power_hold_v, power_hold)
|
||||
self.power_heat_v = np.append(self.power_heat_v, power_heat)
|
||||
self.error_v = np.append(self.error_v, ctrl_theta_err)
|
||||
self.power_soll = power_soll
|
||||
|
||||
self.time += self.dt
|
||||
|
||||
if self.timer_ist > 0:
|
||||
self.timer_ist -= self.dt
|
||||
else:
|
||||
self.timer_ist = 0
|
||||
if self.ovenState == ovenStates.HOLD:
|
||||
if self.timer_ist == 0:
|
||||
if not rast["waitForUser"]:
|
||||
ovenStateNext = ovenStates.NOP
|
||||
|
||||
# -----------------------------------------
|
||||
rast_finished = False
|
||||
|
||||
if self.ovenState == ovenStates.IDLE:
|
||||
if (ctrl_theta + 1.0) < self.ctrl_theta_soll:
|
||||
ovenStateNext = ovenStates.HEAT
|
||||
|
||||
if self.ovenState == ovenStates.HEAT:
|
||||
if (ctrl_theta + 1.0) >= self.ctrl_theta_soll:
|
||||
ovenStateNext = ovenStates.HOLD
|
||||
|
||||
if ovenStateNext != self.ovenState:
|
||||
self.log("{} -> {}".format(self.ovenState, ovenStateNext))
|
||||
if ovenStateNext == ovenStates.HEAT:
|
||||
self.power_hold_offset = power_hold
|
||||
self.pid_rate.reset()
|
||||
self.stirrer.setSpeed(self.stirrSpeedHeat)
|
||||
self.stirrer.setDutyCycle(1.0)
|
||||
if self.ovenState == ovenStates.HOLD:
|
||||
self.power_hold_offset = power_hold
|
||||
|
||||
if ovenStateNext == ovenStates.HOLD:
|
||||
self.power_heat_offset = power_heat
|
||||
self.pid_hold.reset()
|
||||
self.stirrer.setSpeed(self.stirrSpeedRast)
|
||||
self.stirrer.setDutyCycle(self.stirrDutyRast)
|
||||
if self.ovenState == ovenStates.HEAT:
|
||||
self.power_heat_offset = power_heat
|
||||
|
||||
# ------------------------------
|
||||
self.rast_running = True
|
||||
self.ovenState = ovenStateNext
|
||||
return self.ovenState == ovenStates.NOP
|
||||
return self.ovenState == ovenStates.HOLD
|
||||
|
||||
@@ -9,6 +9,7 @@ class Timer(object):
|
||||
self.mode = 'periodic'
|
||||
self.interval = 0
|
||||
self.elapseTime = 0
|
||||
self.elapseDuration = 0
|
||||
self.count = 0
|
||||
|
||||
def start(self, interval, mode='periodic'):
|
||||
@@ -29,9 +30,11 @@ class Timer(object):
|
||||
def isElapsed(self):
|
||||
result = False
|
||||
now = time.time()
|
||||
self.elapseDuration = max(0, self.elapseTime - now)
|
||||
if self.isActive:
|
||||
if self.elapseTime <= now:
|
||||
if self.elapseDuration == 0:
|
||||
result = True
|
||||
self.isActive = False
|
||||
if 'periodic' in self.mode:
|
||||
self.start(self.interval, self.mode)
|
||||
|
||||
@@ -61,7 +64,6 @@ class TimerManager(object):
|
||||
timer.count += 1
|
||||
|
||||
def getMinTimeout(self, default=1.0):
|
||||
minTimeout = default
|
||||
timeouts = []
|
||||
for timer in self.timers:
|
||||
now = time.time()
|
||||
@@ -72,7 +74,7 @@ class TimerManager(object):
|
||||
if len(timeouts) > 0:
|
||||
minTimeout = min(timeouts)
|
||||
|
||||
return minTimeout
|
||||
return max(default, minTimeout)
|
||||
|
||||
if __name__ == '__main__':
|
||||
class Test (TimerListener):
|
||||
|
||||
Reference in New Issue
Block a user