- added stirrer

- many other improvements

git-svn-id: http://moon:8086/svn/projects/HendiControl@116 fda53097-d464-4ada-af97-ba876c37ca34
This commit is contained in:
2019-03-21 17:55:37 +00:00
parent 75b920047a
commit 08d8c32106
10 changed files with 213 additions and 71 deletions
+30 -6
View File
@@ -1,13 +1,37 @@
{ {
"Name" : "Rezept-001", "Name" : "Rezept-001",
"Schuettung_kg" : 6, "Schuettung_kg" : 6,
"Wasser_kg" : 18, "Wasser_kg" : 18,
"stirrRpmHeat" : 999,
"stirrRpmRast" : 555,
"stirrDutyRast" : 0.2,
"stirrCycleTime" : 120,
"Rasten" : "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" : 15,
{"time" : 30, "temp" : 70.0, "heatRate" : 0.5, "stirDutyCycle" : 0.5, "waitForUser" : false}, "temp" : 50.0,
{"time" : 5, "temp" : 80.0, "heatRate" : 0.5, "stirDutyCycle" : 0.5, "waitForUser" : true} "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
}
] ]
} }
+1 -1
View File
@@ -7,7 +7,7 @@ class APlant(abc.ABC):
pass pass
@abc.abstractmethod @abc.abstractmethod
def process(self, dt): def process(self):
pass pass
@abc.abstractmethod @abc.abstractmethod
+35
View File
@@ -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
+10 -4
View File
@@ -37,23 +37,29 @@
}, },
"Track": { "Track": {
"Pid" : { "Pid" : {
"kp" : 8, "kp" : 1,
"ki" : 0.03, "ki" : 0.2,
"kd" : 8, "kd" : 4,
"rho" : 0.999 "rho" : 0.999
} }
} }
} }
}, },
"Simulation" : "WaterSim" :
{ {
"dt" : 1.0,
"theta_amb" : 20, "theta_amb" : 20,
"C" : 4190, "C" : 4190,
"M" : 20, "M" : 20,
"L" : 0.3, "L" : 0.3,
"Td" : 10, "Td" : 10,
"kn" : 0 "kn" : 0
},
"Stirrer" :
{
"dt" : 1.0
} }
} }
+62 -44
View File
@@ -11,11 +11,12 @@ controllerStates = Enum('controllerStates', 'ACQU TRACK')
class Controller(): class Controller():
def __init__(self, params, plant): 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.ovenState = ovenStates.NOP self.ovenState = ovenStates.NOP
self.controllerState = controllerStates.ACQU self.controllerState = controllerStates.ACQU
self.plant = plant self.plant = plant
self.stirrer = stirrer
self.dt = params['dt'] self.dt = params['dt']
self.params = params self.params = params
self.pid = Pid() self.pid = Pid()
@@ -23,20 +24,23 @@ class Controller():
self.heatrate_v = np.empty(0) self.heatrate_v = np.empty(0)
self.time_v = np.empty(0) self.time_v = np.empty(0)
self.power_v = np.empty(0) self.power_v = np.empty(0)
self.theta_err_v = np.empty(0) self.error_v = np.empty(0)
self.heatrate_err_v = np.empty(0)
self.theta_ist = 0
self.time = 0 self.time = 0
pass
def start(self, recipe): def start(self, recipe):
print(json.dumps({recipe['Name'] : recipe}, indent=4, sort_keys=True)) 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 self.time = 0
for rast in recipe['Rasten']: for rast in recipe['Rasten']:
self.rast(rast) self.rast(rast)
def stop(self): def stop(self):
figure(1) figure(1)
subplot(4, 1, 1) subplot(4, 1, 1)
@@ -50,8 +54,8 @@ class Controller():
grid(True) grid(True)
ylabel('W') ylabel('W')
subplot(4, 1, 3) subplot(4, 1, 3)
plot(self.time_v, self.theta_err_v, 'b-', linewidth=1) plot(self.time_v, self.error_v, 'b-', linewidth=1)
title('Temperature Error') title('Error')
grid(True) grid(True)
ylabel('°C') ylabel('°C')
subplot(4, 1, 4) subplot(4, 1, 4)
@@ -63,53 +67,58 @@ class Controller():
show() show()
def rast(self, rast): 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) # Stirrer
heatrate_err_sm = Smoother(0.5) 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 # The loop
# ------------------------------ # ------------------------------
doLoop = True
theta_err_sm = Smoother(0.5)
heatrate_err_sm = Smoother(0.5)
while(doLoop): while(doLoop):
controllerStateNext = self.controllerState controllerStateNext = self.controllerState
ovenStateNext = self.ovenState ovenStateNext = self.ovenState
# ----------------------------------------- # -----------------------------------------
self.plant.process(self.dt) self.stirrer.process()
self.plant.process()
theta_ist = self.plant.getTemperature() theta_ist = self.plant.getTemperature()
if heatRateMeasureCount <= 0: heatrate_ist = 60/self.dt * (theta_ist - theta_last)
heatrate_ist = 60 * (theta_ist - self.theta_ist) / heatRateMeasureInterval;
heatRateMeasureCount = heatRateMeasureInterval;
theta_last = theta_ist;
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 heatrate_err = heatrate_soll - heatrate_ist
theta_err_sm.process(theta_err) theta_err_sm.process(theta_err)
heatrate_err_sm.process(heatrate_err) heatrate_err_sm.process(heatrate_err)
if self.ovenState == ovenStates.NOP: if self.ovenState == ovenStates.NOP:
if theta_ist < temp_soll: if theta_ist < theta_soll:
ovenStateNext = ovenStates.HEAT ovenStateNext = ovenStates.HEAT
pid_err = 0
if self.ovenState == ovenStates.HEAT: if self.ovenState == ovenStates.HEAT:
pid_err = heatrate_err_sm.get_y() pid_err = heatrate_err_sm.get_y()
pid_params_acqu = self.params['Heat']['Acqu']['Pid'] pid_params_acqu = self.params['Heat']['Acqu']['Pid']
pid_params_track = self.params['Heat']['Track']['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 ovenStateNext = ovenStates.HOLD
timer_ist = 0 timer_ist = 0
@@ -117,6 +126,8 @@ class Controller():
pid_err = theta_err_sm.get_y() pid_err = theta_err_sm.get_y()
pid_params_acqu = self.params['Hold']['Acqu']['Pid'] pid_params_acqu = self.params['Hold']['Acqu']['Pid']
pid_params_track = self.params['Hold']['Track']['Pid'] pid_params_track = self.params['Hold']['Track']['Pid']
pid_thresh_acqu = 0.2
pid_thresh_track = 0.5
timer_ist += self.dt timer_ist += self.dt
if timer_ist >= timer_soll: if timer_ist >= timer_soll:
ovenStateNext = ovenStates.NOP ovenStateNext = ovenStates.NOP
@@ -125,42 +136,49 @@ class Controller():
if self.ovenState != ovenStates.NOP: if self.ovenState != ovenStates.NOP:
if self.controllerState == controllerStates.ACQU: if self.controllerState == controllerStates.ACQU:
pid_params = pid_params_acqu pid_params = pid_params_acqu
if abs(theta_err) < 0.2: if abs(pid_err) < pid_thresh_acqu:
controllerStateNext = controllerStates.TRACK controllerStateNext = controllerStates.TRACK
if self.controllerState == controllerStates.TRACK: if self.controllerState == controllerStates.TRACK:
pid_params = pid_params_track pid_params = pid_params_track
if abs(theta_err) > 0.5: if abs(pid_err) > pid_thresh_track:
controllerStateNext = controllerStates.ACQU controllerStateNext = controllerStates.ACQU
self.pid.process(pid_params, pid_err) self.pid.process(self.dt, pid_params, pid_err)
y = self.pid.get_y() 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)) 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.plant.setPower(power)
self.time += self.dt self.time += self.dt
self.theta_ist = theta_ist theta_last = theta_ist
self.theta_v = np.append(self.theta_v, theta_ist) self.theta_v = np.append(self.theta_v, theta_ist)
self.heatrate_v = np.append(self.heatrate_v, heatrate_ist) self.heatrate_v = np.append(self.heatrate_v, heatrate_ist)
self.time_v = np.append(self.time_v, self.time/60) self.time_v = np.append(self.time_v, self.time/60)
self.power_v = np.append(self.power_v, power) 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.error_v = np.append(self.error_v, pid_err)
self.heatrate_err_v = np.append(self.heatrate_err_v, heatrate_err_sm.get_y()) #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: if controllerStateNext != self.controllerState:
print("{} -> {}".format(self.controllerState, controllerStateNext)) print("{} -> {}".format(self.controllerState, controllerStateNext))
if ovenStateNext != self.ovenState:
print("{} -> {}".format(self.ovenState, ovenStateNext))
self.controllerState = controllerStateNext
self.ovenState = ovenStateNext self.ovenState = ovenStateNext
self.controllerState = controllerStateNext
# ------------------------------
# ------------------------------ self.stirrer.stop()
+4 -4
View File
@@ -6,20 +6,20 @@ import configparser
from pid import Pid from pid import Pid
from controller import Controller from controller import Controller
from mass import Mass from mass import Mass
from stirrer import Stirrer
if __name__ == '__main__': if __name__ == '__main__':
fp = open("brewpi.cfg.json") fp = open("brewpi.cfg.json")
configJson = json.load(fp) configJson = json.load(fp)
plant = Mass(configJson["Simulation"]) plant = Mass(configJson["WaterSim"])
temp = plant.getTemperature() ruehrer = Stirrer(configJson["Stirrer"])
ablauf = Controller(configJson["Controller"], plant, ruehrer)
fp = open("Rezept-001.json") fp = open("Rezept-001.json")
recipeJson = json.load(fp) recipeJson = json.load(fp)
ablauf = Controller(configJson["Controller"], plant)
ablauf.start(recipeJson) ablauf.start(recipeJson)
ablauf.stop() ablauf.stop()
+8 -6
View File
@@ -5,9 +5,12 @@ from aplant import APlant
class Mass(APlant): class Mass(APlant):
def __init__(self, params): def __init__(self, params):
print(json.dumps({'Mass': params}, indent=4, sort_keys=True)) print(json.dumps({'Mass': params}, indent=4, sort_keys=True))
self.dt = params['dt']
self.alpha = 1.0
self.e = 0 self.e = 0
self.x = 0 self.x = 0
self.gain = 0.8 self.gain = 0.95
self.C = params['C'] self.C = params['C']
self.M = params['M'] self.M = params['M']
@@ -18,13 +21,12 @@ class Mass(APlant):
self.theta = 0 self.theta = 0
self.P = 0 self.P = 0
def process(self, dt):
alpha = 1.0
if self.Td > 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)) def process(self):
self.x = (1-alpha)*self.x + self.gain*alpha*self.P*dt 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.e += self.x
self.theta = self.e/(self.M*self.C) self.theta = self.e/(self.M*self.C)
+4 -4
View File
@@ -11,7 +11,7 @@ class Pid():
# Auto windup # Auto windup
self.y_min = 0.0 self.y_min = 0.0
self.y_max = 80.0 self.y_max = 100.0
self.diff_aw = 0.0 self.diff_aw = 0.0
# Output # Output
@@ -23,7 +23,7 @@ class Pid():
p = dict(kp=kp, ki=ki, kd=kd, rho=rho) p = dict(kp=kp, ki=ki, kd=kd, rho=rho)
return p return p
def process(self, params, err): def process(self, dt, params, err):
kp = params['kp'] kp = params['kp']
ki = params['ki'] ki = params['ki']
kd = params['kd'] kd = params['kd']
@@ -34,8 +34,8 @@ class Pid():
yd = err - self.xd yd = err - self.xd
_yp = kp * err _yp = kp * err
_yi = ki * yi _yi = ki*dt * yi
_yd = kd * yd _yd = kd/dt * yd
self.y = _yp + _yi + _yd self.y = _yp + _yi + _yd
+52
View File
@@ -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")
+7 -2
View File
@@ -6,6 +6,7 @@ class Smoother:
def process(self, x): def process(self, x):
self.y = self.b*self.y + self.a*x self.y = self.b*self.y + self.a*x
return self.y
def get_y(self): def get_y(self):
return self.y return self.y
@@ -15,6 +16,7 @@ class Stable:
self.stable_time = stable_count self.stable_time = stable_count
self.count = stable_count self.count = stable_count
self.err_max = err_max self.err_max = err_max
self.is_stable = 0
self.x = 0 self.x = 0
def process(self, x): def process(self, x):
@@ -24,6 +26,9 @@ class Stable:
if abs(x - self.x) >= self.err_max: if abs(x - self.x) >= self.err_max:
self.count = self.stable_time 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