kopia lustrzana https://github.com/jamesgao/kiln_controller
Backend state machine likely finished, need to test
rodzic
a1b47bd1b6
commit
59e60eab57
|
@ -0,0 +1,94 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("kiln.PID")
|
||||||
|
|
||||||
|
class PID(object):
|
||||||
|
"""
|
||||||
|
Discrete PID control
|
||||||
|
#The recipe gives simple implementation of a Discrete Proportional-Integral-Derivative (PID) controller. PID controller gives output value for error between desired reference input and measurement feedback to minimize error value.
|
||||||
|
#More information: http://en.wikipedia.org/wiki/PID_controller
|
||||||
|
#
|
||||||
|
#cnr437@gmail.com
|
||||||
|
#
|
||||||
|
####### Example #########
|
||||||
|
#
|
||||||
|
#p=PID(3.0,0.4,1.2)
|
||||||
|
#p.setPoint(5.0)
|
||||||
|
#while True:
|
||||||
|
# pid = p.update(measurement_value)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, P=2.0, I=0.0, D=1.0, Derivator=0, Integrator=0, Integrator_max=500, Integrator_min=-500):
|
||||||
|
|
||||||
|
self.Kp=P
|
||||||
|
self.Ki=I
|
||||||
|
self.Kd=D
|
||||||
|
self.Derivator=Derivator
|
||||||
|
self.Integrator=Integrator
|
||||||
|
self.Integrator_max=Integrator_max
|
||||||
|
self.Integrator_min=Integrator_min
|
||||||
|
|
||||||
|
self.set_point=0.0
|
||||||
|
self.error=0.0
|
||||||
|
|
||||||
|
def update(self,current_value):
|
||||||
|
"""
|
||||||
|
Calculate PID output value for given reference input and feedback
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.error = self.set_point - current_value
|
||||||
|
|
||||||
|
self.P_value = self.Kp * self.error
|
||||||
|
self.D_value = self.Kd * ( self.error - self.Derivator)
|
||||||
|
self.Derivator = self.error
|
||||||
|
|
||||||
|
self.Integrator = self.Integrator + self.error
|
||||||
|
|
||||||
|
if self.Integrator > self.Integrator_max:
|
||||||
|
self.Integrator = self.Integrator_max
|
||||||
|
elif self.Integrator < self.Integrator_min:
|
||||||
|
self.Integrator = self.Integrator_min
|
||||||
|
|
||||||
|
self.I_value = self.Integrator * self.Ki
|
||||||
|
PID = self.P_value + self.I_value + self.D_value
|
||||||
|
|
||||||
|
logger.info("P: %f, I: %f, D:%f, Output:%f"%(self.P_value, self.I_value, self.D_value, PID))
|
||||||
|
|
||||||
|
return PID
|
||||||
|
|
||||||
|
def setPoint(self,set_point):
|
||||||
|
"""
|
||||||
|
Initilize the setpoint of PID
|
||||||
|
"""
|
||||||
|
self.set_point = set_point
|
||||||
|
self.Integrator=0
|
||||||
|
self.Derivator=0
|
||||||
|
|
||||||
|
def setIntegrator(self, Integrator):
|
||||||
|
self.Integrator = Integrator
|
||||||
|
|
||||||
|
def setDerivator(self, Derivator):
|
||||||
|
self.Derivator = Derivator
|
||||||
|
|
||||||
|
def setKp(self,P):
|
||||||
|
self.Kp=P
|
||||||
|
|
||||||
|
def setKi(self,I):
|
||||||
|
self.Ki=I
|
||||||
|
|
||||||
|
def setKd(self,D):
|
||||||
|
self.Kd=D
|
||||||
|
|
||||||
|
def getPoint(self):
|
||||||
|
return self.set_point
|
||||||
|
|
||||||
|
def getError(self):
|
||||||
|
return self.error
|
||||||
|
|
||||||
|
def getIntegrator(self):
|
||||||
|
return self.Integrator
|
||||||
|
|
||||||
|
def getDerivator(self):
|
||||||
|
return self.Derivator
|
170
kiln/manager.py
170
kiln/manager.py
|
@ -5,31 +5,45 @@ import thermo
|
||||||
import warnings
|
import warnings
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
import logging
|
||||||
|
import states
|
||||||
|
|
||||||
|
logger = logging.getLogger("kiln.manager")
|
||||||
|
|
||||||
class Manager(threading.Thread):
|
class Manager(threading.Thread):
|
||||||
def __init__(self):
|
def __init__(self, start=states.Idle):
|
||||||
"""
|
"""
|
||||||
Create a Manager instance that manages the electronics for the kiln.
|
Implement a state machine that cycles through States
|
||||||
Fundamentally, this just means that four components need to be connected:
|
|
||||||
|
|
||||||
The thermocouple, the regulator stepper, and PID controller, and the webserver
|
|
||||||
"""
|
"""
|
||||||
|
super(Manager, self).__init__()
|
||||||
self._send = None
|
self._send = None
|
||||||
self.monitor = thermo.Monitor(self._send_state)
|
self.thermocouple = thermo.Monitor(self._send_state)
|
||||||
self.regulator = stepper.Regulator(self._regulator_error)
|
self.regulator = stepper.Regulator()
|
||||||
|
|
||||||
self.profile = None
|
self.profile = None
|
||||||
|
|
||||||
|
self.state = start(self)
|
||||||
|
self.state_change = threading.Event()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.start()
|
||||||
|
|
||||||
def register(self, webapp):
|
def register(self, webapp):
|
||||||
self._send = webapp.send
|
self._send = webapp.send
|
||||||
|
|
||||||
|
def notify(self, data):
|
||||||
|
if self._send is not None:
|
||||||
|
self._send(data)
|
||||||
|
else:
|
||||||
|
logging.warn("No notifier set, ignoring message: %s"%data)
|
||||||
|
|
||||||
def _send_state(self, time, temp):
|
def _send_state(self, time, temp):
|
||||||
profile = None
|
profile = None
|
||||||
if self.profile is not None:
|
if self.profile is not None:
|
||||||
profile.get_state()
|
profile.get_state()
|
||||||
|
|
||||||
state = dict(
|
state = dict(
|
||||||
output=self.regulator.output
|
output=self.regulator.output,
|
||||||
profile=profile,
|
profile=profile,
|
||||||
time=time,
|
time=time,
|
||||||
temp=temp,
|
temp=temp,
|
||||||
|
@ -37,121 +51,37 @@ class Manager(threading.Thread):
|
||||||
if self._send is not None:
|
if self._send is not None:
|
||||||
self._send(state)
|
self._send(state)
|
||||||
|
|
||||||
def _regulator_error(self, msg):
|
def __getattr__(self, name):
|
||||||
if self._send is not None:
|
"""Mutates the manager to return State actions
|
||||||
self._send(dict())
|
If the requested attribute is a function, wrap the function
|
||||||
|
such that returned objects which are States indicate a state change
|
||||||
|
"""
|
||||||
|
attr = getattr(self.state, name)
|
||||||
|
if hasattr(attr, "__call__"):
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
self._change_state(attr(*args, **kwargs))
|
||||||
|
return func
|
||||||
|
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def _change_state(self, output):
|
||||||
|
if isinstance(output, states.State) :
|
||||||
|
self.state = output()
|
||||||
|
self.state_change.set()
|
||||||
|
self.notify(dict(type="change", state=newstate.__name__))
|
||||||
|
elif isinstance(output, tuple) and isinstance(output[0], states.State):
|
||||||
|
newstate, kwargs = output
|
||||||
|
self.state = output(**kwargs)
|
||||||
|
self.notify(dict(type="change", state=newstate.__name__))
|
||||||
|
elif isinstance(output, dict) and "type" in dict:
|
||||||
|
self.notify(output)
|
||||||
|
elif output is not None:
|
||||||
|
logger.warn("Unknown state output: %s"%output)
|
||||||
|
|
||||||
class Profile(object):
|
def run(self):
|
||||||
def __init__(self, schedule, monitor, regulator, interval=5, start_time=None, Kp=.025, Ki=.01, Kd=.005):
|
while running:
|
||||||
self.schedule = schedule
|
self._change_state(self.state.run())
|
||||||
self.monitor = monitor
|
|
||||||
self.interval = interval
|
|
||||||
self.start_time = start_time
|
|
||||||
if start_time is None:
|
|
||||||
self.start_time = time.time()
|
|
||||||
self.regulator = regulator
|
|
||||||
self.pid = PID(Kp, Ki, Kd)
|
|
||||||
self.running = True
|
|
||||||
|
|
||||||
def get_state(self):
|
|
||||||
state = dict(
|
|
||||||
start_time=self.start_time,
|
|
||||||
elapsed=self.elapsed,
|
|
||||||
)
|
|
||||||
return state
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.running = False
|
self.running = False
|
||||||
|
self.state_change.set()
|
||||||
class PID(object):
|
|
||||||
"""
|
|
||||||
Discrete PID control
|
|
||||||
#The recipe gives simple implementation of a Discrete Proportional-Integral-Derivative (PID) controller. PID controller gives output value for error between desired reference input and measurement feedback to minimize error value.
|
|
||||||
#More information: http://en.wikipedia.org/wiki/PID_controller
|
|
||||||
#
|
|
||||||
#cnr437@gmail.com
|
|
||||||
#
|
|
||||||
####### Example #########
|
|
||||||
#
|
|
||||||
#p=PID(3.0,0.4,1.2)
|
|
||||||
#p.setPoint(5.0)
|
|
||||||
#while True:
|
|
||||||
# pid = p.update(measurement_value)
|
|
||||||
#
|
|
||||||
#
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, P=2.0, I=0.0, D=1.0, Derivator=0, Integrator=0, Integrator_max=500, Integrator_min=-500):
|
|
||||||
|
|
||||||
self.Kp=P
|
|
||||||
self.Ki=I
|
|
||||||
self.Kd=D
|
|
||||||
self.Derivator=Derivator
|
|
||||||
self.Integrator=Integrator
|
|
||||||
self.Integrator_max=Integrator_max
|
|
||||||
self.Integrator_min=Integrator_min
|
|
||||||
|
|
||||||
self.set_point=0.0
|
|
||||||
self.error=0.0
|
|
||||||
|
|
||||||
def update(self,current_value):
|
|
||||||
"""
|
|
||||||
Calculate PID output value for given reference input and feedback
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.error = self.set_point - current_value
|
|
||||||
|
|
||||||
self.P_value = self.Kp * self.error
|
|
||||||
self.D_value = self.Kd * ( self.error - self.Derivator)
|
|
||||||
self.Derivator = self.error
|
|
||||||
|
|
||||||
self.Integrator = self.Integrator + self.error
|
|
||||||
|
|
||||||
if self.Integrator > self.Integrator_max:
|
|
||||||
self.Integrator = self.Integrator_max
|
|
||||||
elif self.Integrator < self.Integrator_min:
|
|
||||||
self.Integrator = self.Integrator_min
|
|
||||||
|
|
||||||
self.I_value = self.Integrator * self.Ki
|
|
||||||
PID = self.P_value + self.I_value + self.D_value
|
|
||||||
|
|
||||||
print "PID: %f, %f, %f: %f"%(self.P_value, self.I_value, self.D_value, PID)
|
|
||||||
|
|
||||||
return PID
|
|
||||||
|
|
||||||
def setPoint(self,set_point):
|
|
||||||
"""
|
|
||||||
Initilize the setpoint of PID
|
|
||||||
"""
|
|
||||||
self.set_point = set_point
|
|
||||||
self.Integrator=0
|
|
||||||
self.Derivator=0
|
|
||||||
|
|
||||||
def setIntegrator(self, Integrator):
|
|
||||||
self.Integrator = Integrator
|
|
||||||
|
|
||||||
def setDerivator(self, Derivator):
|
|
||||||
self.Derivator = Derivator
|
|
||||||
|
|
||||||
def setKp(self,P):
|
|
||||||
self.Kp=P
|
|
||||||
|
|
||||||
def setKi(self,I):
|
|
||||||
self.Ki=I
|
|
||||||
|
|
||||||
def setKd(self,D):
|
|
||||||
self.Kd=D
|
|
||||||
|
|
||||||
def getPoint(self):
|
|
||||||
return self.set_point
|
|
||||||
|
|
||||||
def getError(self):
|
|
||||||
return self.error
|
|
||||||
|
|
||||||
def getIntegrator(self):
|
|
||||||
return self.Integrator
|
|
||||||
|
|
||||||
def getDerivator(self):
|
|
||||||
return self.Derivator
|
|
|
@ -18,26 +18,27 @@ class ClientSocket(websocket.WebSocketHandler):
|
||||||
self.parent.sockets.remove(self)
|
self.parent.sockets.remove(self)
|
||||||
|
|
||||||
class DataRequest(tornado.web.RequestHandler):
|
class DataRequest(tornado.web.RequestHandler):
|
||||||
def initialize(self, monitor):
|
def initialize(self, manager):
|
||||||
self.monitor = monitor
|
self.manager = manager
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
data = self.monitor.history
|
data = self.manager.thermocouple.history
|
||||||
output = [dict(time=ts[0], temp=ts[1]) for ts in ]
|
output = [dict(time=ts.time, temp=ts.temp) for ts in data]
|
||||||
self.write(json.dumps(output))
|
self.write(json.dumps(output))
|
||||||
|
|
||||||
class DoAction(tornado.web.RequestHandler):
|
class DoAction(tornado.web.RequestHandler):
|
||||||
def initialize(self, controller):
|
def initialize(self, manager):
|
||||||
self.controller = controller
|
self.manager = manager
|
||||||
|
|
||||||
def get(self, action):
|
def get(self, action):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class WebApp(object):
|
class WebApp(object):
|
||||||
def __init__(self, monitor, port=8888):
|
def __init__(self, manager, port=8888):
|
||||||
self.handlers = [
|
self.handlers = [
|
||||||
(r"/ws/", ClientSocket, dict(parent=self)),
|
(r"/ws/", ClientSocket, dict(parent=self)),
|
||||||
(r"/data.json", DataRequest, dict(monitor=monitor)),
|
(r"/temperature.json", DataRequest, dict(manager=manager)),
|
||||||
|
(r"/do/(.*)", DoAction, dict(manager=manager)),
|
||||||
(r"/(.*)", tornado.web.StaticFileHandler, dict(path=cwd)),
|
(r"/(.*)", tornado.web.StaticFileHandler, dict(path=cwd)),
|
||||||
]
|
]
|
||||||
self.sockets = []
|
self.sockets = []
|
||||||
|
@ -55,14 +56,12 @@ class WebApp(object):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
import thermo
|
import manager
|
||||||
monitor = thermo.Monitor()
|
kiln = manager.Manager()
|
||||||
|
app = WebApp(kiln)
|
||||||
|
kiln.register(app)
|
||||||
|
kiln.start()
|
||||||
|
|
||||||
app = WebApp(monitor)
|
|
||||||
def send_temp(time, temp):
|
|
||||||
app.send(dict(time=time, temp=temp))
|
|
||||||
monitor.callback = send_temp
|
|
||||||
monitor.start()
|
|
||||||
app.run()
|
app.run()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
monitor.stop()
|
kiln.stop()
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
"""Based on the pattern provided here:
|
"""Based on the pattern provided here:
|
||||||
http://python-3-patterns-idioms-test.readthedocs.org/en/latest/StateMachine.html
|
http://python-3-patterns-idioms-test.readthedocs.org/en/latest/StateMachine.html
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import PID
|
||||||
|
|
||||||
class State(object):
|
class State(object):
|
||||||
def __init__(self, machine):
|
def __init__(self, machine):
|
||||||
|
@ -9,16 +11,17 @@ class State(object):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Action that must be continuously run while in this state"""
|
"""Action that must be continuously run while in this state"""
|
||||||
pass
|
self.parent.state_change.clear()
|
||||||
|
self.parent.state_change.wait()
|
||||||
|
|
||||||
class Idle(State):
|
class Idle(State):
|
||||||
def ignite(self):
|
def ignite(self):
|
||||||
_ignite(self.parent.regulator, self.parent.notify)
|
_ignite(self.parent.regulator, self.parent.notify)
|
||||||
return Lit,
|
return Lit
|
||||||
|
|
||||||
def start(self):
|
def start(self, **kwargs):
|
||||||
_ignite(self.parent.regulator, self.parent.notify)
|
_ignite(self.parent.regulator, self.parent.notify)
|
||||||
return Running,
|
return Running, kwargs
|
||||||
|
|
||||||
class Lit(State):
|
class Lit(State):
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
|
@ -28,20 +31,39 @@ class Lit(State):
|
||||||
except:
|
except:
|
||||||
self.parent.notify(dict(type="error", msg=traceback.format_exc()))
|
self.parent.notify(dict(type="error", msg=traceback.format_exc()))
|
||||||
|
|
||||||
def start(self, schedule, interval=5, start_time=None):
|
def start(self, **kwargs):
|
||||||
return Running, dict(schedule=schedule, interval=interval, start_time=start_time)
|
return Running, kwargs
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
_shutoff(self.parent.regulator, self.parent.notify)
|
_shutoff(self.parent.regulator, self.parent.notify)
|
||||||
return Idle,
|
return Idle
|
||||||
|
|
||||||
|
|
||||||
class Running(State):
|
class Running(State):
|
||||||
|
def __init__(self, parent, schedule, interval=5, start_time=None, Kp=.025, Ki=.01, Kd=.005):
|
||||||
|
self.schedule = schedule
|
||||||
|
self.thermocouple = parent.thermocouple
|
||||||
|
self.interval = interval
|
||||||
|
self.start_time = start_time
|
||||||
|
if start_time is None:
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.regulator = parent.regulator
|
||||||
|
self.pid = PID.PID(Kp, Ki, Kd)
|
||||||
|
self.running = True
|
||||||
|
super(Running, self).__init__(parent)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def elapsed(self):
|
def elapsed(self):
|
||||||
''' Returns the elapsed time from start in seconds'''
|
''' Returns the elapsed time from start in seconds'''
|
||||||
return time.time() - self.start_time
|
return time.time() - self.start_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _info(self):
|
||||||
|
return dict(type="profile",
|
||||||
|
output=pid_out,
|
||||||
|
start_time=self.start_time,
|
||||||
|
elapsed=self.elapsed,
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
ts = self.elapsed
|
ts = self.elapsed
|
||||||
|
@ -54,24 +76,26 @@ class Running(State):
|
||||||
setpoint = frac * (temp1 - temp0) + temp0
|
setpoint = frac * (temp1 - temp0) + temp0
|
||||||
self.pid.setPoint(setpoint)
|
self.pid.setPoint(setpoint)
|
||||||
|
|
||||||
temp = self.monitor.temperature
|
temp = self.thermocouple.temperature
|
||||||
pid_out = self.pid.update(temp)
|
pid_out = self.pid.update(temp)
|
||||||
if pid_out < 0: pid_out = 0
|
if pid_out < 0: pid_out = 0
|
||||||
if pid_out > 1: pid_out = 1
|
if pid_out > 1: pid_out = 1
|
||||||
self.regulator.set(pid_out, block=True)
|
self.regulator.set(pid_out, block=True)
|
||||||
|
self.parent.notify(self.info)
|
||||||
|
|
||||||
if ts > self.schedule[-1][0]:
|
if ts > self.schedule[-1][0]:
|
||||||
|
self.parent.notify(dict(type="profile",status="complete"))
|
||||||
_shutoff(self.parent.regulator, self.parent.notify)
|
_shutoff(self.parent.regulator, self.parent.notify)
|
||||||
return Idle,
|
return Idle,
|
||||||
|
|
||||||
time.sleep(self.interval - (time.time()-now))
|
time.sleep(self.interval - (time.time()-now))
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
return Lit,
|
return Lit
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
_shutoff(self.parent.regulator, self.parent.notify)
|
_shutoff(self.parent.regulator, self.parent.notify)
|
||||||
return Idle,
|
return Idle
|
||||||
|
|
||||||
|
|
||||||
def _ignite(regulator, notify):
|
def _ignite(regulator, notify):
|
||||||
|
|
|
@ -5,7 +5,7 @@ import warnings
|
||||||
import Queue
|
import Queue
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.get_logger("Stepper")
|
logger = logging.getLogger("kiln.Stepper")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from RPi import GPIO
|
from RPi import GPIO
|
||||||
|
@ -175,6 +175,9 @@ class Regulator(threading.Thread):
|
||||||
self.ignite_pin = ignite_pin
|
self.ignite_pin = ignite_pin
|
||||||
if ignite_pin is not None:
|
if ignite_pin is not None:
|
||||||
GPIO.setup(ignite_pin, OUT)
|
GPIO.setup(ignite_pin, OUT)
|
||||||
|
self.flame_pin = flame_pin
|
||||||
|
if flame_pin is not None:
|
||||||
|
GPIO.setup(flame_pin, IN)
|
||||||
|
|
||||||
def exit():
|
def exit():
|
||||||
if self.current != 0:
|
if self.current != 0:
|
||||||
|
@ -216,3 +219,8 @@ class Regulator(threading.Thread):
|
||||||
@property
|
@property
|
||||||
def output(self):
|
def output(self):
|
||||||
return (self.current - self.min) / (self.max - self.min)
|
return (self.current - self.min) / (self.max - self.min)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Check the status of the flame sensor"""
|
||||||
|
#since the flame sensor does not yet exist, we'll save this for later
|
||||||
|
pass
|
Ładowanie…
Reference in New Issue