// Ethanol Analyzer v2.1 with BLE & optional DAC analog out(MCP4725 required).. This code only works with AVR based boards(ATmega/Keywish/eMakeFun).  Will NOT work with Nano 33 BLE!
// GOFi2cOLED - Version: Latest 
// v1.4 change notes: Analog out range 0-5v, DAC
// v1.5 change notes: dacOut 5.15v adjustment
// v1.6 change notes: averaging A1 code to reduce noise
// v2.0 change notes: Enable/disable certain features to make it easier for yall, also merged all code branches into this one
// v2.1 change notes: Slight memory efficiency, re-init screen if gets disconnected w/o having to key off/on, watchdog reset if board freezes, dacOut 5.00v
#include <Wire.h>
#include <Adafruit_SleepyDog.h>
#include <GOFi2cOLED.h>
#include <Adafruit_MCP4725.h>
GOFi2cOLED LCD;
Adafruit_MCP4725 dac;

#define A1 1
#define SENSOR_PIN 10
#define OUTPUT_PIN 9
float verz = 2.1;               //code release

///////// Features can be enabled or disabled below ///////
bool featureDAC = 1;             //DAC analog output (MCP4725 required)           default=1   (enabled)
bool featurePWMout = 1;          //PWM Analog output (pin D9)                     default=1   (enabled)
int voltOUTRange = 0;            //PWM&DAC Output Range (0=0-5v , 1=0.5-4.5v)     default=0   (0-5v)     Setting to 1 will output 0.5v-4.5v and also enable Cobb error conditions

///// If you choose to use input A1 pin to attach to a MAP sensor, or some other sensor to monitor input voltage (voltage follower needed for resistance circuits (ie ECT/IAT) //////////
bool featureAnalogInA1 = 0;      //0-5v Analog Input(Pin A1) for bar graph        default=0   (disabled) (setting this to zero essentially turns off monitoring A1 input pin)
float DangerToManifoldA1 = 5.1;  //Danger to Manifold Warning voltage threshold   default=5.1 (5.1v - basically should never cross this, thus will never show)
bool ShowBarGraphA1 = 0;         //Show Analog Input Bar Graph                    default=0   (must have A1 input pin attached to a 0-5v input source)
bool ShowTriangleA1 = 1;         //Show Analog Input Triangle Gauge               default=1   (must have A1 input pin attached to a 0-5v input source)
bool ReverseGraphA1 = 0;         //Swap left/right start point                    default=0   (by default, graph starts from the left side of the screen when detecting 0 voltage)
bool ShowTickMarksA1 = 1;        //Show tick marks for A1 gauge                   default=1   (to show 1/4, 1/2, 3/4 tick marks, like a fuel gauge)
//// To adjust Triangle/Graph gauge, for example: Min(195) Max(530) monitors Fiesta IAT sensor at two given points: 2.6v(59F) to .96v(138F), triangle stays pegged below 59F and above 138F.
int GraphScalerP1A1 = 0;         //A1 Scaler start point                          default=0   (view range window start point, ex. set to 512 to set start point at half of full range) 
int GraphScalerP2A1 = 1024;      //A1 Scaler end point                            default=1024(view range window end point)
//// Below is if you decide to swap the fuel temp Celsius metric with whatever is attached to A1 pin, for example Fuel/IAT temps side by side
bool SwapFuelCwithA1 = 0;        //Replace Celsius field with A1 input            default=0
int AlternateScalerP1A1 = 0;     //Alternate Scaler start point IAT example       default=0   (138 for tapping into Fiesta IAT)   
int AlternateScalerP2A1 = 1024;  //Alternate Scaler end point IAT example         default=1024(59 for tapping into Fiesta IAT)

int eContentSkew = 50;           //Skew Ethanol Content readings                  default=50   (50 is neutral, set to 57 to subtract "-7%" ethanol or 45 to add "5%", I know it sounds backwards)

int splashScreenDelay = 2000;    //show splash screen in miliseconds              default=2000 OR default=0 for Cobb (helps to get ethanol content immediately after key on start
int refreshDelay = 0;            //delay between refresing new ethanol content    default=0    (zero = refresh as quickly as possible)
int eContentOverride = 0;        //Set to above 0 to override ethanol content     default=0    (for testing purposes ONLY)





unsigned int counttick = 0;      //for skipping/retry invalid data
bool resetDisplay = false;       //if oled gets disconnected, resync without key-off/on again

void setup()
{   /////////// INTRO SCREEN & initializations ///////////
  Serial.begin(9600);
  if (featurePWMout){
    // Setup for analog out, pin 9 to fast mode.
    pinMode(OUTPUT_PIN, OUTPUT);
    TCCR1B = TCCR1B & 0b11111000 | 0x01;
    int analogOut = 255 * (0.1 / 5.0); // setting analog to 0.1v until content can be calculated
    analogWrite(OUTPUT_PIN, int(analogOut));
  }
  if (featureDAC){
    dac.begin(0x60);
    int dacOut = 4095 * (0.1 / 5.0);  // setting DAC to 0.1v until content can be calculated
    dac.setVoltage(dacOut, false);
  }
	LCD.init(0x3C);
	LCD.setTextWrap(false);
	LCD.clearDisplay();
	LCD.setCursor(0, 0);
	LCD.setTextSize(1);
	LCD.print(featureDAC);
	LCD.print(featurePWMout);
  LCD.print(voltOUTRange);
	LCD.println(featureAnalogInA1);
  LCD.print("               v");
  LCD.println(verz,1);
	LCD.setTextSize(2);
	LCD.println();
	LCD.println("Ethanol"); // -> Change the text of these two lines if you want a different startup screen
	LCD.println("Analyzer");
	LCD.display();
	delay(splashScreenDelay);
  Watchdog.enable(8000);
}





void loop()
{
  Watchdog.reset();
  //////////// Detect OLED Disconnect & ReSync ////////////
  byte OLEDerror = false;
  Wire.beginTransmission(60);
  OLEDerror = Wire.endTransmission();
  if (OLEDerror != 0){
      resetDisplay=true;
  }
  if (OLEDerror == 0 && resetDisplay==true){
    delay(1000);
    Watchdog.reset();
    LCD.init(0x3C);
    LCD.setTextWrap(false);
    LCD.clearDisplay();
    LCD.setCursor(0, 0);
    LCD.setTextSize(1);
    LCD.print(featureDAC);
    LCD.print(featurePWMout);
    LCD.print(voltOUTRange);
    LCD.println(featureAnalogInA1);
    LCD.print("               v");
    LCD.println(verz,1);
    LCD.setTextSize(2);
    LCD.println();
    LCD.println("Ethanol"); // -> Change the text of these two lines if you want a different startup screen
    LCD.println("Analyzer");
    LCD.display();
    delay(splashScreenDelay);
    Watchdog.reset();
    resetDisplay=false;
  }


  
  /////// READ FLEX-FUEL SENSOR DATA /////////
  unsigned long highTime = pulseIn(SENSOR_PIN, HIGH);
  unsigned long lowTime = pulseIn(SENSOR_PIN, LOW);
  unsigned long pulsetime = highTime + lowTime;

  
  /////// READ 0-5V INPUT VOLTAGE from A1 PIN ////////
  float VInA1;
  int AverageVoltagebitsA1;
  int AltA1;
  if (featureAnalogInA1) {
    // start averaging analog A1 input
    AverageVoltagebitsA1 = 0;
    int MeasurementsToAverage = 16;
    for(int i = 0; i < MeasurementsToAverage; ++i)
    {
      AverageVoltagebitsA1 += MeasureVoltage();
      delay(1);
    }
    AverageVoltagebitsA1 /= MeasurementsToAverage;
    //end averaging input
    VInA1 = AverageVoltagebitsA1 * (5.0 / 1023.0);
    AltA1 = mapf(AverageVoltagebitsA1, GraphScalerP1A1, GraphScalerP2A1, AlternateScalerP1A1, AlternateScalerP2A1);
  } else {
    VInA1 = 0.0; //We are setting Analog In to zero because feature is disabled.
  }



  //////// IF FLEX-FUEL SENSOR DATA OUT OF RANGE, SET VARIOUS Vout ERROR CONDITIONS on DAC & ANALOG ///////
	// 20000 uS = 50 HZ - ~6667 uS = 150 HZ
	if (pulsetime > 20100 || pulsetime < 6400) {
		// If the value falls outside the range just skip it for this iteration, perhaps from noise?
    if (counttick >= 2){  // if flex-fuel sensor data is invalid twice in a row, then display debug content - otherwise skip and use old calculations
      if (voltOUTRange){
        //below are for Cobb specific error codes when using 0.5-4.5v DAC analog out
        if (pulsetime == 0 ) {
          // setting analog to 0.1v because sensor got disconnected
          if (featurePWMout){
            int analogOut = 255 * (0.1 / 5.0);
            analogWrite(OUTPUT_PIN, int(analogOut));
          }
          if (featureDAC){
            int dacOut = 4095 * (0.1 / 5.0);
            dac.setVoltage(dacOut, false);
          }
        }
        if (pulsetime >= 20100 ) {
          // setting analog to 4.8v because fuel is probably contaminated (less than 0% ethanol)
          if (featurePWMout){
            int analogOut = 255 * (4.8 / 5.0);
            analogWrite(OUTPUT_PIN, int(analogOut));
          }
          if (featureDAC){
            int dacOut = 4095 * (4.8 / 5.0);
            dac.setVoltage(dacOut, false);
          }
        }
        if ((pulsetime <= 6400) && (pulsetime >= 1)) {
          // setting analog to 4.9v because fuel is water contaminated (greater than 100% ethanol)
          if (featurePWMout){
            int analogOut = 255 * (4.9 / 5.0);
            analogWrite(OUTPUT_PIN, int(analogOut));
          }
          if (featureDAC){
            int dacOut = 4095 * (4.9 / 5.0);
            dac.setVoltage(dacOut, false);
          }
        }
     }
	    
	    //////// ALSO DISPLAY DEBUG ERROR DATA //////////
	    LCD.clearDisplay();
	    LCD.setCursor(0, 0);
	    LCD.setTextSize(0);
	    LCD.print("Sensor Data = ");
      LCD.println(pulsetime);
      LCD.print("VoltsIn = ");
      LCD.print(VInA1,1);
      LCD.print("   v");
      LCD.print(verz,1);
	    LCD.setTextSize(2);
	    LCD.println("");
	    LCD.println("Is Sensor");
	    LCD.println("Connected?");
	    LCD.display();

      /////// ALSO SEND ERROR DATA TO BLUETOOTH CORDLESS TELEPHONES ////////
	    Serial.print("[");
	    Serial.print("=");
	    Serial.print(pulsetime);
	    Serial.print(",");
	    Serial.print("-,");
      Serial.print("-,");
      Serial.print(verz,1);
      Serial.print(",");
      Serial.print(VInA1,1);
	    Serial.print("]");
    } else if (counttick < 2) {
      //uncomment below to display debug ticks
      //LCD.clearDisplay();
	    //LCD.setCursor(0, 0);
	    //LCD.setTextSize(4);
	    //LCD.println(counttick);
	    //LCD.display();
	    counttick++;
    }
		return;
	}





	//////////// CALCULATE ETHANOL CONTENT FROM PULSETIME & TEMP FROM DUTY CYCLE ///////////
	// 50 HZ = 0% ethanol, 150 HZ = 100% ethanol
	long eContent = int((1000000 / pulsetime) - eContentSkew);
  if (eContentOverride){
    eContent = eContentOverride;	
  } 
	if (eContent < 0) {
		eContent = 0;
	}
	else if (eContent > 100) {
		eContent = 100;
	}

	// 1 millisecond = -40C, 5 milliseconds = 125C
	float frequency = float(1000000 / pulsetime);
	float dutyCycle = 100 * (highTime / float(lowTime + highTime));
	float totalTime = float(1.0 / frequency);
	float period = float(100 - dutyCycle) * totalTime;
	int temperature = 40.25 * 10 * period - 81.25;
	int temperatureF = temperature * 1.8 + 32;

  if (temperatureF < -39 || temperatureF > 250) {
    // throw these results out, perhaps from noise?
    return;
  }



	////////// SEND Vout to ANALOG & DAC /////////////
  float desiredVoltage;
  if (voltOUTRange){
	  desiredVoltage = mapf(eContent, 0, 100, 0.5, 4.5); // DAC out: (0.5 volts = 0%, 4.5 volts = 100%)
  } else {
    desiredVoltage = mapf(eContent, 0, 100, 0, 5); // Analog out: (0 volts = 0%, 5 volts = 100%)
  }
  if (featurePWMout){
	  int analogOut = 255 * (desiredVoltage / 5.0);
    analogWrite(OUTPUT_PIN, int(analogOut));
  }
  if (featureDAC){
    int dacOut = 4095 * (desiredVoltage / 5.0);
	  dac.setVoltage(dacOut, false);
  }



  ////////// SEND DATA TO OLED DISPLAY ///////////////
  LCD.clearDisplay();
  LCD.setCursor(0, 0);
	LCD.setTextSize(2);
  if (temperatureF >= 100 || temperatureF <= -10) {
    LCD.setCursor(0, 0);
  } else {
    LCD.setCursor(10, 0);
  }
	LCD.print(temperatureF);
	LCD.print((char)247);
	LCD.print("F/");
  if (SwapFuelCwithA1){
	  LCD.print(AltA1);
	  LCD.print((char)247);
	  LCD.println("F");
  } else {
    LCD.print(temperature);
    LCD.print((char)247);
    LCD.println("C");    
  }
	LCD.setTextSize(5);
  LCD.setCursor(40, 20);
  if ((eContent >= 10) && (eContent <= 99)) {
    LCD.setCursor(20, 20);
  } else if (eContent == 100) {
    LCD.setCursor(4, 20);
  }
	LCD.print(eContent);
	LCD.println("%");

  ////////////// IF ANALOG INPUT IS ENABLED, DISPLAY BAR GRAPH AND/OR TRIANGLE THINGIE ////////////
  if (featureAnalogInA1) {
    if (VInA1 >= 0.00  && VInA1 <= DangerToManifoldA1){
      //////////////// TRIANGLE GAUGE //////////////
      if (ShowTriangleA1) {
        int bitsA1 = mapf(AverageVoltagebitsA1, GraphScalerP1A1, GraphScalerP2A1, 0, 1024);
        int midpoint;
        if (ReverseGraphA1) {
          midpoint = 127 - (bitsA1 / 8);  //reverses triangle graph
        } else {
          midpoint = (bitsA1 / 8); //normal triangle graph
        }
        int leftfoot = 0;
        int rightfoot = 0;
        if (midpoint > 127){
          midpoint = 127;    
        }
        if (midpoint < 0){
          midpoint = 0;
        }
        if (midpoint < 5){
          leftfoot = 0;
        } else {
          leftfoot = midpoint - 5;
        }
        if (midpoint > 122){
          rightfoot = 127;
        } else {
          rightfoot = midpoint + 5;
        }
        LCD.fillTriangle(leftfoot,63,midpoint,57,rightfoot,63,WHITE); //triangle graph
      }

      //////// RECTANGLE BAR GRAPH ////////////
      if (ShowBarGraphA1) {
        int A1barsize;
        int bitsA1 = mapf(AverageVoltagebitsA1, GraphScalerP1A1, GraphScalerP2A1, 0, 1024);
        if (ReverseGraphA1) {
          A1barsize = 127 - (bitsA1 / 8);
        } else {
          A1barsize = bitsA1 / 8;
        }
        LCD.fillRect(0,60,A1barsize,4,WHITE); //rectangle bar graph
      }

      ///////// TICK MARKS /////////
      if (ShowTickMarksA1) {
        LCD.fillRect(0,58,1,6,WHITE);   //tick mark
        LCD.fillRect(31,61,1,3,WHITE);  //tick mark
        LCD.fillRect(63,58,1,6,WHITE);  //tick mark
        LCD.fillRect(95,61,1,3,WHITE);  //tick mark
        LCD.fillRect(127,58,1,6,WHITE); //tick mark
      }
    } else if (VInA1 > DangerToManifoldA1){
      LCD.setTextSize(0);
      LCD.setCursor(0, 57);
      LCD.print(" !DANGER TO MANIFOLD!");
    }
  }
	LCD.display();
  /////////// SEND DATA TO BLUETOOTH CORDLESS TELEPHONES ////////////
	Serial.print("[");
	Serial.print(temperature);
	Serial.print(",");
	Serial.print(eContent);
	Serial.print(",");
	Serial.print(desiredVoltage,1);
	Serial.print(",");
  Serial.print(verz,1);
  Serial.print(",");
  Serial.print(VInA1,1);
	Serial.print("]");
  Watchdog.reset();
	delay(refreshDelay);
  Watchdog.reset();
	counttick = 0;
}








//////////// FANCY SCALER ////////////
double mapf(double x, double in_min, double in_max, double out_min, double out_max)
{
	return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}


/////////// READ A1 INPUT ///////////
int MeasureVoltage()
 {
   int rawbitsA1 = analogRead(A1);
   return rawbitsA1;
 }
Loading
ssd1306