/**
 * @file espmega_iot_core.cpp
 * @brief Implementation of the ESPMega IoT Core library.
 *
 * This library provides the core functionality for the ESPMega IoT platform, including network connectivity,
 * input/output control, and MQTT communication. It also includes support for an internal LCD display and
 * air conditioner control.
 *
 * @author Siwat Sirichai
 * @date 15 Nov 2023
 */

#include <espmega_iot_core.hpp>

// OS Configuration
// #define FASTBOOT

#ifndef ESPMEGA_REV
#define ESPMEGA_REV "ESPMega PRO R3.3b"
#endif

// Network Connectivity
char HOSTNAME[15];
IPAddress IP(0, 0, 0, 0);
IPAddress SUBNET(0, 0, 0, 0);
IPAddress GATEWAY(0, 0, 0, 0);
IPAddress DNS(0, 0, 0, 0);
IPAddress MQTT_SERVER(0, 0, 0, 0);
uint16_t MQTT_PORT = 0;
#ifdef ENABLE_WEBUI
WebServer otaserver(80);
#endif
bool standalone = true;
char MQTT_BASE_TOPIC[20];
char AVAILABILITY_TOPIC[40];
uint8_t base_topic_length = 0;
char STATE_REQUEST_TOPIC[40];
bool MQTT_USE_AUTH = false;
char MQTT_USERNAME[32];
char MQTT_PASSWORD[32];
#ifdef ENABLE_WEBUI
char WEBUI_USERNAME[32];
char WEBUI_PASSWORD[32];
#endif
uint8_t utc_offset = 7;
#ifdef ENABLE_CLIMATE_MODULE
float current_room_temp = 0;
float current_room_humid = 0;
#endif

// Inputs
#define VINT_COUNT 16
const int DEBOUNCE_TIME_MS = 50;
const int virtual_interrupt_pins[VINT_COUNT] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
int virtual_interupt_state[VINT_COUNT];
unsigned long virtual_interupt_timer[VINT_COUNT];

// Outputs
#define PWM_COUNT 16
const uint8_t pwm_pins[PWM_COUNT] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
bool pwm_states[PWM_COUNT];
uint8_t pwm_states_fram[PWM_COUNT];
uint16_t pwm_values[PWM_COUNT];
uint8_t pwm_values_fram[PWM_COUNT * 2];
// output = m*input+c
const float pwm_linear_scaling_m[PWM_COUNT] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
const float pwm_linear_scaling_c[PWM_COUNT] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
#define PWM_CYCLE_VALUES_COUNT 3
const int PWM_CYCLE_VALUES[PWM_CYCLE_VALUES_COUNT] = {50, 125, 255};
char PWM_SET_STATE_TOPIC[70];
char PWM_SET_VALUE_TOPIC[70];

#ifdef ENABLE_INTERNAL_LCD
// LCD
int lcd_current_page = 1;
int lcd_pwmAdj_id = 0;
EasyNex panel(Serial);
#endif

// Air Conditioner Control
/*
Mode 0: Off, 1: Cool, 2: Fan
Fan Speed 0: Auto, 1: High, 2: Mid, 3: Low
*/
#ifdef ENABLE_CLIMATE_MODULE
uint8_t ac_mode = 0;
uint8_t ac_fan_speed = 0;
uint8_t ac_temperature = 25;
char AC_SET_MODE_TOPIC[75];
char AC_SET_FAN_TOPIC[75];
char AC_SET_TEMPERATURE_TOPIC[75];
char AC_MODE_TOPIC[75];
char AC_FAN_TOPIC[75];
char AC_TEMPERATURE_TOPIC[75];
char AC_ROOM_TEMPERATURE_TOPIC[75];
char AC_HUMIDITY_TOPIC[75];
#endif

#ifdef ENABLE_IR_MODULE
uint16_t ir_buffer[IR_RAW_BUFFER_LENGTH];
uint16_t ir_buffer_length = 0;
#endif

#ifdef ENABLE_ANALOG_MODULE
#define DAC_COUNT 4
#define ADC_COUNT 8
bool dac_states[DAC_COUNT];
uint16_t dac_values[DAC_COUNT];
uint16_t adc_values[ADC_COUNT];
bool adc_report_enable[ADC_COUNT];

char ADC_COMMAND_TOPIC[75];
char ADC_STATE_TOPIC[75];
char ADC_REPORT_TOPIC[75];
char DAC_SET_STATE_TOPIC[75];
char DAC_SET_VALUE_TOPIC[75];
char DAC_STATE_TOPIC[75];
char DAC_VALUE_TOPIC[75];
#endif

// FRAM ADDRESS
#define FRAM_ADDRESS_AC_MODE 0            // 01bytes
#define FRAM_ADDRESS_AC_TEMPERATURE 1     // 01bytes
#define FRAM_ADDRESS_AC_FAN_SPEED 2       // 01bytes
#define FRAM_ADDRESS_PWM_STATE 3          // 16bytes, thru 18
#define FRAM_ADDRESS_PWM_VALUE 19         // 32bytes, thru 50
#define FRAM_ADDRESS_HOSTNAME 65          // 15bytes, thru 79
#define FRAM_ADDRESS_TOPIC 80             // 20bytes, thru 99
#define FRAM_ADDRESS_IP 100               // 04bytes, thru 103
#define FRAM_ADDRESS_SUBNET 104           // 04bytes, thru 107
#define FRAM_ADDRESS_GATEWAY 108          // 04bytes, thru 111
#define FRAM_ADDRESS_DNS 112              // 04bytes, thru 115
#define FRAM_ADDRESS_MQTT_SERVER 116      // 04bytes, thru 119
#define FRAM_ADDRESS_MQTT_PORT 120        // 02bytes, thru 121
#define FRAM_ADDRESS_MQTT_USERNAME 122    // 32bytes, thru 153
#define FRAM_ADDRESS_MQTT_PASSWORD 154    // 32bytes, thru 185
#define FRAM_ADDRESS_MQTT_USEAUTH 186     // 1bytes
#define FRAM_ADDRESS_ADC_REPORT_STATE 187 // 8bytes, thru 194
#define FRAM_ADDRESS_DAC_STATE 195        // 4bytes, thru 198
#define FRAM_ADDRESS_DAC_VALUE 199        // 8bytes, thru 206
#define FRAM_ADDRESS_WEBUI_USERNAME 207   // 32bytes, thru 238
#define FRAM_ADDRESS_WEBUI_PASSWORD 239   // 32bytes, thru 270

char PWM_STATE_TOPIC[75];
char PWM_VALUE_TOPIC[75];
char INPUTS_TOPIC[75];

WiFiClient eth;
PubSubClient mqtt(MQTT_SERVER, 1883, eth);

#ifdef ENABLE_CLIMATE_MODULE
DHTNEW env_sensor(DHT22_PIN);
#endif

Thread mqtt_reconnector = Thread();
Thread environment_reporter = Thread();
Thread fram_pwm_updater = Thread();
Thread user_timer_tick = Thread();
Thread analog_handler = Thread();
StaticThreadController<5> thread_controller(&mqtt_reconnector, &environment_reporter, &fram_pwm_updater, &user_timer_tick, &analog_handler);

#ifdef ENABLE_INTERNAL_LCD
Thread top_bar_updater = Thread();
Thread page_updater = Thread();
StaticThreadController<2> lcd_thread_controller(&top_bar_updater, &page_updater);
#endif

void setup()
{
  Serial.begin(115200);
#ifdef ENABLE_EXTERNAL_LCD
  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
#endif
#ifdef ENABLE_INTERNAL_LCD
  panel.begin(115200);
#endif
  Serial.println("ESPMega R3 Initializing");
  ESPMega_begin();
#ifdef OVERCLOCK_FM2
  Wire.setClock(1000000);
#endif
#ifdef OVERCLOCK_FM
  Wire.setClock(400000);
#endif
  io_begin();
#ifdef VIRTUAL_INTERRUPT_PRELOAD
  virtual_interrupt_preload();
#endif
  fram_retrieve_init();
  user_pre_init();
#ifdef ENABLE_INTERNAL_LCD
  lcd_send_stop_bit();
  lcd_init();
  lcd_begin();
#endif
  check_boot_reset();
#ifdef ENABLE_EXTERNAL_LCD
  Serial2.print("rest");
  Serial2.write(0xFF);
  Serial2.write(0xFF);
  Serial2.write(0xFF);
#endif
  lcd_send_command("boot_state.txt=\"Core Initializing . . .\"");
  Serial.println("Initializing Infrared . . .");
#ifdef ENABLE_IR_MODULE
  lcd_send_command("boot_state.txt=\"Infrared Initializing . . .\"");
  IrReceiver.begin(IR_RECIEVE_PIN);
  IrSender.begin(IR_SEND_PIN);
#endif
  lcd_send_command("boot_state.txt=\"Network Initializing . . .\"");
  network_begin();
  lcd_send_command("boot_state.txt=\"IoT Core Initializing . . .\"");
  mqtt.setSocketTimeout(1000);
  eth.setTimeout(1);
  mqtt.setCallback(&mqtt_callback);
  mqtt_connect();
  lcd_send_command("boot_state.txt=\"Threads Initializing . . .\"");
  thread_initialization();
#ifdef ENABLE_WEBUI
  ota_begin();
#endif
  Serial.println("Initialization Completed.");
  Serial.println("Jumping to User Code.");
  user_init();
  lcd_send_command("page dashboard");
}

void loop()
{
  virtual_interrupt_loop();
  mqtt.loop();
  ESPMega_loop();
#ifdef ENABLE_IR_MODULE
  ir_loop();
#endif
  thread_controller.run();
#ifdef ENABLE_INTERNAL_LCD
  lcd_loop();
#endif
  user_loop();
#ifdef ENABLE_WEBUI
  otaserver.handleClient();
#endif
}

/**
 * @brief Retrieves data from FRAM and initializes various variables and topics.
 *
 */
void fram_retrieve_init()
{
  #ifdef ENABLE_WEBUI
  ESPMega_FRAM.read(FRAM_ADDRESS_WEBUI_USERNAME, (uint8_t *)WEBUI_USERNAME, 32);
  ESPMega_FRAM.read(FRAM_ADDRESS_WEBUI_PASSWORD, (uint8_t *)WEBUI_PASSWORD, 32);
  if(strlen(WEBUI_USERNAME)==0) {
    strcpy(WEBUI_USERNAME,"admin");
    ESPMega_FRAM.write(FRAM_ADDRESS_WEBUI_USERNAME, (uint8_t *)WEBUI_USERNAME, 32);
  }
  if(strlen(WEBUI_PASSWORD)==0) {
    strcpy(WEBUI_PASSWORD,"admin");
    ESPMega_FRAM.write(FRAM_ADDRESS_WEBUI_PASSWORD, (uint8_t *)WEBUI_PASSWORD, 32);
  }
  #endif
// FRAM Data Retrival
#ifdef ENABLE_CLIMATE_MODULE
  ac_mode = ESPMega_FRAM.read8(FRAM_ADDRESS_AC_MODE);
  ac_temperature = ESPMega_FRAM.read8(FRAM_ADDRESS_AC_TEMPERATURE);
  ac_fan_speed = ESPMega_FRAM.read8(FRAM_ADDRESS_AC_FAN_SPEED);
#endif
// FRAM Data Retrival Validation
#ifdef ENABLE_CLIMATE_MODULE
  if (ac_mode > 2)
  {
    ac_mode = 0;
    ESPMega_FRAM.write8(FRAM_ADDRESS_AC_MODE, ac_mode);
  }
  if (ac_temperature > AC_MAX_TEMPERATURE || ac_temperature < AC_MIN_TEMPERATURE)
  {
    ac_temperature = AC_MAX_TEMPERATURE;
    ESPMega_FRAM.write8(FRAM_ADDRESS_AC_TEMPERATURE, ac_temperature);
  }
  if (ac_fan_speed > 3)
  {
    ac_fan_speed = 0;
    ESPMega_FRAM.write8(FRAM_ADDRESS_AC_TEMPERATURE, ac_fan_speed);
  }
  ac_set_state(ac_mode, ac_temperature, ac_fan_speed);
#endif
  ESPMega_FRAM.read(FRAM_ADDRESS_PWM_STATE, pwm_states_fram, 16);
  memcpy(pwm_states, pwm_states_fram, 16);
  ESPMega_FRAM.read(FRAM_ADDRESS_PWM_VALUE, pwm_values_fram, 32);
  memcpy(pwm_values, pwm_values_fram, 32);
  for (int i = 0; i < 15; i++)
  {
    if (pwm_states[i] <= 1)
      pwm_set_state(i, pwm_states[i]);
    else
      pwm_set_state(i, 0);
    if (pwm_values[i] <= 4095)
      pwm_set_value(i, pwm_values[i]);
    else
      pwm_set_value(i, 0);
  }
  IP = fram_ip_retrieve(FRAM_ADDRESS_IP);
  SUBNET = fram_ip_retrieve(FRAM_ADDRESS_SUBNET);
  GATEWAY = fram_ip_retrieve(FRAM_ADDRESS_GATEWAY);
  DNS = fram_ip_retrieve(FRAM_ADDRESS_DNS);
  MQTT_SERVER = fram_ip_retrieve(FRAM_ADDRESS_MQTT_SERVER);
  fram_hostname_retrieve();
  fram_mqtt_port_retrieve();
  fram_mqtt_useauth_retrieve();
  fram_mqtt_username_retrieve();
  fram_mqtt_password_retrieve();
  mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  fram_basetopic_retrieve();
  base_topic_length = strlen(MQTT_BASE_TOPIC) + 1;
  memcpy(STATE_REQUEST_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(STATE_REQUEST_TOPIC, "/requeststate");
  memcpy(PWM_SET_STATE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(PWM_SET_STATE_TOPIC, "/pwm/00/set/state");
  memcpy(PWM_SET_VALUE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(PWM_SET_VALUE_TOPIC, "/pwm/00/set/value");
#ifdef ENABLE_CLIMATE_MODULE
  memcpy(AC_SET_MODE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_SET_MODE_TOPIC, "/ac/set/mode");
  memcpy(AC_SET_FAN_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_SET_FAN_TOPIC, "/ac/set/fan_speed");
  memcpy(AC_SET_TEMPERATURE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_SET_TEMPERATURE_TOPIC, "/ac/set/temperature");
  memcpy(AC_MODE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_MODE_TOPIC, "/ac/mode");
  memcpy(AC_FAN_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_FAN_TOPIC, "/ac/fan_speed");
  memcpy(AC_TEMPERATURE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_TEMPERATURE_TOPIC, "/ac/temperature");
  memcpy(AC_ROOM_TEMPERATURE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_ROOM_TEMPERATURE_TOPIC, "/ac/room_temperature");
  memcpy(AC_HUMIDITY_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AC_HUMIDITY_TOPIC, "/ac/humidity");
#endif
  memcpy(PWM_STATE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(PWM_STATE_TOPIC, "/pwm/00/state");
  memcpy(PWM_VALUE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(PWM_VALUE_TOPIC, "/pwm/00/value");
  memcpy(INPUTS_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(INPUTS_TOPIC, "/input/00");
  memcpy(AVAILABILITY_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(AVAILABILITY_TOPIC, "/availability");
#ifdef ENABLE_ANALOG_MODULE
  memcpy(ADC_COMMAND_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(ADC_COMMAND_TOPIC, "/adc/00/set/state");
  memcpy(ADC_STATE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(ADC_STATE_TOPIC, "/adc/00/state");
  memcpy(ADC_REPORT_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(ADC_REPORT_TOPIC, "/adc/00/report");
  memcpy(DAC_SET_STATE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(DAC_SET_STATE_TOPIC, "/dac/00/set/state");
  memcpy(DAC_SET_VALUE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(DAC_SET_VALUE_TOPIC, "/dac/00/set/value");
  memcpy(DAC_STATE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(DAC_STATE_TOPIC, "/dac/00/state");
  memcpy(DAC_VALUE_TOPIC, MQTT_BASE_TOPIC, 20);
  strcat(DAC_VALUE_TOPIC, "/dac/00/value");
  ESPMega_FRAM.read(FRAM_ADDRESS_ADC_REPORT_STATE, (uint8_t *)adc_report_enable, 8);
  ESPMega_FRAM.read(FRAM_ADDRESS_DAC_STATE, (uint8_t *)dac_states, 4);
  ESPMega_FRAM.read(FRAM_ADDRESS_DAC_VALUE, (uint8_t *)dac_values, 8);
  for (int i = 0; i < DAC_COUNT; i++)
  {
    dac_set_state(i, dac_states[i]);
    dac_set_value(i, dac_values[i]);
  }
#endif
}

#ifdef ENABLE_WEBUI
/**
 * Initializes the OTA (Over-The-Air) update server and sets up the necessary routes for configuration and firmware updates.
 *
 * The OTA server can be accessed through the root URL ("/") and the configuration page can be accessed through "/config".
 * The firmware update can be initiated through the "/update" route using HTTP POST method.
 *
 * The configuration page allows the user to set the device IP address, network mask, gateway, DNS server, hostname, BMS server IP address, BMS server port, BMS server endpoint, and BMS server authentication credentials.
 *
 * @return void
 */
void ota_begin()
{
  otaserver.on("/", HTTP_GET, []()
               {
    if(!otaserver.authenticate(WEBUI_USERNAME, WEBUI_PASSWORD))
      return otaserver.requestAuthentication();
    otaserver.sendHeader("Connection", "close");
    String otabuffer = ota_part1;
    otabuffer+=ota_part2_1+"Hostname"+ota_part2_2+String(HOSTNAME)+ota_part2_3;
    otabuffer+=ota_part2_1+"IP Address"+ota_part2_2+IP.toString()+ota_part2_3;
    otabuffer+=ota_part2_1+"MAC Address"+ota_part2_2+ETH.macAddress()+ota_part2_3;
    otabuffer+=ota_part2_1+"Device"+ota_part2_2+ESPMEGA_REV+ota_part2_3;
    #ifdef FW_VERSION
    otabuffer+=ota_part2_1+"Firmware"+ota_part2_2+FW_VERSION+ota_part2_3;
    #else
    otabuffer+=ota_part2_1+"Firmware"+ota_part2_2+"Out of Tree"+ota_part2_3;
    #endif
    otabuffer+=ota_part2_1+"BMS Server"+ota_part2_2+MQTT_SERVER.toString()+ota_part2_3;
    otabuffer+=ota_part2_1+"BMS Endpoint"+ota_part2_2+String(MQTT_BASE_TOPIC)+ota_part2_3;
    otabuffer+=ota_part2_1+"Centrally Managed"+ota_part2_2;
    if(standalone)
    otabuffer+=String("No");
    else
    otabuffer+=String("Yes");
    otabuffer+=ota_part2_3+ota_part3;
    otaserver.send(200, "text/html", otabuffer); });
  otaserver.on("/config", HTTP_GET, []()
               {
    if(!otaserver.authenticate(WEBUI_USERNAME, WEBUI_PASSWORD))
      return otaserver.requestAuthentication();
    otaserver.sendHeader("Connection", "close");
    String configbuffer = config_part1;
    configbuffer+=config_txt_part1+"IP Address"+config_txt_part2+"text"+config_txt_part3+"dev_ip"+config_txt_part4+"dev_ip"+config_txt_part5+IP.toString()+config_txt_part6;
    configbuffer+=config_txt_part1+"Network Mask"+config_txt_part2+"text"+config_txt_part3+"netmask"+config_txt_part4+"netmask"+config_txt_part5+SUBNET.toString()+config_txt_part6;
    configbuffer+=config_txt_part1+"Gateway"+config_txt_part2+"text"+config_txt_part3+"gateway"+config_txt_part4+"gateway"+config_txt_part5+GATEWAY.toString()+config_txt_part6;
    configbuffer+=config_txt_part1+"DNS Server"+config_txt_part2+"text"+config_txt_part3+"dns"+config_txt_part4+"dns"+config_txt_part5+DNS.toString()+config_txt_part6;
    configbuffer+=config_txt_part1+"Hostname"+config_txt_part2+"text"+config_txt_part3+"hostname"+config_txt_part4+"hostname"+config_txt_part5+String(HOSTNAME)+config_txt_part6;
    configbuffer+=config_txt_part1+"BMS Server - IP Address"+config_txt_part2+"text"+config_txt_part3+"bms_ip"+config_txt_part4+"bms_ip"+config_txt_part5+MQTT_SERVER.toString()+config_txt_part6;
    configbuffer+=config_txt_part1+"BMS Server - Port"+config_txt_part2+"text"+config_txt_part3+"bms_port"+config_txt_part4+"bms_port"+config_txt_part5+String(MQTT_PORT)+config_txt_part6;
    configbuffer+=config_auth_part1+(MQTT_USE_AUTH?"checked=\"checked\"":"")+config_auth_part2;
    configbuffer+=config_txt_part1+"BMS Server - Username"+config_txt_part2+"text"+config_txt_part3+"bms_username"+config_txt_part4+"bms_username"+config_txt_part5+String(MQTT_USERNAME)+config_txt_part6;
    configbuffer+=config_txt_part1+"BMS Server - Password"+config_txt_part2+"password"+config_txt_part3+"bms_password"+config_txt_part4+"bms_password"+config_txt_part5+String(MQTT_PASSWORD)+config_txt_part6;
    configbuffer+=config_txt_part1+"BMS Server - Endpoint"+config_txt_part2+"text"+config_txt_part3+"bms_endpoint"+config_txt_part4+"bms_endpoint"+config_txt_part5+String(MQTT_BASE_TOPIC)+config_txt_part6;
    configbuffer+=config_txt_part1+"WebUI Username"+config_txt_part2+"text"+config_txt_part3+"webui_username"+config_txt_part4+"webui_username"+config_txt_part5+String(WEBUI_USERNAME)+config_txt_part6;
    configbuffer+=config_txt_part1+"WebUI Password"+config_txt_part2+"password"+config_txt_part3+"webui_password"+config_txt_part4+"webui_password"+config_txt_part5+String(WEBUI_PASSWORD)+config_txt_part6;
    configbuffer+=config_part2;
    otaserver.send(200, "text/html", configbuffer); });

  otaserver.on("/save_config", HTTP_GET, []()
               {
    if(!otaserver.authenticate(WEBUI_USERNAME, WEBUI_PASSWORD))
      return otaserver.requestAuthentication();
    otaserver.sendHeader("Connection", "close");
    String configbuffer = "Configuration Saved. Rebooting . . .";
    otaserver.send(200, "text/html", configbuffer);
    bool use_auth = false;
    for(int i=0;i<otaserver.args();i++) {
      String arg = otaserver.argName(i);
      String value = otaserver.arg(i);
      if(!arg.compareTo("dev_ip")) {
        set_ip(value);
      } else if(!arg.compareTo("netmask")) {
        set_netmask(value);
      } else if(!arg.compareTo("gateway")) {
        set_gw(value);
      } else if(!arg.compareTo("dns")) {
        set_dns(value);
      } else if(!arg.compareTo("hostname")) {
        set_hostname(value);
      } else if(!arg.compareTo("bms_ip")) {
        set_mqtt_server(value);
      }  else if(!arg.compareTo("bms_username")) {
        set_mqtt_username(value);
      }else if(!arg.compareTo("bms_port")) {
        uint16_t port = value.toInt();
        mqtt_port_set(port);
      }else if(!arg.compareTo("bms_endpoint")) {
        set_basetopic(value);
      }
       else if(!arg.compareTo("bms_password")) {
        set_mqtt_password(value);
      } else if(!arg.compareTo("bms_useauth")) {
        if(!value.compareTo("yes"))
          use_auth = true;
      } else if(!arg.compareTo("webui_username")) {
        ESPMega_FRAM.write(FRAM_ADDRESS_WEBUI_USERNAME, (uint8_t *)value.c_str(), value.length()+1);
      } else if(!arg.compareTo("webui_password")) {
        ESPMega_FRAM.write(FRAM_ADDRESS_WEBUI_PASSWORD, (uint8_t *)value.c_str(), value.length()+1);
      }
    }
      set_mqtt_useauth(use_auth);
      otaserver.send(200, "text/html", configbuffer); 
      delay(500);
      ESP.restart(); });

  otaserver.on(
      "/update", HTTP_POST, []()
      {
    if(!otaserver.authenticate(WEBUI_USERNAME, WEBUI_PASSWORD))
      return otaserver.requestAuthentication();
    otaserver.sendHeader("Connection", "close");
    otaserver.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart(); },
      []()
      {
        HTTPUpload &upload = otaserver.upload();
        if (upload.status == UPLOAD_FILE_START)
        {
          lcd_send_command("page ota");
          Serial.println(upload.currentSize);

          String otafiletxt = "Downloading File : " + upload.filename;
#ifdef ENABLE_INTERNAL_LCD
          lcd_send_stop_bit();
          panel.writeStr("otatxt.txt", otafiletxt);
#endif
          Serial.printf("Update: %s\n", upload.filename.c_str());
          if (!Update.begin(UPDATE_SIZE_UNKNOWN))
          {
#ifdef ENABLE_INTERNAL_LCD
            panel.writeStr("otatxt.txt", "Update Failed, Rebooting . . .");
#endif
            Update.printError(Serial);
          }
        }
        else if (upload.status == UPLOAD_FILE_WRITE)
        {
          if (Update.write(upload.buf, upload.currentSize) != upload.currentSize)
          {
#ifdef ENABLE_INTERNAL_LCD
            panel.writeStr("otatxt.txt", "Update Failed, Rebooting . . .");
#endif
            Update.printError(Serial);
          }
          if (upload.currentSize != 0 && upload.totalSize != 0)
          {
            lcd_send_stop_bit();
            uint32_t totalsize_kb = upload.totalSize / 1000;
            uint32_t upload_pct = 100 * upload.totalSize / 1000000;
            String otafiletxt = "Downloading File : " + upload.filename + " (" + String(totalsize_kb) + "KB)";
#ifdef ENABLE_INTERNAL_LCD
            panel.writeNum("prog.val", upload_pct);
            panel.writeStr("otatxt.txt", otafiletxt);
#endif
          }
        }
        else if (upload.status == UPLOAD_FILE_END)
        {
          if (Update.end(true))
          {
#ifdef ENABLE_INTERNAL_LCD
            panel.writeStr("otatxt.txt", "Update Completed, Rebooting . . .");
#endif
            Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
          }
          else
          {
#ifdef ENABLE_INTERNAL_LCD
            panel.writeStr("otatxt.txt", "Update Failed, Rebooting . . .");
#endif
            Update.printError(Serial);
          }
        }
      });
  otaserver.begin();
}
#endif

void io_begin()
{
  Serial.println("Initializing I/O . . .");
#ifdef ENABLE_IR_MODULE
  pinMode(IR_RECIEVE_PIN, INPUT_PULLUP);
  pinMode(IR_SEND_PIN, OUTPUT);
#endif
}

/**
 * @brief Initializes the network and sets the hostname and IP configuration.
 *
 * @details This function initializes the Ethernet connection and sets the hostname, IP address, gateway, subnet, and DNS server.
 * It also initializes the NTP core and updates the time from the NTP server.
 *
 * @return void
 */
void network_begin()
{
  Serial.print("Initializing Network ");
  ETH.begin();
  ETH.setHostname(HOSTNAME);
  ETH.config(IP, GATEWAY, SUBNET, DNS, DNS);
#ifndef FASTBOOT
  delay(1000);
  lcd_send_command("boot_state.txt=\"Ethernet Core Initializing      \"");
  delay(500);
  lcd_send_command("boot_state.txt=\"Ethernet Core Initializing .    \"");
  delay(500);
  lcd_send_command("boot_state.txt=\"Ethernet Core Initializing . . \"");
  delay(500);
  lcd_send_command("boot_state.txt=\"Ethernet Core Initializing . . .\"");
  delay(500);
  lcd_send_command("boot_state.txt=\"NTP Core Initializing . . .\"");
  delay(500);
#endif
  char ntp[19];
  MQTT_SERVER.toString().toCharArray(ntp, 19);
  ESPMega_configNTP(utc_offset * 3600, 0, ntp);
  ESPMega_updateTimeFromNTP();
  Serial.println();
}

/**
 * @brief Connects to the MQTT broker and subscribes to topics.
 *
 * If MQTT_USE_AUTH is true, it connects to the broker using the provided username and password.
 * If the connection is successful, it subscribes to topics and publishes PWM and input states.
 * If ENABLE_CLIMATE_MODULE is defined, it also publishes AC state.
 *
 * @note This function assumes that the MQTT client has been initialized and WiFi is connected.
 *
 * @return void
 */
void mqtt_connect()
{
  if (!mqtt.connected())
  {
    Serial.print("MQTT not connected, connecting . . .\n");
    lcd_send_stop_bit();
    if (MQTT_USE_AUTH)
      mqtt.connect(HOSTNAME, MQTT_USERNAME, MQTT_PASSWORD, AVAILABILITY_TOPIC, 0, true, "offline");
    else
      mqtt.connect(HOSTNAME, AVAILABILITY_TOPIC, 0, true, "offline");
    if (mqtt.connected())
    {
      mqtt_subscribe();
      Serial.print("MQTT connected\n");
      mqtt.publish(AVAILABILITY_TOPIC, "online", true);
      lcd_send_stop_bit();
      publish_pwm_states();
      publish_input_states();
#ifdef ENABLE_CLIMATE_MODULE
      publish_ac_state();
#endif
      mqtt_connected_user_callback();
#ifdef ENABLE_ANALOG_MODULE
      publish_dac_states();
      publish_dac_values();
      publish_adc_values();
      publish_adc_states();
#endif
      standalone = false;
      ESPMega_updateTimeFromNTP();
    }
    else
    {
      standalone = true;
      Serial.print("MQTT not connected, continuing in standalone mode\n");
      lcd_send_stop_bit();
    }
    lcd_send_stop_bit();
#ifdef ENABLE_INTERNAL_LCD
    lcd_refresh();
    lcd_top_bar_update();
#endif
  }
}

/**
 * @brief Subscribes to MQTT topics for controlling PWM, climate module and requesting device state.
 *
 */
void mqtt_subscribe()
{
  for (int i = 0; i < PWM_COUNT; i++)
  {
    PWM_SET_VALUE_TOPIC[base_topic_length + 4] = ((i - i % 10) / 10) + '0';
    PWM_SET_VALUE_TOPIC[base_topic_length + 5] = (i % 10) + '0';
    PWM_SET_STATE_TOPIC[base_topic_length + 4] = ((i - i % 10) / 10) + '0';
    PWM_SET_STATE_TOPIC[base_topic_length + 5] = (i % 10) + '0';
    mqtt.subscribe(PWM_SET_STATE_TOPIC);
    mqtt.subscribe(PWM_SET_VALUE_TOPIC);
  }
#ifdef ENABLE_CLIMATE_MODULE
  mqtt.subscribe(AC_SET_FAN_TOPIC);
  mqtt.subscribe(AC_SET_TEMPERATURE_TOPIC);
  mqtt.subscribe(AC_SET_MODE_TOPIC);
#endif
  mqtt.subscribe(STATE_REQUEST_TOPIC);
#ifdef ENABLE_ANALOG_MODULE
  for (int i = 0; i < ADC_COUNT; i++)
  {
    ADC_COMMAND_TOPIC[base_topic_length + 4] = ((i - i % 10) / 10) + '0';
    ADC_COMMAND_TOPIC[base_topic_length + 5] = (i % 10) + '0';
    mqtt.subscribe(ADC_COMMAND_TOPIC);
  }
  for (int i = 0; i < DAC_COUNT; i++)
  {
    DAC_SET_STATE_TOPIC[base_topic_length + 4] = ((i - i % 10) / 10) + '0';
    DAC_SET_STATE_TOPIC[base_topic_length + 5] = (i % 10) + '0';
    DAC_SET_VALUE_TOPIC[base_topic_length + 4] = ((i - i % 10) / 10) + '0';
    DAC_SET_VALUE_TOPIC[base_topic_length + 5] = (i % 10) + '0';
    mqtt.subscribe(DAC_SET_STATE_TOPIC);
    mqtt.subscribe(DAC_SET_VALUE_TOPIC);
  }
#endif
}

/**
 * @brief Callback function for MQTT messages received by the device.
 *
 * @param topic The topic of the received message.
 * @param payload The payload of the received message.
 * @param length The length of the payload.
 */
void mqtt_callback(char *topic, byte *payload, unsigned int length)
{
  uint8_t topic_length = strlen(topic);
  char topic_trim[50];
  char payload_nt[length + 1];
  memcpy(payload_nt, payload, length);
  payload_nt[length] = NULL;
  strncpy(topic_trim, topic + base_topic_length - 1, topic_length);
  if ((!strncmp(topic_trim, "/pwm/", 5)) && !strncmp(topic_trim + 7, "/set/state", 10))
  {
    pwm_state_callback(topic_trim, topic_length, payload_nt, length);
  }
  else if ((!strncmp(topic_trim, "/pwm/", 5)) && !strncmp(topic_trim + 7, "/set/value", 10))
  {
    pwm_value_callback(topic_trim, topic_length, payload_nt, length);
  }
#ifdef ENABLE_ANALOG_MODULE
  else if ((!strncmp(topic_trim, "/adc/", 5)) && !strncmp(topic_trim + 7, "/set/state", 10))
  {
    adc_set_state_callback(topic_trim, topic_length, payload_nt, length);
  }
  else if ((!strncmp(topic_trim, "/dac/", 5)) && !strncmp(topic_trim + 7, "/set/state", 10))
  {
    dac_set_state_callback(topic_trim, topic_length, payload_nt, length);
  }
  else if ((!strncmp(topic_trim, "/dac/", 5)) && !strncmp(topic_trim + 7, "/set/value", 10))
  {
    dac_set_value_callback(topic_trim, topic_length, payload_nt, length);
  }
#endif
#ifdef ENABLE_IR_MODULE
  else if (!strcmp(topic_trim, "/ir/send"))
  {
    const char* delimiter = ",";
    char* token = strtok(const_cast<char*>(payload_nt), delimiter);
    while (token != nullptr && ir_buffer_length < IR_RAW_BUFFER_LENGTH) {
        ir_buffer[ir_buffer_length++] = atoi(token);
        token = strtok(nullptr, delimiter);
    }
    IrSender.sendRaw(ir_buffer,ir_buffer_length ,NEC_KHZ);
  }
#endif
  else if (!strcmp(topic, STATE_REQUEST_TOPIC))
  {
    state_request_callback();
  }
  else
  {
#ifdef ENABLE_CLIMATE_MODULE
    ac_state_callback(topic, topic_length, payload_nt, length);
#endif
  }
  user_mqtt_callback(topic, topic_length, payload_nt, length);
}

/**
 * @brief Initializes the threads for various tasks such as MQTT connection, environment reporting, FRAM PWM update, and user timer tick.
 *
 */
void thread_initialization()
{
  Serial.println("Initializing Threads . . .");
  Serial.println("Initializing MQTT Thread . . .");
  mqtt_reconnector.onRun(mqtt_connect);
  mqtt_reconnector.setInterval(15000);
#ifdef ENABLE_CLIMATE_MODULE
  environment_reporter.onRun(publish_env_state);
  environment_reporter.setInterval(5000);
#endif
  fram_pwm_updater.onRun(fram_pwm_update);
  fram_pwm_updater.setInterval(1000);
  user_timer_tick.onRun(timer_tick_callback);
  user_timer_tick.setInterval(15000);
#ifdef ENABLE_ANALOG_MODULE
  analog_handler.onRun(adc_loop);
  analog_handler.setInterval(ANALOG_REPORTING_INTERVAL);
#endif
}

/**
 * @brief Callback function for handling PWM state changes.
 *
 * @param topic The MQTT topic on which the message was received.
 * @param topic_length The length of the MQTT topic.
 * @param payload The message payload.
 * @param payload_length The length of the message payload.
 */
void pwm_state_callback(char *topic, uint8_t topic_length, char *payload, unsigned int payload_length)
{
  int a = topic[5] - '0';
  int b = topic[6] - '0';
  int id = 10 * a + b;
  if (!strcmp(payload, "on"))
  {
    pwm_set_state(id, true);
  }
  else if (!strcmp(payload, "off"))
  {
    pwm_set_state(id, false);
  }
}

/**
 * @brief Callback function for PWM value updates.
 *
 * This function is called whenever a message is received on the PWM value update topic.
 * It extracts the PWM ID and value from the topic and payload, respectively, and sets the PWM value.
 *
 * @param topic The topic on which the message was received.
 * @param topic_length The length of the topic.
 * @param payload The message payload.
 * @param payload_length The length of the message payload.
 */
void pwm_value_callback(char *topic, uint8_t topic_length, char *payload, unsigned int payload_length)
{
  int a = topic[5] - '0';
  int b = topic[6] - '0';
  int id = 10 * a + b;
  int value = atoi(payload);
  pwm_set_value(id, value);
}

/**
 * @brief Callback function for virtual interrupts.
 *
 * This function publishes the input state of the virtual interrupt pin and calls the user-defined callback function.
 * If ENABLE_INTERNAL_LCD is defined, it also updates the LCD display with the input state.
 *
 * @param pin The virtual interrupt pin number.
 * @param state The state of the virtual interrupt pin (HIGH or LOW).
 */
void virtual_interrupt_callback(int pin, int state)
{

  publish_input_state(pin, state);
#ifdef ENABLE_INTERNAL_LCD
  if (lcd_current_page == 2)
    panel.writeNum("I" + String(pin) + ".val", state);
#endif
  virtual_interrupt_user_callback(pin, state);
}

void virtual_interrupt_loop()
{
  for (int i = 0; i < 16; i++)
  {
    int current_pin_value = ESPMega_digitalRead(virtual_interrupt_pins[i]);
    if (virtual_interupt_state[i] != current_pin_value)
    {
      if (millis() - virtual_interupt_timer[i] > DEBOUNCE_TIME_MS)
      {
        virtual_interupt_state[i] = current_pin_value;
        virtual_interrupt_callback(i, current_pin_value);
      }
    }
    else
    {
      virtual_interupt_timer[i] = millis();
    }
    yield();
  }
}

/**
 * @brief Publishes the current state of all PWM channels.
 *
 */
void publish_pwm_states()
{
  for (int i = 0; i < PWM_COUNT; i++)
  {
    publish_pwm_state(i);
  }
}

/**
 * @brief Publishes the state and value of a PWM signal to MQTT broker.
 *
 * @param id The ID of the PWM signal.
 */
void publish_pwm_state(int id)
{
  int state = pwm_states[id];
  int value = pwm_values[id];
  PWM_STATE_TOPIC[base_topic_length + 4] = ((id - id % 10) / 10) + '0';
  PWM_STATE_TOPIC[base_topic_length + 5] = (id % 10) + '0';
  PWM_VALUE_TOPIC[base_topic_length + 4] = ((id - id % 10) / 10) + '0';
  PWM_VALUE_TOPIC[base_topic_length + 5] = (id % 10) + '0';
  if (state == 1)
  {
    mqtt.publish(PWM_STATE_TOPIC, "on");
  }
  else if (state == 0)
  {
    mqtt.publish(PWM_STATE_TOPIC, "off");
  }
  char temp[6];
  itoa(value, temp, DEC);
  mqtt.publish(PWM_VALUE_TOPIC, temp);
}

/**
 * @brief Sets the state of a PWM pin and publishes the new state.
 *
 * @param id The ID of the PWM pin.
 * @param state The new state of the PWM pin.
 */
void pwm_set_state(int id, int state)
{
  if (state != pwm_states[id])
  {
    pwm_states[id] = state;
    int pwm_value = pwm_values[id];
    ESPMega_analogWrite(pwm_pins[id], state * (int)(pwm_linear_scaling_m[id] * pwm_value + pwm_linear_scaling_c[id]));
#ifdef ENABLE_INTERNAL_LCD
    if (lcd_current_page == 3)
      panel.writeNum("j" + String(id) + ".ppic", pwm_states[id] ? 33 : 48);
    else if (lcd_current_page == 5 && id == lcd_pwmAdj_id)
      panel.writeStr("pwm_state.txt", pwm_states[lcd_pwmAdj_id] ? "ON" : "OFF");
#endif
    publish_pwm_state(id);
    pwm_changed_user_callback(id);
  }
}

/**
 * @brief Sets the PWM value for a given id.
 *
 * @param id The id of the PWM pin.
 * @param value The value to set the PWM pin to.
 */
void pwm_set_value(int id, int value)
{
  pwm_values[id] = value;
  int pwm_state = pwm_states[id];
  ESPMega_analogWrite(pwm_pins[id], pwm_state * (int)(pwm_linear_scaling_m[id] * value + pwm_linear_scaling_c[id]));
#ifdef ENABLE_INTERNAL_LCD
  if (lcd_current_page == 3)
    panel.writeNum("j" + String(id) + ".val", int(value / 4095.0 * 100.0));
  else if (lcd_current_page == 5 && id == lcd_pwmAdj_id)
    panel.writeNum("pwm_value.val", pwm_values[lcd_pwmAdj_id]);
#endif
  publish_pwm_state(id);
  pwm_changed_user_callback(id);
}

/**
 * @brief Toggles the state of a PWM pin.
 *
 * @param id The ID of the PWM pin to toggle.
 */
void pwm_toggle(int id)
{
  int state = !pwm_states[id];
  pwm_set_state(id, state);
}

/**
 * Toggles the state of two PWM pins.
 * @param id1 The first PWM pin ID.
 * @param id2 The second PWM pin ID.
 */
void pwm_toggle(int id1, int id2)
{
  boolean state = pwm_group_state(id1, id2);
  if (state)
  {
    pwm_set_state(id1, 0);
    pwm_set_state(id2, 0);
  }
  else
  {
    pwm_set_state(id1, 1);
    pwm_set_state(id2, 1);
  }
}

/**
 * @brief Returns a boolean value indicating whether either of the two specified PWM states is true.
 *
 * @param id1 The first PWM state ID.
 * @param id2 The second PWM state ID.
 * @return true if either of the two specified PWM states is true, false otherwise.
 */
boolean pwm_group_state(int id1, int id2)
{
  int state1 = pwm_states[id1 - 1], state2 = pwm_states[id2 - 1];
  if (state1 || state2)
    return true;
  return false;
}

/**
 * @brief Cycles the PWM value for the given id.
 *
 * @param id The id of the PWM pin.
 */
void pwm_cycle_value(int id)
{
  int state = pwm_states[id];
  int value = pwm_values[id];
  if (state == 1)
    for (int i = 0; i < PWM_CYCLE_VALUES_COUNT; i++)
    {
      if (PWM_CYCLE_VALUES[i] == value)
      {
        if (i > 0)
        {
          pwm_set_value(id, PWM_CYCLE_VALUES[i - 1]);
          return;
        }
        else
        {
          pwm_set_state(id, 0);
          return;
        }
      }
    }
  pwm_set_state(id, 1);
  pwm_set_value(id, PWM_CYCLE_VALUES[PWM_CYCLE_VALUES_COUNT - 1]);
}

/**
 * @brief Publishes the input states for all virtual input pins.
 *
 */
void publish_input_states()
{
  for (int i = 0; i < VINT_COUNT; i++)
  {
    publish_input_state(i);
  }
}

/**
 * @brief Publishes the input state of a specific pin.
 *
 * @param id The ID of the pin to publish the state of.
 */
void publish_input_state(int id)
{
  int state = ESPMega_digitalRead(virtual_interrupt_pins[id]);
  publish_input_state(id, state);
}

/**
 * @brief Publishes the state of an input to the MQTT broker.
 *
 * @param id The ID of the input.
 * @param state The state of the input (0 or 1).
 */
void publish_input_state(int id, int state)
{
  INPUTS_TOPIC[base_topic_length + 6] = ((id - id % 10) / 10) + '0';
  INPUTS_TOPIC[base_topic_length + 7] = (id % 10) + '0';
  mqtt.publish(INPUTS_TOPIC, state ? "1" : "0");
}

/**
 * @brief Callback function to request the current state of the device.
 *
 * This function publishes the input and PWM states of the device. If the climate module is enabled, it also publishes the AC and environment states.
 * Finally, it calls the user-defined state request callback function.
 */
void state_request_callback()
{
  publish_input_states();
  publish_pwm_states();
#ifdef ENABLE_CLIMATE_MODULE
  publish_ac_state();
  publish_env_state();
#endif
  user_state_request_callback();
#ifdef ENABLE_ANALOG_MODULE
  publish_adc_states();
  publish_adc_values();
  publish_dac_states();
  publish_dac_values();
#endif
}

#ifdef ENABLE_IR_MODULE
void ir_loop()
{
  if (IrReceiver.decode())
  {
    IrReceiver.resume();
  }
}
#endif

#ifdef ENABLE_CLIMATE_MODULE
/**
 * @brief Publishes the current state of the air conditioner to MQTT topics.
 *
 */
void publish_ac_state()
{
  switch (ac_mode)
  {
  case 0:
    mqtt.publish(AC_MODE_TOPIC, "off");
    break;
  case 1:
    mqtt.publish(AC_MODE_TOPIC, "cool");
    break;
  case 2:
    mqtt.publish(AC_MODE_TOPIC, "fan_only");
  default:
    break;
  }
  char temp[5];
  itoa(ac_temperature, temp, DEC);
  mqtt.publish(AC_TEMPERATURE_TOPIC, temp);
  switch (ac_fan_speed)
  {
  case 0:
    mqtt.publish(AC_FAN_TOPIC, "auto");
    break;
  case 1:
    mqtt.publish(AC_FAN_TOPIC, "high");
    break;
  case 2:
    mqtt.publish(AC_FAN_TOPIC, "medium");
    break;
  case 3:
    mqtt.publish(AC_FAN_TOPIC, "low");
    break;
  }
}

/**
 * @brief Callback function for handling AC state changes.
 *
 * This function is called whenever a message is received on the AC_SET_TEMPERATURE_TOPIC, AC_SET_MODE_TOPIC, or AC_SET_FAN_TOPIC.
 * It parses the payload of the message and updates the AC state accordingly.
 *
 * @param topic The topic of the message.
 * @param topic_length The length of the topic.
 * @param payload The payload of the message.
 * @param payload_length The length of the payload.
 */
void ac_state_callback(char *topic, uint8_t topic_length, char *payload, unsigned int payload_length)
{
  if (!strcmp(topic, AC_SET_TEMPERATURE_TOPIC))
  {
    int new_temp = atoi(payload);
    if (new_temp >= AC_MIN_TEMPERATURE && new_temp <= AC_MAX_TEMPERATURE)
    {
      ac_set_state(ac_mode, new_temp, ac_fan_speed);
    }
  }
  else if (!strcmp(topic, AC_SET_MODE_TOPIC))
  {
    if (!strcmp(payload, "off"))
    {
      ac_set_state(0, ac_temperature, ac_fan_speed);
    }
    else if (!strcmp(payload, "cool"))
    {
      ac_set_state(1, ac_temperature, ac_fan_speed);
    }
    else if (!strcmp(payload, "fan_only"))
    {
      ac_set_state(2, ac_temperature, ac_fan_speed);
    }
  }
  else if (!strcmp(topic, AC_SET_FAN_TOPIC))
  {
    if (!strcmp(payload, "auto"))
    {
      ac_set_state(ac_mode, ac_temperature, 0);
    }
    else if (!strcmp(payload, "high"))
    {
      ac_set_state(ac_mode, ac_temperature, 1);
    }
    else if (!strcmp(payload, "medium"))
    {
      ac_set_state(ac_mode, ac_temperature, 2);
    }
    else if (!strcmp(payload, "low"))
    {
      ac_set_state(ac_mode, ac_temperature, 3);
    }
  }
}

/**
 * @brief Sets the state of the air conditioner.
 *
 * @param mode The mode of the air conditioner (0 = off, 1 = cool, 2 = fan).
 * @param temperature The temperature to set the air conditioner to (in Celsius).
 * @param fan_speed The fan speed of the air conditioner (0 = low, 1 = medium, 2 = high).
 */
void ac_set_state(int mode, int temperature, int fan_speed)
{
  ac_mode = mode;
  ac_temperature = temperature;
  if (ac_temperature < AC_MIN_TEMPERATURE)
    ac_temperature = AC_MIN_TEMPERATURE;
  else if (ac_temperature > AC_MAX_TEMPERATURE)
    ac_temperature = AC_MAX_TEMPERATURE;
  ac_fan_speed = fan_speed;
  temperature -= AC_MIN_TEMPERATURE;
#ifdef ENABLE_INTERNAL_LCD
  if (lcd_current_page == 4)
  {
    lcd_ac_refresh_fan();
    lcd_ac_refresh_mode();
    if (ac_mode != 2)
      panel.writeStr("temp.txt", String(ac_temperature) + "C");
    else
      panel.writeStr("temp.txt", "--C");
  }
#endif
  publish_ac_state();
  uint8_t ac_datablock[3] = {ac_mode, ac_temperature, ac_fan_speed};
  ESPMega_FRAM.write(0, ac_datablock, 3);
  switch (mode)
  {
  case 0:
    IrSender.sendRaw(ir_code_off, sizeof(ir_code_off) / sizeof(ir_code_off[0]), NEC_KHZ);
    break;
  case 1:
    IrSender.sendRaw(ir_code_cool[fan_speed][temperature], sizeof(ir_code_cool[fan_speed][temperature]) / sizeof(ir_code_cool[fan_speed][temperature][0]), NEC_KHZ);
    break;
  case 2:
    IrSender.sendRaw(ir_code_fan[fan_speed], sizeof(ir_code_fan[fan_speed]) / sizeof(ir_code_fan[fan_speed][0]), NEC_KHZ);
    break;
  }
  ac_changed_user_callback(mode, ac_temperature, fan_speed);
}

/**
 * @brief Reads the environment sensor and publishes the temperature and humidity to MQTT topics.
 *
 * @details Reads the environment sensor and publishes the temperature and humidity to MQTT topics.
 * If the internal LCD is enabled, it also updates the temperature and humidity values on the LCD screen.
 *
 * @return void
 */
void publish_env_state()
{
  int errorCode = env_sensor.read();
  yield();
  switch (errorCode)
  {
  case DHTLIB_OK:
    current_room_humid = env_sensor.getHumidity();
    current_room_temp = env_sensor.getTemperature();
    char temp[5];
    dtostrf(current_room_temp, 0, 2, temp);
    mqtt.publish(AC_ROOM_TEMPERATURE_TOPIC, temp);
    dtostrf(current_room_humid, 0, 2, temp);
    mqtt.publish(AC_HUMIDITY_TOPIC, temp);
    mqtt.loop();
#ifdef ENABLE_INTERNAL_LCD
    if (lcd_current_page == 4)
    {
      Serial.printf("roomtemp.txt=\"%.01fC\"", current_room_temp);
      lcd_send_stop_bit();
      Serial.printf("roomhumid.txt=\"%d%%\"", (int)current_room_humid);
      lcd_send_stop_bit();
    }
#endif
    break;
  default:
    mqtt.publish(AC_ROOM_TEMPERATURE_TOPIC, "ERROR");
    mqtt.loop();
    mqtt.publish(AC_HUMIDITY_TOPIC, "ERROR");
    mqtt.loop();
  }
}
#endif

#ifdef ENABLE_INTERNAL_LCD
/**
 * @brief Initializes the LCD display and sets up the refresh intervals for the top bar and page.
 *
 */
void lcd_begin()
{
  top_bar_updater.onRun(lcd_top_bar_update);
  top_bar_updater.setInterval(10000);
  page_updater.onRun(lcd_refresh_pd);
  page_updater.setInterval(5000);
  lcd_refresh();
}

/**
 * @brief Runs the LCD loop, updating the display and listening for input.
 *
 */
void lcd_loop()
{
  lcd_thread_controller.run();
  panel.NextionListen();
  if (panel.currentPageId != lcd_current_page)
  {
    lcd_current_page = panel.currentPageId;
    lcd_refresh();
    lcd_top_bar_update();
  }
}

/**
 * @brief Refreshes the LCD display if the current page is 1.
 *
 */
void lcd_refresh_pd()
{
  if (lcd_current_page == 1)
  {
    lcd_refresh();
  }
}

/**
 * @brief Refreshes the LCD display based on the current page.
 *
 * @details The function writes the appropriate values to the LCD display based on the current page.
 *
 * @note This function assumes that the LCD panel has already been initialized.
 *
 */
void lcd_refresh()
{
  switch (lcd_current_page)
  {
  case 1:
    panel.writeStr("hostname.txt", HOSTNAME);
    panel.writeStr("server_address.txt", MQTT_SERVER.toString());
    panel.writeStr("ip_address.txt", IP.toString());
    panel.writeStr("status_txt.txt", standalone ? "Standalone" : "BMS Managed");
    break;
  case 2:
    for (int i = 0; i <= 15; i++)
    {
      panel.writeNum("I" + String(i) + ".val", virtual_interupt_state[i]);
    }
    break;
  case 3:
    for (int i = 0; i <= 15; i++)
    {
      panel.writeNum("j" + String(i) + ".val", int(pwm_values[i] / 4095.0 * 100.0));
      panel.writeNum("j" + String(i) + ".ppic", pwm_states[i] ? 33 : 48);
    }
    break;
  case 4:
#ifdef ENABLE_CLIMATE_MODULE
    Serial.printf("roomtemp.txt=\"%.01fC\"", current_room_temp);
    lcd_send_stop_bit();
    Serial.printf("roomhumid.txt=\"%d%%\"", (int)current_room_humid);
    lcd_send_stop_bit();
    if (ac_mode != 2)
      panel.writeStr("temp.txt", String(ac_temperature) + "C");
    else
      panel.writeStr("temp.txt", "--C");
    lcd_ac_refresh_fan();
    lcd_ac_refresh_mode();
#else
    lcd_send_command("page climate_nomod");
#endif
  case 5:
    panel.writeStr("pwm_id.txt", String("P") + String(lcd_pwmAdj_id));
    panel.writeStr("pwm_state.txt", pwm_states[lcd_pwmAdj_id] ? "ON" : "OFF");
    panel.writeNum("pwm_value.val", pwm_values[lcd_pwmAdj_id]);
    break;
  case 6:
    panel.writeStr("ip_set.txt", IP.toString());
    panel.writeStr("netmask_set.txt", SUBNET.toString());
    panel.writeStr("gateway_set.txt", GATEWAY.toString());
    panel.writeStr("dns_set.txt", DNS.toString());
    panel.writeStr("host_set.txt", HOSTNAME);
    break;
  case 11:
    panel.writeStr("mqttsv_set.txt", MQTT_SERVER.toString());
    panel.writeNum("port_set.val", MQTT_PORT);
    panel.writeStr("topic_set.txt", MQTT_BASE_TOPIC);
    panel.writeStr("user_set.txt", MQTT_USERNAME);
    panel.writeStr("password_set.txt", MQTT_PASSWORD);
    panel.writeNum("use_auth.val", MQTT_USE_AUTH);
    break;
  default:
    break;
  }
}

/**
 * @brief Updates the top bar of the LCD display with the current time, server status, and LAN status.
 *
 * @details This function gets the current time using ESPMega_getTime() function and formats it to display on the LCD.
 * It also updates the server and LAN status icons based on the standalone and ETH.linkUp() values respectively.
 *
 * @return void
 */
void lcd_top_bar_update()
{
  char time_buffer[15];
  rtctime_t time = ESPMega_getTime();
  sprintf(time_buffer, "%02d:%02d", time.hours, time.minutes);
  panel.writeStr("time.txt", time_buffer);
  panel.writeNum("server.pic", standalone ? 4 : 5);
  panel.writeNum("lan.pic", ETH.linkUp() ? 3 : 2);
}

#ifdef ENABLE_CLIMATE_MODULE
/**
 * @brief Refreshes the LCD display with the current AC mode.
 *
 * This function updates the LCD display with the current AC mode, which can be one of the following:
 *  - 0: Off
 *  - 1: Cool
 *  - 2: Fan
 *
 * The function uses the panel object to write the appropriate image to the display for each mode.
 */
void lcd_ac_refresh_mode()
{
  // auto high mid low
  panel.writeNum("mode_cool.pic", ac_mode == 1 ? 12 : 13);
  panel.writeNum("mode_fan.pic", ac_mode == 2 ? 22 : 23);
  panel.writeNum("mode_off.pic", ac_mode == 0 ? 24 : 25);
}

/**
 * @brief Refreshes the fan speed icons on the LCD panel based on the current AC fan speed.
 *
 * This function updates the fan speed icons on the LCD panel based on the current AC fan speed.
 * It uses the panel.writeNum() function to write the appropriate fan speed icon to the panel.
 *
 */
void lcd_ac_refresh_fan()
{
  panel.writeNum("fan_auto.pic", ac_fan_speed == 0 ? 14 : 15);
  panel.writeNum("fan_low.pic", ac_fan_speed == 3 ? 18 : 19);
  panel.writeNum("fan_mid.pic", ac_fan_speed == 2 ? 20 : 21);
  panel.writeNum("fan_high.pic", ac_fan_speed == 1 ? 16 : 17);
}
#endif

void trigger0()
{
  if (lcd_pwmAdj_id >= 15)
    lcd_pwmAdj_id = 0;
  else
    lcd_pwmAdj_id++;
  lcd_refresh();
}

void trigger1()
{
  if (lcd_pwmAdj_id <= 0)
    lcd_pwmAdj_id = 15;
  else
    lcd_pwmAdj_id--;
  lcd_refresh();
}

void trigger2()
{
  pwm_toggle(lcd_pwmAdj_id);
}

void trigger3()
{
  int value = panel.readNumber("pwm_value.val");
  lcd_send_stop_bit();
  pwm_set_value(lcd_pwmAdj_id, value);
}
void trigger4()
{
#ifdef ENABLE_CLIMATE_MODULE
  if (ac_temperature < AC_MAX_TEMPERATURE && ac_mode != 2)
    ac_set_state(ac_mode, ac_temperature + 1, ac_fan_speed);
#endif
}

void trigger5()
{
#ifdef ENABLE_CLIMATE_MODULE
  if (ac_temperature > AC_MIN_TEMPERATURE && ac_mode != 2)
    ac_set_state(ac_mode, ac_temperature - 1, ac_fan_speed);
#endif
}

void trigger6()
{
#ifdef ENABLE_CLIMATE_MODULE
  ac_set_state(ac_mode, ac_temperature, 0);
#endif
}

void trigger7()
{
#ifdef ENABLE_CLIMATE_MODULE
  ac_set_state(ac_mode, ac_temperature, 3);
#endif
}

void trigger8()
{
#ifdef ENABLE_CLIMATE_MODULE
  ac_set_state(ac_mode, ac_temperature, 2);
#endif
}

void trigger9()
{
#ifdef ENABLE_CLIMATE_MODULE
  ac_set_state(ac_mode, ac_temperature, 1);
#endif
}

void trigger10()
{
#ifdef ENABLE_CLIMATE_MODULE
  ac_set_state(1, ac_temperature, ac_fan_speed);
#endif
}

void trigger11()
{
#ifdef ENABLE_CLIMATE_MODULE
  ac_set_state(2, ac_temperature, ac_fan_speed);
#endif
}

void trigger12()
{
#ifdef ENABLE_CLIMATE_MODULE
  ac_set_state(0, ac_temperature, ac_fan_speed);
#endif
}
void trigger13()
{
  set_ip(panel.readStr("ip_set.txt"));
  set_netmask(panel.readStr("netmask_set.txt"));
  set_gw(panel.readStr("gateway_set.txt"));
  set_dns(panel.readStr("dns_set.txt"));
  set_hostname(panel.readStr("host_set.txt"));
  delay(100);
  ESP.restart();
}

void trigger14()
{
  Serial.print("page dashboard");
  lcd_send_stop_bit();
}

void trigger15()
{
  set_mqtt_server(panel.readStr("mqttsv_set.txt"));
  set_basetopic(panel.readStr("topic_set.txt"));
  uint16_t port = panel.readNumber("port_set.val");
  mqtt_port_set(port);
  set_mqtt_useauth(panel.readNumber("use_auth.val"));
  set_mqtt_username(panel.readStr("user_set.txt"));
  set_mqtt_password(panel.readStr("password_set.txt"));
  delay(100);
  ESP.restart();
}
#endif // End Internal LCD Code Block

/**
 * @brief Updates the PWM states and values in FRAM if they have changed.
 *
 * If the current PWM states or values are different from the ones stored in FRAM,
 * this function updates the FRAM with the current values.
 */
void fram_pwm_update()
{
  if (memcmp(pwm_states, pwm_states_fram, 16))
  {
    memcpy(pwm_states_fram, pwm_states, 16);
    ESPMega_FRAM.write(3, pwm_states_fram, 16);
  }
  if (memcmp(pwm_values, pwm_values_fram, 32))
  {
    memcpy(pwm_values_fram, pwm_values, 32);
    ESPMega_FRAM.write(19, pwm_values_fram, 32);
  }
}

/**
 * @brief Sets the IP address and updates it in FRAM.
 *
 * @param address The IP address to set.
 */
void set_ip(String address)
{
  IP.fromString(address);
  fram_ip_update(FRAM_ADDRESS_IP, IP[0], IP[1], IP[2], IP[3]);
}

/**
 * @brief Sets the subnet mask for the device.
 *
 * @param address The subnet mask address to set.
 */
void set_netmask(String address)
{
  SUBNET.fromString(address);
  fram_ip_update(FRAM_ADDRESS_SUBNET, SUBNET[0], SUBNET[1], SUBNET[2], SUBNET[3]);
}

/**
 * Sets the DNS server address.
 * @param address The IP address of the DNS server.
 */
void set_dns(String address)
{
  DNS.fromString(address);
  fram_ip_update(FRAM_ADDRESS_DNS, DNS[0], DNS[1], DNS[2], DNS[3]);
}

/**
 * @brief Sets the gateway IP address and updates the FRAM.
 *
 * @param address The gateway IP address as a String.
 */
void set_gw(String address)
{
  GATEWAY.fromString(address);
  fram_ip_update(FRAM_ADDRESS_GATEWAY, GATEWAY[0], GATEWAY[1], GATEWAY[2], GATEWAY[3]);
}

/**
 * @brief Sets the MQTT server address and updates the FRAM.
 *
 * @param address The MQTT server address as a String.
 */
void set_mqtt_server(String address)
{
  MQTT_SERVER.fromString(address);
  fram_ip_update(FRAM_ADDRESS_MQTT_SERVER, MQTT_SERVER[0], MQTT_SERVER[1], MQTT_SERVER[2], MQTT_SERVER[3]);
}

/**
 * @brief Updates the IP address in FRAM at the specified ROM address.
 *
 * @param rom_address The ROM address where the IP address is stored.
 * @param byte1 The first byte of the IP address.
 * @param byte2 The second byte of the IP address.
 * @param byte3 The third byte of the IP address.
 * @param byte4 The fourth byte of the IP address.
 */
void fram_ip_update(uint16_t rom_address, uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
{
  uint8_t addressblock[4] = {byte1, byte2, byte3, byte4};
  ESPMega_FRAM.write(rom_address, addressblock, 4);
}

/**
 * @brief Retrieves an IP address from FRAM memory.
 *
 * @param rom_address The address in FRAM memory where the IP address is stored.
 * @return The retrieved IP address.
 */
IPAddress fram_ip_retrieve(uint16_t rom_address)
{
  uint8_t addressblock[4];
  ESPMega_FRAM.read(rom_address, addressblock, 4);
  return IPAddress(addressblock[0], addressblock[1], addressblock[2], addressblock[3]);
}

/**
 * @brief Sets the hostname for the device and writes it to non-volatile memory.
 *
 * @param hostname The hostname to set.
 */
void set_hostname(String hostname)
{
  hostname.toCharArray(HOSTNAME, 15);
  ESPMega_FRAM.write(FRAM_ADDRESS_HOSTNAME, (uint8_t *)HOSTNAME, 15);
}

/**
 * @brief Retrieves the hostname from FRAM and stores it in the HOSTNAME variable.
 *
 */
void fram_hostname_retrieve()
{
  ESPMega_FRAM.read(FRAM_ADDRESS_HOSTNAME, (uint8_t *)HOSTNAME, 15);
}

/**
 * @brief Sets the base topic for MQTT communication and writes it to FRAM.
 *
 * @param topic The base topic to set.
 */
void set_basetopic(String topic)
{
  topic.toCharArray(MQTT_BASE_TOPIC, 20);
  ESPMega_FRAM.write(FRAM_ADDRESS_TOPIC, (uint8_t *)MQTT_BASE_TOPIC, 20);
}

/**
 * @brief Retrieves the MQTT base topic from FRAM and stores it in MQTT_BASE_TOPIC array.
 *
 */
void fram_basetopic_retrieve()
{
  ESPMega_FRAM.read(FRAM_ADDRESS_TOPIC, (uint8_t *)MQTT_BASE_TOPIC, 20);
}

/**
 * @brief Sets the MQTT port in the FRAM.
 *
 * @param port The MQTT port to be set.
 */
void mqtt_port_set(uint16_t port)
{
  uint8_t port_arr[2];
  memcpy(port_arr, &port, 2);
  ESPMega_FRAM.write(FRAM_ADDRESS_MQTT_PORT, port_arr, 2);
}
/**
 * @brief Retrieves the MQTT port from FRAM and stores it in the MQTT_PORT variable.
 *
 */
void fram_mqtt_port_retrieve()
{
  uint8_t port_arr[2];
  ESPMega_FRAM.read(FRAM_ADDRESS_MQTT_PORT, port_arr, 2);
  memcpy(&MQTT_PORT, port_arr, 2);
}

/**
 * @brief Returns the state of the PWM with the given ID.
 *
 * @param id The ID of the PWM.
 * @return true if the PWM is on, false otherwise.
 */
boolean pwm_get_state(int id)
{
  return pwm_states[id];
}

/**
 * Returns the current PWM value for the specified ID.
 *
 * @param id The ID of the PWM channel to get the value for.
 * @return The current PWM value for the specified ID.
 */
uint16_t pwm_get_value(int id)
{
  return pwm_values[id];
}

/**
 * @brief Returns the state of the virtual interrupt with the given ID.
 *
 * @param id The ID of the virtual interrupt.
 * @return The state of the virtual interrupt.
 */
boolean input_get_state(int id)
{
  return virtual_interupt_state[id];
}

#ifdef ENABLE_CLIMATE_MODULE
/**
 * @brief Returns the current temperature value.
 *
 * @return The current temperature value.
 */
uint8_t ac_get_temperature()
{
  return ac_temperature;
}
/**
 * @brief Get the current mode of the AC system.
 *
 * @return uint8_t The current mode of the AC system.
 */
uint8_t ac_get_mode()
{
  return ac_mode;
}
/**
 * @brief Get the current fan speed.
 *
 * @return uint8_t The current fan speed.
 */
uint8_t ac_get_fan_speed()
{
  return ac_fan_speed;
}
#endif

/**
 * @brief Retrieves the MQTT username from FRAM and stores it in the MQTT_USERNAME global variable.
 *
 */
void fram_mqtt_username_retrieve()
{
  ESPMega_FRAM.read(FRAM_ADDRESS_MQTT_USERNAME, (uint8_t *)MQTT_USERNAME, 32);
}
/**
 * @brief Retrieves the MQTT password from FRAM and stores it in the MQTT_PASSWORD global variable.
 *
 */
void fram_mqtt_password_retrieve()
{
  ESPMega_FRAM.read(FRAM_ADDRESS_MQTT_PASSWORD, (uint8_t *)MQTT_PASSWORD, 32);
}
/**
 * @brief Sets the MQTT username and writes it to the FRAM.
 *
 * @param username The MQTT username to set.
 */
void set_mqtt_username(String username)
{
  username.toCharArray(MQTT_USERNAME, 32);
  ESPMega_FRAM.write(FRAM_ADDRESS_MQTT_USERNAME, (uint8_t *)MQTT_USERNAME, 20);
}
/**
 * @brief Sets the MQTT password and writes it to the FRAM.
 *
 * @param password The MQTT password to set.
 */
void set_mqtt_password(String password)
{
  password.toCharArray(MQTT_PASSWORD, 32);
  ESPMega_FRAM.write(FRAM_ADDRESS_MQTT_PASSWORD, (uint8_t *)MQTT_PASSWORD, 20);
}

void fram_mqtt_useauth_retrieve()
{
  MQTT_USE_AUTH = ESPMega_FRAM.read8(FRAM_ADDRESS_MQTT_USEAUTH);
}
/**
 * @brief Sets the MQTT_USE_AUTH flag and writes it to the FRAM.
 *
 * @param use_auth A boolean value indicating whether to use authentication for MQTT.
 */
void set_mqtt_useauth(bool use_auth)
{
  MQTT_USE_AUTH = use_auth;
  ESPMega_FRAM.write8(FRAM_ADDRESS_MQTT_USEAUTH, MQTT_USE_AUTH);
}

#ifdef ENABLE_WEBUI
void set_webui_username(String username)
{
  username.toCharArray(WEBUI_USERNAME, 32);
  ESPMega_FRAM.write(FRAM_ADDRESS_WEBUI_USERNAME, (uint8_t *)WEBUI_USERNAME, 32);
}

void set_webui_password(String password)
{
  password.toCharArray(WEBUI_PASSWORD, 32);
  ESPMega_FRAM.write(FRAM_ADDRESS_WEBUI_PASSWORD, (uint8_t *)WEBUI_PASSWORD, 32);
}
#endif

/**
 * @brief Resets the device to factory default settings.
 *
 * This function resets the device to its factory default settings by formatting the FRAM,
 * setting default IP address, gateway, and netmask values, and restarting the device.
 *
 */
void factory_reset()
{
  for (int i = 5; i > 0; i--)
  {
    if (digitalRead(2) == HIGH)
    {
      lcd_send_command("boot_state.txt=\"Factory Reset Canceled, Restarting\"");
      delay(5000);
      ESP.restart();
    }
#ifdef ENABLE_INTERNAL_LCD
    Serial.printf("boot_state.txt=\"Factory Reset in %d\"", i);
    lcd_send_stop_bit();
#endif
    delay(1000);
  }
#ifdef ENABLE_INTERNAL_LCD
  lcd_send_command("boot_state.txt=\"Factory Reseting . . .\"");
#endif
  // Format FRAM
  for (int i = 0; i < 32768; i++)
  {
    ESPMega_FRAM.write8(i, 0);
  }
  // Load Default Values
  set_ip("192.168.0.10");
  set_gw("192.168.0.1");
  set_netmask("255.255.255.0");
  #ifdef ENABLE_WEBUI
  set_webui_username("admin");
  set_webui_password("admin");
  #endif

  // Reboot
#ifdef ENABLE_INTERNAL_LCD
  lcd_send_stop_bit();
  lcd_send_command("boot_state.txt=\"Factory Reset OK. Release Button.\"");
  delay(3000);
#endif
  ESP.restart();
}

/**
 * @brief Checks if the device should perform a factory reset on boot.
 *
 * This function checks if pin 2 is pulled low on boot. If it is, the device will perform a factory reset.
 *
 */
void check_boot_reset()
{
  pinMode(2, INPUT_PULLUP);
  if (digitalRead(2) == LOW)
  {
    factory_reset();
  }
}

#ifdef ENABLE_ANALOG_MODULE
/**
 * Enables the ADC reporting for the specified ID.
 *
 * @param id The ID of the ADC to enable reporting for.
 */
void enable_adc(int id)
{
  adc_report_enable[id] = true;
  ESPMega_FRAM.write8(FRAM_ADDRESS_ADC_REPORT_STATE + id, 1);
  publish_adc_state(id);
}
/**
 * @brief Disables the ADC reporting for the specified ID.
 *
 * This function sets the adc_report_enable flag to false for the specified ID and writes the state to the FRAM.
 *
 * @param id The ID of the ADC to disable reporting for.
 */
void disable_adc(int id)
{
  adc_report_enable[id] = false;
  ESPMega_FRAM.write8(FRAM_ADDRESS_ADC_REPORT_STATE + id, 0);
  publish_adc_state(id);
}

/**
 * Publishes the state of an ADC (Analog-to-Digital Converter) to the MQTT broker.
 * 
 * @param id The ID of the ADC.
 */
void publish_adc_state(int id)
{
  ADC_STATE_TOPIC[base_topic_length + 4] = ((id - id % 10) / 10) + '0';
  ADC_STATE_TOPIC[base_topic_length + 5] = (id % 10) + '0';
  mqtt.publish(ADC_STATE_TOPIC, adc_report_enable[id] ? "on" : "off");
}

/**
 * @brief Publishes the ADC states.
 * 
 * This function iterates over the ADC channels and publishes the state of each channel.
 */
void publish_adc_states()
{
  for (int i = 0; i < ADC_COUNT; i++)
  {
    publish_adc_state(i);
  }
}

/**
 * @brief Updates the ADC value for the specified ID if ADC reporting is enabled.
 *
 * @param id The ID of the ADC channel.
 */
void adc_update(int id)
{
  if (adc_report_enable[id])
  {
    adc_values[id] = ESPMega_analogRead(id);
  }
}

/**
 * @brief Updates the ADC value for the specified ID, do so even if reporting is disabled..
 *
 * @param id The ID of the ADC pin.
 */
void adc_update_force(int id)
{
  adc_values[id] = ESPMega_analogRead(id);
}

/**
 * @brief Updates all ADC channels.
 *
 * This function updates all ADC channels by calling the `adc_update` function for each channel.
 *
 * @return void
 */
void adc_update_all()
{
  for (int i = 0; i < ADC_COUNT; i++)
  {
    adc_update(i);
  }
}

/**
 * @brief Performs ADC loop operations.
 *
 * This function updates all ADC values and publishes them.
 */
void adc_loop()
{
  adc_update_all();
  publish_adc_values();
}

/**
 * Publishes the ADC value to the MQTT broker.
 *
 * @param id The ID of the ADC channel.
 */
void publish_adc_value(int id)
{
  ADC_REPORT_TOPIC[base_topic_length + 4] = ((id - id % 10) / 10) + '0';
  ADC_REPORT_TOPIC[base_topic_length + 5] = (id % 10) + '0';
  char temp[8];
  itoa(adc_values[id], temp, DEC);
  mqtt.publish(ADC_REPORT_TOPIC, temp);
}

/**
 * Publishes the values of all enabled ADC channels.
 * This function iterates through all ADC channels and publishes the values
 * of the enabled channels using the publish_adc() function.
 */
void publish_adc_values()
{
  for (int i = 0; i < ADC_COUNT; i++)
  {
    if (adc_report_enable[i])
      publish_adc_value(i);
  }
}

/**
 * Publishes the state of a DAC (Digital-to-Analog Converter) to the MQTT broker.
 * 
 * @param id The ID of the DAC.
 */
void publish_dac_state(int id)
{
  DAC_STATE_TOPIC[base_topic_length + 4] = ((id - id % 10) / 10) + '0';
  DAC_STATE_TOPIC[base_topic_length + 5] = (id % 10) + '0';
  mqtt.publish(DAC_STATE_TOPIC, dac_states[id] ? "on" : "off");
}

/**
 * Publishes the DAC value for a given ID.
 * 
 * @param id The ID of the DAC value to publish.
 */
void publish_dac_value(int id)
{
  DAC_VALUE_TOPIC[base_topic_length + 4] = ((id - id % 10) / 10) + '0';
  DAC_VALUE_TOPIC[base_topic_length + 5] = (id % 10) + '0';
  char temp[6];
  itoa(dac_values[id], temp, DEC);
  mqtt.publish(DAC_VALUE_TOPIC, temp);
}

/**
 * @brief Retrieves the ADC value for the specified ID.
 *
 * This function checks if the ADC report is enabled for the given ID. If not, it forces an update.
 * It then returns the ADC value for the specified ID.
 *
 * @param id The ID of the ADC channel.
 * @return The ADC value for the specified ID.
 */
uint16_t get_adc_value(int id)
{
  if (!adc_report_enable[id])
    adc_update_force(id);
  return adc_values[id];
}

/**
 * @brief Sets the state of an ADC based on the received MQTT message.
 *
 * This function is called when an MQTT message is received to set the state of an ADC (Analog-to-Digital Converter).
 * The function extracts the ADC ID from the topic and enables or disables the ADC based on the payload value.
 *
 * @param topic The topic of the MQTT message.
 * @param topic_length The length of the topic.
 * @param payload The payload of the MQTT message.
 * @param payload_length The length of the payload.
 */
void adc_set_state_callback(char *topic, uint8_t topic_length, char *payload, unsigned int payload_length)
{
  int a = topic[5] - '0';
  int b = topic[6] - '0';
  int id = 10 * a + b;
  if (!strcmp(payload, "on"))
  {
    enable_adc(id);
  }
  else if (!strcmp(payload, "off"))
  {
    disable_adc(id);
  }
}

/**
 * @brief Sets the value of a DAC channel.
 *
 * This function sets the value of a DAC channel specified by the `id` parameter.
 * The `value` parameter represents the desired value for the DAC channel.
 * The function updates the internal DAC value array, writes the value to the DAC,
 * and also stores the value in the FRAM memory.
 *
 * @param id The ID of the DAC channel.
 * @param value The desired value for the DAC channel.
 */
void dac_set_value(int id, int value)
{
  dac_values[id] = value;
  ESPMega_dacWrite(id, dac_values[id] * dac_states[id]);
  ESPMega_FRAM.write16(FRAM_ADDRESS_DAC_VALUE + id * 2, dac_values[id]);
  publish_dac_value(id);
}

/**
 * @brief Sets the state of a DAC channel.
 *
 * This function updates the state of a DAC channel and writes the new state to the DAC output.
 * It also saves the state to the FRAM for persistence across power cycles.
 *
 * @param id The ID of the DAC channel.
 * @param state The new state of the DAC channel.
 */
void dac_set_state(int id, bool state)
{
  dac_states[id] = state;
  ESPMega_dacWrite(id, dac_values[id] * dac_states[id]);
  ESPMega_FRAM.write8(FRAM_ADDRESS_DAC_STATE + id, dac_states[id]);
  publish_dac_state(id);
}

/**
 * @brief Publishes the states of all DACs.
 * 
 * This function iterates through all DACs and publishes their states.
 * 
 * @return void
 */
void publish_dac_states()
{
  for (int i = 0; i < DAC_COUNT; i++)
  {
    publish_dac_state(i);
  }
}

/**
 * @brief Publishes the DAC values.
 * 
 * This function iterates through all the DAC channels and publishes their values.
 * 
 * @return void
 */
void publish_dac_values()
{
  for (int i = 0; i < DAC_COUNT; i++)
  {
    publish_dac_value(i);
  }
}

/**
 * @brief Sets the value of the DAC with a callback function.
 *
 * @param topic The topic of the message.
 * @param topic_length The length of the topic string.
 * @param payload The payload of the message.
 * @param payload_length The length of the payload string.
 */
void dac_set_value_callback(char *topic, uint8_t topic_length, char *payload, unsigned int payload_length)
{
  int a = topic[5] - '0';
  int b = topic[6] - '0';
  int id = 10 * a + b;
  int value = atoi(payload);
  dac_set_value(id, value);
}
/**
 * @brief Callback function for setting the state of the DAC.
 *
 * This function is called when a message is received on the specified topic.
 * It takes the topic, topic length, payload, and payload length as parameters.
 *
 * @param topic The topic of the received message.
 * @param topic_length The length of the topic string.
 * @param payload The payload of the received message.
 * @param payload_length The length of the payload string.
 */
void dac_set_state_callback(char *topic, uint8_t topic_length, char *payload, unsigned int payload_length)
{
  int a = topic[5] - '0';
  int b = topic[6] - '0';
  int id = 10 * a + b;
  if (!strcmp(payload, "on"))
  {
    dac_set_state(id, true);
  }
  else if (!strcmp(payload, "off"))
  {
    dac_set_state(id, false);
  }
}

/**
 * @brief Get the ADC value for the specified ID.
 * 
 * @param id The ID of the ADC channel.
 * @return The ADC value.
 */
uint16_t adc_get_value(int id)
{
  return adc_values[id];
}
/**
 * @brief Get the state of the ADC with the specified ID.
 * 
 * @param id The ID of the ADC.
 * @return true if the ADC is enabled, false otherwise.
 */
bool adc_get_state(int id)
{
  return adc_report_enable[id];
}
/**
 * @brief Get the value of a DAC channel.
 * 
 * @param id The ID of the DAC channel.
 * @return The value of the DAC channel.
 */
uint16_t dac_get_value(int id)
{
  return dac_values[id];
}
/**
 * @brief Get the state of the DAC with the specified ID.
 * 
 * @param id The ID of the DAC.
 * @return The state of the DAC.
 */
bool dac_get_state(int id)
{
  return dac_states[id];
}

#endif

void virtual_interrupt_preload() {
  for (int i = 0; i < 16; i++) {
    virtual_interupt_state[i] = ESPMega_digitalRead(virtual_interrupt_pins[i]);
  }
}