Analog gauge meter

Status
Not open for further replies.

toxic

Member
Hi,
I’m working with a speedometer and have have not the right knowledge how to code analog meter on ILI9341 TFT with Teensy 3.1 with sinus/co-sinus code.
How can I draw a analog meter with a needle with simple coding? If you have a tutorial I will be thankful for the advices.

Best regards
Toxic
 
I've not seen anything - nor done this myself. FrankB did post code of a nice bar graph equalizer he did once.

Ideally draw one line at a time in the foreground color (if a prior line was drawn - redraw that in background color first). [This will make the line flash from On>Off>On - so refinement later may be needed - especially if it swings widely between updates.]

The upper end of that line would follow an arc bounded by screen the top and edges. So the 240 wide display might have say 200 valid points. The base of a needle would be a single central point - for flair the need could extend below that fixed point but might only sweep 20 pixels and would be best added later if not done carefully the axis point will jump to either side of the axis as the line displays making a weird jitter with rounding.

>So pick your low central point for the axis.
>Then draw the line to a point in that upper arc/circle. [with preceding redraw as noted]
> The location of any label will need to be above or below the 'needle' or it will lose pixels unless redrawn

To get the upper point take the analog value scaled from 1 to 200 and do the needed math to find where on that arc that upper point would lie. With good math and coding that equation should pop out.

That's my first guess - maybe there are better ideas?
 
Hi,

Thank you for the guideline but I’m looking code example. I’m not the best in this type of math in this level but if I get some tutorial it would be easier to go forward.
I tried to search in this forum about FrankB example but I did not find it. If you have the link it would be great..

Best regards
Toxic
 
Do you have the unit powered up and working? https://www.pjrc.com/store/display_ili9341.html.

Open the GraphicsTest sample and look it over - maybe isolate out the line draw elements. Doing that will - if not get you closer - prepare for working with whatever sample may be provided later.

I just did a search and found this full circle gauge versus what I described: https://www.youtube.com/watch?v=o6QjVqq1lXM it doesn't show sample code - but a similar one might.

There is an active GoCart project on the Forum - not sure if added dial gauges?

http://www.instructables.com/id/Arduino-sketch-for-a-retro-analogue-meter-graphic-/?ALLSTEPS
 
Last edited:
Hi,
I have working TFT but I’m interested in tutorial or example from simple code single pixel drawing so I can learn for later project.

Best regards
Toxic
 
Hi,
Thank you :)

Still interested simple example. Draw circle is no problem but fix the needle is interesting. Tutorial?

Best regards
Toxic
 
Same instructable I linked. I'd look through it and port it to the code used on Teensy - not drag in a library they use as in the end you shouldn't need it.

If you look over the examples "Teensy\hardware\teensy\avr\libraries\ILI9341_t3\examples\graphicstest" you'll find code for doing most of anything the device can do. Each subgroup of testing is a unique function - start by taking the most sub-function most like what you want to do and make it work in a stand alone sketch and then add parts that do more like what you want.

If that works to get you started you are good to go - if that doesn't go so smooth - start with some other examples and figure out the environment or look for other tutorials that speak to you: http://www.bing.com/search?q=ili9341+tutotial&qs=n&form=QBLH&pq=ili9341+tutotial&sc=1-16&sp=-1&sk=&cvid=1719e3f326314b218babcf26ba9dc5c6

If nothing else start simpler - learn to control debug text spew of your analog value and draw some lines then as that will be part of your project development
 
I took the example above and stripped out the special library functions.
It's not very elegant, but it works and it's a good starting point.

Here it is working on a Teensy 3.1 using the highly optimized ILI9341_t3 library:
https://github.com/PaulStoffregen/ILI9341_t3

With fonts from here:
https://github.com/PaulStoffregen/ILI9341_fonts

Teensy Meter.jpg

Here is the working code:
Code:
/*
Based on VU Meter Sketch From Instructables by Bodmer
http://www.instructables.com/id/Arduino-sketch-for-a-retro-analogue-meter-graphic-/

 An example analogue meter using a ILI9341 TFT LCD screen
 This example uses the hardware SPI only
 Alan Senior 23/2/2015

Stripped out the special library functions
Wozzy 09/01/2015
 */

#include "SPI.h"
#include "ILI9341_t3.h"

 #include "font_DroidSans.h"
 #include "font_DroidSans_Bold.h"


#define TFT_RST 8
#define TFT_DC  9
#define TFT_CS 10
ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC, TFT_RST);
#define RGB(r,g,b) (r<<11|g<<5|b)  //  R = 5 bits 0 to 31, G = 6 bits 0 to 63, B = 5 bits 0 to 31

#define ILI9341_GREY 0x5AEB

float ltx = 0;    // Saved x coord of bottom of needle
uint16_t osx = 120, osy = 120; // Saved x & y coords
uint32_t updateTime = 0;       // time for next update

int old_analog =  -999; // Value last displayed
int old_digital = -999; // Value last displayed


void setup(void) {
  tft.begin();
  tft.setRotation(2);

  tft.fillScreen(ILI9341_BLACK);

  analogMeter(); // Draw analogue meter
  
  updateTime = millis(); // Next update time
}


void loop() {
  if (updateTime <= millis()) {
    updateTime = millis() + 500;

    int reading = 0;
    reading = random(0, 100); // Test with random value
  //reading = map(analogRead(A0),0,1023,0,100); // Test with value form Analogue 0  
    plotNeedle(reading, 8); // Update analogue meter, 8ms delay per needle increment
  }
}

// #########################################################################
//  Draw the analogue meter on the screen
// #########################################################################
void analogMeter()
{
  // Meter outline
   tft.setRotation(1);
   tft.fillRect(0, 0, 239, 126, ILI9341_GREY);
   tft.fillRect(5, 3, 230, 119, ILI9341_WHITE);
   
  
   tft.setTextColor(ILI9341_BLACK);  // Text colour
   tft.setRotation(1);  
  
  // Draw ticks every 5 degrees from -50 to +50 degrees (100 deg. FSD swing)
  for (int i = -50; i < 51; i += 5) {
    // Long scale tick length
    int tl = 15;
    
    // Coodinates of tick to draw
    float sx = cos((i - 90) * 0.0174532925);
    float sy = sin((i - 90) * 0.0174532925);
    uint16_t x0 = sx * (100 + tl) + 120;
    uint16_t y0 = sy * (100 + tl) + 140;
    uint16_t x1 = sx * 100 + 120;
    uint16_t y1 = sy * 100 + 140;
    
    // Coordinates of next tick for zone fill
    float sx2 = cos((i + 5 - 90) * 0.0174532925);
    float sy2 = sin((i + 5 - 90) * 0.0174532925);
    int x2 = sx2 * (100 + tl) + 120;
    int y2 = sy2 * (100 + tl) + 140;
    int x3 = sx2 * 100 + 120;
    int y3 = sy2 * 100 + 140;

    // White zone limits
    //if (i >= -50 && i < -45) {
    //  tft.fillTriangle(x0, y0, x1, y1, x2, y2, ILI9341_WHITE);
    //  tft.fillTriangle(x1, y1, x2, y2, x3, y3, ILI9341_WHITE);
    //}
      
    // Green zone limits
    if (i >= -50 && i < 30) {
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, ILI9341_GREEN);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, ILI9341_GREEN);
    }
    
    // Yellow zone limits
    if (i >= 30 && i < 40) {
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, ILI9341_YELLOW);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, ILI9341_YELLOW);
    }

    // Orange zone limits
    if (i >= 40 && i < 45) {
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, ILI9341_ORANGE);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, ILI9341_ORANGE);
    }

    // Red zone limits
    if (i >= 45 && i < 50) {
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, ILI9341_RED);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, ILI9341_RED);
    }

    
    // Short scale tick length
    if (i % 25 != 0) tl = 8;
    
    // Recalculate coords incase tick lenght changed
    x0 = sx * (100 + tl) + 120;
    y0 = sy * (100 + tl) + 140;
    x1 = sx * 100 + 120;
    y1 = sy * 100 + 140;
    
    // Draw tick
    tft.drawLine(x0, y0, x1, y1, ILI9341_BLACK);

 tft.setFont(DroidSans_12);
 tft.setCursor(25,50);
 tft.print("0");
 tft.setCursor(60,20);
 tft.print("25");
 tft.setCursor(110,9);
 tft.print("50");
 tft.setCursor(160,20);
 tft.print("75");
 tft.setCursor(202,50);
 tft.print("100");
 tft.setFont(DroidSans_14_Bold);
 tft.setCursor(8,100);
 tft.print("TEMP");
 tft.setCursor(210,100);
 tft.print("°F");

    
    // Now draw the arc of the scale
    sx = cos((i + 5 - 90) * 0.0174532925);
    sy = sin((i + 5 - 90) * 0.0174532925);
    x0 = sx * 100 + 120;
    y0 = sy * 100 + 140;
    // Draw scale arc, don't draw the last part
    if (i < 50) tft.drawLine(x0, y0, x1, y1, ILI9341_BLACK);
  }

  tft.drawRect(5, 3, 230, 119, ILI9341_BLACK); // Draw bezel line
  
  plotNeedle(0,0); // Put meter needle at 0
}

// #########################################################################
// Update needle position
// This function is blocking while needle moves, time depends on ms_delay
// 10ms minimises needle flicker if text is drawn within needle sweep area
// Smaller values OK if text not in sweep area, zero for instant movement but
// does not look realistic... (note: 100 increments for full scale deflection)
// #########################################################################
void plotNeedle(int value, byte ms_delay)
{
  tft.setTextColor(ILI9341_BLACK, ILI9341_WHITE);
  char buf[8]; dtostrf(value, 4, 0, buf);
 // tft.drawRightString(buf, 40, 119 - 20, 2);

  if (value < -10) value = -10; // Limit value to emulate needle end stops
  if (value > 110) value = 110;

  // Move the needle util new value reached
  while (!(value == old_analog)) {
    if (old_analog < value) old_analog++;
    else old_analog--;
    
    if (ms_delay == 0) old_analog = value; // Update immediately id delay is 0
    
    float sdeg = map(old_analog, -10, 110, -150, -30); // Map value to angle 
    // Calcualte tip of needle coords
    float sx = cos(sdeg * 0.0174532925);
    float sy = sin(sdeg * 0.0174532925);

    // Calculate x delta of needle start (does not start at pivot point)
    float tx = tan((sdeg+90) * 0.0174532925);
    
    // Erase old needle image
    tft.drawLine(120 + 20 * ltx - 1, 140 - 20, osx - 1, osy, ILI9341_WHITE);
    tft.drawLine(120 + 20 * ltx, 140 - 20, osx, osy, ILI9341_WHITE);
    tft.drawLine(120 + 20 * ltx + 1, 140 - 20, osx + 1, osy, ILI9341_WHITE);
    
    // Re-plot text under needle
    tft.setTextColor(ILI9341_BLACK);
  //  tft.drawCentreString("%RH", 120, 70, 4); // // Comment out to avoid font 4
    
    // Store new needle end coords for next erase
    ltx = tx;
    osx = sx * 98 + 120;
    osy = sy * 98 + 140;
    
    // Draw the needle in the new postion, magenta makes needle a bit bolder
    // draws 3 lines to thicken needle
    tft.drawLine(120 + 20 * ltx - 1, 140 - 20, osx - 1, osy, ILI9341_RED);
    tft.drawLine(120 + 20 * ltx, 140 - 20, osx, osy, ILI9341_MAGENTA);
    tft.drawLine(120 + 20 * ltx + 1, 140 - 20, osx + 1, osy, ILI9341_RED);
    
    // Slow needle down slightly as it approaches new postion
    if (abs(old_analog - value) < 10) ms_delay += ms_delay / 5;
    
    // Wait before next update
    delay(ms_delay);
  }
}

Teensy 3.1
ILI9341 SPI Display 320x240
Arduino 1.6.5_R5
Teensyduino 1.25_B2
 
Last edited:
Good work Wozzy - my ILI9341 is offline - as I am mostly running house on generator since Sat 8/29. Was hoping toxic might be up to starting on that easy to find tutorial.
 
I was sitting here playing with the new font library when I noticed the post on the forums, so I thought "What the heck! I'll give it a try"
 
Toxic,
If you look at the code I posted, You'll see that you can modify it to produce a speedometer display similar to what you want.
 
Awesome looks like a complete tutorial with a 90% solution. [ Hopefully this is a personal quest and not some assignment ]. Depending on how that sample code was written - there should be a simple translation that will zoom out giving the larger range shown in the crude picture just posted. If you show some work and get stuck perhaps further suggestions would be in order.
 
Your pictures from Spark Fun are nice and simple. How about a simple warmup exercise? Let's draw the spokes of a wagon wheel. For now, we're not going to worry about erasing anything or plotting a value based on analogRead(), etc.

The spokes all start in the middle of the display and radiate outward. The middle of the display has a horizontal coordinate of the width of the display divided in half and a vertical coordinate of the height of the display divided in half. So far, so good? Let's call the starting point (x0, y0).

Now for the other ends of the spokes, we could get out the old graph paper and do some counting, but if there are lots and lots of spokes, there's a quicker way. For this purpose, it is inconvenient that the way we draw a line is by specifying the two end points of the line. What we'd really like is one end point, a direction and a distance. If you search the web for 'convert rectangular to polar coordinates', you should find what you need to make a more convenient line drawing command.

This is where the sine and cosine functions come in handy. We'd like to know (x1, y1) where the distance between (x0, y0) and (x1, y1) is fixed at some value r and the direction varies. Just for fun, let's set r to 1/4 of the smaller of the width and the height. If the display is 240 pixels in the smaller dimension that makes r = 80.

Referring to your second Spark Fun image, there's an angle indicated. Let's call it d for direction. The sine function is the ratio of the side opposite the angle to the hypotenuse (long side) of the triangle. This is just what we want. In the picture the side opposite the angle is the horizontal leg of the triangle. So, x1 = r * sinf(d). The cosine function is the ratio of the side adjacent the angle to the hypotenuse. So, y1 = r * cosf(d).

PIC6_SMALL.jpeg

If you were to plot all the spokes in 15 degree steps, you might discover a little catch. The sinf() and cosf() functions expect the angle in radians, not degrees, which simply means you need to multiply degrees by 2 * pi radians and divide by 360 degrees.

The code to draw wagon spokes might look something like this (note: I haven't compiled this):
Code:
for (float ddeg = 0.0f; ddeg < 360.0f; ddeg += 15.0f) {
  float drad = ddeg * 2.0f * 3.142f / 360.0f;
  float x1 = x0 + r * cosf(drad);
  float y1 = y0 + r * sinf(drad);
  display.drawLine(x0, y0, x1, y1, ILI9341_WHITE);
}

If you're paying close attention, you might have noticed that I swapped sinf and cosf between the text and the code. That's because the angle indicated in the picture is not the conventional one. The convention is to measure the angle from horizontal moving counter-clockwise. From three o'clock measuring back towards two o'clock.
 
Last edited:
Nice summary pictographer - I could work with that - hopefully toxic can. I'd say a wagon wheel was pretty olde schoole - but these days so is a clock with the hand things.
 
Hi,
but where do you add speed value as input to this code?
Toxic

Ultimately you need to assign the speed into the variable MPH in the equation you provided.
What are you using for a speed sensor?
It's hard to help you if you only tell us little bits and pieces of what you are trying to accomplish.
Also see the "Forum Rule" and post the code blocks that that you are trying to make work on the teensy.
It's always a good idea to start out with little steps.
I'm not sure what your experience level is, but maybe a good starting point is the teensy tutorials and examples.
http://www.pjrc.com/teensy/tutorial.html
 
Last edited:
Hi,
Input to speedometer is from rear axle, 48 pulses/ 1 turn 15" wheel.
Yes, add this questions to make a tutorial and it would spare some questions.

Best regards
Toxic
 
Toxic, maybe you've got the wrong impression of how this works.

We're eager to help anyone with an earnest interest in learning and solving problems. It's rewarding to help someone make that lightbulb go on, so to speak. But we can't spare you the frustration of working through challenges yourself. Even if we could, you'd miss out on the learning and pride of doing it yourself.

After you've tried everything you can think of to solve your problem, tell us in detail what you tried and where you're stuck. We'll help if we can.
 
Hi,
Thanks for the advice. Dont worry I will solve it. I have working with electronics since I was 15 year old. This is new to me so I learn, Iḿ trying to understand method and Im not expert on every area. I always ask the stupid question what people miss and I learn new things this way to.

I be back and show some progress :D

Best regards
Toxic
 
Uhm.... Isn't coordinate transformation 8th grade and rotary movement equations 9th grade stuff?
The gauge needle has always the same given fixed length (l_needle), a given fixed origin (x0, y0) and an end point which moves on a circle segment between two angles, alpha_min and alpha_max.
The current angle is a function of the current speed to display (there are minus signs, since mathematically, increasing angles go counterclockwise, while increasing speed will move the needle clockwise):
(1) alpha_curr = alpha_min - (alpha_min - alpha_max) * speed_curr / speed_max
From this, we can immediately calculate the end point coordinates:
(2a) x1 = x0 + l_needle * cos(alpha_curr)
(2b) y1 = y0 + l_needle * sin(alpha_curr)
Now we'll still have to determinate the given start values:
Let's assume that we have a 320 pixel wide and 240 pixel high display, than we choose the center point as a fixed point for the needle:
(3a) x0 = 160
(3b) y0 = 120
The needle should leave some space for numbers around that speedometer, so, instead of the maximum length of 120px (with which it would touch the upper border of the display) we choose 100px which leaves space for a 12px font numbering and some spacing:
(4) l_needle = 100px
In most speedometers, the needle describes a 270° or 3pi/2 angle, thus
(5) alpha_max = alpha_min - 3pi/2
Thus there remains an opening of 90° or pi/2 downwards centered around the vertical axis:
(6) alpha_min = 5pi/4, thus with (5): alpha_max = -pi/4
And here we are. The rest is drawing routine.

Notice: The coordinate equations apply for a mathematical coordinate system, where the point (0,0) is in the lower left corner. Some display drivers locate it in the upper left corner instead. To compensate that, it is sufficient to mirror the y1 coordinate with the modified equation (2b), the others remain unchanged:
(2b) y1 = y0 - l_needle * sin(alpha_curr)

Questions?
 
Status
Not open for further replies.
Back
Top