Add screen with simple menu for offline use, plus some hacks

pull/244/head
Daniel Hultgren 2025-08-30 16:33:39 +02:00
rodzic c39c235c4f
commit ab5b8ab514
10 zmienionych plików z 672 dodań i 148 usunięć

Wyświetl plik

@ -41,8 +41,8 @@ gpio_heat = 23 # Switches zero-cross solid-state-relay
### Thermocouple Adapter selection:
# max31855 - bitbang SPI interface
# max31856 - bitbang SPI interface. must specify thermocouple_type.
max31855 = 1
max31856 = 0
max31855 = 0
max31856 = 1
# see lib/max31856.py for other thermocouple_type, only applies to max31856
# uncomment this if using MAX-31856
thermocouple_type = MAX31856.MAX31856_S_TYPE
@ -60,7 +60,7 @@ gpio_sensor_di = 10 # only used with max31856
# Every N seconds a decision is made about switching the relay[s]
# on & off and for how long. The thermocouple is read
# temperature_average_samples times during and the average value is used.
sensor_time_wait = 3
sensor_time_wait = 5
########################################################################
@ -71,9 +71,9 @@ sensor_time_wait = 3
# well with the simulated oven. You must tune them to work well with
# your specific kiln. Note that the integral pid_ki is
# inverted so that a smaller number means more integral action.
pid_kp = 25 # Proportional 25,200,200
pid_ki = 10 # Integral
pid_kd = 200 # Derivative
pid_kp = 20 # Proportional 25,200,200
pid_ki = 100 # Integral
pid_kd = 1000 # Derivative
########################################################################
@ -87,7 +87,7 @@ stop_integral_windup = True
########################################################################
#
# Simulation parameters
simulate = True
simulate = False
sim_t_env = 20.0 # deg C
sim_c_heat = 500.0 # J/K heat capacity of heat element
sim_c_oven = 500.0 # J/K heat capacity of oven
@ -128,7 +128,7 @@ kiln_must_catch_up = True
# or 100% off because the kiln is too hot. No integral builds up
# outside the window. The bigger you make the window, the more
# integral you will accumulate. This should be a positive integer.
pid_control_window = 10 #degrees
pid_control_window = 15 #degrees
# thermocouple offset
# If you put your thermocouple in ice water and it reads 36F, you can
@ -143,7 +143,7 @@ thermocouple_offset=0
temperature_average_samples = 40
# Thermocouple AC frequency filtering - set to True if in a 50Hz locale, else leave at False for 60Hz locale
ac_freq_50hz = False
ac_freq_50hz = True
########################################################################
# Emergencies - or maybe not

54
lib/display.py 100644
Wyświetl plik

@ -0,0 +1,54 @@
import logging
import threading
from RPLCD.i2c import CharLCD
log = logging.getLogger(__name__)
lock = threading.Lock()
class Display(object):
def __init__(self):
self.width = 16
self.height = 2
self.lcd = CharLCD(i2c_expander='PCF8574', address=0x27, port=1,
cols=self.width, rows=self.height,
auto_linebreaks=False,
backlight_enabled=True)
self.lcd.cursor_mode = 'hide'
self.clear()
self.last_text = [''.ljust(self.width, ' ')] * self.height
def set_text(self, lines, force_redraw=True):
lines = list(map(lambda l: l.ljust(self.width, ' '), lines))
while len(lines) < self.height:
lines.append(''.ljust(self.width, ' '))
diffs = []
for j in range(0, len(lines)):
if (len(lines[j]) is not len(self.last_text[j])):
log.info('########### ' + lines[j] + ' - ' + self.last_text[j])
for i in range(0, len(lines[j])):
if lines[j][i] != self.last_text[j][i]:
diffs.append((j, i))
if len(diffs) > 0:
log.info('setting text to ' + str(lines))
with lock:
if len(diffs) < self.width*self.height/4 and not force_redraw:
log.info('updating only diffs')
for diff in diffs:
c = str(lines[diff[0]][diff[1]])
#log.info('updating pos ' + str(diff) + ' to "' + c + '"')
self.lcd.cursor_pos = diff
self.lcd.write_string(c)
self.lcd.cursor_pos = (0, 0)
else:
log.info('updating full display')
#self.clear()
self.lcd.cursor_pos = (0, 0)
for line in lines:
self.lcd.write_string(line)
self.lcd.crlf()
self.lcd.cursor_pos = (0, 0)
log.info('updating last_text from\n' + str(self.last_text) + ' to\n' + str(lines))
self.last_text = lines
def clear(self):
self.lcd.clear()

234
lib/menu.py 100644
Wyświetl plik

@ -0,0 +1,234 @@
import threading,logging,time,json
import os
import config
from datetime import datetime, timedelta
from lib.display import Display
from lib.rotaryinput import RotaryInput
log = logging.getLogger(__name__)
def get_profiles():
try:
profile_files = os.listdir(config.kiln_profiles_directory)
except:
profile_files = []
profiles = []
for filename in profile_files:
with open(os.path.join(config.kiln_profiles_directory, filename), 'r') as f:
profiles.append(json.load(f))
return profiles
class LCD():
def __init__(self):
self.clear()
def write_string(self, string):
s = self.text[self.cursor[0]]
self.text[self.cursor[0]] = s[:self.cursor[1]] + string + s[self.cursor[1]+len(string):]
def clear(self):
self.text = [' ' * 16, ' ' * 16]
self.cursor = (0, 0)
def cursor_pos(self, pos):
self.cursor = pos
def show(self):
#os.system('clear')
print('-'*18)
for t in self.text:
print('|'+t+'|')
print('-'*18)
#lcd = LCD()
lcd = Display()
class MenuNode():
def __init__(self, text=None, type='menu', children=[], action=None, profile=None):
self.parent = None
self.type = type
self.text = text
self.action = action
self.children = children
self.profile = profile
for child in children:
child.set_parent(self)
self.reset()
def add_child(self, child):
if self.children == None:
self.children = []
child.set_parent(self)
self.children.append(child)
def set_parent(self, parent):
self.parent = parent
def update_index(self, direction):
self.index = max(0, min(self.index+direction, len(self.children)-1))
def display_as_child(self, selected):
return ('>' if selected else ' ') + self.text.ljust(14, ' ')[:14:] + ('>' if self.type == 'menu' else ' ')
def display_confirm_options(self):
no = ('>' if self.index == 0 else ' ') + self.children[0].text
yes = ('>' if self.index == 1 else ' ') + self.children[1].text
return no.ljust(16-len(yes), ' ') + yes
def reset(self):
self.index = 0
class Menu():
def __init__(self, profiles, oven):
self.build_menu(profiles)
self.node = None
self.oven = oven
#self.node = self.off.children[1].children[1]
#self.node.index = 1
def build_menu(self, profiles):
self.off = MenuNode(type='menu', children=[
MenuNode(text='Back', action=self.go_up),
MenuNode(text='Profiles', children=self.build_profiles_list(profiles))
])
self.running = MenuNode(type='menu', children=[
MenuNode(text='Back', action=self.go_up),
MenuNode(text='Abort profile', children=[self.create_confirm_node(self.abort_profile)])
])
def build_profiles_list(self, profiles):
nodes = [MenuNode(text='Back', action=self.go_up)]
for profile in profiles:
node = MenuNode(text=profile['name'], profile=profile, children=[
MenuNode(text='Back', action=self.go_up),
MenuNode(text='Start', children=[self.create_confirm_node(self.start_profile, profile)])
])
nodes.append(node)
return nodes
def create_confirm_node(self, action, profile=None):
return MenuNode(text=' Are you sure?', type='confirm', profile=profile, children=[
MenuNode(text='No', action=self.go_up),
MenuNode(text='Yes', action=action)
])
def click(self):
if self.node is None:
# select menu based on oven state
self.node = self.off if self.oven.profile is None else self.running
return
child = self.node.children[self.node.index]
if child.action != None:
child.action()
elif len(child.children) == 0:
return
else:
self.node = child
while len(self.node.children) == 1:
self.node = self.node.children[0]
def next(self):
if self.node is not None:
self.node.update_index(1)
def previous(self):
if self.node is not None:
self.node.update_index(-1)
def go_up(self):
self.node = self.node.parent
while self.node is not None and len(self.node.children) == 1:
self.node = self.node.parent
def start_profile(self):
self.oven.run_json_profile(json.dumps(self.node.profile))
#self.oven.profile = self.node.profile
self.node.reset()
self.node = None
def abort_profile(self):
#self.oven.profile = None
self.oven.abort_run()
self.node.reset()
self.node = None
fake_profiles = [{'profile': 'Bisque'}, {'profile': 'Glaze'}, {'profile': 'GlazeHigh'}, {'profile': 'Earthen'}]
real_profiles = get_profiles()
class OvenDisplay(threading.Thread):
def __init__(self,oven):
log.info('Starting oven display')
threading.Thread.__init__(self)
self.sleep_time = 1
#self.daemon = True
self.oven = oven
log.info(real_profiles)
self.menu = Menu(real_profiles, oven)
self.input = RotaryInput()
self.input.on_next(self.next)
self.input.on_previous(self.previous)
self.input.on_click(self.click)
self.start()
def next(self):
self.menu.next()
self.update_display()
def previous(self):
self.menu.previous()
self.update_display()
def click(self):
self.menu.click()
self.update_display()
def run(self):
while True:
self.update_display()
#inp = input()
#if inp == 'a':
# self.menu.previous()
#if inp == 'd':
# self.menu.next()
#if inp == 'w':
# self.menu.click()
time.sleep(self.sleep_time)
def update_display(self):
node = self.menu.node
if node is None:
self.show_status()
elif node.type == 'menu':
first_index = max(0, min(node.index, len(node.children)-2))
new_text = [node.children[first_index].display_as_child(node.index == first_index)]
if first_index+1 < len(node.children):
new_text.append(node.children[first_index+1].display_as_child(node.index == first_index+1))
lcd.set_text(new_text)
elif node.type == 'confirm':
lcd.set_text([node.text, node.display_confirm_options()])
def show_status(self):
new_text = [(str(round(self.oven.board.temp_sensor.temperature + config.thermocouple_offset)) +
#('/' + str(round(self.oven.target)) if self.oven.target > 0 else '')).ljust(12, ' ') +
#(str(self.oven.load) + '%' if self.oven.load > 0 else ' OFF') +
('/' + str(round(self.oven.target)) if self.oven.target > 0 else '')).ljust(12, ' ') +
(' H' if (self.oven.heat > 0 and self.oven.profile is not None) else ' OFF')]
if self.oven.profile is None:
new_text.append('Kiln on standby')
else:
time_left = timedelta(seconds=self.oven.totaltime - self.oven.runtime)
new_text.append(self.oven.profile.name.ljust(10, ' ')[:10:] + ' ' + (datetime.now() + time_left).strftime("%H:%M"))
lcd.set_text(new_text, True)
class Oven():
def __init__(self):
self.profile = None
self.temp = 1030
self.target_temp = 1035
self.load = 75
self.heating = True

73
lib/menu_test.py 100644
Wyświetl plik

@ -0,0 +1,73 @@
import pytest
from unittest.mock import Mock
from lib.menu import Menu
fake_profiles = [{'profile': 'Bisque'}, {'profile': 'Glaze'}, {'profile': 'GlazeHigh'}, {'profile': 'Earthen'}]
def setup(profile=None):
oven = Mock()
oven.profile = profile
return Menu(fake_profiles, oven)
def test_init():
m = setup()
assert m.node == None
def test_click_none_oven_off():
m = setup()
m.click()
assert m.node == m.off
def test_click_none_oven_running():
m = setup(True)
m.click()
assert m.node == m.running
def test_click_menu_back():
m = setup()
m.click()
m.click()
assert m.node == None
def test_previous_next():
m = setup()
m.click()
m.next()
assert m.node.index == 1
m.previous()
m.click()
assert m.node == None
def test_start_program():
m = setup()
m.click() #menu
m.next()
m.click() #profiles
m.next()
m.click() #Bisque
profile = m.node.profile
m.next()
m.click() #Start
assert "Are you sure" in m.node.text
m.click() #No
m.click() #Start
m.next() #Yes
m.click()
assert m.node == None #Menu reset
assert m.oven.profile == profile #Profile started
def test_abort_program():
m = setup(fake_profiles[0])
m.click() #Menu
m.click() #Back
assert m.node == None
assert m.oven.profile == fake_profiles[0]
m.click() #Menu
m.next()
m.click() #Abort
assert "Are you sure" in m.node.text
m.next() #Yes
m.click()
assert m.node == None #Menu reset
assert m.oven.profile == None #Profile aborted

Wyświetl plik

@ -7,6 +7,8 @@ import json
import config
import os
from lib.menu import OvenDisplay
log = logging.getLogger(__name__)
class DupFilter(object):
@ -49,11 +51,23 @@ class Output(object):
def heat(self,sleepfor):
self.GPIO.output(config.gpio_heat, self.GPIO.HIGH)
log.info('Starting heat')
time.sleep(sleepfor)
def cool(self,sleepfor):
'''no active cooling, so sleep'''
self.GPIO.output(config.gpio_heat, self.GPIO.LOW)
log.info('Ending heat')
time.sleep(sleepfor)
class OutputSimulated(object):
def __init__(self):
self.active = True
def heat(self,sleepfor):
time.sleep(sleepfor)
def cool(self,sleepfor):
time.sleep(sleepfor)
# FIX - Board class needs to be completely removed
@ -199,9 +213,11 @@ class Oven(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
self.temperature = 0
self.time_step = config.sensor_time_wait
self.board = Board()
self.output = Output()
self.reset()
self.display = OvenDisplay(self)
def reset(self):
self.cost = 0
@ -213,10 +229,11 @@ class Oven(threading.Thread):
self.target = 0
self.heat = 0
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
self.on_first = True
self.switch_count = 0
self.load = 0
def run_profile(self, profile, startat=0):
log.info(profile)
self.reset()
if self.board.temp_sensor.noConnection:
@ -240,6 +257,9 @@ class Oven(threading.Thread):
self.state = "RUNNING"
log.info("Running schedule %s starting at %d minutes" % (profile.name,startat))
log.info("Starting")
def run_json_profile(self, profile_json):
self.run_profile(Profile(profile_json))
def abort_run(self):
self.reset()
@ -309,12 +329,12 @@ class Oven(threading.Thread):
def get_state(self):
temp = 0
try:
temp = self.board.temp_sensor.temperature + config.thermocouple_offset
except AttributeError as error:
#try:
temp = self.board.temp_sensor.temperature + config.thermocouple_offset
#except AttributeError as error:
# this happens at start-up with a simulated oven
temp = 0
pass
# temp = 0
# pass
state = {
'cost': self.cost,
@ -327,7 +347,8 @@ class Oven(threading.Thread):
'kwh_rate': config.kwh_rate,
'currency_type': config.currency_type,
'profile': self.profile.name if self.profile else None,
'pidstats': self.pid.pidstats,
'switch_count': self.switch_count,
'pidstats': self.pid.pidstats
}
return state
@ -388,6 +409,41 @@ class Oven(threading.Thread):
log.info("ovenwatcher set in oven class")
self.ovenwatcher = watcher
def set_heat(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature +
config.thermocouple_offset)
if pid > 0:
self.output.heat(self.time_step)
if (self.heat != self.time_step):
self.switch_count += 1
self.heat = self.time_step
else:
self.output.cool(self.time_step)
if (self.heat != 0):
self.switch_count += 1
self.heat = 0
time_left = self.totaltime - self.runtime
try:
log.info("temp=%.2f, target=%.2f, error=%.2f, pid=%.2f, p=%.2f, i=%.2f, d=%.2f, heat=%d, run_time=%d, total_time=%d, time_left=%d, switch_count=%d" %
(self.pid.pidstats['ispoint'],
self.pid.pidstats['setpoint'],
self.pid.pidstats['err'],
self.pid.pidstats['pid'],
self.pid.pidstats['p'],
self.pid.pidstats['i'],
self.pid.pidstats['d'],
self.heat,
self.runtime,
self.totaltime,
time_left,
self.switch_count))
except KeyError:
pass
def run(self):
while True:
if self.state == "IDLE":
@ -401,14 +457,19 @@ class Oven(threading.Thread):
self.kiln_must_catch_up()
self.update_runtime()
self.update_target_temp()
self.heat_then_cool()
self.set_heat()
self.reset_if_emergency()
self.reset_if_schedule_ended()
#self.display.update_display()
class SimulatedOven(Oven):
def __init__(self):
# call parent init
Oven.__init__(self)
self.board = BoardSimulated()
#self.output = OutputSimulated()
self.t_env = config.sim_t_env
self.c_heat = config.sim_c_heat
self.c_oven = config.sim_c_oven
@ -421,8 +482,6 @@ class SimulatedOven(Oven):
self.t = self.t_env # deg C temp of oven
self.t_h = self.t_env #deg C temp of heating element
super().__init__()
# start thread
self.start()
log.info("SimulatedOven started")
@ -446,69 +505,18 @@ class SimulatedOven(Oven):
#temperature change of oven by cooling to environment
self.p_env = (self.t - self.t_env) / self.R_o_nocool
self.t -= self.p_env * self.time_step / self.c_oven
self.temperature = self.t
self.board.temp_sensor.temperature = self.t
def heat_then_cool(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature +
config.thermocouple_offset)
heat_on = float(self.time_step * pid)
if not self.on_first and heat_on < 0.1 * self.time_step:
heat_on = 0
if self.on_first and heat_on > 0.9 * self.time_step:
heat_on = self.time_step
heat_off = self.time_step - heat_on
if (heat_off > 0 and heat_off < self.time_step) or (heat_on > 0 and heat_on < self.time_step):
self.switch_count += 1
def set_heat(self):
Oven.set_heat(self)
self.heating_energy(heat_on)
self.heating_energy(self.heat)
self.temp_changes()
# self.heat is for the front end to display if the heat is on
self.heat = 0.0
if heat_on > 0:
self.heat = heat_on
log.info("simulation: -> %dW heater: %.0f -> %dW oven: %.0f -> %dW env" % (int(self.p_heat * pid),
self.t_h,
int(self.p_ho),
self.t,
int(self.p_env)))
time_left = self.totaltime - self.runtime
self.on_first = not self.on_first
try:
log.info("temp=%.2f, target=%.2f, error=%.2f, pid=%.2f, p=%.2f, i=%.2f, d=%.2f, heat_on=%.2f, heat_off=%.2f, run_time=%d, total_time=%d, time_left=%d, switch_count=%d" %
(self.pid.pidstats['ispoint'],
self.pid.pidstats['setpoint'],
self.pid.pidstats['err'],
self.pid.pidstats['pid'],
self.pid.pidstats['p'],
self.pid.pidstats['i'],
self.pid.pidstats['d'],
heat_on,
heat_off,
self.runtime,
self.totaltime,
time_left,
self.switch_count))
except KeyError:
pass
# we don't actually spend time heating & cooling during
# a simulation, so sleep.
time.sleep(self.time_step)
class RealOven(Oven):
def __init__(self):
self.board = Board()
self.output = Output()
self.reset()
# call parent init
Oven.__init__(self)
@ -518,56 +526,7 @@ class RealOven(Oven):
def reset(self):
super().reset()
self.output.cool(0)
def heat_then_cool(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature +
config.thermocouple_offset)
heat_on = float(self.time_step * pid)
if not self.on_first and heat_on < 0.1 * self.time_step:
heat_on = 0
if self.on_first and heat_on > 0.9 * self.time_step:
heat_on = self.time_step
heat_off = self.time_step - heat_on
if (heat_off > 0 and heat_off < self.time_step) or (heat_on > 0 and heat_on < self.time_step):
self.switch_count += 1
# self.heat is for the front end to display if the heat is on
self.heat = 0.0
if heat_on > 0:
self.heat = heat_on
if self.on_first:
if heat_on:
self.output.heat(heat_on)
if heat_off:
self.output.cool(heat_off)
else:
if heat_off:
self.output.cool(heat_off)
if heat_on:
self.output.heat(heat_on)
time_left = self.totaltime - self.runtime
self.on_first = not self.on_first
try:
log.info("temp=%.2f, target=%.2f, error=%.2f, pid=%.2f, p=%.2f, i=%.2f, d=%.2f, heat_on=%.2f, heat_off=%.2f, run_time=%d, total_time=%d, time_left=%d, switch_count=%d" %
(self.pid.pidstats['ispoint'],
self.pid.pidstats['setpoint'],
self.pid.pidstats['err'],
self.pid.pidstats['pid'],
self.pid.pidstats['p'],
self.pid.pidstats['i'],
self.pid.pidstats['d'],
heat_on,
heat_off,
self.runtime,
self.totaltime,
time_left,
self.switch_count))
except KeyError:
pass
class Profile():
def __init__(self, json_data):
@ -615,24 +574,15 @@ class PID():
self.lastErr = 0
self.pidstats = {}
# FIX - this was using a really small window where the PID control
# takes effect from -1 to 1. I changed this to various numbers and
# settled on -50 to 50 and then divide by 50 at the end. This results
# in a larger PID control window and much more accurate control...
# instead of what used to be binary on/off control.
def compute(self, setpoint, ispoint):
now = datetime.datetime.now()
timeDelta = (now - self.lastNow).total_seconds()
window_size = 100
error = float(setpoint - ispoint)
# this removes the need for config.stop_integral_windup
# it turns the controller into a binary on/off switch
# any time it's outside the window defined by
# config.pid_control_window
icomp = 0
output = 0
out4logs = 0
dErr = 0
@ -645,21 +595,15 @@ class PID():
log.info("kiln outside pid control window, max heating")
output = 1
else:
icomp = (error * timeDelta * (1/self.ki))
self.iterm += (error * timeDelta * (1/self.ki))
dErr = (error - self.lastErr) / timeDelta
output = self.kp * error + self.iterm + self.kd * dErr
output = sorted([-1 * window_size, output, window_size])[1]
out4logs = output
output = float(output / window_size)
output = round(sorted([0, output, 1])[1])
self.lastErr = error
self.lastNow = now
# no active cooling
if output < 0:
output = 0
self.pidstats = {
'time': time.mktime(now.timetuple()),
'timeDelta': timeDelta,

201
lib/rotary.py 100644
Wyświetl plik

@ -0,0 +1,201 @@
# Rotary encoder class based on pigpio library
# version: 0.2.5
try:
import pigpio
except ModuleNotFoundError:
import sys
from unittest.mock import MagicMock
sys.modules['pigpio'] = MagicMock()
import time
# States
dt_gpio1 = 'D' # dt_gpio is high
dt_gpio0 = 'd' # dt_gpio is low
clk_gpio1 = 'C' # clk_gpio is high
clk_gpio0 = 'c' # clk_gpio is low
# State sequences
SEQUENCE_UP = dt_gpio1 + clk_gpio1 + dt_gpio0 + clk_gpio0
SEQUENCE_DOWN = clk_gpio1 + dt_gpio1 + clk_gpio0 + dt_gpio0
class Rotary:
sequence = ''
# Default values for the rotary encoder
min = 0
max = 100
scale = 1
debounce = 300
_counter = 0
last_counter = 0
rotary_callback = None
# Default values for the sw_gpioitch
sw_gpio_debounce = 300
long_press_opt = False
sw_gpio_short_callback = None
sw_gpio_long_callback = None
up_callback = None
down_callback = None
sw_short_callback = None
sw_long_callback = None
sw_debounce = None
wait_time = time.time()
long = False
def __init__(self, clk_gpio=None, dt_gpio=None, sw_gpio=None, debug=False):
if not (clk_gpio and dt_gpio):
raise BaseException("clk_gpio and dt_gpio pin must be specified!")
self.DEBUG = debug
self.pi = pigpio.pi()
self.clk_gpio = clk_gpio
self.dt_gpio = dt_gpio
self.pi.set_glitch_filter(self.clk_gpio, self.debounce)
self.pi.set_glitch_filter(self.dt_gpio, self.debounce)
if sw_gpio is not None:
self.sw_gpio = sw_gpio
self.pi.set_pull_up_down(self.sw_gpio, pigpio.PUD_UP)
self.pi.set_glitch_filter(self.sw_gpio, self.sw_gpio_debounce)
self.setup_pigpio_callbacks()
def setup_pigpio_callbacks(self):
self.pi.callback(self.clk_gpio, pigpio.FALLING_EDGE, self.clk_gpio_fall)
self.pi.callback(self.clk_gpio, pigpio.RISING_EDGE, self.clk_gpio_rise)
self.pi.callback(self.dt_gpio, pigpio.FALLING_EDGE, self.dt_gpio_fall)
self.pi.callback(self.dt_gpio, pigpio.RISING_EDGE, self.dt_gpio_rise)
if self.sw_gpio is not None:
self.pi.callback(self.sw_gpio, pigpio.FALLING_EDGE, self.sw_gpio_fall)
self.pi.callback(self.sw_gpio, pigpio.RISING_EDGE, self.sw_gpio_rise)
@property
def counter(self):
return self._counter
@counter.setter
def counter(self, value):
if self._counter != value:
self._counter = value
if self.rotary_callback:
self.rotary_callback(self._counter)
def clk_gpio_fall(self, _gpio, _level, _tick):
if self.DEBUG:
print(self.sequence + ':{}'.format(clk_gpio1))
if len(self.sequence) > 2:
self.sequence = ''
self.sequence += clk_gpio1
def clk_gpio_rise(self, _gpio, _level, _tick):
if self.DEBUG:
print(self.sequence + ':{}'.format(clk_gpio0))
self.sequence += clk_gpio0
if self.sequence == SEQUENCE_UP:
if self.counter < self.max:
self.counter += self.scale
if self.up_callback:
self.up_callback(self._counter)
self.sequence = ''
def dt_gpio_fall(self, _gpio, _level, _tick):
if self.DEBUG:
print(self.sequence + ':{}'.format(dt_gpio1))
if len(self.sequence) > 2:
self.sequence = ''
self.sequence += dt_gpio1
def dt_gpio_rise(self, _gpio, _level, _tick):
if self.DEBUG:
print(self.sequence + ':{}'.format(dt_gpio0))
self.sequence += dt_gpio0
if self.sequence == SEQUENCE_DOWN:
if self.counter > self.min:
self.counter -= self.scale
if self.down_callback:
self.down_callback(self._counter)
self.sequence = ''
def sw_gpio_rise(self, _gpio, _level, _tick):
if self.long_press_opt:
if not self.long:
self.short_press()
def sw_gpio_fall(self, _gpio, _level, _tick):
if self.long_press_opt:
self.long = False
press_time = time.time()
while self.pi.read(self.sw_gpio) == 0:
self.wait_time = time.time()
time.sleep(0.1)
if self.wait_time - press_time > 1.5:
self.long_press()
self.long = True
break
else:
self.short_press()
def setup_rotary(
self,
rotary_callback=None,
up_callback=None,
down_callback=None,
min=None,
max=None,
scale=None,
debounce=None,
):
if not (rotary_callback or up_callback or down_callback):
print('At least one callback should be given')
# rotary callback has to be set first since the self.counter property depends on it
self.rotary_callback = rotary_callback
self.up_callback = up_callback
self.down_callback = down_callback
if min is not None:
self.min = min
self.counter = self.min
self.last_counter = self.min
if max is not None:
self.max = max
if scale is not None:
self.scale = scale
if debounce is not None:
self.debounce = debounce
self.pi.set_glitch_filter(self.clk_gpio, self.debounce)
self.pi.set_glitch_filter(self.dt_gpio, self.debounce)
def setup_switch(self,
sw_short_callback=None,
sw_long_callback=None,
debounce=None,
long_press=None
):
assert sw_short_callback is not None or sw_long_callback is not None
if sw_short_callback is not None:
self.sw_short_callback = sw_short_callback
if sw_long_callback is not None:
self.sw_long_callback = sw_long_callback
if debounce is not None:
self.sw_debounce = debounce
self.pi.set_glitch_filter(self.sw_gpio, self.sw_debounce)
if long_press is not None:
self.long_press_opt = long_press
@staticmethod
def watch():
"""
A simple convenience function to have a waiting loop
"""
while True:
time.sleep(10)
def short_press(self):
self.sw_short_callback()
def long_press(self):
self.sw_long_callback()

19
lib/rotaryinput.py 100644
Wyświetl plik

@ -0,0 +1,19 @@
from RPi_GPIO_Rotary import rotary
class RotaryInput():
def __init__(self):
## Initialise (clk, dt, sw, ticks)
self.input = rotary.Rotary(18,15,14,2)
self.input.start()
def on_next(self, action):
self.input.register(increment=action)
def on_previous(self, action):
self.input.register(decrement=action)
def on_click(self, action):
self.input.register(pressed=action)
def stop(self):
self.input.stop()

Wyświetl plik

@ -126,15 +126,11 @@ body {
.bar {
width:70%;
//padding: 2px 2px 0px 2px;
//display:block;
display: inline-block;
font-family:arial;
font-size:12px;
background-color:#ca3c38;
color:#000;
//position:absolute;
//bottom:0;
}
.ds-led-hazard-active {

Wyświetl plik

@ -502,10 +502,10 @@ $(document).ready(function()
ws_status.onmessage = function(e)
{
console.log("received status data")
console.log(e.data);
x = JSON.parse(e.data);
document.getElementById('json').innerHTML = JSON.stringify(x, undefined, 4);
if (x.type == "backlog")
{
if (x.profile)
@ -562,7 +562,7 @@ $(document).ready(function()
updateProgress(parseFloat(x.runtime)/parseFloat(x.totaltime)*100);
$('#state').html('<span class="glyphicon glyphicon-time" style="font-size: 22px; font-weight: normal"></span><span style="font-family: Digi; font-size: 40px;">' + eta + '</span>');
$('#target_temp').html(parseInt(x.target));
$('#cost').html(x.currency_type + parseFloat(x.cost).toFixed(2));
$('#cost').html(x.currency_type + parseFloat(x.cost).toFixed(0));
@ -575,7 +575,8 @@ $(document).ready(function()
}
$('#act_temp').html(parseInt(x.temperature));
$('#heat').html('<div class="bar" style="height:'+x.pidstats.out*70+'%;"></div>')
//$('#heat').html('<div class="bar" style="height:'+x.pidstats.out*70+'%;"></div>')
if (x.heat > 0.5) { $('#heat').addClass("ds-led-heat-active"); } else { $('#heat').removeClass("ds-led-heat-active"); }
if (x.cool > 0.5) { $('#cool').addClass("ds-led-cool-active"); } else { $('#cool').removeClass("ds-led-cool-active"); }
if (x.air > 0.5) { $('#air').addClass("ds-led-air-active"); } else { $('#air').removeClass("ds-led-air-active"); }
if (x.temperature > hazardTemp()) { $('#hazard').addClass("ds-led-hazard-active"); } else { $('#hazard').removeClass("ds-led-hazard-active"); }

Wyświetl plik

@ -36,7 +36,7 @@
<div class="ds-panel">
<div class="display ds-num"><span id="act_temp">25</span><span class="ds-unit" id="act_temp_scale" >&deg;C</span></div>
<div class="display ds-num ds-target"><span id="target_temp">---</span><span class="ds-unit" id="target_temp_scale">&deg;C</span></div>
<div class="display ds-num ds-cost"><span id="cost">0.00</span><span class="ds-unit" id="cost"></span></div>
<div class="display ds-num ds-cost"><span id="cost">0</span><span class="ds-unit" id="cost"></span></div>
<div class="display ds-num ds-text" id="state"></div>
<div class="display pull-right ds-state" style="padding-right:0"><span class="ds-led" id="heat">&#92;</span><span class="ds-led" id="cool">&#108;</span><span class="ds-led" id="air">&#91;</span><span class="ds-led" id="hazard">&#73;</span><span class="ds-led" id="door">&#9832;</span></div>
</div>
@ -92,6 +92,8 @@
</div>
</div>
<pre style="white-space: pre-wrap;" id="json"></pre>
<div id="jobSummaryModal" class="modal fade" tabindex="-1" aria-hidden="true" style="display: none;">
<div class="modal-dialog">
<div class="modal-content">