Add a new Calculator App
InfiniTime PR #375 "Calculator App" Co-authored-by: Raupinger <fgrauper@gmail.com> Co-authored-by: Florian <florian@florian-thinkpad.local> Updated to 1.14 by: JustScott
This commit is contained in:
parent
bf786dac8e
commit
68384fc34e
9 changed files with 452 additions and 1 deletions
8
doc/Calculator.md
Normal file
8
doc/Calculator.md
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -26,6 +26,7 @@ namespace Pinetime {
|
|||
StopWatch,
|
||||
Metronome,
|
||||
Motion,
|
||||
Calculator,
|
||||
Steps,
|
||||
Dice,
|
||||
Weather,
|
||||
|
|
|
@ -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 ()
|
||||
|
|
|
@ -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,
|
||||
|
|
394
src/displayapp/screens/Calculator.cpp
Normal file
394
src/displayapp/screens/Calculator.cpp
Normal file
|
@ -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(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<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;
|
||||
}
|
43
src/displayapp/screens/Calculator.h
Normal file
43
src/displayapp/screens/Calculator.h
Normal file
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include "Screen.h"
|
||||
#include "Symbols.h"
|
||||
#include "components/motor/MotorController.h"
|
||||
#include "systemtask/SystemTask.h"
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
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<Apps::Calculator> {
|
||||
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);
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue