kopia lustrzana https://github.com/jamesgao/kiln_controller
starting to work on state machine for backend
rodzic
96767913d1
commit
a1b47bd1b6
101
kiln/manager.py
101
kiln/manager.py
|
@ -1,70 +1,69 @@
|
|||
import stepper
|
||||
import time
|
||||
import random
|
||||
import thermo
|
||||
import warnings
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
class StateMachine(object):
|
||||
class Manager(threading.Thread):
|
||||
def __init__(self):
|
||||
import stepper
|
||||
import thermo
|
||||
self.monitor = thermo.Monitor()
|
||||
self.regulator = stepper.Regulator()
|
||||
"""
|
||||
Create a Manager instance that manages the electronics for the kiln.
|
||||
Fundamentally, this just means that four components need to be connected:
|
||||
|
||||
class KilnController(object):
|
||||
def __init__(self, schedule, monitor, interval=5, start_time=None, Kp=.025, Ki=.01, Kd=.001, simulate=True):
|
||||
The thermocouple, the regulator stepper, and PID controller, and the webserver
|
||||
"""
|
||||
self._send = None
|
||||
self.monitor = thermo.Monitor(self._send_state)
|
||||
self.regulator = stepper.Regulator(self._regulator_error)
|
||||
|
||||
self.profile = None
|
||||
|
||||
def register(self, webapp):
|
||||
self._send = webapp.send
|
||||
|
||||
def _send_state(self, time, temp):
|
||||
profile = None
|
||||
if self.profile is not None:
|
||||
profile.get_state()
|
||||
|
||||
state = dict(
|
||||
output=self.regulator.output
|
||||
profile=profile,
|
||||
time=time,
|
||||
temp=temp,
|
||||
)
|
||||
if self._send is not None:
|
||||
self._send(state)
|
||||
|
||||
def _regulator_error(self, msg):
|
||||
if self._send is not None:
|
||||
self._send(dict())
|
||||
|
||||
|
||||
|
||||
class Profile(object):
|
||||
def __init__(self, schedule, monitor, regulator, interval=5, start_time=None, Kp=.025, Ki=.01, Kd=.005):
|
||||
self.schedule = schedule
|
||||
self.monitor = monitor
|
||||
self.interval = interval
|
||||
self.start_time = start_time
|
||||
if start_time is None:
|
||||
self.start_time = time.time()
|
||||
self.regulator = stepper.Regulator(simulate=simulate)
|
||||
self.regulator = regulator
|
||||
self.pid = PID(Kp, Ki, Kd)
|
||||
self.simulate = simulate
|
||||
if simulate:
|
||||
self.schedule.insert(0, [0, 15])
|
||||
else:
|
||||
self.schedule.insert(0, [0, 15])
|
||||
self.running = True
|
||||
|
||||
@property
|
||||
def elapsed(self):
|
||||
''' Returns the elapsed time from start in seconds'''
|
||||
return time.time() - self.start_time
|
||||
def get_state(self):
|
||||
state = dict(
|
||||
start_time=self.start_time,
|
||||
elapsed=self.elapsed,
|
||||
)
|
||||
return state
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.regulator.ignite()
|
||||
print self.elapsed, self.schedule[-1][0]
|
||||
while self.elapsed < self.schedule[-1][0]:
|
||||
now = time.time()
|
||||
ts = self.elapsed
|
||||
#find epoch
|
||||
for i in range(len(self.schedule)-1):
|
||||
if self.schedule[i][0] < ts < self.schedule[i+1][0]:
|
||||
time0, temp0 = self.schedule[i]
|
||||
time1, temp1 = self.schedule[i+1]
|
||||
frac = (ts - time0) / (time1 - time0)
|
||||
setpoint = frac * (temp1 - temp0) + temp0
|
||||
self.pid.setPoint(setpoint)
|
||||
print "In epoch %d, elapsed time %f, temperature %f"%(i, ts, setpoint)
|
||||
if self.simulate:
|
||||
temp = setpoint + random.gauss(-20, 15)
|
||||
else:
|
||||
temp = self.monitor.temperature
|
||||
|
||||
pid_out = self.pid.update(temp)
|
||||
if pid_out < 0: pid_out = 0
|
||||
if pid_out > 1: pid_out = 1
|
||||
self.regulator.set(pid_out, block=True)
|
||||
|
||||
time.sleep(self.interval - (time.time()-now))
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print "Started at %f"%self.start_time
|
||||
|
||||
self.regulator.off()
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
class PID(object):
|
||||
"""
|
||||
|
|
|
@ -22,9 +22,17 @@ class DataRequest(tornado.web.RequestHandler):
|
|||
self.monitor = monitor
|
||||
|
||||
def get(self):
|
||||
output = [dict(time=ts[0], temp=ts[1]) for ts in self.monitor.history]
|
||||
data = self.monitor.history
|
||||
output = [dict(time=ts[0], temp=ts[1]) for ts in ]
|
||||
self.write(json.dumps(output))
|
||||
|
||||
class DoAction(tornado.web.RequestHandler):
|
||||
def initialize(self, controller):
|
||||
self.controller = controller
|
||||
|
||||
def get(self, action):
|
||||
pass
|
||||
|
||||
class WebApp(object):
|
||||
def __init__(self, monitor, port=8888):
|
||||
self.handlers = [
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
"""Based on the pattern provided here:
|
||||
http://python-3-patterns-idioms-test.readthedocs.org/en/latest/StateMachine.html
|
||||
"""
|
||||
import traceback
|
||||
|
||||
class State(object):
|
||||
def __init__(self, machine):
|
||||
self.parent = machine
|
||||
|
||||
def run(self):
|
||||
"""Action that must be continuously run while in this state"""
|
||||
pass
|
||||
|
||||
class Idle(State):
|
||||
def ignite(self):
|
||||
_ignite(self.parent.regulator, self.parent.notify)
|
||||
return Lit,
|
||||
|
||||
def start(self):
|
||||
_ignite(self.parent.regulator, self.parent.notify)
|
||||
return Running,
|
||||
|
||||
class Lit(State):
|
||||
def set(self, value):
|
||||
try:
|
||||
self.parent.regulator.set(value)
|
||||
self.parent.notify(dict(type="success"))
|
||||
except:
|
||||
self.parent.notify(dict(type="error", msg=traceback.format_exc()))
|
||||
|
||||
def start(self, schedule, interval=5, start_time=None):
|
||||
return Running, dict(schedule=schedule, interval=interval, start_time=start_time)
|
||||
|
||||
def stop(self):
|
||||
_shutoff(self.parent.regulator, self.parent.notify)
|
||||
return Idle,
|
||||
|
||||
|
||||
class Running(State):
|
||||
@property
|
||||
def elapsed(self):
|
||||
''' Returns the elapsed time from start in seconds'''
|
||||
return time.time() - self.start_time
|
||||
|
||||
def run(self):
|
||||
now = time.time()
|
||||
ts = self.elapsed
|
||||
#find epoch
|
||||
for i in range(len(self.schedule)-1):
|
||||
if self.schedule[i][0] < ts < self.schedule[i+1][0]:
|
||||
time0, temp0 = self.schedule[i]
|
||||
time1, temp1 = self.schedule[i+1]
|
||||
frac = (ts - time0) / (time1 - time0)
|
||||
setpoint = frac * (temp1 - temp0) + temp0
|
||||
self.pid.setPoint(setpoint)
|
||||
|
||||
temp = self.monitor.temperature
|
||||
pid_out = self.pid.update(temp)
|
||||
if pid_out < 0: pid_out = 0
|
||||
if pid_out > 1: pid_out = 1
|
||||
self.regulator.set(pid_out, block=True)
|
||||
|
||||
if ts > self.schedule[-1][0]:
|
||||
_shutoff(self.parent.regulator, self.parent.notify)
|
||||
return Idle,
|
||||
|
||||
time.sleep(self.interval - (time.time()-now))
|
||||
|
||||
def pause(self):
|
||||
return Lit,
|
||||
|
||||
def stop(self):
|
||||
_shutoff(self.parent.regulator, self.parent.notify)
|
||||
return Idle,
|
||||
|
||||
|
||||
def _ignite(regulator, notify):
|
||||
try:
|
||||
regulator.ignite()
|
||||
msg = dict(type="success")
|
||||
except ValueError:
|
||||
msg = dict(type="error", msg="Cannot ignite: regulator not off")
|
||||
except Exception as e:
|
||||
msg = dict(type="error", msg=traceback.format_exc())
|
||||
notify(msg)
|
||||
|
||||
def _shutoff(regulator, notify):
|
||||
try:
|
||||
regulator.off()
|
||||
msg = dict(type="success")
|
||||
except ValueError:
|
||||
msg = dict(type="error", msg="Cannot shutoff: regulator not lit")
|
||||
except Exception as e:
|
||||
msg = dict(type="error", msg=traceback.format_exc())
|
||||
notify(msg)
|
|
@ -3,6 +3,9 @@ import atexit
|
|||
import threading
|
||||
import warnings
|
||||
import Queue
|
||||
import logging
|
||||
|
||||
logger = logging.get_logger("Stepper")
|
||||
|
||||
try:
|
||||
from RPi import GPIO
|
||||
|
@ -17,6 +20,9 @@ class Stepper(threading.Thread):
|
|||
[1,0,0,1]]
|
||||
|
||||
def __init__(self, pin1=5, pin2=6, pin3=13, pin4=19, timeout=1):
|
||||
super(Stepper, self).__init__()
|
||||
self.daemon = True
|
||||
|
||||
self.queue = Queue.Queue()
|
||||
self.finished = threading.Event()
|
||||
|
||||
|
@ -29,8 +35,7 @@ class Stepper(threading.Thread):
|
|||
|
||||
self.phase = 0
|
||||
self.timeout = timeout
|
||||
super(Stepper, self).__init__()
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
self.queue.put((None, None, None))
|
||||
|
@ -135,12 +140,15 @@ class StepperSim(object):
|
|||
def stop(self):
|
||||
print "stopping"
|
||||
|
||||
|
||||
class Regulator(object):
|
||||
def __init__(self, maxsteps=4500, minsteps=2480, speed=150, ignite_pin=None, simulate=False):
|
||||
class Regulator(threading.Thread):
|
||||
def __init__(self, maxsteps=4500, minsteps=2480, speed=150, ignite_pin=None, flame_pin=None, simulate=False):
|
||||
"""Set up a stepper-controlled regulator. Implement some safety measures
|
||||
to make sure everything gets shut off at the end
|
||||
|
||||
TODO: integrate flame sensor by converting this into a thread, and checking
|
||||
flame state regularly. If flame sensor off, immediately increase gas and attempt
|
||||
reignition, or shut off after 5 seconds of failure.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
maxsteps : int
|
||||
|
@ -152,11 +160,13 @@ class Regulator(object):
|
|||
ignite_pin : int or None
|
||||
If not None, turn on this pin during the ignite sequence
|
||||
"""
|
||||
|
||||
if simulate:
|
||||
self.stepper = StepperSim()
|
||||
else:
|
||||
self.stepper = Stepper()
|
||||
self.stepper.start()
|
||||
|
||||
self.current = 0
|
||||
self.max = maxsteps
|
||||
self.min = minsteps
|
||||
|
@ -173,7 +183,10 @@ class Regulator(object):
|
|||
atexit.register(exit)
|
||||
|
||||
def ignite(self, start=2800, delay=1):
|
||||
print "Ignition..."
|
||||
if self.current != 0:
|
||||
raise ValueError("Must be off to ignite")
|
||||
|
||||
logger.log("Ignition start")
|
||||
self.stepper.step(start, self.speed, block=True)
|
||||
if self.ignite_pin is not None:
|
||||
GPIO.output(self.ignite_pin, True)
|
||||
|
@ -182,13 +195,12 @@ class Regulator(object):
|
|||
GPIO.output(self.ignite_pin, False)
|
||||
self.stepper.step(self.min - start, self.speed, block=True)
|
||||
self.current = self.min
|
||||
print "Done!"
|
||||
logger.log("Ignition complete")
|
||||
|
||||
def off(self, block=True):
|
||||
print "Turning off..."
|
||||
logger.log("Shutting off gas")
|
||||
self.stepper.step(-self.current, self.speed, block=block)
|
||||
self.current = 0
|
||||
print "Done!"
|
||||
|
||||
def set(self, value, block=False):
|
||||
if self.current == 0:
|
||||
|
@ -200,3 +212,7 @@ class Regulator(object):
|
|||
print "Currently at %d, target %d, stepping %d"%(self.current, target, nsteps)
|
||||
self.current = target
|
||||
self.stepper.step(nsteps, self.speed, block=block)
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
return (self.current - self.min) / (self.max - self.min)
|
|
@ -59,8 +59,7 @@ var tempgraph = (function(module) {
|
|||
module.Monitor.prototype.last = function() {
|
||||
return this.temperature[this.temperature.length-1];
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.Monitor.prototype.setScale = function(scale) {
|
||||
$("a#temp_scale_C").parent().removeClass("active");
|
||||
$("a#temp_scale_F").parent().removeClass("active");
|
||||
|
@ -121,7 +120,7 @@ var tempgraph = (function(module) {
|
|||
return module;
|
||||
}(tempgraph || {}));
|
||||
|
||||
d3.json("data.json", function(error, data) {
|
||||
d3.json("data2.json", function(error, data) {
|
||||
monitor = new tempgraph.Monitor(data);
|
||||
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import time
|
|||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
from collections import deque
|
||||
from collections import deque, namedtuple
|
||||
|
||||
logger = logging.getLogger("thermo")
|
||||
|
||||
|
@ -18,8 +18,13 @@ def temp_to_cone(temp):
|
|||
return names[i]+'.%d'%int(frac*10)
|
||||
return "13+"
|
||||
|
||||
tempsample = namedtuple("tempsample", ['time', 'temp'])
|
||||
|
||||
class Monitor(threading.Thread):
|
||||
def __init__(self, name="3b-000000182b57", callback=None):
|
||||
super(Monitor, self).__init__()
|
||||
self.daemon = True
|
||||
|
||||
self.device = "/sys/bus/w1/devices/%s/w1_slave"%name
|
||||
self.history = deque(maxlen=1048576)
|
||||
self.callback = callback
|
||||
|
@ -33,8 +38,8 @@ class Monitor(threading.Thread):
|
|||
logger.info("Could not start AlphaScroller")
|
||||
self.display = None
|
||||
|
||||
super(Monitor, self).__init__()
|
||||
self.running = True
|
||||
self.start()
|
||||
|
||||
def _read_temp(self):
|
||||
with open(self.device, 'r') as f:
|
||||
|
@ -59,16 +64,12 @@ class Monitor(threading.Thread):
|
|||
return self.history[-1][1]
|
||||
|
||||
def run(self):
|
||||
# with open("/home/pi/data.txt", "w") as f:
|
||||
# f.write("time\ttemp\n")
|
||||
while self.running:
|
||||
temp = self._read_temp()
|
||||
now = time.time()
|
||||
self.history.append((now, temp))
|
||||
self.history.append(tempsample(now, temp))
|
||||
if self.callback is not None:
|
||||
self.callback(now, temp)
|
||||
with open("/home/pi/data.txt", "a") as f:
|
||||
f.write("%f\t%f\n"%(now, temp))
|
||||
|
||||
if self.display is not None:
|
||||
if temp > 50:
|
||||
|
@ -84,4 +85,4 @@ class Monitor(threading.Thread):
|
|||
|
||||
if __name__ == "__main__":
|
||||
mon = Monitor()
|
||||
mon.start()
|
||||
mon.join()
|
||||
|
|
Ładowanie…
Reference in New Issue