/*
content of User_Setup.h : (to be re-modified in case library is updated)
// For ESP32 Dev board (only tested with GC9A01 display)
// The hardware SPI can be mapped to any pins
#define TFT_MOSI 5 //23 In some display driver board, it might be written as "SDA" and so on.
#define TFT_SCLK 15 //18
#define TFT_CS 18 //22 Chip select control pin
#define TFT_DC 16 // Data Command control pin
#define TFT_RST 4 // Reset pin (could connect to Arduino RESET pin)
//#define TFT_BL 22 // LED back-light
*/
//#include "SPIFFS.h"
#include <ArduinoJson.h>
#include "NotoSansBold36.h"
#include <TJpg_Decoder.h>
#include <TFT_eSPI.h>
//#include "dial.h"
#include "gauge_backgrounds.h"
#define AA_FONT_LARGE NotoSansBold36
#define MEASURES_COUNT 7
#define MEASURES_TYPE_RES 1 //resistance measure
#define MEASURES_TYPE_VOLT 2 //Volt measure
#define MEASURES_TYPE_RPM 3 //RPM measure
#define NEEDLE_LENGTH 35 // Visible length
#define NEEDLE_WIDTH 5 // Width of needle - make it an odd number
#define NEEDLE_RADIUS 90 // Radius at tip
#define NEEDLE_COLOR1 TFT_MAROON // Needle periphery colour
#define NEEDLE_COLOR2 TFT_RED // Needle centre colour
#define DIAL_CENTRE_X 120
#define DIAL_CENTRE_Y 120
// Define rotary encoder pins
#define PIN_ENC_A 14 // rotary encoder pin A
#define PIN_ENC_B 27 // rotary encoder pin A
#define PIN_ENC_SWITCH 13 // rotary encoder switch pin
#define PIN_ONBOARD_LED 2 // onboard LED pin
#define PIN_VOLTAGE_SENSOR 39 // 13 OK voltage sensor pin - should be in JSON config!
#define VOLTAGE_OFFSET 72 // Voltage offset or voltage sensor output adjustment.
//encoder variables
unsigned long _lastIncReadTime = micros();
unsigned long _lastDecReadTime = micros();
int _pauseLength = 25000;
int _fastIncrement = 1;
volatile int counter = 0;
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite needle = TFT_eSprite(&tft); // Sprite object for needle
TFT_eSprite spr = TFT_eSprite(&tft); // Sprite for meter reading
uint16_t* tft_buffer;
bool buffer_loaded = false;
uint16_t spr_width = 0;
uint16_t bg_color = 0;
const char* config_filename = "/config.json";
//measures configuration strucure & array
struct Measure {
int id;
String name;
int type;
int min;
int max;
double value;
int angle;
int pin;
int refResistance;
int backgroundID;
};
Measure measures[MEASURES_COUNT];
volatile int currentMeasureId = 0;
int previousMeasureId = -1; //
// =======================================================================================
// This function will be called during decoding of the jpeg file
// =======================================================================================
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
// Stop further decoding as image is running off bottom of screen
if (y >= tft.height()) return 0;
// This function will clip the image block rendering automatically at the TFT boundaries
tft.pushImage(x, y, w, h, bitmap);
// Return 1 to decode next block
return 1;
}
// =======================================================================================
// Setup
// =======================================================================================
void setup() {
Serial.begin(115200); // Debug only 115200
// pinMode(ONBOARD_LED,OUTPUT);
Serial.println("Start Set Up");
// Set encoder pins and attach interrupts
pinMode(PIN_ENC_A, INPUT_PULLUP);
pinMode(PIN_ENC_B, INPUT_PULLUP);
pinMode(PIN_ENC_SWITCH, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_A), read_encoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_B), read_encoder, CHANGE);
// open the configuration JSON file
// if (!SPIFFS.begin(true)) {
// Serial.println("An Error has occurred while mounting SPIFFS");
// return;
// }
// File file = SPIFFS.open(config_filename);
// if (!file) {
// Serial.println("Failed to open file for reading");
// return;
// }
const char* file = R"RAW(
{"measures":[{"ID":1,"name":"Oil Temp.","type":1,"min":30,"max":150,"pin":25,"refResistance":10,"backgroundID":0},{"ID":2,"name":"Oil Pres.","type":1,"min":1,"max":6,"pin":32,"refResistance":10,"backgroundID":0},{"ID":3,"name":"Head Temp.","type":1,"min":30,"max":150,"pin":26,"refResistance":10,"backgroundID":0},{"ID":4,"name":"Fuel","type":1,"min":0,"max":40,"pin":34,"refResistance":60,"backgroundID":0},{"ID":5,"name":"Fuel Res.","type":1,"min":0,"max":1,"pin":35,"refResistance":10,"backgroundID":0},{"ID":6,"name":"Tension","type":2,"min":9,"max":12,"pin":39,"backgroundID":1},{"ID":7,"name":"RPM","type":3,"min":0,"max":5000,"pin":0,"backgroundID":0}]}
)RAW";
StaticJsonDocument<1024> doc;
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, file);
if (error) {
Serial.println(F("Failed to read file, using default configuration"));
Serial.println(error.c_str());
}
//copy values from JSON document to global variable
for (int i = 0; i < MEASURES_COUNT; i++) {
//measures[i].name = strdup(doc["measures"][i]["name"]);
measures[i].name = doc["measures"][i]["name"].as<String>(); // | "undefined";
measures[i].type = doc["measures"][i]["type"] | 1;
measures[i].min = doc["measures"][i]["min"] | 0;
measures[i].max = doc["measures"][i]["max"] | 100;
measures[i].pin = doc["measures"][i]["pin"] | 0;
measures[i].refResistance = doc["measures"][i]["refResistance"] | 0;
measures[i].backgroundID = doc["measures"][i]["backgroundID"] | 0;
}
//close json and file
//file.close();
doc.clear();
Serial.println("Measures configuration :");
// looping through measures now to double check...
for (int i = 0; i < MEASURES_COUNT; i++) {
Serial.print("i = ");
Serial.print(i);
Serial.print(" / name = ");
Serial.print(measures[i].name);
Serial.print(" / pin = ");
Serial.print(measures[i].pin);
Serial.print(" / type = ");
Serial.print(measures[i].type);
Serial.print(" / refResistance = ");
Serial.print(measures[i].refResistance);
Serial.print(" / backgroundID = ");
Serial.print(measures[i].backgroundID);
Serial.println("");
}
// The byte order can be swapped (set true for TFT_eSPI)
TJpgDec.setSwapBytes(true);
// The jpeg decoder must be given the exact name of the rendering function above
TJpgDec.setCallback(tft_output);
tft.begin();
tft.setRotation(0);
drawGaugeBackground();
// Define where the needle pivot point is on the TFT before
// creating the needle so boundary calculation is correct
tft.setPivot(DIAL_CENTRE_X, DIAL_CENTRE_Y);
// Create the needle Sprite
createNeedle();
// Reset needle position to 0
plotNeedle(0, 0);
// delay(2000);
Serial.println("--- SET UP DONE ---");
}
// =======================================================================================
// Loop
// =======================================================================================
void loop() {
static int lastCounter = 50;
// uint16_t angle = random(241); // random speed in range 0 to 240
//Serial.print ("angle=");
//Serial.println (angle);
// If count has changed print the new value to serial
if (counter != lastCounter) {
Serial.print("Counter=");
Serial.println(counter);
lastCounter = counter;
}
//read measures values
for (int i = 0; i < MEASURES_COUNT; i++) {
measures[i].value = 0; // TO DO
switch (measures[i].type) {
case MEASURES_TYPE_RES:
// read resistance using pin and refResistance values
measures[i].value = random(241);
measures[i].angle = measures[i].value;
break;
case MEASURES_TYPE_VOLT:
measures[i].value = readVoltage(measures[i].pin);
measures[i].angle = map( measures[i].value , 0, 16.5, 0, 241);
break;
case MEASURES_TYPE_RPM:
// read RPM on pin value
measures[i].value = random(241);
measures[i].angle = measures[i].value;
break;
}
}
//Display the value of current selected measure
if (currentMeasureId != previousMeasureId) {
//someone turned the encoder, currentMeasureId has changed, let's display the new background for the new measureId
previousMeasureId = currentMeasureId;
//DISPLAY NEW BG
}
int buttonState = digitalRead(PIN_ENC_SWITCH);
if(buttonState == 1){
Serial.print("button pressed! ");
Serial.println(buttonState);
digitalWrite(PIN_ONBOARD_LED,HIGH);
delay(100);
digitalWrite(PIN_ONBOARD_LED,LOW);
}
//double voltage = readVoltage(PIN_VOLTAGE_SENSOR);
//Serial.println( voltage );//print the voltage
// Plot needle at random angle in range 0 to 240, speed 40ms per increment
//plotNeedle(counter, 10); //angle
plotNeedle( measures[currentMeasureId].angle , 10); //angle
// Pause at new position
//delay(500);
}
// =======================================================================================
// Draw Gauge Background
// =======================================================================================
void drawGaugeBackground(){
tft.fillScreen(TFT_BLACK);
// Draw the dial
TJpgDec.drawJpg(0, 0, gauge_background[ measures[currentMeasureId].backgroundID ], sizeof( gauge_background[ measures[currentMeasureId].backgroundID ] ));
tft.drawCircle(DIAL_CENTRE_X, DIAL_CENTRE_Y, NEEDLE_RADIUS - NEEDLE_LENGTH, TFT_DARKGREY);
// Load the font and create the Sprite for reporting the value
spr.loadFont(AA_FONT_LARGE);
spr_width = spr.textWidth("7777"); // 7 is widest numeral in this font
spr.createSprite(spr_width, spr.fontHeight());
bg_color = tft.readPixel(120, 120); // Get colour from dial centre
spr.fillSprite(bg_color);
spr.setTextColor(TFT_WHITE, bg_color, true);
spr.setTextDatum(MC_DATUM);
spr.setTextPadding(spr_width);
spr.drawNumber(0, spr_width / 2, spr.fontHeight() / 2);
spr.pushSprite(DIAL_CENTRE_X - spr_width / 2, DIAL_CENTRE_Y - spr.fontHeight() / 2);
// Plot the label text
tft.setTextColor(TFT_WHITE, bg_color);
tft.setTextDatum(MC_DATUM);
tft.drawString( measures[currentMeasureId].name , DIAL_CENTRE_X, DIAL_CENTRE_Y + 48, 2);
}
// =======================================================================================
// Create the needle Sprite
// =======================================================================================
void createNeedle(void) {
needle.setColorDepth(16);
needle.createSprite(NEEDLE_WIDTH, NEEDLE_LENGTH); // create the needle Sprite
needle.fillSprite(TFT_BLACK); // Fill with black
// Define needle pivot point relative to top left corner of Sprite
uint16_t piv_x = NEEDLE_WIDTH / 2; // pivot x in Sprite (middle)
uint16_t piv_y = NEEDLE_RADIUS; // pivot y in Sprite
needle.setPivot(piv_x, piv_y); // Set pivot point in this Sprite
// Draw the red needle in the Sprite
needle.fillRect(0, 0, NEEDLE_WIDTH, NEEDLE_LENGTH, TFT_MAROON);
needle.fillRect(1, 1, NEEDLE_WIDTH - 2, NEEDLE_LENGTH - 2, TFT_RED);
// Bounding box parameters to be populated
int16_t min_x;
int16_t min_y;
int16_t max_x;
int16_t max_y;
// Work out the worst case area that must be grabbed from the TFT,
// this is at a 45 degree rotation
needle.getRotatedBounds(45, &min_x, &min_y, &max_x, &max_y);
// Calculate the size and allocate the buffer for the grabbed TFT area
tft_buffer = (uint16_t*)malloc(((max_x - min_x) + 2) * ((max_y - min_y) + 2) * 2);
}
// =======================================================================================
// Move the needle to a new position
// =======================================================================================
void plotNeedle(int16_t angle, uint16_t ms_delay) {
static int16_t old_angle = -120; // Starts at -120 degrees
// Bounding box parameters
static int16_t min_x;
static int16_t min_y;
static int16_t max_x;
static int16_t max_y;
if (angle < 0) angle = 0; // Limit angle to emulate needle end stops
if (angle > 240) angle = 240;
angle -= 120; // Starts at -120 degrees
// Move the needle until new angle reached
while (angle != old_angle || !buffer_loaded) {
if (old_angle < angle) old_angle++;
else old_angle--;
// Only plot needle at even values to improve plotting performance
if ((old_angle & 1) == 0) {
if (buffer_loaded) {
// Paste back the original needle free image area
tft.pushRect(min_x, min_y, 1 + max_x - min_x, 1 + max_y - min_y, tft_buffer);
}
if (needle.getRotatedBounds(old_angle, &min_x, &min_y, &max_x, &max_y)) {
// Grab a copy of the area before needle is drawn
tft.readRect(min_x, min_y, 1 + max_x - min_x, 1 + max_y - min_y, tft_buffer);
buffer_loaded = true;
}
// Draw the needle in the new position, black in needle image is transparent
needle.pushRotated(old_angle, TFT_BLACK);
// Wait before next update
delay(ms_delay);
}
// Update the number at the centre of the dial
spr.setTextColor(TFT_WHITE, bg_color, true);
spr.drawNumber(old_angle + 120, spr_width / 2, spr.fontHeight() / 2);
spr.pushSprite(120 - spr_width / 2, 120 - spr.fontHeight() / 2);
// Slow needle down slightly as it approaches the new position
if (abs(old_angle - angle) < 10) ms_delay += ms_delay / 5;
}
}
// =======================================================================================
// READ ENCODER - called by interrupts on pins ENC_A & ENC_B
// Code from : https://github.com/mo-thunderz/RotaryEncoder/
// YT video : https://www.youtube.com/watch?v=fgOfSHTYeio&t=532s&ab_channel=MoThunderz
// =======================================================================================
void read_encoder() {
// Encoder interrupt routine for both pins. Updates counter
// if they are valid and have rotated a full indent
static uint8_t old_AB = 3; // Lookup table index
static int8_t encval = 0; // Encoder value
static const int8_t enc_states[] = { 0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0 }; // Lookup table
old_AB <<= 2; // Remember previous state
if (digitalRead(PIN_ENC_A)) old_AB |= 0x02; // Add current state of pin A
if (digitalRead(PIN_ENC_B)) old_AB |= 0x01; // Add current state of pin B
encval += enc_states[(old_AB & 0x0f)];
// Update counter if encoder has rotated a full indent, that is at least 4 steps
if (encval > 3) { // Four steps forward
int changevalue = 1;
if ((micros() - _lastIncReadTime) < _pauseLength) {
changevalue = _fastIncrement * changevalue;
}
_lastIncReadTime = micros();
counter = counter + changevalue; // Update counter
encval = 0;
} else if (encval < -3) { // Four steps backward
int changevalue = -1;
if ((micros() - _lastDecReadTime) < _pauseLength) {
changevalue = _fastIncrement * changevalue;
}
_lastDecReadTime = micros();
counter = counter + changevalue; // Update counter
encval = 0;
}
}
// =======================================================================================
// READ VOLTAGE
// =======================================================================================
double readVoltage(int pin) {
int volt = analogRead(pin); // read the input from the pin passed as parameter
double voltage = map(volt, 0, 4095, 0, 1650) + VOLTAGE_OFFSET; // map 0-4095 to 0-1650 and add correction offset
voltage /= 100; // divide by 100 to get the decimal values
return voltage;
}