Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
### value is used.
sensor_time_wait = 2

# GPIO to use for the beeper
gpio_beeper = 12


########################################################################
#
Expand All @@ -57,7 +60,7 @@
# 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

Expand All @@ -66,11 +69,11 @@
#
# 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
Expand All @@ -96,20 +99,20 @@
# 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
Expand Down
210 changes: 210 additions & 0 deletions kiln-display.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions lib/init/kiln-controller.service
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
9 changes: 9 additions & 0 deletions lib/init/kiln-display.service
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions requirements-kiln-display.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
websocket-client
Pillow
adafruit-circuitpython-rgb-display
numpy