Created
July 3, 2025 02:20
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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