diff --git a/config.py b/config.py index 10d3a247..78483233 100644 --- a/config.py +++ b/config.py @@ -49,6 +49,9 @@ ### value is used. sensor_time_wait = 2 +# GPIO to use for the beeper +gpio_beeper = 12 + ######################################################################## # @@ -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 @@ -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 @@ -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 diff --git a/kiln-display.py b/kiln-display.py new file mode 100644 index 00000000..83686b0b --- /dev/null +++ b/kiln-display.py @@ -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) diff --git a/lib/init/kiln-controller.service b/lib/init/kiln-controller.service index 318e4c05..e8cb3efd 100644 --- a/lib/init/kiln-controller.service +++ b/lib/init/kiln-controller.service @@ -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] diff --git a/lib/init/kiln-display.service b/lib/init/kiln-display.service new file mode 100644 index 00000000..87fd412e --- /dev/null +++ b/lib/init/kiln-display.service @@ -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 diff --git a/requirements-kiln-display.txt b/requirements-kiln-display.txt new file mode 100644 index 00000000..2611ff86 --- /dev/null +++ b/requirements-kiln-display.txt @@ -0,0 +1,4 @@ +websocket-client +Pillow +adafruit-circuitpython-rgb-display +numpy