InfiniTimeOrg / InfiniTime

Firmware for Pinetime smartwatch written in C++ and based on FreeRTOS
GNU General Public License v3.0
2.75k stars 941 forks source link

Simple calculator #1483

Open minacode opened 1 year ago

minacode commented 1 year ago

This PR implements a simple fixed-point calculator. In contrast to #375 it aims to be simpler by not parsing whole equations but just evaluating one operation at a time over an accumulated result.

It takes an operation and a value (digit-wise) and applies the value to the result with the chosen operation whenever a new operation is chosen or = is pressed.

=: evaluate current input <: removes the input charwise or reset the result if the input is zero. (-): toggle the sign of the current input (thanks @FintasticMan) + - / * /: press the button to cycle through the operators and "no-operator".

It uses fixed-point numbers and checks for errors like too big results and zero division.

Known issues:

from @yusufmte's comment below:

joseph58tech commented 1 year ago

I feel like the original design from #375 was simpler and cleaner, the multifunctional +/- and */ buttons might be too much of a hassle for regular use.

FintasticMan commented 1 year ago

About the printing of the value, I think that doing something like what's done here should be fine: https://github.com/InfiniTimeOrg/InfiniTime/blob/361e381ac32ff7a05abeaaf1e85eb10818193f64/src/displayapp/screens/BatteryInfo.cpp#L38 Edit: looking at the code, seems like you're already using something like this.

I would say that it would be better to use fixed width integers instead of long ints, so that it's obvious what the min and max values are.

minacode commented 1 year ago

@joseph58tech, you mean the design with two keyboard layers?

@FintasticMan, I can change long int to int64_t, but the format string will still contain %ld which means "long decimal".

FintasticMan commented 1 year ago

You can either leave the format string as "%ld", or you can use fixed width format literals. This would mean that the format string would become "%" PRId64. To use those you need to include the <cinttypes> header. It's worth mentioning that it seems like in this codebase printing fixed width integers is usually just done with normal format specifiers.

yusufmte commented 1 year ago

This looks amazing, thanks for making it! The UI looks really nicely designed with a good balance of button size and ability to do most of the major operations you would need. Personally I prefer the simplicity of this app though I see advantages in both. I also love the inclusion of ^ and using the same button for +- and */ to save space.

I would love to one of the calculator apps merged in an upcoming release and I'll probably be using it on my PT until then. It feels like the biggest thing currently "missing" from the PineTime to me. Along with the usefulness of having a calculator close by, a small part of that is probably from reminiscing about these calculator watches from childhood, haha.

image

minacode commented 1 year ago

Thank you! 😊 Nice to hear that you want to try it already. This may help with finding any eventual bugs. I haven't tried the last two commits on my watch because I have another test build running right now.

FintasticMan commented 1 year ago

Looks good, I've tried this on my watch and it works quite nicely. I do think that it is better to use floats rather than doubles for the pow calculation. Why did you switch to doubles?

Ceimour commented 1 year ago

Thanks! I thought I had worked through all the replacements of float for double. Where did you find a double?

From: @.> Sent: Monday, December 12, 2022 4:44 PM To: @.> Cc: @.***> Subject: Re: [InfiniTimeOrg/InfiniTime] Simple calculator (PR #1483)

Looks good, I've tried this on my watch and it works quite nicely. I do think that it is better to use floats rather than doubles for the pow calculation. Why did you switch to doubles?

β€” Reply to this email directly, view it on GitHubhttps://github.com/InfiniTimeOrg/InfiniTime/pull/1483#issuecomment-1347444920, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3C6AGUV6STVDJILC6TNB23WM6TD3ANCNFSM6AAAAAASYQQYU4. You are receiving this because you are subscribed to this thread.Message ID: @.***>

minacode commented 1 year ago

I thought that double matches the 64-bit integers better than float. Is that correct? πŸ€” The workaround is necessary because I wanted to have a power function and could not find one the works with the fixed point numbers. Speed should be no concern for one calculation.

Ceimour commented 1 year ago

I switched to floats instead of doubles to see if the compiler would make use of the FPU. And I was very pleased to see big performance increase (something like 10x). So I assume the FPU is in use.

From: Max @.> Sent: Monday, December 12, 2022 5:22 PM To: @.> Cc: @.>; @.> Subject: Re: [InfiniTimeOrg/InfiniTime] Simple calculator (PR #1483)

I thought that double matches the 64-bit integers better than float. Is that correct? πŸ€” The workaround is necessary because I wanted to have a power function and could not find one the works with the fixed point numbers. Speed should be no concern for one calculation.

β€” Reply to this email directly, view it on GitHubhttps://github.com/InfiniTimeOrg/InfiniTime/pull/1483#issuecomment-1347483769, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3C6AGUHY7CZYVJSGQM3CE3WM6XR7ANCNFSM6AAAAAASYQQYU4. You are receiving this because you commented.Message ID: @.***>

Ceimour commented 1 year ago

Oh, and the datasheet specifies that the FPU is restricted to float.

From: Max @.> Sent: Monday, December 12, 2022 5:22 PM To: @.> Cc: @.>; @.> Subject: Re: [InfiniTimeOrg/InfiniTime] Simple calculator (PR #1483)

I thought that double matches the 64-bit integers better than float. Is that correct? πŸ€” The workaround is necessary because I wanted to have a power function and could not find one the works with the fixed point numbers. Speed should be no concern for one calculation.

β€” Reply to this email directly, view it on GitHubhttps://github.com/InfiniTimeOrg/InfiniTime/pull/1483#issuecomment-1347483769, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3C6AGUHY7CZYVJSGQM3CE3WM6XR7ANCNFSM6AAAAAASYQQYU4. You are receiving this because you commented.Message ID: @.***>

minacode commented 1 year ago

Ok, I will test both types on the watch. Thank you for the input!

Ceimour commented 1 year ago

So sorry, I thought this was related to my recent pull request and responded in that context. Anyway, you might try powf() with floats.

From: Max @.> Sent: Monday, December 12, 2022 6:27 PM To: @.> Cc: @.>; @.> Subject: Re: [InfiniTimeOrg/InfiniTime] Simple calculator (PR #1483)

Ok, I will test both types on the watch. Thank you for the input!

β€” Reply to this email directly, view it on GitHubhttps://github.com/InfiniTimeOrg/InfiniTime/pull/1483#issuecomment-1347570614, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3C6AGQO2EACQDE7LOS6PI3WM67FHANCNFSM6AAAAAASYQQYU4. You are receiving this because you commented.Message ID: @.***>

Ceimour commented 1 year ago

I apologize for any confusion, I thought this was related to a pull request I made.

From: @.> Sent: Monday, December 12, 2022 4:44 PM To: @.> Cc: @.***> Subject: Re: [InfiniTimeOrg/InfiniTime] Simple calculator (PR #1483)

Looks good, I've tried this on my watch and it works quite nicely. I do think that it is better to use floats rather than doubles for the pow calculation. Why did you switch to doubles?

β€” Reply to this email directly, view it on GitHubhttps://github.com/InfiniTimeOrg/InfiniTime/pull/1483#issuecomment-1347444920, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A3C6AGUV6STVDJILC6TNB23WM6TD3ANCNFSM6AAAAAASYQQYU4. You are receiving this because you are subscribed to this thread.Message ID: @.***>

FintasticMan commented 1 year ago

I thought that double matches the 64-bit integers better than float. Is that correct? :thinking:

While doubles are 64 bits wide, that doesn't make a difference for int > float and float > int conversion. 32-bit floats will definitely have enough precision for this calculation, and as Ceimour already said, the FPU in the nRF 52832 is for 32-bit floats only, so double calculations will be done much less efficiently.

minacode commented 1 year ago

This says that there are 9 digits for float and 17 for double. 9 is not enough. That is only 6 integer digits.

CCF100 commented 1 year ago

Hey @minacode, if the variable used to store the calculated value overflows, the watch reboots...

minacode commented 1 year ago

Thank you for testing this! It doesn't happen in the simulator so I wonder what makes the watch crash. Does it happen with every operation or only with the power function?

Edit: I tested the last changes on the watch and it seems like a lot broke. Maybe we have to revert some of the changes.

yusufmte commented 1 year ago

@minacode : I've been using the calculator for a little while now. Overall for most calculations it's working great, doing just what it's supposed to, and has been very useful-- gonna keep using it on my build and I would love to see it included in an upcoming milestone!

Basically it has worked flawlessly for me with any small integer operations (small meaning anything not huge, like less than 10^12). Deviating from small integers, there are a few bugs to iron out:

  1. Bug with crashing on large operations. As @CCF100 mentioned-- try feeding the calculator a very large operation (eg large number ^ large number), and InfiniTime will crash and reboot. I do agree this is probably because of overflow, as this consistently this happens with 2^54 but not with 2^53. It is not just the power function -- the same crash happens when reaching similar magnitudes by repeatedly doing 10*10. Suggested behavior: when an operation is about to cause overflow, the result label should just say "TOO LARGE." This would be fine, as in daily use most people wouldn't need to conduct operations that large.

  2. Bug when typing a number larger than the interface allows. Try repeatedly tapping "9" (or any digit)-- eventually the input label will start cycling between a jumble of random numbers, and as you keep tapping it, eventually it will show "-1". Not sure exactly what causes this. Suggested behavior: When the current number in the input field fills the field (looks like that happens as 12 digits), further digits input should be ignored, and some feedback should be sent to the user to indicate that this happened (eg short vibration as if to say "Nope, no more digits allowed.").

  3. Bug with any use of the decimal point. Try using the . (for example, type 5.1) and the number shown in the input field would be different (for 5.1, you would see 5.15663343). Not sure exactly what is causing this, but it has been consistent for me. However, the number actually stored in the input field is what you typed (5.1). For example, try 49^0.5 -- the result will, appropriately, be 7, even though the operation will look like you are doing 49^0.15663343.

  4. Bug where if the result exceeds 12 digits, you will only see the last 12. For example, if the result is 1234567890123, you will only see 234567890123. Suggested behavior: Either (a) the result label just says "TOO LARGE" if the result exceeds 12 digits, or (b) use scientific notation with E to denote numbers larger than 12 digits (for example, the result above would display as 1.2345678E12). I think solution (a) would be just fine, but solution (b) would ultimately be ideal.

  5. Bug when pressing ^, sometimes it will repeat whichever number you last placed. This one is tricky as it is not consistent, but when it starts happening (and I'm not quite sure what triggers it to start happening), it continues to happen consistently until the PineTime reboots. When pressing ^, instead of changing the operation to ^, it will repeat the digit you just typed. For example, if you tap 2 then ^, you will see 22 in the input field and no operation. If you keep tapping ^, it will keep repeating the same digit, then eventually clear the input field after a few taps.

  6. UI gripe / Suggestion (not a bug). This is somewhat subjective of a design decision and it might just be me, but when I see the < I sort of expect it to work as a BACKSPACE rather than a CLEAR. Along similar lines, I feel like a BACKSPACE would be quite handy for the sausage-fingered among us, as the buttons are quite small and it's easy to mistype :) Suggestion: I would keep the < button in it's place and change it to a BACKSPACE function, removing the last digit placed in the input field rather than clearing the whole thing. Then, I would use the space in the top-right corner (above the operation label) for a CLEAR button, and I would use the symbol C or even something like an eraser symbol from the FontAwesome library.

I hope this is helpful-- FYI these are all on PineTime hardware, I haven't tested any of these in the sim! If I have time in the upcoming days, I might poke around the code and see if I can help troubleshoot the causes for some of these.

minacode commented 1 year ago

Thank you so much for this detailed feedback! You even gave examples to reproduce. This is very helpful. I added your points to the description as "known issues" and will investigate them.

There seems to be a difference in behaviour between the simulator (x86) and the watch. I am currently diving in the details to get everything a little more safer. The format strings are way less forgiving on the watch it seems.

At first I ignored the issues around overflows but I get that the UX would be heavily improved by a controlled maximum value.

This is somewhat subjective of a design decision and it might just be me, but when I see the < I sort of expect it to work as a BACKSPACE rather than a CLEAR

This is a good idea. It would also be a lot more forgiving since it would not delete the result instantly. I don't know about a clear button and will think about it. Nulling the result could technically also be done via the = button.

Avamander commented 1 year ago

Nulling the result could technically also be done via the = button.

That's usually "repeat the last operation"

minacode commented 1 year ago

Then this shall be it.

yusufmte commented 1 year ago

Glad it is helpful-- thanks for your diligence!

At first I ignored the issues around overflows but I get that the UX would be heavily improved by a controlled maximum value.

I was thinking the same thing at first: probably very few users would need to perform calculations at the level needed to cause overflow. It's more the kind of bug that comes out of the user looking for ways to break the app. But out of principle it is best to foolproof it such that the user is incapable of causing a crash with any combination of inputs, and to make it clear when the user passes into too-large territory where the calculations or interface start to break down. :slightly_smiling_face:

That's usually "repeat the last operation"

Agree! That would be a more expected behavior for =.

PS: This might be a good BACKSPACE symbol if you are interested! (But < works fine as well!)

yusufmte commented 1 year ago
  1. Bug with any use of the decimal point. Try using the . (for example, type 5.1) and the number shown in the input field would be different (for 5.1, you would see 5.15663343). Not sure exactly what is causing this, but it has been consistent for me. However, the number actually stored in the input field is what you typed (5.1). For example, try 49^0.5 -- the result will, appropriately, be 7, even though the operation will look like you are doing 49^0.15663343.

Just wanted to add briefly -- In case it is helpful with troubleshooting this one: this bug also happens with division of non-divisible integers. For example, 52/53 and 1/2 both evaluate to 0.48957.

minacode commented 1 year ago

Ok, so I changed some things in the last commits. It's not done yet, but we are getting there.

All changes are only tested in the simulator for now.

And I have a question: How exactly do you expect the = button to behave?

Currently it calculates a new result and keeps the value and operation untouched. Therefore you can press it multiple times for repeated calculation. But this causes two issues:

  1. pressing any operation button afterwards will evaluate the calculation again (as it normally does as well). I think this would be unexpected.
  2. number input will extend the number that is already there.

One could use the backspace but it seems very inconvenient to me. Another solution could be an internal "= was pressed"-flag that causes an immediate reset of the operation and number once anything other than = gets pressed.

minacode commented 1 year ago

The last commit is an experiment for the UX. Instead of standard button presses you can now hold the touch and drag it accross the button matrix. The current button is colored with a different background. Releasing triggers the button handle. I hope that this help with pressing the correct buttons on the small screen.

If anyone has a good suggestion for the focus color I will add it. I am not good with colors :grinning:

yusufmte commented 1 year ago

Awesome, thanks for your work on this-- love the changes!! Tested new build on PineTime hardware and can confirm that:

  1. Overflow no longer causes a crash-- any overflow seemingly evaluates to 0.0001. Remaining suggestion: Probably best if it's eventually reported as OVERFLOW or TOO LARGE, for accuracy, but I think this is great.

  2. Values bounding is working well and it is not possible to type a number larger than the interface allows. Remaining suggestion: It feels a bit odd that you can only type 6 digits before the decimal point, and 4 digits after. With room for 11 characters (including the decimal point), it feels like you should be able to type either n.nnnnnnnnn or nnnnnnnnn.n , for example. My suggestion would be to check for the total number of characters (digits before decimal point, decimal point itself, and digits after), and stop accepting inputs once the total is 11.

  3. Calculations with non-integer numbers have been working perfectly, and there is no more discrepancy between the number typed and the number in the register! Remaining suggestion: Similar to the suggestion in (2) above, it would be nice if the number of digits after the decimal point in the result extended to the end of the result area. For example, 1/9 currently evaluates to 0.1111. With room for 11 characters, I feel it should show 0.111111111. In other words, the behavior would be: (a) show all digits to the left of the . -- if there are more than 11, instead report the number as too large or use a scientific notation; (b) show as many digits as possible to the right of the ., based on how much room is left in the label. The reason for this is that it's clear to the user that the calculator will round when the label is full, but not necessarily if it gets cut off before that (so user might ask "are there only 4 digits here, or is it rounding?'). However feel free to disregard this one if it's overcomplicating things.

  4. The backspace is looking/working great! :slightly_smiling_face:


And I have a question: How exactly do you expect the = button to behave? Currently it calculates a new result and keeps the value and operation untouched. Therefore you can press it multiple times for repeated calculation. But this causes two issues:

  1. pressing any operation button afterwards will evaluate the calculation again (as it normally does as well). I think this would be unexpected.
  2. number input will extend the number that is already there. One could use the backspace but it seems very inconvenient to me. Another solution could be an internal "= was pressed"-flag that causes an immediate reset of the operation and number once anything other than = gets pressed.

The way = now is working is exactly as I would expect, with allowing multiple presses for repeated calculation!

Here's the way I would recommend addressing those two issues:

i. I agree this is confusing. I would make it so that pressing the operation buttons never causes a calculation-- only changes the current operation. The ONLY way to cause a calculation therefore would be pressing =, and so the user would never do it accidentally. If you did this, I would also modify the backspace so that it doesn't remove the operation (since you can change it freely), only: (a) a digit from the input value, then (b) the result. (I think that change to the backspace is an added bonus, because it's potentially confusing that the backspace would remove the operation first even though it may not be the last thing typed.)

ii. I like your solution-- when = is pressed and a calculation happens, if you type any digit after that, it should reset the current input label and place the digit freshly. (This also makes clearing more convenient.) However, I don't think the operation needs to be reset (only the number), especially if the tweak in (i) is made where the operation can be changed anytime.


  1. Unrelated: division by zero does nothing (tapping the = doesn't register). I think this is fine to stay that way, but if you eventually had reported flags like TOO LARGE or OVERFLOW, it would probably make sense to report this as well as something like UNDEFINED, for accuracy.

The last commit is an experiment for the UX. Instead of standard button presses you can now hold the touch and drag it accross the button matrix. The current button is colored with a different background. Releasing triggers the button handle. I hope that this help with pressing the correct buttons on the small screen.

This sounds like a great idea, excited to try it out!

(Of course, feel free to take or leave any of the suggestions above at your discretion! Just my thoughts but others might think differently and it is your app :slightly_smiling_face:)

minacode commented 1 year ago

Again thank you for the feedback! It means a lot 😊

  1. There needs to be an overflow check for the power function. I haven't done this yet.
  2. (and 3.) The current implementation builds on fixed point numbers. You basically suggest changing to floats. Maybe that is a thing, I don't know. I didn't use floats based on advice from the chat in the early development and will keep it like that for now. But I will change back to 3 decimals digits and 7 integer digits, because I guess that is the most practical format. 4 is just better for debugging.
  3. Nice! I like it as well 😌
minacode commented 1 year ago

I did some updates:

  1. Power overflow is done.
  2. Error messages are done for "too large" and "zero division".
  3. The operation label is gone and instead the buttons are now colored. The numbers can be a lot larger now. I hope that this doesn't open any new bugs :grinning:

A gif probably says the most: InfiniSim_2022-12-31_131450

There are some minor issues left that I will continue working on.

yusufmte commented 1 year ago

Awesome updates! Thanks for your work :+1: Testing latest build on PineTime hardware and all the bugs number 1-7 reported above are resolved from my end :heavy_check_mark: I love the design changes like the touch/drag coloring, and coloring the currently active operation! Very clever & sleek.

(Also, regarding the fixed vs floating point, that makes sense :slightly_smiling_face: I agree, I've become familiar with it and think fixed point is probably favorable for simplicity of design.)

minacode commented 1 year ago

Ok, nice, then there is only the color issue left 😊 I have no knowledge about that. Do we use InfiniTime colors? Or the global theme? If anyone can help there it would be much appreciated.

Edit: @Riksu9000, you seem to know a lot about theming. Can you give some feedback regarding the colors?

Riksu9000 commented 1 year ago

The default button color is being phased out in favor of Colors::bgAlt, which will become the default at some point. You can use whichever for now, but I suggest you try the new color. There's no standard for the rest of the colors currently, so you can be creative. If you want to experiment, you can try to swap the deep red for a deep orange for example LV_COLOR_MAKE(0xff, 0x40, 0).

The highlighting of the pressed button is making the app less responsive. I think this is because the entire matrix is being redrawn. I suggest removing this feature to make it feel more responsive.

minacode commented 1 year ago

Thank you!

Do you have an alternative idea how to reduce false inputs? The coloring of the current button was my best. It's a shame that it's so slow.

Edit: I will try single buttons instead of the matrix.

minacode commented 1 year ago

Alright, It's done (I hope :grinning:). I removed the "swipe to type" for better visuals and added better colors. Unless we find a serious bug I would like to have this merged as is. We can discuss eventual features in subsequent PRs if that is fine.

yusufmte commented 1 year ago

Most current version is working very nicely! The "swipe to type" style matrix was nice but I confess it is working more swiftly without this feature, and probably best to leave it out for now, and think about later features / methods to prevent false inputs in later PRs as needed. It does what it's intended to just great :slightly_smiling_face: I hope it can be merged in soon!

CCF100 commented 1 year ago

Thank you for testing this! It doesn't happen in the simulator so I wonder what makes the watch crash. Does it happen with every operation or only with the power function?

Edit: I tested the last changes on the watch and it seems like a lot broke. Maybe we have to revert some of the changes.

It happens with any operation, simply typing too many digits crashes the watch, although I haven't updated my watch since making the original comment...

minacode commented 1 year ago

That should be fixed and you should update to get the shiny new version βœ¨πŸ˜‰

minacode commented 1 year ago

The new negative numbers seem to break the bounds checks. -3 * -5 resulted in a "too large" error.

FintasticMan commented 1 year ago

There are actually quite a few issues with the negative numbers, let me see if I can figure out what's causing them.

minacode commented 1 year ago

Oh, no πŸ˜„ I might have time this weekend to take a look as well. Just comment the issues and I will gather them in a todo list in the description.

github-actions[bot] commented 1 year ago

Build checks have not completed. Possible reasons for this are:

  1. The checks need to be approved by a maintainer
  2. The branch has conflicts
  3. The firmware build has failed
minacode commented 1 year ago

Apart from squashing everything I once again think that this PR is done. I cannot find any remaining bugs.

As before, any help for final testing and reviews would be highly appreciated.

Is there any interest from the maintainers to actually merge this when done or do we need to solve the general app situation first?

yusufmte commented 1 year ago

Is there any interest from the maintainers to actually merge this when done or do we need to solve the general app situation first?

My vote as a user would definitely be to merge it into the firmware soon, before arbitrarily waiting to find a way to make apps more modular/customizable. It is a pretty fundamental / universally useful function rivalling the other apps currently built into the firmware. I've been using it and updating it as new commits are pushed and it's been great for daily use. There have been numerous requests for a calculator app and a number of attempts as well so there is definitely interest, and the resource cost seems relatively small in comparison to the value it provides.

LinuxinaBit commented 1 year ago

Agreed, If Pong, 2048, Navigation, and the Accelerometer readout had to go, I would much rather have this built in. Even when app installations are possible, it seems like such an integral part of this kind of watch that you should have it even without setting anything up with a phone.

Eragonfr commented 1 year ago

I would love to get a calculator app on my watch. If it can be merged in the next release.

minacode commented 1 year ago

Since I suppose that the main issue regarding merging this is the available memory, I would like to (shamelessly) advertise #1775 to anyone who wants to raise the chance of this PR being merged.

Boteium commented 1 year ago

I have two questions.

(1) After entering the first number, press equal sign twice without pressing any operator will reset everything to zero. Is this the intended behavior ? e.g. pressing "1", "=", "=" would yield zero. but I was expecting 1

(2) It is not possible to edit the value (press backspace) after pressing any operator, because pressing the equal sign subsequently will cause (a) result being replaced by value (b) value becoming zero. e.g. pressing "1", "0", "+", "1", "0", "backspace", "=" would yield 1. but I was expecting 11.

minacode commented 1 year ago

I don't have a firmware with the calculator on me right now. This is what I think happens:

  1. Yes, this is expected. The "=" copies the value to the result if no operator is selected. The new value becomes zero in that case. After pressing "=" twice the zeroed value got copied as well and therefore the result is zero.

  2. I can't look into the example right now, but it looks weird to me, too. Note that you can get rid of the currently selected operator by pressing an operator button until no operator is selected. Hope that helps.

Boteium commented 1 year ago
2. I can't look into the example right now, but it looks weird to me, too. Note that you can get rid of the currently selected operator by pressing an operator button until no operator is selected. Hope that helps.

Okay, I guess this is an incorrect behavior then, because my intention is to edit value rather than change operator.

Boteium commented 1 year ago

There's still problem with overflow check : For example, 987654321*100 does not trigger a "too large" warning.