- 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:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class APlant(abc.ABC):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def process(self, dt):
|
||||
def process(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user