Bike computer build help- using smooth arc

First, let me apologize for the length of the post. There is a TL;DR at the bottom.

I consider myself the world's second worst programmer so please assume I know less than you think and use small words.
I'm trying to develop my own bike computer because I figure I can do it half as well and for twice the price
of buying one. I'm using this module from Waveshare: https://www.waveshare.com/esp32-s3-touch-lcd-1.28.htm
For software, Attempt 1 was with the "animated dial" project under Examples > TFT_eSPI > Sprite. I had some measure of success but I was having a real hard time understanding the code so I started again with a simpler one. That seems to be going better, but I'm still struggling. Attempt 2 is from Examples > TFT_eSPI >
Smooth Graphics > Smooth Arc.

Explanation: The outer ring is just an image and I'm happy with that.
The inner arc (CYAN), is supposed to move clockwise about the inner circumference with speed. 0 mph is at 60 degrees (0 deg. is at the 6 o'clock position). 30mph (the max) or higher values is supposed to peg at 300 deg. See picture 1 it is sort of what I'm looking for. (problem 1)
In the middle, in large numbers is the speed with one decimal place accuracy. (problem 2). The "MPH" label under it is fine. In the 120 degree gap in the arc at the bottom I have placed other info. In this case it's the total miles traveled. I'm happy with the "TOTAL MILES" label, but the numeric value is also a problem. (problem 3).
I am not getting any warnings. The code compiles fine.
(Problem 4) = documentation.

PROBLEM 1: In my code the random number generated will not be used. The data will come from elsewhere, but because it's messing up, I need to know if it's the arc that's the problem or if the number I'm generating is out of range. The arc works like this: If the speed is increasing, I just draw more arc, but if the speed is decreasing, I "erase" the excess arc by creating a black arc from the new value to the previous end value. I never draw the black arc into the out-of-bounds area, so when the cyan arc goes there, it never goes away. I started by running random (min,max); with 60 and 301 as the arguments, so I figured this would keep the arc
between those values. When that didn't work, I inserted the if (a < 60) a = 60; and if (a > 300) a = 300; statements. That didn't work either. See picture 2.

PROBLEM 2: I'm happy with the look and placement of the numbers indicating speed. I'd like one decimal place accuracy so I used a float. I know there are problems with floats and I can change it later if it is too slow.
But the problem here, as with the odometer number below it, is that when I write a new number, it goes over the old number. This was not a problem with Attempt 1, probably because it used Sprites, which I don't understand. I don't really know how to get rid of the old value when the new one writes on top of it. I don't want to use
a black box, because I don't seem to be able to get it to cover the number at the correct rate (either the number never shows up, just a black box or a horrible flicker with both numbers briefly showing). Also, the numbers are close to the arc and a box might cut a chunk out of it. See picture 4.

PROBLEM 3: I can't seem to make the odometer number font size smaller than the speed font size. I thought if I loaded the font IN the command, it would take precedence over the tft.loadFont(AA_FONT_LARGE); command. I just want a more normal font, like the MPH and TOTAL MILES labels, but a little bit bigger, not the
NotoSansBold72 font. Even picture 3 is a little too big.

PROBLEM 4: Documentation. I think I struggle a lot because I can't seem to find a complete definition for a lot of topics (e.g. Sprites.) The arduino site defining the syntax of many of the basic commands is good. And this site TFT_eSPI::drawFloat - TFT_eSPI library is super helpful. I would like
more sites like it. For instance I found it by searching for a command. I think it was tft.drawNumber, but when I search tft.pushSprite, I can find specific examples from users on forums and stuff, but there has to be a place with all of the commands for, well, everything doesn't there? I mean, someone developed this stuff.

TL;DR
Look at code.
Problem 1. The arc I'm drawing seems to go into the out-of-bounds area between 60 and 300 degrees. Why?
Problem 2. How do I "delete" a number on the screen when I update it so new values don't appear over old ones?
Problem 3. How do I get two font sizes on one display? Using tft.loadFont seems to set it for everything.
Problem 4. Can you lead me to a "dictionary" or "dictionaries" of every command (or as many as possible)?

Thank you in advance. Now I have to hope I can properly post all of this info.




/ 11-29-24 Bike computer interface with smooth arc instead of sprite with no erase.
#include "outer_ring_test_gradiant_pink_blue_glow3.h"
#include <TFT_eSPI.h>  

uint16_t x = 120;  // Position of centre of arc, round screen is 240 x 240 with corners chopped off.
uint16_t y = 120;

uint16_t fg_color = TFT_CYAN;  // Foreground and background colour used for smoothing (anti-aliasing)
uint16_t bg_color = TFT_BLACK;       

uint8_t radius       = 100;   // Outer arc radius
uint8_t thickness    = 15;    // Thickness or arc
uint8_t inner_radius = radius - thickness;        // Calculate inner radius (can be 0 for circle segment)

const unsigned long refresh_rate = 1000;  //interval to update speed/screen (millis)
unsigned long previousTime = 0; //longs are needed because millis can get big fast
float speed;
static int16_t old_end_angle = 60;  //start old_end_angle for program.

TFT_eSPI tft = TFT_eSPI();  // Create object "tft"
#include "NotoSansBold72.h"
#define AA_FONT_LARGE NotoSansBold72

long a;

//SETUP *********************************************************************************************************
void setup(void) 
{
  Serial.begin(115200);
  tft.init();
  tft.setRotation(0); //Rotate display to appropriate angle. 0 degrees is at 6 o'clock position
  tft.fillScreen(TFT_BLACK);
  tft.pushImage(0, 0, 240, 240, outer_ring_test_gradiant_pink_blue_glow3);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("MPH", x, y + 35, 2);
  tft.drawString("TOTAL MILES",x, y + 90, 2);
  
}
//MAIN LOOP********************************************************************************************************
void loop()
{
  unsigned long currentTime = millis();
  if (currentTime - previousTime >= refresh_rate)
  {
  a = random(60,301);
  if (a < 60) a = 60;
  if (a > 300) a = 300;
  speed = 27.7;
  
  
  uint16_t start_angle = 60; // Start angle must be in range 0 to 360. Arcs are drawn clockwise from start_angle to end_angle
  uint16_t end_angle   = a; // End angle must be in range 0 to 360
  
  while (end_angle != old_end_angle)
  {
    tft.drawSmoothArc(x, y, radius, inner_radius, start_angle, end_angle, fg_color, bg_color);
    if (end_angle < old_end_angle)
    {
      tft.drawSmoothArc(x, y, radius, inner_radius, end_angle, old_end_angle, TFT_BLACK, bg_color);
    }
   float distance = 400.3;
  //tft.fillRect(x, y + 70, 80, 12, TFT_BLACK);
   tft.setTextSize(2);
   tft.setTextFont(4);
   tft.drawFloat(distance, 1, x, y + 65);  
   tft.loadFont(AA_FONT_LARGE);
   tft.drawFloat(speed, 1,  x ,  y );     //x and y are the upper left corner of the speed text, apparently
   
   old_end_angle = end_angle;

  }

  previousTime = currentTime;

  }


}type or paste code here

Only you will know. But...

To "erase" the arc, first draw the full black arc, then overlay the cyan arc of changing value.

Store the old number and write the old number in black just before writing the new number in white.

That is in your code.

HI, it looks like a fun project.
I'd like to help you solve some of the problems.

First, here's an image that reproduces your code. (Font settings and surrounding circle graphics have been omitted.)

Indeed, part of the text TOTAL MILES is hidden.

Problem 1.

Unfortunately, I have not yet been able to find the cause of the out-of-range.

However, the following two lines are not necessary as they are not the cause of the problem.

if (a < 60) a = 60;
if (a > 300) a = 300;

I'll attach a picture so hopefully someone can solve it.

Problem 2.

In this case, it's good to use a sprite to display miles.

TFT_eSprite sprite_miles(&tft); // Create sprite for miles text

If you specify the foreground and background colors as the font colors, the old characters will be erased with the background color and the new characters will be drawn. So in setup() :

sprite_miles.setTextColor(TFT_WHITE, TFT_BLACK);
sprite_miles.setTextSize(2);
sprite_miles.setTextFont(4);

Problem 3.

I don't know how to get the height of the font, but you can find the width of the drawn text like this:

int width = tft.drawFloat(distance, 1, x, y + 65);

When I printed the width to the serial monitor, it was 126. I roughly estimated the height to be 50 and put a red box around the mileage numbers:

Based on this, draw the text in sprite_miles and transfer it to tft. At this time, you can specify the background color as the transparent color.
(You can replace 126 / 2 and 50 / 2 with appropriate symbols.)

sprite_miles.drawFloat(distance, 1, 0, 0);
sprite_miles.pushSprite(x - (126 / 2), y + 65 - (50 / 2), TFT_BLACK);

At last, my code and image look like this:

// 11-29-24 Bike computer interface with smooth arc instead of sprite with no erase.
#include "outer_ring_test_gradiant_pink_blue_glow3.h"
#include <TFT_eSPI.h>

uint16_t x = 120;  // Position of centre of arc, round screen is 240 x 240 with corners chopped off.
uint16_t y = 120;

uint16_t fg_color = TFT_CYAN;  // Foreground and background colour used for smoothing (anti-aliasing)
uint16_t bg_color = TFT_BLACK;

uint8_t radius = 100;                       // Outer arc radius
uint8_t thickness = 15;                     // Thickness or arc
uint8_t inner_radius = radius - thickness;  // Calculate inner radius (can be 0 for circle segment)

const unsigned long refresh_rate = 1000;  //interval to update speed/screen (millis)
unsigned long previousTime = 0;           //longs are needed because millis can get big fast
float speed;
static int16_t old_end_angle = 60;  //start old_end_angle for program.

TFT_eSPI tft = TFT_eSPI();  // Create object "tft"
#include "NotoSansBold72.h"
#define AA_FONT_LARGE NotoSansBold72

TFT_eSprite sprite_miles(&tft); // Create sprite for miles text

long a;

//SETUP *********************************************************************************************************
void setup(void) {
  Serial.begin(115200);
  tft.init();
  tft.setRotation(0);  //Rotate display to appropriate angle. 0 degrees is at 6 o'clock position
  tft.fillScreen(TFT_BLACK);
  //tft.pushImage(0, 0, 240, 240, outer_ring_test_gradiant_pink_blue_glow3);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("MPH", x, y + 35, 2);
  tft.drawString("TOTAL MILES", x, y + 90, 2);

  // move the following two lines in loop() to setup()
  tft.setTextSize(2);
  tft.setTextFont(4);

  // Create a sprite for the mileage display
#define MILE_TEXT_WIDTH   126
#define MILE_TEXT_HEIGHT  50
  sprite_miles.createSprite(MILE_TEXT_WIDTH, MILE_TEXT_HEIGHT);
  sprite_miles.setTextColor(TFT_WHITE, TFT_BLACK);
  sprite_miles.setTextSize(2);
  sprite_miles.setTextFont(4);
}
//MAIN LOOP********************************************************************************************************
void loop() {
  unsigned long currentTime = millis();
  if (currentTime - previousTime >= refresh_rate) {
    a = random(60, 301);
    speed = 27.7;

    uint16_t start_angle = 60;  // Start angle must be in range 0 to 360. Arcs are drawn clockwise from start_angle to end_angle
    uint16_t end_angle = a;     // End angle must be in range 0 to 360

    while (end_angle != old_end_angle) {
      tft.drawSmoothArc(x, y, radius, inner_radius, start_angle, end_angle, fg_color, bg_color);

      if (end_angle < old_end_angle) {
        tft.drawSmoothArc(x, y, radius, inner_radius, end_angle, old_end_angle, TFT_BLACK, bg_color);
      }

      float distance = 400.3;
      sprite_miles.drawFloat(distance, 1, 0, 0);
      sprite_miles.pushSprite(x - (MILE_TEXT_WIDTH / 2), y + 65 - (MILE_TEXT_HEIGHT / 2), TFT_BLACK);

      tft.loadFont(AA_FONT_LARGE);
      tft.drawFloat(speed, 1, x, y);  //x and y are the upper left corner of the speed text, apparently

      old_end_angle = end_angle;
    }

    previousTime = currentTime;
  }
}

Problem 4.

Unfortunately, I don't know of any command dictionary sites. I enjoy programming by reading code and experimenting with how to use it.

If anyone knows, I'd like to know too.

Hope this help.

Thank you for the responses.

Embeddedkiddie, Thank you for the code. I will check it in the morning. The problem with the font size wasn't really the fact that the 400.3 was over the top of the "TOTAL MILES", because I could change that size if I wanted to. The problem was, the only way I was able to get the font to be right for the speed caused it to be wrong for the miles. In image 3, it was smaller, but over the "TOTAL MILES" label, but the real problem was in image four. As soon as it ran through the loop one time it jumped to the larger font for BOTH the speed and the miles. I couldn't seem to get two different sizes for the numbers that changed. I will look at the sprites, but like I said, I wish I could find some official definitions of the usage, like the link I included. It's excellent.

It's strange that my images came in all crooked. I took the pictures in the same orientation. They came into my computer in the same orientation and I edited them in the same orientation. As soon as I uploaded them. Boom! One is upside down and another is sideways. LOL.

So I'm glad you could reproduce the problem with the smooth arc. How did you do that? Is it some kind of simulator or do you have one of these boards sitting around?

Anyway, thank you.

Hey, I think I fixed the arc problem. All I had to do was to change tft.drawSmoothArc to tft.drawArc. Looks fine and it's been running for 15 minutes so far with no errors.

I ran Embeddedkiddie's code and I'm still having some problems. First, the text for the miles is a little small at font size 1 and too big at font size 2. I tried using the AdaFruit_GFX libraries, but all I get are errors. Any suggestions for that?

More importantly, with the code as is, if the miles is large like 1000.1, then it is centered nicely, but if it's small like 1.3, then it's shoved over to the left side. Can anyone tell me how to print the text in the center regardless of how many digits there are? I tried using the setTextDatum commands, but after playing around with that for about an hour, I guess it's not the way to go.

Thanks for the help.

This sounds like the display is left-justified.

Size of character widths vary, so find an average of narrow and wide characters, in pixels.

Count the number of characters in the number string (1001.1 = 6chars, 1.3 = 3chars), multiply that value by average number of pixels per character, then divide that number of pixels in half. "half_total_character_pixels"

Divide the width of the screen in pixels in half. "half_screen_pixels"

Subtract "half total character pixels" from "half screen pixels" and you will have the starting pixel of the first character.

screen_middle_pixel = screen_pixel_width / 2;
characters_middle_pixel = number_of_characters * average_pixels_per_character / 2;
start_pixel = screen_middle_pixel - characters_middle_pixel;
// start_pixel = (screen_pixel_width / 2) - ((characters * average_pixels_per_character) / 2);

I see what you are saying, but is there a count the number of characters command? Otherwise I'm just splitting the difference between a small number of characters and a large number of characters, which I can just do visually.

float value = 101.1;

void setup() {
  Serial.begin(115200);
  Serial.print(countthenumberofcharacters(value));
}

void loop() {
}

int countthenumberofcharacters(float val) {
  int places;
  if (val > int(val)) places += 2; // number has a decimal point and a decimal place
  if (val >= 10) places += 2; // value has a units and tens place
  if (val >= 100) places++; // value a hundreds place
  return (places);
}

Should the first line read "float val = 101.1;"?

You could use "value" without passing the value to a function... but...

(sorry for using the word "value" as a variable... I confused the situation doing that)

"value" is global variable at 101.1
"val" is a local variable, but receives the value of "value"

This is what could happen when using the same name for global and local variables.

// worst coding evAr
// Variable scope shown on the Serial Monitor output.

int a = 9600; // global "a"
void setup() {
  Serial.begin(a); // using the global variable value of "a"
  Serial.println(a); // global "a"

  int a = 2; // this new "a" is locally defined here, in "setup()"

  for (int a = 7; a < 9; a++) { // this new "a" is local only inside "for"
    Serial.println(a); // this "a" is local to "for"
  }

  Serial.println(a); // local "a" from the definition inside "setup"
}

void loop() {
  Serial.println(a); // this is the global "a"
  while(1); //stay
}

Thank you for the responses. I'm still quite confused. Please remember, second worst programmer ever.

In the first code you posted above, I understand logically how you are determining the places. In words:
-places is an integer
-if 101.1 > integer value of 101.1(=101), then add 2 to places (so places would be 2)
-if 101.1>= 10, then add 2 to places (places = 4)
-if 101.1>= 100, then add 1 to places (places =5).

What I don't understand is where the function is called. Inside the void loop, isn't
"int countthenumberofcharacters(float val)" just taking the integer portion of the floating variable called val and assigning it to countthenumberofcharacters? I'm guessing countthenumberofcharacters is the function call. And then the if statements inside define the function. Is that correct? But then there is an open curly bracket, but doesn't seem to have a closing curly bracket.

I'm sorry to be so dense here. I've never really called a function like this in arduino.

The braces are opened and closed. The compiler would not let a missing brace compile.

countthenumberofcharacters() is the function name.
value is the argument I send to the function countthenumberofcharacters()

In the following line I am doing some things...

First , I am sending "value" (101.1) as I call the function countthenumberofcharacters().

countthenumberofcharacters(value)

Next, I am returning an "int" from countthenumberofcharacters()

int countthenumberofcharacters() { // 'int' is the return type

Last, I am printing the "return" (int) of the function call.

Serial.print(the int returned by the function call countthenumberofcharacters with an argument of "value");

All together;

Ok, so this takes place OUTSIDE of void loop. I see that the curly bracket is closed then.

When you call the function isn't the argument inside of the parenthesis? Because you don't have "value" in the parenthesis. You have "float val."

Also, if you define places to be an int, do you need to define countthenumberofcharacters as int?

Hi @profozone , @xfpd ,

I like your detailed discussion. But It is good to leave the tedious calculations to the library. (Sorry for the code I showed in post #3. It just wasn't practical.)

Could you please delete all of the sprite code I showed you and change the "TOTAL MILES" display part to the following and try it?
(MILE_TEXT_HEIGHT needs to be changed according to the font size.)

#define MILE_TEXT_WIDTH   240
#define MILE_TEXT_HEIGHT  50
tft.setTextDatum(TC_DATUM);
tft.drawFloat(distance, 1, MILE_TEXT_WIDTH / 2, y + 65 - MILE_TEXT_HEIGHT / 2);

Regardless of the length of the text, it will always be displayed in the center of the screen.

However, the problem with this code is that if the length of the string becomes shorter, an afterimage will remain. Since the miles would be increasing, I think it would be fine to clear it only when resetting it to 0.

Wrong.

Learning programming from only using libraries undermines discovery of how and why. Low level learning has a very good place. By your rules, why even program? Just pay $10 for a pre-made toy and call it your's. Just because you have enough credit to charge an airplane does not mean you know how to fly.

Not if the calculations and data were stored.

Embeddedkiddie, So this worked well but the font and font size are the same as the speed font, because the new font was commented out with the rest of the sprite stuff. So back to the problem of displaying two numbers with different fonts/sizes. Also, I used MC_DATUM above so this changes that. What I did was commented out the TC_DATUM and just adjusted the "y + 65 - MILE_TEXT_HEIGHT..." to "y + 85 - MILE_TEXT_HEIGHT..."

Xfpd, thank you for all of the help. I still have a long way to go with this project (touchscreen, gps, interrupts?, etc.). I can use all of the help I can get. But I have to say, I'm not really trying to be a programmer. I'm trying to make a project. I have done probably 4 microprocessor projects over the last 10 years, but that just means I have plenty of time between, to forget everything I learned.

I totally agreed. That's why I said "I like your detailed discussion." I myself learned a lot from this thread in finding a solution to this problem :smile:

How about setting the font and font size every time before excuting tft.drawString()? That won't work?

The called function recieved "value" and placed it in "val" as a float for use inside the called function.

The "int" prefixing the function name is the "type" of variable that will be returned from the function, in this case, the contents of "places"

Doing that alone did nothing. I believe I would have to load a different font in as well currently tft.loadFont(AA_FONT_LARGE) defined above as NotoSansBold72. This is a font I created from the NotoSansBold36 font. Since I actually wanted a font larger than a 1 and smaller than a 2, I tried to use one of the fonts from the AdaFruit_GFX library, but I got nothing but error after error. I'll see if I can find another one and report back.

Ok, actually that did work. Can't believe I didn't think of that. I went a little too far though. I'll look around for something cool that is the right size.