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 stepper
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
|
import thermo
|
||||||
import warnings
|
import warnings
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
|
||||||
class StateMachine(object):
|
class Manager(threading.Thread):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
import stepper
|
"""
|
||||||
import thermo
|
Create a Manager instance that manages the electronics for the kiln.
|
||||||
self.monitor = thermo.Monitor()
|
Fundamentally, this just means that four components need to be connected:
|
||||||
self.regulator = stepper.Regulator()
|
|
||||||
|
|
||||||
class KilnController(object):
|
The thermocouple, the regulator stepper, and PID controller, and the webserver
|
||||||
def __init__(self, schedule, monitor, interval=5, start_time=None, Kp=.025, Ki=.01, Kd=.001, simulate=True):
|
"""
|
||||||
|
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.schedule = schedule
|
||||||
self.monitor = monitor
|
self.monitor = monitor
|
||||||
self.interval = interval
|
self.interval = interval
|
||||||
self.start_time = start_time
|
self.start_time = start_time
|
||||||
if start_time is None:
|
if start_time is None:
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
self.regulator = stepper.Regulator(simulate=simulate)
|
self.regulator = regulator
|
||||||
self.pid = PID(Kp, Ki, Kd)
|
self.pid = PID(Kp, Ki, Kd)
|
||||||
self.simulate = simulate
|
self.running = True
|
||||||
if simulate:
|
|
||||||
self.schedule.insert(0, [0, 15])
|
|
||||||
else:
|
|
||||||
self.schedule.insert(0, [0, 15])
|
|
||||||
|
|
||||||
@property
|
def get_state(self):
|
||||||
def elapsed(self):
|
state = dict(
|
||||||
''' Returns the elapsed time from start in seconds'''
|
start_time=self.start_time,
|
||||||
return time.time() - self.start_time
|
elapsed=self.elapsed,
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
def run(self):
|
def stop(self):
|
||||||
try:
|
self.running = False
|
||||||
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()
|
|
||||||
|
|
||||||
class PID(object):
|
class PID(object):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -22,9 +22,17 @@ class DataRequest(tornado.web.RequestHandler):
|
||||||
self.monitor = monitor
|
self.monitor = monitor
|
||||||
|
|
||||||
def get(self):
|
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))
|
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):
|
class WebApp(object):
|
||||||
def __init__(self, monitor, port=8888):
|
def __init__(self, monitor, port=8888):
|
||||||
self.handlers = [
|
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 threading
|
||||||
import warnings
|
import warnings
|
||||||
import Queue
|
import Queue
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.get_logger("Stepper")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from RPi import GPIO
|
from RPi import GPIO
|
||||||
|
@ -17,6 +20,9 @@ class Stepper(threading.Thread):
|
||||||
[1,0,0,1]]
|
[1,0,0,1]]
|
||||||
|
|
||||||
def __init__(self, pin1=5, pin2=6, pin3=13, pin4=19, timeout=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.queue = Queue.Queue()
|
||||||
self.finished = threading.Event()
|
self.finished = threading.Event()
|
||||||
|
|
||||||
|
@ -29,8 +35,7 @@ class Stepper(threading.Thread):
|
||||||
|
|
||||||
self.phase = 0
|
self.phase = 0
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
super(Stepper, self).__init__()
|
self.start()
|
||||||
self.daemon = True
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.queue.put((None, None, None))
|
self.queue.put((None, None, None))
|
||||||
|
@ -135,12 +140,15 @@ class StepperSim(object):
|
||||||
def stop(self):
|
def stop(self):
|
||||||
print "stopping"
|
print "stopping"
|
||||||
|
|
||||||
|
class Regulator(threading.Thread):
|
||||||
class Regulator(object):
|
def __init__(self, maxsteps=4500, minsteps=2480, speed=150, ignite_pin=None, flame_pin=None, simulate=False):
|
||||||
def __init__(self, maxsteps=4500, minsteps=2480, speed=150, ignite_pin=None, simulate=False):
|
|
||||||
"""Set up a stepper-controlled regulator. Implement some safety measures
|
"""Set up a stepper-controlled regulator. Implement some safety measures
|
||||||
to make sure everything gets shut off at the end
|
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
|
Parameters
|
||||||
----------
|
----------
|
||||||
maxsteps : int
|
maxsteps : int
|
||||||
|
@ -152,11 +160,13 @@ class Regulator(object):
|
||||||
ignite_pin : int or None
|
ignite_pin : int or None
|
||||||
If not None, turn on this pin during the ignite sequence
|
If not None, turn on this pin during the ignite sequence
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if simulate:
|
if simulate:
|
||||||
self.stepper = StepperSim()
|
self.stepper = StepperSim()
|
||||||
else:
|
else:
|
||||||
self.stepper = Stepper()
|
self.stepper = Stepper()
|
||||||
self.stepper.start()
|
self.stepper.start()
|
||||||
|
|
||||||
self.current = 0
|
self.current = 0
|
||||||
self.max = maxsteps
|
self.max = maxsteps
|
||||||
self.min = minsteps
|
self.min = minsteps
|
||||||
|
@ -173,7 +183,10 @@ class Regulator(object):
|
||||||
atexit.register(exit)
|
atexit.register(exit)
|
||||||
|
|
||||||
def ignite(self, start=2800, delay=1):
|
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)
|
self.stepper.step(start, self.speed, block=True)
|
||||||
if self.ignite_pin is not None:
|
if self.ignite_pin is not None:
|
||||||
GPIO.output(self.ignite_pin, True)
|
GPIO.output(self.ignite_pin, True)
|
||||||
|
@ -182,13 +195,12 @@ class Regulator(object):
|
||||||
GPIO.output(self.ignite_pin, False)
|
GPIO.output(self.ignite_pin, False)
|
||||||
self.stepper.step(self.min - start, self.speed, block=True)
|
self.stepper.step(self.min - start, self.speed, block=True)
|
||||||
self.current = self.min
|
self.current = self.min
|
||||||
print "Done!"
|
logger.log("Ignition complete")
|
||||||
|
|
||||||
def off(self, block=True):
|
def off(self, block=True):
|
||||||
print "Turning off..."
|
logger.log("Shutting off gas")
|
||||||
self.stepper.step(-self.current, self.speed, block=block)
|
self.stepper.step(-self.current, self.speed, block=block)
|
||||||
self.current = 0
|
self.current = 0
|
||||||
print "Done!"
|
|
||||||
|
|
||||||
def set(self, value, block=False):
|
def set(self, value, block=False):
|
||||||
if self.current == 0:
|
if self.current == 0:
|
||||||
|
@ -200,3 +212,7 @@ class Regulator(object):
|
||||||
print "Currently at %d, target %d, stepping %d"%(self.current, target, nsteps)
|
print "Currently at %d, target %d, stepping %d"%(self.current, target, nsteps)
|
||||||
self.current = target
|
self.current = target
|
||||||
self.stepper.step(nsteps, self.speed, block=block)
|
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() {
|
module.Monitor.prototype.last = function() {
|
||||||
return this.temperature[this.temperature.length-1];
|
return this.temperature[this.temperature.length-1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.Monitor.prototype.setScale = function(scale) {
|
module.Monitor.prototype.setScale = function(scale) {
|
||||||
$("a#temp_scale_C").parent().removeClass("active");
|
$("a#temp_scale_C").parent().removeClass("active");
|
||||||
$("a#temp_scale_F").parent().removeClass("active");
|
$("a#temp_scale_F").parent().removeClass("active");
|
||||||
|
@ -121,7 +120,7 @@ var tempgraph = (function(module) {
|
||||||
return module;
|
return module;
|
||||||
}(tempgraph || {}));
|
}(tempgraph || {}));
|
||||||
|
|
||||||
d3.json("data.json", function(error, data) {
|
d3.json("data2.json", function(error, data) {
|
||||||
monitor = new tempgraph.Monitor(data);
|
monitor = new tempgraph.Monitor(data);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import time
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque, namedtuple
|
||||||
|
|
||||||
logger = logging.getLogger("thermo")
|
logger = logging.getLogger("thermo")
|
||||||
|
|
||||||
|
@ -18,8 +18,13 @@ def temp_to_cone(temp):
|
||||||
return names[i]+'.%d'%int(frac*10)
|
return names[i]+'.%d'%int(frac*10)
|
||||||
return "13+"
|
return "13+"
|
||||||
|
|
||||||
|
tempsample = namedtuple("tempsample", ['time', 'temp'])
|
||||||
|
|
||||||
class Monitor(threading.Thread):
|
class Monitor(threading.Thread):
|
||||||
def __init__(self, name="3b-000000182b57", callback=None):
|
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.device = "/sys/bus/w1/devices/%s/w1_slave"%name
|
||||||
self.history = deque(maxlen=1048576)
|
self.history = deque(maxlen=1048576)
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
@ -33,8 +38,8 @@ class Monitor(threading.Thread):
|
||||||
logger.info("Could not start AlphaScroller")
|
logger.info("Could not start AlphaScroller")
|
||||||
self.display = None
|
self.display = None
|
||||||
|
|
||||||
super(Monitor, self).__init__()
|
|
||||||
self.running = True
|
self.running = True
|
||||||
|
self.start()
|
||||||
|
|
||||||
def _read_temp(self):
|
def _read_temp(self):
|
||||||
with open(self.device, 'r') as f:
|
with open(self.device, 'r') as f:
|
||||||
|
@ -59,16 +64,12 @@ class Monitor(threading.Thread):
|
||||||
return self.history[-1][1]
|
return self.history[-1][1]
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# with open("/home/pi/data.txt", "w") as f:
|
|
||||||
# f.write("time\ttemp\n")
|
|
||||||
while self.running:
|
while self.running:
|
||||||
temp = self._read_temp()
|
temp = self._read_temp()
|
||||||
now = time.time()
|
now = time.time()
|
||||||
self.history.append((now, temp))
|
self.history.append(tempsample(now, temp))
|
||||||
if self.callback is not None:
|
if self.callback is not None:
|
||||||
self.callback(now, temp)
|
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 self.display is not None:
|
||||||
if temp > 50:
|
if temp > 50:
|
||||||
|
@ -84,4 +85,4 @@ class Monitor(threading.Thread):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mon = Monitor()
|
mon = Monitor()
|
||||||
mon.start()
|
mon.join()
|
||||||
|
|
Ładowanie…
Reference in New Issue