Skip to content

Instantly share code, notes, and snippets.

@quasar098
Created July 3, 2025 02:20
Show Gist options
  • Save quasar098/c36889977aed67edac6cdde8caefdcfe to your computer and use it in GitHub Desktop.
Save quasar098/c36889977aed67edac6cdde8caefdcfe to your computer and use it in GitHub Desktop.
calculator gadget code using shunting yard algo and custom pcb for mechanical keys (arduino)
#include "LiquidCrystal_I2C.h"
#include <Keyboard.h>
#include "Wire.h"
#define HEIGHT 5
#define WIDTH 4
#define MAX_EXPR_WIDTH 128
#define MAX_RES_STRING_SIZE 20
// ==== expression evaluation below ====
#include <MemoryUsage.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
class Token {
public:
char tokenType; // 0 = float, 1-244 = operator, 255 = head
float value;
Token* nextNode = 0;
Token(char type, float val) {
tokenType = type;
value = val;
}
};
void debugToken(Token* t) {
if (t->tokenType > 1) {
printf("addr: %p\ttype: %d\tval: \"%c\"\t next: %p\n", t, t->tokenType, (char) t->tokenType, t->nextNode);
return;
}
printf("addr: %p\ttype: %d\tval: %f\t next: %p\n", t, t->tokenType, t->value, t->nextNode);
}
void debugTokenLinkedList(Token* headToken) {
Token* n = headToken;
while (n->nextNode != 0) {
n = n->nextNode;
debugToken(n);
}
Serial.println("==========\n");
}
void appendToken(Token* head, Token* item) {
Token* n = head;
while (n->nextNode != 0) {
n = n->nextNode;
}
n->nextNode = item;
item->nextNode = 0;
}
void appendInteger(Token* head, float item) {
appendToken(head, new Token(0, item));
}
void appendOperator(Token* head, char c) {
appendToken(head, new Token(c, 0));
}
Token* popFirstToken(Token* headToken) {
if (headToken->nextNode == 0) {
Serial.println("attempt to pop from empty list first token");
delay(1000000);
}
Token* retValue = headToken->nextNode;
headToken->nextNode = headToken->nextNode->nextNode;
return retValue;
}
Token* popLastToken(Token* headToken) {
if (headToken->nextNode == 0) {
Serial.println("attempt to pop from empty list last token");
delay(1000000);
}
Token* n = headToken;
while (n->nextNode != 0) {
if (n->nextNode->nextNode == 0) {
Token* retValue = n->nextNode;
n->nextNode = 0;
return retValue;
}
n = n->nextNode;
}
return 0;
}
Token* getLastToken(Token* headToken) {
if (headToken->nextNode == 0) {
Serial.println("attempt to get from empty list");
delay(1000000);
}
Token* n = headToken;
while (n->nextNode != 0) {
if (n->nextNode->nextNode == 0) {
return n->nextNode;
}
n = n->nextNode;
}
return 0;
}
int getLength(Token* headToken) {
int count = 0;
Token* n = headToken;
while (n->nextNode != 0) {
n = n->nextNode;
count += 1;
}
return count;
}
int getPrecedenceOfToken(Token* t) {
if (t->tokenType == 0) {
Serial.println("cannot get precedence of number");
delay(1000000);
}
switch (t->tokenType) {
case 47: // division
return 4;
case 42: // multiplication
return 3;
case 45: // minus
return 2;
case 43: // plus
return 1;
default:
return 0;
}
}
float evaluateStringExpression(char* input) {
// convert input to tokens
Token* headInputToken = new Token(255, 0);
int index = 0;
int inputLength = strlen(input);
float builder = 0;
bool decimaling = false;
int decimaledAmt = 0;
bool built = false;
while (index != inputLength) {
char c = input[index];
if (c == 32) {
index += 1;
continue;
}
if (strchr("()+-/*", c) != NULL) {
if (built) {
appendInteger(headInputToken, builder);
builder = 0;
built = false;
decimaling = false;
decimaledAmt = 0;
}
appendOperator(headInputToken, c);
} else if (strchr(".", c) != NULL) {
decimaling = true;
} else {
built = true;
// todo handle decimals properly
if (decimaling) {
builder += (c-48) * pow(10, -(++decimaledAmt));
} else {
builder = builder * 10 + (c-48);
}
}
index += 1;
}
if (built) {
appendInteger(headInputToken, builder);
}
// shunting yard algorithm
Token* headOutputToken = new Token(255, 0);
Token* headOperatorStackToken = new Token(255, 0);
// MEMORY_PRINT_HEAPSIZE
// FREERAM_PRINT;
while (getLength(headInputToken) != 0) {
Token* token = popFirstToken(headInputToken);
if (token->tokenType == 0) {
// integer token
appendToken(headOutputToken, token);
} else {
// lparen
if (token->tokenType == 40) {
appendToken(headOperatorStackToken, token);
continue;
}
// rparen
if (token->tokenType == 41) {
while (true) {
Token* lastToken = popLastToken(headOperatorStackToken);
if (lastToken->tokenType == 40 && lastToken->tokenType > 1) {
delete lastToken;
break;
}
appendToken(headOutputToken, lastToken);
}
delete token;
continue;
}
// other operators
while (getLength(headOperatorStackToken) && getPrecedenceOfToken(getLastToken(headOperatorStackToken)) >= getPrecedenceOfToken(token)) {
appendToken(headOutputToken, popLastToken(headOperatorStackToken));
}
appendToken(headOperatorStackToken, token);
}
}
while (getLength(headOperatorStackToken)) {
appendToken(headOutputToken, popLastToken(headOperatorStackToken));
}
delete headInputToken;
delete headOperatorStackToken;
// MEMORY_PRINT_HEAPSIZE
// FREERAM_PRINT;
// evaluate reverse polish expression
Token* numberStack = new Token(255, 0);
while (getLength(headOutputToken)) {
Token* popped = popFirstToken(headOutputToken);
if (popped->tokenType == 0) {
appendToken(numberStack, popped);
} else {
Token* b = popLastToken(numberStack);
Token* a = popLastToken(numberStack);
if (popped->tokenType == 47) { // division
a->value /= b->value;
}
if (popped->tokenType == 42) { // multiplication
a->value *= b->value;
}
if (popped->tokenType == 45) { // subtraction
a->value -= b->value;
}
if (popped->tokenType == 43) { // addition
a->value += b->value;
}
appendToken(numberStack, a);
delete b;
delete popped;
}
}
Token* final = popLastToken(numberStack);
float finalValue = final->value;
delete final;
delete numberStack;
delete headOutputToken;
return finalValue;
}
// ==== actual calculator part below ====
char expr[MAX_EXPR_WIDTH] = {};
unsigned int exprPos = 0;
char resultString[MAX_RES_STRING_SIZE] = {};
const int rows[HEIGHT] = {9, 8, 7, 6, 5};
const int cols[WIDTH] = {13, 12, 11, 10};
bool prevPressed[HEIGHT][WIDTH] = {};
char keyMap[HEIGHT][WIDTH] = {
{'-', '*', '/', 'H'},
{'+', '9', '8', '7'},
{'?', '6', '5', '4'},
{'?', '3', '2', '1'},
{'E', '.', '0', '?'}
};
char keyMapSpecial[HEIGHT][WIDTH] = {
{'M', '*', '/', 'H'},
{'+', '9', '^', '7'},
{'?', '>', '5', '<'},
{'?', '3', 'V', '1'},
{'E', 'B', 'R', '?'}
};
#define KEY_F13 0xF0
#define KEY_F14 0xF1
#define KEY_F15 0xF2
#define KEY_F16 0xF3
#define KEY_F17 0xF4
#define KEY_F18 0xF5
#define KEY_F19 0xF6
#define KEY_F20 0xF7
#define KEY_F21 0xF8
#define KEY_F22 0xF9
#define KEY_F23 0xFA
#define KEY_F24 0xFB
#define KEY_UP_ARROW 0xDA
#define KEY_DOWN_ARROW 0xD9
#define KEY_LEFT_ARROW 0xD8
#define KEY_RIGHT_ARROW 0xD7
LiquidCrystal_I2C lcd(0x27, 16, 2);
bool dirtyDisplay = false;
unsigned long currentTick = 0;
unsigned long longLastKeyPressTick = 0;
bool macropadMode;
void setup() {
// lcd
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
macropadMode = !!Serial;
lcd.print(macropadMode ? "Idle (Macropad)" : "Idle (Calc)");
Keyboard.begin();
dirtyDisplay = true;
// keys
for (int i = 0; i < HEIGHT; i++) {
pinMode(rows[i], OUTPUT);
digitalWrite(rows[i], HIGH); // set rows HIGH by default
}
for (int j = 0; j < WIDTH; j++) {
pinMode(cols[j], INPUT_PULLUP); // enable internal pull-ups on columns
}
Serial.begin(9600);
}
void updateDisplay() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(expr);
}
void updateDisplay(char* msg) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(msg);
}
void registerMacropadModePress(char key) {
if (key == 'H') {
// do nothing
} else if (key == '?') {
updateDisplay("error! oh no");
} else if ('9' >= key && key >= '0') {
Keyboard.write(KEY_F13+(key-'0'));
} else if (key == 'M') { // mode (macropad vs calc)
macropadMode = !macropadMode;
lcd.setCursor(0, 0);
lcd.clear();
lcd.print(macropadMode ? "Idle (Macropad)" : "Idle (Calc)");
dirtyDisplay = true;
} else if (key == '^') {
Keyboard.write(KEY_UP_ARROW);
} else if (key == '<') {
Keyboard.write(KEY_LEFT_ARROW);
} else if (key == '>') {
Keyboard.write(KEY_RIGHT_ARROW);
} else if (key == 'V') {
Keyboard.write(KEY_DOWN_ARROW);
}
}
void registerPress(char key) {
if (dirtyDisplay) {
dirtyDisplay = false;
if (exprPos > 15) {
updateDisplay(expr+exprPos-15);
} else {
updateDisplay();
}
}
if (macropadMode) {
registerMacropadModePress(key);
return;
}
if (key == 'H') { // home
// do nothing
} else if (key == '?') {
updateDisplay("error! oh no");
} else if (key == 'B') { // backspace
if (exprPos > 0) {
expr[--exprPos] = '\x00';
if (exprPos > 14) {
updateDisplay(expr+exprPos-15);
} else {
lcd.setCursor(exprPos, 0);
lcd.print(' ');
lcd.setCursor(exprPos, 0);
}
}
} else if (key == 'R') { // reset expression
exprPos = 0;
memset(expr, 0, MAX_EXPR_WIDTH);
updateDisplay();
} else if (key == 'E') { // enter
exprPos = 0;
float result = evaluateStringExpression(expr);
memset(expr, 0, MAX_RES_STRING_SIZE);
dtostrf(result, 16, 3, resultString);
int resultStringLen = strlen(resultString);
Serial.println(resultString);
lcd.setCursor(16-resultStringLen, 1);
lcd.print(resultString);
memset(expr, 0, MAX_EXPR_WIDTH);
dirtyDisplay = true;
} else if (key == 'M') { // mode (macropad vs calc)
macropadMode = !macropadMode;
lcd.setCursor(0, 0);
lcd.clear();
lcd.print(macropadMode ? "Idle (Macropad)" : "Idle (Calc)");
dirtyDisplay = true;
} else {
if (exprPos < MAX_EXPR_WIDTH) {
if (key == '>') {
key = '6';
} else if (key == '<') {
key = '4';
} else if (key == '^') {
key = '8';
} else if (key == 'V') {
key = '2';
}
expr[exprPos++] = key;
if (exprPos > 15) {
updateDisplay(expr+exprPos-15);
} else {
// if (dirtyDisplay) {
// dirtyDisplay = false;
// lcd.clear();
// }
lcd.setCursor(exprPos-1, 0);
lcd.print(expr+exprPos-1);
}
}
}
}
int hertz = 100;
void loop() {
for (int r = 0; r < HEIGHT; r++) {
digitalWrite(rows[r], LOW); // activate current row
for (int c = 0; c < WIDTH; c++) {
if (digitalRead(cols[c]) == LOW) { // button pressed
if (!prevPressed[r][c]) {
longLastKeyPressTick = currentTick;
registerPress(prevPressed[0][3] ? keyMapSpecial[r][c] : keyMap[r][c]);
}
prevPressed[r][c] = true;
} else {
prevPressed[r][c] = false;
}
}
digitalWrite(rows[r], HIGH); // deactivate row
}
// timeout
currentTick++;
if (currentTick < longLastKeyPressTick+10*hertz) {
lcd.backlight();
if (!dirtyDisplay) {
lcd.blink();
} else {
lcd.noBlink();
}
} else {
lcd.noBacklight();
lcd.noBlink();
}
// delay
delay(1000/hertz);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment