#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
 
const char* ssid = "MY_Wi-Fi";
const char* password = "password";
 
AsyncWebServer server(80);
byte reconnect = 0;
// *****************************************************************************
#include <Arduino.h>
#define ESP32_CAN_TX_PIN GPIO_NUM_17 // Set CAN TX port to 17
#define ESP32_CAN_RX_PIN GPIO_NUM_16  // Set CAN RX port to 16
#include <N2kMessages.h>
#include <NMEA2000_CAN.h>
#include <TFT_eSPI.h>       // Библиотека для дисплея
#include <XPT2046_Touchscreen.h>  // Библиотека для тачскрина
#include <Preferences.h>
#include <N2kMessagesEnumToStr.h>

#define ENABLE_DEBUG_LOG 1 // Debug log, set to 1 to enable
#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS    15 
#define TFT_DC    2 
#define TFT_RST   4 

#define TOUCH_CS  21  // T_CS
#define TOUCH_DO  19
#define TOUCH_CLK 18
#define TOUCH_DIN 23

#define BACKLIGHT_PIN 0  // Пин для управления подсветкой
#define PWM_CHANNEL 0     // Канал PWM
#define PWM_FREQ 5000     // Частота ШИМ (Гц)
#define PWM_RESOLUTION 8  // Разрешение 8 бит (0-255)

int LCD_Brightness = 255; // Начальная яркость (0-255)

#define TOUCH_MIN_X  384
#define TOUCH_MAX_X  3695
#define TOUCH_MIN_Y  418
#define TOUCH_MAX_Y  3762

TFT_eSPI tft = TFT_eSPI();
XPT2046_Touchscreen ts(TOUCH_CS);  // Создаем объект для работы с тачскрином
Preferences preferences;    // Nonvolatile storage on ESP32 - To store LastDeviceAddress
//*****************************************************************************
    double WaterTemperature = 0.0;
//    double SystemTime = 0.0;
    double RudderPosition;
//    double AngleOrder = 0.0;
    double EngineSpeed = 0.0;
//    double EngineBoostPressure = 0.0;
//    double EngineOilPress = 0.0;
//    double EngineOilTemp = 0.0;
    double EngineCoolantTemp = 0.0;
    double AltenatorVoltage = 0.0;
    double FuelRate = 0.0;
    double EngineHours = 0.0;
//    double EngineCoolantPress = 0.0;
//    double EngineFuelPress = 0.0; 
//    double OilPressure = 0.0;
//    double OilTemperature = 0.0;
    double TripFuelUsed = 0.0;
//    double FuelRateAverage = 0.0; 
    double FuelRateEconomy = 0.0; 
//    double InstantaneousFuelEconomy = 0.0; 
//    double Heading = 0.0;
//    double Deviation = 0.0;
//    double Variation = 0.0;
    double COG = 0.0;  
    double SOG = 0.0;       // (Speed over the Ground).
//    double SecondsSinceMidnight = 0.0; 
    double Latitude = 0.0;
    double Longitude = 0.0;
//    double Altitude = 0.0; 
//    double HDOP = 0.0;
//    double PDOP = 0.0;
//    double GeoidalSeparation = 0.0;
//    double AgeOfCorrection = 0.0;
    double SOW = 0.0;
//    double BatCapacity = 0.0;
//    double PeukertExponent = 0.0; 
//    double ActualTemperature = 0.0;
//    double SetTemperature = 0.0;
//    double ActualPressure = 0.0;
//    double ActualHumidity,SetHumidity = 0.0;
//    double val = 0.0;
    double OutsideAmbientAirTemperature = 0.0;
    double AtmosphericPressure = 0.0;
//    double TimeRemaining = 0.0;
//    double RippleVoltage = 0.0;
//    double Capacity = 0.0;
    double DepthBelowTransducer = 0.0;
//    double Offset = 0.0;
//    double Yaw = 0.0;
//    double Pitch = 0.0;
//    double Roll = 0.0;
//    double Level=0;
    double minutes;
    double gear ;
    double t = 0;  // Time
//*****************************************************************************

int page = 0;  // Initial page to show
int pages = 5; // количество pages -1 

bool TimeSet = false;
unsigned long MyTime = 0;

void debug_log(char* str) {
#if ENABLE_DEBUG_LOG == 1
  Serial.println(str);
#endif
}

typedef struct {
  unsigned long PGN;
  void (*Handler)(const tN2kMsg &N2kMsg); 
} tNMEA2000Handler;

void SystemTime(const tN2kMsg &N2kMsg);
void Rudder(const tN2kMsg &N2kMsg);
void EngineRapid(const tN2kMsg &N2kMsg);
void EngineDynamicParameters(const tN2kMsg &N2kMsg);
void TransmissionParameters(const tN2kMsg &N2kMsg);
void TripFuelConsumption(const tN2kMsg &N2kMsg);
void Speed(const tN2kMsg &N2kMsg);
void WaterDepth(const tN2kMsg &N2kMsg);
void BinaryStatus(const tN2kMsg &N2kMsg);
void FluidLevel(const tN2kMsg &N2kMsg);
void OutsideEnvironmental(const tN2kMsg &N2kMsg);
void Temperature(const tN2kMsg &N2kMsg);
void TemperatureExt(const tN2kMsg &N2kMsg);
void DCStatus(const tN2kMsg &N2kMsg);
void BatteryConfigurationStatus(const tN2kMsg &N2kMsg);
void COGSOG(const tN2kMsg &N2kMsg);
void GNSS(const tN2kMsg &N2kMsg);
void LocalOffset(const tN2kMsg &N2kMsg);
void Attitude(const tN2kMsg &N2kMsg);
void Heading(const tN2kMsg &N2kMsg);
void Humidity(const tN2kMsg &N2kMsg);
void Pressure(const tN2kMsg &N2kMsg);
void UserDatumSettings(const tN2kMsg &N2kMsg);
void GNSSSatsInView(const tN2kMsg &N2kMsg);

tNMEA2000Handler NMEA2000Handlers[]={
  {126992L,&SystemTime},
  {127245L,&Rudder },
  {127250L,&Heading},
  {127257L,&Attitude},
  {127488L,&EngineRapid},
  {127489L,&EngineDynamicParameters},
  {127493L,&TransmissionParameters},
  {127497L,&TripFuelConsumption},
  {127501L,&BinaryStatus},
  {127505L,&FluidLevel},
  {127506L,&DCStatus},
  {127513L,&BatteryConfigurationStatus},
  {128259L,&Speed},
  {128267L,&WaterDepth},
  {129026L,&COGSOG},
  {129029L,&GNSS},
  {129033L,&LocalOffset},
  {129045L,&UserDatumSettings},
  {129540L,&GNSSSatsInView},
  {130310L,&OutsideEnvironmental},
  {130312L,&Temperature},
  {130313L,&Humidity},
  {130314L,&Pressure},
  {130316L,&TemperatureExt},
  {0,0}
};

Stream *OutputStream;

void HandleNMEA2000Msg(const tN2kMsg &N2kMsg);

void setup() {
// *****************************************************************************
  Serial.begin(115200);
  delay(500);
  // Ожидаем соединения
  if (WiFi.status() != WL_CONNECTED) {       // если не подключены к wifi
    while (reconnect < 10 ) {                 // если количество попыток подключения меньше указанного, то
      connect_wifi();                        // пытаемся подключиться
      reconnect ++;                          // добавляем единицу к количеству попыток
      delay(500);
        Serial.println("");
       Serial.print("reconnect: ");
        Serial.println(reconnect);
       
    }
  }

  
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
 
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/plain", "Hi! I am ESP32.");
  });
 
  AsyncElegantOTA.begin(&server);    // Start ElegantOTA
  server.begin();
  Serial.println("HTTP server started");
// *****************************************************************************  
  tft.begin();
  tft.setRotation(3); // Поворот экрана при необходимости (1)
  tft.fillScreen(TFT_BLACK); // Очистка экрана для проверки работы дисплея
  pinMode(BACKLIGHT_PIN, OUTPUT);
  
  // Настроим аппаратный PWM на ESP32
  ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
  ledcAttachPin(BACKLIGHT_PIN, PWM_CHANNEL);
  
  // Установим начальную яркость
  ledcWrite(PWM_CHANNEL, LCD_Brightness);

  // Инициализация тачскрина
    ts.begin();
    ts.setRotation(0);

    tft.setTextSize(1);
    tft.setTextColor(TFT_YELLOW, TFT_BLACK);
    tft.setCursor(0, 0, 1);
  
  OutputStream=&Serial;
//   while (!Serial) 
   
//  NMEA2000.SetN2kCANReceiveFrameBufSize(50);
  // Do not forward bus messages at all
  NMEA2000.SetForwardType(tNMEA2000::fwdt_Text);
  NMEA2000.SetForwardStream(OutputStream);
  // Set false below, if you do not want to see messages parsed to HEX withing library
  NMEA2000.EnableForward(false);
  NMEA2000.SetMsgHandler(HandleNMEA2000Msg);
//  NMEA2000.SetN2kCANMsgBufSize(2);
  NMEA2000.Open();
  OutputStream->print("Running...");
}

//*****************************************************************************
template<typename T> void PrintLabelValWithConversionCheckUnDef(const char* label, T val, double (*ConvFunc)(double val)=0, bool AddLf=false, int8_t Desim=-1 ) {
  OutputStream->print(label);
  if (!N2kIsNA(val)) {
    if ( Desim<0 ) {
      if (ConvFunc) { OutputStream->print(ConvFunc(val)); } else { OutputStream->print(val); }
    } else {
      if (ConvFunc) { OutputStream->print(ConvFunc(val),Desim); } else { OutputStream->print(val,Desim); }
    }
  } else OutputStream->print("not available");
  if (AddLf) OutputStream->println();
}

//*****************************************************************************
void SystemTime(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    uint16_t SystemDate;
    double SystemTime;
    tN2kTimeSource TimeSource;
    
    if (ParseN2kSystemTime(N2kMsg, SID, SystemDate, SystemTime, TimeSource)) {
        OutputStream->println("System time:");
        PrintLabelValWithConversionCheckUnDef("  SID: ", SID, 0, true);
        PrintLabelValWithConversionCheckUnDef("  days since 1.1.1970: ", SystemDate, 0, true);
        PrintLabelValWithConversionCheckUnDef("  seconds since midnight: ", SystemTime, 0, true);
        OutputStream->print("  time source: "); 
        PrintN2kEnumType(TimeSource, OutputStream);
        
        // Сохраняем время в формате Unix Timestamp (секунды с 1.1.1970)
        MyTime = (SystemDate * 86400) + (unsigned long)SystemTime;
    } else {
        OutputStream->print("Failed to parse PGN: "); 
        OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void Rudder(const tN2kMsg &N2kMsg) {
    unsigned char Instance;
    tN2kRudderDirectionOrder RudderDirectionOrder;
//    double RudderPosition;
    double AngleOrder;
    
    if (ParseN2kRudder(N2kMsg,RudderPosition,Instance,RudderDirectionOrder,AngleOrder) ) {
      PrintLabelValWithConversionCheckUnDef("Rudder: ",Instance,0,true);
      PrintLabelValWithConversionCheckUnDef("  position (deg): ",RudderPosition,&RadToDeg,true);
                        OutputStream->print("  direction order: "); PrintN2kEnumType(RudderDirectionOrder,OutputStream);
      PrintLabelValWithConversionCheckUnDef("  angle order (deg): ",AngleOrder,&RadToDeg,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void EngineRapid(const tN2kMsg &N2kMsg) {
    unsigned char EngineInstance;
//    double EngineSpeed;
    double EngineBoostPressure;
    int8_t EngineTiltTrim;
    
    if (ParseN2kEngineParamRapid(N2kMsg,EngineInstance,EngineSpeed,EngineBoostPressure,EngineTiltTrim) ) {
      PrintLabelValWithConversionCheckUnDef("Engine rapid params: ",EngineInstance,0,true);
      PrintLabelValWithConversionCheckUnDef("  RPM: ",EngineSpeed,0,true);
      PrintLabelValWithConversionCheckUnDef("  boost pressure (Pa): ",EngineBoostPressure,0,true);
      PrintLabelValWithConversionCheckUnDef("  tilt trim: ",EngineTiltTrim,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void EngineDynamicParameters(const tN2kMsg &N2kMsg) {
    unsigned char EngineInstance;
    double EngineOilPress;
    double EngineOilTemp;
//    double EngineCoolantTemp;
//    double AltenatorVoltage;
//    double FuelRate;
//    double EngineHours;
    double EngineCoolantPress;
    double EngineFuelPress; 
    int8_t EngineLoad;
    int8_t EngineTorque;
    tN2kEngineDiscreteStatus1 Status1;
    tN2kEngineDiscreteStatus2 Status2;
    
    if (ParseN2kEngineDynamicParam(N2kMsg,EngineInstance,EngineOilPress,EngineOilTemp,EngineCoolantTemp,
                                 AltenatorVoltage,FuelRate,EngineHours,
                                 EngineCoolantPress,EngineFuelPress,
                                 EngineLoad,EngineTorque,Status1,Status2) ) {
      PrintLabelValWithConversionCheckUnDef("Engine dynamic params: ",EngineInstance,0,true);
      PrintLabelValWithConversionCheckUnDef("  oil pressure (Pa): ",EngineOilPress,0,true);
      PrintLabelValWithConversionCheckUnDef("  oil temp (C): ",EngineOilTemp,&KelvinToC,true);
      PrintLabelValWithConversionCheckUnDef("  coolant temp (C): ",EngineCoolantTemp,&KelvinToC,true);
      PrintLabelValWithConversionCheckUnDef("  altenator voltage (V): ",AltenatorVoltage,0,true);
      PrintLabelValWithConversionCheckUnDef("  fuel rate (l/h): ",FuelRate,0,true);
      PrintLabelValWithConversionCheckUnDef("  engine hours (h): ",EngineHours,&SecondsToh,true);
      PrintLabelValWithConversionCheckUnDef("  coolant pressure (Pa): ",EngineCoolantPress,0,true);
      PrintLabelValWithConversionCheckUnDef("  fuel pressure (Pa): ",EngineFuelPress,0,true);
      PrintLabelValWithConversionCheckUnDef("  engine load (%): ",EngineLoad,0,true);
      PrintLabelValWithConversionCheckUnDef("  engine torque (%): ",EngineTorque,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void TransmissionParameters(const tN2kMsg &N2kMsg) {
    unsigned char EngineInstance;
    tN2kTransmissionGear TransmissionGear;
    double OilPressure;
    double OilTemperature;
    unsigned char DiscreteStatus1;
    
    if (ParseN2kTransmissionParameters(N2kMsg,EngineInstance, TransmissionGear, OilPressure, OilTemperature, DiscreteStatus1) ) {
      PrintLabelValWithConversionCheckUnDef("Transmission params: ",EngineInstance,0,true);
                        OutputStream->print("  gear: "); PrintN2kEnumType(TransmissionGear,OutputStream);
      PrintLabelValWithConversionCheckUnDef("  oil pressure (Pa): ",OilPressure,0,true);
      PrintLabelValWithConversionCheckUnDef("  oil temperature (C): ",OilTemperature,&KelvinToC,true);
      PrintLabelValWithConversionCheckUnDef("  discrete status: ",DiscreteStatus1,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void TripFuelConsumption(const tN2kMsg &N2kMsg) {
    unsigned char EngineInstance;
    double TripFuelUsed;
    double FuelRateAverage; 
    double FuelRateEconomy; 
    double InstantaneousFuelEconomy; 
    
    if (ParseN2kEngineTripParameters(N2kMsg,EngineInstance, TripFuelUsed, FuelRateAverage, FuelRateEconomy, InstantaneousFuelEconomy) ) {
      PrintLabelValWithConversionCheckUnDef("Trip fuel consumption: ",EngineInstance,0,true);
      PrintLabelValWithConversionCheckUnDef("  fuel used (l): ",TripFuelUsed,0,true);
      PrintLabelValWithConversionCheckUnDef("  average fuel rate (l/h): ",FuelRateAverage,0,true);
      PrintLabelValWithConversionCheckUnDef("  economy fuel rate (l/h): ",FuelRateEconomy,0,true);
      PrintLabelValWithConversionCheckUnDef("  instantaneous fuel economy (l/h): ",InstantaneousFuelEconomy,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void Heading(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    tN2kHeadingReference HeadingReference;
    double Heading;
    double Deviation;
    double Variation;
    
    if (ParseN2kHeading(N2kMsg,SID,Heading,Deviation,Variation,HeadingReference) ) {
                      OutputStream->println("Heading:");
      PrintLabelValWithConversionCheckUnDef("  SID: ",SID,0,true);
                        OutputStream->print("  reference: "); PrintN2kEnumType(HeadingReference,OutputStream);
      PrintLabelValWithConversionCheckUnDef("  Heading (deg): ",Heading,&RadToDeg,true);
      PrintLabelValWithConversionCheckUnDef("  Deviation (deg): ",Deviation,&RadToDeg,true);
      PrintLabelValWithConversionCheckUnDef("  Variation (deg): ",Variation,&RadToDeg,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void COGSOG(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    tN2kHeadingReference HeadingReference;
    double COG;
    double SOG;
    
    if (ParseN2kCOGSOGRapid(N2kMsg,SID,HeadingReference,COG,SOG) ) {
                      OutputStream->println("COG/SOG:");
      PrintLabelValWithConversionCheckUnDef("  SID: ",SID,0,true);
                        OutputStream->print("  reference: "); PrintN2kEnumType(HeadingReference,OutputStream);
      PrintLabelValWithConversionCheckUnDef("  COG (deg): ",COG,&RadToDeg,true);
      PrintLabelValWithConversionCheckUnDef("  SOG (m/s): ",SOG,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void GNSS(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    uint16_t DaysSince1970;
    double SecondsSinceMidnight; 
//    double Latitude;
//    double Longitude;
    double Altitude; 
    tN2kGNSStype GNSStype;
    tN2kGNSSmethod GNSSmethod;
    unsigned char nSatellites;
    double HDOP;
    double PDOP;
    double GeoidalSeparation;
    unsigned char nReferenceStations;
    tN2kGNSStype ReferenceStationType;
    uint16_t ReferenceSationID;
    double AgeOfCorrection;

    if (ParseN2kGNSS(N2kMsg,SID,DaysSince1970,SecondsSinceMidnight,
                  Latitude,Longitude,Altitude,
                  GNSStype,GNSSmethod,
                  nSatellites,HDOP,PDOP,GeoidalSeparation,
                  nReferenceStations,ReferenceStationType,ReferenceSationID,
                  AgeOfCorrection) ) {
                      OutputStream->println("GNSS info:");
      PrintLabelValWithConversionCheckUnDef("  SID: ",SID,0,true);
      PrintLabelValWithConversionCheckUnDef("  days since 1.1.1970: ",DaysSince1970,0,true);
      PrintLabelValWithConversionCheckUnDef("  seconds since midnight: ",SecondsSinceMidnight,0,true);
      PrintLabelValWithConversionCheckUnDef("  latitude: ",Latitude,0,true,9);
      PrintLabelValWithConversionCheckUnDef("  longitude: ",Longitude,0,true,9);
      PrintLabelValWithConversionCheckUnDef("  altitude: (m): ",Altitude,0,true);
                        OutputStream->print("  GNSS type: "); PrintN2kEnumType(GNSStype,OutputStream);
                        OutputStream->print("  GNSS method: "); PrintN2kEnumType(GNSSmethod,OutputStream);
      PrintLabelValWithConversionCheckUnDef("  satellite count: ",nSatellites,0,true);
      PrintLabelValWithConversionCheckUnDef("  HDOP: ",HDOP,0,true);
      PrintLabelValWithConversionCheckUnDef("  PDOP: ",PDOP,0,true);
      PrintLabelValWithConversionCheckUnDef("  geoidal separation: ",GeoidalSeparation,0,true);
      PrintLabelValWithConversionCheckUnDef("  reference stations: ",nReferenceStations,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void UserDatumSettings(const tN2kMsg &N2kMsg) {
  if (N2kMsg.PGN!=129045L) return;
  int Index=0;
  double val;

  OutputStream->println("User Datum Settings: ");
  val=N2kMsg.Get4ByteDouble(1e-2,Index);
  PrintLabelValWithConversionCheckUnDef("  delta x (m): ",val,0,true);
  val=N2kMsg.Get4ByteDouble(1e-2,Index);
  PrintLabelValWithConversionCheckUnDef("  delta y (m): ",val,0,true);
  val=N2kMsg.Get4ByteDouble(1e-2,Index);
  PrintLabelValWithConversionCheckUnDef("  delta z (m): ",val,0,true);
  val=N2kMsg.GetFloat(Index);
  PrintLabelValWithConversionCheckUnDef("  rotation in x (deg): ",val,&RadToDeg,true,5);
  val=N2kMsg.GetFloat(Index);
  PrintLabelValWithConversionCheckUnDef("  rotation in y (deg): ",val,&RadToDeg,true,5);
  val=N2kMsg.GetFloat(Index);
  PrintLabelValWithConversionCheckUnDef("  rotation in z (deg): ",val,&RadToDeg,true,5);
  val=N2kMsg.GetFloat(Index);
  PrintLabelValWithConversionCheckUnDef("  scale: ",val,0,true,3);
}

//*****************************************************************************
void GNSSSatsInView(const tN2kMsg &N2kMsg) {
  unsigned char SID;
  tN2kRangeResidualMode Mode;
  uint8_t NumberOfSVs;
  tSatelliteInfo SatelliteInfo;

  if (ParseN2kPGNSatellitesInView(N2kMsg,SID,Mode,NumberOfSVs) ) {
    OutputStream->println("Satellites in view: ");
                      OutputStream->print("  mode: "); OutputStream->println(Mode);
                      OutputStream->print("  number of satellites: ");  OutputStream->println(NumberOfSVs);
    for ( uint8_t i=0; i<NumberOfSVs && ParseN2kPGNSatellitesInView(N2kMsg,i,SatelliteInfo); i++) {
                        OutputStream->print("  Satellite PRN: ");  OutputStream->println(SatelliteInfo.PRN);
      PrintLabelValWithConversionCheckUnDef("    elevation: ",SatelliteInfo.Elevation,&RadToDeg,true,1);
      PrintLabelValWithConversionCheckUnDef("    azimuth:   ",SatelliteInfo.Azimuth,&RadToDeg,true,1);
      PrintLabelValWithConversionCheckUnDef("    SNR:       ",SatelliteInfo.SNR,0,true,1);
      PrintLabelValWithConversionCheckUnDef("    residuals: ",SatelliteInfo.RangeResiduals,0,true,1);
                        OutputStream->print("    status: "); OutputStream->println(SatelliteInfo.UsageStatus);
    }
  }
}

//*****************************************************************************
void LocalOffset(const tN2kMsg &N2kMsg) {
    uint16_t SystemDate;
    double SystemTime;
    int16_t Offset;
    
    if (ParseN2kLocalOffset(N2kMsg,SystemDate,SystemTime,Offset) ) {
                      OutputStream->println("Date,time and local offset: ");
      PrintLabelValWithConversionCheckUnDef("  days since 1.1.1970: ",SystemDate,0,true);
      PrintLabelValWithConversionCheckUnDef("  seconds since midnight: ",SystemTime,0,true);
      PrintLabelValWithConversionCheckUnDef("  local offset (min): ",Offset,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void OutsideEnvironmental(const tN2kMsg &N2kMsg) {
    unsigned char SID;
//    double WaterTemperature;
//    double OutsideAmbientAirTemperature;
//    double AtmosphericPressure;
    
    if (ParseN2kOutsideEnvironmentalParameters(N2kMsg,SID,WaterTemperature,OutsideAmbientAirTemperature,AtmosphericPressure) ) {
      PrintLabelValWithConversionCheckUnDef("Water temp: ",WaterTemperature,&KelvinToC);
      PrintLabelValWithConversionCheckUnDef(", outside ambient temp: ",OutsideAmbientAirTemperature,&KelvinToC);
      PrintLabelValWithConversionCheckUnDef(", pressure: ",AtmosphericPressure,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: ");  OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void Temperature(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    unsigned char TempInstance;
    tN2kTempSource TempSource;
    double ActualTemperature;
    double SetTemperature;
    
    if (ParseN2kTemperature(N2kMsg,SID,TempInstance,TempSource,ActualTemperature,SetTemperature) ) {
                        OutputStream->print("Temperature source: "); PrintN2kEnumType(TempSource,OutputStream,false);
      PrintLabelValWithConversionCheckUnDef(", actual temperature: ",ActualTemperature,&KelvinToC);
      PrintLabelValWithConversionCheckUnDef(", set temperature: ",SetTemperature,&KelvinToC,true);
    } else {
      OutputStream->print("Failed to parse PGN: ");  OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void Humidity(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    unsigned char Instance;
    tN2kHumiditySource HumiditySource;
    double ActualHumidity,SetHumidity;
    
    if ( ParseN2kHumidity(N2kMsg,SID,Instance,HumiditySource,ActualHumidity,SetHumidity) ) {
                        OutputStream->print("Humidity source: "); PrintN2kEnumType(HumiditySource,OutputStream,false);
      PrintLabelValWithConversionCheckUnDef(", humidity: ",ActualHumidity,0,false);
      PrintLabelValWithConversionCheckUnDef(", set humidity: ",SetHumidity,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: ");  OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void Pressure(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    unsigned char Instance;
    tN2kPressureSource PressureSource;
    double ActualPressure;
    
    if ( ParseN2kPressure(N2kMsg,SID,Instance,PressureSource,ActualPressure) ) {
                        OutputStream->print("Pressure source: "); PrintN2kEnumType(PressureSource,OutputStream,false);
      PrintLabelValWithConversionCheckUnDef(", pressure: ",ActualPressure,&PascalTomBar,true);
    } else {
      OutputStream->print("Failed to parse PGN: ");  OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void TemperatureExt(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    unsigned char TempInstance;
    tN2kTempSource TempSource;
    double ActualTemperature;
    double SetTemperature;
    
    if (ParseN2kTemperatureExt(N2kMsg,SID,TempInstance,TempSource,ActualTemperature,SetTemperature) ) {
                        OutputStream->print("Temperature source: "); PrintN2kEnumType(TempSource,OutputStream,false);
      PrintLabelValWithConversionCheckUnDef(", actual temperature: ",ActualTemperature,&KelvinToC);
      PrintLabelValWithConversionCheckUnDef(", set temperature: ",SetTemperature,&KelvinToC,true);
    } else {
      OutputStream->print("Failed to parse PGN: ");  OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void BatteryConfigurationStatus(const tN2kMsg &N2kMsg) {
    unsigned char BatInstance;
    tN2kBatType BatType;
    tN2kBatEqSupport SupportsEqual;
    tN2kBatNomVolt BatNominalVoltage;
    tN2kBatChem BatChemistry;
    double BatCapacity;
    int8_t BatTemperatureCoefficient;
    double PeukertExponent; 
    int8_t ChargeEfficiencyFactor;

    if (ParseN2kBatConf(N2kMsg,BatInstance,BatType,SupportsEqual,BatNominalVoltage,BatChemistry,BatCapacity,BatTemperatureCoefficient,PeukertExponent,ChargeEfficiencyFactor) ) {
      PrintLabelValWithConversionCheckUnDef("Battery instance: ",BatInstance,0,true);
                        OutputStream->print("  - type: "); PrintN2kEnumType(BatType,OutputStream);
                        OutputStream->print("  - support equal.: "); PrintN2kEnumType(SupportsEqual,OutputStream);
                        OutputStream->print("  - nominal voltage: "); PrintN2kEnumType(BatNominalVoltage,OutputStream);
                        OutputStream->print("  - chemistry: "); PrintN2kEnumType(BatChemistry,OutputStream);
      PrintLabelValWithConversionCheckUnDef("  - capacity (Ah): ",BatCapacity,&CoulombToAh,true);
      PrintLabelValWithConversionCheckUnDef("  - temperature coefficient (%): ",BatTemperatureCoefficient,0,true);
      PrintLabelValWithConversionCheckUnDef("  - peukert exponent: ",PeukertExponent,0,true);
      PrintLabelValWithConversionCheckUnDef("  - charge efficiency factor (%): ",ChargeEfficiencyFactor,0,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void DCStatus(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    unsigned char DCInstance;
    tN2kDCType DCType;
    unsigned char StateOfCharge;
    unsigned char StateOfHealth;
    double TimeRemaining;
    double RippleVoltage;
    double Capacity;

    if (ParseN2kDCStatus(N2kMsg,SID,DCInstance,DCType,StateOfCharge,StateOfHealth,TimeRemaining,RippleVoltage,Capacity) ) {
      OutputStream->print("DC instance: ");
      OutputStream->println(DCInstance);
      OutputStream->print("  - type: "); PrintN2kEnumType(DCType,OutputStream);
      OutputStream->print("  - state of charge (%): "); OutputStream->println(StateOfCharge);
      OutputStream->print("  - state of health (%): "); OutputStream->println(StateOfHealth);
      OutputStream->print("  - time remaining (h): "); OutputStream->println(TimeRemaining/60);
      OutputStream->print("  - ripple voltage: "); OutputStream->println(RippleVoltage);
      OutputStream->print("  - capacity: "); OutputStream->println(Capacity);
    } else {
      OutputStream->print("Failed to parse PGN: ");  OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
void Speed(const tN2kMsg &N2kMsg) {
    unsigned char SID;
//    double SOW;
//    double SOG;
    tN2kSpeedWaterReferenceType SWRT;

    if (ParseN2kBoatSpeed(N2kMsg,SID,SOW,SOG,SWRT) ) {
      OutputStream->print("Boat speed:");
      PrintLabelValWithConversionCheckUnDef(" SOW:",N2kIsNA(SOW)?SOW:msToKnots(SOW));
      PrintLabelValWithConversionCheckUnDef(", SOG:",N2kIsNA(SOG)?SOG:msToKnots(SOG));
      OutputStream->print(", ");
      PrintN2kEnumType(SWRT,OutputStream,true);
    }
}

//*****************************************************************************
void WaterDepth(const tN2kMsg &N2kMsg) {
    unsigned char SID;
//    double DepthBelowTransducer;
    double Offset;

    if (ParseN2kWaterDepth(N2kMsg,SID,DepthBelowTransducer,Offset) ) {
      if ( N2kIsNA(Offset) || Offset == 0 ) {
        PrintLabelValWithConversionCheckUnDef("Depth below transducer",DepthBelowTransducer);
        if ( N2kIsNA(Offset) ) {
          OutputStream->println(", offset not available");
        } else {
          OutputStream->println(", offset=0");
        }
      } else {
        if (Offset>0) {
          OutputStream->print("Water depth:");
        } else {
          OutputStream->print("Depth below keel:");
        }
        if ( !N2kIsNA(DepthBelowTransducer) ) { 
          OutputStream->println(DepthBelowTransducer+Offset); 
        } else {  OutputStream->println(" not available"); }
      }
    }
}

//*****************************************************************************
void printLLNumber(Stream *OutputStream, unsigned long long n, uint8_t base=10)
{
  unsigned char buf[16 * sizeof(long)]; // Assumes 8-bit chars.
  unsigned long long i = 0;

  if (n == 0) {
    OutputStream->print('0');
    return;
  }

  while (n > 0) {
    buf[i++] = n % base;
    n /= base;
  }

  for (; i > 0; i--)
    OutputStream->print((char) (buf[i - 1] < 10 ?
      '0' + buf[i - 1] :
      'A' + buf[i - 1] - 10));
}

//*****************************************************************************
void BinaryStatusFull(const tN2kMsg &N2kMsg) {
    unsigned char BankInstance;
    tN2kBinaryStatus BankStatus;

    if (ParseN2kBinaryStatus(N2kMsg,BankInstance,BankStatus) ) {
      OutputStream->print("Binary status for bank "); OutputStream->print(BankInstance); OutputStream->println(":");
      OutputStream->print("  "); //printLLNumber(OutputStream,BankStatus,16);
      for (uint8_t i=1; i<=28; i++) {
        if (i>1) OutputStream->print(",");
        PrintN2kEnumType(N2kGetStatusOnBinaryStatus(BankStatus,i),OutputStream,false);
      }
      OutputStream->println();
    }
}

//*****************************************************************************
void BinaryStatus(const tN2kMsg &N2kMsg) {
    unsigned char BankInstance;
    tN2kOnOff Status1,Status2,Status3,Status4;

    if (ParseN2kBinaryStatus(N2kMsg,BankInstance,Status1,Status2,Status3,Status4) ) {
      if (BankInstance>2) { // note that this is only for testing different methods. MessageSender.ini sends 4 status for instace 2
        BinaryStatusFull(N2kMsg);
      } else {
        OutputStream->print("Binary status for bank "); OutputStream->print(BankInstance); OutputStream->println(":");
        OutputStream->print("  Status1=");PrintN2kEnumType(Status1,OutputStream,false);
        OutputStream->print(", Status2=");PrintN2kEnumType(Status2,OutputStream,false);
        OutputStream->print(", Status3=");PrintN2kEnumType(Status3,OutputStream,false);
        OutputStream->print(", Status4=");PrintN2kEnumType(Status4,OutputStream,false);
        OutputStream->println();
      }
    }
}

//*****************************************************************************
void FluidLevel(const tN2kMsg &N2kMsg) {
    unsigned char Instance;
    tN2kFluidType FluidType;
    double Level=0;
    double Capacity=0;

    if (ParseN2kFluidLevel(N2kMsg,Instance,FluidType,Level,Capacity) ) {
      switch (FluidType) {
        case N2kft_Fuel:
          OutputStream->print("Fuel level :");
          break;
        case N2kft_Water:
          OutputStream->print("Water level :");
          break;
        case N2kft_GrayWater:
          OutputStream->print("Gray water level :");
          break;
        case N2kft_LiveWell:
          OutputStream->print("Live well level :");
          break;
        case N2kft_Oil:
          OutputStream->print("Oil level :");
          break;
        case N2kft_BlackWater:
          OutputStream->print("Black water level :");
          break;
        case N2kft_FuelGasoline:
          OutputStream->print("Gasoline level :");
          break;
        case N2kft_Error:
          OutputStream->print("Error level :");
          break;
        case N2kft_Unavailable:
          OutputStream->print("Unknown level :");
          break;
      }
      OutputStream->print(Level); OutputStream->print("%"); 
      OutputStream->print(" ("); OutputStream->print(Capacity*Level/100); OutputStream->print("l)");
      OutputStream->print(" capacity :"); OutputStream->println(Capacity);
    }
}

//*****************************************************************************
void Attitude(const tN2kMsg &N2kMsg) {
    unsigned char SID;
    double Yaw;
    double Pitch;
    double Roll;
    
    if (ParseN2kAttitude(N2kMsg,SID,Yaw,Pitch,Roll) ) {
                      OutputStream->println("Attitude:");
      PrintLabelValWithConversionCheckUnDef("  SID: ",SID,0,true);
      PrintLabelValWithConversionCheckUnDef("  Yaw (deg): ",Yaw,&RadToDeg,true);
      PrintLabelValWithConversionCheckUnDef("  Pitch (deg): ",Pitch,&RadToDeg,true);
      PrintLabelValWithConversionCheckUnDef("  Roll (deg): ",Roll,&RadToDeg,true);
    } else {
      OutputStream->print("Failed to parse PGN: "); OutputStream->println(N2kMsg.PGN);
    }
}

//*****************************************************************************
//NMEA 2000 message handler
void HandleNMEA2000Msg(const tN2kMsg &N2kMsg) {
  int iHandler;
  
  // Find handler
  OutputStream->print("In Main Handler: "); OutputStream->println(N2kMsg.PGN);
  for (iHandler=0; NMEA2000Handlers[iHandler].PGN!=0 && !(N2kMsg.PGN==NMEA2000Handlers[iHandler].PGN); iHandler++);
  
  if (NMEA2000Handlers[iHandler].PGN!=0) {
    NMEA2000Handlers[iHandler].Handler(N2kMsg); 
  }
}
//*****************************************************************************
void loop() 
{ 
 AsyncElegantOTA.loop();

  if (ts.touched()) {
    TS_Point p = ts.getPoint();
    int touch_x = map(p.x, TOUCH_MIN_X, TOUCH_MAX_X, 0, 320);
    int touch_y = map(p.y, TOUCH_MIN_Y, TOUCH_MAX_Y, 0, 240);
    Serial.println(touch_y);  

    // Определяем, в какую зону нажали
    if (touch_x < 106) {  // Левая зона
      delay(200);
      page--;
      if (page < 0) page = pages;
      t -= 1000;
    } else if (touch_x > 213) {  // Правая зона
      delay(200);
      page++;
      if (page > pages) page = 0;
      t -= 1000;
    } else {  // Средняя зона (яркость)
      delay(200);
      LCD_Brightness += 10;
      if (LCD_Brightness > 255) LCD_Brightness = 10;
      ledcWrite(PWM_CHANNEL, LCD_Brightness); // Применяем новую яркость
    }
  }

  if (millis() > t + 1000) {
    t = millis();
    
    if (page == 0) Page_1();
    if (page == 1) Page_2();
    if (page == 2) Page_3();
    if (page == 3) Page_4();
    if (page == 4) Page_5();
    if (page == 5) Page_6();

    set_time();
    DisplayDateTime();
    NMEA2000.ParseMessages();
}
}
//*****************************************************************************
void Page_6(void) {
  char buffer[40];

  tft.fillRect(0, 31, 320, 178, 0x439);
  tft.setCursor(0, 50, 2);
  tft.setTextSize(4);
  sprintf(buffer, " RPM %3.0f ", EngineSpeed);
  tft.println(buffer);
  sprintf(buffer, " T %3.1f ",EngineCoolantTemp - 273);
  tft.print(buffer);
  tft.setTextSize(1);
  tft.print(" o ");
  tft.setTextSize(4);
  tft.println("C  ");
}
//*****************************************************************************
void Page_5(void) {
  char buffer[40];

  tft.fillRect(0, 31, 320, 178, 0x439);
  tft.setCursor(0, 45, 2);
  tft.setTextSize(3);
  sprintf(buffer, " Water %2.1f",WaterTemperature - 273);
  tft.print(buffer);
  tft.setTextSize(1);
  tft.print(" o ");
  tft.setTextSize(3);
  tft.println("C  ");
  sprintf(buffer, " Air  %2.1f",OutsideAmbientAirTemperature - 273);
  tft.print(buffer);
  tft.setTextSize(1);
  tft.print(" o ");
  tft.setTextSize(3);
  tft.println("C  ");
  sprintf(buffer, "Press %3.1f kPa",AtmosphericPressure /1000);
  tft.println(buffer);
}
//*****************************************************************************
void Page_4(void) {
  char buffer[40];

  const double radToDeg = 180.0 / M_PI;
  tft.fillRect(0, 31, 320, 178, 0x439);
  tft.setCursor(0, 45, 2);
  tft.setTextSize(3);
  sprintf(buffer, " STW %2.1f km/h ", SOG * 3600.0/1852.0);  // (сокращенно STW от Speed Through the Water). 
  tft.print(buffer);
  sprintf(buffer, " Depth %4.1f m  ",DepthBelowTransducer);
  tft.print(buffer);
  sprintf(buffer, " Rudder %2.1f",RudderPosition * radToDeg);
  tft.print(buffer);
  tft.setTextSize(1);
  tft.print(" o ");
  tft.setTextSize(3);
}
//*****************************************************************************
void Page_3(void) {
  char buffer[40];

  tft.fillRect(0, 31, 320, 178, 0x439);
  tft.setCursor(0, 45, 2);
  tft.setTextSize(3);
  sprintf(buffer, "Rashod %2.1f l/h ",FuelRate); 
  tft.print(buffer);
  tft.setTextSize(3);
  sprintf(buffer, "Hours %5.2f ",EngineHours /3600);
  tft.print(buffer);
  tft.setTextSize(3);
  sprintf(buffer, "Altenator %2.1f V" , AltenatorVoltage );
  tft.println(buffer);
}
//*****************************************************************************
void Page_2(void) {
  char buffer[40];

  tft.fillRect(0, 31, 320, 178, 0x439);
  tft.setCursor(0, 40, 2);
  tft.setTextSize(2);

  minutes = Latitude - trunc(Latitude);
  minutes = minutes / 100 * 60;

  tft.println("LAT");
  tft.setTextSize(3);
  sprintf(buffer, "   %02.0f", trunc(Latitude));
  tft.print(buffer);
  tft.setTextSize(1);
  tft.print("o ");
  tft.setTextSize(3);
  sprintf(buffer, "%06.3f", minutes * 100);
  tft.print(buffer);
  if (Latitude > 0 ) tft.println("'N"); else tft.println("'S");

  minutes = Longitude - trunc(Longitude);
  minutes = minutes / 100 * 60;

  tft.setTextSize(2);
  tft.println("LON");
  tft.setTextSize(3);
  sprintf(buffer, "  %03.0f", trunc(Longitude));
  tft.print(buffer);
  tft.setTextSize(1);
  tft.print("o ");
  tft.setTextSize(3);
  sprintf(buffer, "%06.3f", minutes * 100);
  tft.print(buffer);
  if (Longitude > 0 ) tft.println("'E"); else tft.println("'W");
}
//*****************************************************************************
void Page_1(void) {
  char buffer[40];

  tft.fillRect(0, 31, 320, 178, 0x439);
  tft.setCursor(0, 70, 2);
  tft.setTextSize(3);
  tft.print("RPM ");
  tft.setTextSize(7);
  sprintf(buffer, "%3.0f", EngineSpeed);
  tft.println(buffer);
}
//*****************************************************************************
void DisplayDateTime(void) {
  char buffer[50];

  tft.setTextFont(1);
  tft.fillRect(0, 0, 320, 30, 0x1E9F);                 // tft.fillRect(0, 0, 320, 30, 0x1E9F);     /* Upper dark blue area */
//  tft.fillRect(0, 31, 320, 178, 0x439);                   /* Main light blue area */
  tft.fillRect(0, 210, 320, 30, 0x1E9F);                    /* Lower dark blue area */
  tft.fillRect(0, 30, 320, 4, 0xffff);                     /* Upper white line */
  tft.fillRect(0, 208, 320, 4, 0xffff);                    /* Lower white line */
  tft.setTextSize(2);                                      /* Set text size to 2 */
  tft.setTextColor(TFT_WHITE);                                 /* Set text color to white */
  tft.setCursor(10, 218);                                    /* Display header info */
  tft.print("BACK   BRIGHTNESS    NEXT");

  tft.fillRect(0, 0, 320, 30, 0x1E9F);  // tft.fillRect(0, 210, 320, 30, 0x1E9F);
  tft.setCursor(10, 7);

  if (MyTime != 0) {

    time_t rawtime = MyTime; // Create time from NMEA 2000 time (UTC)
    struct tm  ts;
    ts = *localtime(&rawtime);

    strftime(buffer, sizeof(buffer), "%d.%m.%Y - %H:%M:%S UTC", &ts); // Create directory name from date
    tft.print(buffer);
  }

}
//*****************************************************************************
void connect_wifi () {
 if (WiFi.status() != WL_CONNECTED) { 
  //WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");
 }
}
//*****************************************************************************
void set_time(void) {
    if (MyTime != 0 && !TimeSet) { // Устанавливаем время, если оно валидное и еще не было установлено
        struct timeval tv;
        tv.tv_sec = MyTime;
        tv.tv_usec = 0;
        settimeofday(&tv, NULL);
        TimeSet = true;
    }
}