// simple project using Arduino UNO and 16x2 character display to display smooth gauge,
// created by upir, 2022
// youtube channel: https://www.youtube.com/upir_upir
// FULL TUTORIAL: https://youtu.be/ZzIGHiHObYw
// GAUGE IN 11 MINUTES TUTORIAL: https://youtu.be/upE17NHrdPc
// Links related to this project:
// Arduino UNO - https://s.click.aliexpress.com/e/_AXDw1h
// Arduino breadboard prototyping shield - https://s.click.aliexpress.com/e/_ApbCwx
// 16x2 displays with IIC - https://s.click.aliexpress.com/e/_9Hl3JV
// 16x2 display with RGB backlight - https://s.click.aliexpress.com/e/_9wgpeb
// original sketch from YWROBOT - https://wokwi.com/arduino/libraries/LiquidCrystal_I2C/HelloWorld
// character creator - https://tusindfryd.github.io/screenduino/
// another character creator - https://maxpromer.github.io/LCD-Character-Creator/
// sprintf explanation - https://www.programmingelectronics.com/sprintf-arduino/
// custom characters simplest project - https://wokwi.com/projects/294395602645549578
// Arduino I2C scanner - https://playground.arduino.cc/Main/I2cScanner/
// 16x2 available characters - https://docs.wokwi.com/parts/wokwi-lcd1602#font
// Bitwise Operators in GIFs - https://blog.wokwi.com/bitwise-operators-in-gifs/
// Bitwise operators Arduino documentation - https://www.arduino.cc/reference/en/language/structure/bitwise-operators/bitshiftleft/
#include <LiquidCrystal_I2C.h> // if you don´t have I2C version of the display, use LiquidCrystal.h library instead
LiquidCrystal_I2C lcd(0x27,16,2); // set the LCD address to 0x27 for a 16 chars and 2 line display
//LiquidCrystal_I2C lcd(0x3f,16,2); // set the LCD address to 0x3f for a 16 chars and 2 line display
// if you don´t know the I2C address of the display, use I2C scanner first (https://playground.arduino.cc/Main/I2cScanner/)
byte gauge_empty[8] = {B11111, B00000, B00000, B00000, B00000, B00000, B00000, B11111}; // empty middle piece
byte gauge_fill_1[8] = {B11111, B10000, B10000, B10000, B10000, B10000, B10000, B11111}; // filled gauge - 1 column
byte gauge_fill_2[8] = {B11111, B11000, B11000, B11000, B11000, B11000, B11000, B11111}; // filled gauge - 2 columns
byte gauge_fill_3[8] = {B11111, B11100, B11100, B11100, B11100, B11100, B11100, B11111}; // filled gauge - 3 columns
byte gauge_fill_4[8] = {B11111, B11110, B11110, B11110, B11110, B11110, B11110, B11111}; // filled gauge - 4 columns
byte gauge_fill_5[8] = {B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111}; // filled gauge - 5 columns
byte gauge_left[8] = {B11111, B10000, B10000, B10000, B10000, B10000, B10000, B11111}; // left part of gauge - empty
byte gauge_right[8] = {B11111, B00001, B00001, B00001, B00001, B00001, B00001, B11111}; // right part of gauge - empty
byte gauge_mask_left[8] = {B01111, B11111, B11111, B11111, B11111, B11111, B11111, B01111}; // mask for rounded corners for leftmost character
byte gauge_mask_right[8] = {B11110, B11111, B11111, B11111, B11111, B11111, B11111, B11110}; // mask for rounded corners for rightmost character
byte gauge_left_dynamic[8]; // left part of gauge dynamic - will be set in the loop function
byte gauge_right_dynamic[8]; // right part of gauge dynamic - will be set in the loop function
int cpu_gauge = 0; // value for the CPU gauge
char buffer[10]; // helper buffer to store C-style strings (generated with sprintf function)
int move_offset = 0; // used to shift bits for the custom characters
const int gauge_size_chars = 8; // width of the gauge in number of characters
char gauge_string[gauge_size_chars+1]; // string that will include all the gauge character to be printed
/////////////////Varibles for global Main and Function//////////////////////////
int8_t distance,stepEnc,menuSet,setTemp,temperature;
int16_t volume, minDistance,maxDistance,duration,maxVolume,minVolume,criticalLivel;
bool setUpdateDisplay, setMenu,flagEnc,encRight,encLeft,autoSet,vaterRelay,minLivel,maxLivel;
//Encoder
#define C1 2
#define C2 8
//Ultrasound Sensor
#define trigPin 13
#define echoPin 12
///////////////////////////Setup//////////////////////////////////
void setup()
{
Serial.begin(9600);
/////////////LCD Set/////////////
lcd.init(); // initialize the 16x2 lcd module
lcd.createChar(7, gauge_empty); // middle empty gauge
lcd.createChar(1, gauge_fill_1); // filled gauge - 1 column
lcd.createChar(2, gauge_fill_2); // filled gauge - 2 columns
lcd.createChar(3, gauge_fill_3); // filled gauge - 3 columns
lcd.createChar(4, gauge_fill_4); // filled gauge - 4 columns
lcd.backlight(); // enable backlight for the LCD module
/////////////Set Button/////////////
pinMode(5, OUTPUT);//relay
pinMode(10, INPUT_PULLUP); //Buton menu touchbutton
pinMode(9, INPUT_PULLUP); //Button select menu encoder
pinMode(C2, INPUT); //encoder pin clock
pinMode(C1, INPUT); //encoder pin data
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
//interupt
attachInterrupt(INT0, enc1, FALLING);
/////////////////Varibles set//////////
volume = 500;
minDistance=118;
maxDistance=23390;
temperature=0;
maxVolume = 500;
minVolume = 100;
criticalLivel = 30;
}
void loop()
{
vaterRelayVal =vaterRelay;
encRight = encLeft = 0;
static bool button;
if (digitalRead(10) == 0) {bool newbutton = 1; setUpdateDisplay = 1;
if(button == !newbutton) setMenu = !setMenu;
button = 1;
} else button =0;
/////////////////////////////Main Display Set/////////////
if (setMenu == 0) {
// if ((millis()/1000)%2){setUpdateDisplay = 1;}
// if (setUpdateDisplay) {
getLivel();
setLivelMetr();
setDisplayInfo();
//}
setUpdateDisplay = 0;
}
//////////////////////////Display Setup////////////////////////
if(setMenu == 1){
static bool button1;
if (digitalRead(9) == 0) { bool newbutton = 1;
if(button1 == !newbutton){ menuSet ++; if(menuSet>9){menuSet=0; setUpdateDisplay = 1;} setUpdateDisplay = 1;}
button1 = 1;
} else button1 =0;
// if (flagEnc) { // если изменение было
// setDisplayMenu(&stepEnc);
// flagEnc = 0; // сбрасываем флаг
//}
int8_t point = menuSet;
switch(menuSet){
if (flagEnc) {setUpdateDisplay=1;}
case 0 : if(encRight){autoSet = !autoSet; setUpdateDisplay=1; } if(encLeft){autoSet = !autoSet; setUpdateDisplay=1;} break;
case 1 : if (autoSet){getLivel();setLivelMetr();{if(encRight){if(cpu_gauge < volume){vaterRelay = !vaterRelay;} setUpdateDisplay=1; } if(encLeft){if(cpu_gauge < volume){vaterRelay = !vaterRelay;} setUpdateDisplay=1;} }}
else {if(encRight){ if(maxVolume < volume){maxVolume += 10; setUpdateDisplay=1;} } if(encLeft){if(maxVolume>0){maxVolume -= 10; setUpdateDisplay=1;}}} break;
case 2 : if(encRight){volume +=10; setUpdateDisplay=1; } if(encLeft){volume -=10; setUpdateDisplay=1;} break;
case 3 : if(!autoSet){if(encRight){if(minVolume <( maxVolume - 100)){minVolume +=10; setUpdateDisplay=1; }} if(encLeft)if(minVolume > criticalLivel){{minVolume -=10;setUpdateDisplay=1; }}} break;
case 4 : if(encRight){if(criticalLivel<( maxVolume - 100)){criticalLivel +=10; setUpdateDisplay=1;} } if(encLeft){if(criticalLivel>10){criticalLivel -=10; setUpdateDisplay=1;}} break;
case 5 : if(encRight){if(criticalLivel<( maxVolume - 100)){criticalLivel +=10; setUpdateDisplay=1;} } if(encLeft){if(criticalLivel>10){criticalLivel -=10; setUpdateDisplay=1;}} break;
case 6 : setLivelMetr();setDisplayMenu("Metr value", "Min calib", duration , minDistance, point);//if(duration == !oldduration){setLivelMetr(); setUpdateDisplay=1;} if(encLeft){setLivelMetr();setUpdateDisplay=1; } break;
case 7 : if(encRight){minDistance ++; setUpdateDisplay=1;} if(encLeft){minDistance --;setUpdateDisplay=1; } break;
case 8 : setLivelMetr();setDisplayMenu("Metr value", "Max calib", duration , maxDistance, point); break;
case 9 : if(encRight){maxDistance ++; setUpdateDisplay=1;} if(encLeft){maxDistance --;setUpdateDisplay=1; } break;
// }
}
if(setUpdateDisplay && (menuSet == 0 || menuSet == 1)) setDisplayMenu(!autoSet ? "Auto Set Mode":"Manual Set Mode" ,(autoSet == 1 ? (vaterRelay ? "Relay On Set":"Relay Off Set"):"Max L vol"),0 ,maxVolume, point);
if(setUpdateDisplay && (menuSet == 2 || menuSet == 3)) setDisplayMenu("Set Volum", "Min L Vol" ,volume, minVolume, point);
if(setUpdateDisplay && (menuSet == 4 || menuSet == 5)) setDisplayMenu("Critical V", "Critical V" ,criticalLivel, criticalLivel, point);
if(setUpdateDisplay && menuSet==7) setDisplayMenu("Metr value:", "Min calib", duration , minDistance, point);
if(setUpdateDisplay && menuSet==9) setDisplayMenu("Metr value:", "Max calib", duration, maxDistance, point);
setUpdateDisplay = 0;flagEnc = 0;
}
//maxLivel = (digitalRead(5) == 1 ? ((cpu_gauge >= (maxVolume - 10 ))? 1 :0) :0);
if(autoSet){if(cpu_gauge >= volume) vaterRelay=0; }
else {
if(cpu_gauge<criticalLivel)vaterRelay=1;
if(cpu_gauge >= (maxVolume - 10 )){maxLivel =1;minLivel =0;}
if(cpu_gauge < minVolume)minLivel =1;
if(minLivel){maxLivel =0;vaterRelay=1;}
if(cpu_gauge >= maxVolume || cpu_gauge >= volume) vaterRelay=0;
}
//if(autoSet)()cpu_gauge < volume ? (vaterRelay == 1? 1:0):0);
digitalWrite(5,vaterRelay);
}
/////////////////////////////////////////////////////////////
void getLivel(){
float units_per_pixel = (gauge_size_chars*5.0)/volume; // every character is 5px wide, we want to count from 0-100
int value_in_pixels = round(cpu_gauge * units_per_pixel); // cpu_gauge value converted to pixel width
int tip_position = 0; // 0= not set, 1=tip in first char, 2=tip in middle, 3=tip in last char
if (value_in_pixels < 5) {tip_position = 1;} // tip is inside the first character
else if (value_in_pixels > gauge_size_chars*5.0-5) {tip_position = 3;} // tip is inside the last character
else {tip_position = 2;} // tip is somewhere in the middle
move_offset = 4 - ((value_in_pixels-1) % 5); // value for offseting the pixels for the smooth filling
for (int i=0; i<8; i++) { // dynamically create left part of the gauge
if (tip_position == 1) {gauge_left_dynamic[i] = (gauge_fill_5[i] << move_offset) | gauge_left[i];} // tip on the first character
else {gauge_left_dynamic[i] = gauge_fill_5[i];} // tip not on the first character
gauge_left_dynamic[i] = gauge_left_dynamic[i] & gauge_mask_left[i]; // apply mask for rounded corners
}
for (int i=0; i<8; i++) { // dynamically create right part of the gauge
if (tip_position == 3) {gauge_right_dynamic[i] = (gauge_fill_5[i] << move_offset) | gauge_right[i];} // tip on the last character
else {gauge_right_dynamic[i] = gauge_right[i];} // tip not on the last character
gauge_right_dynamic[i] = gauge_right_dynamic[i] & gauge_mask_right[i]; // apply mask for rounded corners
}
lcd.createChar(5, gauge_left_dynamic); // create custom character for the left part of the gauge
lcd.createChar(6, gauge_right_dynamic); // create custom character for the right part of the gauge
for (int i=0; i<gauge_size_chars; i++) { // set all the characters for the gauge
if (i==0) {gauge_string[i] = byte(5);} // first character = custom left piece
else if (i==gauge_size_chars-1) {gauge_string[i] = byte(6);} // last character = custom right piece
else { // character in the middle, could be empty, tip or fill
if (value_in_pixels <= i*5) {gauge_string[i] = byte(7);} // empty character
else if (value_in_pixels > i*5 && value_in_pixels < (i+1)*5) {gauge_string[i] = byte(5-move_offset);} // tip
else {gauge_string[i] = byte(255);} // filled character
}
}
//distance= counter;
}
void setDisplayInfo(){
byte warning_icon[8] = {B00000,B00111,B01001,B01001,B01001,B01001,B10001,B00000}; // warning icon - just because we still have one custom character left
lcd.createChar(0, warning_icon); // warning icon - just because we have one more custom character that we could use
// gauge drawing
if(setUpdateDisplay)lcd.clear();
lcd.setCursor(8,1); // move cursor to top left
sprintf(buffer, "%4d% ", cpu_gauge); // set a string as CPU: XX%, with the number always taking at least 3 character
lcd.print(buffer); // print the string on the display
lcd.write(byte(0)); // print warning character
lcd.setCursor(0,1); // move the cursor to the next line
lcd.print(gauge_string);
lcd.setCursor(0,0); // move the cursor to the next line
lcd.print(maxLivel);
lcd.setCursor(2,0); // move the cursor to the next line
lcd.print(minLivel);
lcd.setCursor(4,0); // move the cursor to the next line
lcd.print(vaterRelay);
}
void setDisplayMenu(const char* info1,const char* info2, int16_t setPar1, int16_t setPar2, int8_t point){
if(setUpdateDisplay)lcd.clear();
int8_t setpoint = point%2;
byte customChar[] = {B00010,B00110,B01110,B11111,B11111,B01110,B00110,B00010};
lcd.setCursor(11,0);
lcd.print(setPar1);
lcd.setCursor(0,0);
lcd.print(info1);
lcd.setCursor(11,1);
lcd.print(setPar2);
lcd.setCursor(0,1);
lcd.print(info2);
lcd.createChar(0, customChar);
lcd.setCursor(10,setpoint);
lcd.write(byte(0));
lcd.setCursor(15,1); // move the cursor to the next line
lcd.print(vaterRelay);
}
void enc1() {
static unsigned long timer;
if (timer > millis()) return; // если вызывается слишком часто, значит это дребезг, он нам не нужен
//stepEnc += digitalRead(C2) == digitalRead(C1) ? -1 : 1; // определяем направление по состоянию второй ножки
if (digitalRead(C2) != digitalRead(C1))encRight = 1;
if (digitalRead(C2) == digitalRead(C1))encLeft = 1;
flagEnc = 1;
timer = millis() + 10; // пауза антидребезга 5 мс, на отслеживание не влияет
}
void setLivelMetr(){
// для большей точности установим значение LOW на пине Trig
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// Теперь установим высокий уровень на пине Trig
digitalWrite(trigPin, HIGH);
// Подождем 10 μs
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Узнаем длительность высокого сигнала на пине Echo
duration = pulseIn(echoPin, HIGH);
// Рассчитаем расстояние
cpu_gauge = map(duration, minDistance, maxDistance, volume, 0);// 58; set calibration in menu
}