/*
* ESP32 BASIC Scientific Calculator - Refactored v2
* Compatible with ESP32 Arduino framework
*/
#include <Arduino.h>
#include <math.h>
// --- Configuration ---
#define STACK_SIZE 64
#define MAX_PROGRAM_LINES 2000
#define MAX_LOOP_DEPTH 32
#define MAX_ARRAY_SIZE 1024
#define PI_VAL 3.141592653589793
#define E_VAL 2.718281828459045
// --- Global Variables ---
double variables[26];
double* arrays[26];
int arraySizes[26];
String programLines[MAX_PROGRAM_LINES];
int lineNumbers[MAX_PROGRAM_LINES];
int programSize = 0;
int pc = 0;
int printPrecision = 6;
bool radiansMode = true;
enum State { IDLE, RUNNING, AWAIT_INPUT };
State state = IDLE;
int inputVarIndex = -1;
String inputBuffer = "";
struct ForLoop {
int varIndex;
double endVal;
double stepVal;
int returnPC;
};
ForLoop loopStack[MAX_LOOP_DEPTH];
int loopSP = -1;
// --- Stack Classes ---
String strStack[STACK_SIZE];
int strTop = -1;
double numStack[STACK_SIZE];
int numTop = -1;
void strPush(String s) { if (strTop < STACK_SIZE - 1) strStack[++strTop] = s; }
String strPop() { return strTop >= 0 ? strStack[strTop--] : ""; }
String strPeek() { return strTop >= 0 ? strStack[strTop] : ""; }
bool strEmpty() { return strTop < 0; }
void strClear() { strTop = -1; }
void numPush(double v) { if (numTop < STACK_SIZE - 1) numStack[++numTop] = v; }
double numPop() { return numTop >= 0 ? numStack[numTop--] : NAN; }
bool numEmpty() { return numTop < 0; }
void numClear() { numTop = -1; }
// --- Helper Functions ---
int varIndex(char c) {
c = toupper(c);
return (c >= 'A' && c <= 'Z') ? c - 'A' : -1;
}
bool isVar(char c) { return varIndex(c) >= 0; }
int toInt(double v) { return (int)(v + (v >= 0 ? 0.5 : -0.5)); }
bool isProgramLine(String s) {
s.trim();
if (s.length() == 0 || !isDigit(s.charAt(0))) return false;
int i = 0;
while (i < (int)s.length() && isDigit(s.charAt(i))) i++;
if (i < (int)s.length() && s.charAt(i) == '.') return false;
return (i == (int)s.length() || s.charAt(i) == ' ');
}
double getArray(int idx, int i) {
if (idx < 0 || idx >= 26 || arrays[idx] == NULL || i < 0 || i >= arraySizes[idx]) {
Serial.println(F("Array error!"));
return NAN;
}
return arrays[idx][i];
}
void setArray(int idx, int i, double val) {
if (idx < 0 || idx >= 26 || i < 0) {
Serial.println(F("Array error!"));
return;
}
// Auto-allocate or grow
if (arrays[idx] == NULL || i >= arraySizes[idx]) {
int oldSize = arraySizes[idx];
int newSize = i + 1;
if (newSize > MAX_ARRAY_SIZE) {
Serial.println(F("Array too large!"));
return;
}
double* newArr = (double*)malloc(newSize * sizeof(double));
if (newArr == NULL) {
Serial.println(F("Memory error!"));
return;
}
for (int j = 0; j < newSize; j++) newArr[j] = 0.0;
if (arrays[idx] != NULL) {
for (int j = 0; j < oldSize; j++) newArr[j] = arrays[idx][j];
free(arrays[idx]);
}
arrays[idx] = newArr;
arraySizes[idx] = newSize;
}
arrays[idx][i] = val;
}
// --- Calculator Functions ---
bool isFunc(String t) {
t.toUpperCase();
if (t == "SIN" || t == "COS" || t == "TAN") return true;
if (t == "ASIN" || t == "ACOS" || t == "ATAN") return true;
if (t == "SIND" || t == "COSD" || t == "TAND") return true;
if (t == "SINH" || t == "COSH" || t == "TANH") return true;
if (t == "ASINH" || t == "ACOSH" || t == "ATANH") return true;
if (t == "SQRT" || t == "LN" || t == "LOG" || t == "EXP") return true;
if (t == "ABS" || t == "FLOOR" || t == "CEIL" || t == "ROUND") return true;
if (t == "RAD" || t == "DEG") return true;
if (t.startsWith("GET_") && t.length() == 5) return true;
return false;
}
int precedence(String op) {
if (isFunc(op)) return 5;
if (op == "^") return 4;
if (op == "*" || op == "/" || op == "%") return 3;
if (op == "+" || op == "-") return 2;
return 0;
}
bool rightAssoc(String op) {
return op == "^";
}
String toPostfix(String expr) {
strClear();
// Preprocess
expr.replace(" ", "");
expr.toUpperCase();
// Handle unary minus/plus
expr.replace("(-", "(0-");
expr.replace("(+", "(0+");
expr.replace("*-", "*(0-");
expr.replace("*+", "*(0+");
expr.replace("/-", "/(0-");
expr.replace("/+", "/(0+");
expr.replace("^-", "^(0-");
expr.replace("^+", "^(0+");
expr.replace("%-", "%(0-");
expr.replace("%+", "%(0+");
expr.replace(",-", ",(0-");
expr.replace(",+", ",(0+");
if (expr.length() > 0 && expr.charAt(0) == '-') expr = "0" + expr;
if (expr.length() > 0 && expr.charAt(0) == '+') expr = expr.substring(1);
String output = "";
int i = 0;
while (i < (int)expr.length()) {
char c = expr.charAt(i);
// Number
if (isDigit(c) || (c == '.' && i + 1 < (int)expr.length() && isDigit(expr.charAt(i+1)))) {
String num = "";
while (i < (int)expr.length() && (isDigit(expr.charAt(i)) || expr.charAt(i) == '.')) {
num += expr.charAt(i);
i++;
}
// Scientific notation
if (i < (int)expr.length() && expr.charAt(i) == 'E') {
if (i + 1 < (int)expr.length() && (isDigit(expr.charAt(i+1)) || expr.charAt(i+1) == '+' || expr.charAt(i+1) == '-')) {
num += expr.charAt(i);
i++;
if (expr.charAt(i) == '+' || expr.charAt(i) == '-') {
num += expr.charAt(i);
i++;
}
while (i < (int)expr.length() && isDigit(expr.charAt(i))) {
num += expr.charAt(i);
i++;
}
}
}
output += num + " ";
continue;
}
// Identifier
if (isAlpha(c)) {
String token = "";
while (i < (int)expr.length() && (isAlpha(expr.charAt(i)) || isDigit(expr.charAt(i)))) {
token += expr.charAt(i);
i++;
}
token.toUpperCase();
if (token == "PI") {
output += String(PI_VAL, 15) + " ";
}
else if (token == "E" && (i >= (int)expr.length() || (!isDigit(expr.charAt(i)) && expr.charAt(i) != '+' && expr.charAt(i) != '-'))) {
output += String(E_VAL, 15) + " ";
}
else if (isFunc(token)) {
strPush(token);
}
else if (token.length() == 1 && isVar(token.charAt(0))) {
// Check for array access
if (i < (int)expr.length() && expr.charAt(i) == '(') {
strPush("GET_" + token);
strPush("(");
i++;
} else {
output += token + " ";
}
}
continue;
}
// Parentheses
if (c == '(') {
strPush("(");
i++;
continue;
}
if (c == ')') {
while (!strEmpty() && strPeek() != "(") {
output += strPop() + " ";
}
if (!strEmpty()) strPop(); // Remove '('
if (!strEmpty() && isFunc(strPeek())) {
output += strPop() + " ";
}
i++;
continue;
}
// Operators
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == '^') {
String op = String(c);
while (!strEmpty() && strPeek() != "(" &&
(precedence(strPeek()) > precedence(op) ||
(precedence(strPeek()) == precedence(op) && !rightAssoc(op)))) {
output += strPop() + " ";
}
strPush(op);
i++;
continue;
}
i++;
}
while (!strEmpty()) {
output += strPop() + " ";
}
output.trim();
return output;
}
double power(double base, double exp) {
if (exp == 0) return 1;
if (base == 0) return (exp > 0) ? 0 : NAN;
if (exp == floor(exp) && exp > 0 && exp <= 100) {
double r = 1;
int e = (int)exp;
while (e > 0) {
if (e & 1) r *= base;
base *= base;
e >>= 1;
}
return r;
}
return pow(base, exp);
}
double evalPostfix(String expr) {
numClear();
int i = 0;
while (i < (int)expr.length()) {
// Find next token
int sp = expr.indexOf(' ', i);
if (sp < 0) sp = expr.length();
String token = expr.substring(i, sp);
i = sp + 1;
if (token.length() == 0) continue;
char first = token.charAt(0);
// Number
if (isDigit(first) || first == '.' ||
(first == '-' && token.length() > 1 && (isDigit(token.charAt(1)) || token.charAt(1) == '.'))) {
numPush(token.toDouble());
continue;
}
// Variable
if (token.length() == 1 && isVar(first)) {
numPush(variables[varIndex(first)]);
continue;
}
// Function
if (isFunc(token)) {
double v = numPop();
double r = 0;
if (token == "SIN") r = radiansMode ? sin(v) : sin(v * PI_VAL / 180);
else if (token == "COS") r = radiansMode ? cos(v) : cos(v * PI_VAL / 180);
else if (token == "TAN") r = radiansMode ? tan(v) : tan(v * PI_VAL / 180);
else if (token == "ASIN") r = radiansMode ? asin(v) : asin(v) * 180 / PI_VAL;
else if (token == "ACOS") r = radiansMode ? acos(v) : acos(v) * 180 / PI_VAL;
else if (token == "ATAN") r = radiansMode ? atan(v) : atan(v) * 180 / PI_VAL;
else if (token == "SIND") r = sin(v * PI_VAL / 180);
else if (token == "COSD") r = cos(v * PI_VAL / 180);
else if (token == "TAND") r = tan(v * PI_VAL / 180);
else if (token == "SINH") r = sinh(v);
else if (token == "COSH") r = cosh(v);
else if (token == "TANH") r = tanh(v);
else if (token == "ASINH") r = asinh(v);
else if (token == "ACOSH") r = acosh(v);
else if (token == "ATANH") r = atanh(v);
else if (token == "SQRT") r = sqrt(v);
else if (token == "LN") r = log(v);
else if (token == "LOG") r = log10(v);
else if (token == "EXP") r = exp(v);
else if (token == "ABS") r = fabs(v);
else if (token == "FLOOR") r = floor(v);
else if (token == "CEIL") r = ceil(v);
else if (token == "ROUND") r = round(v);
else if (token == "RAD") r = v * PI_VAL / 180;
else if (token == "DEG") r = v * 180 / PI_VAL;
else if (token.startsWith("GET_") && token.length() == 5) {
int idx = varIndex(token.charAt(4));
r = getArray(idx, toInt(v));
}
numPush(r);
continue;
}
// Binary operator
double b = numPop();
double a = numPop();
if (token == "+") numPush(a + b);
else if (token == "-") numPush(a - b);
else if (token == "*") numPush(a * b);
else if (token == "/") numPush(b == 0 ? NAN : a / b);
else if (token == "%") numPush(b == 0 ? NAN : fmod(a, b));
else if (token == "^") numPush(power(a, b));
}
return numPop();
}
double evaluate(String expr) {
return evalPostfix(toPostfix(expr));
}
// --- Program Management ---
int findLine(int num) {
for (int i = 0; i < programSize; i++) {
if (lineNumbers[i] == num) return i;
}
return -1;
}
void storeLine(String line) {
line.trim();
int sp = line.indexOf(' ');
if (sp < 0) sp = line.length();
int num = line.substring(0, sp).toInt();
if (num <= 0) return;
String code = (sp < (int)line.length()) ? line.substring(sp + 1) : "";
code.trim();
int idx = findLine(num);
if (idx >= 0) {
// Line exists
if (code.length() == 0) {
// Delete line
for (int j = idx; j < programSize - 1; j++) {
lineNumbers[j] = lineNumbers[j + 1];
programLines[j] = programLines[j + 1];
}
programSize--;
} else {
// Update line
programLines[idx] = code;
}
} else if (code.length() > 0) {
// New line
if (programSize >= MAX_PROGRAM_LINES) {
Serial.println(F("Program memory full!"));
return;
}
// Find insertion point
int pos = 0;
while (pos < programSize && lineNumbers[pos] < num) pos++;
// Shift lines
for (int j = programSize; j > pos; j--) {
lineNumbers[j] = lineNumbers[j - 1];
programLines[j] = programLines[j - 1];
}
lineNumbers[pos] = num;
programLines[pos] = code;
programSize++;
}
}
void newProgram() {
programSize = 0;
loopSP = -1;
state = IDLE;
for (int i = 0; i < 26; i++) {
variables[i] = 0;
if (arrays[i] != NULL) {
free(arrays[i]);
arrays[i] = NULL;
}
arraySizes[i] = 0;
}
}
void listProgram() {
for (int i = 0; i < programSize; i++) {
Serial.print(lineNumbers[i]);
Serial.print(" ");
Serial.println(programLines[i]);
}
}
// --- Execution ---
void execLine(String line, bool inProgram);
void execNextLine() {
if (pc >= programSize) {
state = IDLE;
return;
}
execLine(programLines[pc], true);
}
void execLine(String line, bool inProgram) {
line.trim();
if (line.length() == 0) {
if (inProgram) pc++;
return;
}
String cmd, args;
int sp = line.indexOf(' ');
if (sp >= 0) {
cmd = line.substring(0, sp);
args = line.substring(sp + 1);
} else {
cmd = line;
args = "";
}
cmd.toUpperCase();
args.trim();
// REM
if (cmd == "REM") {
if (inProgram) pc++;
return;
}
// END
if (cmd == "END") {
state = IDLE;
return;
}
// RAD/DEG
if (cmd == "RAD") {
radiansMode = true;
if (!inProgram) Serial.println(F("Radians mode"));
if (inProgram) pc++;
return;
}
if (cmd == "DEG") {
radiansMode = false;
if (!inProgram) Serial.println(F("Degrees mode"));
if (inProgram) pc++;
return;
}
// PREC
if (cmd == "PREC") {
int p = args.toInt();
if (p < 0) p = 0;
if (p > 15) p = 15;
printPrecision = p;
if (!inProgram) {
Serial.print(F("Precision: "));
Serial.println(printPrecision);
}
if (inProgram) pc++;
return;
}
// PRINT
if (cmd == "PRINT") {
while (args.length() > 0) {
int semi = args.indexOf(';');
String token;
bool noNewline = false;
if (semi >= 0) {
token = args.substring(0, semi);
args = args.substring(semi + 1);
noNewline = true;
} else {
token = args;
args = "";
}
token.trim();
if (token.startsWith("\"") && token.endsWith("\"")) {
Serial.print(token.substring(1, token.length() - 1));
} else if (token.length() > 0) {
Serial.print(evaluate(token), printPrecision);
}
if (!noNewline && args.length() == 0) Serial.println();
}
if (inProgram) pc++;
return;
}
// INPUT
if (cmd == "INPUT") {
if (inProgram && args.length() > 0) {
inputVarIndex = varIndex(args.charAt(0));
if (inputVarIndex >= 0) {
state = AWAIT_INPUT;
Serial.print(F("? "));
}
}
return;
}
// GOTO
if (cmd == "GOTO") {
if (inProgram) {
int target = findLine(args.toInt());
if (target >= 0) {
pc = target;
} else {
pc++;
}
}
return;
}
// IF/THEN
if (cmd == "IF") {
if (inProgram) {
String upper = args;
upper.toUpperCase();
int thenPos = upper.indexOf("THEN");
if (thenPos >= 0) {
String cond = args.substring(0, thenPos);
cond.trim();
int targetLine = args.substring(thenPos + 4).toInt();
// Find comparison operator
int opPos = -1;
String op = "";
if ((opPos = cond.indexOf("<=")) >= 0) op = "<=";
else if ((opPos = cond.indexOf(">=")) >= 0) op = ">=";
else if ((opPos = cond.indexOf("<>")) >= 0) op = "<>";
else if ((opPos = cond.indexOf("!=")) >= 0) op = "!=";
else if ((opPos = cond.indexOf("<")) >= 0) op = "<";
else if ((opPos = cond.indexOf(">")) >= 0) op = ">";
else if ((opPos = cond.indexOf("=")) >= 0) op = "=";
if (opPos >= 0) {
double lhs = evaluate(cond.substring(0, opPos));
double rhs = evaluate(cond.substring(opPos + op.length()));
bool result = false;
if (op == "<") result = lhs < rhs;
else if (op == ">") result = lhs > rhs;
else if (op == "<=") result = lhs <= rhs;
else if (op == ">=") result = lhs >= rhs;
else if (op == "=") result = fabs(lhs - rhs) < 0.0001;
else if (op == "<>" || op == "!=") result = fabs(lhs - rhs) >= 0.0001;
if (result) {
int target = findLine(targetLine);
if (target >= 0) {
pc = target;
return;
}
}
}
}
pc++;
}
return;
}
// FOR
if (cmd == "FOR") {
if (inProgram) {
String upper = args;
upper.toUpperCase();
int eqPos = upper.indexOf('=');
int toPos = upper.indexOf("TO");
int stepPos = upper.indexOf("STEP");
if (eqPos >= 0 && toPos >= 0 && args.length() > 0) {
int vi = varIndex(args.charAt(0));
String startExpr = args.substring(eqPos + 1, toPos);
String endExpr, stepExpr;
if (stepPos >= 0) {
endExpr = args.substring(toPos + 2, stepPos);
stepExpr = args.substring(stepPos + 4);
} else {
endExpr = args.substring(toPos + 2);
stepExpr = "1";
}
if (loopSP >= MAX_LOOP_DEPTH - 1) {
Serial.println(F("Loop depth exceeded!"));
state = IDLE;
return;
}
loopSP++;
loopStack[loopSP].varIndex = vi;
loopStack[loopSP].endVal = evaluate(endExpr);
loopStack[loopSP].stepVal = evaluate(stepExpr);
loopStack[loopSP].returnPC = pc;
variables[vi] = evaluate(startExpr);
}
pc++;
}
return;
}
// NEXT
if (cmd == "NEXT") {
if (inProgram && args.length() > 0) {
int vi = varIndex(args.charAt(0));
if (loopSP < 0 || loopStack[loopSP].varIndex != vi) {
Serial.println(F("NEXT without FOR!"));
state = IDLE;
return;
}
variables[vi] += loopStack[loopSP].stepVal;
bool cont;
if (loopStack[loopSP].stepVal >= 0) {
cont = variables[vi] <= loopStack[loopSP].endVal + 0.0001;
} else {
cont = variables[vi] >= loopStack[loopSP].endVal - 0.0001;
}
if (cont) {
pc = loopStack[loopSP].returnPC + 1;
} else {
loopSP--;
pc++;
}
}
return;
}
// DIM
if (cmd == "DIM") {
if (args.length() > 0) {
int vi = varIndex(args.charAt(0));
int lp = args.indexOf('(');
int rp = args.lastIndexOf(')');
if (vi >= 0 && lp >= 0 && rp > lp) {
int size = toInt(evaluate(args.substring(lp + 1, rp)));
if (size < 0) size = 0;
if (size > MAX_ARRAY_SIZE) size = MAX_ARRAY_SIZE;
if (arrays[vi] != NULL) {
free(arrays[vi]);
arrays[vi] = NULL;
}
if (size > 0) {
arrays[vi] = (double*)malloc(size * sizeof(double));
if (arrays[vi] != NULL) {
for (int j = 0; j < size; j++) arrays[vi][j] = 0.0;
arraySizes[vi] = size;
}
} else {
arraySizes[vi] = 0;
}
if (!inProgram) {
Serial.print(F("DIM "));
Serial.print((char)('A' + vi));
Serial.print(F("("));
Serial.print(size);
Serial.println(F(")"));
}
}
}
if (inProgram) pc++;
return;
}
// LET or assignment
String assignLine = line;
String upperLine = line;
upperLine.toUpperCase();
if (upperLine.startsWith("LET ")) {
assignLine = line.substring(4);
assignLine.trim();
}
int eqPos = assignLine.indexOf('=');
if (eqPos > 0) {
String lhs = assignLine.substring(0, eqPos);
String rhs = assignLine.substring(eqPos + 1);
lhs.trim();
rhs.trim();
// Array assignment A(i)=expr
int lp = lhs.indexOf('(');
if (lp >= 0 && lhs.endsWith(")") && lhs.length() > 0 && isVar(lhs.charAt(0))) {
int vi = varIndex(lhs.charAt(0));
int rp = lhs.lastIndexOf(')');
int idx = toInt(evaluate(lhs.substring(lp + 1, rp)));
double val = evaluate(rhs);
setArray(vi, idx, val);
if (!inProgram) {
Serial.print((char)('A' + vi));
Serial.print(F("("));
Serial.print(idx);
Serial.print(F(") = "));
Serial.println(val, printPrecision);
}
}
// Simple variable
else if (lhs.length() == 1 && isVar(lhs.charAt(0))) {
int vi = varIndex(lhs.charAt(0));
double val = evaluate(rhs);
variables[vi] = val;
if (!inProgram) {
Serial.print((char)('A' + vi));
Serial.print(F(" = "));
Serial.println(val, printPrecision);
}
}
if (inProgram) pc++;
return;
}
// Expression evaluation (immediate mode only)
if (!inProgram) {
Serial.println(evaluate(line), printPrecision);
} else {
pc++;
}
}
// --- Input Processing ---
void processInput(String input) {
input.trim();
if (input.length() == 0) return;
// Handle INPUT statement response
if (state == AWAIT_INPUT && inputVarIndex >= 0) {
variables[inputVarIndex] = evaluate(input);
inputVarIndex = -1;
state = RUNNING;
pc++;
return;
}
// Handle backslash-separated lines
int pos = 0;
while (pos < (int)input.length()) {
int slash = input.indexOf('\\', pos);
if (slash < 0) slash = input.length();
String segment = input.substring(pos, slash);
segment.trim();
if (segment.length() > 0) {
String upper = segment;
upper.toUpperCase();
if (upper == "RUN") {
pc = 0;
state = RUNNING;
loopSP = -1;
}
else if (upper == "LIST") {
listProgram();
}
else if (upper == "NEW") {
newProgram();
Serial.println(F("OK"));
}
else if (upper == "BREAK") {
state = IDLE;
loopSP = -1;
Serial.println(F("*** BREAK ***"));
}
else if (isProgramLine(segment)) {
storeLine(segment);
}
else {
execLine(segment, false);
}
}
pos = slash + 1;
}
}
// --- Setup & Loop ---
void setup() {
Serial.begin(9600);
while (!Serial) delay(10);
for (int i = 0; i < 26; i++) {
arrays[i] = NULL;
arraySizes[i] = 0;
}
newProgram();
Serial.println(F("\n=== ESP32 BASIC Calculator ==="));
Serial.println(F("Commands: RUN LIST NEW BREAK"));
Serial.println(F("Use \\ to paste multiple lines"));
Serial.println(F("==============================\n"));
Serial.print(F("> "));
}
void loop() {
// Execute program
if (state == RUNNING) {
execNextLine();
}
// Handle serial input
while (Serial.available()) {
char c = Serial.read();
// Backslash = line separator
if (c == '\\') {
if (inputBuffer.length() > 0) {
Serial.println();
processInput(inputBuffer);
inputBuffer = "";
}
continue;
}
// Enter
if (c == '\r' || c == '\n') {
if (inputBuffer.length() > 0) {
Serial.println();
processInput(inputBuffer);
inputBuffer = "";
}
if (state == IDLE) Serial.print(F("> "));
continue;
}
// Backspace
if (c == '\b' || c == 127) {
if (inputBuffer.length() > 0) {
inputBuffer.remove(inputBuffer.length() - 1);
Serial.print(F("\b \b"));
}
continue;
}
// Printable character
if (isPrintable(c)) {
inputBuffer += c;
Serial.print(c);
}
}
}