//////////////////////////////////////////////////////////////////////////////
/// @brief Example program
/////////////////////////////////////////////////////////////////////////////////
/// @file main.cpp
/// @author Kai R. ()
/// @brief  PWM Fan control - Suitable for microcontroller boards based on the ATMega328.
///
/// @date 2024-02-25
/// @version 1.0
///
//////////////////////////////////////////////////////////////////////////////

#include <Arduino.h>
#include <Streaming.h>           // https://github.com/janelia-arduino/Streaming
#include <LiquidCrystal_I2C.h>   // https://github.com/johnrickman/LiquidCrystal_I2C
#include <util/atomic.h>

//////////////////////////////////////////////////////////////////////////////
/// global definitions / constants
///
//////////////////////////////////////////////////////////////////////////////

// #define SERIALOUT
#ifdef SERIALOUT
Print &cout = Serial;
#endif

#define MAX_FANS 2U   // Define number of fans 1 or 2 !
static_assert(MAX_FANS > 0 && MAX_FANS < 3, "Only 1U or 2U are allowed");

constexpr uint8_t PIN_ANALOG {A0};

// Values for generating the PWM signal
constexpr uint16_t HERTZ {25000};
constexpr uint16_t PRESCALER {1};
constexpr uint8_t CLOCKSET {_BV(CS10)};
constexpr uint16_t ICOUNTER {static_cast<uint16_t>((F_CPU / (2UL * PRESCALER * HERTZ)) - 1)};

// Timeconstants
constexpr uint16_t INTERVAL_MS {1000};
constexpr uint16_t STARTUP_TIME_SEC {10};

constexpr uint8_t MAX_AVERAGE_IDX {12};   // Number of array elements for calculating the average

// LCD values
constexpr uint8_t LCD_LINES {4};   // Set 2 for 2x16 Display, 4 for 4x20 Display
constexpr uint8_t LCD_COLONS {(LCD_LINES == 2) ? 16 : 20};
constexpr uint8_t SECOND_LINE {(LCD_LINES == 2) ? 1 : 2};
constexpr uint8_t LEFT_COL {(LCD_LINES == 2) ? 0 : 2};
constexpr uint8_t LEFT_COL_CNTDWN {(LCD_LINES == 2) ? 2 : 4};
constexpr uint8_t DATA_DIGITS {5};
constexpr uint8_t DATA_WIDTH {DATA_DIGITS + 2};
constexpr uint8_t DATA_COL {LEFT_COL + DATA_DIGITS + 1};
constexpr uint8_t CNTDWN_BAR {10};
constexpr uint8_t CNTDWN_CHAR {'*'};

//////////////////////////////////////////////////////////////////////////////
/// @brief Timerclass for millis()
///
//////////////////////////////////////////////////////////////////////////////
class Timer {
public:
  void start() { timeStamp = millis(); }
  bool operator()(const unsigned long duration) const { return (millis() - timeStamp >= duration) ? true : false; }

private:
  unsigned long timeStamp {0};
};

//////////////////////////////////////////////////////////////////////////////
/// @brief Class for calculating a simple average
///
/// @tparam T           Datatype
/// @tparam MAX_SIZE    Maximum number of individual values for calculating the average
//////////////////////////////////////////////////////////////////////////////
template <typename T, uint8_t MAX_SIZE = 1> class Average {
public:
  uint8_t getIdx() const { return idx; };

  const void setValue(T val) {
    values[idx] = val;
    if (++idx >= maxIdx) {   // array is filled
      divideByMaxIdx = true;
      idx = 0;
    }
  }

  T getAverage() const {
    T tmp = 0;
    for (auto value : values) { tmp += value; }
    // Only divide by the total number of array elements
    // once the array has been completely filled with measured values.
    return (divideByMaxIdx == true) ? tmp / maxIdx : tmp / idx;
  }

private:
  const uint8_t maxIdx {MAX_SIZE};
  T values[MAX_SIZE] {0};
  uint8_t idx {0};
  bool divideByMaxIdx {false};
};

//////////////////////////////////////////////////////////////////////////////
/// @brief Calculates the average rotation frequency of a fan.
///
/// @tparam T         Type
/// @tparam MAX_SIZE  Arraysize
/// @param duration   Time between two edge signals
/// @param readings   Object which is used to calculate the average
/// @return uint32_t  Average value
//////////////////////////////////////////////////////////////////////////////
template <typename T, uint8_t MAX_SIZE = 1> uint32_t calcAvgFrequency(volatile uint32_t &duration, T &readings) {
  uint32_t freq {0};
  if (duration > 0) { freq = 100000000UL / duration; }
  readings.setValue(freq);
  return readings.getAverage();
}

//////////////////////////////////////////////////////////////////////////////
/// Global variables / objects
///
//////////////////////////////////////////////////////////////////////////////

// used by interrupt routine(s)
volatile uint32_t durationInt0;
volatile uint32_t lastDurInt0;

volatile uint32_t durationInt1;
volatile uint32_t lastDurInt1;

Timer timer;
Average<uint32_t, MAX_AVERAGE_IDX> rpmInt[MAX_FANS];
// LiquidCrystal_I2C lcd(0x27, LCD_COLONS, LCD_LINES);   // I2C Addr 2x16
LiquidCrystal_I2C lcd(0x3F, LCD_COLONS, LCD_LINES);   // I2C Addr 4x20
Print &coutLcd = lcd;

//////////////////////////////////////////////////////////////////////////////
/// @brief Enable external interrupts for INT0 and INT1 Pins (D2, D3)
///
//////////////////////////////////////////////////////////////////////////////
void enableExternalInterrupts() {
#if (MAX_FANS > 1U)
  EICRA = _BV(ISC11) | _BV(ISC10) | _BV(ISC01) | _BV(ISC00);   // Int0 & Int1 -> rising edge
  EIMSK = _BV(INT1) | _BV(INT0);
#else
  EICRA = _BV(ISC01) | _BV(ISC00);   // Int0 & Int1 -> rising edge
  EIMSK = _BV(INT0);
#endif
}

// External Interrupt Pin D2
//
//////////////////////////////////////////////////////////////////////////////
/// @brief  To determine the speed, measure the time between the edge changes (rising edge).
///         A fan provides two edge changes per revolution.
//////////////////////////////////////////////////////////////////////////////
ISR(INT0_vect) {
  uint32_t timestamp = micros();
  durationInt0 = (timestamp - lastDurInt0);
  lastDurInt0 = timestamp;
}

// External Interrupt Pin D3, second fan
ISR(INT1_vect) {
  uint32_t timestamp = micros();
  durationInt1 = (timestamp - lastDurInt1);
  lastDurInt1 = timestamp;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Init PWM signal (25kHz)
///
//////////////////////////////////////////////////////////////////////////////
void pwmInit() {
  DDRB |= _BV(PB1);   // OCR1A Pin (PB1 / D9)
  TCCR1A = _BV(WGM11);
  TCCR1B = _BV(WGM13);
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ICR1 = ICOUNTER; }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Set dutycycle of the PWM signal
///
/// @param dc   0-100%
//////////////////////////////////////////////////////////////////////////////
void pwmSetDutyCycle(uint8_t dc) {
  uint16_t ocr1a = (dc > 100) ? 100 : dc;
  ocr1a = static_cast<uint16_t>((((ICR1 * 10UL * ocr1a) / 100) + 5) / 10);   // Result must be 16Bit
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { OCR1A = ocr1a; }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief start PWM signal
///
//////////////////////////////////////////////////////////////////////////////
void pwmStart() {
  TCCR1A |= _BV(COM1A1);
  TCCR1B |= CLOCKSET;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief stop PWM signal
///
//////////////////////////////////////////////////////////////////////////////
void pwmStop() {
  TCCR1B &= ~(_BV(CS10) | _BV(CS11) | _BV(CS12));
  TCCR1A &= ~(_BV(COM1A1));
  PORTB &= ~(_BV(PB1));   // OCR1A Pin (PB1 / D9) to LOW
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Show Frequency and RPM on the serial console
///
/// @param freq
//////////////////////////////////////////////////////////////////////////////
#ifdef SERIALOUT
void showRPM(uint32_t freq, uint8_t fanNr) {
  uint32_t rpm = freq * 60;
  rpm /= 200;
  cout << "Lüfter Nr: " << fanNr << " Frequenz: " << freq / 100 << "." << freq % 100 << "Hz Rpm: " << rpm << endl;
}
#endif

//////////////////////////////////////////////////////////////////////////////
/// @brief Show start sequence on LCD
///
//////////////////////////////////////////////////////////////////////////////
void lcdCountDown() {
  uint8_t counter {STARTUP_TIME_SEC};
  lcd.setCursor(LEFT_COL_CNTDWN, 0);
  coutLcd << F("Start up: ") << STARTUP_TIME_SEC;
  lcd.setCursor(LEFT_COL_CNTDWN, SECOND_LINE);
  coutLcd << "[" << _PAD(CNTDWN_BAR, ' ') << "]";
  timer.start();
  do {
    if (timer(INTERVAL_MS) == true) {
      timer.start();
      lcd.setCursor(LEFT_COL_CNTDWN + CNTDWN_BAR, 0);
      coutLcd << _FMT(("%"), _WIDTH(--counter, 2));
      lcd.setCursor(LEFT_COL_CNTDWN + 1, SECOND_LINE);
      coutLcd << _PAD(CNTDWN_BAR - counter, CNTDWN_CHAR);
    }
  } while (counter > 0);
  delay(1000);
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Display the main output text
///
//////////////////////////////////////////////////////////////////////////////
void lcdMain() {
  lcd.clear();
  lcd.setCursor(LEFT_COL, 0);
  coutLcd << "FAN 1:" << _PAD(DATA_WIDTH, ' ') << "RPM";
#if (MAX_FANS > 1U)
  lcd.setCursor(LEFT_COL, SECOND_LINE);
  coutLcd << "FAN 2:" << _PAD(DATA_WIDTH, ' ') << "RPM";
#endif
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Display the RPM
///
/// @param freq
/// @param fanNr
//////////////////////////////////////////////////////////////////////////////
void lcdData(uint32_t freq, uint8_t fanNr) {
#if (MAX_FANS > 1U)
  uint8_t lcdLine = (fanNr == 0) ? 0 : SECOND_LINE;
#else
  uint8_t lcdLine {0};
#endif
  uint32_t rpm = freq * 60;
  rpm /= 200;
  lcd.setCursor(DATA_COL, lcdLine);
  coutLcd << _FMT(("%"), _WIDTH(rpm, DATA_WIDTH - 1));
}

//////////////////////////////////////////////////////////////////////////////////////
// Main program
//////////////////////////////////////////////////////////////////////////////////////
void setup(void) {
#ifdef SERIALOUT
  Serial.begin(115200);
#endif
  lcd.init();
  Wire.setClock(400000);   // Comment out if there are problems with the display.
  lcd.backlight();
  enableExternalInterrupts();
  pwmInit();
  pwmSetDutyCycle(100);
  pwmStart();
  lcdCountDown();
  lcdMain();
  timer.start();
}

void loop() {
  static uint16_t oldValue {0};
  static uint32_t freq[MAX_FANS] {0};

  // Determine the potentiometer position and set the duty cycle accordingly.
  uint16_t value = map(analogRead(PIN_ANALOG), 0, 1015, 20, 100);
  if (value != oldValue) {
    oldValue = value;
    pwmSetDutyCycle(value);
  }

  // Determine the fan speed and output it via the serial interface / LCD.
  // Do this every INTERVAL_MS milliseconds
  if (timer(INTERVAL_MS) == true) {
    timer.start();
    freq[0] = calcAvgFrequency(durationInt0, rpmInt[0]);
    freq[0] = random(100,120);
#if (MAX_FANS > 1U)
    freq[1] = calcAvgFrequency(durationInt1, rpmInt[1]);
    freq[1] = random(200,220);
    for (uint8_t i = 0; i < MAX_FANS; ++i) {
  #ifdef SERIALOUT
      showRPM(freq[i], i);
  #endif
      lcdData(freq[i], i);
    }
  #ifdef SERIALOUT
    cout << endl;
  #endif
#else
  #ifdef SERIALOUT
    showRPM(freq[0], 0);
  #endif
    lcdData(freq[0], 0);
#endif
  }
}