Merge 60c78175a3 into 2674cff92b
This commit is contained in:
commit
1785b64937
19
config.py
19
config.py
@ -49,6 +49,9 @@ gpio_sensor_di = 10 # only used with max31856
|
||||
### value is used.
|
||||
sensor_time_wait = 2
|
||||
|
||||
# GPIO to use for the beeper
|
||||
gpio_beeper = 12
|
||||
|
||||
|
||||
########################################################################
|
||||
#
|
||||
@ -57,7 +60,7 @@ sensor_time_wait = 2
|
||||
# These parameters work 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
|
||||
pid_kp = 25 # Proportional
|
||||
pid_ki = 200 # Integral
|
||||
pid_kd = 200 # Derivative
|
||||
|
||||
@ -66,11 +69,11 @@ pid_kd = 200 # Derivative
|
||||
#
|
||||
# Initial heating and Integral Windup
|
||||
#
|
||||
# During initial heating, if the temperature is constantly under the
|
||||
# During initial heating, if the temperature is constantly under the
|
||||
# setpoint,large amounts of Integral can accumulate. This accumulation
|
||||
# causes the kiln to run above the setpoint for potentially a long
|
||||
# period of time. These settings allow integral accumulation only when
|
||||
# the temperature is within stop_integral_windup_margin percent below
|
||||
# the temperature is within stop_integral_windup_margin percent below
|
||||
# or above the setpoint. This applies only to the integral.
|
||||
stop_integral_windup = True
|
||||
stop_integral_windup_margin = 10
|
||||
@ -96,20 +99,20 @@ sim_R_ho_air = 0.05 # K/W " with internal air circulation
|
||||
# If you change the temp_scale, all settings in this file are assumed to
|
||||
# be in that scale.
|
||||
|
||||
temp_scale = "f" # c = Celsius | f = Fahrenheit - Unit to display
|
||||
temp_scale = "f" # c = Celsius | f = Fahrenheit - Unit to display
|
||||
time_scale_slope = "h" # s = Seconds | m = Minutes | h = Hours - Slope displayed in temp_scale per time_scale_slope
|
||||
time_scale_profile = "m" # s = Seconds | m = Minutes | h = Hours - Enter and view target time in time_scale_profile
|
||||
|
||||
# emergency shutoff the profile if this temp is reached or exceeded.
|
||||
# This just shuts off the profile. If your SSR is working, your kiln will
|
||||
# naturally cool off. If your SSR has failed/shorted/closed circuit, this
|
||||
# naturally cool off. If your SSR has failed/shorted/closed circuit, this
|
||||
# means your kiln receives full power until your house burns down.
|
||||
# this should not replace you watching your kiln or use of a kiln-sitter
|
||||
emergency_shutoff_temp = 2264 #cone 7
|
||||
emergency_shutoff_temp = 2264 #cone 7
|
||||
|
||||
# If the kiln cannot heat or cool fast enough and is off by more than
|
||||
# If the kiln cannot heat or cool fast enough and is off by more than
|
||||
# kiln_must_catch_up_max_error the entire schedule is shifted until
|
||||
# the desired temperature is reached. If your kiln cannot attain the
|
||||
# the desired temperature is reached. If your kiln cannot attain the
|
||||
# wanted temperature, the schedule will run forever.
|
||||
kiln_must_catch_up = True
|
||||
kiln_must_catch_up_max_error = 10 #degrees
|
||||
|
||||
210
kiln-display.py
Normal file
210
kiln-display.py
Normal file
@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import websocket
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
import argparse
|
||||
import digitalio
|
||||
import board
|
||||
import adafruit_rgb_display.st7789 as st7789
|
||||
import RPi.GPIO as GPIO
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import config
|
||||
|
||||
# This is designed to drive an Adafruit Mini PiTFT 1.3" (https://www.adafruit.com/product/4484)
|
||||
#
|
||||
# You will require a copy of DroidSans.ttf in /home/pi
|
||||
#
|
||||
# As this occupies the GPIOs currently used as defaults in config.py, you'll have to rewrire your Pi.
|
||||
# Remember to update config.py with the new ones!
|
||||
#
|
||||
# Technically you do not need to install numpy, but it is very much recommended as the
|
||||
# non-numpy fallback code will consume much CPU.
|
||||
|
||||
|
||||
def beep(delay):
|
||||
GPIO.output(config.gpio_beeper, GPIO.HIGH)
|
||||
time.sleep(delay)
|
||||
GPIO.output(config.gpio_beeper, GPIO.LOW)
|
||||
|
||||
|
||||
def morse(code):
|
||||
for c in code:
|
||||
if c == '.':
|
||||
beep(0.25)
|
||||
|
||||
elif c == '-':
|
||||
beep(0.5)
|
||||
|
||||
time.sleep(0.25)
|
||||
|
||||
|
||||
def display(hostname, minupdatesecs, font_ttf):
|
||||
status_ws = websocket.WebSocket()
|
||||
storage_ws = websocket.WebSocket()
|
||||
|
||||
# setup beeper GPIO
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(config.gpio_beeper, GPIO.OUT)
|
||||
GPIO.output(config.gpio_beeper, GPIO.LOW)
|
||||
|
||||
# Configuration for CS and DC pins for Raspberry Pi
|
||||
cs_pin = digitalio.DigitalInOut(board.CE0)
|
||||
dc_pin = digitalio.DigitalInOut(board.D25)
|
||||
reset_pin = None
|
||||
BAUDRATE = 64000000 # The pi can be very fast!
|
||||
# Create the ST7789 display:
|
||||
display = st7789.ST7789(
|
||||
board.SPI(),
|
||||
cs=cs_pin,
|
||||
dc=dc_pin,
|
||||
rst=reset_pin,
|
||||
baudrate=BAUDRATE,
|
||||
height=240, y_offset=80, rotation=180
|
||||
)
|
||||
display.fill()
|
||||
|
||||
# turn backlight on
|
||||
backlight = digitalio.DigitalInOut(board.D22)
|
||||
backlight.switch_to_output()
|
||||
backlight.value = True
|
||||
|
||||
# create screen and font
|
||||
screen = Image.new("RGB", (display.width, display.height), (0, 0, 0))
|
||||
screend = ImageDraw.Draw(screen)
|
||||
screenfont = ImageFont.truetype(font_ttf, 46)
|
||||
chartminx = 0
|
||||
chartw = display.width
|
||||
chartminy = int(display.height / 2)
|
||||
charth = int(display.height / 2)
|
||||
|
||||
# main loop
|
||||
state = 'idle'
|
||||
cur_profile = None
|
||||
last_update = datetime.datetime.now()
|
||||
while True:
|
||||
# gather data from kiln controller.
|
||||
try:
|
||||
msg = json.loads(status_ws.recv())
|
||||
if msg.get('profile') and not cur_profile:
|
||||
storage_ws.send('GET')
|
||||
for profile in json.loads(storage_ws.recv()):
|
||||
if profile['name'] == msg.get('profile'):
|
||||
cur_profile = profile
|
||||
break
|
||||
|
||||
elif not msg.get('profile'):
|
||||
cur_profile = None
|
||||
|
||||
except websocket.WebSocketException:
|
||||
try:
|
||||
status_ws.connect(f'ws://{hostname}/status')
|
||||
storage_ws.connect(f'ws://{hostname}/storage')
|
||||
except Exception:
|
||||
time.sleep(5)
|
||||
|
||||
continue
|
||||
|
||||
if state == 'idle' and cur_profile:
|
||||
state = 'profile_tempok'
|
||||
|
||||
elif state != 'idle' and not cur_profile:
|
||||
state = 'idle'
|
||||
morse('-.-.') # (C) Profile Complete
|
||||
|
||||
if state == 'profile_tempok':
|
||||
tempdelta = abs(msg.get('temperature', 0) - msg.get('target', 0))
|
||||
if tempdelta > 5:
|
||||
state = 'profile_catchup'
|
||||
morse('....') # (H) Temp bad
|
||||
|
||||
elif state == 'profile_catchup':
|
||||
tempdelta = abs(msg.get('temperature', 0) - msg.get('target', 0))
|
||||
if tempdelta < 1:
|
||||
state = 'profile_tempok'
|
||||
morse('-') # (T) Temp ok
|
||||
|
||||
# we don't need to update ALL the time
|
||||
if (datetime.datetime.now() - last_update).total_seconds() < minupdatesecs:
|
||||
continue
|
||||
last_update = datetime.datetime.now()
|
||||
|
||||
# setup the basic display
|
||||
screend.rectangle([0, 0, display.width, display.height], fill='black')
|
||||
screend.line([chartminx, chartminy, chartminx + chartw, chartminy], fill='white')
|
||||
|
||||
# show the current temperature
|
||||
if msg.get('temperature'):
|
||||
temp = int(msg['temperature'])
|
||||
text = f"{temp}°"
|
||||
(tw, th) = screenfont.getsize(text)
|
||||
screend.text((0, display.height - th), text, font=screenfont, fill='blue')
|
||||
|
||||
# inform if we're actively heating
|
||||
if msg.get('heat'):
|
||||
spot_radius = 20
|
||||
spot_x = (display.width - spot_radius) / 2
|
||||
spot_y = display.height - spot_radius - 5
|
||||
screend.ellipse((spot_x, spot_y, spot_x + spot_radius, spot_y + spot_radius), fill='red')
|
||||
|
||||
# if we have a profile, show details of that!
|
||||
if cur_profile:
|
||||
cur_profile_data = cur_profile['data']
|
||||
|
||||
# compute ranges of data
|
||||
mintime = min([i[0] for i in cur_profile_data])
|
||||
maxtime = max([i[0] for i in cur_profile_data])
|
||||
timerange = maxtime - mintime
|
||||
mintemp = 0
|
||||
maxtemp = max([i[1] for i in cur_profile_data])
|
||||
temprange = maxtemp - mintemp
|
||||
|
||||
# draw chart of the temperature profie
|
||||
line = []
|
||||
for i in sorted(cur_profile_data, key=lambda x: x[0]):
|
||||
x = chartminx + (((i[0] - mintime) * chartw) / timerange)
|
||||
y = chartminy - (((i[1] - mintemp) * charth) / temprange)
|
||||
line.extend([x, y])
|
||||
screend.line(line, fill='yellow')
|
||||
|
||||
# draw current position as a blue line
|
||||
cur_time = msg['runtime'] if msg['runtime'] > 0 else 0
|
||||
cur_time_x = ((cur_time - mintime) * chartw) / timerange
|
||||
cur_temp = int(msg['temperature'])
|
||||
cur_temp_y = ((cur_temp - mintemp) * charth) / temprange
|
||||
screend.line([chartminx + cur_time_x, chartminy, chartminx + cur_time_x, chartminy - charth], fill='blue')
|
||||
|
||||
# draw target temperature
|
||||
target = int(msg['target'])
|
||||
text = f"{target}°"
|
||||
(tw, th) = screenfont.getsize(text)
|
||||
screend.text((display.width - tw, display.height - th), text, font=screenfont, fill='yellow')
|
||||
|
||||
# show where we are
|
||||
time_done = msg['runtime'] if msg['runtime'] > 0 else 0
|
||||
time_done_mins = int((time_done / 60) % 60)
|
||||
time_done_hours = int(time_done / 60 / 60)
|
||||
screend.text((0, chartminy), f"{time_done_hours:02d}:{time_done_mins:02d}", font=screenfont, fill='blue')
|
||||
|
||||
# show how long we have left
|
||||
time_left = msg['totaltime'] - msg['runtime']
|
||||
time_left_mins = int((time_left / 60) % 60)
|
||||
time_left_hours = int((time_left / 60) / 60)
|
||||
text = f"{time_left_hours:02d}:{time_left_mins:02d}"
|
||||
(tw, th) = screenfont.getsize(text)
|
||||
screend.text((display.width - tw, chartminy), text, font=screenfont, fill='white')
|
||||
|
||||
# update display
|
||||
display.image(screen)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Log kiln data for analysis.')
|
||||
parser.add_argument('--hostname', type=str, default="localhost:8081", help="The kiln-controller hostname:port")
|
||||
parser.add_argument('--minupdatesecs', type=int, default="10", help="Number of seconds between screen updates")
|
||||
parser.add_argument('--font_ttf', type=str, default='/home/pi/DroidSans.ttf', help="Font to use for text display")
|
||||
args = parser.parse_args()
|
||||
|
||||
display(args.hostname, args.minupdatesecs, args.font_ttf)
|
||||
@ -2,6 +2,7 @@
|
||||
Description=kiln-controller
|
||||
|
||||
[Service]
|
||||
Nice=-20
|
||||
ExecStart=/home/pi/kiln-controller/venv/bin/python /home/pi/kiln-controller/kiln-controller.py
|
||||
|
||||
[Install]
|
||||
|
||||
9
lib/init/kiln-display.service
Normal file
9
lib/init/kiln-display.service
Normal file
@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=kiln-display
|
||||
|
||||
[Service]
|
||||
Nice=10
|
||||
ExecStart=/home/pi/kiln-controller/venv/bin/python /home/pi/kiln-controller/kiln-display.py
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
4
requirements-kiln-display.txt
Normal file
4
requirements-kiln-display.txt
Normal file
@ -0,0 +1,4 @@
|
||||
websocket-client
|
||||
Pillow
|
||||
adafruit-circuitpython-rgb-display
|
||||
numpy
|
||||
Loading…
Reference in New Issue
Block a user