// DCF77 Emulator
// 
//
/* MIT License
Copyright 2022 Marcel Thraenhardt

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, 
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//
// based on ideas and code from:
// https://www.elektormagazine.de/magazine/elektor-201803/41314
// https://github.com/thijse/Arduino-DCF77/tree/master/examples/DCFBinaryStream
//
// Schematic and Simulation can be found on Wokwi:
//  https://wokwi.com/projects/347759046168674898
// 
// 
// 
#include <WiFi.h>
#include <Wire.h>
#include <time.h>
#include <Ticker.h>
#include <esp_sntp.h>
#include <LiquidCrystal_I2C.h>
#include <DCF77.h> // Decode DCF77 for testing 
#include <TimeLib.h>
#include "arduino_secrets.h"

LiquidCrystal_I2C LCD = LiquidCrystal_I2C(0x27, 16, 2);

// Settings
#define NTP_SERVER  "ptbtime3.ptb.de" // Same master clock as the original DCF77 Transmitter ;-)
#define TIMEZONE    "CET-1CEST,M3.5.0,M10.5.0/3" // Europe-Berlin  
#define LedPin 23 // blink LED
#define DSTPin 15 // DST Output, just for debugging
#define DCFPin 12 // DCF output, connect to zour clock instead of DCF reveiver module
#define DCFInPin 14 // DCF input for buil-in decoder
#define INVERTDCF true // true or false; most DCF77 modules have an inverted output (=true)

char ssid[] = SECRET_SSID;  // your network SSID (name) in arduino_secrets.h
char pass[] = SECRET_PASS;  // your network password in arduino_secrets.h

// DCF generator settings
//how many total pulses we have
//three complete minutes + 2 head pulses and one tail pulse
#define MaxPulseNumber 183
#define FirstMinutePulseBegin 2
#define SecondMinutePulseBegin 62
#define ThirdMinutePulseBegin 122

//complete array of pulses for three minutes
//0 = no pulse, 1=100msec, 2=200msec
int PulseArray[MaxPulseNumber];

//Timers
Ticker DcfOutTimer, Displaytimer;

// DCF Decocder
DCF77 DCF = DCF77(DCFInPin,DCFInPin,INVERTDCF);


// Variables
int PulseCount = 0;
int DCFOutputOn = 0;
int PartialPulseCount = 0;
time_t last_sync;

// Debug for NTP sync
void timeSyncCallback(struct timeval *tv)
{
  Serial.println("\n----Time Sync-----");
  Serial.println(tv->tv_sec);
  last_sync = tv->tv_sec; 
  Serial.println(ctime(&tv->tv_sec));
}

// DCF Decode Output
void digitalClockDisplay(time_t _time){
  tmElements_t tm;   
  breakTime(_time, tm);

  Serial.println("DCF77 decoded datagram:");
  Serial.print("Time: ");
  printDigits(tm.Hour);
  Serial.print(":");
  printDigits(tm.Minute);
  Serial.print(":");
  printDigits(tm.Second);
  Serial.print(" Date: ");
  Serial.print(tm.Year+1970);
  Serial.print("-");
  Serial.print(tm.Month);
  Serial.print("-");
  Serial.println(tm.Day);
}

void printDigits(int digits){
  // utility function for digital clock display: prints preceding colon and leading 0
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

// DCF generation routines
void initDCFencoder() {
  //first 2 pulses: 1 + blank to simulate the packet beginning
  //il primo bit e' un 1
  PulseArray[0] = 1;
  //missing pulse indicates start of minute
  PulseArray[1] = 0;
  //last pulse after the third 59° blank
  PulseArray[MaxPulseNumber - 1] = 1;
  
  PulseCount = 0;
  DCFOutputOn = 0;    //we begin with the output OFF
}

void prepareDCFdatagram() {
  //note: we add two minutes because the dcf protocol send the time of the FOLLOWING minute
  //and our transmission begins the next minute more
  //time_t ThisTime = secsSince1900 - seventyYears + ( timeZone * 3600 ) + 120;
  time_t now = time(nullptr);
  time_t now_plus_2_min = now + 120;
  struct tm now_tm2 = *localtime( &now_plus_2_min);

  //if we are over about the 56° second we risk to begin the pulses too late, so it's better
  //to skip at the half of the next minute and NTP+recalculate all again
  if (now_tm2.tm_sec > 56){
    delay(30000);
    return;      
  }

  //calculate bis array for the first minute
  CalculateDCFDataArray(FirstMinutePulseBegin, now_plus_2_min);

  //add one minute and calculate array again for the second minute
  now_plus_2_min += 60;
  CalculateDCFDataArray(SecondMinutePulseBegin, now_plus_2_min);

  //one minute more for the third minute
  now_plus_2_min += 60;
  CalculateDCFDataArray(ThirdMinutePulseBegin, now_plus_2_min);

  //how many to the minute end ?
  //don't forget that we begin transmission at second 58°
  int SkipSeconds = 58 - now_tm2.tm_sec;
  delay(SkipSeconds * 1000);
  //begin
  DCFOutputOn = 1;

  //three minutes are needed to transmit all the packet
  //then wait more 30 secs to locate safely at the half of minute
  //NB 150+60=210sec, 60secs are lost from main routine
  delay(150000);
} 

void CalculateDCFDataArray(int ArrayOffset, time_t t) {
  int n,Tmp,TmpIn;
  int ParityCount = 0;
  struct tm timeinfo = *localtime(&t);

  //first 20 bits are logical 0s
  for (n=0;n<20;n++)
    PulseArray[n+ArrayOffset] = 1;

  //DayLightSaving bit
  if (timeinfo.tm_isdst)
    PulseArray[17+ArrayOffset] = 2;
  else
    PulseArray[18+ArrayOffset] = 2;
    
  //bit 20 must be 1 to indicate time active
  PulseArray[20+ArrayOffset] = 2;

  //calculate minutes bits
  TmpIn = Bin2Bcd(timeinfo.tm_min);
  for (n=21;n<28;n++) {
    Tmp = TmpIn & 1;
    PulseArray[n+ArrayOffset] = Tmp + 1;
    ParityCount += Tmp;
    TmpIn >>= 1;
  };
  if ((ParityCount & 1) == 0)
    PulseArray[28+ArrayOffset] = 1;
  else
    PulseArray[28+ArrayOffset] = 2;

  //calculate hour bits
  ParityCount = 0;
  TmpIn = Bin2Bcd(timeinfo.tm_hour);
  for (n=29;n<35;n++) {
    Tmp = TmpIn & 1;
    PulseArray[n+ArrayOffset] = Tmp + 1;
    ParityCount += Tmp;
    TmpIn >>= 1;
  }
  if ((ParityCount & 1) == 0)
    PulseArray[35+ArrayOffset] = 1;
  else
    PulseArray[35+ArrayOffset] = 2;
   ParityCount = 0;
  //calculate day bits
  TmpIn = Bin2Bcd(timeinfo.tm_mday);
  for (n=36;n<42;n++) {
    Tmp = TmpIn & 1;
    PulseArray[n+ArrayOffset] = Tmp + 1;
    ParityCount += Tmp;
    TmpIn >>= 1;
  }
  //calculate weekday bits
  int tmp_wday = timeinfo.tm_wday;
  if (tmp_wday == 0) tmp_wday = 7; // map sunday to day 7
  TmpIn = Bin2Bcd(tmp_wday); 
  for (n=42;n<45;n++) {
    Tmp = TmpIn & 1;
    PulseArray[n+ArrayOffset] = Tmp + 1;
    ParityCount += Tmp;
    TmpIn >>= 1;
  }
  //calculate month bits
  TmpIn = Bin2Bcd(timeinfo.tm_mon+1); // 0...11 -> +1
  for (n=45;n<50;n++) {
    Tmp = TmpIn & 1;
    PulseArray[n+ArrayOffset] = Tmp + 1;
    ParityCount += Tmp;
    TmpIn >>= 1;
  }
  //calculate year bits
  TmpIn = Bin2Bcd(timeinfo.tm_year +1900 - 2000);   //only last 2 digits coded
  for (n=50;n<58;n++) {
    Tmp = TmpIn & 1;
    PulseArray[n+ArrayOffset] = Tmp + 1;
    ParityCount += Tmp;
    TmpIn >>= 1;
  }
  //date parity
  if ((ParityCount & 1) == 0)
    PulseArray[58+ArrayOffset] = 1;
  else
    PulseArray[58+ArrayOffset] = 2;

  //last missing pulse
  PulseArray[59+ArrayOffset] = 0;

   //for debug: print the whole 180 secs array
  /*Serial.print(':');
  for (n=0;n<60;n++)
    Serial.print(PulseArray[n+ArrayOffset]);
  Serial.print('\n');
  */
}

//called every 100msec
//for DCF77 output
void DcfOut() {

  if (DCFOutputOn == 1) {
    switch (PartialPulseCount++) {
      case 0:
        if (PulseArray[PulseCount] != 0)
          if (INVERTDCF) digitalWrite(DCFPin, HIGH);
          else digitalWrite(DCFPin, LOW);
        break;
      case 1:
        if (PulseArray[PulseCount] == 1)
          if (INVERTDCF) digitalWrite(DCFPin, LOW);
          else digitalWrite(DCFPin, HIGH);
        break;
      case 2:
        if (INVERTDCF) digitalWrite(DCFPin, LOW);
        else digitalWrite(DCFPin, HIGH);
        break;
      case 9:
        if (PulseCount++ == (MaxPulseNumber -1 )){     //one less because we FIRST tx the pulse THEN count it
          PulseCount = 0;
          DCFOutputOn = 0;
        };
        PartialPulseCount = 0;
        break;
    };
  };

}

void DisplayUpdate(){
  printLocalTime();
  digitalWrite(LedPin, !digitalRead(LedPin)); //LED blink
}

int Bin2Bcd(int dato) {
  int msb,lsb;

  if (dato < 10)
    return dato;
  msb = (dato / 10) << 4;
  lsb = dato % 10; 
  return msb + lsb;
}

void setTimezone(String timezone){
  Serial.printf("  Setting Timezone to %s\n",timezone.c_str());
  setenv("TZ",timezone.c_str(),1);  //  Now adjust the TZ.  Clock settings are adjusted to show the new local time
  tzset();
}

void initTime(String timezone){
  struct tm timeinfo;

  Serial.println("Setting up time");
  sntp_set_time_sync_notification_cb(timeSyncCallback);
  sntp_set_sync_interval(1*60*60*1000); // in ms shouldn't be more often than every 15mins when using public servers
  configTime(0, 0, NTP_SERVER);    // First connect to NTP server, with 0 TZ offset
  if(!getLocalTime(&timeinfo)){
    Serial.println("  Failed to obtain time");
    return;
  }
  Serial.println("  Got the time from NTP");
  // Now we can set the real timezone
  setTimezone(timezone);
}

void setTime(int yr, int month, int mday, int hr, int minute, int sec, int isDst){
  struct tm tm;

  tm.tm_year = yr - 1900;   // Set date
  tm.tm_mon = month-1;
  tm.tm_mday = mday;
  tm.tm_hour = hr;      // Set time
  tm.tm_min = minute;
  tm.tm_sec = sec;
  tm.tm_isdst = isDst;  // 1 or 0
  time_t t = mktime(&tm);
  Serial.printf("Setting time: %s", asctime(&tm));
  struct timeval now = { .tv_sec = t };
  settimeofday(&now, NULL);
}

void spinner() {
  static int8_t counter = 0;
  const char* glyphs = "\xa1\xa5\xdb";
  LCD.setCursor(15, 1);
  LCD.print(glyphs[counter++]);
  if (counter == strlen(glyphs)) {
    counter = 0;
  }
}

void printLocalTime() {
  struct tm timeinfo;
  //LCD.clear();
  if (!getLocalTime(&timeinfo)) {
    LCD.setCursor(0, 1);
    LCD.println("Connection Err");
    return;
  }

  time_t now = time(nullptr);
  int since_last_sync = now - last_sync;
  LCD.setCursor(0, 0);
  char strBuf[6];
  snprintf(strBuf,6,"%5d",since_last_sync);
  LCD.print(strBuf);LCD.print("s");
  LCD.setCursor(6, 0);
  LCD.println(&timeinfo, "  %H:%M:%S");

  LCD.setCursor(0, 1);
  LCD.println(&timeinfo, "%Y-%m-%d %Z  ");
  // query DST flag and set Pin
  if (timeinfo.tm_isdst) { 
    digitalWrite(DSTPin, HIGH);
  } else {
    digitalWrite(DSTPin, LOW);
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LedPin, OUTPUT);
  pinMode(DSTPin, OUTPUT);
  pinMode(DCFPin, OUTPUT);

  // start value of DCF pin
  if (INVERTDCF) digitalWrite(DCFPin, LOW);
  else digitalWrite(DCFPin, HIGH);


  LCD.init();
  LCD.backlight();
  LCD.setCursor(0, 0);
  LCD.print("Connecting to ");
  LCD.setCursor(0, 1);
  LCD.print("WiFi ");

  WiFi.begin(ssid, pass, 6);
  while (WiFi.status() != WL_CONNECTED) {
    delay(250);
    spinner();
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  LCD.clear();
  LCD.setCursor(0, 0);
  LCD.println("Online");
  LCD.setCursor(0, 1);
  LCD.println("Updating time...");

  // DST test
  /*
  Serial.println("Now change the time.  1 min before DST should finish. (1st Sunday of April)");
  setTime(2021,3,28,1,59,50,0);    // Set it to 1 minute before daylight savings comes in.  Note. isDst=1 to indicate that the time we set is in DST.
  int i;
  for(i=0; i<20; i++){
    delay(1000);
    printLocalTime();
  }
  */
  //
  initTime(TIMEZONE);

  //handle DCF pulses
  DcfOutTimer.attach_ms(100, DcfOut);
  // Display updates
  Displaytimer.attach_ms(250, DisplayUpdate);
  
  // DFC Encoder
  initDCFencoder();

  //DCF Decoder 
  DCF.Start();
}

void loop() {
  int n;
  //printLocalTime();
  prepareDCFdatagram(); //every minute
  for (n=0;n<60;n++) {
    delay(1000); //every second
    time_t DCFtime = DCF.getTime(); // Check if new DCF77 time is available
    if (DCFtime!=0) {
      time_t now = time(nullptr);
      int since_last_sync = now - last_sync;
      int time_diff = DCFtime - now;
      digitalClockDisplay(DCFtime);
      Serial.println("---------------------------");
      Serial.print("Time Difference DCF to NTP: ");
      Serial.print(time_diff);
      Serial.println("s");
      Serial.print("Time since last NTP sync: ");
      Serial.print(since_last_sync);
      Serial.println("s");
      Serial.println("---------------------------");

    }
  }
}