#define WOKWI
//Display Libraries
#include "LiquidCrystal_I2C.h"
#include <Wire.h>
//Time Libraries
#ifndef WOKWI
#include "Time.h"
#endif
#include "TimeLib.h"
#include "RTClib.h"
//SD Card Libraries
#include <SPI.h>
#include <SD.h>
#include <Servo.h>
#include "ClassRotMenue.h"
#include <TMRpcm.h> // Audio library...
TMRpcm audio; // create an object for use in this sketch
#define countof(a) (sizeof(a) / sizeof(a[0]))
#define ultraTrigPin 6
#define ultraEchoPin 5
#define sensorAlk A1
/*
UNO/ MEGA2560 – PINOUT
0 RXD
1 TXD
2 Endcoder CLK
3 Endcoder DT
4 Endcoder SW
5 Ultraschall Echo
6 Ultraschall Trigger
7 Relay 1
8 Relay 2
9 Servo
10 SS/PWM/SC SD 1
11 Lautsprecher
12 Gas Warner
13
14 A0 DHT 11 Temperatur Sensor
15 A1 Alkohol Sensor
16 A2
17 A3
18 A4 / SDA Display
19 A5 / SCL Display
20 A4.2 SDA Echtzeit Uhr SDA DS3231
21 A5.2 SCL Echtzeit Uhr SCL DS3231
50 MISO 50 SD1 // UNO 11
51 MOSI 51 SD1 // UNO 12
52 SCK SD1 // UNO 13
*/
enum MenueStates {
MAINAUS,
MAIN, // Zustand Hauptmenü
LICHT,
LICHTAUSSENAN,
LICHTAUSSENAUS,
LICHTINNENAN,
LICHTINNENAUS,
GASSENSOR,
GASSENSORAN,
GASSENSORAUS,
SERVO,
SERVOOEFFNEN,
SERVOSCHLIESSEN,
SERVOEINSTELLEN,
SERVOPOSOFFEN,
SERVOPOSGESCHLOSSEN,
MASCHINE,
MASCHINENSENSORAN,
MASCHINENSENSORAUS,
ALKTESTER,
ALKTESTERAN,
ALKTESTERAUS
};
LiquidCrystal_I2C lcd(0x27, 20, 4); // set the LCD address to 0x3F for a 20 chars and 4 line display
#ifndef WOKWI
RTC_DS3231 rtc;
#else
//############## WOKWI spezifisch ##############
RTC_DS1307 rtc;
//############## WOKWI spezifisch ##############
#endif
File myFile; //Dateivariable
String mydate = ""; //SD Datum des Ereignisses
String myyear = ""; //SD Jahr des Ereignisses
String myshortdate = ""; //SD Tag und Monat des Ereignisses
String ausgabeDaten = ""; //Art des Ereignisses
String input = ""; //Temporärer String
char tempstring; //SD eingelesenes Zeichen
const int sdPin = 10; //Pin der SD Card 10 ??
//Datums und Hilfsvariablen
String VarDatum = ""; //Datum zwischenspeichern
String VarShortDatum = ""; //Tag und Monat zwischenspeichern
String VarRTCDatum = ""; //Ausgelesenes DCF Datum
byte status = 0; // Stündlich in Status 0, setzt DCF Zeit
// ################################# von hier ###########################
// Pinbelegung
const byte clkPin = 2; // CLK Anschluss des Encoders
const byte dtPin = 3; // DT Anschluss des Encoders
const byte swPin = 4; // SW Anschluss des Encoders
const byte servoPin = 9; // Servo Anschluss
int servoPos = 0;
int servoPosLeft = 0;
int servoPosRight = 180;
Servo aServo;
byte state = MAINAUS;
// Aktuelle Datum/Zeit-Variable
// aus "DateTime now;"" ist "DateTime nun;" geworden, da es bei globaler Deklaration
// ansonsten einen Konflikt mit now() aus Time/TimeLib gibt
DateTime nun;
DateTime lastTime;
const byte MaxAusgaben = 6;
byte AnzahlAusgaben = 0;
String Ausgaben[MaxAusgaben];
RotMenue HauptMenue(clkPin, dtPin, swPin, "Hauptmen\365");
RotMenue LichtMenue(clkPin, dtPin, swPin, "Licht");
RotMenue GassensorMenue(clkPin, dtPin, swPin, "Gassensor");
RotMenue ServoMenue(clkPin, dtPin, swPin, "Servo");
RotMenue MaschinenMenue(clkPin, dtPin, swPin, "Maschinensensor");
RotMenue AlkTesterMenue(clkPin, dtPin, swPin, "AlkTester");
RotMenue ServoPosMenue(clkPin, dtPin, swPin, "Servo einstellen");
// ################################# bis hier ###########################
//-------------------------
// Setup Initialprogramm
//-------------------------
void setup() {
lcd.init(); // initialize the lcd
lcd.clear(); // Display löschen
lcd.backlight(); // Display anschalten
Serial.begin(115200); // Open serial communications
aServo.attach(servoPin); // Den Servo anschliessen
// ####################################################################################
// Das macht man ggf. in der echten Anwundung erst nachdem die Defaultwerte von SD
// oder aus dem EEPROM gelesen wurden:
aServo.write(servoPosLeft); // den Servo auf eine der beiden Positionen fahren
// ####################################################################################
rtcStart();
sdStart();
InitMenues();
lastTime = rtc.now();
}
//-------------------------
// Loop Hauptprogramm
//-------------------------
void loop() {
StateMachine();
}
//--------------------------------------------------------------------------------
//Startbildschirm
//--------------------------------------------------------------------------------
void StartBildschirm() {
ZeitDatumsVerwaltung();
TerminVerwaltung();
ZeigeAusgabeDaten();
}
#include "Funktionen.h"
//--------------------------------------------------------------------------------
//State Machine
//--------------------------------------------------------------------------------
void StateMachine() {
switch (state) {
case MAINAUS :
StartBildschirm();
if (EncoderConfirmed()) state = MAIN;
break;
case MAIN :
ActMenue(HauptMenue);
// if (state == MAINAUS) {lcdClearLine(1);lcdClearLine(2);}
if (state == MAINAUS) {
lcd.clear();
}
break;
case LICHT :
ActMenue(LichtMenue);
break;
case LICHTAUSSENAN :
LichtAussenAn();
state = LICHT;
break;
case LICHTAUSSENAUS :
LichtAussenAus();
state = LICHT;
break;
case LICHTINNENAN :
LichtInnenAn();
state = LICHT;
break;
case LICHTINNENAUS :
LichtInnenAus();
state = LICHT;
break;
case GASSENSOR :
ActMenue(GassensorMenue);
break;
case GASSENSORAN :
GasSensorAn();
state = GASSENSOR;
break;
case GASSENSORAUS :
GasSensorAus();
state = GASSENSOR;
break;
case SERVO :
ActMenue(ServoMenue);
break;
case SERVOOEFFNEN :
ServoOeffnet();
state = SERVO;
break;
case SERVOSCHLIESSEN :
ServoSchliesst();
state = SERVO;
break;
case SERVOEINSTELLEN :
ActMenue(ServoPosMenue);
break;
case SERVOPOSOFFEN :
ControlServo(servoPosLeft, SERVOPOSOFFEN);
// Hier muss der servoPosLeft-Wert noch im EEPROM oder auf SD gespeichert werden
break;
case SERVOPOSGESCHLOSSEN :
ControlServo(servoPosRight, SERVOPOSGESCHLOSSEN);
// Hier muss der servoPosRight-Wert noch im EEPROM oder auf SD gespeichert werden
break;
case MASCHINE :
ActMenue(MaschinenMenue);
break;
case MASCHINENSENSORAN :
MaschinenSensorAn();
state = MASCHINE;
break;
case MASCHINENSENSORAUS :
MaschinenSensorAus();
state = MASCHINE;
break;
case ALKTESTER :
ActMenue(AlkTesterMenue);
break;
case ALKTESTERAN :
AlkTesterAn();
state = ALKTESTER;
break;
case ALKTESTERAUS :
AlkTesterAus();
state = ALKTESTER;
break;
default:
state = MAIN;
break;
}
}
/*
relays
-- Licht Aussen an/aus
-- Licht Innen an/aus
GasSensor
-- an/aus
Servo Schlieser
-- Öffen/ schliessen
--Einstellen
Maschinenwache
-- Sensor an/ aus
Alkoholtester
-- Tester an/aus
Menü schliessen
*/
void InitMenues() {
HauptMenue.SetRevolving(true);
HauptMenue.Add(F("Licht"), LICHT);
HauptMenue.Add(F("Gassensor"), GASSENSOR);
HauptMenue.Add(F("Servo"), SERVO);
HauptMenue.Add(F("Maschinensensor"), MASCHINE);
HauptMenue.Add(F("Alkoholtester"), ALKTESTER);
HauptMenue.Add(F("Men\365 aus"), MAINAUS);
LichtMenue.SetRevolving(true);
LichtMenue.Add(F("Aussen an"), LICHTAUSSENAN);
LichtMenue.Add(F("Aussen aus"), LICHTAUSSENAUS);
LichtMenue.Add(F("Innen an"), LICHTINNENAN);
LichtMenue.Add(F("Innen aus"), LICHTINNENAUS);
LichtMenue.Add(F("Zur\365ck"), MAIN);
GassensorMenue.SetRevolving(true);
GassensorMenue.Add(F("An"), GASSENSORAN);
GassensorMenue.Add(F("Aus"), GASSENSORAUS);
GassensorMenue.Add(F("Zur\365ck"), MAIN);
ServoMenue.SetRevolving(true);
ServoMenue.Add(F("\357ffnen"), SERVOOEFFNEN);
ServoMenue.Add(F("schliessen"), SERVOSCHLIESSEN);
ServoMenue.Add(F("einstellen"), SERVOEINSTELLEN);
ServoMenue.Add(F("Zur\365ck"), MAIN);
MaschinenMenue.SetRevolving(true);
MaschinenMenue.Add(F("An"), MASCHINENSENSORAN);
MaschinenMenue.Add(F("Aus"), MASCHINENSENSORAUS);
MaschinenMenue.Add(F("Zur\365ck"), MAIN);
AlkTesterMenue.SetRevolving(true);
AlkTesterMenue.Add(F("An"), ALKTESTERAN);
AlkTesterMenue.Add(F("Aus"), ALKTESTERAUS);
AlkTesterMenue.Add(F("Zur\365ck"), MAIN);
ServoPosMenue.SetRevolving(true);
ServoPosMenue.Add(F("Offen-Pos."), SERVOPOSOFFEN);
ServoPosMenue.Add(F("Geschlossen-Pos."), SERVOPOSGESCHLOSSEN);
ServoPosMenue.Add(F("Zur\365ck"), SERVO);
};
/*
\365 ü F5
\341 ä E1
\357 ö EF
*/
void PrintMenue(RotMenue &aMenue) {
Serial.print(aMenue.ActMenueNo());
Serial.print("\t");
Serial.println(aMenue.ActMenueName());
WriteLcd(aMenue.ActMenueName(), aMenue.ActMenueTitle());
}
void ActMenue(RotMenue &aMenue) {
if (aMenue.StateHasChanged() || aMenue.Changed()) {
PrintMenue(aMenue);
}
if (aMenue.confirmed()) {
Serial.println(" Confirmed Menue\t" + aMenue.ActMenueName());
state = aMenue.ActMenueState();
}
}
void lcdClearLine(int i) {
lcd.setCursor(0, i);
lcd.print(F(" "));
}
void WriteLcd(String MenueName, String MenueTitle) {
lcd.clear();
// lcdClearLine(1);
lcd.setCursor(0, 0);
lcd.print(MenueTitle);
// lcdClearLine(2);
lcd.setCursor(0, 1);
lcd.print(MenueName);
}
boolean EncoderConfirmed() {
int Button = digitalRead(swPin);
static int lastButton = HIGH;
boolean returnValue = false;
if (lastButton != Button) {
lastButton = Button;
if (Button) {
returnValue = true;
}
}
return returnValue;
}
int EncoderValue() {
int encoderValue = 0;
int newClk = digitalRead(clkPin);
static int lastClk = LOW;
if (newClk != lastClk) {
lastClk = newClk;
int dtValue = digitalRead(dtPin);
if (newClk == LOW && dtValue == HIGH) {
encoderValue = 1;
}
if (newClk == LOW && dtValue == LOW) {
encoderValue = -1;
}
}
return encoderValue;
};
void ControlServo(int &value, int sub) {
static boolean ServoFirstIn = true;
int encoderV = EncoderValue(); // Ermittelt den aktuellen Encoder-Wert
if (encoderV != 0 || ServoFirstIn) { // Falls dies nicht Null ist oder die Routine neu aufgerufen wurde
ServoFirstIn = false; // Merken, dass es jetzt nicht mehr das "erste Mal" ist
// ###############################################################################################
// Es kann sein, dass die Werte "feinfühliger" als um den Wert n = 10 verändert werden müssen ....
// Das muss in der eigentlichen Anwendung ermittelt und ggf. korrigiert werden
// value += n * encoderV;
// ###############################################################################################
value += 10 * encoderV; // Den Wert um das 10-fache des Encoderwerts 10 x (1 oder -1) verändern
if (value < 0) value = 0; // Werte unter Null auf Null setzen
if (value > 180) value = 180; // Werte größer 180 auf 180 setzen
aServo.write(value); // Wert per analogWrite als PWM ausgeben
if (sub == SERVOPOSOFFEN) WriteLcd("Position : " + String(value), "Servo offen"); // Das LCD ansteuern
if (sub == SERVOPOSGESCHLOSSEN) WriteLcd("Position : " + String(value), "Servo geschlossen"); // Das LCD ansteuern
};
// Falls die Encodertaste betätigt wurde, FirstIn fürs nächste Mal auf true setzen
// die State Machine auf "SUBGREEN" umschalten und das zugehörige Menü auf dem LCD ausgeben
if (EncoderConfirmed()) {
// Die rechte Servogrenze kann nicht links von der linken sein
if (servoPosRight < servoPosLeft) servoPosRight = servoPosLeft;
// Die linke Servogrenze kann nicht rechts von der rechten sein
if (servoPosLeft > servoPosRight) servoPosLeft = servoPosRight;
aServo.write(servoPosLeft);
ServoFirstIn = true;
state = SERVOEINSTELLEN;
PrintMenue(ServoMenue);
};
}
//--------------------------------------------------------------------------------
// Zeit/Datumsverwaltung
//--------------------------------------------------------------------------------
void ZeitDatumsVerwaltung() {
nun = rtc.now();
datumZeit();
}
//--------------------------------------------------------------------------------
// SD-Card-Lesen und Termindaten erfassen
//--------------------------------------------------------------------------------
void TerminVerwaltung() {
if (status == 0) { // Status 0 Setze DCF und wenn neuer Tag, lese SD Daten
VarRTCDatum = AddleadinZero(nun.day()); //Wandel Datum in String
VarRTCDatum += ".";
VarRTCDatum += AddleadinZero(nun.month());
VarRTCDatum += ".";
VarRTCDatum += AddleadinZero(nun.year()); // Datum formatieren fertig
if (VarRTCDatum != VarDatum) { // Neuer Tag oder Erstlauf
lcd.clear(); // Display löschen
VarDatum = VarRTCDatum; // Abspeichern des neuen Tages
VarShortDatum = VarDatum.substring(0, 6); //Erzeuge dd.mm.
myFile = SD.open("daten.txt"); // Einlesen der Daten
if (myFile) {
while (myFile.available()) {
tempstring = myFile.read(); //Zeichenweise einlesen
if (tempstring != '\n') { //Erkennen von Zeilenumbruch
input += tempstring; //String aufaddieren
} else { //Zeilenumbruch gefunden, String complete
mydate = input.substring(0, 10); //Aufteilen des Strings Datum 10 Zeichen 01.01.1970
// Serial.println(mydate); //Ausgabe von Text in Zeile
myshortdate = input.substring(0, 6); //Aufteilen des Strings KurzDatum 6 Zeichen 01.01.
// Serial.println(myshortdate); //Ausgabe von Text in Zeile
myyear = input.substring(6, 10); //Aufteilen des Strings Jahr 4 Zeichen 1970
// Serial.println(myyear); //Ausgabe von Text in Zeile
ausgabeDaten = input.substring(11, input.length() - 1); //Auslesen des Eriegnisses
input = ""; //Löschen des Strings
/*------------------------------- dieser teil im wechsel ------------------------------------*/
//Serial.println(mydate); //Ausgabe von Text in Zeile
if (VarDatum == mydate) { //Wenn Datum gleich Ereignisdatum
AddAusgaben(ausgabeDaten); // Wenn es geht, in die Liste aufnehmen
}
if (((nun.year() - myyear.toInt()) > 0) && (VarShortDatum == myshortdate)) {
AddAusgaben("[" + ausgabeDaten + "]"); // Wenn es geht, in die Liste aufnehmen, ohne Vorzeichen
// als Wiederholtermin mit [...]
}
}
}
} else
{
while (!SD.begin(sdPin)) { // SD Karte nicht lesbar oder Textfile fehlt
lcd.print("SD fehler");
delay(3000);
}
lcd.print("SD neu Start!");
lcd.print(VarRTCDatum);
}//Ende File einlesen
myFile.close();
}
status = 1;
}
}
//--------------------------------------------------------------------------------
//Funktionen
//--------------------------------------------------------------------------------
// ################################# von hier #############################
/*
\365 ü F5
\341 ä E1
\357 ö EF
*/
void Sonderzeichen (String &Input) {
Input.replace("ä", "\341");
Input.replace("ö", "\357");
Input.replace("ü", "\365");
Input.replace("Ä", "\341");
Input.replace("Ö", "\357");
Input.replace("Ü", "\365");
}
void AddAusgaben(String Text) {
if (AnzahlAusgaben < MaxAusgaben) {
Sonderzeichen(Text);
Ausgaben[AnzahlAusgaben] = Text;
AnzahlAusgaben++;
Ausgaben[MaxAusgaben - 1] = "Zu viele Ausgaben!"; // Der letzte String ist immer diese Warnung!
}
}
void ZeigeAusgabeDaten() {
static unsigned long lastOutput = 0;
static byte Eintrag = 0;
if (millis() - lastOutput > 5000) { // Alle - hier im Beispiel - drei Sekunden
lastOutput = millis();
if (AnzahlAusgaben > 0) {
// Und nun den aktuellen Eintrag ausgeben
lcd.setCursor(0, 3);
lcd.print(Ausgaben[Eintrag]);
for (byte i = 0; i < 20 - Ausgaben[Eintrag].length(); i++, lcd.print(" "));
Eintrag++;
if (Eintrag >= AnzahlAusgaben) Eintrag = 0;
}
}
}
// ################################# bis hier ###########################
void datumZeit() {
// aktuelle Zeit holen
DateTime aktuell = rtc.now();
// Nur wenn die Sekundenangabe abweicht, werden Zeit und Datum ausgegeben
// sonst nimmt dies so viel Zeit in Anspruch, dass das sichere Erkennen
// der Encoder-Bedienung gefährdet ist
if (lastTime.second() != aktuell.second()) {
lastTime = aktuell;
lcd.setCursor(0, 0);
char Datum[] = "DD.MM.YYYY";
lcd.print(aktuell.toString(Datum));
char Zeit[] = " hh:mm:ss";
lcd.print(aktuell.toString(Zeit));
}
}
void rtcStart()
{
if (! rtc.begin()) {
Serial.println("keine uhr gefunden");
while (1);
}
#ifndef WOKWI
// Gibt es für die emulierte RTC im Wokwi nicht:
if (rtc.lostPower()) {
Serial.println("RTC lost power, lets set the time!");
// following line sets the RTC to the date & time this sketch was compiled
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// This line sets the RTC with an explicit date & time, for example to set
// January 21, 2014 at 3am you would call:
// rtc.adjust(DateTime(2022, 3, 06, 3, 0, 0));
}
#endif
}
void sdStart() {
if (!SD.begin(sdPin)) { // Öffne SD Karte
lcd.print("SD Karte Fehlt!");
delay(3000);
softReset(); // Reboot
} else {
lcd.print("SD Karte Gefunden");
delay(5000);
lcd.print(" ");
}
}
//Ergänze Zahl mit führender Null wenn kleiner 10
String AddleadinZero(int digit) {
String Returncode = "";
if (digit < 10) {
Returncode = "0";
Returncode += digit;
} else {
Returncode = digit;
}
return Returncode; //Zahl nun im Format "01"
}
//Reboot Funktion
void softReset() {
asm volatile (" jmp 0");
}