Install Libraries
Make sure you are running the latest version of Adafruit CircuitPython for your board. This project requires at least CircuitPython version 6.1.0. You will need to install the appropriate libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle matching your version of CircuitPython.Â
You can find these files in the CircuitPython Library bundle here.
- adafruit_bitmap_font/
- adafruit_bus_device/
- adafruit_button.mpy
- adafruit_display_shapes/
- adafruit_display_text/
- adafruit_esp32spi/
- adafruit_io/
- adafruit_mcp9600.mpy
- adafruit_portalbase/
- adafruit_pyportal/
- adafruit_register/
- adafruit_requests.mpy
- adafruit_touchscreen.mpy
- neopixel.mpy
Install Code
Download the project files from the GitHub repo.
In any of the code boxes below, you can get all the files in one step by clicking the "Download Project Bundle" button.
In addition to the code.py file, this project also requires several supporting folders and files. These folders are required for this project:
- fonts - Contains fonts used for displaying text on the PyPortal
- profiles - Contains profiles of solder pastes
These additional files are also needed:
- codecalibrate.py - A program used to calibrate the toaster oven with the EZ Make Oven code. Run this when installing new versions or replacing the toaster oven.
- config.json - Contains configuration settings, including the values created from the codecalibrate.py program.
Calibrating the Oven
Not all toaster ovens are created equally. The EZ Make Oven takes into account various factors in a toaster oven, including size, wattage, and rate of temperature rise. This is done using the calibration sketch, which is used gather information about how the oven reacts to temperature changes.
You will need to temporarily rename the codecalibrate.py program to code.py (and rename code.py to another name temporarily) in order to run the calibration program.
This program will turn on the oven, let the oven temperature ramp up to 100 degrees C, turn it off, and measure the time and temperature difference when the temperature levels off. These values will be printed on the display, which will need to be written down and then entered in the config.json file.
This calibration program should be run when installing new versions of the EZ Make Oven or when replacing the toaster oven for another toaster oven.
# SPDX-FileCopyrightText: 2019 Dan Cogliano for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import sys
import board
import busio
import digitalio
from adafruit_mcp9600 import MCP9600
SENSOR_ADDR = 0X67
i2c = busio.I2C(board.SCL, board.SDA,frequency=200000)
try:
sensor = MCP9600(i2c,SENSOR_ADDR,"K")
except ValueError as e:
print(e)
print("Unable to connect to the thermocouple sensor.")
sys.exit(1)
oven = digitalio.DigitalInOut(board.D4)
oven.direction = digitalio.Direction.OUTPUT
def oven_control(enable=False):
#board.D4
oven.value = enable
check_temp = 100
print("This program will determine calibration settings ")
print("for your oven to use with the EZ Make Oven.\n\n")
for i in range(10):
print("Calibration will start in %d seconds..." % (10-i))
time.sleep(1)
print("Starting...")
print("Calibrating oven temperature to %d C" % check_temp)
finish = False
oven_control(True)
maxloop=300
counter = 0
while not finish:
time.sleep(1)
counter += 1
current_temp = sensor.temperature
print("%.02f C" % current_temp)
if current_temp >= check_temp:
finish = True
oven_control(False)
if counter >= maxloop:
raise Exception("Oven not working or bad sensor")
print("checking oven lag time and temperature")
finish = False
start_time = time.monotonic()
start_temp = sensor.temperature
last_temp = start_temp
while not finish:
time.sleep(1)
current_temp = sensor.temperature
print(current_temp)
if current_temp <= last_temp:
finish = True
last_temp = current_temp
lag_temp = last_temp - check_temp
lag_time = int(time.monotonic() - start_time)
print("** Calibration Results **")
print("Modify config.json with these values for your oven:")
print("calibrate_temp:", lag_temp)
print("calibrate_seconds:",lag_time)
Solder Paste Profiles
The EZ Make Oven contains a few solder paste profiles in the profiles folder. You can view these profiles with a text editor to see if one of them closely matches the solder paste you are using.
If it does, great! Update the config.json file with the appropriate solder paste file name (without the ".json" extension).
If not, you can easily create a new profile in the profiles folder. Use one of the existing profiles as a guide. It would not hurt to add a few additional data points to smooth out the curve, as the EZ Make Oven uses straight lines to connect the profile points.
Here are some of the items in the profile file explained:
-
temp_range: temperature range of the profile, used to define the minimum and maximum y axis of the graph -
time_range: time range of the profile, used to define the minimum and maximum x axis of the graph -
stages: start points for the stages preheat, soak, reflow and cool. The format of the point is [x, y], where x is the time value and y is the temperature value of the starting point of the stage -
profile: data points that make up the solder profile. The format of each point is [x, y], where x is the time value and y is the temperature value value of a point on the solder profile graph
The Code for the EZ Make Oven
The code is available from GitHub. You will need to copy and modify the config.json file, which contains the solder profile, the MCP9600 I2C address of the sensor board and the calibration settings for the oven.
{
"sensor_address": 103,
"profile": "sn63pb37",
"calibrate_temp": 29.5625,
"calibrate_seconds": 37
}
The EZ Make Oven code can be downloaded from the link below or copied and paste to the file code.py on the PyPortal CIRCUITPY drive.Â
# SPDX-FileCopyrightText: 2019 Dan Cogliano for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import json
import array
import math
import gc
import board
import busio
import audioio
import audiocore
import displayio
import digitalio
import os
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import bitmap_label as label
from adafruit_display_shapes.circle import Circle
from adafruit_button import Button
import adafruit_touchscreen
from adafruit_mcp9600 import MCP9600
TITLE = "EZ Make Oven Controller"
VERSION = "1.3.3"
print(TITLE, "version ", VERSION)
time.sleep(2)
PROFILE_SIZE = 2 # plot thickness
GRID_SIZE = 2
GRID_STYLE = 3
TEMP_SIZE = 2
AXIS_SIZE = 2
BLACK = 0x0
BLUE = 0x2020FF
GREEN = 0x00FF55
RED = 0xFF0000
YELLOW = 0xFFFF00
WIDTH = board.DISPLAY.width
HEIGHT = board.DISPLAY.height
palette = displayio.Palette(5)
palette[0] = BLACK
palette[1] = GREEN
palette[2] = BLUE
palette[3] = RED
palette[4] = YELLOW
palette.make_transparent(0)
BACKGROUND_COLOR = 0
PROFILE_COLOR = 1
GRID_COLOR = 2
TEMP_COLOR = 3
AXIS_COLOR = 2
GXSTART = 100
GYSTART = 80
GWIDTH = WIDTH - GXSTART
GHEIGHT = HEIGHT - GYSTART
ts = adafruit_touchscreen.Touchscreen(
board.TOUCH_XL,
board.TOUCH_XR,
board.TOUCH_YD,
board.TOUCH_YU,
calibration=((5200, 59000), (5800, 57000)),
size=(WIDTH, HEIGHT),
)
class Beep(object):
def __init__(self):
self.duration = 0
self.start = 0
tone_volume = 1 # volume is from 0.0 to 1.0
frequency = 440 # Set this to the Hz of the tone you want to generate.
length = 4000 // frequency
sine_wave = array.array("H", [0] * length)
for i in range(length):
sine_wave[i] = int(
(1 + math.sin(math.pi * 2 * i / length)) * tone_volume * (2 ** 15 - 1)
)
self.sine_wave_sample = audiocore.RawSample(sine_wave)
self._speaker_enable = digitalio.DigitalInOut(board.SPEAKER_ENABLE)
self._speaker_enable.switch_to_output(False)
if hasattr(board, "AUDIO_OUT"):
self.audio = audioio.AudioOut(board.AUDIO_OUT)
elif hasattr(board, "SPEAKER"):
self.audio = audioio.AudioOut(board.SPEAKER)
else:
raise AttributeError("Board does not have a builtin speaker!")
# pylint: disable=protected-access
def play(self, duration=0.1):
if not self._speaker_enable.value:
self._speaker_enable.value = True
self.audio.play(self.sine_wave_sample, loop=True)
self.start = time.monotonic()
self.duration = duration
if duration <= 0.5:
# for beeps less than .5 sec, sleep here,
# otherwise, use refresh() in loop to turn off long beep
time.sleep(duration)
self.stop()
def stop(self):
if self._speaker_enable.value:
self.duration = 0
self.audio.stop()
self._speaker_enable.value = False
def refresh(self):
if time.monotonic() - self.start >= self.duration:
self.stop()
class ReflowOvenControl(object):
global message, timediff, sgraph, timer_data
states = ("wait", "ready", "start", "preheat", "soak", "reflow", "cool")
def __init__(self, pin):
self.oven = digitalio.DigitalInOut(pin)
self.oven.direction = digitalio.Direction.OUTPUT
with open("/config.json", mode="r") as fpr:
self.config = json.load(fpr)
fpr.close()
self.sensor_status = False
with open("/profiles/" + self.config["profile"] + ".json", mode="r") as fpr:
self.sprofile = json.load(fpr)
fpr.close()
i2c = busio.I2C(board.SCL, board.SDA, frequency=100000)
try:
self.sensor = MCP9600(i2c, self.config["sensor_address"], "K")
self.ontemp = self.sensor.temperature
self.offtemp = self.ontemp
self.sensor_status = True
except ValueError:
print("temperature sensor not available")
self.control = False
self.reset()
self.beep = Beep()
self.set_state("ready")
if self.sensor_status:
if self.sensor.temperature >= 50:
self.last_state = "wait"
self.set_state("wait")
def reset(self):
self.ontime = 0
self.offtime = 0
self.enable(False)
self.reflow_start = 0
def get_profile_temp(self, seconds):
x1 = self.sprofile["profile"][0][0]
y1 = self.sprofile["profile"][0][1]
for point in self.sprofile["profile"]:
x2 = point[0]
y2 = point[1]
if x1 <= seconds < x2:
temp = y1 + (y2 - y1) * (seconds - x1) // (x2 - x1)
return temp
x1 = x2
y1 = y2
return 0
def set_state(self, state):
self.state = state
self.check_state()
self.last_state = state
# pylint: disable=too-many-branches, too-many-statements
def check_state(self):
try:
temp = self.sensor.temperature
except AttributeError:
temp = 32 # sensor not available, use 32 for testing
self.sensor_status = False
# message.text = "Temperature sensor missing"
self.beep.refresh()
if self.state == "wait":
self.enable(False)
if self.state != self.last_state:
# change in status, time for a beep!
self.beep.play(0.1)
if temp < 35:
self.set_state("ready")
oven.reset()
draw_profile(sgraph, oven.sprofile)
timer_data.text = format_time(0)
if self.state == "ready":
self.enable(False)
if self.state == "start" and temp >= 50:
self.set_state("preheat")
if self.state == "start":
message.text = "Starting"
self.enable(True)
if self.state == "preheat" and temp >= self.sprofile["stages"]["soak"][1]:
self.set_state("soak")
if self.state == "preheat":
message.text = "Preheat"
if self.state == "soak" and temp >= self.sprofile["stages"]["reflow"][1]:
self.set_state("reflow")
if self.state == "soak":
message.text = "Soak"
if (
self.state == "reflow"
and temp >= self.sprofile["stages"]["cool"][1]
and self.reflow_start > 0
and (
time.monotonic() - self.reflow_start
>= self.sprofile["stages"]["cool"][0]
- self.sprofile["stages"]["reflow"][0]
)
):
self.set_state("cool")
self.beep.play(5)
if self.state == "reflow":
message.text = "Reflow"
if self.last_state != "reflow":
self.reflow_start = time.monotonic()
if self.state == "cool":
self.enable(False)
message.text = "Cool Down, Open Door"
if self.state in ("start", "preheat", "soak", "reflow"):
if self.state != self.last_state:
# change in status, time for a beep!
self.beep.play(0.1)
# oven temp control here
# check range of calibration to catch any humps in the graph
checktime = 0
checktimemax = self.config["calibrate_seconds"]
checkoven = False
if not self.control:
checktimemax = max(
0,
self.config["calibrate_seconds"]
- (time.monotonic() - self.offtime),
)
while checktime <= checktimemax:
check_temp = self.get_profile_temp(int(timediff + checktime))
if (
temp + self.config["calibrate_temp"] * checktime / checktimemax
< check_temp
):
checkoven = True
break
checktime += 5
if not checkoven:
# hold oven temperature
if (
self.state in ("start", "preheat", "soak")
and self.offtemp > self.sensor.temperature
):
checkoven = True
self.enable(checkoven)
# turn oven on or off
def enable(self, enable):
try:
self.oven.value = enable
self.control = enable
if enable:
self.offtime = 0
self.ontime = time.monotonic()
self.ontemp = self.sensor.temperature
print("oven on")
else:
self.offtime = time.monotonic()
self.ontime = 0
self.offtemp = self.sensor.temperature
print("oven off")
except AttributeError:
# bad sensor
pass
class Graph(object):
def __init__(self):
self.xmin = 0
self.xmax = 720 # graph up to 12 minutes
self.ymin = 0
self.ymax = 240
self.xstart = 0
self.ystart = 0
self.width = GWIDTH
self.height = GHEIGHT
# pylint: disable=too-many-branches
def draw_line(self, x1, y1, x2, y2, size=PROFILE_SIZE, color=1, style=1):
# print("draw_line:", x1, y1, x2, y2)
# convert graph coords to screen coords
x1p = self.xstart + self.width * (x1 - self.xmin) // (self.xmax - self.xmin)
y1p = self.ystart + int(
self.height * (y1 - self.ymin) / (self.ymax - self.ymin)
)
x2p = self.xstart + self.width * (x2 - self.xmin) // (self.xmax - self.xmin)
y2p = self.ystart + int(
self.height * (y2 - self.ymin) / (self.ymax - self.ymin)
)
# print("screen coords:", x1p, y1p, x2p, y2p)
if (max(x1p, x2p) - min(x1p, x2p)) > (max(y1p, y2p) - min(y1p, y2p)):
for xx in range(min(x1p, x2p), max(x1p, x2p)):
if x2p != x1p:
yy = y1p + (y2p - y1p) * (xx - x1p) // (x2p - x1p)
if style == 2:
if xx % 2 == 0:
self.draw_point(xx, yy, size, color)
elif style == 3:
if xx % 8 == 0:
self.draw_point(xx, yy, size, color)
elif style == 4:
if xx % 12 == 0:
self.draw_point(xx, yy, size, color)
else:
self.draw_point(xx, yy, size, color)
else:
for yy in range(min(y1p, y2p), max(y1p, y2p)):
if y2p != y1p:
xx = x1p + (x2p - x1p) * (yy - y1p) // (y2p - y1p)
if style == 2:
if yy % 2 == 0:
self.draw_point(xx, yy, size, color)
elif style == 3:
if yy % 8 == 0:
self.draw_point(xx, yy, size, color)
elif style == 4:
if yy % 12 == 0:
self.draw_point(xx, yy, size, color)
else:
self.draw_point(xx, yy, size, color)
def draw_graph_point(self, x, y, size=PROFILE_SIZE, color=1):
""" draw point using graph coordinates """
# wrap around graph point when x goes out of bounds
x = (x - self.xmin) % (self.xmax - self.xmin) + self.xmin
xx = self.xstart + self.width * (x - self.xmin) // (self.xmax - self.xmin)
yy = self.ystart + int(self.height * (y - self.ymin) / (self.ymax - self.ymin))
print("graph point:", x, y, xx, yy)
self.draw_point(xx, max(0 + size, yy), size, color)
def draw_point(self, x, y, size=PROFILE_SIZE, color=1):
"""Draw data point on to the plot bitmap at (x,y)."""
if y is None:
return
offset = size // 2
for xx in range(x - offset, x + offset + 1):
if xx in range(self.xstart, self.xstart + self.width):
for yy in range(y - offset, y + offset + 1):
if yy in range(self.ystart, self.ystart + self.height):
try:
yy = GHEIGHT - yy
plot[xx, yy] = color
except IndexError:
pass
def draw_profile(graph, profile):
global label_reflow
"""Update the display with current info."""
for i in range(GWIDTH * GHEIGHT):
plot[i] = 0
# draw stage lines
# preheat
graph.draw_line(
profile["stages"]["preheat"][0],
profile["temp_range"][0],
profile["stages"]["preheat"][0],
profile["temp_range"][1] * 1.1,
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
graph.draw_line(
profile["time_range"][0],
profile["stages"]["preheat"][1],
profile["time_range"][1],
profile["stages"]["preheat"][1],
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
# soak
graph.draw_line(
profile["stages"]["soak"][0],
profile["temp_range"][0],
profile["stages"]["soak"][0],
profile["temp_range"][1] * 1.1,
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
graph.draw_line(
profile["time_range"][0],
profile["stages"]["soak"][1],
profile["time_range"][1],
profile["stages"]["soak"][1],
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
# reflow
graph.draw_line(
profile["stages"]["reflow"][0],
profile["temp_range"][0],
profile["stages"]["reflow"][0],
profile["temp_range"][1] * 1.1,
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
graph.draw_line(
profile["time_range"][0],
profile["stages"]["reflow"][1],
profile["time_range"][1],
profile["stages"]["reflow"][1],
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
# cool
graph.draw_line(
profile["stages"]["cool"][0],
profile["temp_range"][0],
profile["stages"]["cool"][0],
profile["temp_range"][1] * 1.1,
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
graph.draw_line(
profile["time_range"][0],
profile["stages"]["cool"][1],
profile["time_range"][1],
profile["stages"]["cool"][1],
GRID_SIZE,
GRID_COLOR,
GRID_STYLE,
)
# draw labels
x = profile["time_range"][0]
y = profile["stages"]["reflow"][1]
xp = int(GXSTART + graph.width * (x - graph.xmin) // (graph.xmax - graph.xmin))
yp = int(GHEIGHT * (y - graph.ymin) // (graph.ymax - graph.ymin))
label_reflow.x = xp + 10
label_reflow.y = HEIGHT - yp
label_reflow.text = str(profile["stages"]["reflow"][1])
print("reflow temp:", str(profile["stages"]["reflow"][1]))
print("graph point: ", x, y, "->", xp, yp)
x = profile["stages"]["reflow"][0]
y = profile["stages"]["reflow"][1]
# draw time line (horizontal)
graph.draw_line(
graph.xmin, graph.ymin + 1, graph.xmax, graph.ymin + 1, AXIS_SIZE, AXIS_COLOR, 1
)
graph.draw_line(
graph.xmin, graph.ymax, graph.xmax, graph.ymax, AXIS_SIZE, AXIS_COLOR, 1
)
# draw time ticks
tick = graph.xmin
while tick < (graph.xmax - graph.xmin):
graph.draw_line(
tick, graph.ymin, tick, graph.ymin + 10, AXIS_SIZE, AXIS_COLOR, 1
)
graph.draw_line(
tick,
graph.ymax,
tick,
graph.ymax - 10 - AXIS_SIZE,
AXIS_SIZE,
AXIS_COLOR,
1,
)
tick += 60
# draw temperature line (vertical)
graph.draw_line(
graph.xmin, graph.ymin, graph.xmin, graph.ymax, AXIS_SIZE, AXIS_COLOR, 1
)
graph.draw_line(
graph.xmax - AXIS_SIZE + 1,
graph.ymin,
graph.xmax - AXIS_SIZE + 1,
graph.ymax,
AXIS_SIZE,
AXIS_COLOR,
1,
)
# draw temperature ticks
tick = graph.ymin
while tick < (graph.ymax - graph.ymin) * 1.1:
graph.draw_line(
graph.xmin, tick, graph.xmin + 10, tick, AXIS_SIZE, AXIS_COLOR, 1
)
graph.draw_line(
graph.xmax,
tick,
graph.xmax - 10 - AXIS_SIZE,
tick,
AXIS_SIZE,
AXIS_COLOR,
1,
)
tick += 50
# draw profile
x1 = profile["profile"][0][0]
y1 = profile["profile"][0][1]
for point in profile["profile"]:
x2 = point[0]
y2 = point[1]
graph.draw_line(x1, y1, x2, y2, PROFILE_SIZE, PROFILE_COLOR, 1)
# print(point)
x1 = x2
y1 = y2
def format_time(seconds):
minutes = seconds // 60
seconds = int(seconds) % 60
return "{:02d}:{:02d}".format(minutes, seconds, width=2)
def check_buttons_press_location(p, button_details):
"""
Function to easily check a button press within a list of Buttons
"""
for button in button_details:
if button.contains(p):
return button
return None
def change_profile(oven):
"""
Function added to render the available profile selections to screen and then load into memory
Limitations: Only the first 6 profiles will be displayed to honor to the size format of the screen
"""
selected_file = None
display_group = displayio.Group()
gc.collect()
title_label = label.Label(font3, text=TITLE)
title_label.x = 5
title_label.y = 14
display_group.append(title_label)
profile_label = label.Label(font2, text="Profile Change")
profile_label.x = 5
profile_label.y = 45
display_group.append(profile_label)
selected_label_default_text = "Selected Profile: "
selected_label = label.Label(font1, text=selected_label_default_text)
selected_label.x = 5
selected_label.y = HEIGHT - 20
display_group.append(selected_label)
buttons = []
button_details = {}
button_x = 20
button_y = 60
button_y_start = 60
button_height = 30
button_width = 120
spacing = 10
profile_limit = 6
count = 0
dir_list = os.listdir("/profiles/")
for f in dir_list:
f = f.split('.')[0]
button = Button(
x=button_x, y=button_y, width=button_width, height=button_height, label=f, label_font=font1
)
button_details[f] = [button_x, button_y, button_width, button_height]
buttons.append(button)
button_y += button_height + spacing
count+=1
if count == 3:
button_x += button_width + spacing
button_y = button_y_start
if count >= 6:
break
save_button = Button(x=WIDTH-70, y=HEIGHT-50, width=60, height=40, label="Save", label_font=font2)
button_details["Save"] = [WIDTH-70, HEIGHT-50, 60, 40]
buttons.append(save_button)
for b in buttons:
display_group.append(b)
board.DISPLAY.root_group = display_group
try:
board.DISPLAY.refresh(target_frames_per_second=60)
except AttributeError:
board.DISPLAY.refresh_soon()
print("Profile change display complete")
while True:
gc.collect()
try:
board.DISPLAY.refresh(target_frames_per_second=60)
except AttributeError:
board.DISPLAY.refresh_soon()
p = ts.touch_point
if p:
button_pressed = check_buttons_press_location(p, buttons)
if button_pressed:
print(f"{button_pressed.label} button pressed")
if button_pressed.label == "Save":
with open("/profiles/" + selected_file + ".json", mode="r") as fpr:
oven.sprofile = json.load(fpr)
fpr.close()
oven.reset()
return
else:
selected_file = button_pressed.label
print(f"Profile selected: {selected_file}")
selected_label.text = selected_label_default_text + " " + button_pressed.label
time.sleep(1) # for debounce
def default_view():
"""
The below code was wrapped into this fucntion to give execution back and forth between this the default view
of the EZ Make Oven and the alternative profile selection view.
As such there were numerous global variables that have been declared as needed in the various other functions
that use them.
"""
global label_reflow, oven, message, timediff, plot, sgraph, timer_data
display_group = displayio.Group()
board.DISPLAY.root_group = display_group
plot = displayio.Bitmap(GWIDTH, GHEIGHT, 4)
display_group.append(
displayio.TileGrid(plot, pixel_shader=palette, x=GXSTART, y=GYSTART)
)
timediff = 0
print("melting point: ", oven.sprofile["melting_point"])
label_reflow = label.Label(font1, text="", color=0xFFFFFF, line_spacing=0)
label_reflow.x = 0
label_reflow.y = -20
display_group.append(label_reflow)
title_label = label.Label(font3, text=TITLE)
title_label.x = 5
title_label.y = 14
display_group.append(title_label)
# version_label = label.Label(font1, text=VERSION, color=0xAAAAAA)
# version_label.x = 300
# version_label.y = 40
# display_group.append(version_label)
message = label.Label(font2, text="Wait")
message.x = 100
message.y = 40
display_group.append(message)
alloy_label = label.Label(font1, text="Alloy:", color=0xAAAAAA)
alloy_label.x = 5
alloy_label.y = 40
display_group.append(alloy_label)
alloy_data = label.Label(font1, text=str(oven.sprofile["alloy"]))
alloy_data.x = 10
alloy_data.y = 60
display_group.append(alloy_data)
profile_label = label.Label(font1, text="Profile:", color=0xAAAAAA)
profile_label.x = 5
profile_label.y = 80
display_group.append(profile_label)
profile_data = label.Label(font1, text=oven.sprofile["title"])
profile_data.x = 10
profile_data.y = 100
display_group.append(profile_data)
timer_label = label.Label(font1, text="Time:", color=0xAAAAAA)
timer_label.x = 5
timer_label.y = 120
display_group.append(timer_label)
timer_data = label.Label(font3, text=format_time(timediff))
timer_data.x = 10
timer_data.y = 140
display_group.append(timer_data)
temp_label = label.Label(font1, text="Temp(C):", color=0xAAAAAA)
temp_label.x = 5
temp_label.y = 160
display_group.append(temp_label)
temp_data = label.Label(font3, text="--")
temp_data.x = 10
temp_data.y = 180
display_group.append(temp_data)
circle = Circle(308, 12, 8, fill=0)
display_group.append(circle)
sgraph = Graph()
# sgraph.xstart = 100
# sgraph.ystart = 4
sgraph.xstart = 0
sgraph.ystart = 0
# sgraph.width = WIDTH - sgraph.xstart - 4 # 216 for standard PyPortal
# sgraph.height = HEIGHT - 80 # 160 for standard PyPortal
sgraph.width = GWIDTH # 216 for standard PyPortal
sgraph.height = GHEIGHT # 160 for standard PyPortal
sgraph.xmin = oven.sprofile["time_range"][0]
sgraph.xmax = oven.sprofile["time_range"][1]
sgraph.ymin = oven.sprofile["temp_range"][0]
sgraph.ymax = oven.sprofile["temp_range"][1] * 1.1
print("x range:", sgraph.xmin, sgraph.xmax)
print("y range:", sgraph.ymin, sgraph.ymax)
draw_profile(sgraph, oven.sprofile)
buttons = []
if oven.sensor_status:
button = Button(
x=0, y=HEIGHT - 40, width=80, height=40, label="Start", label_font=font2
)
buttons.append(button)
profile_button = Button(
x=WIDTH - 100, y=40, width=100, height=30, label="Profile Change", label_font=font1
)
buttons.append(profile_button)
for b in buttons:
display_group.append(b)
try:
board.DISPLAY.refresh(target_frames_per_second=60)
except AttributeError:
board.DISPLAY.refresh_soon()
print("display complete")
last_temp = 0
last_state = "ready"
last_control = False
second_timer = time.monotonic()
timer = time.monotonic()
display_profile_button = True
is_Running = True
while is_Running:
gc.collect()
try:
board.DISPLAY.refresh(target_frames_per_second=60)
except AttributeError:
board.DISPLAY.refresh_soon()
oven.beep.refresh() # this allows beeps less than one second in length
try:
oven_temp = int(oven.sensor.temperature)
except AttributeError:
oven_temp = 32 # testing
oven.sensor_status = False
message.text = "Bad/missing temp sensor"
if oven.control != last_control:
last_control = oven.control
if oven.control:
circle.fill = 0xFF0000
else:
circle.fill = 0x0
p = ts.touch_point
status = ""
last_status = ""
if p:
if button.contains(p):
print("touch!")
if oven.state == "ready":
button.label = "Stop"
oven.set_state("start")
else:
# cancel operation
message.text = "Wait"
button.label = "Wait"
oven.set_state("wait")
time.sleep(1) # for debounce
elif profile_button.contains(p):
if message.text == "Ready": # only allow profile change when NOT running
print("Profile change button pressed")
is_Running = False
change_profile(oven)
if oven.sensor_status:
if oven.state == "ready":
status = "Ready"
display_profile_button = True
if last_state != "ready":
oven.beep.refresh()
oven.reset()
draw_profile(sgraph, oven.sprofile)
timer_data.text = format_time(0)
if button.label != "Start":
button.label = "Start"
if oven.state == "start":
status = "Starting"
display_profile_button = False
if last_state != "start":
timer = time.monotonic()
if oven.state == "preheat":
if last_state != "preheat":
timer = time.monotonic() # reset timer when preheat starts
status = "Preheat"
display_profile_button = False
if oven.state == "soak":
status = "Soak"
display_profile_button = False
if oven.state == "reflow":
status = "Reflow"
display_profile_button = False
if oven.state == "cool" or oven.state == "wait":
status = "Cool Down, Open Door"
if last_status != status:
message.text = status
last_status = status
if oven_temp != last_temp and oven.sensor_status:
last_temp = oven_temp
temp_data.text = str(oven_temp)
# update once per second when oven is active
if oven.state != "ready" and time.monotonic() - second_timer >= 1.0:
second_timer = time.monotonic()
oven.check_state()
if oven.state == "preheat" and last_state != "preheat":
timer = time.monotonic() # reset timer at start of preheat
timediff = int(time.monotonic() - timer)
timer_data.text = format_time(timediff)
print(oven.state)
if oven_temp >= 50:
sgraph.draw_graph_point(
int(timediff), oven_temp, size=TEMP_SIZE, color=TEMP_COLOR
)
last_state = oven.state
# Manage whether the Profile Button should be displayed or not
if display_profile_button == True:
try:
display_group.append(profile_button)
except ValueError:
# profile_button already in the display_group
pass
else:
try:
display_group.remove(profile_button)
except ValueError:
# profile_button is missing, handle gracefully
pass
# Global variables that are used in numerous of the supporting functions
font1 = bitmap_font.load_font("/fonts/OpenSans-9.bdf")
font2 = bitmap_font.load_font("/fonts/OpenSans-12.bdf")
font3 = bitmap_font.load_font("/fonts/OpenSans-16.bdf")
label_reflow = None
oven = ReflowOvenControl(board.D4)
message = None
timediff = 0
plot = None
sgraph = None
timer_data = None
# Essentially the main function of the entire codebase
while True:
default_view()
Page last edited January 22, 2025
Text editor powered by tinymce.