#include <Arduino.h>
#include <Streaming.h>
Print& cout {Serial};
using uint = unsigned int;
using ulong = unsigned long;
//////////////////////////////////////////////////
// Global constants
//////////////////////////////////////////////////
namespace gc {
constexpr ulong mainDelay_ms {15000};
constexpr bool isOutPinInverted {false}; // if true then LOW is on and HIGH is off, elsewhere choose "false"
} // namespace gc
//////////////////////////////////////////////////
// Class and structure definitions
//////////////////////////////////////////////////
class Timer {
public:
void start() { timeStamp = millis(); }
bool operator()(const ulong duration) const { return (millis() - timeStamp >= duration) ? true : false; }
private:
ulong timeStamp {0};
};
namespace MoistureMeter {
class Interface {
public:
Interface() = default;
virtual ~Interface() {}
Interface(const Interface&) = delete; // prevent copy
Interface& operator=(const Interface&) = delete; // prevent assignment
virtual void begin(bool) = 0;
virtual uint run() = 0;
virtual void setThreshold(const uint) = 0;
virtual void unLock() = 0;
virtual uint getThreshold() const = 0;
virtual uint getMinVal() const = 0;
virtual uint getMaxVal() const = 0;
virtual uint getAnalogVal() const = 0;
virtual byte getDeviceId() const = 0;
virtual bool isActive() const = 0;
virtual operator bool() const = 0;
virtual bool operator()() const = 0;
};
template <byte deviceId, byte analogPin, uint minVal, uint maxVal, uint thVal, byte switchPin, ulong delay_ms>
class Device : public Interface {
static_assert(analogPin >= A0 && analogPin <= A7, "No usable analog analogPin.");
static_assert(maxVal > minVal, "The upper limit value must be greater than the lower limit value.");
static_assert(thVal >= minVal && thVal <= maxVal,
"The threshold value must be between the minimum and maximum value.");
public:
explicit Device(uint hyst = 0) : threshold {thVal}, hysteresis {hyst} {
if (threshold + hysteresis < threshold) { hysteresis = 0; }
}
~Device() {}
Device(const Device&) = delete; // prevent copy
Device& operator=(const Device&) = delete; // prevent assignment
virtual void begin(bool isInverted = false) {
on = (byte)!isInverted;
off = !on;
digitalWrite(switchPin, off);
pinMode(switchPin, OUTPUT);
}
virtual uint run() override {
mValue = analogRead(analogPin);
if (!locked) {
byte state = digitalRead(switchPin);
if (state == off && checkMvalue()) {
switchOn();
} else if (state == on && timer(delay_ms)) {
switchOff();
}
}
return mValue;
}
virtual void setThreshold(const uint threshold) override {
if (threshold >= minVal && threshold <= maxVal) { this->threshold = threshold; }
}
virtual void unLock() { locked = false; } // Enable switchPins for switching again
virtual uint getThreshold() const override { return threshold; }
virtual uint getMinVal() const override { return minVal; }
virtual uint getMaxVal() const override { return maxVal; }
virtual uint getAnalogVal() const override { return mValue; }
virtual byte getDeviceId() const override { return deviceId; }
virtual bool isActive() const override { return digitalRead(switchPin) ^ (on == LOW); } // XOR!!
virtual operator bool() const override { return mValue >= threshold; }
virtual bool operator()() const override { return operator bool(); }
bool switchOn() {
timer.start();
cout << F("Device ") << deviceId << F(": Switch on watering for ") << delay_ms << F(" ms on pin nr. ") << switchPin
<< "\n";
digitalWrite(switchPin, on);
return on;
}
bool switchOff() {
cout << F("Device ") << deviceId << F(": Switch off watering.\n");
digitalWrite(switchPin, off);
locked = true; // Lock so that the pin does not become active again immediately, even if the soil is still
// considered too dry. Unlocking with the unLock() method.
return off;
}
bool checkMvalue() {
if (!isBelow && mValue < threshold) { // Add hysteresis value at the transition from sufficient moist to dry
isBelow = true;
threshold += hysteresis;
} else if (isBelow &&
mValue >= threshold) { // Reset hysteresis at the transition from too dry to sufficiently moist
isBelow = false;
threshold -= hysteresis;
}
return (mValue < threshold);
}
private:
uint threshold;
uint hysteresis;
uint mValue {0};
Timer timer;
byte on {HIGH};
byte off {LOW};
bool locked {false};
bool isBelow {false};
};
} // namespace MoistureMeter
// Helper structure for flow control
struct ProcessFlow {
const size_t maxIndex; // Number of soil moisture meters
uint deviceIndex; // Index to the device currently being controlled
uint activityFlags; // A flag bit indicates whether a switchPin is active for the respective device.
};
//////////////////////////////////////////////////
// Global variables
//////////////////////////////////////////////////
// DeviceNr, Analogpin, Min-MeasureValue, Max-MeasureValue, Threshold(wet/dry), Solenoid Pin, Switching time in ms
// {} <- no parameter -> Hysteresis = 0 e.g. {5} -> Hysteresis = 5
MoistureMeter::Interface* mMeters[] {
new MoistureMeter::Device<1, A0, 400, 800, 550, 3, 2000> {5},
new MoistureMeter::Device<2, A1, 400, 600, 475, 4, 3000> {5},
new MoistureMeter::Device<3, A2, 400, 800, 650, 5, 1500> {5},
};
namespace MoistureMeter {
constexpr size_t devices {sizeof(mMeters) / sizeof(mMeters[0])};
constexpr size_t maxIndex {devices - 1};
} // namespace MoistureMeter
Timer mainTimer;
ProcessFlow processFlow {MoistureMeter::maxIndex, 0, 0};
//////////////////////////////////////////////////
// Functions
//////////////////////////////////////////////////
template <size_t N> void printValues(MoistureMeter::Interface* (&devices)[N]) {
for (auto& device : devices) {
cout << F("Device ") << device->getDeviceId() << F(": Analog value: ") << device->getAnalogVal()
<< F(" Threshold: ") << device->getThreshold() << F(" -> Soil ");
((*device)() == true) ? cout << F("moisture sufficient") : cout << F("too dry!");
cout << "\n";
}
}
template <size_t N> bool checkSoilMoisture(MoistureMeter::Interface* (&devices)[N], ProcessFlow& pcf) {
if (pcf.deviceIndex > pcf.maxIndex) {
pcf.deviceIndex = 0;
return true; // All moisture meters have been checked.
}
devices[pcf.deviceIndex]->run();
bool isActive = devices[pcf.deviceIndex]->isActive();
// Set a flag for the respective device if a pin is active.
// Delete the flag bit again when the pin has been set to inactive again.
(isActive) ? bitSet(pcf.activityFlags, pcf.deviceIndex) : bitClear(pcf.activityFlags, pcf.deviceIndex);
++pcf.deviceIndex;
return false;
}
template <size_t N> void unlockSwitchPins(MoistureMeter::Interface* (&devices)[N]) {
for (auto& device : devices) { device->unLock(); }
}
void setup() {
Serial.begin(115200);
cout << F("Start...\n");
for (auto& mMeter : mMeters) { mMeter->begin(gc::isOutPinInverted); }
}
void loop() {
static bool mustDoCheck {true};
switch (mustDoCheck) {
case true:
if (checkSoilMoisture(mMeters, processFlow) && !processFlow.activityFlags) {
mainTimer.start();
printValues(mMeters); // Just print data
cout << F("\nWaiting for the next soil moisture meter test...\n");
unlockSwitchPins(mMeters); // Unlock so that the switch pins can be switched again during the next test
mustDoCheck = false;
}
break;
case false:
if (mainTimer(gc::mainDelay_ms)) { mustDoCheck = true; } // Waiting for Godot
break;
}
}