// #define LCDI2C
#define DHT_DEBUG
#ifdef LCDI2C
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#else
#include <LiquidCrystal.h>
#endif
#include <OneButton.h>
#include <dht.h> // https://github.com/RobTillaart/DHTlib
#include <RTClib.h>
#include <Streaming.h>
//
// Datentyp-Alias(e)
//
using Funktionspointer = void (*)(void);
//
// Eigene(r) Datentyp(en)
//
enum Schritte : uint8_t { FsProzent, FsLiter, FsSpannung, TemperaturMax, TemperaturMin, TemperaturLuftfeuchte, Zeit };
struct Dht22_t {
float Temperatur;
float Luftfeuchte;
};
//
// Klasse zur Ermittlung eines gleitenden Mittelwertes
//
template <const size_t Buffersize = 2> class AnalogFilter {
static_assert(Buffersize > 0 && Buffersize < 256, "The Buffer size must be between 2 and 255 bytes. ");
public:
constexpr size_t size() { return Buffersize; }
uint16_t& operator[](size_t Idx) { return Buffer[Idx]; }
uint16_t average(uint16_t);
private:
uint16_t Buffer[Buffersize] {0};
uint8_t BufferIndex {0};
uint32_t Total {0};
};
template <const size_t Buffersize> uint16_t AnalogFilter<Buffersize>::average(uint16_t Reading) {
// Perform average on sensor readings
Total = Total - Buffer[BufferIndex]; // subtract the last reading:
Buffer[BufferIndex] = Reading; // store adc value
Total = Total + Buffer[BufferIndex]; // add value to Total:
BufferIndex = ((Buffersize - 1) > BufferIndex) ? BufferIndex + 1 : 0;
// calculate and return the average (rounded):
return static_cast<uint16_t>(((Total * 10) / Buffersize + 5) / 10);
}
//
// Globale Konstante(n)
//
// Pins
constexpr uint8_t gkPinButton {12};
constexpr uint8_t gkPinDht22 {13};
constexpr uint8_t gkPinAnalog {A1};
// Analogwandler (ADC)
constexpr int gkAdcBits {10}; // Bitbreite des Analogdigitalwandlers
constexpr float gkBetriebsspannung {5.0}; // Betriebsspannung des µControllers
// Divisor für die Umrechnung des ADC Wertes in eine Spannung
const float gkSpannungDivisor {1.0 / (gkBetriebsspannung / pow(2, gkAdcBits))};
//
// Globale Objekte
//
#ifdef LCDI2C
LiquidCrystal_I2C Lcd(0x3F, 20, 4);
#else
LiquidCrystal Lcd(8, 9, 4, 5, 6, 7); // initialize the library Syntax: LiquidCrystal Lcd(rs, enable, d4, d5, d6, d7)
#endif
Print& lCout {Lcd};
Print& cout {Serial};
OneButton Button;
dht Dht22;
RTC_DS1307 Rtc; // RTC Clock Object
// RTC_DS3231 Rtc; // RTC Clock Object
AnalogFilter<15> AdcWerte; // Gleitender Mittelwert aus 15 ADC Messwerten
//
// Globale Variablen
//
size_t AktuellerSchritt {0};
uint8_t KorrWS {0}; // Korrektur Anzeige Stunde Winter- / Sommerzeit
float Spannung {0.0}; // Variable "Spannung" als am Display anzuzeigenden Wert und für weitere Berechungen definieren
float Tmax {-111.0}; // Variable für Temperatur max. definieren
float Tmin {111.0}; // Variable für Temperatur min. definieren
Dht22_t Klima; // Temperatur und Luftfeuchte Speicher
//
// --- Funktionen definieren ---
//
Dht22_t dhtAbfrage() {
#ifdef DHT_DEBUG
int chk = Dht22.read22(gkPinDht22);
switch (chk) {
case DHTLIB_OK: cout << F("OK"); break;
case DHTLIB_ERROR_CHECKSUM: cout << F("Checksum error"); break;
case DHTLIB_ERROR_TIMEOUT: cout << F("Time out error"); break;
case DHTLIB_ERROR_CONNECT: cout << F("Connect error"); break;
case DHTLIB_ERROR_ACK_L: cout << F("Ack Low error"); break;
case DHTLIB_ERROR_ACK_H: cout << F("Ack High error"); break;
default: cout << F("Unknown error"); break;
}
cout << endl;
#else
Dht22.read22(gkPinDht22);
#endif
return {Dht22.temperature, Dht22.humidity};
}
void aktualisiereMinMax(const Dht22_t& Daten) {
if (Daten.Temperatur > Tmax) { Tmax = Daten.Temperatur; }
if (Daten.Temperatur < Tmin) { Tmin = Daten.Temperatur; }
}
void fsaText() // Text "Köbis Wassertank Füllstand" anzeigen
{
Lcd.setCursor(0, 0); // set the cursor to column 0, line 1(note: counting begins with 0 ! line 1 is the second row)
lCout << F("K\357bis Wassertank"); // \357 = "ö" (\47 = "´ ")
Lcd.setCursor(0, 1);
lCout << F("F\365llstand "); // \365 = "ü"
}
void gwhText() {
Lcd.setCursor(0, 0);
lCout << F("Gew\341chshaus");
Lcd.setCursor(0, 1);
}
void resetText(const char* Text) {
Lcd.clear();
Lcd.setCursor(0, 0);
lCout << Text;
}
void celsiusText() { lCout << _BYTE(223) << 'C'; }
void dhtInnen(const Dht22_t& Daten) {
gwhText();
lCout << _WIDTH(_FLOAT(Daten.Temperatur, 1), 4) << ' ';
celsiusText();
lCout << ' ' << _WIDTH(_FLOAT(Daten.Luftfeuchte, 1), 4) << F(" %rF");
}
void zeitanzeige() {
constexpr char Wochentag[7][3] {"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"};
DateTime Jetzt = Rtc.now(); // Auslesen von Datum und Uhrzeit
Lcd.setCursor(0, 0);
lCout << _FMT("% %.%.%", Wochentag[Jetzt.dayOfTheWeek()], _WIDTHZ(Jetzt.day(), 2), _WIDTHZ(Jetzt.month(), 2),
Jetzt.year());
Lcd.setCursor(0, 1);
lCout << _FMT("%:%:%", _WIDTHZ(Jetzt.hour() + KorrWS, 2), _WIDTHZ(Jetzt.minute(), 2), _WIDTHZ(Jetzt.second(), 2));
}
// Funktionen bei AktuellerSchritt 0 definieren -> Anzeige in %
void schritt0() {
fsaText();
if (Spannung < 1) { Spannung = 1; } // Werte <0 in der Anzeige eliminieren
lCout << _WIDTH(_FLOAT(((Spannung - 0.814) * 45.2284), 0), 4) << F(" %"); // ...,0 => 0 Nachkommastellen
}
// Funktionen bei AktuellerSchritt 1 definieren -> Anzeige in Liter
void schritt1() {
fsaText();
if (Spannung < 1) { Spannung = 1; } // Werte <0 in der Anzeige eliminieren
// Volt in Liter Füllstand umrechnen (...,0 => Anzeige keine Nachkommastellen)
lCout << _WIDTH(_FLOAT(((Spannung - 0.814) * 2487.562), 0), 4) << F(" L");
}
// Funktionen bei AktuellerSchritt 2 definieren - Anzeige Messwert Füllstandssensor in Volt
void schritt2() {
fsaText();
lCout << _WIDTH(_FLOAT(Spannung, 2), 4) << F(" V");
}
// Funktionen bei AktuellerSchritt 3 definieren - Anzeige Temperatur max.
void schritt3() {
gwhText();
lCout << F("max. ") << _FLOAT(Tmax, 1) << ' ';
celsiusText();
}
// Funktionen bei AktuellerSchritt 4 definieren - Anzeige Temperatur min.
void schritt4() {
gwhText();
lCout << F("min. ") << _FLOAT(Tmin, 1) << ' ';
celsiusText();
}
// Funktionen bei AktuellerSchritt 5 definieren -> DHT22 Gewächshaus aktuell Innen Temperatur und Feuchte anzeigen
void schritt5() { dhtInnen(Klima); }
// Funktionen bei AktuellerSchritt 6 definieren -> Datum und Uhrzeit anzeigen
void schritt6() { zeitanzeige(); }
// Mit Taster bei kurzem Drücken Schritte hoch zählen
void click() {
++AktuellerSchritt;
Lcd.clear();
}
// Mit Doppelklick Variable "KorrWS" zwischen +0 und +1 Stunde umschalten
void doubleclick() {
if (AktuellerSchritt == Schritte::Zeit) {
KorrWS = (!KorrWS) ? 1 : 0;
zeitanzeige();
}
}
void longPressStop() { // Funktionen bei langem Taster drücken und nach loslassen in den Schritten auslösen
if (AktuellerSchritt == Schritte::TemperaturMax) { // Temp. max. Reset
resetText("Temp. max. Reset");
Tmax = -111;
delay(2000);
Lcd.clear();
}
if (AktuellerSchritt == Schritte::TemperaturMin) { // Temp. min. Reset
resetText("Temp. min. Reset");
Tmin = 111;
delay(2000);
Lcd.clear();
}
// Datum, Wochentag, Uhrzeit übernehmen wie nachfolgend gesetzt
if (AktuellerSchritt == Schritte::Zeit) { Rtc.adjust(DateTime(2022, 9, 25, 20, 5, 0)); }
}
void setup() {
Serial.begin(115200);
cout << F("Start ") << __FILE__ << ' ' << __TIME__ << ' ' << __DATE__ << endl;
Rtc.begin();
#ifdef LCDI2C
Lcd.init();
Lcd.backlight();
#else
Lcd.begin(16, 2); // Display mit 16 Zeichen und 2 Zeilen definieren
#endif
Lcd.clear();
Button.setup(gkPinButton, INPUT_PULLUP, true);
Button.attachClick(click);
Button.attachDoubleClick(doubleclick);
Button.attachLongPressStop(longPressStop);
}
void loop() {
constexpr Funktionspointer Fp[7] {schritt0, schritt1, schritt2, schritt3, schritt4, schritt5, schritt6};
constexpr size_t MaxSchritte {sizeof(Fp) / sizeof(Fp[0]) - 1};
static uint8_t Sekunde {1};
static uint8_t VorSekunde {1};
static size_t VorSchritt {99};
Button.tick();
if (AktuellerSchritt > MaxSchritte) { AktuellerSchritt = 0; }
// Anzeige jede Sekunde aktualisieren.
// Schrittvergleich um im schlechtesten Fall keine ganze Sekunde auf den Anzeigewechsel warten zu müssen.
if (VorSekunde != Sekunde || AktuellerSchritt != VorSchritt) {
VorSekunde = Sekunde;
VorSchritt = AktuellerSchritt;
// Nur alle Sekunde die Spannung messen, benötigt 15 Sekunden bis die Anzeige eingependelt ist (AnalogFilter<15>)
Spannung = (AdcWerte.average(analogRead(gkPinAnalog)) / gkSpannungDivisor);
// Nur alle 5 Sekunden die Temperatur messen
if (!(Sekunde % 5)) {
Klima = dhtAbfrage();
aktualisiereMinMax(Klima);
}
Fp[AktuellerSchritt]();
}
DateTime Jetzt = Rtc.now(); // Auslesen von Datum und Uhrzeit
Sekunde = Jetzt.second();
}