The code is long enough (around 400 lines) that a line by line explanation would be too verbose. Here are some high level notes to give you an overview:

class AngleConvert:
   ...

The AngleConvert class helps implement the "degrees / radians / gradians" mode common on scientific calculators. It wraps each of the 6 trig functions, converting to/from radians as necessary. To improve the precision of the result, these steps are carried out with extra digits of precision using the extraprec decorator function.

getcontext().prec = 14
getcontext().Emax = 99
getcontext().Emin = -99

Sets the default precision (number of decimal places), minimum and maximum exponents. These values were chosen so that any number should fit on the screen without being cut off, but any of these values can be increased or decreased if desired.

class MatrixKeypadBase:
    ...

class MatrixKeypad:
    ...

class LayerSelect:
    ...
    
...
layers = (
    (
        ('^', 'l', 'r', LS1),
        ...
    ),
    ...
)

These classes implement a matrix keyboard, including layer selection. The "layers" tuple has "layer 0" (normal layer) followed by "layer 1" (alternate layer). Except for the special values LS1 (layer shift 1) and LL0 (layer lock 0), each one specifies a (possibly empty) string that is handled later by the the dictionary of ops or by the main loop.

class Impl:
    ...

The "Impl" (implementation) class has the details of how to interact with the keyboard matrix and update the display. The calculator program originally ran on a PC in a terminal window, and this class is a vestige of trying to support for both CircuitPython and standard Python3 within a single program.

def do_op(arity, fun):
    ...
    
ops = {
    '\'': (1, lambda x: -x),
    '\\': (2, lambda x, y: x/y),  # keypad: SHIFT+/
    ...
    '@': angleconvert.next_state,
    ...
}

ops is a dictionary where the key is a character produced by the key matrix and the value is either a callable or a tuple of (arity, callable).

When an operation is a callable, it has to manage the stack itself. When it's a tuple, then the first number in the tuple (called the arity) specifies how many arguments the function expects. That number of arguments are passed as arguments to the function. If the function succeeds, the arguments are removed from the stack. Then, the return value, if it's not None, is put back on the stack.

def pstack(msg):
    ...

The pstack function updates the screen.

def loop():
    ...

The loop function runs continuously, waiting for key to be entered. Some keys are processed specially (like the digits, "./E", backspace, and enter/dup), other keys are handled by looking up the operation in ops.

Adding new functions

To add a new function, you will need several things:

  • The implementation of the mathematical function. This can be a function that you write, but for this example we will use the existing function Decimal.log10
  • The physical key location you choose for the function. Let's choose the ALT function of the key that is normally "+"
  • The character you use to represent it. Let's choose "O" (capital Oscar), perhaps standing for the "o" of log10 but mostly because it is not used yet.
  • The arity of the function. log10 takes a single number (x), so its arity is 1

With all these pieces of information, we can start to modify the code. First, we have to modify the keymap, called layers:

layers = (
    (
        ('^', 'l', 'r', LS1),
        ('s', 'c', 't', '/'),
        ('7', '8', '9', '*'),
        ('4', '5', '6', '-'),
        ('1', '2', '3', '+'),
        ('0', '.',  BS,  CR)
    ),

    (
        ('v', 'L', 'R', LL0),
        ('S', 'C', 'T', 'N'),
        ( '',  '',  '',  ''),
        ( '',  '',  '', 'n'),
        ( '',  '',  '', 'O'), # modify this line, changing '' to 'O'
        ('=', '@',  BS, '~')
    ),
)

Next, add 'O': Decimal.log10, to ops:

ops = {
    'O': (1, Decimal.log10),   # Add this line
    '\'': (1, lambda x: -x),
    ...                        # Keep the other lines as-is
}

When you save the file, CircuitPython will automatically restart. Type "1000 ALT +" and you should see the result "3", because 1000=10^3.

Here are some other ideas about functions to add to the calculator:

  • Unit conversions like inches to millimeters
  • Constants like π or e. π can be computed as Decimal('1').atan() * 4, and e can be computed as Decimal('1').exp()
  • Engineering functions, statistical functions, etc

Other ideas for customization and improvement

  • The Feather nRF52840 can also act as a BLE (bluetooth) keyboard. Convert the calculator to run from a rechargeable battery and make it paste over Bluetooth instead of or in addition to USB.
  • Using the jepler_udecimal library, implement a standard "infix" calculator instead.
  • Add the ability to enter a formula from the keypad and create a CircuitPython graphing calculator

This guide was first published on Oct 21, 2020. It was last updated on Oct 21, 2020.

This page (Code Highlights and Customization) was last updated on Mar 26, 2021.

Text editor powered by tinymce.