/* niceGauge
by VolosR
GitHub: https://github.com/VolosR/TDisplayDashboard/blob/main/niceGauge/niceGauge.ino
YouTube: https://www.youtube.com/watch?v=cBtsLxZ13hQ
based on T-Display S3 (ESP32) development board: https://www.lilygo.cc/products/t-display-s3?bg_ref=7LJo9u5wwo
**/
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft);
//............INPUT PINS...............switches and buttons
#define THROTTLE 43
#define BRAKE 44
#define LEFT 17 // turn indicator - left
#define RIGHT 18 // turn indicator - right
#define SHORT 16 // headlights - dipped
#define LONG 21 // headlights - high beams
#define GEARUP 12
#define GEARDOWN 13
#define HORN 10
#define BRIGHTNESS 14
//............OUTPUT PINS..............lights, pointes, horn
#define left_pointer 1 // turn indicator - left
#define right_pointer 11 // turn indicator - right
#define head_lights 2 // headlights
#define buzzer 3 // horn
//.....................................colors
#define backColor 0x0026
#define gaugeColor 0x055D
#define dataColor 0x0311
#define purple 0xEA16
#define needleColor 0xF811
//.....................................dont edit this
int cx = 75;
int cy = 75;
int r = 72;
int ir = 70;
int n = 0;
int angle = 0;
// collection of points (x,y) for outer circle of Speed gauge
float x[360];
float y[360];
// collection of points (x,y) for inner circle of Speed gauge
float px[360];
float py[360];
// collection of points (x,y) for text markers of Speed gauge
float lx[360];
float ly[360];
// collection of points (x,y) for needle (low) marker of Speed gauge
float nx[360];
float ny[360];
// collection of points (x,y) for outer circle of RPM gauge
float x2[360];
float y2[360];
// collection of points (x,y) for inner cicrcle of RPM gauge
float px2[360];
float py2[360];
// collection of points (x,y) for text markers of RPM gauge
float lx2[360];
float ly2[360];
// collection of points (x,y) for needle (low) markers of RPM gauge
float nx2[360];
float ny2[360];
//.....................................runtime variables
double rad = 0.01745;
unsigned short color1;
unsigned short color2;
float sA;
float rA;
int blinkPeriod = 500;
unsigned long currentTimeL = 0;
unsigned long currentTimeR = 0;
int brightnesses[5] = {40, 80, 120, 150, 240};
int selectedBrightness = 3;
int deb1 = 0;
int deb2 = 0;
int debB = 0;
//.....................................gears
int gearMaxSpeed[8] = {12, 0, 60, 90, 120, 150, 190, 236};
String gears[8] = {"R", "N", "1", "2", "3", "4", "5", "6"};
int selectedGear = 1;
//.....................................colors
unsigned short blockColor[4] = {0x0312, 0x0290, 0x01EC, 0x016A};
unsigned short dirColor[2] = {0x0312, TFT_ORANGE};
unsigned short lightColor[3] = {0x01EC, 0x0FA8, 0xB79F};
//.....................................important variables
bool leftPointer = 0;
bool rightPointer = 0;
bool braking;
int lights = 0; // 0 is lights off, 1 is short light, 2 is long lights
float speedAngle = 0; //...speed variable 0-240
float rpmAngle = 5; //.....RPM variable 0-9
void setup() {
pinMode(THROTTLE, INPUT_PULLUP);
pinMode(BRAKE, INPUT_PULLUP);
pinMode(LEFT, INPUT_PULLUP);
pinMode(RIGHT, INPUT_PULLUP);
pinMode(GEARUP, INPUT_PULLUP);
pinMode(GEARDOWN, INPUT_PULLUP);
pinMode(SHORT, INPUT_PULLUP);
pinMode(LONG, INPUT_PULLUP);
pinMode(HORN, INPUT_PULLUP);
pinMode(BRIGHTNESS, INPUT_PULLUP);
pinMode(left_pointer, OUTPUT);
pinMode(right_pointer, OUTPUT);
pinMode(head_lights, OUTPUT);
pinMode(buzzer, OUTPUT);
tft.init();
tft.setRotation(1);
tft.fillScreen(backColor);
sprite.createSprite(320, 150);
sprite.setSwapBytes(true);
sprite.setTextDatum(4);
sprite.setTextColor(TFT_WHITE, backColor);
sprite.setTextDatum(4);
ledcSetup(0, 10000, 8);
ledcAttachPin(38, 0);
ledcWrite(0, brightnesses[selectedBrightness]); // brightnes of screen
ledcSetup(1, 10000, 8);
ledcAttachPin(head_lights, 1);
//ledcWrite(1, 10);
int a = 120;
for (int i = 0; i < 360; i++) {
// points (x,y) along outer circle of Speed gauge in 1 degree increments
x[i] = ((r - 10) * cos(rad * a)) + cx;
y[i] = ((r - 10) * sin(rad * a)) + cy;
// points (x,y) along inner circle of Speed gauge in 1 degree increments
px[i] = ((r - 14) * cos(rad * a)) + cx;
py[i] = ((r - 14) * sin(rad * a)) + cy;
// points (x,y) along circle for text labels of Speed gauge in 1 degree increments
lx[i] = ((r - 24) * cos(rad * a)) + cx;
ly[i] = ((r - 24) * sin(rad * a)) + cy;
// points (x,y) along circle for needle (low) of Speed gauge in 1 degree increments
nx[i] = ((r - 36) * cos(rad * a)) + cx;
ny[i] = ((r - 36) * sin(rad * a)) + cy;
// points (x,y) along outer circle of RPM gauge in 1 degree increments
x2[i] = ((r - 10) * cos(rad * a)) + 320 - cx;
y2[i] = ((r - 10) * sin(rad * a)) + cy;
// points (x,y) along inner circle of RPM gauge in 1 degree increments
px2[i] = ((r - 14) * cos(rad * a)) + 320 - cx;
py2[i] = ((r - 14) * sin(rad * a)) + cy;
// points (x,y) along circle for text labels of RPM gauge in 1 degree increments
lx2[i] = ((r - 24) * cos(rad * a)) + 320 - cx;
ly2[i] = ((r - 24) * sin(rad * a)) + cy;
// points (x,y) along circle for needle (low) of RPM gauge in 1 degree increments
nx2[i] = ((r - 36) * cos(rad * a)) + 320 - cx;
ny2[i] = ((r - 36) * sin(rad * a)) + cy;
a++;
if (a == 360) {
a = 0;
}
}
}
void draw() {
sprite.fillSprite(backColor); // clear sprite by filling with black
// draw four rectangles vertically in the center of the screen
// these will later be used for:
// fuel level gauge (120,28)
// low fuel indicator (120,52)
// light icon (120,76)
// odometer (120,100)
for (int i = 0; i < 4; i++) {
sprite.fillRect(120, 28 + (24 * i), 80, 22, blockColor[i]);
}
// draw a number of orange rectagles corresponding to select level of illumination
// Level 1 (8,6)
// Level 2 (12,6)
// Level 3 (16,6)
// Level 4 (20,6)
// Level 5 (24,6)
for (int i = 0; i < selectedBrightness; i++) {
sprite.fillSmoothRoundRect(8 + (4 * i), 6, 2, 9, 1, TFT_ORANGE, backColor);
}
// clear SPEED & RPM gauge by filling with black circle
sprite.fillSmoothCircle(cx, cy, r + 2, backColor);
sprite.fillSmoothCircle(320 - cx, cy, r + 2, backColor);
// draw turn indicator - left
sprite.fillTriangle(126, 14, 136, 7, 136, 21, dirColor[leftPointer]);
sprite.fillRect(136, 11, 8, 7, dirColor[leftPointer]);
// draw turn indicator - left
sprite.fillTriangle(126 + 68, 14, 136 + 48, 7, 136 + 48, 21, dirColor[rightPointer]);
sprite.fillRect(176, 11, 8, 7, dirColor[rightPointer]);
// draw number of filled/empty squares corresponding to fuel level
// Level 1 (144,36)
// Level 2 (151,36)
// Level 3 (158,36)
// Level 4 (165,36)
// Level 5 (172,36)
for (int i = 0; i < 5; i++) {
// fuel level is hard coded as first three squares (i <= 2)
if (i <= 2) {
sprite.fillRect(144 + (7 * i), 36, 5, 5, TFT_WHITE);
} else {
sprite.drawRect(144 + (7 * i), 36, 5, 5, TFT_WHITE);
}
}
// draw low fuel indicator
sprite.fillSmoothRoundRect(155, 54, 9, 16, 2, TFT_WHITE, blockColor[1]);
sprite.fillSmoothRoundRect(166, 56, 2, 14, 2, TFT_WHITE, blockColor[1]);
sprite.fillSmoothRoundRect(156, 56, 7, 5, 1, blockColor[1], TFT_WHITE);
sprite.drawLine(153, 69, 166, 69, TFT_WHITE);
// draw light blue ring of SPEED & RPM gauge (outer most ring)
sprite.drawSmoothArc(cx, cy, r, ir, 30, 330, gaugeColor, backColor);
sprite.drawSmoothArc(320 - cx, cy, r, ir, 30, 330, gaugeColor, backColor);
// draw white ring of SPEED & RPM gauge (first inner ring)
sprite.drawSmoothArc(cx, cy, r - 5, r - 6, 30, 330, TFT_WHITE, backColor);
sprite.drawSmoothArc(320 - cx, cy, r - 5, r - 6, 30, 330, TFT_WHITE, backColor);
// draw purple ring of SPEED gauge connecting last few ticks to create danger area
sprite.drawSmoothArc(cx, cy, r - 9, r - 8, 270, 330, purple, backColor);
// todo - add red ranger area for RPM gauge
// draw light blue ring of SPEED & RPM gauge (inner most ring)
sprite.drawSmoothArc(cx, cy, r - 38, ir - 37, 10, 350, gaugeColor, backColor);
sprite.drawSmoothArc(320 - cx, cy, r - 38, ir - 37, 10, 350, gaugeColor, backColor);
// draw 26 tick marks and 13 values for SPEED gauge
for (int i = 0; i < 26; i++) {
if (i < 20) {
color1 = gaugeColor;
color2 = TFT_WHITE;
} else {
color1 = purple;
color2 = purple;
}
// draw minor & major tick marks with alternating color
// and values for major tick marks
if (i % 2 == 0) {
sprite.drawWedgeLine(x[i * 12], y[i * 12], px[i * 12], py[i * 12], 2, 1, color1);
sprite.setTextColor(color2, backColor);
sprite.drawString(String(i * 10), lx[i * 12], ly[i * 12]);
} else {
sprite.drawWedgeLine(x[i * 12], y[i * 12], px[i * 12], py[i * 12], 1, 1, color2);
}
}
// draw 19 tick marks and 10 values for RPM gauge
for (int i = 0; i < 19; i++) {
if (i < 20) {
color1 = gaugeColor;
color2 = TFT_WHITE;
} else {
// not enough tick marks to enter this block (todo - add red danger area here)
color1 = purple;
color2 = purple;
}
// draw minor & major tick marks with alternating color
// and values for major tick marks
if (i % 2 == 0) {
sprite.drawWedgeLine(x2[i * 16], y2[i * 16], px2[i * 16], py2[i * 16], 2, 1, color1);
sprite.setTextColor(color2, backColor);
sprite.drawString(String(i / 2), lx2[i * 16], ly2[i * 16]);
} else {
sprite.drawWedgeLine(x2[i * 16], y2[i * 16], px2[i * 16], py2[i * 16], 1, 1, color2);
}
}
// draw needle indicators for SPEED & RPM gauges
sA = speedAngle * 1.2;
rA = 2 * rpmAngle * 1.6;
sprite.drawWedgeLine(px[(int)sA], py[(int)sA], nx[(int)sA], ny[(int)sA], 2, 2, needleColor);
sprite.drawWedgeLine(px2[(int)rA], py2[(int)rA], nx2[(int)rA], ny2[(int)rA], 2, 2, needleColor);
// draw speed value and measurement units in the center of SPEED gauge
sprite.setTextColor(TFT_WHITE, backColor);
sprite.drawString(String((int)speedAngle), cx, cy, 4);
sprite.drawString("KM/H", cx, cy + 16);
// draw selected gear value in the center of RPM gauge
sprite.drawString(String(gears[selectedGear]), 320 - cx, cy, 4);
sprite.drawString("GEAR", 320 - cx, cy + 16);
// draw odometer reading
sprite.setTextColor(TFT_WHITE, blockColor[3]);
sprite.drawString("14356", 160, 110);
// draw unit scaling for RPM gauge
sprite.setTextColor(TFT_ORANGE, backColor);
sprite.drawString("x1000", 320 - cx, 136);
sprite.drawString("RPM", 320 - cx, 126);
// drawing parking brake symbol (between turn indicators)
if (braking == true) {
sprite.drawSmoothArc(160, 10, 9, 8, 10, 350, TFT_RED, backColor);
sprite.drawSmoothArc(160, 10, 12, 11, 50, 130, TFT_RED, backColor);
sprite.drawSmoothArc(160, 10, 12, 11, 230, 310, TFT_RED, backColor);
//sprite.drawCircle(320-cx, cy-20, 6, TFT_RED);
sprite.setTextColor(TFT_RED, backColor);
sprite.drawString("!", 160, 12, 2);
}
// drawing headlight indicator
sprite.fillSmoothRoundRect(152, 82, 14, 10, 7, lightColor[lights], lightColor[0]);
sprite.fillRect(161, 82, 5, 10, lightColor[0]);
sprite.drawLine(163, 82, 167, 84 - lights, lightColor[lights]);
sprite.drawLine(163, 85, 167, 87 - lights, lightColor[lights]);
sprite.drawLine(163, 88, 167, 90 - lights, lightColor[lights]);
sprite.drawLine(163, 91, 167, 93 - lights, lightColor[lights]);
// draw dot in top right corner (not sure what this is for)
sprite.fillSmoothCircle(300, 10, 4, TFT_RED, backColor);
// finally push computed sprite to screen
sprite.pushSprite(0, 10);
}
int blinking = 1;
void loop() {
// check lights status
if (digitalRead(SHORT) == 0) {
lights = 1;
} else if (digitalRead(LONG) == 0) {
lights = 2;
} else {
lights = 0;
}
ledcWrite(1, lights * 4);
if (digitalRead(HORN) == 0) {
digitalWrite(buzzer, 1);
} else {
digitalWrite(buzzer, 0);
}
braking = !(digitalRead(BRAKE));
if (digitalRead(BRIGHTNESS) == 0) {
if (debB == 0) {
debB = 1;
selectedBrightness++;
if (selectedBrightness == 5) {
selectedBrightness = 0;
}
ledcWrite(0, brightnesses[selectedBrightness]); // brightnes of screen
}
} else {
debB = 0;
}
if (digitalRead(GEARUP) == 0) {
if (deb1 == 0) {
deb1 = 1;
if (selectedGear < 7) {
selectedGear++;
}
if (speedAngle > 10) {
speedAngle = speedAngle - 4;
}
}
} else {
deb1 = 0;
}
if (digitalRead(GEARDOWN) == 0) {
if (deb2 == 0) {
deb2 = 1;
if (selectedGear > 0) {
selectedGear--;
}
if (speedAngle > 10) {
speedAngle = speedAngle - 4;
}
}
} else {
deb2 = 0;
}
if (digitalRead(LEFT) == 0) {
if (millis() > currentTimeL + blinkPeriod) {
leftPointer = !leftPointer;
digitalWrite(left_pointer, leftPointer);
currentTimeL = millis();
}
} else {
leftPointer = 0;
digitalWrite(left_pointer, leftPointer);
}
if (digitalRead(RIGHT) == 0) {
if (millis() > currentTimeR + blinkPeriod) {
rightPointer = !rightPointer;
digitalWrite(right_pointer, rightPointer);
currentTimeR = millis();
}
} else {
rightPointer = 0;
digitalWrite(right_pointer, rightPointer);
}
if (braking == true && speedAngle > 4) {
speedAngle = speedAngle - 4;
}
if (speedAngle < 0) {
speedAngle = 0;
}
draw();
if (digitalRead(THROTTLE) == 0 && speedAngle < gearMaxSpeed[selectedGear]) {
speedAngle = speedAngle + 2 - (0.24 * selectedGear);
}
if (digitalRead(THROTTLE) == 1 && speedAngle > 0) {
speedAngle--;
}
if (digitalRead(THROTTLE) == 0 && rpmAngle < 75) {
rpmAngle = rpmAngle + 1 - (0.1 * selectedGear);
}
if (digitalRead(THROTTLE) == 1 && rpmAngle > 0) {
if (rpmAngle >= 3) {
rpmAngle = rpmAngle - 3;
} else {
rpmAngle = 0;
}
}
}