#include <U8g2lib.h>
#include <RotaryEncoder.h>
#include <Button_SL.hpp>

using ulong = unsigned long int;
using uint = unsigned int;

//
// Gobal constants
//
constexpr byte PIN_IN1 {2};
constexpr byte PIN_IN2 {3};
constexpr byte PIN_BTN {4};
constexpr byte PIN_STEPPER {5};

constexpr byte VALUE_MIN {1};   // Counter minimum value
constexpr byte VALUE_MAX {4};   // Counter maximum value
constexpr byte DIGITS {1};
constexpr byte BUFFERLENGTH {DIGITS + 1};

constexpr uint COMPARTMENTS {8};   // The table should make a 360° turn in eight steps.
// constexpr ulong TIMEUNIT_MS {1000UL * 60 * 60};             // One hour in milliseconds
constexpr ulong TIMEUNIT_MS {1000UL * 60};                  // Minutes for demonstration purposes
constexpr byte STEPS_PER_REVOLUTION {200};                  // Number of steps for a 360° rotation
constexpr byte STEPS_PER_TIMEPERIOD {200 / COMPARTMENTS};   // Step segments

constexpr byte DISPLAY_MAX_X {128};
constexpr byte DISPLAY_MAX_Y {64};

// Font u8g2_font_logisoso42_tn    // 24 Width 51 Hight
constexpr byte FONT_WIDTH {24};
constexpr byte FONT_HIGHT {51};
constexpr const char *STATUS_STRING[4] {"set", "running", "\0", "\0"};   // Note enum class status

constexpr byte STATUS_X {5};   // Statusstring coordinates
constexpr byte STATUS_Y {5};
constexpr byte TRGL_X {10};   // TRGL = Triangle coordinates
constexpr byte TRGL_Y {20};
constexpr byte TRGL_STEP_X {20};
constexpr byte TRGL_STEP_Y {15};
constexpr byte COUNTER_X {(DISPLAY_MAX_X - FONT_WIDTH * (BUFFERLENGTH - 1)) / 2};   // Column = X Coordinate
constexpr byte COUNTER_Y {(DISPLAY_MAX_Y + FONT_HIGHT) / 2};                        // Row = Y coordinate

//
// Data definitions
//
class Interval {
public:
  bool operator()(const ulong duration) {
    if (false == isStarted) { return start(false); }
    return (millis() - timeStamp >= duration) ? start(true) : false;
  }
  void stop() { isStarted = false; }

private:
  bool start(bool state = false) {
    isStarted = !state;   // Set the value to true on the first call
    timeStamp = millis();
    return state;
  }

private:
  bool isStarted {false};   // Flag = true if the first Operator() call has been made.
  ulong timeStamp {0};
};

enum class Status : byte { set, running, start, calc };

//
// Extend the RotaryEncoder class with
// 1. a lower and upper limit for a counter
// 2. a variable step count
//
template <typename T> class RotaryEncoderExt : public RotaryEncoder {
public:
  RotaryEncoderExt(uint8_t pin1, uint8_t pin2, T valMin, T valMax, LatchMode mode = LatchMode::FOUR0)
      : RotaryEncoder {pin1, pin2, mode}, valMin {valMin}, valMax {valMax} {}
  template <typename V> bool query(V &value);
  void setStep(int value) { step = value; }

private:
  const T valMin;   // value minimum
  const T valMax;   // value maximum
  int step {1};
};

template <typename T> template <typename V> bool RotaryEncoderExt<T>::query(V &value) {
  uint8_t flag {true};
  tick();
  switch (getDirection()) {
    case RotaryEncoder::Direction::NOROTATION: flag = false; break;
    case RotaryEncoder::Direction::CLOCKWISE: value = value < valMax ? value + step : valMin; break;
    case RotaryEncoder::Direction::COUNTERCLOCKWISE: value = value > valMin ? value - step : valMax; break;
  }
  return flag;
}

struct StepperData {
  const byte pin;           // Pin to which the stepper driver is connected
  const uint stepsPerRev;   // Number of steps for a 360° rotation
  int remainingSteps;
};

struct TurnTableData {
  StepperData stepper;
  const byte compartments;   // Number of table compartments
  const ulong timeUnit;      //
  byte timeCounter;          // Counter for a time unit (eg. counter * time)
  ulong timePeriod;          // Time between each table rotation
  byte rotationCount;        // Number of rotations performed
  Status runningStatus;      // Status e.g set time (hours) or running (drive stepper)
  Interval wait;             // Object for controlling the waiting cycles
};

//
// Global objects / variables
//
using DisplayType = U8G2_SSD1306_128X64_NONAME_2_HW_I2C;
DisplayType u8g2(U8G2_R0);

using namespace Btn;
ButtonSL button {PIN_BTN};

using Encoder = RotaryEncoderExt<decltype(VALUE_MIN)>;
Encoder encoder {PIN_IN1, PIN_IN2, VALUE_MIN, VALUE_MAX, RotaryEncoder::LatchMode::FOUR3};

//
// Function(s).
//

//////////////////////////////////////////////////////////////////////////////
/// @brief Display Data on the OLED
///
/// @param disp     Display object
/// @param value    Value to be displayed
/// @param status   set or running
//////////////////////////////////////////////////////////////////////////////
void displayData(DisplayType &disp, byte value, Status status) {
  char charBuffer[BUFFERLENGTH];
  snprintf(charBuffer, BUFFERLENGTH, "%d", value);
  disp.firstPage();
  do {
    disp.setFont(u8g2_font_6x10_tr);
    if (status != Status::running) {
      disp.drawStr(STATUS_X, STATUS_Y, STATUS_STRING[static_cast<byte>(Status::set)]);
      disp.drawDisc(TRGL_X + TRGL_STEP_X / 2, TRGL_Y + TRGL_STEP_Y, TRGL_STEP_X / 2);
    } else {
      disp.drawStr(STATUS_X, STATUS_Y, STATUS_STRING[static_cast<byte>(Status::running)]);
      disp.drawTriangle(TRGL_X, TRGL_Y, TRGL_X + TRGL_STEP_X, TRGL_Y + TRGL_STEP_Y, TRGL_X, TRGL_Y + TRGL_STEP_Y * 2);
    }
    disp.setFont(u8g2_font_logisoso42_tn);   // 24 Width 51 Hight
    disp.drawStr(COUNTER_X - 10, COUNTER_Y, charBuffer);
    disp.setFont(u8g2_font_logisoso22_tr);   // 27 Width 33 Hight
    disp.drawStr(COUNTER_X + 20, COUNTER_Y, "hrs.");
  } while (u8g2.nextPage());
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Rotate the stepper
///
/// @param pin      Pin to which the motor is connected
/// @param steps    Number of steps
//////////////////////////////////////////////////////////////////////////////
void spinMotor(byte pin, uint steps) {
  for (byte x = 0; x < steps; ++x) {
    digitalWrite(pin, HIGH);
    delay(20);
    digitalWrite(pin, LOW);
    delay(20);
  }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief State machine
///
/// @param data   Turntable data
/// @param disp   Display object
/// @param enc    Encoder object
/// @param btn    Button for changing the status
//////////////////////////////////////////////////////////////////////////////
void fsm(TurnTableData &data, DisplayType &disp, Encoder &enc, ButtonSL &btn) {
  switch (data.runningStatus) {
    case Status::start:
      data.runningStatus = Status::set;
      displayData(disp, data.timeCounter, data.runningStatus);
      break;
    case Status::set:
      if (enc.query(data.timeCounter)) { displayData(disp, data.timeCounter, data.runningStatus); }
      break;
    case Status::calc:
      data.runningStatus = Status::running;
      data.timePeriod = data.timeCounter * (data.timeUnit / data.compartments);
      displayData(disp, data.timeCounter, data.runningStatus);   // Display running status
      break;
    case Status::running:
      if (data.wait(data.timePeriod)) {
        // Recalculate each step sequence to avoid possible rounding errors in the case of dividers 
        // that prevent the total number of steps from being divided without a fraction.
        uint steps = data.stepper.remainingSteps / (data.compartments - data.rotationCount);
        data.stepper.remainingSteps -= steps;
        spinMotor(data.stepper.pin, steps);
        ++data.rotationCount;
      }
      // if periodcount = COMPARTMENTS stop running mode (rotation ends)
      if (data.rotationCount == data.compartments) {
        data.runningStatus = Status::set;
        data.rotationCount = 0;
        data.stepper.remainingSteps = data.stepper.stepsPerRev;
        displayData(disp, data.timeCounter, data.runningStatus);
      }
      break;
    default: break;
  }

  // Press button to change status between set and run
  if (btn.tick() != ButtonState::notPressed) {
    data.runningStatus = (data.runningStatus == Status::set) ? Status::calc : Status::set;
    data.wait.stop();   // Reset timer
    displayData(disp, data.timeCounter, data.runningStatus);
  }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Initialization part of the main program
///
//////////////////////////////////////////////////////////////////////////////
void setup(void) {
  // Serial.begin(115200);
  u8g2.begin();
  pinMode(PIN_STEPPER, OUTPUT);

  button.begin();
  button.releaseOn();
  button.setDebounceTime_ms(100);
}

//////////////////////////////////////////////////////////////////////////////
/// @brief main program
///
//////////////////////////////////////////////////////////////////////////////
void loop() {
  // {Stepper Pin, Steps per Rev, remaining Steps}, Compartment, Time Unit, start value, timeperiod , rotation count, status
  static TurnTableData ttData {
      {PIN_STEPPER, STEPS_PER_REVOLUTION, STEPS_PER_REVOLUTION},
      COMPARTMENTS, TIMEUNIT_MS, VALUE_MIN, 0, 0, Status::start
  };
  fsm(ttData, u8g2, encoder, button);
}
Push Button (middle) to start. Turn encoder (arrows) to change the value
A4988