// modi.ino - de modi van de klimaatmodule als toestandsmachine (hoofdstuk 32; klimaat.ino versie 4)
// OPSTART -> DAG of NACHT volgens de klok; ALARM bij een fout; ONDERHOUD met een lange druk op S1 (3 s).
#include <Wire.h>
#include <RTClib.h>
#include <Adafruit_SHT31.h>
#include "pinnen.h"
#include "dak.h"

enum Modus { OPSTART, DAG, NACHT, ALARM, ONDERHOUD };
const char* NAAM[] = {"OPSTART", "DAG", "NACHT", "ALARM", "ONDERHOUD"};
const byte A_SENSOR = 1, A_WATER = 2, A_NIVEAU = 4, A_DAK = 8;     // oorzaken van een alarm, als bits

const unsigned long MEET_MS = 2000;
const unsigned long LANG_MS = 3000;                  // zo lang indrukken = onderhoud aan of uit
const unsigned long ONDERHOUD_MAX_MS = 3600000UL;    // vergeten uit te zetten: na een uur terug
const int WERKLICHT = 60, LAMPEN_DAG = 200;          // PWM van de groeilampen
const int VENT_LAAG = 90, VENT_VOL = 255;

Adafruit_SHT31 sht;
RTC_DS3231 rtc;
DateTime klok;
Modus modus = OPSTART;
unsigned long modusSinds = 0, vorigeMeting = 0, knopSinds = 0, droogSinds = 0, ledSinds = 0;
byte alarmen = 0;                                    // welke alarmen er nu zijn
float luchtT = NAN, gemAan = 0.5;
int geenMeting = 0;
bool verwarmen = false, koelen = false, warm = false, knopWas = false, langGemeld = false, niveauLaag = false;
bool ledAan = false;

// ---------------------------------------------------------------- meldingen
void printTwee(int x) {
  if (x < 10) Serial.print('0');
  Serial.print(x);
}

void melding(const char* tekst) {
  DateTime k = rtc.now();
  Serial.print("# ");
  printTwee(k.hour());
  Serial.print(':');
  printTwee(k.minute());
  Serial.print(':');
  printTwee(k.second());
  Serial.print(' ');
  Serial.println(tekst);
}

// ---------------------------------------------------------------- sensoren en alarmen
float niveauCm() {                                   // MPX5010 onderaan het reservoir (hoofdstuk 11 en 29)
  float u = analogRead(DRUK) * 5.0 / 1024.0;
  float p = (u / 5.0 - 0.04) / 0.09 * 1000.0;
  return p / (1000.0 * 9.81) * 100.0;
}

void meten(unsigned long nu) {
  float t, rv;
  if (sht.readBoth(&t, &rv)) { luchtT = t; geenMeting = 0; }
  else { luchtT = NAN; geenMeting++; }
  bool droog = digitalRead(VLOTTER) == HIGH;         // verbreekt ook als de draad los is
  if (!droog) droogSinds = nu;
  float h = niveauCm();
  if (!niveauLaag && h < 10.0) niveauLaag = true;    // hysterese: 10 en 12 cm
  else if (niveauLaag && h > 12.0) niveauLaag = false;
  byte nieuw = 0;
  if (geenMeting >= 15) nieuw |= A_SENSOR;           // 30 s geen temperatuur
  if (nu - droogSinds >= 10000) nieuw |= A_WATER;    // 10 s droog: geen dender van een golfje
  if (niveauLaag) nieuw |= A_NIVEAU;
  if (dak == DAK_FOUT) nieuw |= A_DAK;
  if (nieuw & ~alarmen & A_SENSOR) melding("oorzaak: geen temperatuur");
  if (nieuw & ~alarmen & A_WATER) melding("oorzaak: vlotter droog");
  if (nieuw & ~alarmen & A_NIVEAU) melding("oorzaak: reservoir onder 10 cm");
  if (nieuw & ~alarmen & A_DAK) melding("oorzaak: dak haalt zijn eindpunt niet");
  alarmen = nieuw;
}

// ---------------------------------------------------------------- de knop: kort of lang
void leesKnop(unsigned long nu, bool& kort, bool& lang) {
  bool in = digitalRead(KNOP) == HIGH;               // na de 74HC14: hoog = ingedrukt (hoofdstuk 24)
  kort = lang = false;
  if (in && !knopWas) { knopSinds = nu; langGemeld = false; }
  if (in && !langGemeld && nu - knopSinds >= LANG_MS) { lang = true; langGemeld = true; }
  if (!in && knopWas && !langGemeld) kort = true;
  knopWas = in;
}

// ---------------------------------------------------------------- de modi
bool isDag() { return klok.hour() >= 6 && klok.hour() < 22; }
Modus gewoon() { return isDag() ? DAG : NACHT; }

void naar(Modus nieuw, unsigned long nu) {
  Modus oud = modus;
  modus = nieuw;
  modusSinds = nu;
  char tekst[32] = "modus ";
  strcat(tekst, NAAM[nieuw]);
  melding(tekst);
  // bij binnenkomen en bij verlaten
  if (nieuw == ONDERHOUD) dakBlokkeer(true, nu);     // niemand onder een bewegend dak
  if (oud == ONDERHOUD) dakBlokkeer(false, nu);
  if (nieuw == NACHT) dakVraag(false);
  if (nieuw == DAG) dakVraag(warm);                  // overdag volgt het dak de temperatuur
}

void taakModus(unsigned long nu, bool kort, bool lang) {
  switch (modus) {
    case OPSTART:                                    // 5 s: sensoren en klok lezen
      if (nu - modusSinds >= 5000) naar(gewoon(), nu);   // een alarm volgt dan meteen vanuit DAG of NACHT
      break;
    case DAG:
    case NACHT:
      if (alarmen) naar(ALARM, nu);
      else if (lang) naar(ONDERHOUD, nu);
      else if (gewoon() != modus) naar(gewoon(), nu);
      else if (kort) dakKnop(nu);                    // korte druk: het dak met de hand
      break;
    case ALARM:
      if (kort) {                                    // bevestigen
        if (alarmen & A_DAK) {                       // het dak nog eens laten sluiten
          dakHerstel(nu);
          alarmen &= ~A_DAK;                         // het dak is niet meer in DAK_FOUT
        }
        if (alarmen == 0) naar(gewoon(), nu);
        else melding("alarm blijft: oorzaak nog aanwezig");
      } else if (lang) naar(ONDERHOUD, nu);
      break;
    case ONDERHOUD:
      if (lang || nu - modusSinds >= ONDERHOUD_MAX_MS) naar(gewoon(), nu);
      break;
  }
}

// ---------------------------------------------------------------- wat elke modus doet
void acties(unsigned long nu) {
  bool dag = isDag();
  // verwarming en ventilatoren met hysterese (hoofdstuk 31); zonder meting blind
  float laag = dag ? 18.0 : 16.0, hoog = laag + 1.0;
  if (isnan(luchtT)) {
    verwarmen = (nu / 1000UL) % 600UL < (unsigned long)(gemAan * 600);
    koelen = false;
  } else {
    if (!verwarmen && luchtT < laag) verwarmen = true;
    else if (verwarmen && luchtT > hoog) verwarmen = false;
    if (!koelen && luchtT > 25.0) koelen = true;
    else if (koelen && luchtT < 24.0) koelen = false;
    if (verwarmen) koelen = false;
    gemAan += ((verwarmen ? 1.0 : 0.0) - gemAan) * (MEET_MS / 3600000.0);
    // het dak: hysterese 25/27 geeft gebeurtenissen; overdag in DAG en ALARM (niet bij een dakfout)
    bool automatisch = dag && (modus == DAG || (modus == ALARM && !(alarmen & A_DAK)));
    if (!warm && luchtT > 27.0) { warm = true; if (automatisch) dakVraag(true); }
    else if (warm && luchtT < 25.0) { warm = false; if (automatisch) dakVraag(false); }
  }
  if (modus == ONDERHOUD) koelen = true;             // verse lucht voor wie binnen werkt
  digitalWrite(VERWARMING, (verwarmen && modus != OPSTART) ? HIGH : LOW);
  analogWrite(VENTILATOREN, koelen ? VENT_VOL : VENT_LAAG);
  // groeilampen: overdag aan (het dimmen van hoofdstuk 25 laten we hier weg), werklicht bij onderhoud
  int lamp = 0;
  if (modus == ONDERHOUD) lamp = WERKLICHT;
  else if (dag && modus != OPSTART) lamp = LAMPEN_DAG;
  analogWrite(LAMPEN, lamp);
  // pomp: minuut 0 tot 9 van elk uur, nooit zonder water, niet bij onderhoud
  bool pomp = (modus == DAG || modus == NACHT || modus == ALARM) && klok.minute() < 10
              && !(alarmen & (A_WATER | A_NIVEAU));
  digitalWrite(POMP, pomp ? HIGH : LOW);
  // CO2: alleen in DAG, met het dak dicht en zonder koelen: om het halfuur 30 s
  bool co2 = modus == DAG && dak == DICHT && !koelen && klok.minute() % 30 == 0 && klok.second() < 30;
  digitalWrite(CO2_KLEP, co2 ? HIGH : LOW);
}

void taakStatusled(unsigned long nu) {               // DAG en NACHT: rustig, ALARM: snel, ONDERHOUD: vast aan
  if (modus == ONDERHOUD) { digitalWrite(STATUSLED, HIGH); return; }
  unsigned long periode = (modus == ALARM) ? 100 : 500;
  if (nu - ledSinds < periode) return;
  ledSinds = nu;
  ledAan = !ledAan;
  digitalWrite(STATUSLED, (ledAan && modus != OPSTART) ? HIGH : LOW);
}

// ---------------------------------------------------------------- setup en loop
void setup() {
  const int UIT[] = {LAMPEN, PWM_DICHT, PWM_OPEN, DAK_ENABLE, POMP, CO2_KLEP, VENTILATOREN, VERWARMING, STATUSLED};
  for (int p : UIT) { pinMode(p, OUTPUT); digitalWrite(p, LOW); }   // veilig starten: alles uit
  digitalWrite(DAK_ENABLE, HIGH);
  pinMode(VLOTTER, INPUT_PULLUP);
  pinMode(KNOP, INPUT);
  pinMode(EIND_OPEN, INPUT_PULLUP);
  pinMode(EIND_DICHT, INPUT_PULLUP);
  Serial.begin(115200);
  Wire.begin();
  rtc.begin();
  sht.begin(0x44);
  klok = rtc.now();
  unsigned long nu = millis();
  droogSinds = nu;
  melding("klimaatmodule v4 (hoofdstuk 32)");
  naar(OPSTART, nu);
  dakStart(nu);
}

void loop() {
  unsigned long nu = millis();
  bool kort, lang;
  leesKnop(nu, kort, lang);
  if (nu - vorigeMeting >= MEET_MS) {
    vorigeMeting += MEET_MS;
    klok = rtc.now();
    meten(nu);
    acties(nu);
  }
  taakModus(nu, kort, lang);
  taakDak(nu);
  taakStatusled(nu);
}
