/*
==================================================================================
TEMPERATURE & FUEL GAUGE DRIVER
==================================================================================
DESCRIPTION:
Calibrates and drives classic bimetallic automotive dashboard gauges using an
Arduino Uno and an HD44780 16x2 I2C display panel. Features 4-stage software-
dampened alarm tracking (Low/Low-Low Fuel, High/High-High Temp) and stores
calibrations permanently in internal EEPROM memory.
HARDWARE CONSTRAINTS:
- Transistor Drivers: BD139 bases must connect to pins D9 and D10 via 220-270R resistors.
- Timer 1 Clock Frequency: Scaled down to 30.64 Hz to protect classic bimetallic
gauge elements from high-frequency thermal burnout.
- Buttons: Momentary types configured with internal pull-ups (active LOW).
==================================================================================
PIN CONNECTION MAP
==================================================================================
[ARDUINO PIN] --> [COMPONENT / SIGNAL]
------------------------------------------------------------------------------
ANALOG INPUTS:
- Pin A0 --> Fuel Level Sender (via 100-Ohm Voltage Divider)
- Pin A1 --> Engine Temperature Sender (via Voltage Divider)
- Pin A4 --> I2C LCD - SDA (Data Line)
- Pin A5 --> I2C LCD - SCL (Clock Line)
DIGITAL OUTPUTS:
- Pin D9 --> Fuel Gauge BD139 Driver Base (Low-Frequency PWM)
- Pin D10 --> Temperature Gauge BD139 Driver Base (Low-Frequency PWM)
- Pin D2 --> Dedicated Low Fuel Warning Indicator LED / Relay
- Pin D6 --> Dedicated Hot Temp Warning Indicator LED / Relay
DIGITAL INPUTS (MOMENTARY SWITCHES - WIRE TO PIN AND GND):
- Pin D3 --> [MODE] Button (Cycle Master Menu Screens)
- Pin D8 --> [STEP] Button (Cycle Needle Positions / Alarm Types)
- Pin D4 --> [UP] Button (Increment Calibration Profile Counts)
- Pin D5 --> [DOWN] Button (Decrement Calibration Profile Counts)
==================================================================================
INTERFACE MENU MODES
==================================================================================
1. NORMAL MODE (Driving View)
- Row 1: Shows live Fuel input count (0-1023) and live output PWM driving gauge.
- Row 2: Shows live Temp input count (0-1023) and live output PWM driving gauge.
2. CALIBRATE FUEL MODE (F In)
- Set sender tank depth physically, then tap/hold UP/DOWN to align the dash
needle on the selected target tier (0, 1/4, 1/2, 3/4, 1/1).
3. CALIBRATE TEMP MODE (T In)
- Set sender resistance physically, then tap/hold UP/DOWN to align the dash
needle on the selected target tier (0, 1/4, 1/2, 3/4, 1/1).
4. WARN SETTINGS MODE (Set Input Alarms)
- Press [STEP] to cycle and set the following raw input value targets:
- F_Lo : Solid Fuel Light (Triggers below target after 5 sec slosh buffer)
- F_LL : Flashing Fuel Light (Triggers below target after 5 sec slosh buffer)
- T_Hi : Solid Temp Light (Triggers above target after 2 sec spike buffer)
- T_HH : Flashing Temp Light (Triggers above target after 2 sec spike buffer)
* NOTE: Exiting the warning screen back to Normal Mode saves all values to EEPROM.
==================================================================================
*/
#include <Wire.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>
#include <EEPROM.h>
// --- HARDWARE PIN CONFIGURATIONS ---
const int FuelVin = A0;
const int TempVin = A1;
const int FuelGaugeOutPin = 9;
const int TempGaugeOutPin = 10;
const int FuelWarnLightPin = 2;
const int TempWarnLightPin = 6;
// Buttons (Active LOW)
const int MODE_PIN = 3;
const int STEP_PIN = 8;
const int UP_PIN = 4;
const int DOWN_PIN = 5;
hd44780_I2Cexp lcd;
// --- CALIBRATION ARRAYS ---
int fuelCalPWM[] = {20, 60, 110, 180, 245};
int tempCalPWM[] = {10, 50, 95, 170, 254};
int fuelCalIn[] = {200, 300, 400, 500, 600};
int tempCalIn[] = {230, 330, 430, 530, 630};
const char* stepLabels[] = {"0 ", "1/4", "1/2", "3/4", "1/1"};
int fuelWarnThreshold[] = {240, 210};
int tempWarnThreshold[] = {750, 850};
const char* warnLabels[] = {"F_Lo", "F_LL", "T_Hi", "T_HH"};
enum OperationalMode { STATE_NORMAL, STATE_CAL_FUEL, STATE_CAL_TEMP, STATE_WARN_TRIGGERS };
OperationalMode currentMode = STATE_NORMAL;
int currentStep = 0;
bool lastModeState = HIGH, lastStepState = HIGH, lastUpState = HIGH, lastDownState = HIGH;
unsigned long lastFlashTime = 0;
bool flashState = false;
unsigned long fuelWarnLowTimer = 0, fuelWarnLLTimer = 0, tempWarnHiTimer = 0, tempWarnHHTimer = 0;
const unsigned long SLOSH_DELAY = 5000, TEMP_DELAY = 2000;
// Forward Declarations
void scanMomentaryButtons(int fuelIn, int tempIn);
void saveCalibrationToMemory();
void loadCalibrationFromMemory();
int interpolateGauge(int val, int calIn[], int calOut[]);
void setup() {
pinMode(FuelWarnLightPin, OUTPUT);
pinMode(TempWarnLightPin, OUTPUT);
digitalWrite(FuelWarnLightPin, LOW);
digitalWrite(TempWarnLightPin, LOW);
pinMode(MODE_PIN, INPUT_PULLUP);
pinMode(STEP_PIN, INPUT_PULLUP);
pinMode(UP_PIN, INPUT_PULLUP);
pinMode(DOWN_PIN, INPUT_PULLUP);
TCCR1B = (TCCR1B & B11111000) | B00000101; // Low-frequency 30.64 Hz PWM
if(lcd.begin(16, 2) != 0) while(1);
lcd.backlight();
lcd.setCursor(0, 0); // Start at column 0, top row
lcd.print("Temp/Fuel Gauge"); // Prints 15 characters on line 1
lcd.setCursor(0, 1); // Move down to column 0, bottom row
lcd.print("Calibrator "); // Prints line 2 (with spaces to clear old text)
delay(1000);
lcd.clear();
loadCalibrationFromMemory();
}
void loop() {
int fuelIn = analogRead(FuelVin);
int tempIn = analogRead(TempVin);
scanMomentaryButtons(fuelIn, tempIn);
if (millis() - lastFlashTime >= 250) {
lastFlashTime = millis();
flashState = !flashState;
}
int fuelOut = interpolateGauge(fuelIn, fuelCalIn, fuelCalPWM);
int tempOut = interpolateGauge(tempIn, tempCalIn, tempCalPWM);
switch (currentMode) {
case STATE_NORMAL:
analogWrite(FuelGaugeOutPin, fuelOut);
analogWrite(TempGaugeOutPin, tempOut);
// Slosh/Delay Warning Code
if (fuelIn < fuelWarnThreshold[1]) { if(fuelWarnLLTimer==0) fuelWarnLLTimer=millis(); } else fuelWarnLLTimer=0;
if (fuelIn < fuelWarnThreshold[0]) { if(fuelWarnLowTimer==0) fuelWarnLowTimer=millis(); } else fuelWarnLowTimer=0;
if (tempIn > tempWarnThreshold[1]) { if(tempWarnHHTimer==0) tempWarnHHTimer=millis(); } else tempWarnHHTimer=0;
if (tempIn > tempWarnThreshold[0]) { if(tempWarnHiTimer==0) tempWarnHiTimer=millis(); } else tempWarnHiTimer=0;
digitalWrite(FuelWarnLightPin, (fuelWarnLLTimer && (millis()-fuelWarnLLTimer >= SLOSH_DELAY)) ? (flashState ? HIGH : LOW) : ((fuelWarnLowTimer && (millis()-fuelWarnLowTimer >= SLOSH_DELAY)) ? HIGH : LOW));
digitalWrite(TempWarnLightPin, (tempWarnHHTimer && (millis()-tempWarnHHTimer >= TEMP_DELAY)) ? (flashState ? HIGH : LOW) : ((tempWarnHiTimer && (millis()-tempWarnHiTimer >= TEMP_DELAY)) ? HIGH : LOW));
lcd.setCursor(0, 0); lcd.print("Fin:"); lcd.print(fuelIn); lcd.print(" Out:"); lcd.print(fuelOut); lcd.print(" ");
lcd.setCursor(0, 1); lcd.print("Tin:"); lcd.print(tempIn); lcd.print(" Out:"); lcd.print(tempOut); lcd.print(" ");
break;
case STATE_CAL_FUEL:
fuelCalIn[currentStep] = fuelIn;
analogWrite(FuelGaugeOutPin, fuelCalPWM[currentStep]);
lcd.setCursor(0, 0);
lcd.print("F In:"); lcd.print(fuelIn); lcd.print(" / 1023 ");
// Shortened "Gauge:" to "Gge:" to make space for 3-digit PWM values
lcd.setCursor(0, 1);
lcd.print("Gge:");
lcd.print(stepLabels[currentStep]);
lcd.print(" Out:");
lcd.print(fuelCalPWM[currentStep]);
lcd.print(" "); // Clears old trailing text
break;
case STATE_CAL_TEMP:
tempCalIn[currentStep] = tempIn;
analogWrite(TempGaugeOutPin, tempCalPWM[currentStep]);
lcd.setCursor(0, 0);
lcd.print("T In:"); lcd.print(tempIn); lcd.print(" / 1023 ");
// Shortened "Gauge:" to "Gge:" to make space for 3-digit PWM values
lcd.setCursor(0, 1);
lcd.print("Gge:");
lcd.print(stepLabels[currentStep]);
lcd.print(" Out:");
lcd.print(tempCalPWM[currentStep]);
lcd.print(" "); // Clears old trailing text
break;
case STATE_WARN_TRIGGERS:
analogWrite(FuelGaugeOutPin, 0); analogWrite(TempGaugeOutPin, 0);
lcd.setCursor(0, 0); lcd.print("SET WARNINGS");
lcd.setCursor(0, 1); lcd.print(warnLabels[currentStep]); lcd.print(":");
lcd.print((currentStep < 2) ? fuelWarnThreshold[currentStep] : tempWarnThreshold[currentStep - 2]);
lcd.print(" I:"); lcd.print((currentStep < 2) ? fuelIn : tempIn); lcd.print(" ");
break;
}
delay(20);
}
int interpolateGauge(int val, int calIn[], int calOut[]) {
bool rising = (calIn[0] < calIn[4]);
if (rising) {
if (val <= calIn[0]) return calOut[0];
if (val >= calIn[4]) return calOut[4];
for (int i = 0; i < 4; i++) {
if (val >= calIn[i] && val <= calIn[i+1]) return map(val, calIn[i], calIn[i+1], calOut[i], calOut[i+1]);
}
} else {
if (val >= calIn[0]) return calOut[0];
if (val <= calIn[4]) return calOut[4];
for (int i = 0; i < 4; i++) {
if (val <= calIn[i] && val >= calIn[i+1]) return map(val, calIn[i], calIn[i+1], calOut[i], calOut[i+1]);
}
}
return calOut[0];
}
void scanMomentaryButtons(int fuelIn, int tempIn) {
bool cM = digitalRead(MODE_PIN), cS = digitalRead(STEP_PIN), cU = digitalRead(UP_PIN), cD = digitalRead(DOWN_PIN);
static unsigned long lastScrollTime = 0;
static unsigned long holdTimer = 0; // Tracks how long a button is physically held down
// 1. Cycle Master Menus
if (lastModeState == HIGH && cM == LOW) {
lcd.setCursor(0, 0); lcd.print(" ");
lcd.setCursor(0, 1); lcd.print(" ");
if (currentMode == STATE_NORMAL) { currentMode = STATE_CAL_FUEL; currentStep = 0; }
else if (currentMode == STATE_CAL_FUEL) { currentMode = STATE_CAL_TEMP; currentStep = 0; }
else if (currentMode == STATE_CAL_TEMP) { currentMode = STATE_WARN_TRIGGERS; currentStep = 0; }
else {
saveCalibrationToMemory(); currentMode = STATE_NORMAL;
lcd.setCursor(0, 0); lcd.print("Saved to EEPROM!");
lcd.setCursor(0, 1); lcd.print("Updating... "); delay(1200);
lcd.setCursor(0, 0); lcd.print(" "); lcd.setCursor(0, 1); lcd.print(" ");
}
delay(150);
}
// 2. Cycle Sub-steps
if (lastStepState == HIGH && cS == LOW) {
currentStep++;
if (currentStep > ((currentMode == STATE_WARN_TRIGGERS) ? 3 : 4)) currentStep = 0;
delay(150);
}
// Reset hold timer if no adjustment buttons are pressed
if (cU == HIGH && cD == HIGH) {
holdTimer = 0;
}
// 3. Increment Value (Precise single-click, hold 350ms to auto-scroll)
if (cU == LOW) {
bool incrementNow = false;
if (lastUpState == HIGH) {
// Direct action on the initial physical click down
incrementNow = true;
holdTimer = millis();
lastScrollTime = millis();
} else if (millis() - holdTimer > 350) {
// Fast repeat kicks in only after holding down for 350ms
if (millis() - lastScrollTime >= 50) {
incrementNow = true;
lastScrollTime = millis();
}
}
if (incrementNow) {
if (currentMode == STATE_CAL_FUEL && fuelCalPWM[currentStep] < 255) fuelCalPWM[currentStep]++;
if (currentMode == STATE_CAL_TEMP && tempCalPWM[currentStep] < 255) tempCalPWM[currentStep]++;
if (currentMode == STATE_WARN_TRIGGERS) {
if (currentStep < 2) { if(fuelWarnThreshold[currentStep] < 1023) fuelWarnThreshold[currentStep] += 5; }
else { if(tempWarnThreshold[currentStep-2] < 1023) tempWarnThreshold[currentStep-2] += 5; }
}
}
}
// 4. Decrement Value (Precise single-click, hold 350ms to auto-scroll)
if (cD == LOW) {
bool decrementNow = false;
if (lastDownState == HIGH) {
// Direct action on the initial physical click down
decrementNow = true;
holdTimer = millis();
lastScrollTime = millis();
} else if (millis() - holdTimer > 350) {
// Fast repeat kicks in only after holding down for 350ms
if (millis() - lastScrollTime >= 50) {
decrementNow = true;
lastScrollTime = millis();
}
}
if (decrementNow) {
if (currentMode == STATE_CAL_FUEL && fuelCalPWM[currentStep] > 0) fuelCalPWM[currentStep]--;
if (currentMode == STATE_CAL_TEMP && tempCalPWM[currentStep] > 0) tempCalPWM[currentStep]--;
if (currentMode == STATE_WARN_TRIGGERS) {
if (currentStep < 2) { if(fuelWarnThreshold[currentStep] > 0) fuelWarnThreshold[currentStep] -= 5; }
else { if(tempWarnThreshold[currentStep-2] > 0) tempWarnThreshold[currentStep-2] -= 5; }
}
}
}
lastModeState = cM; lastStepState = cS; lastUpState = cU; lastDownState = cD;
}
void saveCalibrationToMemory() {
int addr = 0;
for (int i = 0; i < 5; i++) EEPROM.write(addr++, fuelCalPWM[i]);
for (int i = 0; i < 5; i++) EEPROM.write(addr++, tempCalPWM[i]);
for (int i = 0; i < 5; i++) { EEPROM.write(addr++, highByte(fuelCalIn[i])); EEPROM.write(addr++, lowByte(fuelCalIn[i])); }
for (int i = 0; i < 5; i++) { EEPROM.write(addr++, highByte(tempCalIn[i])); EEPROM.write(addr++, lowByte(tempCalIn[i])); }
for (int i = 0; i < 2; i++) { EEPROM.write(addr++, highByte(fuelWarnThreshold[i])); EEPROM.write(addr++, lowByte(fuelWarnThreshold[i])); }
for (int i = 0; i < 2; i++) { EEPROM.write(addr++, highByte(tempWarnThreshold[i])); EEPROM.write(addr++, lowByte(tempWarnThreshold[i])); }
}
void loadCalibrationFromMemory() {
if (EEPROM.read(0) != 255) {
int addr = 0;
for (int i = 0; i < 5; i++) fuelCalPWM[i] = EEPROM.read(addr++);
for (int i = 0; i < 5; i++) tempCalPWM[i] = EEPROM.read(addr++);
for (int i = 0; i < 5; i++) { byte hi = EEPROM.read(addr++); byte lo = EEPROM.read(addr++); fuelCalIn[i] = word(hi, lo); }
for (int i = 0; i < 5; i++) { byte hi = EEPROM.read(addr++); byte lo = EEPROM.read(addr++); tempCalIn[i] = word(hi, lo); }
for (int i = 0; i < 2; i++) { byte hi = EEPROM.read(addr++); byte lo = EEPROM.read(addr++); fuelWarnThreshold[i] = word(hi, lo); }
for (int i = 0; i < 2; i++) { byte hi = EEPROM.read(addr++); byte lo = EEPROM.read(addr++); tempWarnThreshold[i] = word(hi, lo); }
}
}mode
step
down
up
temp in
fuel in