From dd7d8b8a0194092df51e1d3548fd2afc1b0d25ed Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 28 Apr 2019 11:31:40 +0000 Subject: [PATCH] [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 --- Control/brewpi/brewpi.py | 25 ++-- Control/brewpi/controller.py | 225 ++++++++++++++++++----------------- Control/brewpi/timer.py | 8 +- 3 files changed, 140 insertions(+), 118 deletions(-) diff --git a/Control/brewpi/brewpi.py b/Control/brewpi/brewpi.py index c5827c9..e667482 100755 --- a/Control/brewpi/brewpi.py +++ b/Control/brewpi/brewpi.py @@ -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) diff --git a/Control/brewpi/controller.py b/Control/brewpi/controller.py index 075e4f5..aaf5da5 100644 --- a/Control/brewpi/controller.py +++ b/Control/brewpi/controller.py @@ -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 diff --git a/Control/brewpi/timer.py b/Control/brewpi/timer.py index 2adb312..8f8ac5f 100644 --- a/Control/brewpi/timer.py +++ b/Control/brewpi/timer.py @@ -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):