diff options
| author | Raupinger <fgrauper@gmail.com> | 2021-02-23 08:01:58 (GMT) |
|---|---|---|
| committer | Michele Bini <michele.bini@gmail.com> | 2022-06-11 01:09:51 (GMT) |
| commit | cfb04a6609b013c2c5547d8b404b7ecfaaaf8259 (patch) | |
| tree | 53f5f055cfa423d63d5b4272eb6a3ad53a21acfc | |
| parent | 9d982cea5b7dc616088646f97b7e0d424860f40b (diff) | |
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.
| -rw-r--r-- | doc/Calculator.md | 9 | ||||
| -rw-r--r-- | doc/ui/calc2.jpg | bin | 0 -> 985632 bytes | |||
| -rw-r--r-- | doc/ui/calc3.jpg | bin | 0 -> 141874 bytes | |||
| -rw-r--r-- | doc/ui/calc4.jpg | bin | 0 -> 131313 bytes | |||
| -rw-r--r-- | src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/displayapp/Apps.h | 1 | ||||
| -rw-r--r-- | src/displayapp/DisplayApp.cpp | 4 | ||||
| -rw-r--r-- | src/displayapp/screens/ApplicationList.h | 4 | ||||
| -rw-r--r-- | src/displayapp/screens/Calculator.cpp | 394 | ||||
| -rw-r--r-- | src/displayapp/screens/Calculator.h | 37 |
10 files changed, 450 insertions, 1 deletions
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: +  +- 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 Binary files differnew file mode 100644 index 0000000..7f04a80 --- /dev/null +++ b/doc/ui/calc2.jpg diff --git a/doc/ui/calc3.jpg b/doc/ui/calc3.jpg Binary files differnew file mode 100644 index 0000000..ac65c9f --- /dev/null +++ b/doc/ui/calc3.jpg diff --git a/doc/ui/calc4.jpg b/doc/ui/calc4.jpg Binary files differnew file mode 100644 index 0000000..8069054 --- /dev/null +++ b/doc/ui/calc4.jpg diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6a729c8..474e1ee 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -401,6 +401,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 @@ -611,6 +612,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 7e8ef8f..36e17a4 100644 --- a/src/displayapp/Apps.h +++ b/src/displayapp/Apps.h @@ -41,6 +41,7 @@ namespace Pinetime { SettingChimes, SettingShakeThreshold, SettingBluetooth, + Calculator, Error }; } diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 2ec77c3..9a2e8dd 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" @@ -470,6 +471,9 @@ void DisplayApp::LoadApp(Apps app, DisplayApp::FullRefreshDirections direction) case Apps::Steps: currentScreen = std::make_unique<Screens::Steps>(this, motionController, settingsController); break; + case Apps::Calculator: + currentScreen = std::make_unique<Screens::Calculator>(this, motorController); + break; } currentApp = app; } diff --git a/src/displayapp/screens/ApplicationList.h b/src/displayapp/screens/ApplicationList.h index 84290ca..54a68a0 100644 --- a/src/displayapp/screens/ApplicationList.h +++ b/src/displayapp/screens/ApplicationList.h @@ -45,7 +45,9 @@ namespace Pinetime { {Symbols::drum, Apps::Metronome}, {Symbols::paintbrush, Apps::Paint}, {Symbols::paddle, Apps::Paddle}, - {"2", Apps::Twos} + {"2", Apps::Twos}, + + {Symbols::calculator, Apps::Calculator}, }; std::array<std::array<Tile::Applications, appsPerScreen>, ((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 <string> +#include <stack> +#include <cfloat> +#include <cmath> +#include <map> +#include <memory> + +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<CalcTreeNode> left; + std::shared_ptr<CalcTreeNode> 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<Calculator*>(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<char> input {}; + for (int8_t i = position - 1; i >= 0; i--) { + input.push(text[i]); + } + std::stack<std::shared_ptr<CalcTreeNode>> output {}; + std::stack<char> 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<NumNode>(); + 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<BinOp>(); + 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<BinOp>(); + 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<BinOp>(); + 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 <array> +#include <string> + +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; + }; + + } + } +} |
