[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:
2019-04-28 11:31:40 +00:00
parent 590a663282
commit dd7d8b8a01
3 changed files with 140 additions and 118 deletions
+18 -7
View File
@@ -2,7 +2,7 @@
import signal import signal
import json import json
from controller import Controller from controller import Controller, commands
from mass import Mass from mass import Mass
from stirrer import Stirrer from stirrer import Stirrer
#from pololu1376 import Pololu1376 #from pololu1376 import Pololu1376
@@ -49,20 +49,31 @@ def results_plot(self):
if __name__ == '__main__': if __name__ == '__main__':
stirr_isOn = False
def handler(signum, frame): def handler(signum, frame):
global stirr_isOn
print('Signal handler called with signal', signum) print('Signal handler called with signal', signum)
if signum == signal.SIGINT: if signum == signal.SIGINT:
ablauf.stop() ablauf.command(commands.ABORT)
if signum == signal.SIGSTOP: if signum == signal.SIGSTOP:
ablauf.pause() ablauf.command(commands.PAUSE)
if signum == signal.SIGCONT: if signum == signal.SIGUSR1:
ablauf.cont() 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 # Set the signal handler and a 5-second alarm
signal.signal(signal.SIGABRT, handler) signal.signal(signal.SIGABRT, handler)
signal.signal(signal.SIGINT, 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") fp = open("brewpi.json")
configJson = json.load(fp) configJson = json.load(fp)
@@ -77,7 +88,7 @@ if __name__ == '__main__':
fp = open("Rezept-001.json") fp = open("Rezept-001.json")
recipeJson = json.load(fp) recipeJson = json.load(fp)
ablauf.start(recipeJson) ablauf.setRecipe(recipeJson)
ablauf.wait_finished() ablauf.wait_finished()
results_plot(ablauf) results_plot(ablauf)
+117 -108
View File
@@ -7,24 +7,24 @@ import time
import threading import threading
from kalman import Kalman from kalman import Kalman
from timer import Timer, TimerManager, TimerListener from timer import Timer, TimerManager, TimerListener
import queue
ovenStates = Enum('ovenStates', 'IDLE HEAT HOLD')
ovenStates = Enum('ovenStates', 'NOP HEAT HOLD') states = Enum('controllerStates', 'IDLE NORMAL PAUSE')
commands = Enum('controllerCommands', 'RUN PAUSE ABORT STIRR_OFF STIRR_ON')
class Controller(TimerListener): class Controller(TimerListener):
def __init__(self, params, plant, stirrer): def __init__(self, params, plant, stirrer):
print(json.dumps({'Controller': params}, indent=4, sort_keys=True)) print(json.dumps({'Controller': params}, indent=4, sort_keys=True))
self.sim_warp_factor = params['sim_warp_factor'] self.sim_warp_factor = params['sim_warp_factor']
self.ovenState = ovenStates.NOP self.ovenState = ovenStates.IDLE
self.plant = plant self.plant = plant
self.stirrer = stirrer self.stirrer = stirrer
self.dt = params['dt'] self.dt = params['dt']
self.params = params self.params = params
self.pid_hold = Pid() self.pid_hold = Pid()
self.pid_rate = Pid() self.pid_rate = Pid()
self.timer_ist = 0
self.sensorTime_v = np.empty(0) self.sensorTime_v = np.empty(0)
self.theta_raw_v = np.empty(0) self.theta_raw_v = np.empty(0)
self.theta_v = np.empty(0) self.theta_v = np.empty(0)
@@ -50,16 +50,26 @@ class Controller(TimerListener):
self.kalman = Kalman(params['kalman']) self.kalman = Kalman(params['kalman'])
self.sensor_dt = params['kalman']['dt'] self.sensor_dt = params['kalman']['dt']
self.timerMgr = TimerManager() self.timerMgr = TimerManager()
self.processTimer = Timer("Process", self)
self.rastTimer = Timer("Rast", self) self.rastTimer = Timer("Rast", self)
self.sensorTimer = Timer("Sensor", self) self.sensorTimer = Timer("Sensor", self)
self.plantTimer = Timer("Plant", self) self.plantTimer = Timer("Plant", self)
self.reportTimer = Timer("Report", 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.sensorTimer)
self.timerMgr.registerTimer(self.plantTimer) self.timerMgr.registerTimer(self.plantTimer)
self.timerMgr.registerTimer(self.reportTimer) self.timerMgr.registerTimer(self.reportTimer)
self.rast_running = False
self.power_hold_offset = 200 self.power_hold_offset = 200
self.power_heat_offset = 1500 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): def onTimer(self, timer):
if timer == self.sensorTimer: 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.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) self.heatrate_k_v = np.append(self.heatrate_k_v, self.heatrate_ist_k)
if timer == self.plantTimer: elif timer == self.plantTimer:
# Process plant # Process plant
self.plant.process() self.plant.process()
if timer == self.reportTimer: # Process stirrer
self.stirrer.process()
elif timer == self.reportTimer:
self.report() 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): def log(self, s):
now = time.time() now = time.time()
print("{:.2f}: {}".format(now, s)) print("{:.2f}: {}".format(now, s))
def report(self): def report(self):
if self.rast_running: self.log("Temp SOLL {} °C".format(self.ctrl_theta_soll))
self.log("Temp SOLL {} °C".format(self.ctrl_theta_soll)) self.log("Temp IST = {:0.1f} °C".format(self.ctrl_theta))
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 SOLL = {:0.1f} °C/min.".format(self.ctrl_heatrate_soll)) self.log("Heatrate IST = {:0.1f} °C/min.".format(self.ctrl_heatrate))
self.log("Heatrate IST = {:0.1f} °C/min.".format(self.ctrl_heatrate)) self.log("Power = {:0.0f} W".format(self.power_soll))
self.log("Power = {:0.0f} W".format(self.power_soll)) if self.rastTimer.isActive:
if self.timer_ist > 0: self.log("Rast remaining : {:0.1f} min.".format(self.rastTimer.elapseDuration / 60))
self.log("Rast remaining : {:0.1f} min.".format(self.timer_ist/60))
def start(self, recipe): def setRecipe(self, recipe):
self.receipe = recipe
print(json.dumps({recipe['Name'] : recipe}, indent=4, sort_keys=True)) print(json.dumps({recipe['Name'] : recipe}, indent=4, sort_keys=True))
self.receipe = recipe
self.sensorTimer.start(self.sensor_dt/self.sim_warp_factor) self.rasten = recipe['Rasten']
self.plantTimer.start(1/self.sim_warp_factor) self.rasten.append(None)
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):
# Stirrer # Stirrer
self.stirrSpeedHeat = recipe['stirrSpeedHeat'] self.stirrSpeedHeat = recipe['stirrSpeedHeat']
@@ -144,74 +153,73 @@ class Controller(TimerListener):
self.stirrDutyRast = recipe['stirrDutyRast'] self.stirrDutyRast = recipe['stirrDutyRast']
self.stirrCycleTime = recipe['stirrCycleTime'] self.stirrCycleTime = recipe['stirrCycleTime']
# Stirrer and plant def wait_finished(self):
self.stirrer.activate() self.receipe_sema.acquire()
self.plant.activate(True) 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 run = True
rasten = recipe['Rasten'] while(run and not self.receipe_done):
rasten.append(None)
rast_count = 0
self.timer_ist = 0
while(self.receipe is not None):
self.timerMgr.process() self.timerMgr.process()
if self.rastTimer.isElapsed(): wait = self.timerMgr.getMinTimeout(1.0/self.sim_warp_factor)
rast = rasten[rast_count] try:
if rast is not None: msg = self.queue.get(block=True, timeout=wait)
isFinished = self.rast(rast) print ("Received: ", msg)
if isFinished: if msg == commands.PAUSE:
rast_count += 1 pass
self.timer_ist = 0
else:
break
waut = self.timerMgr.getMinTimeout() elif msg == commands.RUN:
time.sleep(waut) 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 # Stirrer and plant
self.stirrer.deactivate() self.stirrer.deactivate()
self.plant.activate(False) self.plant.activate(False)
self.receipe_sema.release() self.receipe_sema.release()
def rast(self, rast): def rast(self, rast):
# Temperature # Temperature
self.ctrl_theta_soll = rast['temp']
self.ctrl_theta = self.theta_ist_k self.ctrl_theta = self.theta_ist_k
self.ctrl_heatrate_soll = rast['heatRate']
self.ctrl_heatrate = self.heatrate_ist_k self.ctrl_heatrate = self.heatrate_ist_k
# Timer
timer_soll = 60*rast['time']
# ------------------------------
# The loop
# ------------------------------
ovenStateNext = self.ovenState ovenStateNext = self.ovenState
# ----------------------------------------- # -----------------------------------------
self.stirrer.process()
ctrl_theta = self.ctrl_theta ctrl_theta = self.ctrl_theta
ctrl_heatrate = self.ctrl_heatrate ctrl_heatrate = self.ctrl_heatrate
ctrl_theta_err = self.ctrl_theta_soll - ctrl_theta ctrl_theta_err = self.ctrl_theta_soll - ctrl_theta
ctrl_heatrate_err = self.ctrl_heatrate_soll - ctrl_heatrate ctrl_heatrate_err = self.ctrl_heatrate_soll - ctrl_heatrate
if self.ovenState == ovenStates.NOP: if self.ovenState != ovenStates.IDLE:
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:
self.pid_hold.process(self.dt, self.params['Hold']['Pid'], ctrl_theta_err) 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) 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_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_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: if self.ovenState == ovenStates.HEAT:
power_soll = min(power_heat, power_heat) self.power_soll = power_heat
self.plant.setPower(power_soll) self.plant.setPower(self.power_soll)
power_ist = self.plant.getPower()
self.time_v = np.append(self.time_v, self.time/60) self.time_v = np.append(self.time_v, self.time/60)
self.theta_v = np.append(self.theta_v, ctrl_theta) self.theta_v = np.append(self.theta_v, ctrl_theta)
self.heatrate_v = np.append(self.heatrate_v, ctrl_heatrate) 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_hold_v = np.append(self.power_hold_v, power_hold)
self.power_heat_v = np.append(self.power_heat_v, power_heat) self.power_heat_v = np.append(self.power_heat_v, power_heat)
self.error_v = np.append(self.error_v, ctrl_theta_err) self.error_v = np.append(self.error_v, ctrl_theta_err)
self.power_soll = power_soll
self.time += self.dt 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: if ovenStateNext != self.ovenState:
self.log("{} -> {}".format(self.ovenState, ovenStateNext)) self.log("{} -> {}".format(self.ovenState, ovenStateNext))
if ovenStateNext == ovenStates.HEAT: if ovenStateNext == ovenStates.HEAT:
self.power_hold_offset = power_hold
self.pid_rate.reset() self.pid_rate.reset()
self.stirrer.setSpeed(self.stirrSpeedHeat) self.stirrer.setSpeed(self.stirrSpeedHeat)
self.stirrer.setDutyCycle(1.0) self.stirrer.setDutyCycle(1.0)
if self.ovenState == ovenStates.HOLD:
self.power_hold_offset = power_hold
if ovenStateNext == ovenStates.HOLD: if ovenStateNext == ovenStates.HOLD:
self.power_heat_offset = power_heat
self.pid_hold.reset() self.pid_hold.reset()
self.stirrer.setSpeed(self.stirrSpeedRast) self.stirrer.setSpeed(self.stirrSpeedRast)
self.stirrer.setDutyCycle(self.stirrDutyRast) self.stirrer.setDutyCycle(self.stirrDutyRast)
if self.ovenState == ovenStates.HEAT:
self.power_heat_offset = power_heat
# ------------------------------ # ------------------------------
self.rast_running = True self.rast_running = True
self.ovenState = ovenStateNext self.ovenState = ovenStateNext
return self.ovenState == ovenStates.NOP return self.ovenState == ovenStates.HOLD
+5 -3
View File
@@ -9,6 +9,7 @@ class Timer(object):
self.mode = 'periodic' self.mode = 'periodic'
self.interval = 0 self.interval = 0
self.elapseTime = 0 self.elapseTime = 0
self.elapseDuration = 0
self.count = 0 self.count = 0
def start(self, interval, mode='periodic'): def start(self, interval, mode='periodic'):
@@ -29,9 +30,11 @@ class Timer(object):
def isElapsed(self): def isElapsed(self):
result = False result = False
now = time.time() now = time.time()
self.elapseDuration = max(0, self.elapseTime - now)
if self.isActive: if self.isActive:
if self.elapseTime <= now: if self.elapseDuration == 0:
result = True result = True
self.isActive = False
if 'periodic' in self.mode: if 'periodic' in self.mode:
self.start(self.interval, self.mode) self.start(self.interval, self.mode)
@@ -61,7 +64,6 @@ class TimerManager(object):
timer.count += 1 timer.count += 1
def getMinTimeout(self, default=1.0): def getMinTimeout(self, default=1.0):
minTimeout = default
timeouts = [] timeouts = []
for timer in self.timers: for timer in self.timers:
now = time.time() now = time.time()
@@ -72,7 +74,7 @@ class TimerManager(object):
if len(timeouts) > 0: if len(timeouts) > 0:
minTimeout = min(timeouts) minTimeout = min(timeouts)
return minTimeout return max(default, minTimeout)
if __name__ == '__main__': if __name__ == '__main__':
class Test (TimerListener): class Test (TimerListener):