Backend state machine likely finished, need to test

master
James Gao 2014-10-21 18:08:10 -07:00
rodzic a1b47bd1b6
commit 59e60eab57
5 zmienionych plików z 206 dodań i 151 usunięć

94
kiln/PID.py 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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()

Wyświetl plik

@ -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):

Wyświetl plik

@ -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