/*
  SNOOPY EPAPER DISPLAY (Lilygo T5 4.7 S3)
  
  1. follow the instructions:
  https://github.com/Xinyuan-LilyGO/LilyGo-EPD47/tree/esp32s3
  2. Boot + Reset button to upload sketch via USB
  3. Arduino IDE 2.xx not working. Use 1.xx instead

  VER 0.0 16/11/2023  TEST CODE: not working
  VER 0.1 16/11/2023  ORIGINAL CODE + MQTT: not working 
  VER 0.2 20/11/2023  MQTT TEST: MQTT/NTP/Battery reading OK
  VER 0.3 21/11/2023  IMAGE WEB UPDATE TEST: ok
  VER 0.4 21/11/2023  combine v0.2 + 0.3: ok
  VER 0.5 22/11/2023  deep sleep: working but unstable. wake only once after Reset.
  VER 0.6 05/10/2024  so far only working version
  VER 0.7 05/10/2024  time2sleep from NodeRED via MQTT: not working
  VER 0.8 11/03/2025  MQTT trigger image update on server

  TODO:
  1. MQTT time update (currently not working)

*/

#include "epd_driver.h"
#include "pins.h"
#include <WiFi.h>
#include <HTTPClient.h>
//MQTT
#include <PubSubClient.h>
//for time sync
#include <WiFiUdp.h>
#include <NTPClient.h>

#define WIFI_SSID "####"
#define WIFI_PWD "####"
#define mqtt_server "000.000.000.000"
WiFiClient espClient;  
PubSubClient client(espClient);  

//ntp time sync
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
//unsigned long time2sleep = 30;   //min time for retrial when wifi failure

int targetHour = 06;   // Target hour (24-hour format)
int targetMinute = 0; // Target minute
int targetSecond = 0;  // Target second
int timeZone = 10;    //Brisbane: GMT+10

#define DEBUG true
#define log_debug(text) if(DEBUG) Serial.println(text);
  
// Possible results of image download
enum imageState {
  IMAGE_UNCHANGED,
  IMAGE_CHANGED,
  IMAGE_ERROR
};

String imageUrl = "http://000.000.000.000/images/snoopyDisplay/epd_image_0.pgm";

// 4 bit per pixel image buffer
uint8_t *framebuffer;

uint64_t uS_TO_S_FACTOR = 1000000;  /* Conversion factor for micro seconds to seconds */
uint64_t time2sleep;   

// Sort of "hash" of last displayed screen image
RTC_DATA_ATTR uint16_t imageCrc = 0;

int getImage(String imageUrl) {
  log_debug("GET IMAGE " + imageUrl);
  // wait for WiFi connection
  if ((WiFi.status() != WL_CONNECTED)) {
    log_debug("NO CONNECTION");
    return IMAGE_ERROR;
  }

  HTTPClient http;
  http.begin((const char *)(imageUrl.c_str()));
  int httpCode = http.GET();
  // file found at server
  if (httpCode != HTTP_CODE_OK) {
    log_debug("HTTP status " + String(httpCode));
    return IMAGE_ERROR;
  }
  uint32_t frameoffset = 0;
  int len = http.getSize();
  // create buffer for read
  uint8_t buff[128] = { 0 };

  // get tcp stream
  WiFiClient *stream = http.getStreamPtr();

  // Skip the image header
  uint8_t headerRows = 0;
  do {
    headerRows += (stream->read() == 0x0a);
  } while (headerRows < 2 && stream->available());
  stream->read();
  stream->read();
  uint16_t crc = 0;
  // read all data from server, until the framebuffer is filled
  while (http.connected() && (len > 0 || len == -1) && (frameoffset < EPD_WIDTH * EPD_HEIGHT / 2)) {
    // get available data size
    size_t size = stream->available();
    Serial.print(".");

    if (size) {
      // read up to 128 byte
      int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));

      // Convert 8-bit pixels of the input buffer to 4-bit pixels in the framebuffer
      for (uint8_t p = 0; p < c; p += 2) {
        *(framebuffer + frameoffset) = (buff[p + 1] & 0xf0) | (buff[p] >> 4);
        frameoffset++;
        crc ^= (uint16_t)(*(buff + p));
      }
      if (len > 0) {
        len -= c;
      }
    }
    delay(1);
  }  
  http.end();
  log_debug("IMAGE CRC: " + String(crc));
  if (crc == imageCrc) {
    return IMAGE_UNCHANGED;
  }
  imageCrc = crc;
  return IMAGE_CHANGED;
}

void setup() {
  #if DEBUG
    Serial.begin(115200);
  #endif
  setup_wifi();

  framebuffer = (uint8_t *)ps_calloc(sizeof(uint8_t), EPD_WIDTH * EPD_HEIGHT / 2);
  if (framebuffer) {
    memset(framebuffer, 0xFF, EPD_WIDTH * EPD_HEIGHT / 2);
  }

  int loadStatus = getImage(imageUrl);
  //log_debug("MESSAGE: " + String(message));
  switch (loadStatus) {
    case IMAGE_ERROR:
      //      epd_banner("ERRORE DI CONNESSIONE");
      break;
    case IMAGE_CHANGED:
      // Redraw the whole screen
      log_debug("INITIALIZE EPD");
      epd_init();
      epd_clear();
      //drawBattery(900,520,battery);
      epd_draw_grayscale_image(epd_full_screen(), framebuffer);
      epd_poweroff_all();
      break;
    case IMAGE_UNCHANGED:
        // Redraw only the part of the image covered by the banner
        //      epd_cancel_banner();
      break;
  }
  log_debug("ALL DONE");
  delay(500);
   
  //MQTT + time sync
  client.setServer(mqtt_server, 1883);  
  if (!client.connected()) {  
    reconnect();  
  }  
  //NTP
  timeClient.setTimeOffset(timeZone * 3600);      
  timeClient.begin();     
  //MQTT
  client.loop();
  delay(100);
  client.publish("snoopyDisplay/stat", "ePaperScreenUpdated");
  //delay(100);
  timeSync();
  batteryMQTT();
  //client.publish("snoopyDisplay/stat", "now sleeping");
  //Serial.println("enter deep sleep");  
  Serial.flush();
  delay(100);
  //power off code from manufacturer's github
  epd_poweroff_all();
  //deep sleep
  esp_sleep_enable_timer_wakeup(time2sleep * uS_TO_S_FACTOR);
  esp_deep_sleep_start();
}

void setup_wifi() {
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(WIFI_SSID);
  int _try = 0;
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
    _try++;  // If can't connect to WiFi after 10 tries, deep-sleep
    if (_try >= 10) {
      Serial.println("Cannot connect to WiFi, go into deep sleep");
      //ESP.deepSleep(time2sleep * 1e6);
    }
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop(){
  //do nothing
}

void batteryMQTT() {
  float voltage = analogRead(14);  //BAT ADC IO14     https://github.com/Xinyuan-LilyGO/LilyGo-EPD47/blob/master/schematic/T5-4.7-Plus.pdf
  char msg_out[20];
  dtostrf(voltage, 2, 2, msg_out);  //to convert voltage so that MQTT can send. http://www.steves-internet-guide.com/send-and-receive-integers-and-floats-with-arduino-over-mqtt/
  Serial.print("Battery: ");
  Serial.println(voltage);
  client.publish("snoopyDisplay/battery", msg_out);
  //delay(5000);
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Create a random client ID
    String clientId = "ESP32-";
    clientId += String(random(0xffff), HEX);
    // Attempt to connect
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("snoopyDisplay/stat", "standby");
    } else {
        Serial.print("failed, rc=");
        Serial.print(client.state());
        Serial.println(" try again in 5 seconds");
        // Wait 5 seconds before retrying
        delay(5000);
    }
  }
}

void timeSync() {
  timeClient.update();
  unsigned long currentTime = timeClient.getEpochTime();
  unsigned long targetTime = currentTime - (timeClient.getHours() * 3600 + timeClient.getMinutes() * 60 + timeClient.getSeconds()) + targetHour * 3600 + targetMinute * 60 + targetSecond;
  // If the target time has already passed today, assume it's for tomorrow
  if (targetTime < currentTime) {
    targetTime += 24 * 3600 + 1200; // Add one day (24 hours) in seconds + add 20mins (calibrating)
  }
  time2sleep = targetTime - currentTime;
  //time2sleep = 120;      //FOR TESTING
  Serial.print("Remaining seconds: ");
  Serial.println(time2sleep);
  char msg_out [20];
  dtostrf(time2sleep,2,2,msg_out);     //to convert number so that MQTT can send. http://www.steves-internet-guide.com/send-and-receive-integers-and-floats-with-arduino-over-mqtt/
  client.publish("snoopyDisplay/seconds2wake", msg_out);
}
