Initial Setup

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!

Required CircuitPython Libraries

Next, you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle.  Our introduction guide has a great page on how to install the library bundle for both express and non-express boards.

Since the full set of libraries take up a good deal of space, we recommend that you manually install only the necessary libraries from the bundle:

  • 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_fontadafruit_display_shapesadafruit_display_text, adafruit_button, and adafruit_touchscreen files and folders copied over.

PyPortal Calculator Source Code

Next, we'll continue by downloading the Source Code from Github.

All of the files should go in a subdirectory of your PyPortal's main CIRCUITPY.

Here is the main CircuitPython code in its entirety. If you click on Download Project Zip, it will download all files used in this tutorial also.

"""
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(max_size=25)
board.DISPLAY.show(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, max_glyphs=MAX_DIGITS)
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.group)

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)

Here is the CircuitPython calculator class in its entirety. If you click on Download Project Zip, it will download all files used in this tutorial also.

"""
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.

This guide was first published on Jul 13, 2019. It was last updated on Jul 13, 2019. This page (Initial Setup) was last updated on Oct 05, 2019.