From 7f08e2c63608185b741fd5270a0aee4bd225f140 Mon Sep 17 00:00:00 2001 From: Raupinger Date: Tue, 23 Feb 2021 09:01:58 +0100 Subject: Add Calculator screen as new App A calculator based on the Shunting-yard algorithm as described here: https://en.wikipedia.org/wiki/Shunting-yard_algorithm Moving the Motion App into the Settings screen to use the tile in the ApplicationList for the Calculator. diff --git a/doc/Calculator.md b/doc/Calculator.md new file mode 100644 index 0000000..2184e06 --- /dev/null +++ b/doc/Calculator.md @@ -0,0 +1,9 @@ +# 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: + ![](./ui/calc2.jpg) +- 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/doc/ui/calc2.jpg b/doc/ui/calc2.jpg new file mode 100644 index 0000000..7f04a80 Binary files /dev/null and b/doc/ui/calc2.jpg differ diff --git a/doc/ui/calc3.jpg b/doc/ui/calc3.jpg new file mode 100644 index 0000000..ac65c9f Binary files /dev/null and b/doc/ui/calc3.jpg differ diff --git a/doc/ui/calc4.jpg b/doc/ui/calc4.jpg new file mode 100644 index 0000000..8069054 Binary files /dev/null and b/doc/ui/calc4.jpg differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f8715e5..49b3845 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -399,6 +399,7 @@ list(APPEND SOURCE_FILES displayapp/screens/List.cpp displayapp/screens/BatteryInfo.cpp displayapp/screens/Steps.cpp + displayapp/screens/Calculator.cpp displayapp/screens/Timer.cpp displayapp/screens/PassKey.cpp displayapp/screens/Error.cpp @@ -607,6 +608,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/Jumpscore.h displayapp/screens/Alarm.h diff --git a/src/displayapp/Apps.h b/src/displayapp/Apps.h index 4f9fdcc..e1f86ce 100644 --- a/src/displayapp/Apps.h +++ b/src/displayapp/Apps.h @@ -40,6 +40,7 @@ namespace Pinetime { SettingChimes, SettingShakeThreshold, SettingBluetooth, + Calculator, Error }; } diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 376f73a..a8d6281 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -25,6 +25,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" @@ -481,6 +482,9 @@ void DisplayApp::LoadApp(Apps app, DisplayApp::FullRefreshDirections direction) case Apps::Steps: currentScreen = std::make_unique(this, motionController, settingsController); break; + case Apps::Calculator: + currentScreen = std::make_unique(this, motorController); + break; } currentApp = app; } diff --git a/src/displayapp/screens/ApplicationList.h b/src/displayapp/screens/ApplicationList.h index d2ede1b..1640227 100644 --- a/src/displayapp/screens/ApplicationList.h +++ b/src/displayapp/screens/ApplicationList.h @@ -48,6 +48,7 @@ namespace Pinetime { {Symbols::map, Apps::Navigation}, {Symbols::drum, Apps::Jumpscore}, + {Symbols::calculator, Apps::Calculator}, }; std::array, ((std::size(list) + appsPerScreen - 1) / appsPerScreen)> r{};; int idx = 0; diff --git a/src/displayapp/screens/Calculator.cpp b/src/displayapp/screens/Calculator.cpp new file mode 100644 index 0000000..b7a3dfe --- /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(DisplayApp* app, Controllers::MotorController& motorController) : Screen(app), 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 0000000..25c4456 --- /dev/null +++ b/src/displayapp/screens/Calculator.h @@ -0,0 +1,37 @@ + +#pragma once + + +#include "Screen.h" +#include "components/motor/MotorController.h" +#include +#include + +namespace Pinetime { + namespace Applications { + namespace Screens { + + class Calculator : public Screen { + public: + ~Calculator() override; + + Calculator(DisplayApp* app, 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; + }; + + } + } +} diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index f973181..d4f80d1 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -37,6 +37,7 @@ namespace Pinetime { static constexpr const char* chartLine = "\xEF\x88\x81"; static constexpr const char* eye = "\xEF\x81\xAE"; static constexpr const char* home = "\xEF\x80\x95"; + static constexpr const char* calculator = "\xEF\x87\xAC"; // lv_font_sys_48.c static constexpr const char* settings = "\xEE\xA4\x82"; // e902 -- cgit v0.10.2