The CG-35 is a CircuitPython emulation of the Hewlett Packard HP-35 Scientific Reverse-Polish Notation (RPN) calculator designed for the Adafruit ESP32-S3 Feather and 3.5-inch TFT FeatherWing capacitive touch display. The calculator consists of a 10-digit LED-like display backed-up with 20-digit internal calculation precision.
This emulation reproduces the HP-35 calculator's v2.0 firmware where the change sign (CHS) key is active only after digit entry has begun. And because of the udecimal and utrig classes, calculation accuracy of monadic, dyadic, and trigonometric functions was improved. As an added bonus not present on the original calculator, a status message will appear just below the primary display when a calculation error is encountered.
The calculator's graphical layout was designed to mimic the aspect ratio of the original calculator -- that's why the left and right sides of the display screen were left empty. However, to provide a more reliable touch screen experience, the keys are somewhat proportionally larger than the original.
This project was inspired by Jeff Epler's DIY Desktop Calculator with CircuitPython project and Jeff's work to create CircuitPython versions of udecimal and utrig. Thank you Jeff!
GitHub Repository: https://github.com/CedarGroveStudios/CG-35_Calculator
Implementation Notes
Hardware:
- Adafruit ESP32-S3 4Mb/2Mb Feather or UnexpectedMaker FeatherS2
- Adafruit TFT Display FeatherWing - 3.5" 480x320 Capacitive Touchscreen
- Piezo Speaker (optional)
Software:
- Jeff Epler's adaptation of MicroPython udecimal and utrig
- CedarGrove SevenSeg-12.bdf font
Future Feature Wish List
- Implement display brightness control.
- Adjust for automatic scientific notation conversion for entered values less than |1| with a negative exponent greater than -6. This is an inherent behavior of the Decimal class in MicroPython, CircuitPython, and CPython.
- Purge the T-register when calculating trigonometric functions to fully emulate the HP-35 calculation process.
- Incorporate a selectable degrees/radians mode with display indicator. (Enhancement)
- Incorporate a selectable scientific/engineering/fixed decimal point mode. (Enhancement)
- Add a setup button for setting initial parameters. (Enhancement)
CircuitPython Code
The Bundle
The bundle folder of the CG-35_Calculator repository contains all the necessary code, libraries, and fonts needed for the calculator.
Download the contents of the bundle folder and place the files into the root directory of the ESP32-S3 Feather.
The ESP32-S3 Feather's root directory should now contain the following folders and files:
- cg_35_calculator.py
- code.py
- settings.toml
fonts folder:
- OpenSans-9.bdf
- SevenSeg-12.bdf
lib (library) folder containing these required libraries:
- adafruit_bitmap_font
- adafruit_bus_device
- adafruit_button
- adafruit_display_shapes
- adafruit_display_text
- adafruit_ft5336
- adafruit_hx835
- adafruit_register
- jepler_udecimal
- simpleio
Primary Code Module
The primary code module cg_35_calculator.py, imported by code.py, instantiates the display, plots the calculator case and buttons, and implements all calculator operational processes. This module uses a state machine design with the following named states:
- IDLE -- Display results or wait for input
-
C_ENTRY -- Coefficient entry (keys:
0-9,.,CHS,EEX) -
E_ENTRY -- Exponent entry (keys:
0-9,.,CHS) -
STACK -- Stack management (keys:
ENTER,CLR,CLX,STO,RCL,R,x<>y,π) -
MONADIC -- Monadic calculator functions (keys:
LOG,LN,e^x,√x,ARC,SIN,COS,TAN,1/x) -
DYADIC -- Dyadic calculator functions (keys:
x^y,-,+,*,÷) - ERROR -- Calculation error
The calculator's display precision and internal calculation precision are specified using the variables DISPLAY_PRECISION and INTERNAL_PRECISION. Although the internal precision exceeds that of the original HP-35 calculator, it is recommended to keep the existing default settings of 10 digits and 20 digits, respectively, to avoid rounding errors.
The variable DEBUG can be use to provide additional internal register status via the REPL. This boolean variable defaults to False (no additional register status).
# SPDX-FileCopyrightText: 2022, 2024 Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
cg_35_calculator.py 2024-04-16 v2.2
For the ESP32-S3 4Mb/2Mb Feather and 3.5-inch TFT Capacitive FeatherWing
====================================
An HP-35-like RPN calculator application for the Adafruit ESP32-S3 Feather
and 3.5-inch TFT FeatherWing display with capacitive touch. The calculator
consists of a 10-digit LED display with 20-digit internal calculation
precision.
This application emulates the HP-35 calculator's v2.0 firmware where the change
sign (CHS) key is active only after digit entry has begun. Calculation accuracy
of monadic, dyadic, and trigonometric functions was improved. An error descriptor
message area just below the primary display was added.
The calculator's graphical layout was designed to mimic the aspect ratio of the
original calculator. Because of the relative small size of the buttons,
touchscreen accuracy is important, requiring the use of a capacitive touch
screen rather than a resistive touch screen.
For audible key press and status feedback, connect a piezo speaker from
pin A0 to ground.
* Author(s): JG for Cedar Grove Maker Studios
* GitHub: <https://github.com/CedarGroveStudios/CG-35_Calculator>
Implementation Notes
--------------------
**Hardware:**
* Adafruit 'ESP32-S3 4Mb/2Mb FeatherWing
<https://www.adafruit.com/product/5477>
* Adafruit 'Adafruit TFT FeatherWing - 3.5" 480x320 Capacitive Touchscreen
<https://www.adafruit.com/product/5872>
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the ESP32-S3 FeatherWing:
<https://circuitpython.org/downloads>
* Jeff Epler's adaptation of micropython `udecimal` and `utrig`:
<https://github.com/jepler/Jepler_CircuitPython_udecimal>
* CedarGrove's `SevenSeg-12.bdf` font:
<https://github.com/CedarGroveStudios/SevenSeg_font>
Optional/future features:
- Implement display brightness control.
- Adjust for automatic scientific notation conversion for entered
values < |1| with a negative exponent > -6. This is an inherent behavior
of the Decimal class in micropython, CircuitPython, and CPython.
- Purge the T register when calculating trigonometric functions to fully
emulate the HP-35 process.
- Incorporate a selectable degrees/radians mode with indicator
(enhancement).
- Incorporate a selectable scientific/engineering/fixed decimal point mode
(enhancement).
- Add a setup button for setting initial parameters (enhancement).
"""
import board
import displayio
import time
import gc
import adafruit_ft5336
import adafruit_hx8357
from simpleio import tone
# import digitalio
from cedargrove_calculator.buttons import CalculatorButtons
from cedargrove_calculator.case import CalculatorCase, LEDDisplay, Colors
from jepler_udecimal import Decimal, getcontext, setcontext, localcontext, ROUND_HALF_UP
import jepler_udecimal.utrig # Needed for trig functions in Decimal
# User-modifiable parameters
DISPLAY_PRECISION = 10
INTERNAL_PRECISION = 20
DEBUG = False # Turns on debug print ('printd()')function
# Calculator states
IDLE = "IDLE" # Waiting for input or displaying results
C_ENTRY = "C_ENTRY" # Coefficient entry: 0-9, ., CHS, EEX
E_ENTRY = "E_ENTRY" # Exponent entry: 0-9, ., CHS
STACK = "STACK" # Stack management: ENTER, CLR, CLX, STO, RCL, R, x<>y, π
MONADIC = "MONADIC" # Monadic operation: LOG, LN, e^x, √x, ARC, SIN, COS, ,TAN, 1/x
DYADIC = "DYADIC" # Dyadic operation: x^y, -, +, *, ÷
ERROR = "ERROR" # Calculation error
STATE = IDLE # Set initial state to IDLE
# Constants
PI = Decimal(1).atan() * 4 # Alternative: PI = Decimal("3.141592654")
t0 = time.monotonic() # Reset start-up time counter
gc.collect() # Clean-up memory heap space
# Create the primary displayio.Group layer
calculator = displayio.Group()
# 'TFT FeatherWing - 3.5" 480x320 Touchscreen'; portrait orientation
displayio.release_displays() # Release display resources
tft_bus = displayio.FourWire(
board.SPI(), command=board.D10, chip_select=board.D9, reset=None
)
tft = adafruit_hx8357.HX8357(tft_bus, width=480, height=320)
tft.rotation = 270
# Capacitive touch panel
cts = adafruit_ft5336.Adafruit_FT5336(board.I2C())
# tft.brightness = 1 # 0.55 for camera image
# Instantiate case group and buttons class
case_group = CalculatorCase(display=tft)
buttons = CalculatorButtons(
l_margin=case_group.l_margin, timeout=10, click=True, display=tft, touch=cts
)
led_display = LEDDisplay(scale=1, display=tft)
gc.collect() # Clean-up memory heap space
# Set the default internal precision and exponent range
getcontext().prec = INTERNAL_PRECISION
getcontext().Emax = 99
getcontext().Emin = -99
# getcontext().rounding = ROUND_HALF_UP
# Initiate the display, memory, and stack
DISPLAY_C = " 0."
DISPLAY_E = " 00"
X_REG = Y_REG = Z_REG = T_REG = MEM = Decimal("0")
# Add the case, bubble display, and button displayio layers
calculator.append(case_group)
calculator.append(led_display)
calculator.append(buttons)
def printd(line):
"""Debug print function. Use formatted print statements."""
if DEBUG:
print(line)
return
def clr(register=None):
"""Clear the display and all or just X_REG; None for all registers; "x" for X_REG."""
global DISPLAY_C, DISPLAY_E, X_REG, Y_REG, Z_REG, T_REG, MEM, ARC_FLAG
if not register:
# Clear display, stack registers, memory, and reset ARC flag
DISPLAY_C = " 0."
DISPLAY_E = " 00"
X_REG = Y_REG = Z_REG = T_REG = MEM = Decimal("0")
ARC_FLAG = False
elif register == "x":
# Clear display and X_REG
DISPLAY_C = " 0."
DISPLAY_E = " 00"
X_REG = Decimal("0")
else:
return False # No register specified
return True
def get_key():
"""Get pressed key name. This is a blocking method (for now)."""
key_name = None
while not key_name:
key_name, _, hold_time = buttons.read_buttons()
printd(f"get_key: name:{key_name:5s} hold_time:{hold_time:5.3f}s")
return key_name
def push_stack():
"""Push stack values; T_REG is lost."""
global X_REG, Y_REG, Z_REG, T_REG
T_REG = Z_REG
Z_REG = Y_REG
Y_REG = X_REG
return
def pull_stack():
"""Pull (drop) stack values; place "0" into T_REG."""
global X_REG, Y_REG, Z_REG, T_REG
X_REG = Y_REG
Y_REG = Z_REG
Z_REG = T_REG
T_REG = Decimal("0")
return
def roll_stack():
"""Roll stack values; place X_REG into T_REG."""
global X_REG, Y_REG, Z_REG, T_REG
temp = X_REG
X_REG = Y_REG
Y_REG = Z_REG
Z_REG = T_REG
T_REG = temp
return
def convert_display_to_decimal(coefficient=" 0.", exponent=" "):
"""Convert display text to an equivalent Decimal value."""
printd(f"display_to_decimal input: '{coefficient}' '{exponent}'")
getcontext().prec = DISPLAY_PRECISION
sign = coefficient[0]
coefficient = coefficient[1:12]
exponent = exponent[0:3]
# Reformat sign for decimal value conversion
if sign == " ":
sign = "+"
# Clean up and reformat coefficient; remove spaces, put zero before dp if dp is in first position
while coefficient.find(" ") >= 0:
coefficient = coefficient.split(" ", 1)[0] + coefficient.split(" ", 1)[1]
if coefficient[0] == ".":
coefficient = "0" + coefficient
# Pad coefficient with trailing zeros for values with positive exponent
for i in range(len(coefficient), getcontext().prec + 1):
coefficient = coefficient + "0"
# Clean up and reformat exponent; remove spaces, set to zero if blank, add separator E character
while exponent.find(" ") >= 0:
exponent = exponent.split(" ", 1)[0] + exponent.split(" ", 1)[1]
if exponent == "":
exponent = "0"
exponent = "E" + str(int(exponent))
# Reconstruct the value as Decimal type
new_value = Decimal(sign + coefficient + exponent)
# Set display precision and return value
getcontext().prec = INTERNAL_PRECISION
new_value = new_value / 1
printd(f"display_to_decimal: new_value out: '{new_value}'")
return new_value
def convert_decimal_to_display(value=Decimal("0")):
"""Convert a Decimal value into the equivalent display text."""
# Round to display precision and convert to string
printd(f"decimal_to_display value input: '{value}' type: {type(value)}")
if value.is_finite():
value = value / 1
getcontext().prec = DISPLAY_PRECISION
value = value / 1
else:
value = Decimal(0)
decimal_text = str(value)
# Separate coefficient from exponent
if decimal_text.find("E") >= 0:
coefficient, exponent = decimal_text.split("E", 1)
else:
coefficient = decimal_text
exponent = " 00"
# Coefficient: Retain "-" as sign; add " " for positive value
if coefficient[0] != "-":
coefficient = " " + coefficient
# Exponent: Replace + with space; retain minus sign
if exponent[0] == "+": # if plus sign, replace with space
exponent = (" " * (4 - len(exponent))) + exponent[1:]
if exponent[0] == "-" and len(exponent) == 2:
exponent = "- " + exponent[-1]
# If no decimal point in coefficient, add one to the end
if coefficient.find(".") < 0 and len(coefficient) < 12:
coefficient = coefficient + "."
# Remove trailing zeros from coefficient
while coefficient.find(".") <= 12 and coefficient[-1] == "0":
coefficient = coefficient[0:-1]
# Remove leading zeros except at start of digit entry
if coefficient[1:] != "0.":
while coefficient.find(".") > 1 and coefficient[1] == "0":
coefficient = coefficient[0] + coefficient[2:]
# Don't display a minus zero coefficient
if coefficient == "-0.":
coefficient = " 0."
# If no exponent separator or coefficient is zero, blank the exponent value
if decimal_text.find("E") < 0 or coefficient[1:] == "0.":
exponent = " "
getcontext().prec = INTERNAL_PRECISION
printd(f"**** coefficient '{coefficient}', exponent '{exponent}'")
printd(f" len(coefficient):{len(coefficient)}, len(exponent):{len(exponent)}")
return coefficient, exponent
def print_stack():
"""Print stack registers and auxiliary memory in REPL."""
print(f"-" * 48)
print(
f"D.X_REG: {X_REG} DISPLAY: '{convert_decimal_to_display(X_REG)[0] + convert_decimal_to_display(X_REG)[1]}'"
)
print(f"D.Y_REG: {Y_REG}")
print(f"D.Z_REG: {Z_REG}")
print(f"D.T_REG: {T_REG}")
print(f"D.MEM : {MEM}")
print(f"-" * 48)
return
def show_display_reg():
"""Display contents of DISPLAY registers."""
global DISPLAY_C, DISPLAY_E
coefficient = DISPLAY_C
exponent = DISPLAY_E
if exponent[1:3] == "00":
exponent = " "
# If needed, pad coefficient with spaces
coefficient = coefficient + (" " * (12 - len(coefficient)))
led_display.text = coefficient + exponent
return
def display_error(text=""):
"""Flash error indicator on display."""
tone(board.A0, 440, 0.6)
global STATE, ERROR
STATE = ERROR
clr()
led_display.text = "." * 15
time.sleep(0.2)
led_display.text = " " * 15
time.sleep(0.2)
led_display.text = "." * 15
display_status(text, None) # Hold error description in status area
return
def display_status(text="", duration=0.5):
"""Display message in status area for duration in seconds or None to hold
text in status message area."""
case_group.status.text = text
case_group.status.color = Colors.BLUE
if duration:
time.sleep(duration)
case_group.status.color = None
return
def update_x_reg_from_display_reg():
"""Move DISPLAY registers' content to the X_REG."""
global X_REG, DISPLAY_C, DISPLAY_E
if DISPLAY_E == "-00":
DISPLAY_E = " 00"
X_REG = convert_display_to_decimal(DISPLAY_C, DISPLAY_E)
return
def update_display_reg_from_x_reg():
"""Update the LED display with X_REG value."""
global X_REG, DISPLAY_C, DISPLAY_E
coefficient, exponent = convert_decimal_to_display(X_REG)
DISPLAY_C = coefficient
DISPLAY_E = exponent
# If needed, pad coefficient with spaces
coefficient = coefficient + (" " * (12 - len(coefficient)))
led_display.text = coefficient + exponent
return
def convert_degrees_to_radians(value):
"""Convert Decimal degrees value to radians."""
return (value % 360) * (PI * 2) / 360
def convert_radians_to_degrees(value):
"""Convert Decimal radians value to degrees."""
return (value % (PI * 2)) * 360 / (PI * 2)
tft.root_group = calculator
gc.collect()
free_memory = gc.mem_free()
frame = time.monotonic() - t0
print("CG-35 Calculator Cedar Grove Studios")
print(f"setup: {frame:5.02f}sec free memory: {free_memory/1000:6.03f}kb")
print(f"Calculator STATE: {STATE}")
tone(board.A0, 440, 0.25) # Startup beep
display_status("... READY ...", 1)
clr()
update_display_reg_from_x_reg()
while True:
t0 = time.monotonic() # Reset timer for start of frame
key_name = get_key() # Wait for key press
display_status("", 0) # Clear any status messages
# Display Entry Keys Cluster: 0-9, ., CHS, EEX
if key_name in (
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
".",
"CHS",
"EEX",
):
printd(f"Display entry key: {key_name}")
if "ENTRY" not in STATE:
# Prepare for digit entry when previous operation has finished
if STATE == DYADIC:
# Automatic ENTER only after dyadic operations
push_stack()
clr("x")
dp_flag = False
STATE = C_ENTRY
if (
(STATE == C_ENTRY)
and (STATE != E_ENTRY)
and len(DISPLAY_C) < 12
and (key_name not in ("CHS", "EEX"))
):
getcontext().prec = DISPLAY_PRECISION
if DISPLAY_C[1:] == "0." and key_name == ".":
# First digit entry
dp_flag = True
DISPLAY_C = DISPLAY_C.replace("0.", key_name)
elif DISPLAY_C[1:] == "0.":
DISPLAY_C = DISPLAY_C.replace("0", key_name)
else:
dp_index = DISPLAY_C.index(".")
if key_name != ".":
if dp_flag:
# Fractional portion of coefficient (right of decimal separator)
DISPLAY_C = DISPLAY_C + key_name
else:
# Integer portion of coefficient (left of decimal separator)
DISPLAY_C = (
DISPLAY_C[0:dp_index] + key_name + DISPLAY_C[dp_index:]
)
else:
dp_flag = True
if (STATE == E_ENTRY) and (key_name not in ("CHS", "EEX")):
# First digit entry
if DISPLAY_E[1:3] == "00":
DISPLAY_E = DISPLAY_E[0:2] + key_name
# Second digit entry
elif DISPLAY_E[1] == "0":
DISPLAY_E = DISPLAY_E[0] + DISPLAY_E[2] + key_name
if key_name == "CHS":
if (STATE == C_ENTRY) and DISPLAY_C[1:3] != "0.":
# Change the coefficient sign
if DISPLAY_C[0] == "-":
DISPLAY_C = " " + DISPLAY_C[1:]
else:
DISPLAY_C = "-" + DISPLAY_C[1:]
elif STATE == E_ENTRY:
# Change the exponent sign
if DISPLAY_E[0] == "-":
DISPLAY_E = " " + DISPLAY_E[1:]
else:
DISPLAY_E = "-" + DISPLAY_E[1:]
if key_name == "EEX":
STATE = E_ENTRY
show_display_reg()
update_x_reg_from_display_reg()
# Stack Management and Constant Key Cluster: ENTER, CLR, CLX, STO, RCL, R, x<>y, π
if key_name in (
"ENTER",
"CLR",
"CLX",
"STO",
"RCL",
"R",
"x<>y",
"π",
):
STATE = STACK
printd(f"stack management and constant key: {key_name}")
if key_name == "ENTER":
push_stack()
if key_name == "CLR":
clr()
if key_name == "CLX":
clr("x")
if key_name == "STO":
MEM = X_REG
if key_name == "RCL":
push_stack()
X_REG = MEM
if key_name == "R":
roll_stack()
if key_name == "x<>y":
temp = X_REG
X_REG = Y_REG
Y_REG = temp
if key_name == "π":
X_REG = PI
# Monadic Operator Key Cluster: LOG, LN, e^x, √x, ARC, SIN, COS, ,TAN, 1/x
if key_name in (
"LOG",
"LN",
"e^x",
"√x",
"ARC",
"SIN",
"COS",
"TAN",
"1/x",
):
STATE = MONADIC
printd(f"monadic operator key: {key_name}")
try:
getcontext().prec = INTERNAL_PRECISION
if key_name == "LOG":
X_REG = Decimal.log10(X_REG)
if key_name == "LN":
X_REG = Decimal.ln(X_REG)
if key_name == "e^x":
X_REG = Decimal.exp(X_REG)
if key_name == "√x":
X_REG = Decimal.sqrt(X_REG)
if key_name == "ARC":
ARC_FLAG = True
if key_name == "SIN":
if ARC_FLAG:
X_REG = convert_radians_to_degrees(Decimal.asin(X_REG))
else:
X_REG = Decimal.sin(convert_degrees_to_radians(X_REG))
ARC_FLAG = False
if key_name == "COS":
if ARC_FLAG:
X_REG = convert_radians_to_degrees(Decimal.acos(X_REG))
else:
X_REG = Decimal.cos(convert_degrees_to_radians(X_REG))
ARC_FLAG = False
if key_name == "TAN":
if ARC_FLAG:
X_REG = convert_radians_to_degrees(Decimal.atan(X_REG))
else:
X_REG = Decimal.tan(convert_degrees_to_radians(X_REG))
ARC_FLAG = False
if key_name == "1/x":
X_REG = 1 / X_REG
except Exception as err:
print("Exception:", err)
display_error(str(type(err))[8:-2])
if X_REG.is_infinite():
print("Error: Infinite value result")
display_error("InfiniteValue")
# Dyadic Operator Key Cluster: x^y, -, +, *, ÷
if key_name in (
"x^y",
"-",
"+",
"*",
"÷",
):
STATE = DYADIC
printd(f"dyadic operator key: {key_name}")
try:
getcontext().prec = INTERNAL_PRECISION
if key_name == "x^y":
X_REG = X_REG**Y_REG
if key_name == "-":
Y_REG = Y_REG - X_REG
pull_stack()
if key_name == "+":
Y_REG = Y_REG + X_REG
pull_stack()
if key_name == "*":
Y_REG = Y_REG * X_REG
pull_stack()
if key_name == "÷":
Y_REG = Y_REG / X_REG
pull_stack()
except Exception as err:
print("Exception:", err)
display_error(str(type(err))[8:-2])
if X_REG.is_infinite() or Y_REG.is_infinite():
print("Error: Infinite value in X_REG and/or Y_REG")
display_error("InfiniteValue")
if STATE not in ("C_ENTRY", "E_ENTRY", "ERROR"):
STATE = IDLE
update_display_reg_from_x_reg()
gc.collect() # Clean-up memory heap space
print_stack()
frame = time.monotonic() - t0
free_memory = gc.mem_free()
print(f"frame: {frame:5.02f}sec free memory: {free_memory/1000:6.03f}kb")
print(f"Calculator STATE: {STATE}")
Build the Keys
The CalculatorButtons class cedargrove_calculator.buttons.py defines the calculator keys from a table of button characteristics. The class provides a read_buttons function to detect when a button is touched.
# SPDX-FileCopyrightText: 2022, 2024 Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
cedargrove_calculator.buttons.py 2024-03-11 v2.0
modified for 3.5-inch TFT Capacitive FeatherWing with Feather RP2040
=================================================
Calculator buttons class.
* Author(s): JG for Cedar Grove Maker Studios
"""
import board
import time
import displayio
from adafruit_bitmap_font import bitmap_font
from adafruit_button import Button
from simpleio import tone
class Colors:
BLACK = 0x000000
BLUE = 0x4040F0
GRAY = 0x202020
GRAY_DK = 0x101010
RED = 0xFF0000
WHITE = 0xC0C0C0
OUTLINE = GRAY_DK
black_palette = displayio.Palette(1)
black_palette[0] = BLACK
gray_palette = displayio.Palette(1)
gray_palette[0] = GRAY
gray_dk_palette = displayio.Palette(1)
gray_dk_palette[0] = GRAY_DK
# (x, y), (width, height), name, fill_color, outline_color, pressed_color
# object order: back to front
HP_BUTTONS = [
(( 61, 123), (29, 22), "x^y", Colors.BLACK, Colors.OUTLINE),
((103, 123), (29, 22), "LOG", Colors.BLACK, Colors.OUTLINE),
((146, 123), (29, 22), "LN", Colors.BLACK, Colors.OUTLINE),
((187, 123), (29, 22), "e^x", Colors.BLACK, Colors.OUTLINE),
((230, 123), (29, 22), "CLR", Colors.BLUE, Colors.OUTLINE),
(( 61, 167), (29, 22), "√x", Colors.BLACK, Colors.OUTLINE),
((103, 167), (29, 22), "ARC", Colors.BLACK, Colors.OUTLINE),
((146, 167), (29, 22), "SIN", Colors.BLACK, Colors.OUTLINE),
((187, 167), (29, 22), "COS", Colors.BLACK, Colors.OUTLINE),
((230, 167), (29, 22), "TAN", Colors.BLACK, Colors.OUTLINE),
(( 61, 212), (29, 22), "1/x", Colors.BLACK, Colors.OUTLINE),
((103, 212), (29, 22), "x<>y", Colors.BLACK, Colors.OUTLINE),
((146, 212), (29, 22), "R", Colors.BLACK, Colors.OUTLINE),
((187, 212), (29, 22), "STO", Colors.BLACK, Colors.OUTLINE),
((230, 212), (29, 22), "RCL", Colors.BLACK, Colors.OUTLINE),
(( 61, 257), (69, 22), "ENTER", Colors.BLUE, Colors.OUTLINE),
((146, 257), (29, 22), "CHS", Colors.BLUE, Colors.OUTLINE),
((187, 257), (29, 22), "EEX", Colors.BLUE, Colors.OUTLINE),
((230, 257), (29, 22), "CLX", Colors.BLUE, Colors.OUTLINE),
(( 61, 301), (29, 22), "-", Colors.BLUE, Colors.OUTLINE),
((105, 301), (33, 22), "7", Colors.WHITE, Colors.OUTLINE),
((165, 301), (33, 22), "8", Colors.WHITE, Colors.OUTLINE),
((225, 301), (33, 22), "9", Colors.WHITE, Colors.OUTLINE),
(( 61, 346), (22, 22), "+", Colors.BLUE, Colors.OUTLINE),
((105, 346), (33, 22), "4", Colors.WHITE, Colors.OUTLINE),
((165, 346), (33, 22), "5", Colors.WHITE, Colors.OUTLINE),
((225, 346), (33, 22), "6", Colors.WHITE, Colors.OUTLINE),
(( 61, 391), (22, 22), "*", Colors.BLUE, Colors.OUTLINE),
((105, 391), (33, 22), "1", Colors.WHITE, Colors.OUTLINE),
((165, 391), (33, 22), "2", Colors.WHITE, Colors.OUTLINE),
((225, 391), (33, 22), "3", Colors.WHITE, Colors.OUTLINE),
(( 61, 435), (22, 22), "÷", Colors.BLUE, Colors.OUTLINE),
((105, 435), (33, 22), "0", Colors.WHITE, Colors.OUTLINE),
((165, 435), (33, 22), ".", Colors.WHITE, Colors.OUTLINE),
((225, 435), (33, 22), "π", Colors.WHITE, Colors.OUTLINE),
]
class CalculatorButtons(displayio.Group):
def __init__(self, l_margin=0, timeout=1.0, click=True, display=None, touch=None):
"""Instantiate on-screen buttons and build button_group."""
self._timeout = timeout
self._click = click
self._l_margin = l_margin
self.ts = touch
WIDTH = display.width
HEIGHT = display.height
SIZE_FACTOR = 1.2
# Create a list of button names for button creation
self._button_names = [name[2] for name in HP_BUTTONS]
self.FONT_0 = bitmap_font.load_font("/fonts/OpenSans-9.bdf")
# Build displayio button group
self._buttons = []
self._buttons_index = [] # The list of buttons used for detection
button_group = displayio.Group()
# Create the displayio button definitions
for key in HP_BUTTONS:
button = Button(
x=key[0][0],
y=key[0][1],
width=int(key[1][0] * SIZE_FACTOR),
height=int(key[1][1] * SIZE_FACTOR),
style=Button.RECT,
fill_color=key[3],
outline_color=key[4],
name=key[2],
label=key[2],
label_font=self.FONT_0,
label_color=Colors.WHITE,
selected_fill=Colors.RED,
selected_outline=Colors.RED,
)
if button.fill_color == Colors.WHITE:
button.label_color = Colors.BLACK
button_group.append(button)
self._buttons.append(button)
self._buttons_index.append(button.name)
super().__init__()
self.append(button_group)
return
@property
def timeout(self):
"""Button timeout duration setting."""
return self._timeout
@timeout.setter
def timeout(self, hold_time=1.0):
"""Select timeout duration value in seconds, positive float value."""
if hold_time < 0 or hold_time >= 10:
print(
"Invalid button timeout duration value. Must be between 0 and 10 seconds."
)
return
self._timeout = hold_time
return
def read_buttons(self):
button_pressed = button_name = None
hold_time = 0
touch = self.ts.points
if touch:
for button in self._buttons:
if button.contains(touch[0]): # read only the first point touched
button.selected = True
if self._click:
# Make a click sound when button is pressed
tone(board.A0, 2000, 0.025, length=8)
button_pressed = button.name
button_name = self._buttons_index.index(button_pressed)
timeout_beep = False
while self.ts.points:
time.sleep(0.1)
hold_time += 0.1
if hold_time >= self._timeout and not timeout_beep:
if self._click:
# Play a beep tone if button is held
tone(board.A0, 1320, 0.100, length=8)
timeout_beep = True
button.selected = False
if self._click:
# Make a click sound when button is released
tone(board.A0, 2000, 0.025, length=8)
return button_pressed, button_name, hold_time
Make a Case for Calculation
The CalculatorCase, LEDDisplay, and Colors classes cedargrove_calculator.case.py defines the graphic elements needed to display the calculator case and numeric display.
# SPDX-FileCopyrightText: 2022, 2024 JG for Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
cedargrove_calculator.case.py 2024-04-14 v2.2
For the ESP32-S3 4Mb/2Mb Feather and 3.5-inch TFT Capacitive FeatherWing
==============================================
Calculator case graphics classes.
"""
import displayio
import vectorio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
class Colors:
BLACK = 0x000000
BLUE = 0x606060
GRAY = 0x202020
GRAY_DK = 0x101010
black_palette = displayio.Palette(1)
black_palette[0] = BLACK
gray_palette = displayio.Palette(1)
gray_palette[0] = GRAY
gray_dk_palette = displayio.Palette(1)
gray_dk_palette[0] = GRAY_DK
# (x, y), (width, height), name, fill_color
# object order: back to front
HP_CASE = [
((39, 0), (241, 78), "DISPLAY outline", Colors.gray_dk_palette),
((39, 78), (241, 402), "KEYS outline", Colors.gray_palette),
((89, 89), (33, 11), "PWR outline", Colors.gray_dk_palette),
((106, 89), (17, 11), "PWR switch", Colors.black_palette),
((48, 18), (223, 42), "DISPLAY area", Colors.black_palette),
]
class LEDDisplay(displayio.Group):
def __init__(self, scale=1, display_color=0xFF0000, display=None):
"""Instantiate LED display and build led_display_group."""
_scale = scale
FONT_0 = bitmap_font.load_font("/fonts/SevenSeg-12.bdf")
# Build displayio LED display group
led_display_group = displayio.Group()
# LED display label
self._led_digits = Label(
font=FONT_0,
text="",
color=display_color,
)
self._led_digits.anchor_point = (0, 0.5)
self._led_digits.anchored_position = (57, 40)
led_display_group.append(self._led_digits)
super().__init__(scale=_scale)
self.append(led_display_group)
return
@property
def text(self):
return self._led_digits.text
@text.setter
def text(self, text=""):
self._led_digits.text = text[0:15]
return
class CalculatorCase(displayio.Group):
def __init__(self, display=None):
"""Instantiate case graphic and build case group."""
self._l_margin = HP_CASE[0][0][0]
FONT_1 = bitmap_font.load_font("/fonts/OpenSans-9.bdf")
# Build displayio case group
case_group = displayio.Group()
for part in HP_CASE:
case_part = vectorio.Rectangle(
pixel_shader=part[3],
x=part[0][0],
y=part[0][1],
width=part[1][0],
height=part[1][1],
)
case_group.append(case_part)
# Status message area
self._status = Label(
font=FONT_1,
text="",
color=None,
)
self._status.anchor_point = (0.5, 0.5)
self._status.anchored_position = (160, 69)
case_group.append(self._status)
# Power switch label
pwr_text = Label(
font=FONT_1,
text="OFF" + (" " * 14) + "ON" + (" " * 26) + "CG-35",
color=Colors.BLACK,
)
pwr_text.anchor_point = (0, 0)
pwr_text.anchored_position = (61, HP_CASE[2][0][1])
case_group.append(pwr_text)
super().__init__()
self.append(case_group)
return
@property
def l_margin(self):
"""Left margin spacing in pixels."""
return self._l_margin
@property
def status(self):
"""Status message text."""
return self._status
@status.setter
def status(self, text=""):
self._status = text
return
This page (CG-35: A Retro RPN Calculator) was last updated on January 13, 2025.
Text editor powered by tinymce.