From 68384fc34e2d5974f7dc305fdc6a8a94c3e22c10 Mon Sep 17 00:00:00 2001 From: JustScott Date: Sat, 13 Jan 2024 08:34:25 -0600 Subject: [PATCH] Add a new Calculator App InfiniTime PR #375 "Calculator App" Co-authored-by: Raupinger Co-authored-by: Florian Updated to 1.14 by: JustScott --- doc/Calculator.md | 8 + src/CMakeLists.txt | 2 + src/displayapp/DisplayApp.cpp | 1 + src/displayapp/apps/Apps.h.in | 1 + src/displayapp/apps/CMakeLists.txt | 1 + src/displayapp/fonts/fonts.json | 2 +- src/displayapp/screens/Calculator.cpp | 394 ++++++++++++++++++++++++++ src/displayapp/screens/Calculator.h | 43 +++ src/displayapp/screens/Symbols.h | 1 + 9 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 doc/Calculator.md create mode 100644 src/displayapp/screens/Calculator.cpp create mode 100644 src/displayapp/screens/Calculator.h diff --git a/doc/Calculator.md b/doc/Calculator.md new file mode 100644 index 00000000..5857fe85 --- /dev/null +++ b/doc/Calculator.md @@ -0,0 +1,8 @@ +# Calculator Manual +This is a simple Calculator with support for the four basic arithmetic operations, parenthesis and exponents. +Here is what you need to know to make full use of it: +- Swipe left to access parenthesis and exponents +- A long tap on the screen will reset the text field to `0`. +- If the entered term is invalid, the watch will vibrate. +- results are rounded to 4 digits after the decimal point +- **TIP:** you can use `^(1/2)` to calculate square roots diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0a97a015..a281fcc8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -390,6 +390,7 @@ list(APPEND SOURCE_FILES displayapp/screens/CheckboxList.cpp displayapp/screens/BatteryInfo.cpp displayapp/screens/Steps.cpp + displayapp/screens/Calculator.cpp displayapp/screens/Timer.cpp displayapp/screens/Dice.cpp displayapp/screens/PassKey.cpp @@ -610,6 +611,7 @@ set(INCLUDE_FILES displayapp/screens/HeartRate.h displayapp/screens/Metronome.h displayapp/screens/Motion.h + displayapp/screens/Calculator.h displayapp/screens/Timer.h displayapp/screens/Dice.h displayapp/screens/Alarm.h diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index b1594f19..57be7c24 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -23,6 +23,7 @@ #include "displayapp/screens/SystemInfo.h" #include "displayapp/screens/Tile.h" #include "displayapp/screens/Twos.h" +#include "displayapp/screens/Calculator.h" #include "displayapp/screens/FlashLight.h" #include "displayapp/screens/BatteryInfo.h" #include "displayapp/screens/Steps.h" diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 6b4a8b1c..4133a95b 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -26,6 +26,7 @@ namespace Pinetime { StopWatch, Metronome, Motion, + Calculator, Steps, Dice, Weather, diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 60c5c0a5..6e56d9b6 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -14,6 +14,7 @@ else () set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Weather") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Calculator") #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Motion") set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware") endif () diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 41c383c0..0fe49738 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -7,7 +7,7 @@ }, { "file": "FontAwesome5-Solid+Brands+Regular.woff", - "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743" + "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf743, 0xf1ec" } ], "bpp": 1, diff --git a/src/displayapp/screens/Calculator.cpp b/src/displayapp/screens/Calculator.cpp new file mode 100644 index 00000000..5611e5b4 --- /dev/null +++ b/src/displayapp/screens/Calculator.cpp @@ -0,0 +1,394 @@ +#include "Calculator.h" +#include +#include +#include +#include +#include +#include + +using namespace Pinetime::Applications::Screens; + +// Anonymous Namespace for all the structs +namespace { + struct CalcTreeNode { + virtual double calculate() = 0; + }; + + struct NumNode : CalcTreeNode { + double value; + + double calculate() override { + return value; + }; + }; + + struct BinOp : CalcTreeNode { + std::shared_ptr left; + std::shared_ptr right; + + char op; + + double calculate() override { + // make sure we have actual numbers + if (!right || !left) { + errno = EINVAL; + return 0.0; + } + + double rightVal = right->calculate(); + double leftVal = left->calculate(); + switch (op) { + case '^': + // detect overflow + if (log2(leftVal) + rightVal > 31) { + errno = ERANGE; + return 0.0; + } + return pow(leftVal, rightVal); + case 'x': + // detect over/underflowflow + if ((DBL_MAX / abs(rightVal)) < abs(leftVal)) { + errno = ERANGE; + return 0.0; + } + return leftVal * rightVal; + case '/': + // detect under/overflow + if ((DBL_MAX * abs(rightVal)) < abs(leftVal)) { + errno = ERANGE; + return 0.0; + } + // detect divison by zero + if (rightVal == 0.0) { + errno = EDOM; + return 0.0; + } + return leftVal / rightVal; + case '+': + // detect overflow + if ((DBL_MAX - rightVal) < leftVal) { + errno = ERANGE; + return 0.0; + } + return leftVal + rightVal; + case '-': + // detect underflow + if ((DBL_MIN + rightVal) > leftVal) { + errno = ERANGE; + return 0.0; + } + return leftVal - rightVal; + } + errno = EINVAL; + return 0.0; + }; + }; + + uint8_t getPrecedence(char op) { + switch (op) { + case '^': + return 4; + case 'x': + case '/': + return 3; + case '+': + case '-': + return 2; + } + return 0; + } + + bool leftAssociative(char op) { + switch (op) { + case '^': + return false; + case 'x': + case '/': + case '+': + case '-': + return true; + } + return false; + } + +} + +static void eventHandler(lv_obj_t* obj, lv_event_t event) { + auto calc = static_cast(obj->user_data); + calc->OnButtonEvent(obj, event); +} + +Calculator::~Calculator() { + lv_obj_clean(lv_scr_act()); +} + +static const char* buttonMap1[] = { + "7", "8", "9", "/", "\n", + "4", "5", "6", "x", "\n", + "1", "2", "3", "-", "\n", + ".", "0", "=", "+", "", +}; + +static const char* buttonMap2[] = { + "7", "8", "9", "(", "\n", + "4", "5", "6", ")", "\n", + "1", "2", "3", "^", "\n", + ".", "0", "=", "+", "", +}; + +Calculator::Calculator(Controllers::MotorController& motorController) : motorController {motorController} { + result = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_long_mode(result, LV_LABEL_LONG_BREAK); + lv_label_set_text(result, "0"); + lv_obj_set_size(result, 180, 60); + lv_obj_set_pos(result, 0, 0); + + returnButton = lv_btn_create(lv_scr_act(), nullptr); + lv_obj_set_size(returnButton, 52, 52); + lv_obj_set_pos(returnButton, 186, 0); + lv_obj_t* returnLabel; + returnLabel = lv_label_create(returnButton, nullptr); + lv_label_set_text(returnLabel, "<="); + lv_obj_align(returnLabel, nullptr, LV_ALIGN_CENTER, 0, 0); + returnButton->user_data = this; + lv_obj_set_event_cb(returnButton, eventHandler); + + buttonMatrix = lv_btnmatrix_create(lv_scr_act(), nullptr); + lv_btnmatrix_set_map(buttonMatrix, buttonMap1); + lv_obj_set_size(buttonMatrix, 240, 180); + lv_obj_set_pos(buttonMatrix, 0, 60); + lv_obj_set_style_local_pad_all(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 0); + buttonMatrix->user_data = this; + lv_obj_set_event_cb(buttonMatrix, eventHandler); +} + +void Calculator::eval() { + std::stack input {}; + for (int8_t i = position - 1; i >= 0; i--) { + input.push(text[i]); + } + std::stack> output {}; + std::stack operators {}; + bool expectingNumber = true; + int8_t sign = +1; + while (!input.empty()) { + if (input.top() == '.') { + input.push('0'); + } + if (isdigit(input.top())) { + char numberStr[31]; + uint8_t strln = 0; + uint8_t pointpos = 0; + while (!input.empty() && (isdigit(input.top()) || input.top() == '.')) { + if (input.top() == '.') { + if (pointpos != 0) { + motorController.RunForDuration(10); + return; + } + pointpos = strln; + } else { + numberStr[strln] = input.top(); + strln++; + } + input.pop(); + } + // replacement for strtod() since using that increased .txt by 76858 bzt + if (pointpos == 0) { + pointpos = strln; + } + double num = 0; + for (uint8_t i = 0; i < pointpos; i++) { + num += (numberStr[i] - '0') * pow(10, pointpos - i - 1); + } + for (uint8_t i = 0; i < strln - pointpos; i++) { + num += (numberStr[i + pointpos] - '0') / pow(10, i + 1); + } + + auto number = std::make_shared(); + number->value = sign * num; + output.push(number); + + sign = +1; + expectingNumber = false; + continue; + } + + if (expectingNumber && input.top() == '+') { + input.pop(); + continue; + } + if (expectingNumber && input.top() == '-') { + sign *= -1; + input.pop(); + continue; + } + + char next = input.top(); + input.pop(); + + switch (next) { + case '+': + case '-': + case '/': + case 'x': + case '^': + // while ((there is an operator at the top of the operator stack) + while (!operators.empty() + // and (the operator at the top of the operator stack is not a left parenthesis)) + && operators.top() != '(' + // and ((the operator at the top of the operator stack has greater precedence) + && (getPrecedence(operators.top()) > getPrecedence(next) + // or (the operator at the top of the operator stack has equal precedence and the token is left associative)) + || (getPrecedence(operators.top()) == getPrecedence(next) && leftAssociative(next)))) { + // need two elements on the output stack to add a binary operator + if (output.size() < 2) { + motorController.RunForDuration(10); + return; + } + auto node = std::make_shared(); + node->right = output.top(); + output.pop(); + node->left = output.top(); + output.pop(); + node->op = operators.top(); + operators.pop(); + output.push(node); + } + operators.push(next); + expectingNumber = true; + break; + case '(': + // we expect there to be a binary operator here but found a left parenthesis. this occurs in terms like this: a+b(c). This should be + // interpreted as a+b*(c) + if (!expectingNumber) { + operators.push('x'); + } + operators.push(next); + expectingNumber = true; + break; + case ')': + while (operators.top() != '(') { + // need two elements on the output stack to add a binary operator + if (output.size() < 2) { + motorController.RunForDuration(10); + return; + } + auto node = std::make_shared(); + node->right = output.top(); + output.pop(); + node->left = output.top(); + output.pop(); + node->op = operators.top(); + operators.pop(); + output.push(node); + if (operators.empty()) { + motorController.RunForDuration(10); + return; + } + } + // discard the left parentheses + operators.pop(); + } + } + while (!operators.empty()) { + char op = operators.top(); + if (op == ')' || op == '(') { + motorController.RunForDuration(10); + return; + } + // need two elements on the output stack to add a binary operator + if (output.size() < 2) { + motorController.RunForDuration(10); + return; + } + auto node = std::make_shared(); + node->right = output.top(); + output.pop(); + node->left = output.top(); + output.pop(); + node->op = op; + operators.pop(); + output.push(node); + } + // perform the calculation + errno = 0; + double resultFloat = output.top()->calculate(); + if (errno != 0) { + motorController.RunForDuration(10); + return; + } + // make sure the result fits in a 32 bit int + if (INT32_MAX < resultFloat || INT32_MIN > resultFloat) { + motorController.RunForDuration(10); + return; + } + // weird workaround because sprintf crashes when trying to use a float + int32_t upper = resultFloat; + int32_t lower = round(std::abs(resultFloat - upper) * 10000); + // round up to the next int value + if (lower >= 10000) { + lower = 0; + upper++; + } + // see if decimal places have to be printed + if (lower != 0) { + if (upper == 0 && resultFloat < 0) { + position = sprintf(text, "-%ld.%ld", upper, lower); + } else { + position = sprintf(text, "%ld.%ld", upper, lower); + } + // remove extra zeros + while (text[position - 1] == '0') { + position--; + } + } else { + position = sprintf(text, "%ld", upper); + } +} + +void Calculator::OnButtonEvent(lv_obj_t* obj, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + if (obj == buttonMatrix) { + const char* buttonstr = lv_btnmatrix_get_active_btn_text(obj); + if (*buttonstr == '=') { + eval(); + } else { + if (position >= 30) { + motorController.RunForDuration(10); + return; + } + text[position] = *buttonstr; + position++; + } + } else if (obj == returnButton) { + if (position > 1) { + + position--; + } else { + position = 0; + lv_label_set_text(result, "0"); + return; + } + } + + text[position] = '\0'; + lv_label_set_text(result, text); + } +} + +bool Calculator::OnTouchEvent(Pinetime::Applications::TouchEvents event) { + if (event == Pinetime::Applications::TouchEvents::LongTap) { + position = 0; + lv_label_set_text(result, "0"); + return true; + } + if (event == Pinetime::Applications::TouchEvents::SwipeLeft) { + lv_btnmatrix_set_map(buttonMatrix, buttonMap2); + return true; + } + if (event == Pinetime::Applications::TouchEvents::SwipeRight) { + lv_btnmatrix_set_map(buttonMatrix, buttonMap1); + return true; + } + return false; +} diff --git a/src/displayapp/screens/Calculator.h b/src/displayapp/screens/Calculator.h new file mode 100644 index 00000000..d97f81f0 --- /dev/null +++ b/src/displayapp/screens/Calculator.h @@ -0,0 +1,43 @@ +#pragma once + +#include "Screen.h" +#include "Symbols.h" +#include "components/motor/MotorController.h" +#include "systemtask/SystemTask.h" +#include +#include + +namespace Pinetime::Applications { + namespace Screens { + class Calculator : public Screen { + public: + ~Calculator() override; + + Calculator(Controllers::MotorController& motorController); + + void OnButtonEvent(lv_obj_t* obj, lv_event_t event); + + bool OnTouchEvent(Pinetime::Applications::TouchEvents event) override; + + private: + lv_obj_t *result, *returnButton, *buttonMatrix; + + char text[31]; + uint8_t position = 0; + + void eval(); + + Controllers::MotorController& motorController; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Calculator; + static constexpr const char* icon = Screens::Symbols::calculator; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Calculator(controllers.motorController); + }; + }; +} diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index bd958b28..f3b098fc 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -39,6 +39,7 @@ namespace Pinetime { static constexpr const char* eye = "\xEF\x81\xAE"; static constexpr const char* home = "\xEF\x80\x95"; static constexpr const char* sleep = "\xEE\xBD\x84"; + static constexpr const char* calculator = "\xEF\x87\xAC"; // fontawesome_weathericons.c // static constexpr const char* sun = "\xEF\x86\x85";