#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