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
Reverse Polish Notation is a postfix scheme where operator (function) selection follows the input of the operands (values). During the age of mechanical calculator engines, postfix notation was an efficient way of entering values and functions for computing a result. Early electronic calculators were designed to operate with the familiar mechanical notation method, eventually to be mostly replaced by devices using infix ("algebraic" or "arithmetical") notation.
Implementation Notes
Hardware:
- Adafruit ESP32-S3 4Mb/2Mb Feather
- Adafruit TFT Display FeatherWing - 3.5" 480x320 Capacitive Touchscreen
- Piezo Speaker (optional)
Software:
- Adafruit CircuitPython firmware for the ESP32-S3 Feather
- 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