// https://forum.arduino.cc/t/sketch-fur-servo-mit-3-potis-drehgeber-lcd-verbesserungsvorschlage/1385507
// This code is orientated to the llvm coding standard
// https://llvm.org/docs/CodingStandards.html
#include <Servo.h> // https://github.com/arduino-libraries/Servo
#include <LiquidCrystal_I2C.h> // https://github.com/johnrickman/LiquidCrystal_I2C
#include <RotaryEncoder.h> // https://github.com/mathertel/RotaryEncoder
#include <Button_SL.hpp> // https://github.com/DoImant/Button_SL
//
// Global constants (gc)
//
namespace gc {
constexpr uint8_t LcdCols {16};
constexpr uint8_t LcdRows {2};
constexpr uint8_t EncPinClk {6};
constexpr uint8_t EncPinDt {3};
constexpr uint8_t EncPinSw {7}; // Button of encoder
constexpr uint8_t ServoPin {8};
constexpr int CounterStart {5};
constexpr int CounterMax {99};
constexpr int HomePosition {90};
constexpr int LimitLowest {50};
constexpr int LimitHighest {50};
constexpr int LimitDelay_ms {50}; // Maximum delay in milliceconds
constexpr int DelayMin_ms {5}; // Minimum Delay for servo movement in milliseconds;
constexpr int AnalogResolution {(1 << 10) - 1}; // 10 Bit
} // namespace gc
//
// Data definitions
//
enum class State : uint8_t { Input, Run };
enum class PotiName : uint8_t { Lowest, Highest, Delay };
// Return enumerators of class <T> as number
template <typename T> constexpr uint8_t toIndex(T Enumerator) noexcept { return static_cast<uint8_t>(Enumerator); }
struct PotiData {
const uint8_t Pin;
const int MaxValue;
int Value;
};
//
// global Objects / variables
//
// {PinNr, Max. possible Value, measured Value}
PotiData Potis[] {
{A1, gc::LimitLowest, 0}, // lowest
{A0, gc::LimitHighest, 0}, // highest
{A2, gc::LimitDelay_ms, 0} // Speed delay
};
LiquidCrystal_I2C Lcd = LiquidCrystal_I2C(0x27, gc::LcdCols, gc::LcdRows);
RotaryEncoder Encoder {gc::EncPinClk, gc::EncPinDt, RotaryEncoder::LatchMode::FOUR3};
Servo MyServo;
Btn::ButtonSL Button {gc::EncPinSw};
//
// Functions
//
//////////////////////////////////////////////////////////////////////////////
/// \brief Get the Encoder Value object
///
/// \param Enc Reference to Encoder Object.
/// \param Value Value to be set using the encoder (reference).
/// \param ValueMax Maximum value that can be set.
/// \return true Value has been changed.
/// \return false No change.
//////////////////////////////////////////////////////////////////////////////
bool getEncoderValue(RotaryEncoder& Enc, int& Value, const int ValueMax) {
int SaveValue = Value;
Enc.tick();
switch (Enc.getDirection()) {
case RotaryEncoder::Direction::CLOCKWISE: Value = (Value + 1) % ValueMax; break;
case RotaryEncoder::Direction::COUNTERCLOCKWISE: Value = (Value != 0) ? Value - 1 : ValueMax; break;
case RotaryEncoder::Direction::NOROTATION: break;
}
return (Value != SaveValue); // true if the Value has been changed.
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Display data
///
/// \param Disp Reference to Display Object
/// \param PD Pointer to Array of Datasructure (analog values)
/// \param Cnt Value of a counter to be displayed
//////////////////////////////////////////////////////////////////////////////
void dispPrint(LiquidCrystal_I2C& Disp, PotiData* PD, int Cnt) {
char buffer[gc::LcdCols + 1];
snprintf(buffer, sizeof(buffer), "L: %2d R: %2d", PD[toIndex(PotiName::Lowest)].Value,
PD[toIndex(PotiName::Highest)].Value);
Disp.setCursor(0, 0);
Disp.print(buffer);
snprintf(buffer, sizeof(buffer), "S: %2d C: %2d", gc::LimitDelay_ms - PD[toIndex(PotiName::Delay)].Value, Cnt);
Disp.setCursor(0, 1);
Disp.print(buffer);
}
//////////////////////////////////////////////////////////////////////////////
/// \brief Reading out the analog values of a set of potentiometers.
///
/// \tparam (&PD)[N] Reference to Structure array and number of elements [N]
/// \return true At least one value has changed
/// \return false No changes
//////////////////////////////////////////////////////////////////////////////
template <size_t N> bool getPotiValues(PotiData (&PD)[N]) {
decltype(PD[0].Value) SavedValue[N];
for (size_t i = 0; i < N; ++i) {
SavedValue[i] = PD[i].Value;
PD[i].Value = map(analogRead(PD[i].Pin), 0, gc::AnalogResolution, 0, PD[i].MaxValue);
}
// Check whether one of the read analog values has changed.
for (size_t i = 0; i < N; ++i) {
if (SavedValue[i] != PD[i].Value) { return true; } // Yes. At leased one value has been changed
}
return false;
}
//
// Main Program
//
void setup() {
Serial.begin(115200);
MyServo.attach(gc::ServoPin); // attaches the servo on pin 8 to the servo object
pinMode(LED_BUILTIN, OUTPUT);
Button.begin();
Button.setDebounceTime_ms(30);
Lcd.init();
Lcd.backlight();
getPotiValues(Potis);
delay(100);
dispPrint(Lcd, Potis, gc::CounterStart);
}
void loop() {
static State CaseState {State::Input}; // Statemachine
static int Counter {gc::CounterStart}; // Run Counter (Set via rotary encoder)
static int SaveCounter {Counter};
switch (CaseState) {
case State::Input:
// Returns true if at least one analog value or counter has changed. If so display new Value(s).
if (getPotiValues(Potis) || getEncoderValue(Encoder, Counter, gc::CounterMax)) { dispPrint(Lcd, Potis, Counter); }
if (Button.tick() != Btn::ButtonState::notPressed) {
// Do only run if both values aren't zero.
if ((Potis[toIndex(PotiName::Lowest)].Value + Potis[toIndex(PotiName::Highest)].Value) && Counter > 0) {
CaseState = State::Run;
SaveCounter = Counter;
}
}
break;
case State::Run: { // <-This curly bracket is necessary so that local variables can be defined in the case branch.
--Counter;
// Calculate the range of movement
int Negative = (gc::HomePosition - Potis[toIndex(PotiName::Lowest)].Value);
int Positive = (gc::HomePosition + Potis[toIndex(PotiName::Highest)].Value);
// Repeat the movement until the counter is zero.
for (int Pos = Negative; Pos <= Positive; ++Pos) // goes from negativ degrees to positiv degrees
{
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
MyServo.write(Pos); // tell servo to go to position in variable 'pos'
delay(Potis[toIndex(PotiName::Delay)].Value + gc::DelayMin_ms); // waits for the servo to reach the position
}
for (int Pos = Positive; Pos >= Negative; --Pos) // goes from positiv degrees to negativ degrees
{
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
MyServo.write(Pos);
delay(Potis[toIndex(PotiName::Delay)].Value + gc::DelayMin_ms); // waits for the servo to reach the position
}
// Movement sequence completed if Counter = 0. Enable new input.
if (Counter < 1) {
MyServo.write(gc::HomePosition);
Counter = SaveCounter;
CaseState = State::Input;
}
dispPrint(Lcd, Potis, Counter);
} break;
}
}
L
R
Left
Right
Speed
Counter (rotate)
Start (press button)