diff --git a/Control/brewpi/Rezept-001.json b/Control/brewpi/Rezept-001.json index 88e17ef..67c754d 100644 --- a/Control/brewpi/Rezept-001.json +++ b/Control/brewpi/Rezept-001.json @@ -1,13 +1,37 @@ { "Name" : "Rezept-001", - "Schuettung_kg" : 6, - "Wasser_kg" : 18, + "Schuettung_kg" : 6, + "Wasser_kg" : 18, + "stirrRpmHeat" : 999, + "stirrRpmRast" : 555, + "stirrDutyRast" : 0.2, + "stirrCycleTime" : 120, "Rasten" : [ - {"time" : 15, "temp" : 50.0, "heatRate" : 1.0, "stirDutyCycle" : 0.5, "waitForUser" : true}, - {"time" : 20, "temp" : 60.0, "heatRate" : 0.5, "stirDutyCycle" : 0.5, "waitForUser" : false}, - {"time" : 30, "temp" : 70.0, "heatRate" : 0.5, "stirDutyCycle" : 0.5, "waitForUser" : false}, - {"time" : 5, "temp" : 80.0, "heatRate" : 0.5, "stirDutyCycle" : 0.5, "waitForUser" : true} + { + "time" : 15, + "temp" : 50.0, + "heatRate" : 0.25, + "waitForUser" : true + }, + { + "time" : 20, + "temp" : 60.0, + "heatRate" : 0.50, + "waitForUser" : false + }, + { + "time" : 30, + "temp" : 70.0, + "heatRate" : 0.75, + "waitForUser" : false + }, + { + "time" : 5, + "temp" : 80.0, + "heatRate" : 1.00, + "waitForUser" : true + } ] } diff --git a/Control/brewpi/aplant.py b/Control/brewpi/aplant.py index 7509134..b0cdcf6 100644 --- a/Control/brewpi/aplant.py +++ b/Control/brewpi/aplant.py @@ -7,7 +7,7 @@ class APlant(abc.ABC): pass @abc.abstractmethod - def process(self, dt): + def process(self): pass @abc.abstractmethod diff --git a/Control/brewpi/astirrer.py b/Control/brewpi/astirrer.py new file mode 100644 index 0000000..2f722e6 --- /dev/null +++ b/Control/brewpi/astirrer.py @@ -0,0 +1,35 @@ +import numpy as np +import abc + + +class AStirrer(abc.ABC): + def __init__(self): + pass + + @abc.abstractmethod + def process(self): + pass + + @abc.abstractmethod + def setRpm(self, rpm): + pass + + @abc.abstractmethod + def setCycleTime(self, time): + pass + + @abc.abstractmethod + def setDutyCycle(self, dutyCycle): + pass + + @abc.abstractmethod + def getRpm(self): + return None + + @abc.abstractmethod + def start(self): + pass + + @abc.abstractmethod + def stop(self): + pass \ No newline at end of file diff --git a/Control/brewpi/brewpi.cfg.json b/Control/brewpi/brewpi.cfg.json index effcfc2..3785f12 100644 --- a/Control/brewpi/brewpi.cfg.json +++ b/Control/brewpi/brewpi.cfg.json @@ -37,23 +37,29 @@ }, "Track": { "Pid" : { - "kp" : 8, - "ki" : 0.03, - "kd" : 8, + "kp" : 1, + "ki" : 0.2, + "kd" : 4, "rho" : 0.999 } } } }, - "Simulation" : + "WaterSim" : { + "dt" : 1.0, "theta_amb" : 20, "C" : 4190, "M" : 20, "L" : 0.3, "Td" : 10, "kn" : 0 + }, + + "Stirrer" : + { + "dt" : 1.0 } } diff --git a/Control/brewpi/controller.py b/Control/brewpi/controller.py index a8b948c..1fadb09 100644 --- a/Control/brewpi/controller.py +++ b/Control/brewpi/controller.py @@ -11,11 +11,12 @@ controllerStates = Enum('controllerStates', 'ACQU TRACK') class Controller(): - def __init__(self, params, plant): + def __init__(self, params, plant, stirrer): print(json.dumps({'Controller': params}, indent=4, sort_keys=True)) self.ovenState = ovenStates.NOP self.controllerState = controllerStates.ACQU self.plant = plant + self.stirrer = stirrer self.dt = params['dt'] self.params = params self.pid = Pid() @@ -23,20 +24,23 @@ class Controller(): self.heatrate_v = np.empty(0) self.time_v = np.empty(0) self.power_v = np.empty(0) - self.theta_err_v = np.empty(0) - self.heatrate_err_v = np.empty(0) - self.theta_ist = 0 + self.error_v = np.empty(0) self.time = 0 - pass def start(self, recipe): print(json.dumps({recipe['Name'] : recipe}, indent=4, sort_keys=True)) + # Stirrer + self.stirrRpmHeat = recipe['stirrRpmHeat'] + self.stirrRpmRast = recipe['stirrRpmRast'] + self.stirrDutyRast = recipe['stirrDutyRast'] + self.stirrCycleTime = recipe['stirrCycleTime'] + self.time = 0 + for rast in recipe['Rasten']: self.rast(rast) - def stop(self): figure(1) subplot(4, 1, 1) @@ -50,8 +54,8 @@ class Controller(): grid(True) ylabel('W') subplot(4, 1, 3) - plot(self.time_v, self.theta_err_v, 'b-', linewidth=1) - title('Temperature Error') + plot(self.time_v, self.error_v, 'b-', linewidth=1) + title('Error') grid(True) ylabel('°C') subplot(4, 1, 4) @@ -63,53 +67,58 @@ class Controller(): show() def rast(self, rast): - temp_soll = rast['temp'] - heatrate_soll = rast['heatRate'] - print("Target temperature {} °C".format(temp_soll)) - doLoop = True - timer_ist = 0 - heatrate_ist = 0 - timer_soll = 60*rast['time']/self.dt - heatRateMeasureInterval = 1 - heatRateMeasureCount = 1 - theta_err_sm = Smoother(0.5) - heatrate_err_sm = Smoother(0.5) + # Stirrer + self.stirrer.start() + self.stirrer.setCycleTime(self.stirrCycleTime) + + # Temperature + theta_last = self.plant.getTemperature() + theta_soll = rast['temp'] + heatrate_soll = rast['heatRate'] + print("Target temperature {} °C".format(theta_soll)) + + # Timer + timer_ist = 0 + timer_soll = 60*rast['time']/self.dt # ------------------------------ # The loop # ------------------------------ + doLoop = True + theta_err_sm = Smoother(0.5) + heatrate_err_sm = Smoother(0.5) while(doLoop): controllerStateNext = self.controllerState ovenStateNext = self.ovenState # ----------------------------------------- - self.plant.process(self.dt) + self.stirrer.process() + self.plant.process() theta_ist = self.plant.getTemperature() - if heatRateMeasureCount <= 0: - heatrate_ist = 60 * (theta_ist - self.theta_ist) / heatRateMeasureInterval; - heatRateMeasureCount = heatRateMeasureInterval; - theta_last = theta_ist; + heatrate_ist = 60/self.dt * (theta_ist - theta_last) - heatRateMeasureCount = heatRateMeasureCount - self.dt; -# print ("{}: Temp IST = {:0.2f} °C".format(timer_ist, temp_ist)) + # print ("{}: Temp IST = {:0.2f} °C".format(timer_ist, temp_ist)) - theta_err = temp_soll - theta_ist + theta_err = theta_soll - theta_ist heatrate_err = heatrate_soll - heatrate_ist theta_err_sm.process(theta_err) heatrate_err_sm.process(heatrate_err) if self.ovenState == ovenStates.NOP: - if theta_ist < temp_soll: + if theta_ist < theta_soll: ovenStateNext = ovenStates.HEAT + pid_err = 0 if self.ovenState == ovenStates.HEAT: pid_err = heatrate_err_sm.get_y() pid_params_acqu = self.params['Heat']['Acqu']['Pid'] pid_params_track = self.params['Heat']['Track']['Pid'] - if theta_ist >= temp_soll: + pid_thresh_acqu = 0.2 + pid_thresh_track = 0.5 + if theta_ist >= theta_soll: ovenStateNext = ovenStates.HOLD timer_ist = 0 @@ -117,6 +126,8 @@ class Controller(): pid_err = theta_err_sm.get_y() pid_params_acqu = self.params['Hold']['Acqu']['Pid'] pid_params_track = self.params['Hold']['Track']['Pid'] + pid_thresh_acqu = 0.2 + pid_thresh_track = 0.5 timer_ist += self.dt if timer_ist >= timer_soll: ovenStateNext = ovenStates.NOP @@ -125,42 +136,49 @@ class Controller(): if self.ovenState != ovenStates.NOP: if self.controllerState == controllerStates.ACQU: pid_params = pid_params_acqu - if abs(theta_err) < 0.2: + if abs(pid_err) < pid_thresh_acqu: controllerStateNext = controllerStates.TRACK - if self.controllerState == controllerStates.TRACK: pid_params = pid_params_track - if abs(theta_err) > 0.5: + if abs(pid_err) > pid_thresh_track: controllerStateNext = controllerStates.ACQU - self.pid.process(pid_params, pid_err) + self.pid.process(self.dt, pid_params, pid_err) y = self.pid.get_y() -# print ("theta_err = {:0.2f} °C".format(theta_err)) - power = max(self.params['P_min'], min(self.params['P_max'], self.params['P_max']*y)) -# print ("Power = {:0.2f} W".format(power)) self.plant.setPower(power) self.time += self.dt - self.theta_ist = theta_ist + theta_last = theta_ist self.theta_v = np.append(self.theta_v, theta_ist) self.heatrate_v = np.append(self.heatrate_v, heatrate_ist) self.time_v = np.append(self.time_v, self.time/60) self.power_v = np.append(self.power_v, power) - self.theta_err_v = np.append(self.theta_err_v, theta_err_sm.get_y()) - self.heatrate_err_v = np.append(self.heatrate_err_v, heatrate_err_sm.get_y()) + self.error_v = np.append(self.error_v, pid_err) + #self.error_v = np.append(self.error_v, self.stirrer.getRpm()) + + # print ("theta_err = {:0.2f} °C".format(theta_err)) + # print ("Power = {:0.2f} W".format(power)) # ----------------------------------------- + if ovenStateNext != self.ovenState: + print("{} -> {}".format(self.ovenState, ovenStateNext)) + if ovenStateNext == ovenStates.HEAT: + self.stirrer.setRpm(self.stirrRpmHeat) + self.stirrer.setDutyCycle(1.0) + + if ovenStateNext == ovenStates.HOLD: + self.stirrer.setRpm(self.stirrRpmRast) + self.stirrer.setDutyCycle(self.stirrDutyRast) + if controllerStateNext != self.controllerState: print("{} -> {}".format(self.controllerState, controllerStateNext)) - if ovenStateNext != self.ovenState: - print("{} -> {}".format(self.ovenState, ovenStateNext)) - - self.controllerState = controllerStateNext self.ovenState = ovenStateNext + self.controllerState = controllerStateNext + # ------------------------------ - # ------------------------------ + self.stirrer.stop() diff --git a/Control/brewpi/main.py b/Control/brewpi/main.py index b3cf6bb..4100a4b 100644 --- a/Control/brewpi/main.py +++ b/Control/brewpi/main.py @@ -6,20 +6,20 @@ import configparser from pid import Pid from controller import Controller from mass import Mass - +from stirrer import Stirrer if __name__ == '__main__': fp = open("brewpi.cfg.json") configJson = json.load(fp) - plant = Mass(configJson["Simulation"]) - temp = plant.getTemperature() + plant = Mass(configJson["WaterSim"]) + ruehrer = Stirrer(configJson["Stirrer"]) + ablauf = Controller(configJson["Controller"], plant, ruehrer) fp = open("Rezept-001.json") recipeJson = json.load(fp) - ablauf = Controller(configJson["Controller"], plant) ablauf.start(recipeJson) ablauf.stop() diff --git a/Control/brewpi/mass.py b/Control/brewpi/mass.py index c5accd5..b83d6c1 100644 --- a/Control/brewpi/mass.py +++ b/Control/brewpi/mass.py @@ -5,9 +5,12 @@ from aplant import APlant class Mass(APlant): def __init__(self, params): print(json.dumps({'Mass': params}, indent=4, sort_keys=True)) + self.dt = params['dt'] + self.alpha = 1.0 + self.e = 0 self.x = 0 - self.gain = 0.8 + self.gain = 0.95 self.C = params['C'] self.M = params['M'] @@ -18,13 +21,12 @@ class Mass(APlant): self.theta = 0 self.P = 0 - def process(self, dt): - alpha = 1.0 if self.Td > 0: - alpha = dt/self.Td + self.alpha = self.dt/self.Td - self.e = self.e*(1 - ((self.L*self.theta)*dt)/(self.M*self.C)) - self.x = (1-alpha)*self.x + self.gain*alpha*self.P*dt + def process(self): + self.e = self.e*(1 - ((self.L*self.theta)*self.dt)/(self.M*self.C)) + self.x = (1-self.alpha)*self.x + self.gain*self.alpha*self.P self.e += self.x self.theta = self.e/(self.M*self.C) diff --git a/Control/brewpi/pid.py b/Control/brewpi/pid.py index fb84a4d..f77d1bc 100644 --- a/Control/brewpi/pid.py +++ b/Control/brewpi/pid.py @@ -11,7 +11,7 @@ class Pid(): # Auto windup self.y_min = 0.0 - self.y_max = 80.0 + self.y_max = 100.0 self.diff_aw = 0.0 # Output @@ -23,7 +23,7 @@ class Pid(): p = dict(kp=kp, ki=ki, kd=kd, rho=rho) return p - def process(self, params, err): + def process(self, dt, params, err): kp = params['kp'] ki = params['ki'] kd = params['kd'] @@ -34,8 +34,8 @@ class Pid(): yd = err - self.xd _yp = kp * err - _yi = ki * yi - _yd = kd * yd + _yi = ki*dt * yi + _yd = kd/dt * yd self.y = _yp + _yi + _yd diff --git a/Control/brewpi/stirrer.py b/Control/brewpi/stirrer.py new file mode 100644 index 0000000..36e7c7f --- /dev/null +++ b/Control/brewpi/stirrer.py @@ -0,0 +1,52 @@ +from astirrer import AStirrer + +class Stirrer(AStirrer): + def __init__(self, params): + self.params = params + self.rpm = 0 + self.cycleTime = 1 + self.dutyCycle = 1 + self.cycleCounter = 0 + self.isOn = 0 + self.isMasterOn = 0 + + def process(self): + dt = self.params["dt"] + + self.cycleCounter -= dt + if self.cycleCounter <= 0: + self.cycleCounter = self.cycleTime + + if self.cycleCounter <= (self.cycleTime * self.dutyCycle): + if not self.isOn: + print ("Stirrer: On") + self.isOn = 1 + else: + if self.isOn: + print ("Stirrer: Off") + self.isOn = 0 + + def setRpm(self, rpm): + print ("Stirrer: Set RPM to {} rpm".format(rpm)) + self.rpm = rpm + + def setCycleTime(self, time): + print ("Stirrer: Set cycle time to {} s".format(time)) + self.cycleTime = time + + def setDutyCycle(self, dutyCycle): + print ("Stirrer: Set duty cycle to {} %".format(100*dutyCycle)) + self.dutyCycle = dutyCycle + + def getRpm(self): + if self.isOn and self.isMasterOn: + return self.rpm + return 0 + + def start(self): + self.isMasterOn = 1 + print("Stirrer: switched On") + + def stop(self): + self.isMasterOn = 0 + print("Stirrer: switched Off") diff --git a/Control/brewpi/utils.py b/Control/brewpi/utils.py index efd6406..69d3765 100644 --- a/Control/brewpi/utils.py +++ b/Control/brewpi/utils.py @@ -6,6 +6,7 @@ class Smoother: def process(self, x): self.y = self.b*self.y + self.a*x + return self.y def get_y(self): return self.y @@ -15,6 +16,7 @@ class Stable: self.stable_time = stable_count self.count = stable_count self.err_max = err_max + self.is_stable = 0 self.x = 0 def process(self, x): @@ -24,6 +26,9 @@ class Stable: if abs(x - self.x) >= self.err_max: self.count = self.stable_time - is_stable = (self.count == 0) + self.is_stable = (self.count == 0) - return is_stable + return self.is_stable + + def is_stable(self): + return self.is_stable