To get set up, we will need CircuitPython, a few libraries and the PyPortal Calculator Source Code and font downloaded from Github.
CircuitPython
First, make sure you are running the latest version of Adafruit CircuitPython for your board. Because of the speed-ups that were added, the PyPortal Calculator requires at least CircuitPython 4.1.0.
To download the CircuitPython beta, visit the following link for the PyPortal and download the UF2 for CircuitPython beta. You must be using CircuitPython 4.1.0 or later for PyPortal Calculator to work fast enough!
PyPortal Calculator Source Code
Download all the needed files from GitHub by clicking the Download Project Bundle button below.
# SPDX-FileCopyrightText: 2019 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
PyPortal Calculator Demo
"""
import time
from collections import namedtuple
import board
import displayio
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font
from adafruit_display_shapes.rect import Rect
from adafruit_button import Button
from calculator import Calculator
import adafruit_touchscreen
Coords = namedtuple("Point", "x y")
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
board.TOUCH_YD, board.TOUCH_YU,
calibration=((5200, 59000), (5800, 57000)),
size=(320, 240))
# Settings
BUTTON_WIDTH = 60
BUTTON_HEIGHT = 30
BUTTON_MARGIN = 8
MAX_DIGITS = 29
BLACK = 0x0
ORANGE = 0xFF8800
WHITE = 0xFFFFFF
GRAY = 0x888888
LABEL_OFFSET = 290
# Make the display context
calc_group = displayio.Group()
board.DISPLAY.root_group = calc_group
# Make a background color fill
color_bitmap = displayio.Bitmap(320, 240, 1)
color_palette = displayio.Palette(1)
color_palette[0] = GRAY
bg_sprite = displayio.TileGrid(color_bitmap,
pixel_shader=color_palette,
x=0, y=0)
calc_group.append(bg_sprite)
# Load the font
font = bitmap_font.load_font("/fonts/Arial-12.bdf")
buttons = []
# Some button functions
def button_grid(row, col):
return Coords(BUTTON_MARGIN * (row + 1) + BUTTON_WIDTH * row + 20,
BUTTON_MARGIN * (col + 1) + BUTTON_HEIGHT * col + 40)
def add_button(row, col, label, width=1, color=WHITE, text_color=BLACK):
pos = button_grid(row, col)
new_button = Button(x=pos.x, y=pos.y,
width=BUTTON_WIDTH * width + BUTTON_MARGIN * (width - 1),
height=BUTTON_HEIGHT, label=label, label_font=font,
label_color=text_color, fill_color=color, style=Button.ROUNDRECT)
buttons.append(new_button)
return new_button
def find_button(label):
result = None
for _, btn in enumerate(buttons):
if btn.label == label:
result = btn
return result
border = Rect(20, 8, 280, 35, fill=WHITE, outline=BLACK, stroke=2)
calc_display = Label(font, text="0", color=BLACK)
calc_display.y = 25
clear_button = add_button(0, 0, "AC")
add_button(1, 0, "+/-")
add_button(2, 0, "%")
add_button(3, 0, "/", 1, ORANGE, WHITE)
add_button(0, 1, "7")
add_button(1, 1, "8")
add_button(2, 1, "9")
add_button(3, 1, "x", 1, ORANGE, WHITE)
add_button(0, 2, "4")
add_button(1, 2, "5")
add_button(2, 2, "6")
add_button(3, 2, "-", 1, ORANGE, WHITE)
add_button(0, 3, "1")
add_button(1, 3, "2")
add_button(2, 3, "3")
add_button(3, 3, "+", 1, ORANGE, WHITE)
add_button(0, 4, "0", 2)
add_button(2, 4, ".")
add_button(3, 4, "=", 1, ORANGE, WHITE)
# Add the display and buttons to the main calc group
calc_group.append(border)
calc_group.append(calc_display)
for b in buttons:
calc_group.append(b)
calculator = Calculator(calc_display, clear_button, LABEL_OFFSET)
button = ""
while True:
point = ts.touch_point
if point is not None:
# Button Down Events
for _, b in enumerate(buttons):
if b.contains(point) and button == "":
b.selected = True
button = b.label
elif button != "":
# Button Up Events
last_op = calculator.get_current_operator()
op_button = find_button(last_op)
# Deselect the last operation when certain buttons are pressed
if op_button is not None:
if button in ('=', 'AC', 'CE'):
op_button.selected = False
elif button in ('+', '-', 'x', '/') and button != last_op:
op_button.selected = False
calculator.add_input(button)
b = find_button(button)
if b is not None:
if button not in ('+', '-', 'x', '/') or button != calculator.get_current_operator():
b.selected = False
button = ""
time.sleep(0.05)
All of the files should go onto your PyPortal main CIRCUITPY drive in the directories noted.
Here is the CircuitPython calculator class in its entirety. If you click on Download Project Bundle button, it will download all files used in this tutorial also.
# SPDX-FileCopyrightText: 2019 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
CircuitPython library to handle the input and calculations
* Author(s): Melissa LeBlanc-Williams
"""
# pylint: disable=eval-used
def calculate(number_one, operator, number_two):
result = eval(number_one + operator + number_two)
if int(result) == result:
result = int(result)
return str(result)
class Calculator:
def __init__(self, calc_display, clear_button, label_offset):
self._error = False
self._calc_display = calc_display
self._clear_button = clear_button
self._label_offset = label_offset
self._accumulator = "0"
self._operator = None
self._equal_pressed = False
self._operand = None
self._all_clear()
def get_current_operator(self):
operator = self._operator
if operator == "*":
operator = "x"
return operator
def _all_clear(self):
self._accumulator = "0"
self._operator = None
self._equal_pressed = False
self._clear_entry()
def _clear_entry(self):
self._operand = None
self._error = False
self._set_button_ce(False)
self._set_text("0")
def _set_button_ce(self, entry_only):
self._clear_button.selected = False
if entry_only:
self._clear_button.label = "CE"
else:
self._clear_button.label = "AC"
def _set_text(self, text):
self._calc_display.text = text
_, _, screen_w, _ = self._calc_display.bounding_box
self._calc_display.x = self._label_offset - screen_w
def _get_text(self):
return self._calc_display.text
def _handle_number(self, input_key):
display_text = self._get_text()
if self._operand is None and self._operator is not None:
display_text = ""
elif self._operand is not None and self._operator is not None and self._equal_pressed:
self._accumulator = self._operand
self._operator = None
self._operand = None
display_text = ""
elif display_text == "0":
display_text = ""
display_text += input_key
self._set_text(display_text)
if self._operator is not None:
self._operand = display_text
self._set_button_ce(True)
self._equal_pressed = False
def _handle_operator(self, input_key):
if input_key == "x":
input_key = "*"
if self._equal_pressed:
self._operand = None
if self._operator is None:
self._operator = input_key
else:
# Perform current calculation before changing input_keys
if self._operand is not None:
self._accumulator = calculate(self._accumulator, self._operator, self._operand)
self._set_text(self._accumulator)
self._operand = None
self._operator = input_key
self._accumulator = self._get_text()
self._equal_pressed = False
def _handle_equal(self):
if self._operator is not None:
if self._operand is None:
self._operand = self._get_text()
self._accumulator = calculate(self._accumulator, self._operator, self._operand)
self._set_text(self._accumulator)
self._equal_pressed = True
def _update_operand(self):
if self._operand is not None:
self._operand = self._get_text()
def add_input(self, input_key):
try:
if self._error:
self._clear_entry()
elif input_key == "AC":
self._all_clear()
elif input_key == "CE":
self._clear_entry()
elif self._operator is None and input_key == "0":
pass
elif len(input_key) == 1 and 48 <= ord(input_key) <= 57:
self._handle_number(input_key)
elif input_key in ('+', '-', '/', 'x'):
self._handle_operator(input_key)
elif input_key == ".":
if not input_key in self._get_text():
self._set_text(self._get_text() + input_key)
self._set_button_ce(True)
self._equal_pressed = False
elif input_key == "+/-":
self._set_text(calculate(self._get_text(), "*", "-1"))
self._update_operand()
elif input_key == "%":
self._set_text(calculate(self._get_text(), "/", "100"))
self._update_operand()
elif input_key == "=":
self._handle_equal()
except (ZeroDivisionError, RuntimeError):
self._all_clear()
self._error = True
self._set_text("Error")
After copying everything over, your PyPortal should display a Calculator on it. In the next few sections, we'll go over the User Interface Elements and examine the code more closely.
Required CircuitPython Libraries
All the needed libraries should be available in the Project Bundle.
Here are the libraries used:
- adafruit_bitmap_font
- adafruit_display_shapes
- adafruit_display_text
- adafruit_button
- adafruit_touchscreen
Before continuing make sure your board's lib folder or root filesystem have the adafruit_bitmap_font, adafruit_display_shapes, adafruit_display_text, adafruit_button, and adafruit_touchscreen files and folders copied over.
Page last edited January 21, 2025
Text editor powered by tinymce.