From 87fa5aeedbadaaf35c459df873d583b3112caa73 Mon Sep 17 00:00:00 2001 From: Siwat Sirichai Date: Tue, 2 Jan 2024 00:02:05 +0700 Subject: [PATCH] Major Overhaul to ESPMegaOS - Expansion Card Management - Automatic MQTT Session Management - Multi Callback - LCD Component - Switch to OOP Design --- AnalogCard.cpp | 204 ++++ AnalogCard.hpp | 55 ++ AnalogIoT.cpp | 424 +++++++++ AnalogIoT.hpp | 65 ++ ClimateCard.cpp | 470 ++++++++++ ClimateCard.hpp | 123 +++ ClimateIoT.cpp | 257 ++++++ ClimateIoT.hpp | 50 + DigitalInputCard.cpp | 302 ++++++ DigitalInputCard.hpp | 66 ++ DigitalInputIoT.cpp | 120 +++ DigitalInputIoT.hpp | 31 + DigitalOutputCard.cpp | 309 +++++++ DigitalOutputCard.hpp | 116 +++ DigitalOutputIoT.cpp | 325 +++++++ DigitalOutputIoT.hpp | 41 + ESPMegaDisplay.cpp | 498 ++++++++++ ESPMegaDisplay.hpp | 51 + ESPMegaIoT.cpp | 809 ++++++++++++++++ ESPMegaIoT.hpp | 156 ++++ ESPMegaProOS.cpp | 262 ++++++ ESPMegaProOS.hpp | 92 ++ ESPMegaTCP.hpp | 9 + ESPMegaWebServer.cpp | 516 +++++++++++ ESPMegaWebServer.hpp | 58 ++ ExpansionCard.hpp | 24 + InternalDisplay.cpp | 872 ++++++++++++++++++ InternalDisplay.hpp | 135 +++ IoTComponent.cpp | 25 + IoTComponent.hpp | 39 + TimeStructure.hpp | 18 + cards/Analog.cpp | 0 cards/Analog.hpp | 26 - cards/DigitalInput.cpp | 95 -- cards/DigitalInput.hpp | 35 - cards/DigitalOutput.cpp | 31 - cards/DigitalOutput.hpp | 20 - .../PinOperation.cpp | 0 examples/OOP_Firmware/basic_firmware.cpp | 189 ++++ html/all.h | 3 + html/config.h | 696 ++++++++++++++ html/config.html | 306 ++++++ html/ota.h | 363 ++++++++ html/ota.html | 176 ++++ library.json | 71 +- library.properties | 2 +- 46 files changed, 8293 insertions(+), 242 deletions(-) create mode 100644 AnalogCard.cpp create mode 100644 AnalogCard.hpp create mode 100644 AnalogIoT.cpp create mode 100644 AnalogIoT.hpp create mode 100644 ClimateCard.cpp create mode 100644 ClimateCard.hpp create mode 100644 ClimateIoT.cpp create mode 100644 ClimateIoT.hpp create mode 100644 DigitalInputCard.cpp create mode 100644 DigitalInputCard.hpp create mode 100644 DigitalInputIoT.cpp create mode 100644 DigitalInputIoT.hpp create mode 100644 DigitalOutputCard.cpp create mode 100644 DigitalOutputCard.hpp create mode 100644 DigitalOutputIoT.cpp create mode 100644 DigitalOutputIoT.hpp create mode 100644 ESPMegaDisplay.cpp create mode 100644 ESPMegaDisplay.hpp create mode 100644 ESPMegaIoT.cpp create mode 100644 ESPMegaIoT.hpp create mode 100644 ESPMegaProOS.cpp create mode 100644 ESPMegaProOS.hpp create mode 100644 ESPMegaTCP.hpp create mode 100644 ESPMegaWebServer.cpp create mode 100644 ESPMegaWebServer.hpp create mode 100644 ExpansionCard.hpp create mode 100644 InternalDisplay.cpp create mode 100644 InternalDisplay.hpp create mode 100644 IoTComponent.cpp create mode 100644 IoTComponent.hpp create mode 100644 TimeStructure.hpp delete mode 100644 cards/Analog.cpp delete mode 100644 cards/Analog.hpp delete mode 100644 cards/DigitalInput.cpp delete mode 100644 cards/DigitalInput.hpp delete mode 100644 cards/DigitalOutput.cpp delete mode 100644 cards/DigitalOutput.hpp rename examples/{PinOperation => Functional_PinOperation}/PinOperation.cpp (100%) create mode 100644 examples/OOP_Firmware/basic_firmware.cpp create mode 100644 html/all.h create mode 100644 html/config.h create mode 100644 html/config.html create mode 100644 html/ota.h create mode 100644 html/ota.html diff --git a/AnalogCard.cpp b/AnalogCard.cpp new file mode 100644 index 0000000..b74f748 --- /dev/null +++ b/AnalogCard.cpp @@ -0,0 +1,204 @@ +/** + * @file AnalogCard.cpp + * @brief Implementation of the AnalogCard class. + */ + +#include +#include "esp_log.h" + +/** + * @brief Default constructor for the AnalogCard class. + */ +AnalogCard::AnalogCard() : dac0(DAC0_ADDRESS), + dac1(DAC1_ADDRESS), + dac2(DAC2_ADDRESS), + dac3(DAC3_ADDRESS), + analogInputBankA(), + analogInputBankB(), + dac_change_callbacks() +{ + this->handler_count = 0; +} + +/** + * @brief Writes a value to the specified DAC pin. + * @param pin The DAC pin to write to. + * @param value The value to write. + */ +void AnalogCard::dacWrite(uint8_t pin, uint16_t value) +{ + ESP_LOGV("AnalogCard", "DAC Write: %d, %d", pin, value); + this->setDACState(pin, value > 0); + this->setDACValue(pin, value); +} + +/** + * @brief Sets the state of the specified DAC pin. + * @param pin The DAC pin to set the state of. + * @param state The state to set (true = on, false = off). + */ +void AnalogCard::setDACState(uint8_t pin, bool state) +{ + ESP_LOGD("AnalogCard", "Setting DAC state: %d, %d", pin, state); + this->dac_state[pin] = state; + this->sendDataToDAC(pin, this->dac_value[pin] * state); + for (const auto& callback : this->dac_change_callbacks) + { + callback.second(pin, state, this->dac_value[pin]); + } +} + +/** + * @brief Sets the value of the specified DAC pin. + * @param pin The DAC pin to set the value of. + * @param value The value to set. + */ +void AnalogCard::setDACValue(uint8_t pin, uint16_t value) +{ + ESP_LOGD("AnalogCard", "Setting DAC value: %d, %d", pin, value); + this->dac_value[pin] = value; + this->sendDataToDAC(pin, value * this->dac_state[pin]); + for (const auto& callback : this->dac_change_callbacks) + { + callback.second(pin, this->dac_state[pin], value); + } +} + +/** + * @brief Gets the value of the specified DAC pin. + * @param pin The DAC pin to get the value of. + * @return The value of the DAC pin. + */ +uint16_t AnalogCard::getDACValue(uint8_t pin) +{ + return this->dac_value[pin]; +} + +/** + * @brief Gets the state of the specified DAC pin. + * @param pin The DAC pin to get the state of. + * @return The state of the DAC pin (true = on, false = off). + */ +bool AnalogCard::getDACState(uint8_t pin) +{ + return this->dac_state[pin]; +} + +/** + * @brief Sends data to the specified DAC pin. + * @param pin The DAC pin to send data to. + * @param value The data to send. + * @note This function does not call the DAC change callbacks. + */ +void AnalogCard::sendDataToDAC(uint8_t pin, uint16_t value) +{ + switch (pin) + { + case 0: + this->dac0.writeDAC(value); + break; + case 1: + this->dac1.writeDAC(value); + break; + case 2: + this->dac2.writeDAC(value); + break; + case 3: + this->dac3.writeDAC(value); + break; + } +} + +/** + * @brief Reads the value from the specified analog pin. + * @param pin The analog pin to read from. + * @return The value read from the analog pin. + */ +uint16_t AnalogCard::analogRead(uint8_t pin) +{ + if (pin >= 0 && pin <= 3) + { + return this->analogInputBankA.readADC_SingleEnded(pin); + } + else if (pin >= 4 && pin <= 7) + { + return this->analogInputBankB.readADC_SingleEnded(pin - 4); + } + return 65535; +} + +/** + * @brief Initializes the AnalogCard. + * @return True if initialization is successful, false otherwise. + */ +bool AnalogCard::begin() +{ + if (!this->dac0.begin()) + { + ESP_LOGE("AnalogCard", "Card Analog ERROR: Failed to install DAC0"); + return false; + } + if (!this->dac1.begin()) + { + ESP_LOGE("AnalogCard", "Card Analog ERROR: Failed to install DAC1"); + return false; + } + if (!this->dac2.begin()) + { + ESP_LOGE("AnalogCard", "Card Analog ERROR: Failed to install DAC2"); + return false; + } + if (!this->dac3.begin()) + { + ESP_LOGE("AnalogCard", "Card Analog ERROR: Failed to install DAC3"); + return false; + } + if (!this->analogInputBankA.begin()) + { + ESP_LOGE("AnalogCard", "Card Analog ERROR: Failed to install analog input bank A"); + return false; + } + if (!this->analogInputBankB.begin()) + { + ESP_LOGE("AnalogCard", "Card Analog ERROR: Failed to install analog input bank B"); + return false; + } + return true; +} + +/** + * @brief The main loop of the AnalogCard. + * @note This function does nothing. + */ +void AnalogCard::loop() +{ +} + +/** + * @brief Gets the type of the AnalogCard. + * @return The type of the AnalogCard. + */ +uint8_t AnalogCard::getType() +{ + return CARD_TYPE_ANALOG; +} + +/** + * @brief Registers a callback function to be called when the state or value of a DAC pin changes. + * @param callback The callback function to register. + * @return The handler ID of the registered callback. + */ +uint8_t AnalogCard::registerDACChangeCallback(std::function callback) +{ + this->dac_change_callbacks[this->handler_count] = callback; + return this->handler_count++; +} + +/** + * @brief Unregisters a previously registered DAC change callback. + * @param handler The handler ID of the callback to unregister. + */ +void AnalogCard::unregisterDACChangeCallback(uint8_t handler) +{ + this->dac_change_callbacks.erase(handler); +} \ No newline at end of file diff --git a/AnalogCard.hpp b/AnalogCard.hpp new file mode 100644 index 0000000..2b0ad1c --- /dev/null +++ b/AnalogCard.hpp @@ -0,0 +1,55 @@ +#pragma once +#include +#include +#include +#include +#include + +// Analog Card +#define CARD_TYPE_ANALOG 0x02 + +// Analog Card FRAM Address +#define ANALOG_INPUT_BANK_A_ADDRESS 0x48 +#define ANALOG_INPUT_BANK_B_ADDRESS 0x49 +#define DAC0_ADDRESS 0x60 +#define DAC1_ADDRESS 0x61 +#define DAC2_ADDRESS 0x62 +#define DAC3_ADDRESS 0x63 + +/** + * @brief This class represents the Analog Card. + * + * The analog card has 8 analog inputs accross two banks, and 4 DAC outputs. + * + * @note You do not need to specify the ESPMega I/O Address when creating an instance of this class as there can only be one Analog Card installed in the ESPMegaPRO board. + * @warning There can only be one Analog Card installed in the ESPMegaPRO board. + * + */ +class AnalogCard : public ExpansionCard { + public: + AnalogCard(); + void dacWrite(uint8_t pin, uint16_t value); + void sendDataToDAC(uint8_t pin, uint16_t value); + uint16_t analogRead(uint8_t pin); + bool begin(); + void loop(); + bool getDACState(uint8_t pin); + uint16_t getDACValue(uint8_t pin); + void setDACState(uint8_t pin, bool state); + void setDACValue(uint8_t pin, uint16_t value); + uint8_t registerDACChangeCallback(std::function callback); + void unregisterDACChangeCallback(uint8_t handler); + uint8_t getType(); + private: + uint8_t handler_count; + // Map of handler IDs to callback functions + std::map> dac_change_callbacks; + bool dac_state[4]; + uint16_t dac_value[4]; + MCP4725 dac0; + MCP4725 dac1; + MCP4725 dac2; + MCP4725 dac3; + Adafruit_ADS1115 analogInputBankA; + Adafruit_ADS1115 analogInputBankB; +}; \ No newline at end of file diff --git a/AnalogIoT.cpp b/AnalogIoT.cpp new file mode 100644 index 0000000..4479153 --- /dev/null +++ b/AnalogIoT.cpp @@ -0,0 +1,424 @@ +/** + * @file AnalogIoT.cpp + * @brief Implementation of the AnalogIoT class. + * + * This file contains the implementation of the AnalogIoT class, which provides functionality for handling analog input and output operations in an IoT system. + * The class allows for setting the state and value of digital-to-analog converters (DACs), as well as reading the value of analog-to-digital converters (ADCs). + * It also supports publishing the state and value of DACs and ADCs over MQTT. + */ +#include + + +/** + * @brief Default constructor for the AnalogIoT class. + * + * This constructor initializes the AnalogIoT object and sets up the ADC conversion callbacks. + */ +AnalogIoT::AnalogIoT() : adc_conversion_callbacks() { + for (uint8_t i = 0; i < 8; i++) { + adc_publish_enabled[i] = false; + adc_conversion_interval[i] = 1000; + } + this->adc_conversion_callback_index = 0; +} + +/** + * @brief Default destructor for the AnalogIoT class. + */ +AnalogIoT::~AnalogIoT() { + this->adc_conversion_callbacks.clear(); +} + +/** + * @brief Initializes the AnalogIoT object. + * @param card_id The ID of the card. + * @param card A pointer to the card object. + * @param mqtt A pointer to the MQTT client object. + * @param base_topic The base MQTT topic. + * @return True if the initialization was successful, false otherwise. + * @note This function can be called from the main program but it is recommended to use ESPMegaIoT to initialize the IoT Components. + * This function initializes the AnalogIoT object and registers the callbacks for handling DAC changes. + */ +bool AnalogIoT::begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic) { + this->mqtt = mqtt; + this->base_topic = base_topic; + this->card = (AnalogCard*)card; + this-> card_id = card_id; + this->dac_set_state_length = strlen(DAC_SET_STATE_TOPIC); + this->dac_set_value_length = strlen(DAC_SET_VALUE_TOPIC); + this->dac_state_length = strlen(DAC_STATE_TOPIC); + this->dac_value_length = strlen(DAC_VALUE_TOPIC); + this->request_state_length = strlen(REQUEST_STATE_TOPIC); + this->dac_publish_enable_length = strlen(DAC_PUBLISH_ENABLE_TOPIC); + // Register callbacks + auto bindedCallback = std::bind(&AnalogIoT::handleDACChange, this, std::placeholders::_1, std::placeholders::_2); + this->card->registerDACChangeCallback(bindedCallback); + return true; +} + +/** + * @brief Publishes the state of all DACs. + * @note This function is called when a request state message is received. + */ +void AnalogIoT::handleMqttMessage(char *topic, char *payload){ + uint8_t topic_length = strlen(topic); + if(this-> processDACSetStateMessage(topic, payload, topic_length)) return; + if(this-> processDACSetValueMessage(topic, payload, topic_length)) return; + if(this-> processRequestStateMessage(topic, payload, topic_length)) return; + if(this-> processADCSetConversionIntervalMessage(topic, payload, topic_length)) return; + if(this-> processADCSetConversionEnabledMessage(topic, payload, topic_length)) return; +} + +/** + * @brief Publishes the state of all DACs. + */ +void AnalogIoT::publishADCs() { + for (uint8_t i = 0; i < 8; i++) { + this->publishADC(i); + } +} + +/** + * @brief Publishes the state of a DAC. + * @param pin The pin of the DAC. + */ +void AnalogIoT::publishADC(uint8_t pin) { + if (this->adc_publish_enabled[pin]) { + uint16_t value = this->card->analogRead(pin); + char *topic = new char[15]; + sprintf(topic, "adc/%02d/value", pin); + char *payload = new char[10]; + sprintf(payload, "%d", value); + this->publishRelative(topic, payload); + delete[] topic; + delete[] payload; + // Call all callbacks + for (auto& callback : this->adc_conversion_callbacks) { + callback.second(pin, value); + } + } +} + +/** + * @brief Sets the interval at which the state of all DACs is published. + * @param interval The interval in milliseconds. + */ +void AnalogIoT::setADCsPublishInterval(uint32_t interval) { + for (uint8_t i = 0; i < 8; i++) { + adc_conversion_interval[i] = interval; + } +} + +/** + * @brief Sets whether the state of all DACs is published. + * @param enabled True if the state of all DACs should be published, false otherwise. + */ +void AnalogIoT::setADCsPublishEnabled(bool enabled) { + for (uint8_t i = 0; i < 8; i++) { + adc_publish_enabled[i] = enabled; + } +} + +/** + * @brief Registers a callback for handling ADC conversions. + * @param callback The callback function. + * @return The handler of the callback. + */ +uint8_t AnalogIoT::registerADCConversionCallback(std::function callback) { + this->adc_conversion_callbacks[this->adc_conversion_callback_index] = callback; + return this->adc_conversion_callback_index++; +} + +/** + * @brief Unregisters a callback for handling ADC conversions. + * @param handler The handler of the callback. + */ +void AnalogIoT::unregisterADCConversionCallback(uint8_t handler) { + this->adc_conversion_callbacks.erase(handler); +} + +/** + * @brief Sets the interval at which the value of an ADC channel is read. + * @param pin The pin of the ADC channel. + * @param interval The interval in milliseconds. + */ +void AnalogIoT::setADCConversionInterval(uint8_t pin, uint16_t interval) { + adc_conversion_interval[pin] = interval; +} + +/** + * @brief Enables or disables the periodic reading of the value of an ADC channel. + * @param pin The pin of the ADC channel. + * @param enabled True if the value of the ADC channel should be read, false otherwise. + */ +void AnalogIoT::setADCConversionEnabled(uint8_t pin, bool enabled) { + adc_publish_enabled[pin] = enabled; +} + +/** + * @brief Processes a message received on the MQTT topic for setting the state of a DAC. + * @param topic The topic of the message. + * @param payload The payload of the message. + * @param topic_length The length of the topic. + * @note This function is not meant to be called from user code. + * @return True if the message was processed, false otherwise. + */ +bool AnalogIoT::processADCSetConversionIntervalMessage(char *topic, char *payload, uint8_t topic_length) { + // TODO: Process payload matching the criteria + // Topic: adc/<%02d>/set/conversion_interval + // The first 4 characters are "adc/" + // The length of the topic must be 30 characters + // The last 24 characters must be "/set/conversion_interval" + // After all these conditions are met, the topic is valid + // Extract the pin number from the topic + if (topic_length != 30) { + return false; + } + if (strncmp(topic, "adc/", 4)) { + return false; + } + if (strncmp(topic + 26, "/set/conversion_interval", 24)) { + return false; + } + uint8_t pin = (topic[4] - '0') * 10 + (topic[5] - '0'); + // Extract the payload + uint16_t interval = atoi(payload); + // Set the interval + this->setADCConversionInterval(pin, interval); + return true; +} + +/** + * @brief Processes a message received on the MQTT topic for setting the value of a DAC. + * @param topic The topic of the message. + * @param payload The payload of the message. + * @param topic_length The length of the topic. + * @note This function is not meant to be called from user code. + * @return True if the message was processed, false otherwise. + */ +bool AnalogIoT::processADCSetConversionEnabledMessage(char *topic, char *payload, uint8_t topic_length) { + // Topic: adc/<%02d>/set/conversion_enabled + // The first 4 characters are "adc/" + // The length of the topic must be 29 characters + // The last 23 characters must be ""/set/conversion_enabled + // After all these conditions are met, the topic is valid + // Extract the pin number from the topic + if (topic_length != 29) { + return false; + } + if (strncmp(topic, "adc/", 4)) { + return false; + } + if (strncmp(topic + 25, "/set/conversion_enabled", 23)) { + return false; + } + uint8_t pin = (topic[4] - '0') * 10 + (topic[5] - '0'); + // Extract the payload + bool enabled = atoi(payload); + // Set conversion enabled + this->setADCConversionEnabled(pin, enabled); + return true; +} + +/** + * @brief Processes a message received on the MQTT topic for setting the state of a DAC. + * @param topic The topic of the message. + * @param payload The payload of the message. + * @param topic_length The length of the topic. + * @note This function is not meant to be called from user code. + * @return True if the message was processed, false otherwise. + */ +bool AnalogIoT::processDACSetStateMessage(char *topic, char *payload, uint8_t topic_length) { + // Topic: dac/<%02d>/set/state + // The first 4 characters are "dac/" + // The length of the topic must be 16 characters + // The last 10 characters must be "/set/state" + // After all these conditions are met, the topic is valid + // Extract the pin number from the topic + if (topic_length != 16) { + return false; + } + if (strncmp(topic, "dac/", 4)) { + return false; + } + if (strncmp(topic + 12, "/set/state", 10)) { + return false; + } + uint8_t pin = (topic[4] - '0') * 10 + (topic[5] - '0'); + // Extract the payload + bool state = atoi(payload); + // Set the state + this->card->setDACState(pin, state); + return true; +} + +/** + * @brief Processes a message received on the MQTT topic for setting the value of a DAC. + * @param topic The topic of the message. + * @param payload The payload of the message. + * @param topic_length The length of the topic. + * @note This function is not meant to be called from user code. + * @return True if the message was processed, false otherwise. + */ +bool AnalogIoT::processDACSetValueMessage(char *topic, char *payload, uint8_t topic_length) { + // Topic: dac/<%02d>/set/value + // The first 4 characters are "dac/" + // The length of the topic must be 16 characters + // The last 10 characters must be "/set/value" + // After all these conditions are met, the topic is valid + // Extract the pin number from the topic + if (topic_length != 16) { + return false; + } + if (strncmp(topic, "dac/", 4)) { + return false; + } + if (strncmp(topic + 12, "/set/value", 10)) { + return false; + } + uint8_t pin = (topic[4] - '0') * 10 + (topic[5] - '0'); + // Extract the payload + uint16_t value = atoi(payload); + // Set the value + this->card->setDACValue(pin, value); + return true; +} + +/** + * @brief Processes a message received on the MQTT topic for requesting the state of all DACs. + * @param topic The topic of the message. + * @param payload The payload of the message. + * @param topic_length The length of the topic. + * @note This function is not meant to be called from user code. + * @return True if the message was processed, false otherwise. + */ +bool AnalogIoT::processRequestStateMessage(char *topic, char *payload, uint8_t topic_length) { + // Topic: requeststate + // The length of the topic must be 12 characters + // After all these conditions are met, the topic is valid + if (topic_length != 12) { + return false; + } + if (strncmp(topic, REQUEST_STATE_TOPIC, 12)) { + return false; + } + // Publish the state of all DACs + this->publishDACs(); + // Publish the state of all ADCs + this->publishADCs(); + return false; +} + +/** + * @brief Subscribes to all MQTT topics used by the AnalogIoT object. + * @note This function is called when the MQTT client connects. + */ +void AnalogIoT::subscribe() { + // There are 4 DACs and 8 ADCs + // DACs: dac/<%02d>/set/state, dac/<%02d>/set/value, dac/publish_enable + // ADCs: adc/<%02d>/set/conversion_interval, adc/<%02d>/set/conversion_enabled + + // Subscribe to all set state topics + char topic[20]; + for (uint8_t i = 0; i < 4; i++) { + sprintf(topic, "dac/%02d/set/state", i); + this->subscribeRelative(topic); + } + // Subscribe to all set value topics + for (uint8_t i = 0; i < 4; i++) { + sprintf(topic, "dac/%02d/set/value", i); + this->subscribeRelative(topic); + } + // Subscribe to all set conversion interval topics + for (uint8_t i = 0; i < 8; i++) { + sprintf(topic, "adc/%02d/set/conversion_interval", i); + this->subscribeRelative(topic); + } + // Subscribe to all set conversion enabled topics + for (uint8_t i = 0; i < 8; i++) { + sprintf(topic, "adc/%02d/set/conversion_enabled", i); + this->subscribeRelative(topic); + } + // Subscribe to publish enable topic + this->subscribeRelative("dac/publish_enable"); +} +void AnalogIoT::loop() { + // Iterate over all ADCs and publish if enabled and interval has passed + uint32_t now = millis(); + for (uint8_t i = 0; i < 8; i++) { + if (this->adc_publish_enabled[i] && now - this->last_adc_publish > this->adc_conversion_interval[i]) { + this->publishADC(i); + this->last_adc_publish = now; + } + } +} + +/** + * @brief Publishes the state of all DACs. + */ +void AnalogIoT::publishReport() { + publishADCs(); + publishDACs(); +} + +/** + * @brief Gets the type of the card. + * @return The type of the card. + */ +uint8_t AnalogIoT::getType() { + return CARD_TYPE_ANALOG; +} + +/** + * @brief Publishes the state of all DACs. + */ +void AnalogIoT::publishDACs() { + for (uint8_t i = 0; i < 4; i++) { + this->publishDAC(i); + } +} + +/** + * @brief Publishes the state of a DAC. + * @param pin The pin of the DAC. + */ +void AnalogIoT::publishDAC(uint8_t pin) { + this->publishDACState(pin); + this->publishDACValue(pin); +} + +/** + * @brief Publishes the state of a DAC. + * @param pin The pin of the DAC. + */ +void AnalogIoT::publishDACState(uint8_t pin) { + char *topic = new char[15]; + sprintf(topic, "dac/%02d/state", pin); + char *payload = new char[2]; + sprintf(payload, "%d", this->card->getDACState(pin)); + this->publishRelative(topic, payload); + delete[] topic; + delete[] payload; +} + +/** + * @brief Publishes the value of a DAC. + * @param pin The pin of the DAC. + */ +void AnalogIoT::publishDACValue(uint8_t pin) { + char *topic = new char[15]; + sprintf(topic, "dac/%02d/value", pin); + char *payload = new char[5]; + sprintf(payload, "%d", this->card->getDACValue(pin)); + this->publishRelative(topic, payload); + delete[] topic; + delete[] payload; +} + +/** + * @brief Publishes the state of a DAC. + * @param pin The pin of the DAC. + */ +void AnalogIoT::handleDACChange(uint8_t pin, uint16_t value) { + this->publishDAC(pin); +} \ No newline at end of file diff --git a/AnalogIoT.hpp b/AnalogIoT.hpp new file mode 100644 index 0000000..1695469 --- /dev/null +++ b/AnalogIoT.hpp @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include + +// MQTT Topics +#define DAC_SET_STATE_TOPIC "/set/state" +#define DAC_SET_VALUE_TOPIC "/set/value" +#define DAC_STATE_TOPIC "/dac/00/state" +#define DAC_VALUE_TOPIC "/dac/00/value" +#define DAC_PUBLISH_ENABLE_TOPIC "/publish_enable" +#define REQUEST_STATE_TOPIC "requeststate" + +/** + * @brief The AnalogIoT class is a class for connecting the Analog Card to the IoT module. + * + * This function allows you to control the Analog Card using MQTT. + * + * @warning You should not instantiate this class directly, instead use ESPMegaIoT's registerCard function. + */ +class AnalogIoT : public IoTComponent { + public: + AnalogIoT(); + ~AnalogIoT(); + bool begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic); + void handleMqttMessage(char *topic, char *payload); + void handleDACChange(uint8_t pin, uint16_t value); + void publishADCs(); + void publishADC(uint8_t pin); + void publishDACs(); + void publishDAC(uint8_t pin); + void publishDACState(uint8_t pin); + void publishDACValue(uint8_t pin); + void setADCsPublishInterval(uint32_t interval); + void setADCsPublishEnabled(bool enabled); + uint8_t registerADCConversionCallback(std::function callback); + void unregisterADCConversionCallback(uint8_t handler); + void setADCConversionInterval(uint8_t pin, uint16_t interval); + void setADCConversionEnabled(uint8_t pin, bool enabled); + bool processADCSetConversionIntervalMessage(char *topic, char *payload, uint8_t topic_length); + bool processADCSetConversionEnabledMessage(char *topic, char *payload, uint8_t topic_length); + bool processDACSetStateMessage(char *topic, char *payload, uint8_t topic_length); + bool processDACSetValueMessage(char *topic, char *payload, uint8_t topic_length); + bool processRequestStateMessage(char *topic, char *payload, uint8_t topic_length); + void publishReport(); + void subscribe(); + void loop(); + uint8_t getType(); + private: + // The index of the next callback to be registered + uint8_t adc_conversion_callback_index = 0; + // We keep track of the length of the topics so we don't have to calculate it every time + uint8_t dac_set_state_length; + uint8_t dac_set_value_length; + uint8_t dac_state_length; + uint8_t dac_value_length; + uint8_t request_state_length; + uint8_t dac_publish_enable_length; + uint32_t last_adc_publish = 0; + AnalogCard *card; + bool adc_publish_enabled[8]; + uint16_t adc_conversion_interval[8]; + uint32_t last_adc_conversion[8]; + std::map> adc_conversion_callbacks; +}; \ No newline at end of file diff --git a/ClimateCard.cpp b/ClimateCard.cpp new file mode 100644 index 0000000..adae7b9 --- /dev/null +++ b/ClimateCard.cpp @@ -0,0 +1,470 @@ +/** + * @file ClimateCard.cpp + * @brief Implementation file for the ClimateCard class. + * + * This file contains the implementation of the ClimateCard class, which represents a climate control card. + * The ClimateCard class provides methods for controlling an air conditioner, reading temperature and humidity + * from sensors, and saving and loading the state to/from FRAM memory. + */ +#include + +/** + * @brief Construct a new ClimateCard object. + * + * @param ir_pin The GPIO pin number of the IR transmitter. + * @param ac The AirConditioner object that represents the air conditioner. + * @param sensor_type The type of the sensor connected to the card. + * @param sensor_pin The GPIO pin number of the sensor. + */ +ClimateCard::ClimateCard(uint8_t ir_pin, AirConditioner ac, uint8_t sensor_type, uint8_t sensor_pin) +{ + this->ir_pin = ir_pin; + this->ac = ac; + this->sensor_type = sensor_type; + this->sensor_pin = sensor_pin; + // Initialize Pointers + this->dht = nullptr; + this->ds18b20 = nullptr; + this->fram = nullptr; + // Initialize Variables + this->fram_address = 0; + this->fram_auto_save = false; + this->state.ac_temperature = 0; + this->state.ac_mode = 0; + this->state.ac_fan_speed = 0; + this->humidity = 0; + this->room_temperature = 0; + // Initialize state + this->state.ac_temperature = 25; + this->state.ac_mode = 0; + this->state.ac_fan_speed = 0; +} + +/** + * @brief Construct a new ClimateCard object. + * + * @param ir_pin The GPIO pin number of the IR transmitter. + * @param ac The AirConditioner object that represents the air conditioner. + * + * @note This constructor can be used when no sensor is connected to the card. + */ +ClimateCard::ClimateCard(uint8_t ir_pin, AirConditioner ac) : ClimateCard(ir_pin, ac, AC_SENSOR_TYPE_NONE, 0) +{ +} + +/** + * @brief The destructor of the ClimateCard class. + */ +ClimateCard::~ClimateCard() +{ + delete dht; + delete ds18b20; + rmt_driver_uninstall(RMT_TX_CHANNEL); +} + +/** + * @brief Initialize the ClimateCard object. + * + * @return true if initialization was successful. + * @return false if initialization failed. + */ +bool ClimateCard::begin() +{ + switch (sensor_type) + { + case AC_SENSOR_TYPE_DHT22: + dht = new DHTNEW(sensor_pin); + break; + case AC_SENSOR_TYPE_DS18B20: + OneWire oneWire(sensor_pin); + ds18b20 = new DS18B20(&oneWire); + break; + } + updateAirConditioner(); + // We are returning here because sending IR signals is not working yet + return true; + if (sensor_pin != 0) + { + // Initialize RMT + gpio_num_t gpio_num = gpio_num_t(ir_pin); + rmt_config_t rmt_tx = RMT_DEFAULT_CONFIG_TX(gpio_num, RMT_TX_CHANNEL); + rmt_tx.clk_div = 80; // 1MHz clock + rmt_config(&rmt_tx); + rmt_driver_install(rmt_tx.channel, 0, 0); + } +} + +/** + * @brief Loop function of the ClimateCard class. + * + * @note When this card is installed in an ESPMega, this function is called automatically by the ESPMega class. + */ +void ClimateCard::loop() +{ + static uint32_t last_sensor_update = 0; + if (millis() - last_sensor_update >= AC_SENSOR_READ_INTERVAL) + { + last_sensor_update = millis(); + updateSensor(); + } +} + +/** + * @brief bind FRAM memory to the ClimateCard object at the specified address. + * + * @note This function must be called before calling loadStateFromFRAM() or saveStateToFRAM(). + * @note This card takes up 3 bytes of FRAM memory. + * + * @param fram The FRAM object. + * @param fram_address The starting address of the card in FRAM memory. + */ +void ClimateCard::bindFRAM(FRAM *fram, uint16_t fram_address) +{ + this->fram = fram; + this->fram_address = fram_address; +} + +/** + * @brief Set whether the state should be automatically saved to FRAM memory. + * + * @note This function has no effect if bindFRAM() has not been called. + * @param autoSave Whether the state should be automatically saved to FRAM memory. + */ +void ClimateCard::setFRAMAutoSave(bool autoSave) +{ + this->fram_auto_save = autoSave; +} + +/** + * @brief Save the state to FRAM memory. + * @note This function has no effect if bindFRAM() has not been called. + */ +void ClimateCard::saveStateToFRAM() +{ + if (fram == nullptr) + return; + fram->write8(fram_address, state.ac_temperature); + fram->write8(fram_address + 1, state.ac_mode); + fram->write8(fram_address + 2, state.ac_fan_speed); +} + +/** + * @brief Load the state from FRAM memory. + * + * @note This function has no effect if bindFRAM() has not been called. + */ +void ClimateCard::loadStateFromFRAM() +{ + if (fram == nullptr) + return; + if (state.ac_temperature > ac.max_temperature) + state.ac_temperature = ac.max_temperature; + else if (state.ac_temperature < ac.min_temperature) + state.ac_temperature = ac.min_temperature; + // If mode is out of range, set to 0 + if (state.ac_mode > ac.modes) + state.ac_mode = 0; + // If fan speed is out of range, set to 0 + if (state.ac_fan_speed > ac.fan_speeds) + state.ac_fan_speed = 0; + updateAirConditioner(); + for (const auto &callback : callbacks) + { + callback.second(this->state.ac_mode, this->state.ac_fan_speed, this->state.ac_temperature); + } +} + +/** + * @brief Set the temperature of the air conditioner. + * + * @param temperature The temperature to set. + * @note If the temperature is out of range, it will be set to its respective maximum or minimum. + */ +void ClimateCard::setTemperature(uint8_t temperature) +{ + // If temperature is out of range, set to its respective maximum or minimum + if (temperature > ac.max_temperature) + temperature = ac.max_temperature; + else if (temperature < ac.min_temperature) + temperature = ac.min_temperature; + this->state.ac_temperature = temperature; + updateAirConditioner(); + if (fram_auto_save) + saveStateToFRAM(); +} + +/** + * @brief Set the mode of the air conditioner. + * + * @note If the mode is out of range, it will be set to 0. + * @param mode The mode to set. + */ +void ClimateCard::setMode(uint8_t mode) +{ + if (mode > ac.modes) + mode = 0; + this->state.ac_mode = mode; + updateAirConditioner(); + if (fram_auto_save) + saveStateToFRAM(); +} + +/** + * @brief Get the name of the current mode. + * @return The name of the current mode. + */ +char *ClimateCard::getModeName() +{ + return (char *)ac.mode_names[state.ac_mode]; +} + +/** + * @brief Get the name of the current fan speed. + * + * @return The name of the current fan speed. + */ +char *ClimateCard::getFanSpeedName() +{ + return (char *)ac.fan_speed_names[state.ac_fan_speed]; +} + +/** + * @brief Set the fan speed of the air conditioner. + * + * @note If the fan speed is out of range, it will be set to 0. + * @param fan_speed The fan speed to set. + */ +void ClimateCard::setFanSpeed(uint8_t fan_speed) +{ + if (fan_speed > ac.fan_speeds) + fan_speed = 0; + this->state.ac_fan_speed = fan_speed; + updateAirConditioner(); + if (fram_auto_save) + saveStateToFRAM(); +} + +/** + * @brief Set fan speed by name. + * + * @param fan_speed_name The name of the fan speed to set. + * @note If the fan speed is not found, the function will not do anything. + */ +void ClimateCard::setFanSpeedByName(const char *fan_speed_name) +{ + for (uint8_t i = 0; i < ac.fan_speeds; i++) + { + if (strcmp(fan_speed_name, ac.fan_speed_names[i]) == 0) + { + setFanSpeed(i); + return; + } + } +} + +/** + * @brief Set mode by name. + * + * @param mode_name The name of the mode to set. + * @note If the mode is not found, the function will not do anything. + */ +void ClimateCard::setModeByName(const char *mode_name) +{ + for (uint8_t i = 0; i < ac.modes; i++) + { + if (strcmp(mode_name, ac.mode_names[i]) == 0) + { + setMode(i); + return; + } + } +} + +/** + * @brief Register a callback function that will be called when the state of the air conditioner changes. + * + * @param callback The callback function to register. + * + * @return uint8_t The handler of the callback function. + */ +uint8_t ClimateCard::registerChangeCallback(std::function callback) +{ + callbacks[callbacks_handler_count] = callback; + return callbacks_handler_count++; +} + +/** + * @brief Get the type of the card. + * + * @return The handler of the callback function. + */ +uint8_t ClimateCard::getType() +{ + return CARD_TYPE_CLIMATE; +} + +/** + * @brief update environmental sensor data. + * + * @note This function is called automatically by the loop() function. + * @note This function has no effect if no sensor is connected to the card. + * @note This function also calls the sensor callbacks. + */ +void ClimateCard::updateSensor() +{ + if (sensor_type == AC_SENSOR_TYPE_NONE) + return; + // Read sensor data and update variables + switch (sensor_type) + { + case AC_SENSOR_TYPE_DHT22: + if (millis() - dht->lastRead() < AC_SENSOR_READ_INTERVAL) + return; + dht->read(); + room_temperature = dht->getTemperature(); + humidity = dht->getHumidity(); + break; + case AC_SENSOR_TYPE_DS18B20: + ds18b20->requestTemperatures(); + uint32_t start = millis(); + while (!ds18b20->isConversionComplete()) + { + if (millis() - start >= AC_SENSOR_READ_TIMEOUT) + { + return; + } + } + room_temperature = ds18b20->getTempC(); + break; + } + for (const auto &callback : sensor_callbacks) + { + callback.second(room_temperature, humidity); + } +} + +/** + * @brief Update the air conditioner state to match the state of the card. + * + * @warning This function is not working yet. + */ +void ClimateCard::updateAirConditioner() +{ + // // The IR Transmissions are not working yet so we just return + // const uint16_t* ir_code_ptr = nullptr; + // size_t itemCount = (*(this->ac.getInfraredCode))(this->state.ac_mode, this->state.ac_fan_speed, this->state.ac_temperature, &ir_code_ptr); + + // if (ir_code_ptr == nullptr) + // return; + + // rmt_item32_t items[itemCount]; + // // Convert IR timing array to RMT items + // for (size_t i = 0; i < itemCount; i+=2) + // { + // items[i].level0 = 1; + // items[i].duration0 = ir_code_ptr[i]; + // items[i].level1 = 0; + // items[i].duration1 = ir_code_ptr[i+1]; + // } + // // Send IR signal + // rmt_write_items(RMT_TX_CHANNEL, items, itemCount, true); + // rmt_wait_tx_done(RMT_TX_CHANNEL, portMAX_DELAY); + // // Publish state + for (const auto &callback : callbacks) + { + callback.second(this->state.ac_mode, this->state.ac_fan_speed, this->state.ac_temperature); + } +} + +/** + * @brief Get the type of the sensor connected to the card. + * + * @return The type of the sensor connected to the card. + */ +uint8_t ClimateCard::getSensorType() +{ + return sensor_type; +} + +/** + * @brief Get the room temperature in degrees Celsius. + * + * @return The room temperature. + */ +float ClimateCard::getRoomTemperature() +{ + return room_temperature; +} + +/** + * @brief Get the humidity in percent. + * + * @return The humidity. + */ +float ClimateCard::getHumidity() +{ + return humidity; +} + +/** + * @brief Get the temperature of the air conditioner. + * + * @return The temperature of the air conditioner. + */ +uint8_t ClimateCard::getTemperature() +{ + return state.ac_temperature; +} + +/** + * @brief Get the mode of the air conditioner. + * + * @return The mode of the air conditioner. + */ +uint8_t ClimateCard::getMode() +{ + return state.ac_mode; +} + +/** + * @brief Get the fan speed of the air conditioner. + * + * @return The fan speed of the air conditioner. + */ +uint8_t ClimateCard::getFanSpeed() +{ + return state.ac_fan_speed; +} + +/** + * @brief Register a callback function that will be called when the sensor data changes. + * + * @param callback The callback function to register. + * + * @return The handler of the callback function + */ +uint8_t ClimateCard::registerSensorCallback(std::function callback) +{ + sensor_callbacks[sensor_callbacks_handler_count] = callback; + return sensor_callbacks_handler_count++; +} + +/** + * @brief Unregister a callback function. + * + * @param handler The handler of the callback function to unregister. + */ +void ClimateCard::unregisterChangeCallback(uint8_t handler) +{ + callbacks.erase(handler); +} + +/** + * @brief Unregister a sensor callback function. + * + * @param handler The handler of the callback function to unregister. + */ +void ClimateCard::unregisterSensorCallback(uint8_t handler) +{ + sensor_callbacks.erase(handler); +} \ No newline at end of file diff --git a/ClimateCard.hpp b/ClimateCard.hpp new file mode 100644 index 0000000..f4fa416 --- /dev/null +++ b/ClimateCard.hpp @@ -0,0 +1,123 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#define RMT_TX_CHANNEL RMT_CHANNEL_0 + +#define CARD_TYPE_CLIMATE 0x03 + +#define AC_SENSOR_TYPE_NONE 0x00 +#define AC_SENSOR_TYPE_DHT22 0x01 +#define AC_SENSOR_TYPE_DS18B20 0x02 + +#define AC_SENSOR_READ_INTERVAL 5000 +#define AC_SENSOR_READ_TIMEOUT 250 + +/** + * @brief The struct is used to store the state of the air conditioner + * + * @note This struct is stored in FRAM if it is used + * @note This struct is 3 bytes long + */ +struct ClimateCardData { + uint8_t ac_temperature; ///< Temperature of the air conditioner + uint8_t ac_mode; ///< Mode of the air conditioner + uint8_t ac_fan_speed;///< Fan speed of the air conditioner +}; + +/** + * @brief This struct is used to store information about an air conditioner + */ +struct AirConditioner { + uint8_t max_temperature; ///< Maximum temperature + uint8_t min_temperature; ///< Minimum temperature + uint8_t modes; ///< Number of modes + const char **mode_names; ///< Names of modes in the form of an array of strings + uint8_t fan_speeds; ///< Number of fan speeds + const char **fan_speed_names; ///< Names of fan speeds in the form of an array of strings + /** + * @brief Function to get IR code + * + * @param mode Mode of the air conditioner + * @param fan_speed Fan speed of the air conditioner + * @param temperature Temperature of the air conditioner + * @param code Pointer to the IR code array + * + * @return Size of the IR code array + */ + size_t (*getInfraredCode)(uint8_t, uint8_t, uint8_t, const uint16_t**); +}; +// This requires 3 bytes of FRAM + +/** + * @brief The ClimateCard class is a class for controlling an air conditioner + * + * This class allows you to control an air conditioner using an IR LED. + * It is meant to be used with the ESPMega Climate Card. + * + * @note You can also use a DHT22 or DS18B20 temperature sensor to get the room temperature (and humidity if using a DHT22). Although, this is optional. + * + */ +class ClimateCard : public ExpansionCard { + public: + ClimateCard(uint8_t ir_pin, AirConditioner ac, uint8_t sensor_type, uint8_t sensor_pin); + ClimateCard(uint8_t ir_pin, AirConditioner ac); + ~ClimateCard(); + bool begin(); + void loop(); + void bindFRAM(FRAM *fram, uint16_t fram_address); + void setFRAMAutoSave(bool autoSave); + void saveStateToFRAM(); + void loadStateFromFRAM(); + void setTemperature(uint8_t temperature); + uint8_t getTemperature(); + void setMode(uint8_t mode); + void setModeByName(const char* mode_name); + uint8_t getMode(); + char* getModeName(); + void setFanSpeed(uint8_t fan_speed); + void setFanSpeedByName(const char* fan_speed_name); + uint8_t getFanSpeed(); + char* getFanSpeedName(); + float getRoomTemperature(); + float getHumidity(); + uint8_t getSensorType(); + uint8_t registerChangeCallback(std::function callback); + uint8_t registerSensorCallback(std::function callback); + void unregisterChangeCallback(uint8_t handler); + void unregisterSensorCallback(uint8_t handler); + uint8_t getType(); + private: + // Sensor objects + // We use pointers here because we don't know which sensor will be used + DHTNEW *dht; + DS18B20 *ds18b20; + // Callbacks + uint8_t callbacks_handler_count = 0; + uint8_t sensor_callbacks_handler_count = 0; + std::map> callbacks; + std::map> sensor_callbacks; + // Update functions + void updateSensor(); + void updateAirConditioner(); + // IR variables + uint8_t ir_pin; + // Air conditioner variables + AirConditioner ac; + ClimateCardData state; + // Sensor variables + uint8_t sensor_type; + uint8_t sensor_pin; + float humidity; + float room_temperature; + // FRAM variables + FRAM *fram; + uint16_t fram_address; + bool fram_auto_save; + uint16_t* getIrIndex(uint8_t mode, uint8_t fan_speed, uint8_t temperature); +}; diff --git a/ClimateIoT.cpp b/ClimateIoT.cpp new file mode 100644 index 0000000..e6e179c --- /dev/null +++ b/ClimateIoT.cpp @@ -0,0 +1,257 @@ +#include + + +ClimateIoT::ClimateIoT() { + +} + +/** + * @brief Destructor for the ClimateIoT class. + */ +ClimateIoT::~ClimateIoT() { + // Destructor implementation +} + +/** + * @brief Initializes the ClimateIoT component. + * + * This function sets the MQTT client, base topic, card ID, and card pointer. + * It also registers the sensor and air conditioner update callbacks. + * + * @param card_id The ID of the expansion card. + * @param card A pointer to the ExpansionCard object. + * @param mqtt A pointer to the PubSubClient object. + * @param base_topic The base topic for MQTT communication. + * @return True if the initialization is successful, false otherwise. + */ +bool ClimateIoT::begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic) { + this->mqtt = mqtt; + this->base_topic = base_topic; + this->card_id = card_id; + this->card = (ClimateCard *)card; + // Reister Callbacks + auto bindedSensorCallback = std::bind(&ClimateIoT::handleSensorUpdate, this, std::placeholders::_1, std::placeholders::_2); + this->card->registerSensorCallback(bindedSensorCallback); + auto bindedAirConditionerCallback = std::bind(&ClimateIoT::handleAirConditionerUpdate, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + this->card->registerChangeCallback(bindedAirConditionerCallback); + ESP_LOGD("ClimateIoT", "Climate IoT Component initialized"); + return true; +} + +/** + * @brief Handles MQTT messages for the ClimateIoT component. + * + * @param topic The trimmed topic of the MQTT message. + * @param payload The null-terminated payload of the MQTT message. + */ +void ClimateIoT::handleMqttMessage(char *topic, char *payload) { + uint8_t topic_length = strlen(topic); + if (this->processSetTemperatureMessage(topic, payload, topic_length)) + return; + if (this->processSetModeMessage(topic, payload, topic_length)) + return; + if (this->processSetFanSpeedMessage(topic, payload, topic_length)) + return; + if (this->processRequestStateMessage(topic, payload, topic_length)) + return; +} + +/** + * @brief Publishes the temperature of the air conditioner to the MQTT broker. + */ +void ClimateIoT::publishClimateTemperature() { + char payload[5]; + itoa(this->card->getTemperature(), payload, 10); + this->publishRelative(AC_TEMPERATURE_REPORT_TOPIC, payload); +} + +/** + * @brief Publishes the mode of the air conditioner to the MQTT broker. + */ +void ClimateIoT::publishClimateMode() { + this->publishRelative(AC_MODE_REPORT_TOPIC, this->card->getModeName()); +} + +/** + * @brief Publishes the fan speed of the air conditioner to the MQTT broker. + */ +void ClimateIoT::publishClimateFanSpeed() { + this->publishRelative(AC_FAN_SPEED_REPORT_TOPIC, this->card->getFanSpeedName()); +} + +/** + * @brief Publishes the temperature and humidity of the room to the MQTT broker. + */ +void ClimateIoT::publishSensor() { + this->publishRoomTemperature(); + this->publishHumidity(); +} + +/** + * @brief Publishes the climate data (temperature, mode, fan speed) to the MQTT broker. + */ +void ClimateIoT::publishClimate() { + this->publishClimateTemperature(); + this->publishClimateMode(); + this->publishClimateFanSpeed(); +} + +/** + * @brief Publishes the room temperature to the MQTT broker. + */ +void ClimateIoT::publishRoomTemperature() { + if (this->card->getSensorType() == AC_SENSOR_TYPE_NONE ) { + return; + } + char payload[5]; + itoa(this->card->getRoomTemperature(), payload, 10); + this->publishRelative(AC_ROOM_TEMPERATURE_REPORT_TOPIC, payload); +} + +/** + * @brief Publishes the humidity of the room to the MQTT broker. + */ +void ClimateIoT::publishHumidity() { + if (this->card->getSensorType() == AC_SENSOR_TYPE_DHT22) { + char payload[5]; + itoa(this->card->getHumidity(), payload, 10); + this->publishRelative(AC_HUMIDITY_REPORT_TOPIC, payload); + } +} + +/** + * @brief Handle Air Conditioner state change. + * + * @note This function is called by the underlying ClimateCard object and is not meant to be called manually. + * + * @param temperature Temperature of the air conditioner + * @param mode Mode of the air conditioner + * @param fan_speed Fan speed of the air conditioner + */ +void ClimateIoT::handleStateChange(uint8_t temperature, uint8_t mode, uint8_t fan_speed) { + this->publishClimate(); +} + +/** + * @brief Publishes the climate and sensor data to the MQTT broker. + */ +void ClimateIoT::publishReport() { + this->publishClimate(); + this->publishSensor(); +} + +/** + * @brief Subscribes to MQTT topics. + */ +void ClimateIoT::subscribe() { + ESP_LOGD("ClimateIoT", " topics"); + this->subscribeRelative(AC_TEMPERATURE_SET_TOPIC); + this->subscribeRelative(AC_MODE_SET_TOPIC); + this->subscribeRelative(AC_FAN_SPEED_SET_TOPIC); + ESP_LOGD("ClimateIoT", "Subscribed to topics"); +} + +/** + * @brief The loop function for the ClimateIoT component. + * + * @note This function does nothing. + */ +void ClimateIoT::loop() { + +} + +/** + * @brief Returns the type of the expansion card. + * + * @return The type of the expansion card. + */ +uint8_t ClimateIoT::getType() { + return CARD_TYPE_CLIMATE; +} + +/** + * @brief Processes the set temperature MQTT message. + * + * @param topic The trimmed topic of the MQTT message. + * @param payload The null-terminated payload of the MQTT message. + * @param topic_length The length of the topic. + * @return True if the message is processed, false otherwise. + */ +bool ClimateIoT::processSetTemperatureMessage(char *topic, char *payload, uint8_t topic_length) { + if (!strcmp(topic, AC_TEMPERATURE_SET_TOPIC)) { + uint8_t temperature = atoi(payload); + this->card->setTemperature(temperature); + return true; + } + return false; +} + +/** + * @brief Processes the set mode MQTT message. + * + * @param topic The trimmed topic of the MQTT message. + * @param payload The null-terminated payload of the MQTT message. + * @param topic_length The length of the topic. + * @return True if the message is processed, false otherwise. + */ +bool ClimateIoT::processSetModeMessage(char *topic, char *payload, uint8_t topic_length) { + if (!strcmp(topic, AC_MODE_SET_TOPIC)) { + this->card->setModeByName(payload); + return true; + } + return false; +} + +/** + * @brief Processes the set fan speed MQTT message. + * + * @param topic The trimmed topic of the MQTT message. + * @param payload The null-terminated payload of the MQTT message. + * @param topic_length The length of the topic. + * @return True if the message is processed, false otherwise. + */ +bool ClimateIoT::processSetFanSpeedMessage(char *topic, char *payload, uint8_t topic_length) { + if (!strcmp(topic, AC_FAN_SPEED_SET_TOPIC)) { + this->card->setFanSpeedByName(payload); + } + return false; +} + +/** + * @brief Processes the request state MQTT message. + * + * @param topic The trimmed topic of the MQTT message. + * @param payload The null-terminated payload of the MQTT message. + * @param topic_length The length of the topic. + * @return True if the message is processed, false otherwise. + */ +bool ClimateIoT::processRequestStateMessage(char *topic, char *payload, uint8_t topic_length) { + if (!strcmp(topic, AC_REQUEST_STATE_TOPIC)) { + this->publishReport(); + return true; + } + return false; +} + +/** + * @brief This function is a callback function registered with the Climate card to be called when the sensor data is updated. + * + * @param temperature The room temperature. + * @param humidity The room humidity. + * + * @note The temperature and humidity are not used in this function but are required by the ClimateCard class to match the signature of the callback function. + */ +void ClimateIoT::handleSensorUpdate(float temperature, float humidity) { + this->publishSensor(); +} + +/** + * @brief This function is a callback function registered with the Climate card to be called when the air conditioner state is updated. + * + * @param mode The mode of the air conditioner. + * @param fan_speed The fan speed of the air conditioner. + * @param temperature The temperature of the air conditioner. + */ +void ClimateIoT::handleAirConditionerUpdate(uint8_t mode, uint8_t fan_speed, uint8_t temperature) { + this->publishClimate(); +} \ No newline at end of file diff --git a/ClimateIoT.hpp b/ClimateIoT.hpp new file mode 100644 index 0000000..b376afd --- /dev/null +++ b/ClimateIoT.hpp @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include + +// MQTT Topics +#define AC_MODE_REPORT_TOPIC "mode" +#define AC_MODE_SET_TOPIC "set/mode" +#define AC_TEMPERATURE_REPORT_TOPIC "temperature" +#define AC_TEMPERATURE_SET_TOPIC "set/temperature" +#define AC_FAN_SPEED_REPORT_TOPIC "fan_speed" +#define AC_FAN_SPEED_SET_TOPIC "set/fan_speed" +#define AC_ROOM_TEMPERATURE_REPORT_TOPIC "room_temperature" +#define AC_HUMIDITY_REPORT_TOPIC "humidity" +#define AC_REQUEST_STATE_TOPIC "request_state" + +/** + * @brief The ClimateIoT class is a class for connecting the Climate Card to the IoT module. + * + * This function allows you to control the Climate Card using MQTT. + * + * @warning You should not instantiate this class directly, instead use ESPMegaIoT's registerCard function. + */ +class ClimateIoT : public IoTComponent { + public: + ClimateIoT(); + ~ClimateIoT(); + bool begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic); + void handleMqttMessage(char *topic, char *payload); + void publishClimate(); + void publishClimateTemperature(); + void publishClimateMode(); + void publishClimateFanSpeed(); + void publishSensor(); + void publishRoomTemperature(); + void publishHumidity(); + void handleStateChange(uint8_t temperature, uint8_t mode, uint8_t fan_speed); + void handleSensorUpdate(float temperature, float humidity); + void handleAirConditionerUpdate(uint8_t mode, uint8_t fan_speed, uint8_t temperature); + void publishReport(); + void subscribe(); + void loop(); + uint8_t getType(); + private: + ClimateCard *card; + bool processSetTemperatureMessage(char *topic, char *payload, uint8_t topic_length); + bool processSetModeMessage(char *topic, char *payload, uint8_t topic_length); + bool processSetFanSpeedMessage(char *topic, char *payload, uint8_t topic_length); + bool processRequestStateMessage(char *topic, char *payload, uint8_t topic_length); +}; \ No newline at end of file diff --git a/DigitalInputCard.cpp b/DigitalInputCard.cpp new file mode 100644 index 0000000..86ad5a1 --- /dev/null +++ b/DigitalInputCard.cpp @@ -0,0 +1,302 @@ +#include + +/** + * @brief Create a new Digital Input Card object with the specified address + * @note If you are using the ESPMegaI/O board, you should use the dip switch constructor + * + * @param address_a The ESPMegaI/O address of bank A + * @param address_b The ESPMegaI/O address of bank B + */ +DigitalInputCard::DigitalInputCard(uint8_t address_a, uint8_t address_b) : callbacks() +{ + this->address_a = address_a; + this->address_b = address_b; + this->callbacks_handler_index = 0; +} + +/** + * @brief Create a new Digital Input Card object with the specified position on the dip switch + * + * @note The bit 0 are at the left of the dip switch + * + * @warning There are 6 switches on the dip switch, 3 for bank A and 3 for bank B, They should be unique for each bank accross all the cards + * + * @param bit0 The position of the first switch on the dip switch + * @param bit1 The position of the second switch on the dip switch + * @param bit2 The position of the third switch on the dip switch + * @param bit3 The position of the fourth switch on the dip switch + * @param bit4 The position of the fifth switch on the dip switch + * @param bit5 The position of the sixth switch on the dip switch + */ +DigitalInputCard::DigitalInputCard(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4, bool bit5) +{ + this->address_a = 0x20; + this->address_b = 0x20; + this->inputBufferA = 0; + this->inputBufferB = 0; + if (bit0) + this->address_a += 1; + if (bit1) + this->address_a += 2; + if (bit2) + this->address_a += 4; + if (bit3) + this->address_b += 1; + if (bit4) + this->address_b += 2; + if (bit5) + this->address_b += 4; +} + +/** + * @brief Initialize the Digital Input Card + * + * @return True if the initialization is successful, false otherwise + */ +bool DigitalInputCard::begin() +{ + this->inputBankA = PCF8574(this->address_a); + this->inputBankB = PCF8574(this->address_b); + if (!this->inputBankA.begin()) { + ESP_LOGE("DigitalInputCard", "Input Card ERROR: Failed to install input bank A"); + return false; + } + if (!this->inputBankB.begin()) { + ESP_LOGE("DigitalInputCard", "Input Card ERROR: Failed to install input bank B"); + return false; + } + // Set the debounce time for all pins to 50ms + for (int i = 0; i < 16; i++) + { + this->debounceTime[i] = 50; + this->lastDebounceTime[i] = 0; + } + // Initialize the pin map to the default values + for (int i = 0; i < 16; i++) + { + this->pinMap[i] = i; + this->virtualPinMap[i] = i; + } + return true; +} + +/** + * @brief Read the input from the specified pin, always refresh the input buffers + * + * @param pin The pin to read from + * @return True if the pin is HIGH, false if the pin is LOW + */ +bool DigitalInputCard::digitalRead(uint8_t pin) +{ + return this->digitalRead(pin, true); +} + +/** + * @brief Read the input from the specified pin, also refresh the input buffers if refresh is true + * + * @param pin The pin to read from + * @param refresh If true, the input buffers will be refreshed before reading the pin + * @return True if the pin is HIGH, false if the pin is LOW + */ +bool DigitalInputCard::digitalRead(uint8_t pin, bool refresh) +{ + pin = pinMap[pin]; + // First check if the pin is in bank A or B + if (pin >= 0 && pin <= 7) + { + // Refresh the input buffers if refresh is true + if (refresh) + refreshInputBankA(); + // Extract the bit from the buffer + return ((inputBufferA >> (7 - pin)) & 1); + } + else if (pin >= 8 && pin <= 15) + { + // Refresh the input buffers if refresh is true + if (refresh) + refreshInputBankB(); + // Extract the bit from the buffer + return ((inputBufferB >> (15 - pin)) & 1); + } + return 255; +} + +/** + * @brief Check if the specified pin changed since the last call to this function + * + * @note This function compares the current input buffer with the previous input buffer to detect changes + * + * @param pin The pin to check + * @param currentBuffer The current input buffer + * @param previousBuffer The previous input buffer + */ +void DigitalInputCard::handlePinChange(int pin, uint8_t ¤tBuffer, uint8_t &previousBuffer) +{ + // Get the index of the pin in the pin map + uint8_t virtualPin = virtualPinMap[pin]; + // Handle Bank A + if (((previousBuffer >> (7 - pin)) & 1) != ((currentBuffer >> (7 - pin)) & 1)) + { + if (millis() - lastDebounceTime[pin] > debounceTime[pin]) + { + lastDebounceTime[pin] = millis(); + previousBuffer ^= (-((currentBuffer >> (7 - pin)) & 1) ^ previousBuffer) & (1UL << (7 - pin)); + for(const auto& callback : callbacks) + callback.second(virtualPin, ((currentBuffer >> (7 - pin)) & 1)); + } + } + // Handle Bank B + if (((previousBuffer >> (15 - pin)) & 1) != ((currentBuffer >> (15 - pin)) & 1)) + { + if (millis() - lastDebounceTime[pin] > debounceTime[pin]) + { + lastDebounceTime[pin] = millis(); + previousBuffer ^= (-((currentBuffer >> (15 - pin)) & 1) ^ previousBuffer) & (1UL << (15 - pin)); + for (const auto& callback : callbacks) + callback.second(virtualPin, ((currentBuffer >> (15 - pin)) & 1)); + } + } +} + +/** + * @brief A loop to refresh the input buffers and check for pin changes + * + * @note Although this function can be called in the main loop, it is recommended install the card in ESPMega to automatically manage the loop + */ +// Preform a loop to refresh the input buffers +void DigitalInputCard::loop() +{ + // Refresh the input buffers + refreshInputBankA(); + refreshInputBankB(); + // Iterate over all pins and check if they changed + for (int i = 0; i < 16; i++) + { + // Check which bank the pin is in + if (i < 8) + { + handlePinChange(i, inputBufferA, previousInputBufferA); + } + else if (i >= 8 && i <= 15) + { + handlePinChange(i, inputBufferB, previousInputBufferB); + } + } +} + +/** + * @brief Get the input buffer for bank A (the first 8 pins) + * + * @return The input buffer for bank A where the first bit is the first pin and the last bit is the last pin + */ +uint8_t DigitalInputCard::getInputBufferA() +{ + // Rearrange the bits to match the pin map + uint8_t inputBufferA_rearranged = 0; + for (int i = 0; i < 8; i++) + { + inputBufferA_rearranged |= ((inputBufferA >> i) & 1) << (7 - i); + } + return inputBufferA_rearranged; +} + +/** + * @brief Get the input buffer for bank B (the last 8 pins) + * + * @return The input buffer for bank B where the first bit is the first pin and the last bit is the last pin + */ +uint8_t DigitalInputCard::getInputBufferB() +{ + // Rearrange the bits to match the pin map + uint8_t inputBufferB_rearranged = 0; + for (int i = 0; i < 8; i++) + { + inputBufferB_rearranged |= ((inputBufferB >> i) & 1) << (7 - i); + } + return inputBufferB_rearranged; +} + +/** + * @brief Register a callback function to be called when a pin changes + * + * @param callback The callback function to be called + * @return The handler of the callback function + */ +uint8_t DigitalInputCard::registerCallback(std::function callback) +{ + callbacks[this->callbacks_handler_index] = callback; + return this->callbacks_handler_index++; +} + +/** + * @brief Read the input state from the input ic and store it in the input buffer for bank A + */ +void DigitalInputCard::refreshInputBankA() +{ + inputBufferA = inputBankA.read8(); +} + +/** + * @brief Read the input state from the input ic and store it in the input buffer for bank B + */ +void DigitalInputCard::refreshInputBankB() +{ + inputBufferB = inputBankB.read8(); +} + +/** + * @brief Set the debounce time for the specified pin + * + * Debounce is the time in milliseconds that the pin should be stable before the callback function is called + * This is useful to prevent false triggers when the input is noisy + * An example of this is when the input is connected to a mechanical switch + * + * @param pin The pin to set the debounce time for + * @param debounceTime The debounce time in milliseconds + */ +void DigitalInputCard::setDebounceTime(uint8_t pin, uint32_t debounceTime) +{ + pin = pinMap[pin]; + this->debounceTime[pin] = debounceTime; +} + +/** + * @brief Unregister a callback function + * + * @param handler The handler of the callback function to unregister + */ +void DigitalInputCard::unregisterCallback(uint8_t handler) +{ + callbacks.erase(handler); +} + +/** + * @brief Load the pin map for the card + * + * A pin map is an array of 16 elements that maps the physical pins to virtual pins + * The virtual pins are the pins that are used in the callback functions and are used for all the functions in this class + * The physical pins are the pins on the Input IC, This can be found on the schematic of the ESPMegaI/O board + * This function is useful if you want to change the number identification of the pins to match your project needs + * + * @param pinMap The pin map to load + */ +void DigitalInputCard::loadPinMap(uint8_t pinMap[16]) +{ + for (int i = 0; i < 16; i++) + { + // Load the pin map (physical pin to virtual pin) + this->pinMap[i] = pinMap[i]; + // Load the virtual pin map (virtual pin to physical pin) + this->virtualPinMap[pinMap[i]] = i; + } +} + +/** + * @brief Get the type of the card + * + * @return The type of the card + */ +uint8_t DigitalInputCard::getType() +{ + return CARD_TYPE_DIGITAL_INPUT; +} \ No newline at end of file diff --git a/DigitalInputCard.hpp b/DigitalInputCard.hpp new file mode 100644 index 0000000..a140029 --- /dev/null +++ b/DigitalInputCard.hpp @@ -0,0 +1,66 @@ +#pragma once +#include +#include +#include + +// Card Type +#define CARD_TYPE_DIGITAL_INPUT 0x01 + +/** + * @brief ESPMegaPRO Digital Input Card + * + * This class represents the ESPMegaPRO Digital Input Card. + * It allows you to read the state of the digital inputs from the ESPMegaPRO Digital Input Card. + * It also allows you to register callback functions to be called when a pin changes. + * The callback function also support debouncing. + * + */ +class DigitalInputCard : public ExpansionCard { + public: + // Instantiate the card with the specified address + DigitalInputCard(uint8_t address_a, uint8_t address_b); + // Instantiate the card with the specified position on the dip switch + DigitalInputCard(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4, bool bit5); + // Initialize the card + bool begin(); + // Refresh and Read the input from the specified pin, always refresh the input buffers + bool digitalRead(uint8_t pin); + // Read the input from the specified pin, also refresh the input buffers if refresh is true + bool digitalRead(uint8_t pin, bool refresh); + // Preform a loop to refresh the input buffers + void loop(); + // Get the input buffer for bank A + uint8_t getInputBufferA(); + // Get the input buffer for bank B + uint8_t getInputBufferB(); + // Set the debounce time for the specified pin + void setDebounceTime(uint8_t pin, uint32_t debounceTime); + // Register a callback function to be called when a pin changes + uint8_t registerCallback(std::function callback); + // Unregister the callback function + void unregisterCallback(uint8_t handler); + // Load a new pin map + void loadPinMap(uint8_t pinMap[16]); + // Get type of card + uint8_t getType(); + private: + PCF8574 inputBankA; + PCF8574 inputBankB; + uint8_t address_a; + uint8_t address_b; + uint8_t inputBufferA; + uint8_t inputBufferB; + uint8_t previousInputBufferA; + uint8_t previousInputBufferB; + uint32_t debounceTime[16]; + uint32_t lastDebounceTime[16]; + // A map of the physical pin to the virtual pin + uint8_t pinMap[16]; + // A map of the virtual pin to the physical pin + uint8_t virtualPinMap[16]; + uint8_t callbacks_handler_index = 0; + std::map> callbacks; + void refreshInputBankA(); + void refreshInputBankB(); + void handlePinChange(int pin, uint8_t& currentBuffer, uint8_t& previousBuffer); +}; \ No newline at end of file diff --git a/DigitalInputIoT.cpp b/DigitalInputIoT.cpp new file mode 100644 index 0000000..7b7a6b0 --- /dev/null +++ b/DigitalInputIoT.cpp @@ -0,0 +1,120 @@ +#include + + +/** + * @brief Initializes the DigitalInputIoT object. + * + * This function sets the necessary parameters for the DigitalInputIoT object, such as the card ID, expansion card, MQTT client, and base topic. + * It also enables the publishing of digital input values and registers a callback function for handling value changes. + * + * @note Although this function can be called in the main program, it is recommended to use ESPMegaIoT::registerCard() to automatically manage the instantiation and initialization of this component. + * + * @param card_id The ID of the card. + * @param card Pointer to the DigitalInputCard object. + * @param mqtt Pointer to the PubSubClient object. + * @param base_topic The base topic for MQTT communication. + * @return True if the initialization is successful, false otherwise. + */ +bool DigitalInputIoT::begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic) { + this->card = (DigitalInputCard *)card; + this->card_id = card_id; + this->mqtt = mqtt; + this->base_topic = base_topic; + this->setDigitalInputsPublishEnabled(true); + this->card->registerCallback(std::bind(&DigitalInputIoT::handleValueChange, this, std::placeholders::_1, std::placeholders::_2)); + return true; + +} + +/** + * @brief Subscribes to the MQTT topics for the DigitalInputIoT component. + */ +void DigitalInputIoT::subscribe() { + this->subscribeRelative(PUBLISH_ENABLE_TOPIC); +} + +/** + * @brief Handles MQTT messages for the DigitalInputIoT component. + * + * @param topic The trimmed topic of the MQTT message. + * @param payload The null-terminated payload of the MQTT message. + */ +void DigitalInputIoT::handleMqttMessage(char *topic, char *payload) { + // payload is char '0' or '1' + if (!strcmp(topic, PUBLISH_ENABLE_TOPIC)) { + if (payload[0] == '1') { + this->setDigitalInputsPublishEnabled(true); + } else { + this->setDigitalInputsPublishEnabled(false); + } + } +} + +/** + * @brief Publish all digital inputs to the MQTT broker. + */ +void DigitalInputIoT::publishDigitalInputs() { + if (!this->digital_inputs_publish_enabled) { + return; + } + for (int i = 0; i < 16; i++) { + this->publishDigitalInput(i); + } +} + +/** + * @brief Set if the digital inputs should be published to the MQTT broker. + * + * @param enabled True if the digital inputs should be published, false otherwise. + */ +void DigitalInputIoT::setDigitalInputsPublishEnabled(bool enabled) { + this->digital_inputs_publish_enabled = enabled; + if (enabled) { + this->publishDigitalInputs(); + } +} + +/** + * @brief Handles a value change for a digital input. + * + * @note This function is registered as a callback function for the DigitalInputCard object. + * + * @param pin The pin that changed. + * @param value The new value of the pin. + */ +void DigitalInputIoT::handleValueChange(uint8_t pin, uint8_t value) { + if (this->digital_inputs_publish_enabled) { + this->publishDigitalInput(pin); + } + +} + +/** + * @brief Publish all inputs to the MQTT Broker + * + * @note This function is overriden from the IoTComponent class and is called by ESPMegaIoT. + * + * @param pin The pin to publish. + */ +void DigitalInputIoT::publishReport() { + this->publishDigitalInputs(); +} +uint8_t DigitalInputIoT::getType() { + return CARD_TYPE_DIGITAL_INPUT; +} + +/** + * @brief Publish a digital input to the MQTT broker. + * + * @param pin The pin to publish. + */ +void DigitalInputIoT::publishDigitalInput(uint8_t pin) { + char topic[20] = {0}; + char payload[20] = {0}; + topic[0] = pin/10 + '0'; + topic[1] = pin%10 + '0'; + topic[2] = '\0'; + payload[0] = this->card->digitalRead(pin, false) + '0'; + payload[1] = '\0'; + this->publishRelative(topic, payload); +} \ No newline at end of file diff --git a/DigitalInputIoT.hpp b/DigitalInputIoT.hpp new file mode 100644 index 0000000..978135f --- /dev/null +++ b/DigitalInputIoT.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +// MQTT Topics +#define PUBLISH_ENABLE_TOPIC "publish_enable" + +/** + * @brief The DigitalInputIoT class is a class for connecting the Digital Input Card to the IoT module. + * + * This function allows you to control the Digital Input Card using MQTT. + * + * @warning You should not instantiate this class directly, instead use ESPMegaIoT's registerCard function. + */ +class DigitalInputIoT : public IoTComponent { + public: + bool begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic); + void handleMqttMessage(char *topic, char *payload); + void publishDigitalInputs(); + void publishDigitalInput(uint8_t pin); + void setDigitalInputsPublishEnabled(bool enabled); + void handleValueChange(uint8_t pin, uint8_t value); + void publishReport(); + void subscribe(); + uint8_t getType(); + private: + bool digital_inputs_publish_enabled = false; + DigitalInputCard *card; +}; \ No newline at end of file diff --git a/DigitalOutputCard.cpp b/DigitalOutputCard.cpp new file mode 100644 index 0000000..2a7b616 --- /dev/null +++ b/DigitalOutputCard.cpp @@ -0,0 +1,309 @@ +#include + +/** + * @brief Create a new Digital Output Card object with the specified address + * + * @note If you are using the ESPMegaI/O board, you should use the dip switch constructor + * + * @param address The ESPMegaI/O address of the card + */ +DigitalOutputCard::DigitalOutputCard(uint8_t address) : change_callbacks(){ + this->address = address; + // load default pin map + for (int i = 0; i < 16; i++) { + this->pinMap[i] = i; + this->virtualPinMap[i] = i; + } + this->framBinded = false; + this->callbacks_handler_index = 0; +} + +/** + * @brief Create a new Digital Output Card object with the specified position on the dip switch + * + * @note The bit 0 are at the left of the dip switch + * + * @warning There are 5 switches on the dip switch, they should be unique accross all the cards + * + * @param bit0 The position of the first switch on the dip switch + * @param bit1 The position of the second switch on the dip switch + * @param bit2 The position of the third switch on the dip switch + * @param bit3 The position of the fourth switch on the dip switch + * @param bit4 The position of the fifth switch on the dip switch + */ +DigitalOutputCard::DigitalOutputCard(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4) : + DigitalOutputCard(0x20+bit0+bit1*2+bit2*4+bit3*8+bit4*16) +{ + + +} + +/** + * @brief Initialize the Digital Output Card + * + * @note Although this function can be called inside the main program, it is recommended to use ESPMegaPRO::installCard() instead + * + * @return True if the initialization is successful, false otherwise + */ +bool DigitalOutputCard::begin() { + this->pwm = Adafruit_PWMServoDriver(this->address); + this->pwm.begin(); + pwm.setOutputMode(true); + // Output card don't send ack, we can't check if it's connected + // so we just return true + return true; +} + +/** + * @brief Write a digtal LOW or HIGH to the specified pin + * + * @note This function set both the state and the pwm value of the pin + * + * @param pin The pin to set the state + * @param state The logic level to set the pin to + */ +void DigitalOutputCard::digitalWrite(uint8_t pin, bool state) { + this->pwm.setPin(virtualPinMap[pin], state ? 4095 : 0); + this->state_buffer[pin] = state; + this->value_buffer[pin] = state ? 4095 : 0; + if (this->framAutoSave) { + this->saveStateToFRAM(); + this->savePinValueToFRAM(pin); + } + for (const auto& callback : change_callbacks) + { + callback.second(pin, state, state ? 4095 : 0); + } +} + +/** + * @brief Write a pwm value to the specified pin + * + * @note This function set both the state and the pwm value of the pin + * + * @param pin The pin to set the pwm value + * @param value The pwm value to set + */ +void DigitalOutputCard::analogWrite(uint8_t pin, uint16_t value) { + // If value is greater than 4095, set it to 4095 + if (value > 4095) value = 4095; + // Set the pwm value + this->pwm.setPin(virtualPinMap[pin], value); + if (this->framAutoSave) { + this->saveStateToFRAM(); + this->savePinValueToFRAM(pin); + } + this->state_buffer[pin] = value > 0; + this->value_buffer[pin] = value; + for (const auto& callback : change_callbacks) + { + callback.second(pin, value > 0, value); + } +} + +/** + * @brief The main loop for the Digital Output Card object. + * + * @note This function is not used, it is only here to implement the ExpansionCard interface + */ +void DigitalOutputCard::loop() { +} + +/** + * @brief Get the state of the specified pin + * + * @param pin The pin to get the state + * @return The state of the pin + */ +bool DigitalOutputCard::getState(uint8_t pin) { + return this->state_buffer[pin]; +} + +/** + * @brief Get the pwm value of the specified pin + * + * @param pin The pin to get the pwm value + * @return The pwm value of the pin + */ +uint16_t DigitalOutputCard::getValue(uint8_t pin) { + return this->value_buffer[pin]; +} + +/** + * @brief Get the type of the card + * + * @return The type of the card + */ +uint8_t DigitalOutputCard::getType() { + return CARD_TYPE_DIGITAL_OUTPUT; +} + +/** + * @brief Set the state of the specified pin + * + * @param pin The pin to set the state + * @param state The state of the pin + */ +void DigitalOutputCard::setState(uint8_t pin, bool state) { + this-> state_buffer[pin] = state; + this->pwm.setPin(pin, state*value_buffer[pin]); + if(this->framAutoSave) { + this->saveStateToFRAM(); + } + for(const auto& callback : change_callbacks) { + callback.second(pin, state, value_buffer[pin]); + } +} + +/** + * @brief Set the pwm value of the specified pin + * + * @param pin The pin to set the pwm value + * @param value The pwm value to set + */ +void DigitalOutputCard::setValue(uint8_t pin, uint16_t value) { + // If value is greater than 4095, set it to 4095 + if (value > 4095) value = 4095; + this-> value_buffer[pin] = value; + this->pwm.setPin(pin, state_buffer[pin]*value); + if (this->framAutoSave) { + this->savePinValueToFRAM(pin); + } + for (const auto& callback : change_callbacks) + { + callback.second(pin, state_buffer[pin], value); + } +} + +/** + * @brief Register a callback function for the specified pin + * + * @param callback The callback function to be called, the first parameter is the pin, the second parameter is the state, the third parameter is the pwm value + * @return The handler of the callback function + */ +uint8_t DigitalOutputCard::registerChangeCallback(std::function callback) { + this->change_callbacks[this->callbacks_handler_index] = callback; + return this->callbacks_handler_index++; +} + +/** + * @brief Unregister a callback function + * + * @param handler The handler of the callback function to be unregistered + */ +void DigitalOutputCard::unregisterChangeCallback(uint8_t handler) { + this->change_callbacks.erase(handler); +} + +/** + * @brief Load a pin map + * + * A pin map is an array of 16 elements that maps the physical pins to virtual pins + * The virtual pins are the pins that are used in the callback functions and are used for all the functions in this class + * The physical pins are the pins on the Output IC, This can be found on the schematic of the ESPMegaI/O board + * This function is useful if you want to change the number identification of the pins to match your project needs + * + * @param pinMap The pin map to load + */ +void DigitalOutputCard::loadPinMap(uint8_t pinMap[16]) { + for(int i = 0; i < 16; i++) { + this->pinMap[i] = pinMap[i]; + this->virtualPinMap[pinMap[i]] = i; + } +} + +/** + * @brief Bind a FRAM to the card + * + * @note The Output Card use 34 bytes of FRAM + * + * @warning If the fram range overlap with another card, undefined behavior will occur + * + * @param fram The FRAM to bind + * @param address The address of the card in the FRAM + */ +void DigitalOutputCard::bindFRAM(FRAM *fram, uint16_t address) { + this->fram = fram; + this->framBinded = true; + this->framAddress = address; +} + +/** + * @brief Pack the states of all the pins into a 16 bit integer + * + * @return The packed states + */ +uint16_t DigitalOutputCard::packStates() { + uint16_t packed = 0; + for(int i = 0; i < 16; i++) { + packed |= (state_buffer[i] << i); + } + return packed; +} + +/** + * @brief Unpack the states of all the pins from a 16 bit integer + * + * @param states The packed states + */ +void DigitalOutputCard::unpackStates(uint16_t states) { + for(int i = 0; i < 16; i++) { + this->setState(i, (states >> i) & 1); + } +} + +/** + * @brief Save the states and values of all the pins to the FRAM + */ +void DigitalOutputCard::saveToFRAM() { + if(!framBinded) return; + // Save the state + uint16_t packed = packStates(); + this->fram->write16(framAddress, packed); + // Save the value + this->fram->write(framAddress+2, (uint8_t*)value_buffer, 32); +} + +/** + * @brief Load the states and values of all the pins from the FRAM + */ +void DigitalOutputCard::loadFromFRAM() { + if(!framBinded) return; + // Load the state + uint16_t packed = this->fram->read16(framAddress); + unpackStates(packed); + // Load the value + uint16_t value[16]; + this->fram->read(framAddress+2, (uint8_t*)value, 32); + for(int i = 0; i < 16; i++) { + this->setValue(i, value[i]); + } +} + +/** + * @brief Set the auto save to FRAM + * + * @param autoSave True to enable auto save, false to disable auto save + */ +void DigitalOutputCard::setAutoSaveToFRAM(bool autoSave) { + this->framAutoSave = autoSave; +} + +/** + * @brief Save a single pin value to FRAM + * + * @param pin The pin to save + */ +void DigitalOutputCard::savePinValueToFRAM(uint8_t pin) { + if(!framBinded) return; + this->fram->write(framAddress+2+pin*2, (uint8_t*)&value_buffer[pin], 2); +} + +/** + * @brief Save the states of all the pins to FRAM + */ +void DigitalOutputCard::saveStateToFRAM() { + if(!framBinded) return; + uint16_t packed = packStates(); + this->fram->write16(framAddress, packed); +} \ No newline at end of file diff --git a/DigitalOutputCard.hpp b/DigitalOutputCard.hpp new file mode 100644 index 0000000..a3ed97c --- /dev/null +++ b/DigitalOutputCard.hpp @@ -0,0 +1,116 @@ +#pragma once +#include +#include +#include +#include + +// Protocol for digital output card +// Note that pin is always 2 characters long and padded with 0 if necessary +// Set pin state topic: /set/state payload: 0/1 +// Set pin pwm topic: /set/value payload: 0-4095 +// Publish pin state topic: /state payload: 0/1 +// Publish pin pwm topic: /value payload: 0-4095 +// Publish all topic: requeststate payload: N/A +// Enable/disable publish topic: publish_enable payload: 0/1 + +// MQTT Topics +#define SET_STATE_TOPIC "/set/state" +#define SET_VALUE_TOPIC "/set/value" +#define STATE_TOPIC "/state" +#define VALUE_TOPIC "/value" +#define REQUEST_STATE_TOPIC "requeststate" +#define PUBLISH_ENABLE_TOPIC "publish_enable" + +// Card type +#define CARD_TYPE_DIGITAL_OUTPUT 0x00 + +/** + * @brief The DigitalOutputCard class is a class for controlling the Digital Output Card. + * + * This Digital Output Card has 16 digital outputs. + * All outputs are PWM capable. + * ALl outputs are 12V Push-Pull outputs. + * Outputs is grouped in 4 groups of 4 outputs.(0-3, 4-7, 8-11, 12-15) + * Each pin is capable of 0.6A, however each group's total current is limited to 1.2A. + * + * @warning You should not instantiate this class directly, instead use ESPMegaIO's registerCard function. + */ +class DigitalOutputCard : public ExpansionCard +{ +public: + // Instantiate the card with the specified address + DigitalOutputCard(uint8_t address); + // Instantiate the card with the specified position on the dip switch + DigitalOutputCard(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4); + // Initialize the card + bool begin(); + // Dummy loop function + void loop(); + // Set the output to the specified state + // This function set both the state and the pwm value + void digitalWrite(uint8_t pin, bool state); + // Set the output to the specified pwm value + // This function set both the state and the pwm value + void analogWrite(uint8_t pin, uint16_t value); + // Set the state of the specified pin + void setState(uint8_t pin, bool state); + // Set the pwm value of the specified pin + void setValue(uint8_t pin, uint16_t value); + // Get the state of the specified pin + bool getState(uint8_t pin); + // Get the pwm value of the specified pin + uint16_t getValue(uint8_t pin); + // Register a callback function that will be called when the state of a pin changes + uint8_t registerChangeCallback(std::function callback); + // Unregister the callback function + void unregisterChangeCallback(uint8_t handler); + // Load a new pin map + void loadPinMap(uint8_t pinMap[16]); + // Bind the fram object to the card + // The Output card use the fram in this layout: + // [2 bytes] 0-1 : state + // [32 bytes] 2-33 : value + void bindFRAM(FRAM *fram, uint16_t address); + // Save the state and value to the fram + void saveToFRAM(); + // Load the state and value from the fram + void loadFromFRAM(); + // Set the auto save to fram flag + void setAutoSaveToFRAM(bool autoSave); + // Save a single pin value to fram + void savePinValueToFRAM(uint8_t pin); + // Save state to fram + void saveStateToFRAM(); + // Save value to fram + void saveValueToFRAM(); + // Get type of card + uint8_t getType(); +private: + // FRAM address + uint16_t framAddress; + // FRAM is binded + bool framBinded = false; + // Auto save to fram + bool framAutoSave = false; + // The fram object pointer + FRAM *fram; + // The pwm driver + Adafruit_PWMServoDriver pwm; + // The address of the card + uint8_t address; + // The state of the card + bool state_buffer[16]; + // The pwm value of the card + uint16_t value_buffer[16]; + // The callback function + uint8_t callbacks_handler_index = 0; + std::map> change_callbacks; + // Physical pin to virtual pin map + uint8_t pinMap[16]; + // Return 16 bit value representing all 16 channels + uint16_t packStates(); + // Unpack the 16 bit value to the state buffer + void unpackStates(uint16_t states); + // Virtual pin to physical pin map + uint8_t virtualPinMap[16]; +}; \ No newline at end of file diff --git a/DigitalOutputIoT.cpp b/DigitalOutputIoT.cpp new file mode 100644 index 0000000..cd78212 --- /dev/null +++ b/DigitalOutputIoT.cpp @@ -0,0 +1,325 @@ +#include + +/** + * @brief Create a new DigitalOutputIoT object + */ +DigitalOutputIoT::DigitalOutputIoT() +{ + this->state_report_topic = new char[10]; + this->value_report_topic = new char[10]; + this->digital_outputs_publish_enabled = true; +} + +/** + * @brief Destroy the DigitalOutputIoT object + */ +DigitalOutputIoT::~DigitalOutputIoT() +{ + delete[] this->state_report_topic; + delete[] this->value_report_topic; +} + +/** + * @brief Initialize the DigitalOutputIoT object + * + * @note ALthough this function can be called inside the main program, it is recommended to use ESPMegaPRO::installCard() instead + * + * @param card_id The id of the card + * @param card The card object + * @param mqtt The PubSubClient object + * @param base_topic The base topic of the card + * @return True if the initialization is successful, false otherwise + */ +bool DigitalOutputIoT::begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic) +{ + ESP_LOGD("DigitalOutputIoT", "Beginning DigitalOutputIoT"); + this->mqtt = mqtt; + this->base_topic = base_topic; + this->card = (DigitalOutputCard *)card; + this->card_id = card_id; + this->set_state_length = strlen(SET_STATE_TOPIC); + this->set_value_length = strlen(SET_VALUE_TOPIC); + this->state_length = strlen(STATE_TOPIC); + this->value_length = strlen(VALUE_TOPIC); + this->request_state_length = strlen(REQUEST_STATE_TOPIC); + this->publish_enable_length = strlen(PUBLISH_ENABLE_TOPIC); + strcpy(this->state_report_topic, "00/state"); + strcpy(this->value_report_topic, "00/value"); + ESP_LOGV("DigitalOutputIoT", "Registering callbacks inside DigitalOutputIoT::begin"); + // Register callbacks + auto bindedCallback = std::bind(&DigitalOutputIoT::handleValueChange, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + this->card->registerChangeCallback(bindedCallback); + ESP_LOGV("DigitalOutputIoT", "DigitalOutputIoT::begin complete"); + return true; +} + +/** + * @brief Handle the MQTT messages for the DigitalOutputIoT card + * + * @param topic The topic of the message + * @param payload The payload of the message + */ +void DigitalOutputIoT::handleMqttMessage(char *topic, char *payload) +{ + // Protocol for digital output card + // Note that pin is always 2 characters long and padded with 0 if necessary + // Set pin state topic: /set/state payload: 0/1 + // Set pin pwm topic: /set/value payload: 0-4095 + // Publish pin state topic: /state payload: 0/1 + // Publish pin pwm topic: /value payload: 0-4095 + // Publish all topic: requeststate payload: N/A + // Enable/disable publish topic: publish_enable payload: 0/1 + uint8_t topic_length = strlen(topic); + if (this->processSetStateMessage(topic, payload, topic_length)) + return; + if (this->processSetValueMessage(topic, payload, topic_length)) + return; + if (this->processRequestStateMessage(topic, payload, topic_length)) + return; +} + +/** + * @brief Process a set state message + * + * @param topic The null terminated topic + * @param payload The null terminated payload + * @param topic_length The length of the topic + * @return True if the message is a set state message, false otherwise + */ +bool DigitalOutputIoT::processSetStateMessage(char *topic, char *payload, uint8_t topic_length) +{ + // Check if the topic is a set state topic + // The correct format is /set/state + // This mean that the topic must end with /set/state + // Check if the 3rd character is / + if (topic[2] != '/') + { + return false; + } + // The topic must be set_state_length + 2 characters long + if (topic_length != set_state_length + 2) + { + return false; + } + // Check if the topic ends with /set/state + if (!strncmp(topic + 2, SET_STATE_TOPIC, set_state_length)) + { + // Get the pin number + uint8_t pin = (topic[0] - '0') * 10 + (topic[1] - '0'); + // Get the state + bool state = false; + char state_char = payload[0]; + if (state_char == '0') + { + state = false; + } + else if (state_char == '1') + { + state = true; + } + else + { + return false; + } + // Set the state + card->setState(pin, state); + return true; + } + return false; +} + +/** + * @brief Process a set value message + * + * @param topic The null terminated topic + * @param payload The null terminated payload + * @param topic_length The length of the topic + * @return True if the message is a set value message, false otherwise + */ +bool DigitalOutputIoT::processSetValueMessage(char *topic, char *payload, uint8_t topic_length) +{ + // Check if the topic is a set value topic + // The correct format is /set/value + // This mean that the topic must end with /set/value + // Check if the 3rd character is / + if (topic[2] != '/') + { + return false; + } + // The topic must be set_value_length + 2 characters long + if (topic_length != set_value_length + 2) + { + return false; + } + // Check if the topic ends with /set/value + if (!strncmp(topic + 2, SET_VALUE_TOPIC, set_value_length)) + { + // Get the pin number + uint8_t pin = (topic[0] - '0') * 10 + (topic[1] - '0'); + // Get the value + uint16_t value = atoi(payload); + // Set the value + card->setValue(pin, value); + return true; + } + return false; +} + +/** + * @brief Process a request state message + * + * @param topic The null terminated topic + * @param payload The null terminated payload + * @param topic_length The length of the topic + * @return True if the message is a request state message, false otherwise + */ +bool DigitalOutputIoT::processRequestStateMessage(char *topic, char *payload, uint8_t topic_length) +{ + // Check if the topic is a request state topic + // The correct format is requeststate + // This mean that the topic must be request_state_length characters long + // The topic must be request_state_length characters long + if (topic_length != request_state_length) + { + return false; + } + // Check if the topic is requeststate + if (!strncmp(topic, REQUEST_STATE_TOPIC, request_state_length)) + { + // Publish the state of all pins + publishDigitalOutputs(); + return true; + } + return false; +} + +/** + * @brief Publish the state of all digital outputs + */ +void DigitalOutputIoT::publishDigitalOutputs() +{ + if (!digital_outputs_publish_enabled) + return; + for (int i = 0; i < 16; i++) + { + publishDigitalOutput(i); + } +} + +/** + * @brief Publish the state and value of the specified digital output + * + * @param pin The pin to publish + */ +void DigitalOutputIoT::publishDigitalOutput(uint8_t pin) +{ + publishDigitalOutputState(pin); + publishDigitalOutputValue(pin); +} + +/** + * @brief Publish the state of the specified digital output + * + * @param pin The pin to publish + */ +void DigitalOutputIoT::publishDigitalOutputState(uint8_t pin) +{ + if (!digital_outputs_publish_enabled) + return; + state_report_topic[0] = pin / 10 + '0'; + state_report_topic[1] = pin % 10 + '0'; + publishRelative(state_report_topic, card->getState(pin) ? "1" : "0"); +} + +/** + * @brief Publish the value of the specified digital output + * + * @param pin The pin to publish + */ +void DigitalOutputIoT::publishDigitalOutputValue(uint8_t pin) +{ + if (!digital_outputs_publish_enabled) + return; + value_report_topic[0] = pin / 10 + '0'; + value_report_topic[1] = pin % 10 + '0'; + char payload[5]; + sprintf(payload, "%d", card->getValue(pin)); + publishRelative(value_report_topic, payload); +} + +/** + * @brief Enable/disable publishing of digital outputs + * + * @param enabled True to enable publishing, false to disable publishing + */ +void DigitalOutputIoT::setDigitalOutputsPublishEnabled(bool enabled) +{ + digital_outputs_publish_enabled = enabled; +} + +/** + * @brief Handle the value change of a pin + * + * @note This function is registered as a callback function with the DigitalOutputCard object + * + * @param pin The pin that changed + * @param state The new state of the pin + * @param value The new value of the pin + */ +void DigitalOutputIoT::handleValueChange(uint8_t pin, bool state, uint16_t value) +{ + publishDigitalOutput(pin); +} + +/** + * @brief Publish all digital outputs + * + * @note This function is called by the ESPMegaIoT object + */ +void DigitalOutputIoT::publishReport() +{ + publishDigitalOutputs(); +} + +/** + * @brief Get the type of the IoT component + * + * @return The type of the IoT component + */ +uint8_t DigitalOutputIoT::getType() +{ + return CARD_TYPE_DIGITAL_OUTPUT; +} + +/** + * @brief Subscribe to the MQTT topics used by the DigitalOutputIoT object + */ +void DigitalOutputIoT::subscribe() +{ + char topic[20]; + // Subscribe to all set state topics + for (int i = 0; i < 16; i++) + { + sprintf(topic, "%02d/set/state", i); + subscribeRelative(topic); + } + // Subscribe to all set value topics + for (int i = 0; i < 16; i++) + { + sprintf(topic, "%02d/set/value", i); + subscribeRelative(topic); + } + // Subscribe to request state topic + subscribeRelative(REQUEST_STATE_TOPIC); + // Subscribe to publish enable topic + subscribeRelative(PUBLISH_ENABLE_TOPIC); +} + +/** + * @brief The main loop for the DigitalOutputIoT object + * + * @note This function is not used, it is only here to implement the IoTComponent interface + */ +void DigitalOutputIoT::loop() +{ +} \ No newline at end of file diff --git a/DigitalOutputIoT.hpp b/DigitalOutputIoT.hpp new file mode 100644 index 0000000..116f206 --- /dev/null +++ b/DigitalOutputIoT.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include + +/** + * @brief The DigitalOutputIoT class is a class interfacing with the Digital Output Card through MQTT. + * + * @warning You should not instantiate this class directly, instead use ESPMegaIO's registerCard function. + */ +class DigitalOutputIoT : public IoTComponent { + public: + DigitalOutputIoT(); + ~DigitalOutputIoT(); + bool begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic); + void handleMqttMessage(char *topic, char *payload); + void publishDigitalOutputs(); + void publishDigitalOutput(uint8_t pin); + void publishDigitalOutputState(uint8_t pin); + void publishDigitalOutputValue(uint8_t pin); + void setDigitalOutputsPublishEnabled(bool enabled); + void handleValueChange(uint8_t pin, bool state, uint16_t value); + void publishReport(); + void subscribe(); + void loop(); + uint8_t getType(); + private: + bool digital_outputs_publish_enabled = false; + bool processSetStateMessage(char *topic, char *payload, uint8_t topic_length); + bool processSetValueMessage(char *topic, char *payload, uint8_t topic_length); + bool processRequestStateMessage(char *topic, char *payload, uint8_t topic_length); + DigitalOutputCard *card; + char *state_report_topic; + char *value_report_topic; + uint8_t set_state_length; + uint8_t set_value_length; + uint8_t state_length; + uint8_t value_length; + uint8_t request_state_length; + uint8_t publish_enable_length; +}; \ No newline at end of file diff --git a/ESPMegaDisplay.cpp b/ESPMegaDisplay.cpp new file mode 100644 index 0000000..f4d0563 --- /dev/null +++ b/ESPMegaDisplay.cpp @@ -0,0 +1,498 @@ +#include + +/** + * @brief Receives and processes serial commands from the display adapter. + * @param process Flag indicating whether to process the received commands. + * @return True if data is received, false otherwise. + */ +bool ESPMegaDisplay::recieveSerialCommand(bool process) +{ + bool dataRecieved = false; + // Read the serial buffer if available + while (displayAdapter->available()) + { + rx_buffer[rx_buffer_index] = displayAdapter->read(); + rx_buffer_index++; + // Check for overflow + if (rx_buffer_index >= 256) + { + rx_buffer_index = 0; + } + if (process) + this->processSerialCommand(); + dataRecieved = true; + } + return dataRecieved; +} + +/** + * @brief Receives and processes serial commands from the display adapter. + * @return True if data is received, false otherwise. + */ +bool ESPMegaDisplay::recieveSerialCommand() +{ + return recieveSerialCommand(true); +} + +/** + * @brief Processes the received serial command. + * @note This function interacts directly with the rx_buffer. + */ +void ESPMegaDisplay::processSerialCommand() +{ + // Check if the rx buffer ended with stop bytes (0xFF 0xFF 0xFF) + if (!payloadIsValid()) + return; + // The rx buffer ended with stop bytes + // Check if the rx buffer is a push payload + // The payload type is the first byte of the payload + // 0x00 is invalid instruction + // 0x01 is success + // 0x65 is a touch event + // 0x66 is a page report event + if (rx_buffer[0] == 0x65) + { + processTouchPayload(); + } + else if (rx_buffer[0] == 0x66) + { + processPageReportPayload(); + } + this->rx_buffer_index = 0; +} + +/** + * @brief Processes the touch event payload. + * @note This function interacts directly with the rx_buffer. + */ +void ESPMegaDisplay::processTouchPayload() +{ + if (rx_buffer_index != 7) + return; + // The second byte of the payload is the page number + uint8_t page = rx_buffer[1]; + // The third byte of the payload is the component id + uint8_t component = rx_buffer[2]; + // The fourth byte of the payload is the event + uint8_t event = rx_buffer[3]; + // 0x01 is press, 0x00 is release + ESP_LOGV("ESPMegaDisplay", "Touch event: page=%d, component=%d, event=%d", page, component, event); + for (auto const &callback : touch_callbacks) + { + callback.second(page, component, event); + } +} + +/** + * @brief Processes the page report event payload. + * @note This function interacts directly with the rx_buffer. + */ +void ESPMegaDisplay::processPageReportPayload() +{ + if (rx_buffer_index != 5) + return; + // The second byte of the payload is the page number + // Check if the page number is different from the current page + // If it is different, we call the page change callbacks + if (rx_buffer[1] == this->currentPage) + return; + this->currentPage = rx_buffer[1]; + ESP_LOGV("ESPMegaDisplay", "Page change event: page=%d", this->currentPage); + for (auto const &callback : page_change_callbacks) + { + callback.second(this->currentPage); + } +} + +/** + * @brief Sends stop bytes to the display adapter. + */ +void ESPMegaDisplay::sendStopBytes() +{ + displayAdapter->write(0xFF); + displayAdapter->write(0xFF); + displayAdapter->write(0xFF); +} + +/** + * @brief Sends a command to the display adapter. + * @param command The command to send. + */ +void ESPMegaDisplay::sendCommand(char *command) +{ + displayAdapter->print(command); + sendStopBytes(); +} + +/** + * @brief Jumps to the specified page on the display. + * @param page The page number to jump to. + */ +void ESPMegaDisplay::jumpToPage(int page) +{ + this->displayAdapter->print("page "); + this->displayAdapter->print(page); + sendStopBytes(); +} + +/** + * @brief Sets the value of a number component on the display. + * @param component The component name. + * @param value The value to set. + */ +void ESPMegaDisplay::setNumber(const char *component, int value) +{ + this->displayAdapter->print(component); + this->displayAdapter->print("="); + this->displayAdapter->print(value); + sendStopBytes(); +} + +/** + * @brief Sets the value of a string component on the display. + * @param component The component name. + * @param value The value to set. + */ +void ESPMegaDisplay::setString(const char *component, const char *value) +{ + this->displayAdapter->print(component); + this->displayAdapter->print("=\""); + this->displayAdapter->print(value); + this->displayAdapter->print("\""); + sendStopBytes(); +} + +/** + * @brief Gets the value of a number component from the display. + * @warning This function is blocking. + * @warning If the display does not respond or is not connected, this function will block for up to DISPLAY_FETCH_RETRY_COUNT * DISPLAY_FETCH_TIMEOUT milliseconds. + * @param component The component name. + * @return The value of the number component. + */ +uint32_t ESPMegaDisplay::getNumber(const char *component) +{ + // We might be in the middle of a serial command + // We reset the rx buffer index to 0 to ensure that we don't read the wrong data + this->rx_buffer_index = 0; + uint32_t start = millis(); + // Send the get command + this->displayAdapter->print("get "); + this->displayAdapter->print(component); + sendStopBytes(); + // Try to get a valid payload DISPLAY_FETCH_RETRY_COUNT times + // Wait for the response + bool validPayload = false; + for (int i = 0; i < DISPLAY_FETCH_RETRY_COUNT; i++) + { + if (!waitForValidPayload(DISPLAY_FETCH_TIMEOUT)) + { + ESP_LOGE("ESPMegaDisplay", "Timeout while waiting for display response"); + return 0; + } + if (rx_buffer[0] != 0x71) + { + ESP_LOGI("ESPMegaDisplay", "Invalid payload type: %d, retrying.", rx_buffer[0]); + // The rx buffer is invalid, reset the rx buffer index + rx_buffer_index = 0; + continue; + } + else + { + validPayload = true; + break; + } + } + if (!validPayload) + { + ESP_LOGE("ESPMegaDisplay", "Invalid payload type: %d, max retry excceded.", rx_buffer[0]); + return 0; + } + + // The rx buffer is valid + // The expected payload is type 0x71 + + // The 2nd to 5th byte of the payload is the value + // It's a 4 byte 32-bit value in little endian order. + // Convert the 4 bytes to a 32-bit value + uint32_t value = 0; + value |= rx_buffer[1]; + value |= rx_buffer[2] << 8; + value |= rx_buffer[3] << 16; + value |= rx_buffer[4] << 24; + return value; +} + +/** + * @brief Gets the value of a string component from the display. + * @warning This function is blocking. + * @warning If the display does not respond or is not connected, this function will block for up to DISPLAY_FETCH_RETRY_COUNT * DISPLAY_FETCH_TIMEOUT milliseconds. + * @param component The component name. + * @return The value of the string component. + * @note The returned char array must be freed after use. + */ +const char *ESPMegaDisplay::getString(const char *component) +{ + // We might be in the middle of a serial command + // We reset the rx buffer index to 0 to ensure that we don't read the wrong data + this->rx_buffer_index = 0; + uint32_t start = millis(); + // Send the get command + this->displayAdapter->print("get "); + this->displayAdapter->print(component); + sendStopBytes(); + // Wait for the response + // Try to get a valid payload DISPLAY_FETCH_RETRY_COUNT times + // Wait for the response + bool validPayload = false; + for (int i = 0; i < DISPLAY_FETCH_RETRY_COUNT; i++) + { + if (!waitForValidPayload(DISPLAY_FETCH_TIMEOUT)) + { + ESP_LOGE("ESPMegaDisplay", "Timeout while waiting for display response"); + return nullptr; + } + if (rx_buffer[0] != 0x70) + { + ESP_LOGI("ESPMegaDisplay", "Invalid payload type: %d, retrying.", rx_buffer[0]); + // The rx buffer is invalid, reset the rx buffer index + rx_buffer_index = 0; + continue; + } + else + { + validPayload = true; + break; + } + } + if (!validPayload) + { + ESP_LOGE("ESPMegaDisplay", "Invalid payload type: %d, max retry excceded.", rx_buffer[0]); + return nullptr; + } + // The 2nd bytes onwards before the stop bytes is the string + // The length of the string is the length of the payload minus 4 + uint8_t length = rx_buffer_index - 4; + // First we malloc a char array with the length of the string + char *value = (char *)malloc(length + 1); + // Copy the string from the rx buffer to the char array + memcpy(value, rx_buffer + 1, length); + // Add the null terminator + value[length] = '\0'; + // Return the char array + return value; +} + +/** + * @brief Gets the value of a number component from the display and stores it in a buffer. + * @warning This function is blocking. + * @warning If the display does not respond or is not connected, this function will block for up to DISPLAY_FETCH_RETRY_COUNT * DISPLAY_FETCH_TIMEOUT milliseconds. + * @param component The component name. + * @param buffer The buffer to store the value. + * @param buffer_size The size of the buffer. + * @return True if the value is successfully stored in the buffer, false otherwise. + */ +bool ESPMegaDisplay::getStringToBuffer(const char *component, char *buffer, uint8_t buffer_size) +{ + // We might be in the middle of a serial command + // We reset the rx buffer index to 0 to ensure that we don't read the wrong data + this->rx_buffer_index = 0; + uint32_t start = millis(); + // Send the get command + this->displayAdapter->print("get "); + this->displayAdapter->print(component); + sendStopBytes(); + // Wait for the response + // Try to get a valid payload DISPLAY_FETCH_RETRY_COUNT times + // Wait for the response + bool validPayload = false; + for (int i = 0; i < DISPLAY_FETCH_RETRY_COUNT; i++) + { + if (!waitForValidPayload(DISPLAY_FETCH_TIMEOUT)) + { + ESP_LOGE("ESPMegaDisplay", "Timeout while waiting for display response"); + this->sendStopBytes(); + return 0; + } + if (rx_buffer[0] != 0x70) + { + ESP_LOGI("ESPMegaDisplay", "Invalid payload type: %d, retrying.", rx_buffer[0]); + this->sendStopBytes(); + // The rx buffer is invalid, reset the rx buffer index + rx_buffer_index = 0; + continue; + } + else + { + validPayload = true; + break; + } + } + if (!validPayload) + { + ESP_LOGE("ESPMegaDisplay", "Invalid payload type: %d, max retry excceded.", rx_buffer[0]); + this->sendStopBytes(); + return 0; + } + + // The 2nd bytes onwards before the stop bytes is the string + // The length of the string is the length of the payload minus 4 + uint8_t length = rx_buffer_index - 4; + // Check if the buffer is large enough to hold the string + if (length > buffer_size) + { + ESP_LOGE("ESPMegaDisplay", "Buffer size too small"); + this->sendStopBytes(); + return false; + } + // Copy the string from the rx buffer to the char array + memcpy(buffer, rx_buffer + 1, length); + // Add the null terminator + buffer[length] = '\0'; + return 1; +} + +/** + * @brief Waits for a valid payload from the display adapter. + * @param timeout The timeout value in milliseconds. + * @return True if a valid payload is received, false otherwise. + */ +bool ESPMegaDisplay::waitForValidPayload(uint32_t timeout) +{ + uint32_t start = millis(); + // If the payload is not valid, keep reading the serial buffer until timeout + while (!this->payloadIsValid()) + { + if (millis() - start > timeout) + { + return false; + } + recieveSerialCommand(false); + } + return true; +} + +/** + * @brief Checks if the received payload is valid. + * @return True if the payload is valid, false otherwise. + */ +bool ESPMegaDisplay::payloadIsValid() +{ + // Check if the rx buffer ended with stop bytes (0xFF 0xFF 0xFF) + if (rx_buffer_index < 3) + return false; + if (rx_buffer[rx_buffer_index - 1] != 0xFF) + return false; + if (rx_buffer[rx_buffer_index - 2] != 0xFF) + return false; + if (rx_buffer[rx_buffer_index - 3] != 0xFF) + return false; + return true; +} + +/** + * @brief Sets the brightness of the display. + * @param value The brightness value. + */ +void ESPMegaDisplay::setBrightness(int value) +{ + this->displayAdapter->print("dim="); + this->displayAdapter->print(value); + sendStopBytes(); +} + +/** + * @brief Sets the volume of the display. + * @param value The volume value. + */ +void ESPMegaDisplay::setVolume(int value) +{ + this->displayAdapter->print("vol="); + this->displayAdapter->print(value); + sendStopBytes(); +} + +/** + * @brief Restarts the display. + */ +void ESPMegaDisplay::reset() +{ + // First we send a stop bytes to clear the serial buffer + // This ensures that the display is ready to receive the reset command + sendStopBytes(); + this->displayAdapter->print("rest"); + sendStopBytes(); +} + +/** + * @brief Constructor for the ESPMegaDisplay class. + * @param displayAdapter The serial adapter connected to the display. + */ +ESPMegaDisplay::ESPMegaDisplay(HardwareSerial *displayAdapter) +{ + this->displayAdapter = displayAdapter; + this->currentPage = 0; + this->rx_buffer_index = 0; +} + +/** + * @brief Initializes the display. + */ +void ESPMegaDisplay::begin() +{ + this->displayAdapter->begin(115200); + this->displayAdapter->setTimeout(100); + this->displayAdapter->flush(); + this->reset(); +} + +/** + * @brief The main loop function of the display. + */ +void ESPMegaDisplay::loop() +{ + // Check if there is data in the serial buffer + // If there is data, process the data + recieveSerialCommand(); +} + +/** + * @brief Registers a callback function for touch events. + * @param callback The callback function. + * @return The handle of the callback function. + */ +uint16_t ESPMegaDisplay::registerTouchCallback(std::function callback) +{ + uint16_t handle = touch_callbacks.size(); + touch_callbacks[handle] = callback; + return handle; +} + +/** + * @brief Unregisters a callback function for touch events. + * @param handle The handle of the callback function. + */ +void ESPMegaDisplay::unregisterTouchCallback(uint16_t handle) +{ + touch_callbacks.erase(handle); +} + +/** + * @brief Registers a callback function for page change events. + * @param callback The callback function. + * @return The handle of the callback function. + */ +uint16_t ESPMegaDisplay::registerPageChangeCallback(std::function callback) +{ + uint16_t handle = page_change_callbacks.size(); + page_change_callbacks[handle] = callback; + return handle; +} + +/** + * @brief Unregisters a callback function for page change events. + * @param handle The handle of the callback function. + */ +void ESPMegaDisplay::unregisterPageChangeCallback(uint16_t handle) +{ + page_change_callbacks.erase(handle); +} diff --git a/ESPMegaDisplay.hpp b/ESPMegaDisplay.hpp new file mode 100644 index 0000000..5fd009b --- /dev/null +++ b/ESPMegaDisplay.hpp @@ -0,0 +1,51 @@ +#pragma once +#include +#include + +#define DISPLAY_FETCH_TIMEOUT 100 // ms +#define DISPLAY_FETCH_RETRY_COUNT 5 + +/** + * @brief The ESPMegaDisplay class is a class for controlling the ESPMegaDisplay. + * + * @note The ESPMegaDisplay is a UART controlled display. + * @note Connect the Display's TX pin to the ESPMega's RX pin and the Display's RX pin to the ESPMega's TX pin. + */ +class ESPMegaDisplay +{ + public: + ESPMegaDisplay(HardwareSerial *displayAdapter); + void begin(); + void loop(); + void reset(); + void setBrightness(int value); + void setVolume(int value); + void jumpToPage(int page); + void setString(const char* component, const char* value); + void setNumber(const char* component, int value); + const char* getString(const char* component); + bool getStringToBuffer(const char* component, char* buffer, uint8_t buffer_size); + uint32_t getNumber(const char* component); + uint16_t registerTouchCallback(std::function callback); + void unregisterTouchCallback(uint16_t handle); + uint16_t registerPageChangeCallback(std::function callback); + void unregisterPageChangeCallback(uint16_t handle); + + protected: + uint8_t currentPage; + uint8_t rx_buffer_index; + char rx_buffer[256]; + char tx_buffer[256]; + bool recieveSerialCommand(); + bool recieveSerialCommand(bool process); + void processSerialCommand(); + void processTouchPayload(); + void processPageReportPayload(); + void sendStopBytes(); + void sendCommand(char* command); + bool payloadIsValid(); + bool waitForValidPayload(uint32_t timeout); + HardwareSerial *displayAdapter; + std::map> touch_callbacks; + std::map> page_change_callbacks; +}; \ No newline at end of file diff --git a/ESPMegaIoT.cpp b/ESPMegaIoT.cpp new file mode 100644 index 0000000..16ebc47 --- /dev/null +++ b/ESPMegaIoT.cpp @@ -0,0 +1,809 @@ +#include +#include + +/** + * @brief Create a new ESPMegaIoT object + * + * @note You shold not create this object directly, Instead, you should use the ESPMegaPRO::iot object + */ +ESPMegaIoT::ESPMegaIoT() : mqtt(tcpClient) +{ + tcpClient.setTimeout(TCP_TIMEOUT_SEC); + // Initialize the components array + for (int i = 0; i < 255; i++) + { + components[i] = NULL; + } + active = false; + mqtt_connected = false; +} + +/** + * @brief Destroy the ESPMegaIoT object + */ +ESPMegaIoT::~ESPMegaIoT() +{ +} + +/** + * @brief The mqtt callback function, This function is called when a message is received on a subscribed topic + * + * This function is called when a message is received on a subscribed topic + * The payload is copied to a buffer and a null terminator is added + * The payload is then passed to the respective card's mqtt callback + * + * @param topic The topic of the message + * @param payload The payload of the message in byte form + * @param length The length of the payload + */ +void ESPMegaIoT::mqttCallback(char *topic, byte *payload, unsigned int length) +{ + // Create a null terminated string from the payload + memcpy(payload_buffer, payload, length); + payload_buffer[length] = '\0'; + // Remove the base topic from the topic + char *topic_without_base = topic + strlen(this->mqtt_config.base_topic) + 1; + for (const auto &callback : mqtt_relative_callbacks) + { + callback.second(topic_without_base + 3, payload_buffer); + } + for (const auto &callback : mqtt_callbacks) + { + callback.second(topic, payload_buffer); + } + // Call the respective card's mqtt callback + // Note that after the base topic, there should be the card id + // /base_topic/card_id/... + // First, get the card id in integer form + char *card_id_str = strtok(topic_without_base, "/"); + uint8_t card_id = atoi(card_id_str); + // Check if the card is registered + if (components[card_id] == NULL) + { + return; + } + components[card_id]->handleMqttMessage(topic_without_base + 3, payload_buffer); +} + +/** + * @brief Set the base topic for the IoT + * + * @param base_topic The base topic + */ +void ESPMegaIoT::setBaseTopic(char *base_topic) +{ + strcpy(this->mqtt_config.base_topic, base_topic); + base_topic_length = strlen(this->mqtt_config.base_topic); +} + +/** + * @brief Begin the ESPMegaIoT object + * + * @param cards The array of ExpansionCard objects + */ +void ESPMegaIoT::intr_begin(ExpansionCard *cards[]) +{ + this->cards = cards; + active = true; +} + +/** + * @brief The main loop for the ESPMegaIoT object + * + * @note Normally you should not call this function, Instead, you should call ESPMegaPRO::loop() + */ +void ESPMegaIoT::loop() +{ + if (!active) + return; + // Call each component's loop function + for (int i = 0; i < 255; i++) + { + if (components[i] != NULL) + { + components[i]->loop(); + } + } + mqtt.loop(); + sessionKeepAlive(); +} + +/** + * @brief Register an existing card for use with IoT + * + * This function registers an existing card for use with IoT + * The card should be installed using ESPMegaPRO::installCard() before calling this function + * + * @param card_id The id of the card + */ +void ESPMegaIoT::registerCard(uint8_t card_id) +{ + // Check if the card is already registered + if (components[card_id] != NULL) + { + return; + } + // Get the card type + uint8_t card_type = cards[card_id]->getType(); + // Create the respective IoT component + switch (card_type) + { + case CARD_TYPE_ANALOG: + components[card_id] = new AnalogIoT(); + components[card_id]->begin(card_id, cards[card_id], &mqtt, this->mqtt_config.base_topic); + if (mqtt_connected) + { + components[card_id]->subscribe(); + components[card_id]->publishReport(); + } + break; + case CARD_TYPE_DIGITAL_INPUT: + components[card_id] = new DigitalInputIoT(); + components[card_id]->begin(card_id, cards[card_id], &mqtt, this->mqtt_config.base_topic); + if (mqtt_connected) + { + components[card_id]->subscribe(); + components[card_id]->publishReport(); + } + break; + case CARD_TYPE_DIGITAL_OUTPUT: + components[card_id] = new DigitalOutputIoT(); + components[card_id]->begin(card_id, cards[card_id], &mqtt, this->mqtt_config.base_topic); + if (mqtt_connected) + { + components[card_id]->subscribe(); + components[card_id]->publishReport(); + } + break; + case CARD_TYPE_CLIMATE: + components[card_id] = new ClimateIoT(); + components[card_id]->begin(card_id, cards[card_id], &mqtt, this->mqtt_config.base_topic); + if (mqtt_connected) + { + components[card_id]->subscribe(); + components[card_id]->publishReport(); + } + break; + default: + ESP_LOGE("ESPMegaIoT", "Registering card %d failed: Unknown card", card_id); + return; + } +} + +/** + * @brief Unregister a card + * + * @param card_id The id of the card + */ +void ESPMegaIoT::unregisterCard(uint8_t card_id) +{ + // Check if the card is registered + if (components[card_id] == NULL) + { + return; + } + // Delete the IoT component + delete components[card_id]; + components[card_id] = NULL; +} + +/** + * @brief Publish all cards's reports + */ +void ESPMegaIoT::publishCard(uint8_t card_id) +{ + // Check if the card is registered + if (components[card_id] == NULL) + { + return; + } + // Publish the card + components[card_id]->publishReport(); +} + +/** + * @brief Subscribe to a topic + * + * @param topic The topic to subscribe to + */ +void ESPMegaIoT::subscribe(char *topic) +{ + mqtt.subscribe(topic); +} + +/** + * @brief Unsubscribe from a topic + * + * @param topic The topic to unsubscribe from + */ +void ESPMegaIoT::unsubscribeFromTopic(char *topic) +{ + mqtt.unsubscribe(topic); +} + +/** + * @brief Connect to a wifi network + * + * @param ssid The SSID of the wifi network + * @param password The password of the wifi network + */ +void ESPMegaIoT::connectToWifi(char *ssid, char *password) +{ + WiFi.begin(ssid, password); +} + +/** + * @brief Connect to a unsecured wifi network + * + * @param ssid The SSID of the wifi network + */ +void ESPMegaIoT::connectToWifi(char *ssid) +{ + WiFi.begin(ssid); +} + +/** + * @brief Disconnect from the wifi network + */ +void ESPMegaIoT::disconnectFromWifi() +{ + WiFi.disconnect(); +} + +/** + * @brief Check if the wifi is connected + * + * @return True if the wifi is connected, false otherwise + */ +bool ESPMegaIoT::wifiConnected() +{ + return WiFi.status() == WL_CONNECTED; +} + +/** + * @brief Connect to a MQTT broker with authentication + * + * @param client_id The client id to use + * @param mqtt_server The MQTT server to connect to + * @param mqtt_port The MQTT port to connect to + * @param mqtt_user The MQTT username to use + * @param mqtt_password The MQTT password to use + * @return True if the connection is successful, false otherwise + */ +bool ESPMegaIoT::connectToMqtt(char *client_id, char *mqtt_server, uint16_t mqtt_port, char *mqtt_user, char *mqtt_password) +{ + mqtt.setServer(mqtt_server, mqtt_port); + auto boundCallback = std::bind(&ESPMegaIoT::mqttCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + mqtt.setCallback(boundCallback); + if (mqtt_user == nullptr || mqtt_password == nullptr || strlen(mqtt_user) == 0 || strlen(mqtt_password) == 0) + { + mqtt_connected = false; + ESP_LOGE("ESPMegaIoT", "MQTT Connection failed: Username or password not set but MQTT use_auth is true"); + return false; + } + if (mqtt.connect(client_id, mqtt_user, mqtt_password)) + { + sessionKeepAlive(); + mqttSubscribe(); + // Publish all cards + for (int i = 0; i < 255; i++) + { + if (components[i] != NULL) + { + components[i]->publishReport(); + } + } + mqtt_connected = true; + return true; + } + mqtt_connected = false; + return false; +} + +/** + * @brief Connect to a MQTT broker without authentication + * + * @param client_id The client id to use + * @param mqtt_server The MQTT server to connect to + * @param mqtt_port The MQTT port to connect to + * @return True if the connection is successful, false otherwise + */ +bool ESPMegaIoT::connectToMqtt(char *client_id, char *mqtt_server, uint16_t mqtt_port) +{ + ESP_LOGD("ESPMegaIoT", "Setting MQTT server to %s:%d", mqtt_server, mqtt_port); + mqtt.setServer(mqtt_server, mqtt_port); + auto boundCallback = std::bind(&ESPMegaIoT::mqttCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + ESP_LOGD("ESPMegaIoT", "Binding MQTT callback"); + mqtt.setCallback(boundCallback); + if (mqtt.connect(client_id)) + { + ESP_LOGD("ESPMegaIoT", "MQTT Connected, Calling session keep alive"); + sessionKeepAlive(); + ESP_LOGD("ESPMegaIoT", "Subscribing to topics"); + mqttSubscribe(); + ESP_LOGD("ESPMegaIoT", "Publishing reports"); + // Publish all cards + for (int i = 0; i < 255; i++) + { + if (components[i] != NULL) + { + components[i]->publishReport(); + } + } + ESP_LOGI("ESPMegaIoT", "MQTT Connected OK."); + mqtt_connected = true; + return true; + } + ESP_LOGW("ESPMegaIoT", "MQTT Connection failed: %d", mqtt.state()); + mqtt_connected = false; + return false; +} + +/** + * @brief Disconnect from the MQTT broker + */ +void ESPMegaIoT::disconnectFromMqtt() +{ + mqtt.disconnect(); +} + +/** + * @brief Publish a message to a topic + * + * @param topic The topic to publish to + * @param payload The payload to publish + */ +void ESPMegaIoT::publish(const char *topic, const char *payload) +{ + mqtt.publish(topic, payload); +} + +/** + * @brief Register a callback for MQTT messages + * + * @param callback The callback function + * @return The handler for the callback + */ +uint8_t ESPMegaIoT::registerMqttCallback(std::function callback) +{ + mqtt_callbacks[mqtt_callbacks_handler_index] = callback; + return mqtt_callbacks_handler_index++; +} + +/** + * @brief Unregister a callback + * + * @param handler The handler of the callback + */ +void ESPMegaIoT::unregisterMqttCallback(uint8_t handler) +{ + mqtt_callbacks.erase(handler); +} + +/** + * @brief Subscribe to all components's topics + */ +void ESPMegaIoT::mqttSubscribe() +{ + ESP_LOGD("ESPMegaIoT", "Begin MQTT Subscription"); + for (const auto &callback : subscribe_callbacks) + { + callback.second(); + mqtt.loop(); + } + // Subscribe to all topics + for (int i = 0; i < 255; i++) + { + if (components[i] != NULL) + { + ESP_LOGD("ESPMegaIoT","Subscribing component %d", i); + components[i]->subscribe(); + mqtt.loop(); + } + } +} + +/** + * @brief Publish relative to the base topic + */ +void ESPMegaIoT::publishRelative(uint8_t card_id, char *topic, char *payload) +{ + char absolute_topic[100]; + sprintf(absolute_topic, "%s/%d/%s", this->mqtt_config.base_topic, card_id, topic); + mqtt.publish(absolute_topic, payload); +} + +/** + * @brief Subscribe relative to the base topic + */ +bool ESPMegaIoT::mqttReconnect() +{ + if (this->mqtt_config.mqtt_useauth) + { + return this->connectToMqtt(this->network_config.hostname, this->mqtt_config.mqtt_server, this->mqtt_config.mqtt_port, this->mqtt_config.mqtt_user, this->mqtt_config.mqtt_password); + } + else + { + return this->connectToMqtt(this->network_config.hostname, this->mqtt_config.mqtt_server, this->mqtt_config.mqtt_port); + } +} + +/** + * @brief Keep the MQTT session alive + * @note This function is called automatically by the ESPMegaIoT object, You should not call this function directly + */ +void ESPMegaIoT::sessionKeepAlive() +{ + // This reconnect the MQTT if it disconnect. + // If a disconnect happens, this will reconnect the MQTT within 1 second. + // A connection attempt will be made at most once every MQTT_RECONNECT_INTERVAL + // This have the effect of reconnecting to the server immediately if the connection is lost + // and the connection was previously stable for at least MQTT_RECONNECT_INTERVAL + // But will not reconnect if the connection was unstable and the connection was lost + static unsigned long lastSessionKeepAlive = 0; + static unsigned long lastConnectionAttempt = 0; + if (millis() - lastSessionKeepAlive > 1000) + { + lastSessionKeepAlive = millis(); + // Check if mqtt is connected + if (!mqtt.connected()) + { + // Try to reconnect if lastConnectionAttempt exceed MQTT_RECONNECT_INTERVAL + if (millis() - lastConnectionAttempt > MQTT_RECONNECT_INTERVAL) + { + lastConnectionAttempt = millis(); + mqtt_connected = mqttReconnect(); + } + } + } +} + +/** + * @brief Register a callback for MQTT messages relative to the base topic + * + * The message's base topic will be removed before calling the callback + * + * @param callback The callback function + * @return The handler for the callback + */ +uint8_t ESPMegaIoT::registerRelativeMqttCallback(std::function callback) +{ + mqtt_relative_callbacks[mqtt_relative_callbacks_handler_index] = callback; + return mqtt_relative_callbacks_handler_index++; +} + +/** + * @brief Unregister a relative MQTT callback + * + * @param handler The handler of the callback + */ +void ESPMegaIoT::unregisterRelativeMqttCallback(uint8_t handler) +{ + mqtt_relative_callbacks.erase(handler); +} + +/** + * @brief Publish a message relative to the base topic + * + * @param topic The topic to publish to + * @param payload The payload to publish + */ +void ESPMegaIoT::publishRelative(char *topic, char *payload) +{ + char absolute_topic[100]; + sprintf(absolute_topic, "%s/%s", this->mqtt_config.base_topic, topic); + mqtt.publish(absolute_topic, payload); + mqtt.loop(); +} + +/** + * @brief Subscribe to a topic relative to the base topic + * + * @param topic The topic to subscribe to + */ +void ESPMegaIoT::subscribeRelative(char *topic) +{ + char absolute_topic[100]; + sprintf(absolute_topic, "%s/%s", this->mqtt_config.base_topic, topic); + mqtt.subscribe(absolute_topic); + mqtt.loop(); +} + +/** + * @brief Register a function to be called when the ESPMegaIoT object is subscribing to topics + * + * @param callback The callback function + * @return The handler for the callback + */ +uint8_t ESPMegaIoT::registerSubscribeCallback(std::function callback) +{ + subscribe_callbacks[subscribe_callbacks_handler_index] = callback; + return subscribe_callbacks_handler_index++; +} + +/** + * @brief Unregister a subscribe callback + * + * @param handler The handler of the callback + */ +void ESPMegaIoT::unregisterSubscribeCallback(uint8_t handler) +{ + subscribe_callbacks.erase(handler); +} + +/** + * @brief Set the network config + * + * @param network_config The network config struct + */ +void ESPMegaIoT::setNetworkConfig(NetworkConfig network_config) +{ + this->network_config = network_config; +} + +/** + * @brief Load the network config from FRAM + */ +void ESPMegaIoT::loadNetworkConfig() +{ + // Load the network config from FRAM + network_config.ip = fram->read32(IOT_FRAM_ADDRESS); + network_config.gateway = fram->read32(IOT_FRAM_ADDRESS + 4); + network_config.subnet = fram->read32(IOT_FRAM_ADDRESS + 8); + network_config.dns1 = fram->read32(IOT_FRAM_ADDRESS + 12); + network_config.dns2 = fram->read32(IOT_FRAM_ADDRESS + 16); + fram->read(IOT_FRAM_ADDRESS + 20, (uint8_t *)network_config.hostname, 32); + network_config.useStaticIp = fram->read8(IOT_FRAM_ADDRESS + 52); + network_config.useWifi = fram->read8(IOT_FRAM_ADDRESS + 53); + network_config.wifiUseAuth = fram->read8(IOT_FRAM_ADDRESS + 54); + fram->read(IOT_FRAM_ADDRESS + 55, (uint8_t *)network_config.ssid, 32); + fram->read(IOT_FRAM_ADDRESS + 87, (uint8_t *)network_config.password, 32); +} + +/** + * @brief Save the network config to FRAM + */ +void ESPMegaIoT::saveNetworkConfig() +{ + // Save the network config to FRAM + fram->write32(IOT_FRAM_ADDRESS, network_config.ip); + fram->write32(IOT_FRAM_ADDRESS + 4, network_config.gateway); + fram->write32(IOT_FRAM_ADDRESS + 8, network_config.subnet); + fram->write32(IOT_FRAM_ADDRESS + 12, network_config.dns1); + fram->write32(IOT_FRAM_ADDRESS + 16, network_config.dns2); + fram->write(IOT_FRAM_ADDRESS + 20, (uint8_t *)network_config.hostname, 32); + fram->write8(IOT_FRAM_ADDRESS + 52, network_config.useStaticIp); + fram->write8(IOT_FRAM_ADDRESS + 53, network_config.useWifi); + fram->write8(IOT_FRAM_ADDRESS + 54, network_config.wifiUseAuth); + fram->write(IOT_FRAM_ADDRESS + 55, (uint8_t *)network_config.ssid, 32); + fram->write(IOT_FRAM_ADDRESS + 87, (uint8_t *)network_config.password, 32); +} + +/** + * @brief Begin the ethernet interface + */ +void ESPMegaIoT::ethernetBegin() +{ + ethernetIface->setHostname(network_config.hostname); +} + +/** + * @brief Load the MQTT config from FRAM + */ +void ESPMegaIoT::loadMqttConfig() +{ + // Load the mqtt config from FRAM + // We skip bytes 119-127 because they are reserved for the network config + mqtt_config.mqtt_port = fram->read16(IOT_FRAM_ADDRESS + 128); + fram->read(IOT_FRAM_ADDRESS + 130, (uint8_t *)mqtt_config.mqtt_server, 32); + fram->read(IOT_FRAM_ADDRESS + 162, (uint8_t *)mqtt_config.mqtt_user, 32); + fram->read(IOT_FRAM_ADDRESS + 194, (uint8_t *)mqtt_config.mqtt_password, 32); + mqtt_config.mqtt_useauth = fram->read8(IOT_FRAM_ADDRESS + 226); + fram->read(IOT_FRAM_ADDRESS + 227, (uint8_t *)mqtt_config.base_topic, 32); + this->base_topic_length = strlen(mqtt_config.base_topic); +} + +/** + * @brief Save the MQTT config to FRAM + */ +void ESPMegaIoT::saveMqttConfig() +{ + fram->write16(IOT_FRAM_ADDRESS + 128, mqtt_config.mqtt_port); + fram->write(IOT_FRAM_ADDRESS + 130, (uint8_t *)mqtt_config.mqtt_server, 32); + fram->write(IOT_FRAM_ADDRESS + 162, (uint8_t *)mqtt_config.mqtt_user, 32); + fram->write(IOT_FRAM_ADDRESS + 194, (uint8_t *)mqtt_config.mqtt_password, 32); + fram->write8(IOT_FRAM_ADDRESS + 226, mqtt_config.mqtt_useauth); + fram->write(IOT_FRAM_ADDRESS + 227, (uint8_t *)mqtt_config.base_topic, 32); +} + +/** + * @brief Connect to MQTT with the current config + */ +void ESPMegaIoT::connectToMqtt() +{ + if (mqtt_config.mqtt_useauth) + { + ESP_LOGD("ESPMegaIoT", "Connecting to MQTT with auth"); + this->connectToMqtt(network_config.hostname, mqtt_config.mqtt_server, mqtt_config.mqtt_port, mqtt_config.mqtt_user, mqtt_config.mqtt_password); + } + else + { + ESP_LOGD("ESPMegaIoT", "Connecting to MQTT without auth"); + this->connectToMqtt(network_config.hostname, mqtt_config.mqtt_server, mqtt_config.mqtt_port); + } +} + +/** + * @brief Connect to the network using the current config + */ +void ESPMegaIoT::connectNetwork() +{ + if (network_config.useWifi) + { + if (network_config.wifiUseAuth) + this->connectToWifi(network_config.ssid, network_config.password); + else + this->connectToWifi(network_config.ssid); + if (network_config.useStaticIp) + WiFi.config(network_config.ip, network_config.gateway, network_config.subnet); + else + WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); + } + else + { + this->ethernetBegin(); + if (network_config.useStaticIp) + ethernetIface->config(network_config.ip, network_config.gateway, network_config.subnet, network_config.dns1, network_config.dns2); + } +} + +/** + * @brief Set the MQTT config + * + * @param mqtt_config The MQTT config struct + */ +void ESPMegaIoT::setMqttConfig(MqttConfig mqtt_config) +{ + this->mqtt_config = mqtt_config; + this->base_topic_length = strlen(mqtt_config.base_topic); +} + +/** + * @brief Bind an ethernet interface to the ESPMegaIoT object + * + * @param ethernetIface The ethernet interface to bind (ETH for ESPMegaPRO R3) + */ +void ESPMegaIoT::bindEthernetInterface(ETHClass *ethernetIface) +{ + this->ethernetIface = ethernetIface; +} + +/** + * @brief Get the IoTComponent object for a card + * + * @param card_id The id of the card + * @return The IoTComponent object for the card + */ +IoTComponent *ESPMegaIoT::getComponent(uint8_t card_id) +{ + return components[card_id]; +} + +/** + * @brief Get the network config + * + * @warning You should not modify the returned struct directly + * + * @return The network config struct + */ +NetworkConfig *ESPMegaIoT::getNetworkConfig() +{ + return &network_config; +} + +/** + * @brief Get the MQTT config + * + * @warning You should not modify the returned struct directly + * + * @return The MQTT config struct + */ +MqttConfig *ESPMegaIoT::getMqttConfig() +{ + return &mqtt_config; +} + +/** + * @brief Check if the MQTT is connected + * + * @return True if the MQTT is connected, false otherwise + */ +bool ESPMegaIoT::mqttConnected() +{ + //return mqtt_connected; + return mqtt.connected(); +} + +/** + * @brief Check if the network is connected + * + * @return True if the network is connected, false otherwise + */ +bool ESPMegaIoT::networkConnected() +{ + if (network_config.useWifi) + return WiFi.status() == WL_CONNECTED; + else + return ethernetIface->linkUp(); +} + +/** + * @brief Bind a FRAM object to the ESPMegaIoT object + * @note This class is hardcode to use the FRAM address 34-300 + * + * @param fram The FRAM object to bind + */ +void ESPMegaIoT::bindFRAM(FRAM *fram) +{ + this->fram = fram; +} + +/** + * @brief Get the Wifi IP address + * + * @return The Wifi IP address + */ +IPAddress ESPMegaIoT::getWifiIp() { + return WiFi.localIP(); +} + +/** + * @brief Get the Ethernet IP Address + * + * @return The Ethernet IP Address + */ +IPAddress ESPMegaIoT::getETHIp() { + return ETH.localIP(); +} + +/** + * @brief Get the IP address of the currently active network interface + * + * @return The IP address of the currently active network interface + */ +IPAddress ESPMegaIoT::getIp() { + if (network_config.useWifi) + return this->getWifiIp(); + else + return this->getETHIp(); +} + +/** + * @brief Get the MAC Address of the Ethernet interface + * + * @return The MAC Address of the Ethernet interface + */ +String ESPMegaIoT::getETHMac() { + return ETH.macAddress(); +} + +/** + * @brief Get the MAC Address of the Wifi interface + * + * @return The MAC Address of the Wifi interface + */ +String ESPMegaIoT::getWifiMac() { + return WiFi.macAddress(); +} + +/** + * @brief Get the MAC Address of the currently active network interface + * + * @return The MAC Address of the currently active network interface + */ +String ESPMegaIoT::getMac() { + if (network_config.useWifi) + return this->getWifiMac(); + else + return this->getETHMac(); +} diff --git a/ESPMegaIoT.hpp b/ESPMegaIoT.hpp new file mode 100644 index 0000000..70787f6 --- /dev/null +++ b/ESPMegaIoT.hpp @@ -0,0 +1,156 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// MQTT Connection Parameters +#define TCP_TIMEOUT_SEC 5 +#define MQTT_RECONNECT_INTERVAL 30000 + +// FRAM Address for ESPMegaPROIoT +// Starts from 34 +// Ends at 300 (inclusive) +// Total of 267 bytes +#define IOT_FRAM_ADDRESS 34 + +/** + * @brief The network configuration struct + * @note This struct will be saved to FRAM when calling saveNetworkConfig + */ +struct NetworkConfig +{ + IPAddress ip; ///< The IP address + IPAddress gateway; ///< The gateway address + IPAddress subnet; ///< The subnet mask + IPAddress dns1; ///< The primary DNS server + IPAddress dns2; ///< The secondary DNS server + char hostname[32]; ///< The hostname + bool useStaticIp; ///< Whether to use a static IP, if false, DHCP will be used + bool useWifi; ///< Whether to use WiFi or Ethernet, if false, Ethernet will be used + bool wifiUseAuth; ///< Whether to use WiFi authentication, if false, ssid and password will be ignored + char ssid[32]; ///< The WiFi SSID, even if wifiUseAuth is false, this should be set + char password[32]; ///< The WiFi password, even if wifiUseAuth is false, this should be set +}; + +/** + * @brief The MQTT configuration struct + * @note This struct will be saved to FRAM when calling saveMqttConfig + */ +struct MqttConfig +{ + char mqtt_server[32]; ///< The MQTT server address + uint16_t mqtt_port; ///< The MQTT server port + char mqtt_user[32]; ///< The MQTT username, even if mqtt_useauth is false, this should be set + char mqtt_password[32]; ///< The MQTT password, even if mqtt_useauth is false, this should be set + bool mqtt_useauth; ///< Whether to use MQTT authentication, if false, mqtt_user and mqtt_password will be ignored + char base_topic[32]; ///< The base topic for the MQTT messages +}; + +/** + * @brief The ESPMegaIoT class is a class that is used to interface with the ESPMegaPRO IoT module + * + * This class allows you to register IoT components and interface with them through MQTT. + * This class also manages the network and MQTT connections for you. + * Supports both WiFi and Ethernet. + * Also allows you to save and load network and MQTT configurations to and from FRAM. + * Also provides MQTT helpers for publishing and subscribing to topics. + */ +class ESPMegaIoT +{ +public: + ESPMegaIoT(); + ~ESPMegaIoT(); + void intr_begin(ExpansionCard *cards[]); + void loop(); + void registerCard(uint8_t card_id); + void unregisterCard(uint8_t card_id); + void publishCard(uint8_t card_id); + // Publish topic appended with base topic + void publishRelative(char *topic, char *payload); + // Subscribe topic appended with base topic + void subscribeRelative(char *topic); + void subscribe(char *topic); + void unsubscribeFromTopic(char *topic); + void connectToWifi(char *ssid, char *password); + void connectToWifi(char *ssid); + void disconnectFromWifi(); + bool wifiConnected(); + void ethernetBegin(); + void loadNetworkConfig(); + void saveNetworkConfig(); + NetworkConfig* getNetworkConfig(); + MqttConfig* getMqttConfig(); + void setMqttConfig(MqttConfig mqtt_config); + void saveMqttConfig(); + void loadMqttConfig(); + void connectNetwork(); + void setNetworkConfig(NetworkConfig network_config); + void connectToMqtt(); + bool connectToMqtt(char *client_id, char *mqtt_server, uint16_t mqtt_port, char *mqtt_user, char *mqtt_password); + bool connectToMqtt(char *client_id, char *mqtt_server, uint16_t mqtt_port); + bool mqttConnected(); + void disconnectFromMqtt(); + void publish(const char *topic, const char *payload); + uint8_t registerMqttCallback(std::function callback); + void unregisterMqttCallback(uint8_t handler); + uint8_t registerRelativeMqttCallback(std::function callback); + void unregisterRelativeMqttCallback(uint8_t handler); + uint8_t registerSubscribeCallback(std::function callback); + void unregisterSubscribeCallback(uint8_t handler); + void setBaseTopic(char *base_topic); + void bindEthernetInterface(ETHClass *ethernetIface); + bool networkConnected(); + void bindFRAM(FRAM *fram); + + IoTComponent* getComponent(uint8_t card_id); + IPAddress getETHIp(); + IPAddress getWifiIp(); + IPAddress getIp(); + + String getETHMac(); + String getWifiMac(); + String getMac(); + +private: + FRAM *fram; + bool useWifi; + bool WifiUseAuth; + char ssid[32]; + char password[32]; + WiFiClient tcpClient; + void sessionKeepAlive(); + bool mqttReconnect(); + void wifiReconnect(); + void mqttSubscribe(); + void mqttCallback(char *topic, byte *payload, unsigned int length); + uint8_t mqtt_callbacks_handler_index; + uint8_t mqtt_relative_callbacks_handler_index; + uint8_t subscribe_callbacks_handler_index; + std::map> mqtt_callbacks; + std::map> mqtt_relative_callbacks; + std::map> subscribe_callbacks; + void publishRelative(uint8_t card_id, char *topic, char *payload); + bool active; + PubSubClient mqtt; + IoTComponent *components[255]; + char payload_buffer[200]; + uint8_t base_topic_length; + ExpansionCard **cards; // Points to card array in ESPMegaPRO Core + // MQTT Connection Parameters + bool mqtt_connected; + NetworkConfig network_config; + MqttConfig mqtt_config; + ETHClass *ethernetIface; +}; \ No newline at end of file diff --git a/ESPMegaProOS.cpp b/ESPMegaProOS.cpp new file mode 100644 index 0000000..80112b8 --- /dev/null +++ b/ESPMegaProOS.cpp @@ -0,0 +1,262 @@ +#include + +// Reserve FRAM address 0 - 1000 for ESPMegaPRO Internal Use +// (34 Bytes) Address 0-33 for Built-in Digital Output Card +// (266 Bytes) Address 34-300 for ESPMegaPRO IoT Module + +/** + * @brief Create a new ESPMegaPRO object + * + * @warning Only one ESPMegaPRO object can be created, creating more than one will result in undefined behavior + */ +ESPMegaPRO::ESPMegaPRO() { + +} + +/** + * @brief Initializes the ESPMegaPRO object. + * + * This function initializes the ESPMegaPRO object and all of its components. + * It also initializes the built-in Digital Input and Digital Output cards. + * + * @return True if the initialization is successful, false otherwise. + */ +bool ESPMegaPRO::begin() { + Wire.begin(14, 33); + fram.begin(FRAM_ADDRESS); + Serial.begin(115200); + this->installCard(1, &outputs); + outputs.bindFRAM(&fram,0); + outputs.loadFromFRAM(); + outputs.setAutoSaveToFRAM(true); + if(!this->installCard(0, &inputs)) { + ESP_LOGE("ESPMegaPRO", "Failed to initialize inputs"); + ESP_LOGE("ESPMegaPRO", "Is this an ESPMegaPRO device?"); + return false; + } + uint8_t pinMap[16] = {0, 1, 2, 3, 4, 5, 6, 7, 15, 14, 13, 12, 11, 10, 9, 8}; + inputs.loadPinMap(pinMap); + return true; +} + +/** + * @brief The main loop for the ESPMegaPRO object. + * + * @note This function must be called in the main loop of the program. + * + * It will call the loop() function of all installed expansion cards, the ESPMegaIoT module, and the internal display. + * + */ +void ESPMegaPRO::loop() { + inputs.loop(); + outputs.loop(); + for (int i = 0; i < 255; i++) { + if (cardInstalled[i]) { + cards[i]->loop(); + } + } + if(iotEnabled) { + iot->loop(); + } + if(internalDisplayEnabled) { + display->loop(); + } + if(webServerEnabled) { + webServer->loop(); + } +} + +/** + * @brief Installs an expansion card to the specified slot. + * + * @note This function automatically initializes the expansion card. + * + * @param slot The slot to install the card to. + * @param card Pointer to the ExpansionCard object. + * + * @return True if the installation is successful, false otherwise. + */ +bool ESPMegaPRO::installCard(uint8_t slot, ExpansionCard* card) { + if (slot > 255) return false; + if (cardInstalled[slot]) { + ESP_LOGE("ESPMegaPRO", "Card already installed at slot %d", slot); + return false; + } + if (!card->begin()) { + ESP_LOGE("ESPMegaPRO", "Failed to initialize card at slot %d", slot); + return false; + } + cards[slot] = card; + cardInstalled[slot] = true; + cardCount++; + return true; +} + +/** + * @brief Updates the internal RTC from NTP. + * + * @note Network must be connected before calling this function (see ESPMegaPRO.ESPMegaIoT::connectNetwork()). + * + * @return True if the update is successful, false otherwise. + */ +bool ESPMegaPRO::updateTimeFromNTP() { + struct tm timeinfo; + if (getLocalTime(&timeinfo)) + { + rtctime_t rtctime = this->getTime(); + if (rtctime.hours != timeinfo.tm_hour || rtctime.minutes != timeinfo.tm_min || + rtctime.seconds != timeinfo.tm_sec || rtctime.day != timeinfo.tm_mday || + rtctime.month != timeinfo.tm_mon + 1 || rtctime.year != timeinfo.tm_year + 1900) + { + this->setTime(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, + timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900); + + } + return true; + } + return false; +} + +/** + * @brief Gets the current time from the internal RTC. + * + * @return The current time as a rtctime_t struct. + */ +rtctime_t ESPMegaPRO::getTime() { + tmElements_t timeElement; + RTC.read(timeElement); + rtctime_t time; + time.hours = timeElement.Hour; + time.minutes = timeElement.Minute; + time.seconds = timeElement.Second; + time.day = timeElement.Day; + time.month = timeElement.Month; + time.year = timeElement.Year + 1970; + return time; +} + +/** + * @brief Sets the current time of the internal RTC. + * + * @param hours The hours. + * @param minutes The minutes. + * @param seconds The seconds. + * @param day The day. + * @param month The month. + * @param year The year. + */ +void ESPMegaPRO::setTime(int hours, int minutes, int seconds, int day, int month, int year) +{ + tmElements_t timeElement; + timeElement.Hour = hours; + timeElement.Minute = minutes; + timeElement.Second = seconds; + timeElement.Day = day; + timeElement.Month = month; + timeElement.Year = year - 1970; + RTC.write(timeElement); +} + +/** + * @brief Enables, Instanitates, and Initializes the ESPMegaIoT module. + * + * @note This function must be called before using the ESPMegaIoT module. + */ +void ESPMegaPRO::enableIotModule() { + if (iotEnabled) return; + this->iot = new ESPMegaIoT(); + this->iot->bindFRAM(&fram); + this->iot->intr_begin(cards); + iotEnabled = true; +} + +/** + * @brief Gets the expansion card installed at the specified slot. + * + * @param slot The slot to get the card from. + * + * @return Pointer to the ExpansionCard object, or nullptr if no card is installed at the specified slot. + */ +ExpansionCard* ESPMegaPRO::getCard(uint8_t slot) { + if (slot > 255) return nullptr; + if (!cardInstalled[slot]) return nullptr; + return cards[slot]; +} + +/** + * @brief Enables, Instanitates, and Initializes the internal display. + * + * @note &Serial is used for the internal display on ESPMegaPRO R3. + * @note This function can only be called if the ESPMegaIoT module is enabled. + * @note This function must be called before using the internal display. + * + * @param serial Pointer to the HardwareSerial object to use for the internal display (Serial for ESPMegaPRO R3). + */ +void ESPMegaPRO::enableInternalDisplay(HardwareSerial *serial) { + if (internalDisplayEnabled) return; + if (!iotEnabled) { + ESP_LOGE("ESPMegaPRO", "Cannot enable internal display without IoT module enabled"); + return; + } + ESP_LOGD("ESPMegaPRO", "Enabling Internal Display"); + display = new InternalDisplay(serial); + ESP_LOGD("ESPMegaPRO", "Binding Internal Display to IoT Module"); + auto bindedGetTime = std::bind(&ESPMegaPRO::getTime, this); + ESP_LOGD("ESPMegaPRO", "Binding Internal Display to Input/Output Cards"); + display->bindInputCard(&inputs); + display->bindOutputCard(&outputs); + display->begin(this->iot,bindedGetTime); + internalDisplayEnabled = true; + ESP_LOGD("ESPMegaPRO", "Internal Display Enabled"); + +} + +/** + * @brief Dumps the contents of the internal FRAM to the serial port. + * + * @param start The starting address. + * @param end The ending address. + */ +void ESPMegaPRO::dumpFRAMtoSerial(uint16_t start, uint16_t end) { + for (int i = start; i <=end; i++) { + if (i % 16 == 0) { + Serial.printf("\n%03d: ", i); + } + Serial.printf("%03d ", this->fram.read8(i)); + } +} + +/** + * @brief Dumps the contents of the internal FRAM to the serial port in ASCII. + * + * @param start The starting address. + * @param end The ending address. + */ +void ESPMegaPRO::dumpFRAMtoSerialASCII(uint16_t start, uint16_t end) { + for (int i = 0; i < 500; i++) { + Serial.printf("%d: %c\n", i,this->fram.read8(i)); + } +} + +/** + * @brief Enables the internal web server. + * + * @note This function can only be called if the ESPMegaIoT module is enabled. + * @note This function can only be called once. + * + * @param port The port to use for the web server. + */ +void ESPMegaPRO::enableWebServer(uint16_t port) { + if (!iotEnabled) { + ESP_LOGE("ESPMegaPRO", "Cannot enable web server without IoT module enabled"); + return; + } + if (webServerEnabled) { + ESP_LOGE("ESPMegaPRO", "Web server already enabled"); + return; + } + webServer = new ESPMegaWebServer(port, this->iot); + webServer->bindFRAM(&fram); + webServer->begin(); + webServerEnabled = true; +} \ No newline at end of file diff --git a/ESPMegaProOS.hpp b/ESPMegaProOS.hpp new file mode 100644 index 0000000..b20f1ce --- /dev/null +++ b/ESPMegaProOS.hpp @@ -0,0 +1,92 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ESPMega Pro R3 Board Address +#define FRAM_ADDRESS 0x56 +#define INPUT_BANK_A_ADDRESS 0x21 +#define INPUT_BANK_B_ADDRESS 0x22 +#define PWM_BANK_ADDRESS 0x5F +#define RTC_ADDRESS 0x68 + +/** + * @brief The ESPMegaPRO class is the main class for the ESPMegaPRO library. + * + * This class provides functions for managing the ESPMegaPRO board, such as installing expansion cards, managing the internal RTC, and managing the internal FRAM. + * This class also provides functions for managing the ESPMegaIoT module and the internal display. + * + * This class provide a Object Oriented Programming (OOP) interface for the ESPMegaPRO board. + * If you are looking for a more simple and a more procedural interface, please use the ESPMegaPRO class in ESPMegaPRO.hpp. + * But note that the ESPMegaPRO class only interfaces with the built-in Digital Input and Digital Output cards and other onboard components. + * It does not provide an interface for expansion cards, the ESPMegaIoT module, and the internal display. + * + * @warning Only one ESPMegaPRO object can be created, creating more than one will result in undefined behavior. + */ +class ESPMegaPRO { + public: + ESPMegaPRO(); + bool begin(); + void loop(); + bool installCard(uint8_t slot, ExpansionCard* card); + bool updateTimeFromNTP(); + void enableIotModule(); + void enableInternalDisplay(HardwareSerial *serial); + void enableWebServer(uint16_t port); + rtctime_t getTime(); + void dumpFRAMtoSerial(uint16_t start, uint16_t end); + void dumpFRAMtoSerialASCII(uint16_t start, uint16_t end); + void setTime(int hours, int minutes, int seconds, int day, int month, int year); + ExpansionCard* getCard(uint8_t slot); + FRAM fram; + /** + * @brief The Digital Input Card Built-in to the ESPMegaPRO board. + * @typedef DigitalInputCard + * @note This card is installed by default at slot 0 on the ESPMegaPRO R3 board. + */ + DigitalInputCard inputs = DigitalInputCard(INPUT_BANK_A_ADDRESS, INPUT_BANK_B_ADDRESS); + /** + * @brief The Digital Output Card Built-in to the ESPMegaPRO board. + * @typedef DigitalOutputCard + * @note This card is installed by default at slot 1 on the ESPMegaPRO R3 board. + */ + DigitalOutputCard outputs = DigitalOutputCard(PWM_BANK_ADDRESS); + /** + * @brief The Display Built-in to the ESPMegaPRO board. + * @typedef InternalDisplay + * @note SKU EMG-PRO-R3-XXX-(F)-(12/24)V does not have a built-in display. + */ + InternalDisplay *display; + /** + * @brief This component is used to connect the ESPMegaPRO board to the internet and communicate with it through MQTT. + * @typedef ESPMegaIoT + * @note You must call enableIotModule() before using this component. + */ + ESPMegaIoT *iot; + /** + * @brief This component is used to create a web server on the ESPMegaPRO board. + * @typedef ESPMegaWebServer + * @note You must call enableWebServer() before using this component. + */ + ESPMegaWebServer *webServer; + private: + bool iotEnabled = false; + bool internalDisplayEnabled = false; + bool webServerEnabled = false; + ExpansionCard* cards[255]; + bool cardInstalled[255]; + uint8_t cardCount = 0; +}; \ No newline at end of file diff --git a/ESPMegaTCP.hpp b/ESPMegaTCP.hpp new file mode 100644 index 0000000..5f59860 --- /dev/null +++ b/ESPMegaTCP.hpp @@ -0,0 +1,9 @@ +// ESPMega TCP API +#include + +class ESPMegaTCP +{ + public: + void begin(); + void loop(); +}; \ No newline at end of file diff --git a/ESPMegaWebServer.cpp b/ESPMegaWebServer.cpp new file mode 100644 index 0000000..83ff910 --- /dev/null +++ b/ESPMegaWebServer.cpp @@ -0,0 +1,516 @@ +/** + * @file ESPMegaWebServer.cpp + * @brief Implementation file for the ESPMegaWebServer class. + * + * This file contains the implementation of the ESPMegaWebServer class, which is responsible for handling web server functionality for the ESPMegaPRO firmware. + * The ESPMegaWebServer class provides methods for starting the web server, handling HTTP requests, and managing credentials and configurations. + */ +#include + +/** + * @brief Construct a new ESPMegaWebServer::ESPMegaWebServer objecy + * + * @note Although you can instantiate this class directly, it is recommended to use the ESPMegaPRO.webServer object instead. + * + * @param port The TCP port to listen on + * @param iot A pointer to the ESPMegaIoT object + */ +ESPMegaWebServer::ESPMegaWebServer(uint16_t port, ESPMegaIoT *iot) +{ + this->port = port; + this->iot = iot; + this->server = new AsyncWebServer(port); + this->saveConfigHandler = new AsyncCallbackJsonWebHandler("/save_config", std::bind(&ESPMegaWebServer::saveConfigJSONHandler, this, std::placeholders::_1, std::placeholders::_2)); +} + +/** + * @brief Destroy the ESPMegaWebServer::ESPMegaWebServer object + */ +ESPMegaWebServer::~ESPMegaWebServer() +{ + delete this->server; +} + +/** + * @brief Start the web server + * + * This method starts the web server and registers the necessary handlers. + * + * @note This method should be called after the ESPMegaIoT object has been initialized. + * @note This method is automatically called if you use ESPMegaPRO::enableWebServer() + */ +void ESPMegaWebServer::begin() +{ + this->loadCredentialsFromFRAM(); + this->server->begin(); + auto bindedDashboardHandler = std::bind(&ESPMegaWebServer::dashboardHandler, this, std::placeholders::_1); + this->server->on("/", HTTP_GET, bindedDashboardHandler); + auto bindedConfigHandler = std::bind(&ESPMegaWebServer::configHandler, this, std::placeholders::_1); + this->server->on("/config", HTTP_GET, bindedConfigHandler); + this->server->addHandler(saveConfigHandler); + auto bindedOtaRequestHandler = std::bind(&ESPMegaWebServer::otaRequestHandler, this, std::placeholders::_1); + auto bindedOtaUploadHandler = std::bind(&ESPMegaWebServer::otaUploadHandler, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6); + this->server->on("/ota_update", HTTP_POST, bindedOtaRequestHandler, bindedOtaUploadHandler); +} + +/** + * @brief The loop function for the web server + * + * @note This method is not used by the ESPMegaWebServer class as of now. + */ +void ESPMegaWebServer::loop() +{ + // AsyncWebServer doesn't have a loop function +} + +/** + * @brief Bind the FRAM object to the web server + * + * This method binds the FRAM object to the web server so that the web server can read and write credentials to the FRAM. + * + * @note The FRAM object must be bound to the web server before calling ESPMegaWebServer::begin() + * @note This class takes 64 bytes of FRAM starting from address 301, however address 301-400 is reserved for it. + * + * @param fram A pointer to the FRAM object + */ +void ESPMegaWebServer::bindFRAM(FRAM *fram) +{ + this->fram = fram; +} + +/** + * @brief Load web username and password from FRAM + * + * This method loads the web server credentials from the FRAM. + * + * @note This method is automatically called by ESPMegaWebServer::begin() + */ +void ESPMegaWebServer::loadCredentialsFromFRAM() +{ + this->fram->read(301, (uint8_t *)this->webUsername, 32); + this->fram->read(333, (uint8_t *)this->webPassword, 32); + // Verify if credentials are valid + // A valid username and password is null terminated + // Scan for null terminator + bool validUsername = false; + bool validPassword = false; + for (int i = 0; i < 32; i++) + { + if (this->webUsername[i] == '\0') + { + validUsername = true; + break; + } + } + for (int i = 0; i < 32; i++) + { + if (this->webPassword[i] == '\0') + { + validPassword = true; + break; + } + } + if (!validUsername || !validPassword) + { + this->resetCredentials(); + return; + } + // A valid username and password is at least 1 character long + if (strlen(this->webUsername) == 0 || strlen(this->webPassword) == 0) + { + this->resetCredentials(); + return; + } +} + +/** + * @brief Save web username and password to FRAM + * + * This method saves the web server credentials to the FRAM. + */ +void ESPMegaWebServer::saveCredentialsToFRAM() +{ + this->fram->write(301, (uint8_t *)this->webUsername, 32); + this->fram->write(333, (uint8_t *)this->webPassword, 32); +} + +/** + * @brief Reset web username and password to default + * + * This method resets the web server credentials to the default username and password. + * + * @note The default username and password is both "admin" + */ +void ESPMegaWebServer::resetCredentials() +{ + // The default username and password is "admin" + strcpy(this->webUsername, "admin"); + strcpy(this->webPassword, "admin"); + this->saveCredentialsToFRAM(); +} + +/** + * @brief Get the web username + * + * @warning The returned pointer should not be freed or modified. + * + * @return The web username + */ +char *ESPMegaWebServer::getWebUsername() +{ + return this->webUsername; +} + +/** + * @brief Get the web password + * + * @warning The returned pointer should not be freed or modified. + * + * @return The web password + */ +char *ESPMegaWebServer::getWebPassword() +{ + return this->webPassword; +} + +/** + * @brief Set the web username + * + * @param username The new web username + */ +void ESPMegaWebServer::setWebUsername(const char *username) +{ + strcpy(this->webUsername, username); +} + +/** + * @brief Set the web password + * + * @param password The new web password + */ +void ESPMegaWebServer::setWebPassword(const char *password) +{ + strcpy(this->webPassword, password); +} + +/** + * @brief Handle HTTP requests to the dashboard (/) page + * + * @param request The AsyncWebServerRequest object + */ +void ESPMegaWebServer::dashboardHandler(AsyncWebServerRequest *request) +{ + if (!request->authenticate(this->webUsername, this->webPassword)) + { + return request->requestAuthentication(); + } + auto bindedDashboardProcessor = std::bind(&ESPMegaWebServer::dashboardProcessor, this, std::placeholders::_1); + request->send_P(200, "text/html", ota_html, bindedDashboardProcessor); +} + +/** + * @brief Replace placeholders in the dashboard HTML with values + * + * @param var The placeholder name + * @return The value to replace the placeholder with + */ +String ESPMegaWebServer::dashboardProcessor(const String &var) +{ + if (var == "hostname") + { + return String(this->iot->getNetworkConfig()->hostname); + } + else if (var == "ip_address") + { + return this->iot->getIp().toString(); + } + else if (var == "mac_address") + { + return this->iot->getMac(); + } + else if (var == "model") + { + return String("ESPMega PRO R3.3c"); + } + else if (var == "mqtt_connection_string") + { + MqttConfig *mqttConfig = this->iot->getMqttConfig(); + String connectionString; + connectionString += mqttConfig->mqtt_server; + connectionString += ":"; + connectionString += mqttConfig->mqtt_port; + return connectionString; + } + else if (var == "base_topic") + { + return String(this->iot->getMqttConfig()->base_topic); + } + else if (var == "mqtt_connected") + { + return this->iot->mqttConnected() ? "Connected" : "Standalone"; + } + return ""; +} + +/** + * @brief Handle HTTP requests to the config (/config) page + * + * @param request The AsyncWebServerRequest object + */ +void ESPMegaWebServer::configHandler(AsyncWebServerRequest *request) +{ + if (!request->authenticate(this->webUsername, this->webPassword)) + { + return request->requestAuthentication(); + } + auto bindedConfigProcessor = std::bind(&ESPMegaWebServer::configProcessor, this, std::placeholders::_1); + request->send_P(200, "text/html", config_html, bindedConfigProcessor); +} + +/** + * @brief Replace placeholders in the config HTML with values + * + * @param var The placeholder name + * + * @return The value to replace the placeholder with + */ +String ESPMegaWebServer::configProcessor(const String &var) +{ + MqttConfig *mqttConfig = this->iot->getMqttConfig(); + NetworkConfig *networkConfig = this->iot->getNetworkConfig(); + if (var == "ip_address") + { + return networkConfig->ip.toString(); + } + else if (var == "netmask") + { + return networkConfig->subnet.toString(); + } + else if (var == "gateway") + { + return networkConfig->gateway.toString(); + } + else if (var == "dns") + { + return networkConfig->dns1.toString(); + } + else if (var == "hostname") + { + return String(networkConfig->hostname); + } + else if (var == "bms_ip") + { + return String(mqttConfig->mqtt_server); + } + else if (var == "bms_port") + { + return String(mqttConfig->mqtt_port); + } + else if (var == "bms_useauth") + { + return mqttConfig->mqtt_useauth ? "checked=\"checked\"" : ""; + } + else if (var == "bms_username") + { + return String(mqttConfig->mqtt_user); + } + else if (var == "bms_password") + { + return String(mqttConfig->mqtt_password); + } + else if (var == "bms_endpoint") + { + return String(mqttConfig->base_topic); + } + else if (var == "web_username") + { + return String(this->webUsername); + } + else if (var == "web_password") + { + return String(this->webPassword); + } + return ""; +} + +/** + * @brief Handle HTTP requests to the OTA update (/ota_update) page + * + * @param request The AsyncWebServerRequest object + */ +void ESPMegaWebServer::otaRequestHandler(AsyncWebServerRequest *request) +{ + // Prepare to receive firmware + if (!request->authenticate(this->webUsername, this->webPassword)) + { + return request->requestAuthentication(); + } + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + response->addHeader("Connection", "close"); + request->send(response); + // Restart ESPMega + ESP.restart(); +} + +/** + * @brief Handle HTTP upload session to the OTA update (/ota_update) page + * + * @param request The AsyncWebServerRequest object + * @param filename The filename of the firmware + * @param index The index of the firmware + * @param data The firmware data + * @param len The length of the firmware data + * @param final Whether this is the final chunk of firmware + */ +void ESPMegaWebServer::otaUploadHandler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + // Receive firmware + if (!request->authenticate(this->webUsername, this->webPassword)) + { + return request->requestAuthentication(); + } + if (index == 0) + { + ESP_LOGI("ESPMegaWebServer", "OTA Update Start"); + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) + { // start with max available size + ESP_LOGE("ESPMegaWebServer", "OTA Update Start Error"); + Update.printError(Serial); + } + } + if (Update.write(data, len) != len) + { + ESP_LOGE("ESPMegaWebServer", "OTA Update Write Error"); + Update.printError(Serial); + } else { + ESP_LOGI("ESPMegaWebServer", "OTA Update Write Success: %uB", index + len); + } + if (final) + { + if (Update.end(true)) + { // true to set the size to the current progress + ESP_LOGI("ESPMegaWebServer", "OTA Update Success: %uB", index + len); + } + else + { + ESP_LOGE("ESPMegaWebServer", "OTA Update End Error"); + Update.printError(Serial); + } + } +} + +/** + * @brief Handle JSON POST requests to the save_config (/save_config) page + * + * @param request The AsyncWebServerRequest object + * @param json The JSON object representing the request body + */ +void ESPMegaWebServer::saveConfigJSONHandler(AsyncWebServerRequest *request, JsonVariant &json) +{ + /** + * Request POST body should be a JSON object + * containing the following fields: + * ip_address: String, the IP address of the device + * netmask: String, the netmask of the device + * gateway: String, the gateway of the device + * dns: String, the DNS of the device + * hostname: String, the hostname of the device + * bms_ip: String, the IP address of the MQTT broker + * bms_port: int, the port of the MQTT broker + * bms_useauth: Boolean, true if the MQTT broker requires authentication + * bms_username: String, the username of the MQTT broker + * bms_password: String, the password of the MQTT broker + * bms_endpoint: String, the base topic of the MQTT broker + * web_username: String, the username of the web server + * web_password: String, the password of the web server + */ + ESP_LOGD("ESPMegaWebServer", "Saving config"); + JsonObject root = json.as(); + // Network Config + NetworkConfig networkConfig; + networkConfig.useStaticIp = true; + networkConfig.useWifi = false; + IPAddress ip; + ESP_LOGD("ESPMegaWebServer", "Checking IP Address"); + if (!ip.fromString(root["ip_address"].as())) + { + ESP_LOGE("ESPMegaWebServer", "Invalid Config IP Address"); + request->send(400, "text/plain", "Invalid IP Address"); + return; + } + networkConfig.ip = ip; + ESP_LOGD("ESPMegaWebServer", "Checking Netmask"); + if (!ip.fromString(root["netmask"].as())) + { + ESP_LOGE("ESPMegaWebServer", "Invalid Config Netmask"); + request->send(400, "text/plain", "Invalid Netmask"); + return; + } + networkConfig.subnet = ip; + ESP_LOGD("ESPMegaWebServer", "Checking Gateway"); + if (!ip.fromString(root["gateway"].as())) + { + ESP_LOGE("ESPMegaWebServer", "Invalid Config Gateway"); + request->send(400, "text/plain", "Invalid Gateway"); + return; + } + networkConfig.gateway = ip; + ESP_LOGD("ESPMegaWebServer", "Checking DNS"); + if (!ip.fromString(root["dns"].as())) + { + ESP_LOGE("ESPMegaWebServer", "Invalid Config DNS"); + request->send(400, "text/plain", "Invalid DNS"); + return; + } + networkConfig.dns1 = ip; + ESP_LOGD("ESPMegaWebServer", "Setting Hostname"); + strcpy(networkConfig.hostname, root["hostname"].as().c_str()); + // MQTT Config + MqttConfig mqttConfig; + ESP_LOGD("ESPMegaWebServer", "Setting MQTT Server"); + strcpy(mqttConfig.mqtt_server, root["bms_ip"].as().c_str()); + ESP_LOGD("ESPMegaWebServer", "Checking MQTT Port"); + uint16_t mqttPort = root["bms_port"].as(); + if (mqttConfig.mqtt_port <= 0 || mqttConfig.mqtt_port > 65535) + { + ESP_LOGE("ESPMegaWebServer", "Invalid Config MQTT Port"); + request->send(400, "text/plain", "Invalid MQTT Port"); + return; + } + mqttConfig.mqtt_port = mqttPort; + ESP_LOGD("ESPMegaWebServer", "Checking MQTT Use Auth"); + mqttConfig.mqtt_useauth = root["bms_useauth"].as(); + ESP_LOGD("ESPMegaWebServer", "Setting MQTT Username"); + strcpy(mqttConfig.mqtt_user, root["bms_username"].as().c_str()); + ESP_LOGD("ESPMegaWebServer", "Setting MQTT Password"); + strcpy(mqttConfig.mqtt_password, root["bms_password"].as().c_str()); + ESP_LOGD("ESPMegaWebServer", "Setting MQTT Base Topic"); + strcpy(mqttConfig.base_topic, root["bms_endpoint"].as().c_str()); + // Web Server Config + ESP_LOGD("ESPMegaWebServer", "Setting Web Username"); + strcpy(this->webUsername, root["web_username"].as().c_str()); + ESP_LOGD("ESPMegaWebServer", "Setting Web Password"); + strcpy(this->webPassword, root["web_password"].as().c_str()); + // Commit changes to FRAM + ESP_LOGD("ESPMegaWebServer", "Committing Network Config to FRAM"); + this->iot->setNetworkConfig(networkConfig); + this->iot->saveNetworkConfig(); + ESP_LOGD("ESPMegaWebServer", "Committing MQTT Config to FRAM"); + this->iot->setMqttConfig(mqttConfig); + this->iot->saveMqttConfig(); + ESP_LOGD("ESPMegaWebServer", "Committing Web Server Config to FRAM"); + this->saveCredentialsToFRAM(); + ESP_LOGD("ESPMegaWebServer", "Config saved"); + // Send response + request->send(200, "text/plain", "OK"); + ESP.restart(); +} + +/** + * @brief Get the AsyncWebServer object + * + * @return The AsyncWebServer object + */ +AsyncWebServer *ESPMegaWebServer::getServer() { + return this->server; +} \ No newline at end of file diff --git a/ESPMegaWebServer.hpp b/ESPMegaWebServer.hpp new file mode 100644 index 0000000..00a4c9e --- /dev/null +++ b/ESPMegaWebServer.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + + +/** + * @brief Provides a web server for ESPMegaPRO + * + * This class provides a web server for ESPMegaPRO. It is used to configure the device and to update the firmware. + * This class also allows to save the credentials to access the web server in the FRAM memory. + * User can also add custom endpoints to the web server. + * + * This class use FRAM address 301-400 + */ +class ESPMegaWebServer +{ + public: + ESPMegaWebServer(uint16_t port, ESPMegaIoT *iot); + ~ESPMegaWebServer(); + void begin(); + void loop(); + void resetCredentials(); + char* getWebUsername(); + char* getWebPassword(); + void setWebUsername(const char* username); + void setWebPassword(const char* password); + void bindFRAM(FRAM *fram); + void loadCredentialsFromFRAM(); + void saveCredentialsToFRAM(); + AsyncWebServer* getServer(); + private: + // FRAM + FRAM *fram; + // Credentials + char webUsername[32]; + char webPassword[32]; + // Web Server + AsyncWebServer *server; + uint16_t port; + // ESPMegaIoT + ESPMegaIoT *iot; + // Endpoints Handlers + void dashboardHandler(AsyncWebServerRequest *request); + String dashboardProcessor(const String& var); + void configHandler(AsyncWebServerRequest *request); + String configProcessor(const String& var); + AsyncCallbackJsonWebHandler *saveConfigHandler; + void saveConfigJSONHandler(AsyncWebServerRequest *request, JsonVariant &json); + void otaRequestHandler(AsyncWebServerRequest *request); + void otaUploadHandler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); + void restAPIHandler(AsyncWebServerRequest *request); +}; \ No newline at end of file diff --git a/ExpansionCard.hpp b/ExpansionCard.hpp new file mode 100644 index 0000000..b656240 --- /dev/null +++ b/ExpansionCard.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +/** + * @brief The base class for all expansion cards + * + * In order to create a new expansion card, you should create a new class that inherits from this class. + * Your class should implement the following functions: + * - begin() : Initialize the card + * - loop() : A function that is called in the main loop + * - getType() : Get the type of the card, The type should be a unique number between 0 and 255 + * + * @warning This class is abstract and should not be instantiated directly. + */ +class ExpansionCard { + public: + // Instantiate the card with the specified address + ExpansionCard() {} + virtual bool begin(); + // Preform a loop to refresh the input buffers + virtual void loop(); + // Get the card type + virtual uint8_t getType(); +}; \ No newline at end of file diff --git a/InternalDisplay.cpp b/InternalDisplay.cpp new file mode 100644 index 0000000..3f30965 --- /dev/null +++ b/InternalDisplay.cpp @@ -0,0 +1,872 @@ +#include + +/** + * @brief Initialize the Internal Display + * + * @note You should not call this function directly, instead use ESPMegaIoT::enableInternalDisplay() + * + * @param iot The ESPMegaIoT object + * @param getRtcTime A function that returns the current time + */ +void InternalDisplay::begin(ESPMegaIoT *iot, std::function getRtcTime) +{ + this->iot = iot; + this->getRtcTime = getRtcTime; + this->mqttConfig = this->iot->getMqttConfig(); + this->networkConfig = this->iot->getNetworkConfig(); + // Register callbacks + auto bindedPageChangeCallback = std::bind(&InternalDisplay::handlePageChange, this, std::placeholders::_1); + this->registerPageChangeCallback(bindedPageChangeCallback); + auto bindedTouchCallback = std::bind(&InternalDisplay::handleTouch, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + this->registerTouchCallback(bindedTouchCallback); + // Initialize the display + this->displayAdapter->begin(115200); + this->displayAdapter->setTimeout(100); + this->displayAdapter->flush(); + this->reset(); + delay(500); + this->jumpToPage(1); +} + +/** + * @brief The main loop of the Internal Display + * + * @note You should not call this function directly, instead use ESPMega::loop() + */ +void InternalDisplay::loop() +{ + // Keep reading the Serial Adapter + this->recieveSerialCommand(); + + // Refresh the top bar every 5 seconds + static uint32_t lastTopBarRefresh; + if (millis() - lastTopBarRefresh > INTERNAL_DISPLAY_TOP_BAR_REFRESH_INTERVAL) + { + this->updateStatusIcons(this->iot->networkConnected(), this->iot->mqttConnected()); + lastTopBarRefresh = millis(); + } + // Refresh the clock every 10 seconds + static uint32_t lastClockRefresh; + if (millis() - lastClockRefresh > INTERNAL_DISPLAY_CLOCK_REFRESH_INTERVAL) + { + this->updateClock(); + lastClockRefresh = millis(); + } +} + +/** + * @brief Update the display in response to a change in the input state + * + * @param pin The pin that changed + * @param state The new state of the pin + */ +void InternalDisplay::handleInputStateChange(uint8_t pin, bool state) +{ + // If the input card is binded to the display and the current page is the input page + // then update the respective input component + if (this->inputCard != nullptr || this->currentPage != INTERNAL_DISPLAY_INPUT_PAGE) + return; + // Update the input state + this->setInputMarker(pin, state); +} + +/** + * @brief Update the display in response to a change in the PWM state + * + * @param pin The pin that changed + * @param state The new state of the pin + * @param value The new value of the pin + */ +void InternalDisplay::handlePwmStateChange(uint8_t pin, bool state, uint16_t value) +{ + // If the output card is binded to the display and the current page is the output page + // then update the respective output component + if (this->outputCard != nullptr || this->currentPage != INTERNAL_DISPLAY_OUTPUT_PAGE) + return; + // Update the output state + this->setOutputBar(pin, value); + this->setOutputStateColor(pin, state); + // Refresh the PWM Adjustment page if the current page is the PWM Adjustment page and the pin is the same + if (this->currentPage == INTERNAL_DISPLAY_PWM_ADJUSTMENT_PAGE && this->pmwAdjustmentPin == pin) + { + this->refreshPWMAdjustment(); + } +} + +/** + * @brief Update the display in response to page change + * + * @param page The new page + */ +void InternalDisplay::handlePageChange(uint8_t page) +{ + // Refresh the page + this->refreshPage(page); +} + +/** + * @brief Save the network config to the FRAM + */ +void InternalDisplay::saveNetworkConfig() +{ + // The network config page have the following components: + // ip_set -> a text input to set the ip address + // netmask_set -> a text input to set the netmask + // gateway_set -> a text input to set the gateway + // dns_set -> a text input to set the dns + // host_set -> a text input to set the hostname + + // Save the ip address + IPAddress ip; + // 000.000.000.000, 16 characters, 3 dots, 3 characters per octet, 1 null terminator + char ip_buffer[30]; + if(!this->getStringToBuffer("ip_set.txt", ip_buffer, 30)) + { + return; + } + + // Validate the ip address + if (!ip.fromString(ip_buffer)) { + return; + } + + // Save the netmask + IPAddress netmask; + if (!this->getStringToBuffer("netmask_set.txt", ip_buffer, 30)) { + return; + } + // Validate the netmask + if (!netmask.fromString(ip_buffer)) { + return; + } + + // Save the gateway + IPAddress gateway; + if (!this->getStringToBuffer("gateway_set.txt", ip_buffer, 30)) { + return; + } + // Validate the gateway + if (!gateway.fromString(ip_buffer)) { + return; + } + + // Save the dns + IPAddress dns; + if(!this->getStringToBuffer("dns_set.txt", ip_buffer, 30)) + return; + // Validate the dns + if (!dns.fromString(ip_buffer)) + return; + + // Save the hostname + if(!this->getStringToBuffer("host_set.txt", this->networkConfig->hostname, 32)) + return; + + // Write the ip address, netmask and gateway to the network config + this->networkConfig->ip = ip; + this->networkConfig->subnet = netmask; + this->networkConfig->gateway = gateway; + this->networkConfig->dns1 = dns; + this->networkConfig->dns2 = dns; + this->networkConfig->useStaticIp = true; + this->networkConfig->useWifi = false; + this->networkConfig->wifiUseAuth = false; + this->iot->saveNetworkConfig(); + ESP.restart(); +} + +/** + * @brief Save the MQTT config to the FRAM + */ +void InternalDisplay::saveMQTTConfig() +{ + // The MQTT config page have the following components: + // mqttsv_set -> a text input to set the mqtt server + // port_set -> a text input to set the mqtt port + // use_auth -> a checkbox to enable/disable mqtt authentication + // user_set -> a text input to set the mqtt username + // password_set -> a text input to set the mqtt password + // topic_set -> a text input to set the mqtt base topic + + // Send the stop bytes to flush the serial buffer + this->sendStopBytes(); + + // Save the mqtt server + if(!this->getStringToBuffer("mqttsv_set.txt", this->mqttConfig->mqtt_server, 16)) + return; + + // Save the mqtt port + this->mqttConfig->mqtt_port = this->getNumber("port_set.val"); + + // Save the mqtt username + if(!this->getStringToBuffer("user_set.txt", this->mqttConfig->mqtt_user, 16)) + return; + + // Save the mqtt password + if(!this->getStringToBuffer("password_set.txt", this->mqttConfig->mqtt_password, 16)) + return; + + // Save the mqtt base topic + if(!this->getStringToBuffer("topic_set.txt", this->mqttConfig->base_topic, 16)) + return; + + // Save the mqtt use auth + uint8_t use_auth = this->getNumber("use_auth.val"); + this->mqttConfig->mqtt_useauth = use_auth == 1 ? true : false; + this->iot->saveMqttConfig(); + ESP.restart(); +} + +/** + * @brief Update the status icons on the Internal Display's top bar + * + * @param networkStatus The network status + * @param mqttStatus The MQTT status + */ +void InternalDisplay::updateStatusIcons(bool networkStatus, bool mqttStatus) +{ + this->setNumber("server.pic", mqttStatus ? PIC_MQTT_CONNECTED : PIC_MQTT_DISCONNECTED); + this->setNumber("lan.pic", networkStatus ? PIC_LAN_CONNECTED : PIC_LAN_DISCONNECTED); +} + +/** + * @brief Update the clock on the Internal Display's top bar + */ +void InternalDisplay::updateClock() +{ + rtctime_t time = this->getRtcTime(); + this->displayAdapter->printf("time.txt=\"%02d:%02d %s\"", time.hours % 12, time.minutes, time.hours / 12 ? "PM" : "AM"); + this->sendStopBytes(); +} + +/** + * @brief Send data to display element on the current page + */ +void InternalDisplay::refreshPage() +{ + this->refreshPage(this->currentPage); +} + +/** + * @brief Send data to display element on the specified page + * + * @note The current page must be the specified page + * + * @param page The page to refresh + */ +void InternalDisplay::refreshPage(uint8_t page) +{ + switch (page) + { + case INTERNAL_DISPLAY_DASHBOARD_PAGE: + this->refreshDashboard(); + break; + case INTERNAL_DISPLAY_INPUT_PAGE: + if (this->inputCard == nullptr) + { + this->jumpToPage(INTERNAL_DISPLAY_INPUT_NULL_PTR_PAGE); + break; + } + this->refreshInput(); + break; + case INTERNAL_DISPLAY_OUTPUT_PAGE: + if (this->outputCard == nullptr) + { + this->jumpToPage(INTERNAL_DISPLAY_OUTPUT_NULL_PTR_PAGE); + break; + } + this->refreshOutput(); + break; + case INTERNAL_DISPLAY_AC_PAGE: + if (this->climateCard == nullptr) + { + this->jumpToPage(INTERNAL_DISPLAY_CLIMATE_NULL_PTR_PAGE); + break; + } + this->refreshAC(); + break; + case INTERNAL_DISPLAY_PWM_ADJUSTMENT_PAGE: + this->refreshPWMAdjustment(); + break; + case INTERNAL_DISPLAY_NETWORK_CONFIG_PAGE: + this->refreshNetworkConfig(); + break; + case INTERNAL_DISPLAY_MQTT_CONFIG_PAGE: + this->refreshMQTTConfig(); + break; + default: + break; + } +} + +/** + * @brief Send data to display element on the dashboard page + */ +void InternalDisplay::refreshDashboard() +{ + // The dashboard have the following components: + // 1. Hostname + // 2. IP Address + // 3. MQTT Server with port + // 4. MQTT Connection status + this->setString("hostname.txt", this->networkConfig->hostname); + // Construct the IP address string + static char ip_address[25]; + sprintf(ip_address, "%d.%d.%d.%d", this->networkConfig->ip[0], this->networkConfig->ip[1], this->networkConfig->ip[2], this->networkConfig->ip[3]); + this->setString("ip_address.txt", ip_address); + // Send the MQTT server and port + this->displayAdapter->print("server_address.txt=\""); + this->displayAdapter->print(this->mqttConfig->mqtt_server); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Send the MQTT connection status + this->setString("status_txt.txt", this->iot->mqttConnected() ? MSG_MQTT_CONNECTED : MSG_MQTT_DISCONNECTED); +} + +/** + * @brief Send data to display element on the input page + */ +void InternalDisplay::refreshInput() +{ + for (uint8_t i = 0; i < 16; i++) + { + this->setInputMarker(i, this->inputCard->digitalRead(i, false)); + } +} + +/** + * @brief Send data to display element on the output page + */ +void InternalDisplay::refreshOutput() +{ + for (uint8_t i = 0; i < 16; i++) + { + this->setOutputBar(i, this->outputCard->getValue(i)); + this->setOutputStateColor(i, this->outputCard->getState(i)); + } +} + +/** + * @brief Send data to display element on the AC page + */ +void InternalDisplay::refreshAC() +{ + this->displayAdapter->print("temp.txt=\""); + this->displayAdapter->print(this->climateCard->getTemperature()); + this->displayAdapter->print("C\""); + this->sendStopBytes(); + this->displayAdapter->print("fan_auto.pic="); + this->displayAdapter->print(this->climateCard->getFanSpeed() == AC_FAN_SPEED_AUTO ? PIC_AC_FAN_SPEED_AUTO_ACTIVE : PIC_AC_FAN_SPEED_AUTO_INACTIVE); + this->sendStopBytes(); + this->displayAdapter->print("fan_low.pic="); + this->displayAdapter->print(this->climateCard->getFanSpeed() == AC_FAN_SPEED_LOW ? PIC_AC_FAN_SPEED_LOW_ACTIVE : PIC_AC_FAN_SPEED_LOW_INACTIVE); + this->sendStopBytes(); + this->displayAdapter->print("fan_mid.pic="); + this->displayAdapter->print(this->climateCard->getFanSpeed() == AC_FAN_SPEED_MEDIUM ? PIC_AC_FAN_SPEED_MEDIUM_ACTIVE : PIC_AC_FAN_SPEED_MEDIUM_INACTIVE); + this->sendStopBytes(); + this->displayAdapter->print("fan_high.pic="); + this->displayAdapter->print(this->climateCard->getFanSpeed() == AC_FAN_SPEED_HIGH ? PIC_AC_FAN_SPEED_HIGH_ACTIVE : PIC_AC_FAN_SPEED_HIGH_INACTIVE); + this->sendStopBytes(); + this->displayAdapter->print("mode_off.pic="); + this->displayAdapter->print(this->climateCard->getMode() == AC_MODE_OFF ? PIC_AC_MODE_OFF_ACTIVE : PIC_AC_MODE_OFF_INACTIVE); + this->sendStopBytes(); + this->displayAdapter->print("mode_fan.pic="); + this->displayAdapter->print(this->climateCard->getMode() == AC_MODE_FAN_ONLY ? PIC_AC_MODE_FAN_ACTIVE : PIC_AC_MODE_FAN_INACTIVE); + this->sendStopBytes(); + this->displayAdapter->print("mode_cool.pic="); + this->displayAdapter->print(this->climateCard->getMode() == AC_MODE_COOL ? PIC_AC_MODE_COOL_ACTIVE : PIC_AC_MODE_COOL_INACTIVE); + this->sendStopBytes(); + if (this->climateCard->getSensorType() == AC_SENSOR_TYPE_DHT22) + { + this->displayAdapter->print("roomtemp.txt=\""); + this->displayAdapter->print(this->climateCard->getRoomTemperature()); + this->displayAdapter->print("C\""); + this->sendStopBytes(); + this->displayAdapter->print("roomhumid.txt=\""); + this->displayAdapter->print(this->climateCard->getHumidity()); + this->displayAdapter->print("%\""); + this->sendStopBytes(); + } + else if (this->climateCard->getSensorType() == AC_SENSOR_TYPE_DS18B20) + { + this->displayAdapter->print("roomtemp.txt=\""); + this->displayAdapter->print(this->climateCard->getRoomTemperature()); + this->displayAdapter->print("C\""); + this->sendStopBytes(); + this->setString("roomhumid.txt", "N/A"); + } + else + { + this->setString("roomtemp.txt", "N/A"); + this->setString("roomhumid.txt", "N/A"); + } +} + +/** + * @brief Set the PWM status output bar value (Fullness of the bar) + * + * @param pin The pin of the PWM + * @param value The value of the PWM (0 - 4095) + */ +void InternalDisplay::setOutputBar(uint8_t pin, uint16_t value) +{ + // Write the value to the output bar + this->displayAdapter->print("j"); + this->displayAdapter->print(pin); + this->displayAdapter->print(".val="); + this->displayAdapter->print((int)(value * 100 / 4095)); + this->sendStopBytes(); +} + +/** + * @brief Set the PWM status output bar color to match the PWM state + * + * @param pin The pin of the PWM + * @param state The state of the PWM + */ +void InternalDisplay::setOutputStateColor(uint8_t pin, bool state) +{ + this->displayAdapter->print("j"); + this->displayAdapter->print(pin); + this->displayAdapter->print(".ppic="); + this->displayAdapter->print(state ? PIC_PWM_BAR_ON : PIC_PWM_BAR_OFF); + this->sendStopBytes(); +} + +/** + * @brief Set Input Marker to match the input state + * + * @param pin The pin of the input + * @param state The state of the input + */ +void InternalDisplay::setInputMarker(uint8_t pin, bool state) +{ + this->displayAdapter->print("I"); + this->displayAdapter->print(pin); + this->displayAdapter->print(".val="); + this->displayAdapter->print(state ? 1 : 0); + this->sendStopBytes(); +} + +/** + * @brief Create a new Internal Display object + * + * @param displayAdapter The HardwareSerial object that is connected to the display + */ +InternalDisplay::InternalDisplay(HardwareSerial *displayAdapter) : ESPMegaDisplay(displayAdapter) +{ + this->currentPage = INTERNAL_DISPLAY_DASHBOARD_PAGE; + this->iot = nullptr; + this->inputCard = nullptr; + this->outputCard = nullptr; + this->climateCard = nullptr; + this->pmwAdjustmentPin = 0; +} + +/** + * @brief Set the input card to be be shown on the input page + * + * @param inputCard The input card object to be shown + */ +void InternalDisplay::bindInputCard(DigitalInputCard *inputCard) +{ + // Check if the input card is already binded + // If it is, then unbind it first + if (this->inputCard != nullptr) + this->unbindInputCard(); + this->inputCard = inputCard; + auto bindedInputStateChangeCallback = + std::bind(&InternalDisplay::handleInputStateChange, this, + std::placeholders::_1, std::placeholders::_2); + this->bindedInputCardCallbackHandler = + this->inputCard->registerCallback(bindedInputStateChangeCallback); +} + +/** + * @brief Unbind the input card from the display + */ +void InternalDisplay::unbindInputCard() +{ + if (this->inputCard == nullptr) + return; + this->inputCard->unregisterCallback(this->bindedInputCardCallbackHandler); + this->inputCard = nullptr; +} + +/** + * @brief Set the output card to be be shown on the output page + * + * @param outputCard The output card object to be shown + */ +void InternalDisplay::bindOutputCard(DigitalOutputCard *outputCard) +{ + // Check if the output card is already binded + // If it is, then unbind it first + if (this->outputCard != nullptr) + this->unbindOutputCard(); + this->outputCard = outputCard; + auto bindedPwmStateChangeCallback = std::bind(&InternalDisplay::handlePwmStateChange, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + this->bindedOutputCardCallbackHandler = + this->outputCard->registerChangeCallback(bindedPwmStateChangeCallback); +} + +/** + * @brief Unbind the output card from the display + */ +void InternalDisplay::unbindOutputCard() +{ + if (this->outputCard == nullptr) + return; + this->outputCard->unregisterChangeCallback(this->bindedOutputCardCallbackHandler); + this->outputCard = nullptr; +} + +/** + * @brief Set the climate card to be be shown on the AC page + * + * This assume that your ClimeateCard has the mode and fan speed names in the following order: + * mode: [off, fan_only, cool] + * fan_speed: [auto, low, medium, high] + * + * @param climateCard The climate card object to be shown + */ +void InternalDisplay::bindClimateCard(ClimateCard *climateCard) +{ + // Check if the climate card is already binded + // If it is, then unbind it first + if (this->climateCard != nullptr) + this->unbindClimateCard(); + this->climateCard = climateCard; + auto bindedACStateChangeCallback = std::bind(&InternalDisplay::handleACStateChange, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + this->bindedClimateCardCallbackHandler = + this->climateCard->registerChangeCallback(bindedACStateChangeCallback); +} + +/** + * @brief Unbind the climate card from the display + */ +void InternalDisplay::unbindClimateCard() +{ + if (this->climateCard == nullptr) + return; + this->climateCard->unregisterChangeCallback(this->bindedClimateCardCallbackHandler); + this->climateCard = nullptr; +} + +/** + * @brief Send data to display element on the PWM Adjustment page + */ +void InternalDisplay::refreshPWMAdjustment() +{ + // The PWM Adjustment page have the following components: + // pwm_value -> a slider to adjust the PWM value + // pwm_state -> a button to toggle the PWM state + // pwm_id -> a text to show the PWM pin + + // Refresh the PWM pin + this->refreshPWMAdjustmentId(); + // Refresh the PWM value + this->refreshPWMAdjustmentSlider(); + // Refresh the PWM state + this->refreshPWMAdjustmentState(); +} + +/** + * @brief Send the PWM pin id to the display on the PWM Adjustment page + */ +void InternalDisplay::refreshPWMAdjustmentId() +{ + // Send the PWM pin + this->displayAdapter->print("pwm_id.txt=\"P"); + this->displayAdapter->print(pmwAdjustmentPin); + this->displayAdapter->print("\""); + this->sendStopBytes(); +} + +/** + * @brief Send the PWM value to the display on the PWM Adjustment page + */ +void InternalDisplay::refreshPWMAdjustmentSlider() +{ + // Send the PWM value + this->displayAdapter->print("pwm_value.val="); + this->displayAdapter->print(this->outputCard->getValue(this->pmwAdjustmentPin)); + this->sendStopBytes(); +} + +/** + * @brief Send the PWM state to the display on the PWM Adjustment page + */ +void InternalDisplay::refreshPWMAdjustmentState() +{ + // Send the PWM state + this->displayAdapter->print("pwm_state.txt=\""); + this->displayAdapter->print(this->outputCard->getState(this->pmwAdjustmentPin) ? MSG_PWM_ADJUSTMENT_STATE_ON : MSG_PWM_ADJUSTMENT_STATE_OFF); + this->displayAdapter->print("\""); + this->sendStopBytes(); +} + +/** + * @brief Handle the touch event on the display + * + * @param page The page that the touch event occured + * @param component The component that the touch event occured + * @param type The type of the touch event + */ +void InternalDisplay::handleTouch(uint8_t page, uint8_t component, uint8_t type) +{ + // Switch based on the page + switch (page) + { + case INTERNAL_DISPLAY_AC_PAGE: + this->handleACTouch(type, component); + break; + case INTERNAL_DISPLAY_PWM_ADJUSTMENT_PAGE: + this->handlePWMAdjustmentTouch(type, component); + break; + case INTERNAL_DISPLAY_NETWORK_CONFIG_PAGE: + if (type == TOUCH_TYPE_RELEASE && component == 7) + this->saveNetworkConfig(); + break; + case INTERNAL_DISPLAY_MQTT_CONFIG_PAGE: + if (type == TOUCH_TYPE_RELEASE && component == 2) + this->saveMQTTConfig(); + break; + default: + break; + } +} + +/** + * @brief Handle the touch event on the AC page + * + * @param type The type of the touch event + * @param component The component that the touch event occured + */ +void InternalDisplay::handleACTouch(uint8_t type, uint8_t component) +{ + // b1 [component 18] -> inclement AC temperature by 1 + // b0 [component 17] -> declement AC temperature by 1 + // fan_auto [component 4] -> set the fan speed to auto + // fan_low [component 5] -> set the fan speed to low + // fan_med [component 6] -> set the fan speed to medium + // fan_high [component 7] -> set the fan speed to high + // mode_off [component 10] -> set the mode to off + // mode_fan [component 9] -> set the mode to fan only + // mode_cool [component 8] -> set the mode to cool + + // For b0 and b1, if the type is not release then return + // For other components, if the type is not press then return + if ((component == 17 || component == 18) && type != TOUCH_TYPE_RELEASE) + return; + if ((component != 17 && component != 18) && type != TOUCH_TYPE_PRESS) + return; + + // Switch based on the component + switch (component) + { + case 17: + // Decrement the temperature + this->climateCard->setTemperature(this->climateCard->getTemperature() - 1); + break; + case 18: + // Increment the temperature + this->climateCard->setTemperature(this->climateCard->getTemperature() + 1); + break; + case 4: + // Set the fan speed to auto + this->climateCard->setFanSpeed(AC_FAN_SPEED_AUTO); + break; + case 5: + // Set the fan speed to low + this->climateCard->setFanSpeed(AC_FAN_SPEED_LOW); + break; + case 6: + // Set the fan speed to medium + this->climateCard->setFanSpeed(AC_FAN_SPEED_MEDIUM); + break; + case 7: + // Set the fan speed to high + this->climateCard->setFanSpeed(AC_FAN_SPEED_HIGH); + break; + case 10: + // Set the mode to off + this->climateCard->setMode(AC_MODE_OFF); + break; + case 9: + // Set the mode to fan only + this->climateCard->setMode(AC_MODE_FAN_ONLY); + break; + case 8: + // Set the mode to cool + this->climateCard->setMode(AC_MODE_COOL); + break; + default: + break; + } +} + +/** + * @brief Handle the touch event on the PWM Adjustment page + * + * @param type The type of the touch event + * @param component The component that the touch event occured + */ +void InternalDisplay::handlePWMAdjustmentTouch(uint8_t type, uint8_t component) +{ + // b0 [component 5] -> decrement the PWM id if its greater than 0, else set it to 15 + // b1 [component 6] -> increment the PWM id if its less than 15, else set it to 0 + // pwm_state [component 4] -> toggle the PWM state + // pwm_value [component 1] -> set the PWM value based on the slider value + // If the type is not release then return + if (type != TOUCH_TYPE_RELEASE) + return; + uint16_t val = 0; + // switch based on the component + switch (component) + { + case 5: + // Decrement the PWM id + this->pmwAdjustmentPin = this->pmwAdjustmentPin > 0 ? this->pmwAdjustmentPin - 1 : 15; + this->refreshPWMAdjustment(); + break; + case 6: + // Increment the PWM id + this->pmwAdjustmentPin = this->pmwAdjustmentPin < 15 ? this->pmwAdjustmentPin + 1 : 0; + this->refreshPWMAdjustment(); + break; + case 4: + // Toggle the PWM state + this->outputCard->setState(this->pmwAdjustmentPin, !this->outputCard->getState(this->pmwAdjustmentPin)); + this->refreshPWMAdjustmentState(); + break; + case 1: + // Set the PWM value + val = (uint16_t)this->getNumber("pwm_value.val"); + this->outputCard->setValue(this->pmwAdjustmentPin, val); + break; + default: + break; + } +} + +/** + * @brief Send data to display element on the network config page + */ +void InternalDisplay::refreshNetworkConfig() +{ + // The network config page have the following components: + // ip_set -> a text input to set the ip address + // netmask_set -> a text input to set the netmask + // gateway_set -> a text input to set the gateway + // dns_set -> a text input to set the dns + // host_set -> a text input to set the hostname + + // Refresh the ip address + this->displayAdapter->print("ip_set.txt=\""); + this->sendIpToDisplay(this->networkConfig->ip); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the netmask + this->displayAdapter->print("netmask_set.txt=\""); + this->sendIpToDisplay(this->networkConfig->subnet); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the gateway + this->displayAdapter->print("gateway_set.txt=\""); + this->sendIpToDisplay(this->networkConfig->gateway); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the dns + this->displayAdapter->print("dns_set.txt=\""); + this->sendIpToDisplay(this->networkConfig->dns1); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the hostname + this->displayAdapter->print("host_set.txt=\""); + this->displayAdapter->print(this->networkConfig->hostname); + this->displayAdapter->print("\""); + this->sendStopBytes(); +} + +/** + * @brief Send data to display element on the mqtt config page + */ +void InternalDisplay::refreshMQTTConfig() +{ + // The MQTT config page have the following components: + // mqttsv_set -> a text input to set the mqtt server + // port_set -> a text input to set the mqtt port + // use_auth -> a checkbox to enable/disable mqtt authentication + // user_set -> a text input to set the mqtt username + // password_set -> a text input to set the mqtt password + // topic_set -> a text input to set the mqtt base topic + + // Refresh the mqtt server + this->displayAdapter->print("mqttsv_set.txt=\""); + this->displayAdapter->print(this->mqttConfig->mqtt_server); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the mqtt port + this->displayAdapter->print("port_set.val="); + this->displayAdapter->print(this->mqttConfig->mqtt_port); + this->sendStopBytes(); + // Refresh the mqtt username + this->displayAdapter->print("user_set.txt=\""); + this->displayAdapter->print(this->mqttConfig->mqtt_user); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the mqtt password + this->displayAdapter->print("password_set.txt=\""); + this->displayAdapter->print(this->mqttConfig->mqtt_password); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the mqtt base topic + this->displayAdapter->print("topic_set.txt=\""); + this->displayAdapter->print(this->mqttConfig->base_topic); + this->displayAdapter->print("\""); + this->sendStopBytes(); + // Refresh the mqtt use auth + this->displayAdapter->print("use_auth.val="); + this->displayAdapter->print(this->mqttConfig->mqtt_useauth ? 1 : 0); + this->sendStopBytes(); +} + +/** + * @brief Write an ip address to the display + * + * @note This function only writes the ip address to the display, you need to send the prefix and suffix yourself + * + * @param ip The ip address to send + */ +void InternalDisplay::sendIpToDisplay(IPAddress ip) +{ + // Send the ip address + this->displayAdapter->print(ip[0]); + this->displayAdapter->print("."); + this->displayAdapter->print(ip[1]); + this->displayAdapter->print("."); + this->displayAdapter->print(ip[2]); + this->displayAdapter->print("."); + this->displayAdapter->print(ip[3]); +} + +/** + * @brief Handle the AC state change + * + * @note This function is registered as a callback to the ClimateCard + * + * @param mode The new mode + * @param fan_speed The new fan speed + * @param temperature The new temperature + */ +void InternalDisplay::handleACStateChange(uint8_t mode, uint8_t fan_speed, uint8_t temperature) +{ + // If the climate card is binded to the display and the current page is the AC page + // then update the respective AC component + if (this->climateCard == nullptr || this->currentPage != INTERNAL_DISPLAY_AC_PAGE) + return; + this->sendStopBytes(); + // Update the AC state + this->refreshAC(); +} \ No newline at end of file diff --git a/InternalDisplay.hpp b/InternalDisplay.hpp new file mode 100644 index 0000000..21d7ff3 --- /dev/null +++ b/InternalDisplay.hpp @@ -0,0 +1,135 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +// Page IDs +#define INTERNAL_DISPLAY_BOOT_PAGE 0 +#define INTERNAL_DISPLAY_DASHBOARD_PAGE 1 +#define INTERNAL_DISPLAY_INPUT_PAGE 2 +#define INTERNAL_DISPLAY_OUTPUT_PAGE 3 +#define INTERNAL_DISPLAY_AC_PAGE 4 +#define INTERNAL_DISPLAY_PWM_ADJUSTMENT_PAGE 5 +#define INTERNAL_DISPLAY_NETWORK_CONFIG_PAGE 6 +#define INTERNAL_DISPLAY_OTA_PAGE 9 +#define INTERNAL_DISPLAY_CLIMATE_NULL_PTR_PAGE 10 +#define INTERNAL_DISPLAY_MQTT_CONFIG_PAGE 11 +#define INTERNAL_DISPLAY_INPUT_NULL_PTR_PAGE 12 +#define INTERNAL_DISPLAY_OUTPUT_NULL_PTR_PAGE 13 + +// Picture IDs +#define PIC_LAN_DISCONNECTED 2 +#define PIC_LAN_CONNECTED 3 +#define PIC_MQTT_DISCONNECTED 4 +#define PIC_MQTT_CONNECTED 5 +#define PIC_PWM_BAR_ON 33 +#define PIC_PWM_BAR_OFF 48 +#define PIC_AC_MODE_OFF_ACTIVE 24 +#define PIC_AC_MODE_OFF_INACTIVE 25 +#define PIC_AC_MODE_FAN_ACTIVE 22 +#define PIC_AC_MODE_FAN_INACTIVE 23 +#define PIC_AC_MODE_COOL_ACTIVE 12 +#define PIC_AC_MODE_COOL_INACTIVE 13 +#define PIC_AC_FAN_SPEED_AUTO_ACTIVE 14 +#define PIC_AC_FAN_SPEED_AUTO_INACTIVE 15 +#define PIC_AC_FAN_SPEED_LOW_ACTIVE 18 +#define PIC_AC_FAN_SPEED_LOW_INACTIVE 19 +#define PIC_AC_FAN_SPEED_MEDIUM_ACTIVE 20 +#define PIC_AC_FAN_SPEED_MEDIUM_INACTIVE 21 +#define PIC_AC_FAN_SPEED_HIGH_ACTIVE 16 +#define PIC_AC_FAN_SPEED_HIGH_INACTIVE 17 + +// AC Fan Speeds and Mode Position Assumptions +#define AC_FAN_SPEED_AUTO 0 +#define AC_FAN_SPEED_LOW 1 +#define AC_FAN_SPEED_MEDIUM 2 +#define AC_FAN_SPEED_HIGH 3 +#define AC_MODE_OFF 0 +#define AC_MODE_FAN_ONLY 1 +#define AC_MODE_COOL 2 + +// Messages +#define MSG_MQTT_CONNECTED "BMS Managed" +#define MSG_MQTT_DISCONNECTED "Standalone" +#define MSG_PWM_ADJUSTMENT_STATE_ON "ON" +#define MSG_PWM_ADJUSTMENT_STATE_OFF "OFF" + +// Refresh Interval +#define INTERNAL_DISPLAY_CLOCK_REFRESH_INTERVAL 15000 +#define INTERNAL_DISPLAY_TOP_BAR_REFRESH_INTERVAL 5000 + +// Touch Types +#define TOUCH_TYPE_PRESS 0x01 +#define TOUCH_TYPE_RELEASE 0x0 + +/** + * @brief The internal display of the ESPMegaPRO + * + * This is the display that is installed on some ESPMegaPRO Chassis. It is a 3.5" TFT LCD with a resistive touch screen. + * + * You can use this display to monitor the status of the ESPMegaPRO and also to control the various components of the + * ESPMegaPRO. + * + * If you are using a custom display, you need to create a class that inherits from ESPMegaDisplay and implement the + * methods in that class, you may refer to this class for reference. + * + * @note This class is automatically instantiated by the ESPMegaPRO and can be accessed via the `display` variable. + */ +class InternalDisplay : public ESPMegaDisplay { + public: + InternalDisplay(HardwareSerial *displayAdapter); + void begin(ESPMegaIoT *iot, std::function getRtcTime); + void loop(); + void bindInputCard(DigitalInputCard *inputCard); + void bindOutputCard(DigitalOutputCard *outputCard); + void bindClimateCard(ClimateCard *climateCard); + void unbindInputCard(); + void unbindOutputCard(); + void unbindClimateCard(); + + private: + uint8_t bindedInputCardCallbackHandler; + uint8_t bindedOutputCardCallbackHandler; + uint8_t bindedClimateCardCallbackHandler; + uint8_t bindedClimateCardSensorCallbackHandler; + DigitalInputCard *inputCard; + DigitalOutputCard *outputCard; + ClimateCard *climateCard; + void handleInputStateChange(uint8_t pin, bool state); + void handlePwmStateChange(uint8_t pin, bool state, uint16_t value); + void handlePageChange(uint8_t page); + void setOutputBar(uint8_t pin, uint16_t value); + void setOutputStateColor(uint8_t pin, bool state); + void setInputMarker(uint8_t pin, bool state); + void handleACStateChange(uint8_t mode, uint8_t fan_speed, uint8_t temperature); + void saveNetworkConfig(); + void saveMQTTConfig(); + void updateStatusIcons(bool networkStatus, bool mqttStatus); + void updateClock(); + void refreshPage(); + void refreshPage(uint8_t page); + void refreshDashboard(); + void refreshInput(); + void refreshOutput(); + void refreshAC(); + void refreshPWMAdjustment(); + void refreshPWMAdjustmentSlider(); + void refreshPWMAdjustmentState(); + void refreshPWMAdjustmentId(); + void refreshNetworkConfig(); + void refreshMQTTConfig(); + void sendIpToDisplay(IPAddress ip); + uint8_t pmwAdjustmentPin; + // Touch handlers + void handleTouch(uint8_t page, uint8_t component, uint8_t type); + void handlePWMAdjustmentTouch(uint8_t component, uint8_t type); + void handleACTouch(uint8_t component, uint8_t type); + MqttConfig *mqttConfig; + NetworkConfig *networkConfig; + // Pointers to various data + ESPMegaIoT *iot; + std::function getRtcTime; +}; \ No newline at end of file diff --git a/IoTComponent.cpp b/IoTComponent.cpp new file mode 100644 index 0000000..186657e --- /dev/null +++ b/IoTComponent.cpp @@ -0,0 +1,25 @@ +#include + +void IoTComponent::setMqttClient(PubSubClient *mqtt) { + this->mqtt = mqtt; +} + +void IoTComponent::publishRelative(const char *topic, const char *payload) { + static char absolute_topic[100]; + sprintf(absolute_topic, "%s/%02d/%s", base_topic, card_id, topic); + ESP_LOGD("IoTComponent", "Publishing to %s : %s", absolute_topic, payload); + mqtt->publish(absolute_topic, payload); + mqtt->loop(); +} + +void IoTComponent::subscribeRelative(const char *topic) { + char absolute_topic[50]; + sprintf(absolute_topic, "%s/%02d/%s", base_topic, card_id, topic); + ESP_LOGD("IoTComponent", "Subscribing to %s", absolute_topic); + mqtt->subscribe(absolute_topic); + mqtt->loop(); +} + +void IoTComponent::loop() { + // Placeholder, Do nothing +} \ No newline at end of file diff --git a/IoTComponent.hpp b/IoTComponent.hpp new file mode 100644 index 0000000..c323103 --- /dev/null +++ b/IoTComponent.hpp @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include + +/** + * @brief The IoTComponent class is a base class that is used to interface with an expansion card through MQTT. + * + * In order to create a new IoTComponent, you should create a new class that inherits from this class. + * Your class should implement the following functions: + * - begin() : Initialize the component, record the card id, ExpansionCard object, the PubSubClient object and the base topic + * - handleMqttMessage() : Handle the MQTT messages for the component + * - publishReport() : Publish all the reports for the component + * - getType() : Get the type of the component, This should return the underlying ExpansionCard type + * - subscribe() : Subscribe to the MQTT topics used by the component + * - loop() : A function that is called in the main loop + * + * Additionally, the inherited class will have access to these helper functions: + * - publishRelative() : Publish a message to a topic relative to the base topic and the card id + * - subscribeRelative() : Subscribe to a topic relative to the base topic and the card id + * + * @warning This class is abstract and should not be instantiated directly. + */ +class IoTComponent { + public: + virtual bool begin(uint8_t card_id, ExpansionCard *card, PubSubClient *mqtt, char *base_topic); + virtual void handleMqttMessage(char *topic, char *payload); + void setMqttClient(PubSubClient *mqtt); + virtual void publishReport(); + virtual uint8_t getType(); + virtual void subscribe(); + void loop(); + protected: + char *base_topic; + void publishRelative(const char *topic, const char *payload); + void subscribeRelative(const char *topic); + PubSubClient *mqtt; + uint8_t card_id; +}; diff --git a/TimeStructure.hpp b/TimeStructure.hpp new file mode 100644 index 0000000..0df7971 --- /dev/null +++ b/TimeStructure.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +/** + * @brief The rtctime_t struct is a structure for storing the time. + * + * This structure is used by the ESPMegaPRO library to store the time. + * + * @warning This structure is not compatible with the Arduino Time library. + */ +struct rtctime_t { + uint8_t hours; + uint8_t minutes; + uint8_t seconds; + uint8_t day; + uint8_t month; + uint16_t year; +}; diff --git a/cards/Analog.cpp b/cards/Analog.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/cards/Analog.hpp b/cards/Analog.hpp deleted file mode 100644 index b2b8914..0000000 --- a/cards/Analog.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once -#include -#include -#include - -#define ANALOG_INPUT_BANK_A_ADDRESS 0x48 -#define ANALOG_INPUT_BANK_B_ADDRESS 0x49 -#define DAC0_ADDRESS 0x60 -#define DAC1_ADDRESS 0x61 -#define DAC2_ADDRESS 0x62 -#define DAC3_ADDRESS 0x63 - -class AnalogCard { - public: - AnalogCard(); - void dacWrite(uint8_t pin); - uint16_t analogRead(uint8_t pin); - void begin(); - private: - MCP4725 dac0; - MCP4725 dac1; - MCP4725 dac2; - MCP4725 dac3; - Adafruit_ADS1115 analogInputBankA; - Adafruit_ADS1115 analogInputBankB; -}; \ No newline at end of file diff --git a/cards/DigitalInput.cpp b/cards/DigitalInput.cpp deleted file mode 100644 index ab3940f..0000000 --- a/cards/DigitalInput.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#pragma once -#include - -// Instantiate the card with the specified address -DigitalInputCard::DigitalInputCard(uint8_t address_a, uint8_t address_b) { - this->address_a = address_a; - this->address_b = address_b; -} -// Instantiate the card with the specified position on the dip switch -// Bit 0,1,2 are for bank A -// Bit 3,4,5 are for bank B -DigitalInputCard::DigitalInputCard(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4, bool bit5) { - this->address_a = 0x20; - this->address_b = 0x20; - if (bit0) this->address_a += 1; - if (bit1) this->address_a += 2; - if (bit2) this->address_a += 4; - if (bit3) this->address_b += 1; - if (bit4) this->address_b += 2; - if (bit5) this->address_b += 4; - -} -// Initialize the card -void DigitalInputCard::begin() { - this->inputBankA = PCF8574(this->address_a); - this->inputBankB = PCF8574(this->address_b); - this->inputBankA.begin(); - this->inputBankB.begin(); -} -// Refresh and Read the input from the specified pin, always refresh the input buffers -uint8_t DigitalInputCard::digitalRead(uint8_t pin) { - digitalRead(pin, true); -} -// Read the input from the specified pin, also refresh the input buffers if refresh is true -uint8_t DigitalInputCard::digitalRead(uint8_t pin, bool refresh) { - // First check if the pin is in bank A or B - if (pin >= 0 && pin <= 7) { - // Refresh the input buffers if refresh is true - if (refresh) refreshInputBankA(); - // Extract the bit from the buffer - return ((inputBufferA >> (7 - pin)) & 1); - } else if (pin >= 8 && pin <= 15) { - // Refresh the input buffers if refresh is true - if (refresh) refreshInputBankB(); - // Extract the bit from the buffer - return ((inputBufferB >> (15 - pin)) & 1); - } -} -// Preform a loop to refresh the input buffers -void DigitalInputCard::loop() { - // Store the current input buffers - uint8_t inputBufferA_old = inputBufferA; - uint8_t inputBufferB_old = inputBufferB; - // Refresh the input buffers - refreshInputBankA(); - refreshInputBankB(); - // Iterate over all pins and check if they changed - for (int i = 0; i < 16; i++) { - // Check which bank the pin is in - if (i<8) { - // Check if the pin changed - if (((inputBufferA_old >> (7 - i)) & 1) != ((inputBufferA >> (7 - i)) & 1)) { - // Call the callback function if it is not null and pass the pin and the new value - if (callback != NULL) callback(i, ((inputBufferA >> (7 - i)) & 1)); - } - } else { - // Check if the pin changed - if (((inputBufferB_old >> (15 - i)) & 1) != ((inputBufferB >> (15 - i)) & 1)) { - // Call the callback function if it is not null and pass the pin and the new value - if (callback != NULL) callback(i, ((inputBufferB >> (15 - i)) & 1)); - } - } - } -} -// Get the input buffer for bank A -uint8_t DigitalInputCard::getInputBufferA() { - return inputBufferA; -} -// Get the input buffer for bank B -uint8_t DigitalInputCard::getInputBufferB() { - return inputBufferB; -} -// Register a callback function to be called when a pin changes -void DigitalInputCard::registerCallback(void (*callback)(void)) { - this->callback = callback; -} - -// Refresh the input buffer for bank A -void DigitalInputCard::refreshInputBankA() { - inputBufferA = inputBankA.read8(); -} -// Refresh the input buffer for bank B -void DigitalInputCard::refreshInputBankB() { - inputBufferB = inputBankB.read8(); -} \ No newline at end of file diff --git a/cards/DigitalInput.hpp b/cards/DigitalInput.hpp deleted file mode 100644 index 3be4ca6..0000000 --- a/cards/DigitalInput.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -#include -#include - -class DigitalInputCard { - public: - // Instantiate the card with the specified address - DigitalInputCard(uint8_t address_a, uint8_t address_b); - // Instantiate the card with the specified position on the dip switch - DigitalInputCard(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4, bool bit5); - // Initialize the card - void begin(); - // Refresh and Read the input from the specified pin, always refresh the input buffers - uint8_t digitalRead(uint8_t pin); - // Read the input from the specified pin, also refresh the input buffers if refresh is true - uint8_t digitalRead(uint8_t pin, bool refresh); - // Preform a loop to refresh the input buffers - void loop(); - // Get the input buffer for bank A - uint8_t getInputBufferA(); - // Get the input buffer for bank B - uint8_t getInputBufferB(); - // Register a callback function to be called when a pin changes - void registerCallback(void (*callback)(void)); - private: - PCF8574 inputBankA; - PCF8574 inputBankB; - uint8_t address_a; - uint8_t address_b; - uint8_t inputBufferA; - uint8_t inputBufferB; - void (*callback)(void); - void refreshInputBankA(); - void refreshInputBankB(); -} \ No newline at end of file diff --git a/cards/DigitalOutput.cpp b/cards/DigitalOutput.cpp deleted file mode 100644 index d611755..0000000 --- a/cards/DigitalOutput.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include -DigitalOutputCard::(uint8_t address) { - this->address = address; - -} -// Instantiate the card with the specified position on the dip switch -DigitalOutputCard::(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4) { - this->address = 0x20; - if (bit0) this->address += 1; - if (bit1) this->address += 2; - if (bit2) this->address += 4; - if (bit3) this->address += 8; - if (bit4) this->address += 16; - -} -// Initialize the card -DigitalOutputCard::void begin() { - this->pwm = Adafruit_PWMServoDriver(this->address); - this->pwm.begin(); -} -// Set the output to the specified state -DigitalOutputCard::void digitalWrite(uint8_t pin, bool value) { - this->pwm.setPin(pin, value ? 4095 : 0); -} -// Set the output to the specified pwm value -DigitalOutputCard::void analogWrite(uint8_t pin, uint16_t value) { - // If value is greater than 4095, set it to 4095 - if (value > 4095) value = 4095; - // Set the pwm value - this->pwm.setPWM(pin, value); -} \ No newline at end of file diff --git a/cards/DigitalOutput.hpp b/cards/DigitalOutput.hpp deleted file mode 100644 index 7f125de..0000000 --- a/cards/DigitalOutput.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include -#include -class DigitalOutputCard -{ -public: - // Instantiate the card with the specified address - DigitalOutputCard(uint8_t address); - // Instantiate the card with the specified position on the dip switch - DigitalOutputCard(bool bit0, bool bit1, bool bit2, bool bit3, bool bit4); - // Initialize the card - void begin(); - // Set the output to the specified state - void digitalWrite(uint8_t pin, uint8_t value); - // Set the output to the specified pwm value - void analogWrite(uint8_t pin, uint8_t value); -private: - Adafruit_PWMServoDriver pwm; - uint8_t address; -}; \ No newline at end of file diff --git a/examples/PinOperation/PinOperation.cpp b/examples/Functional_PinOperation/PinOperation.cpp similarity index 100% rename from examples/PinOperation/PinOperation.cpp rename to examples/Functional_PinOperation/PinOperation.cpp diff --git a/examples/OOP_Firmware/basic_firmware.cpp b/examples/OOP_Firmware/basic_firmware.cpp new file mode 100644 index 0000000..54e766b --- /dev/null +++ b/examples/OOP_Firmware/basic_firmware.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include + +// #define FRAM_DEBUG +// #define MQTT_DEBUG +// #define WRITE_DEFAULT_NETCONF +#define CLIMATE_CARD_ENABLE +#define MQTT_CARD_REGISTER +//#define DISPLAY_ENABLE +#define WEB_SERVER_ENABLE + +// Demo PLC firmware using the ESPMegaPRO OOP library + +ESPMegaPRO espmega = ESPMegaPRO(); + +#ifdef CLIMATE_CARD_ENABLE +// Climate Card +const uint16_t irCode[15][4][4][1] = {0}; +const char *mode_names[] = {"off", "fan_only", "cool"}; +const char *fan_speed_names[] = {"auto", "low", "medium", "high"}; + +size_t getInfraredCode(uint8_t mode, uint8_t fan_speed, uint8_t temperature, const uint16_t **codePtr) +{ + // Change the code pointer to point to the IR timing array + *codePtr = &(irCode[mode][fan_speed][temperature][0]); + return sizeof(irCode[mode][fan_speed][temperature]) / sizeof(uint16_t); +} + +AirConditioner ac = { + .max_temperature = 30, + .min_temperature = 16, + .modes = 4, + .mode_names = mode_names, + .fan_speeds = 4, + .fan_speed_names = fan_speed_names, + .getInfraredCode = &getInfraredCode}; + +ClimateCard climateCard = ClimateCard(14, ac); +#endif + +void input_change_callback(uint8_t pin, uint8_t value) +{ + Serial.print("Input change callback: "); + Serial.print(pin); + Serial.print(" "); + Serial.println(value); +} + +void mqtt_callback(char *topic, char* payload) +{ + Serial.print("MQTT Callback: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(payload); +} + +#ifdef WRITE_DEFAULT_NETCONF +void setNetworkConfig() +{ + NetworkConfig config = { + .ip = {192, 168, 0, 10}, + .gateway = {192, 168, 0, 1}, + .subnet = {255, 255, 255, 0}, + .dns1 = {8, 8, 8, 8}, + .dns2 = {8, 8, 4, 4}, + .useStaticIp = true, + .useWifi = false, + .wifiUseAuth = false, + }; + strcpy(config.ssid, "your_ssid"); + strcpy(config.password, "your_password"); + strcpy(config.hostname, "your_hostname"); + Serial.println("Setting network config"); + espmega.iot->setNetworkConfig(config); + espmega.iot->saveNetworkConfig(); +} + +void setMqttConfig() +{ + MqttConfig config = { + .mqtt_port = 1883, + .mqtt_useauth = false}; + strcpy(config.mqtt_server, ""); + strcpy(config.base_topic, ""); + espmega.iot->setMqttConfig(config); + espmega.iot->saveMqttConfig(); +} +#endif + +void setup() +{ + ESP_LOGI("Initializer", "Starting ESPMegaPRO OOP demo"); + espmega.begin(); + ESP_LOGI("Initializer", "Enabling IOT module"); + espmega.enableIotModule(); + ESP_LOGI("Initializer", "Enabling Ethernet"); + ETH.begin(); + ESP_LOGI("Initializer", "Binding Ethernet to IOT module"); + espmega.iot->bindEthernetInterface(Ð); + #ifdef WRITE_DEFAULT_NETCONF + setNetworkConfig(); + #else + ESP_LOGI("Initializer", "Loading network config"); + espmega.iot->loadNetworkConfig(); + #endif + ESP_LOGI("Initializer", "Connecting to network"); + espmega.iot->connectNetwork(); + #ifdef WRITE_DEFAULT_NETCONF + setMqttConfig(); + #else + ESP_LOGI("Initializer", "Loading MQTT config"); + espmega.iot->loadMqttConfig(); + #endif + ESP_LOGI("Initializer", "Connecting to MQTT"); + espmega.iot->connectToMqtt(); + espmega.iot->registerMqttCallback(mqtt_callback); + #ifdef MQTT_CARD_REGISTER + ESP_LOGI("Initializer", "Registering cards 0"); + espmega.iot->registerCard(0); + ESP_LOGI("Initializer", "Registering cards 1"); + espmega.iot->registerCard(1); + #endif + ESP_LOGI("Initializer", "Registering Input change callback"); + espmega.inputs.registerCallback(input_change_callback); + #ifdef CLIMATE_CARD_ENABLE + ESP_LOGI("Initializer", "Installing climate card"); + espmega.installCard(2, &climateCard); + ESP_LOGI("Initializer", "Binding climate card to FRAM"); + climateCard.bindFRAM(&espmega.fram, 1001); + ESP_LOGI("Initializer", "Loading climate card state from FRAM"); + climateCard.loadStateFromFRAM(); + ESP_LOGI("Initializer", "Enabling climate card FRAM autosave"); + climateCard.setFRAMAutoSave(true); + ESP_LOGI("Initializer", "Registering cards 2"); + espmega.iot->registerCard(2); + #endif + #ifdef DISPLAY_ENABLE + ESP_LOGI("Initializer", "Enabling internal display"); + espmega.enableInternalDisplay(&Serial); + ESP_LOGI("Initializer", "Binding climate card to internal display"); + espmega.display->bindClimateCard(&climateCard); + #endif + #ifdef WEB_SERVER_ENABLE + ESP_LOGI("Initializer", "Enabling web server"); + espmega.enableWebServer(80); + // This will set the web server username and password to "admin" + // If this line is not commented out, + // the web server credentials will be reset to "admin" every time the device boots + espmega.webServer->setWebUsername("admin"); + espmega.webServer->setWebPassword("admin"); + espmega.webServer->saveCredentialsToFRAM(); + #endif +} + +void loop() +{ + espmega.loop(); +#ifdef FRAM_DEBUG + // Every 20 seconds, dump FRAM 0-500 to serial + static uint32_t last_fram_dump = 0; + if (millis() - last_fram_dump >= 20000) + { + last_fram_dump = millis(); + Serial.println("Dumping FRAM"); + espmega.dumpFRAMtoSerial(0, 500); + Serial.println("Dumping FRAM ASCII"); + espmega.dumpFRAMtoSerialASCII(0, 500); + } +#endif + +// Every 5 seconds, publish "I'm alive" to MQTT +#ifdef MQTT_DEBUG + static uint32_t last_mqtt_publish = 0; + if (millis() - last_mqtt_publish >= 5000) + { + last_mqtt_publish = millis(); + espmega.iot->publish("/espmegai/alive", "true"); + } + static uint32_t last_mqtt_status = 0; + if (millis() - last_mqtt_status >= 1000) + { + last_mqtt_status = millis(); + Serial.print("MQTT Status: "); + Serial.println(espmega.iot->mqttConnected() ? "Connected" : "Disconnected"); + } +#endif +} \ No newline at end of file diff --git a/html/all.h b/html/all.h new file mode 100644 index 0000000..09bffb5 --- /dev/null +++ b/html/all.h @@ -0,0 +1,3 @@ +#pragma once +#include "config.h" +#include "ota.h" diff --git a/html/config.h b/html/config.h new file mode 100644 index 0000000..462ee74 --- /dev/null +++ b/html/config.h @@ -0,0 +1,696 @@ +const char config_html[] PROGMEM = { + 0x3c, 0x64, 0x69, 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x6c, 0x6f, 0x61, + 0x64, 0x69, 0x6e, 0x67, 0x53, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x22, + 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x73, 0x70, 0x69, 0x6e, + 0x6e, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x22, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x64, 0x69, + 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x6e, 0x6f, 0x6e, 0x65, 0x3b, + 0x22, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x64, 0x69, 0x76, 0x20, 0x63, + 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x73, 0x70, 0x69, 0x6e, 0x6e, 0x65, + 0x72, 0x22, 0x3e, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x3c, 0x70, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x6c, 0x6f, 0x61, 0x64, + 0x69, 0x6e, 0x67, 0x54, 0x65, 0x78, 0x74, 0x22, 0x3e, 0x53, 0x61, 0x76, + 0x69, 0x6e, 0x67, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x50, 0x6c, 0x65, 0x61, 0x73, 0x65, + 0x20, 0x77, 0x61, 0x69, 0x74, 0x20, 0x28, 0x3c, 0x73, 0x70, 0x61, 0x6e, + 0x20, 0x69, 0x64, 0x3d, 0x22, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x64, 0x6f, + 0x77, 0x6e, 0x22, 0x3e, 0x31, 0x35, 0x73, 0x3c, 0x2f, 0x73, 0x70, 0x61, + 0x6e, 0x3e, 0x29, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x3c, 0x2f, 0x64, + 0x69, 0x76, 0x3e, 0x0d, 0x0a, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0d, + 0x0a, 0x3c, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x65, 0x6e, 0x63, 0x74, 0x79, + 0x70, 0x65, 0x3d, 0x22, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x61, 0x72, + 0x74, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x2d, 0x64, 0x61, 0x74, 0x61, 0x22, + 0x20, 0x69, 0x64, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, + 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x20, 0x6f, 0x6e, 0x73, 0x75, 0x62, 0x6d, + 0x69, 0x74, 0x3d, 0x22, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x28, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x29, 0x22, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x68, 0x31, 0x3e, 0x45, 0x53, 0x50, 0x4d, + 0x65, 0x67, 0x61, 0x20, 0x50, 0x52, 0x4f, 0x3c, 0x2f, 0x68, 0x31, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x68, 0x33, 0x3e, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3c, 0x2f, 0x68, 0x33, 0x3e, 0x0d, 0x0a, + 0x20, 0x20, 0x3c, 0x70, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x22, 0x3e, 0x49, 0x50, 0x20, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, + 0x74, 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x69, 0x70, 0x5f, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, + 0x22, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, + 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, + 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, + 0x22, 0x25, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x25, 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x70, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x20, 0x4d, 0x61, 0x73, 0x6b, 0x3c, + 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, + 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x6e, 0x65, 0x74, 0x6d, 0x61, 0x73, + 0x6b, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x6e, 0x65, 0x74, + 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, + 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x25, 0x6e, 0x65, 0x74, 0x6d, 0x61, + 0x73, 0x6b, 0x25, 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x3c, 0x70, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, + 0x3e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x3c, 0x2f, 0x70, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, + 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x69, + 0x64, 0x3d, 0x22, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x22, 0x20, + 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, + 0x6e, 0x66, 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3d, 0x22, 0x25, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x25, + 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, + 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x44, 0x4e, + 0x53, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3c, 0x2f, 0x70, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, + 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x69, + 0x64, 0x3d, 0x22, 0x64, 0x6e, 0x73, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, + 0x3d, 0x22, 0x64, 0x6e, 0x73, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, + 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x25, 0x64, 0x6e, 0x73, 0x25, + 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, + 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x48, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, + 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, + 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x69, 0x64, 0x3d, + 0x22, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x20, 0x6e, + 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, + 0x6e, 0x66, 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3d, 0x22, 0x25, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, + 0x25, 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x70, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x42, + 0x4d, 0x53, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x2d, 0x20, + 0x49, 0x50, 0x20, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x3c, 0x2f, + 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, + 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, + 0x20, 0x69, 0x64, 0x3d, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x69, 0x70, 0x22, + 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x69, + 0x70, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, + 0x6e, 0x66, 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3d, 0x22, 0x25, 0x62, 0x6d, 0x73, 0x5f, 0x69, 0x70, 0x25, 0x22, + 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, 0x20, + 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x42, 0x4d, 0x53, + 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x2d, 0x20, 0x50, 0x6f, + 0x72, 0x74, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, + 0x65, 0x78, 0x74, 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x62, 0x6d, 0x73, + 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, + 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x20, 0x63, + 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, + 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x25, + 0x62, 0x6d, 0x73, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x25, 0x22, 0x3e, 0x3c, + 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x3e, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, + 0x79, 0x70, 0x65, 0x3d, 0x22, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x62, 0x6f, + 0x78, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x62, 0x6d, 0x73, + 0x5f, 0x75, 0x73, 0x65, 0x61, 0x75, 0x74, 0x68, 0x22, 0x20, 0x69, 0x64, + 0x3d, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x61, 0x75, 0x74, + 0x68, 0x22, 0x20, 0x25, 0x62, 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x5f, + 0x61, 0x75, 0x74, 0x68, 0x25, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, + 0x22, 0x79, 0x65, 0x73, 0x22, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x3c, 0x73, 0x70, 0x61, 0x6e, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, + 0x22, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x22, 0x3e, + 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x2f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x70, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x42, + 0x4d, 0x53, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x2d, 0x20, + 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x70, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, + 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x69, + 0x64, 0x3d, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x62, + 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, + 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, + 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, + 0x22, 0x25, 0x62, 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x25, 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x3c, 0x70, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, + 0x3e, 0x42, 0x4d, 0x53, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, + 0x2d, 0x20, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x3c, 0x2f, + 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, + 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x62, 0x6d, 0x73, + 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x20, 0x6e, + 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, + 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x25, 0x62, 0x6d, 0x73, 0x5f, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x25, 0x22, 0x3e, 0x3c, + 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, 0x20, 0x63, 0x6c, + 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x42, 0x4d, 0x53, 0x20, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x2d, 0x20, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, + 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, + 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x62, + 0x6d, 0x73, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, + 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x65, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x20, 0x63, 0x6c, 0x61, + 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, 0x78, 0x74, + 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x25, 0x62, 0x6d, + 0x73, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x25, 0x22, + 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, 0x20, + 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3e, 0x57, 0x65, 0x62, + 0x20, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x20, 0x2d, + 0x20, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x3c, 0x2f, 0x70, + 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, + 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x22, 0x20, + 0x69, 0x64, 0x3d, 0x22, 0x77, 0x65, 0x62, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, + 0x77, 0x65, 0x62, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, + 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, + 0x66, 0x5f, 0x74, 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3d, 0x22, 0x25, 0x77, 0x65, 0x62, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x25, 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, + 0x20, 0x20, 0x3c, 0x70, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x22, 0x3e, 0x57, 0x65, 0x62, 0x20, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, + 0x61, 0x63, 0x65, 0x20, 0x2d, 0x20, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x20, 0x69, 0x64, 0x3d, + 0x22, 0x77, 0x65, 0x62, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x77, 0x65, 0x62, + 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x20, 0x63, + 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x74, + 0x78, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x25, + 0x77, 0x65, 0x62, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x25, 0x22, 0x3e, 0x3c, 0x62, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, + 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, + 0x73, 0x3d, 0x22, 0x62, 0x74, 0x6e, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3d, 0x22, 0x53, 0x61, 0x76, 0x65, 0x22, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x3c, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x20, 0x74, 0x79, 0x70, + 0x65, 0x3d, 0x22, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x22, 0x20, 0x63, + 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x22, 0x20, + 0x6f, 0x6e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x3d, 0x22, 0x77, 0x69, 0x6e, + 0x64, 0x6f, 0x77, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x68, 0x72, 0x65, 0x66, 0x3d, 0x27, 0x2f, 0x27, 0x22, 0x3e, 0x42, + 0x61, 0x63, 0x6b, 0x3c, 0x2f, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x3e, + 0x3c, 0x62, 0x72, 0x20, 0x2f, 0x3e, 0x3c, 0x62, 0x72, 0x20, 0x2f, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x62, 0x3e, 0x53, 0x49, 0x57, 0x41, 0x54, + 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x20, 0x32, 0x30, 0x32, 0x33, + 0x3c, 0x2f, 0x62, 0x3e, 0x0d, 0x0a, 0x3c, 0x2f, 0x66, 0x6f, 0x72, 0x6d, + 0x3e, 0x0d, 0x0a, 0x3c, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3e, 0x0d, + 0x0a, 0x20, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, + 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x28, + 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x28, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x47, 0x65, 0x74, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x66, 0x6f, 0x72, 0x6d, + 0x44, 0x61, 0x74, 0x61, 0x20, 0x3d, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, + 0x79, 0x49, 0x64, 0x28, 0x22, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x22, 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6e, 0x65, 0x74, 0x6d, + 0x61, 0x73, 0x6b, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x42, 0x79, 0x49, 0x64, 0x28, 0x22, 0x6e, 0x65, 0x74, 0x6d, 0x61, 0x73, + 0x6b, 0x22, 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, + 0x49, 0x64, 0x28, 0x22, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x22, + 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x64, 0x6e, 0x73, 0x3a, 0x20, 0x64, 0x6f, 0x63, + 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, 0x22, 0x64, 0x6e, + 0x73, 0x22, 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, + 0x6d, 0x65, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, + 0x79, 0x49, 0x64, 0x28, 0x22, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6d, 0x73, 0x5f, 0x69, 0x70, + 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, + 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, + 0x64, 0x28, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x69, 0x70, 0x22, 0x29, 0x2e, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x62, 0x6d, 0x73, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x3a, 0x20, + 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, + 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, + 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x29, 0x2e, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x62, 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x61, 0x75, 0x74, + 0x68, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, + 0x49, 0x64, 0x28, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x61, + 0x75, 0x74, 0x68, 0x22, 0x29, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x65, + 0x64, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6d, + 0x73, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x3a, 0x20, + 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, + 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, + 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6d, 0x73, 0x5f, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, 0x22, 0x62, 0x6d, 0x73, + 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x29, 0x2e, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x62, 0x6d, 0x73, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, + 0x79, 0x49, 0x64, 0x28, 0x22, 0x62, 0x6d, 0x73, 0x5f, 0x65, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x65, + 0x62, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x3a, 0x20, + 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, + 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, + 0x22, 0x77, 0x65, 0x62, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x29, 0x2e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2c, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x65, 0x62, 0x5f, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x3a, 0x20, 0x64, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, 0x22, 0x77, 0x65, 0x62, + 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x29, 0x2e, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, + 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, + 0x53, 0x65, 0x6e, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x50, 0x4f, 0x53, + 0x54, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x66, 0x65, 0x74, 0x63, 0x68, 0x28, 0x22, 0x2f, 0x73, + 0x61, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x2c, + 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x3a, 0x20, 0x22, 0x50, 0x4f, 0x53, + 0x54, 0x22, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x3a, 0x20, 0x7b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x54, 0x79, 0x70, 0x65, + 0x22, 0x3a, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x64, 0x79, 0x3a, + 0x20, 0x4a, 0x53, 0x4f, 0x4e, 0x2e, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x69, 0x66, 0x79, 0x28, 0x66, 0x6f, 0x72, 0x6d, 0x44, 0x61, 0x74, 0x61, + 0x29, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x29, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2e, 0x74, 0x68, 0x65, 0x6e, + 0x28, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20, 0x3d, 0x3e, + 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x6a, 0x73, + 0x6f, 0x6e, 0x28, 0x29, 0x29, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x2e, 0x74, 0x68, 0x65, 0x6e, 0x28, 0x64, 0x61, 0x74, 0x61, 0x20, + 0x3d, 0x3e, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x20, 0x64, 0x61, 0x74, 0x61, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x6c, + 0x6f, 0x67, 0x28, 0x64, 0x61, 0x74, 0x61, 0x29, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x29, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x2e, 0x63, 0x61, 0x74, 0x63, 0x68, 0x28, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x20, 0x3d, 0x3e, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x20, 0x61, 0x6e, 0x79, 0x20, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x73, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x28, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x29, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x53, 0x68, 0x6f, 0x77, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x73, 0x70, + 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, 0x45, + 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, 0x27, + 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x70, 0x69, 0x6e, 0x6e, + 0x65, 0x72, 0x27, 0x29, 0x2e, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x2e, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x20, 0x3d, 0x20, 0x27, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x27, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x2f, 0x2f, 0x20, 0x53, 0x74, 0x61, 0x72, 0x74, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x20, 0x3d, 0x20, 0x64, 0x6f, 0x63, + 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, 0x45, 0x6c, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, 0x27, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x27, 0x29, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x74, 0x69, 0x6d, 0x65, + 0x4c, 0x65, 0x66, 0x74, 0x20, 0x3d, 0x20, 0x31, 0x35, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x72, 0x20, 0x3d, + 0x20, 0x73, 0x65, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, + 0x28, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x29, + 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x74, 0x69, + 0x6d, 0x65, 0x4c, 0x65, 0x66, 0x74, 0x2d, 0x2d, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x64, 0x6f, + 0x77, 0x6e, 0x2e, 0x74, 0x65, 0x78, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x20, 0x3d, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x4c, 0x65, 0x66, + 0x74, 0x2b, 0x22, 0x73, 0x22, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x74, 0x69, 0x6d, 0x65, 0x4c, 0x65, + 0x66, 0x74, 0x20, 0x3c, 0x3d, 0x20, 0x30, 0x29, 0x20, 0x63, 0x6c, 0x65, + 0x61, 0x72, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x28, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x54, 0x69, 0x6d, 0x65, + 0x72, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x20, + 0x31, 0x30, 0x30, 0x30, 0x29, 0x3b, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x57, 0x61, 0x69, 0x74, 0x20, 0x66, 0x6f, + 0x72, 0x20, 0x31, 0x35, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x54, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x28, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x20, 0x28, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x68, 0x6f, + 0x6d, 0x65, 0x70, 0x61, 0x67, 0x65, 0x20, 0x62, 0x61, 0x73, 0x65, 0x64, + 0x20, 0x6f, 0x6e, 0x20, 0x46, 0x6f, 0x72, 0x6d, 0x27, 0x73, 0x20, 0x49, + 0x50, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, 0x6e, + 0x64, 0x6f, 0x77, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x28, 0x22, 0x68, 0x74, + 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x22, 0x2b, 0x66, 0x6f, 0x72, 0x6d, 0x44, + 0x61, 0x74, 0x61, 0x2e, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, + 0x20, 0x31, 0x35, 0x30, 0x30, 0x30, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x7d, 0x0d, 0x0a, 0x3c, 0x2f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3e, + 0x0d, 0x0a, 0x3c, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, + 0x6f, 0x6e, 0x74, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x31, 0x32, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, + 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x62, 0x6f, 0x6c, 0x64, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x65, 0x78, 0x74, 0x2d, + 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x2d, + 0x73, 0x65, 0x6c, 0x66, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x2d, + 0x62, 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x2d, 0x74, + 0x6f, 0x70, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, + 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x68, 0x72, 0x20, 0x7b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, + 0x72, 0x3a, 0x20, 0x23, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x2d, + 0x74, 0x6f, 0x70, 0x3a, 0x20, 0x30, 0x2e, 0x35, 0x65, 0x6d, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x2d, + 0x62, 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3a, 0x20, 0x30, 0x2e, 0x35, 0x65, + 0x6d, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, + 0x69, 0x6e, 0x2d, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, 0x61, 0x75, 0x74, + 0x6f, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, + 0x69, 0x6e, 0x2d, 0x72, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x61, 0x75, + 0x74, 0x6f, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x2d, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3a, 0x20, 0x69, + 0x6e, 0x73, 0x65, 0x74, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, + 0x20, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, + 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x33, 0x70, 0x78, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x23, 0x66, + 0x69, 0x6c, 0x65, 0x2d, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x2c, 0x0d, 0x0a, + 0x20, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x7b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x30, + 0x30, 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x34, 0x34, 0x70, 0x78, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, + 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, 0x34, 0x70, 0x78, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, + 0x3a, 0x20, 0x31, 0x30, 0x70, 0x78, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x73, + 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x31, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, + 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x20, 0x23, 0x66, + 0x31, 0x66, 0x31, 0x66, 0x31, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, + 0x20, 0x30, 0x20, 0x31, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x62, 0x6f, 0x64, 0x79, 0x20, + 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, + 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x3a, + 0x20, 0x75, 0x72, 0x6c, 0x28, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, + 0x2f, 0x2f, 0x66, 0x73, 0x2e, 0x73, 0x69, 0x77, 0x61, 0x74, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x6f, + 0x6e, 0x61, 0x5f, 0x62, 0x67, 0x2e, 0x70, 0x6e, 0x67, 0x22, 0x29, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, + 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, + 0x6f, 0x6e, 0x74, 0x2d, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x3a, 0x20, + 0x73, 0x61, 0x6e, 0x73, 0x2d, 0x73, 0x65, 0x72, 0x69, 0x66, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x73, 0x69, + 0x7a, 0x65, 0x3a, 0x20, 0x31, 0x34, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x37, + 0x37, 0x37, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, + 0x20, 0x20, 0x23, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, + 0x72, 0x3a, 0x20, 0x23, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, + 0x23, 0x35, 0x45, 0x35, 0x45, 0x35, 0x45, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x30, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, + 0x72, 0x3a, 0x20, 0x31, 0x70, 0x78, 0x20, 0x73, 0x6f, 0x6c, 0x69, 0x64, + 0x20, 0x23, 0x64, 0x64, 0x64, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x6c, 0x69, 0x6e, 0x65, 0x2d, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, + 0x20, 0x34, 0x34, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x74, 0x65, 0x78, 0x74, 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, + 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x75, + 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, + 0x20, 0x23, 0x62, 0x61, 0x72, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x23, 0x70, + 0x72, 0x67, 0x62, 0x61, 0x72, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, + 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x44, 0x39, 0x44, 0x39, + 0x44, 0x39, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, + 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, + 0x0d, 0x0a, 0x20, 0x20, 0x23, 0x62, 0x61, 0x72, 0x20, 0x7b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x32, + 0x39, 0x43, 0x44, 0x31, 0x46, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x30, 0x25, 0x25, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, + 0x20, 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, + 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x7b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, + 0x75, 0x6e, 0x64, 0x3a, 0x20, 0x72, 0x67, 0x62, 0x61, 0x28, 0x32, 0x35, + 0x35, 0x2c, 0x20, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x32, 0x35, 0x35, 0x2c, + 0x20, 0x30, 0x2e, 0x39, 0x35, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, + 0x32, 0x35, 0x38, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x3a, 0x20, 0x37, 0x35, 0x70, 0x78, + 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x33, 0x30, 0x70, + 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, + 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, 0x31, + 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x65, + 0x78, 0x74, 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x63, 0x65, + 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, + 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x62, 0x74, 0x6e, 0x20, 0x7b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x3a, 0x20, 0x23, 0x43, 0x41, 0x33, 0x44, 0x33, 0x44, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, + 0x20, 0x23, 0x66, 0x66, 0x66, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, + 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x20, 0x7b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x3a, 0x20, 0x23, 0x34, 0x31, 0x37, 0x64, 0x66, 0x33, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, + 0x20, 0x23, 0x66, 0x66, 0x66, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, + 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x30, 0x30, 0x25, 0x25, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, + 0x20, 0x34, 0x34, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, + 0x73, 0x3a, 0x20, 0x34, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x3a, 0x20, 0x31, 0x30, 0x70, + 0x78, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, + 0x31, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x5f, 0x74, 0x78, 0x74, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, + 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x65, 0x32, 0x65, 0x32, + 0x65, 0x32, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, + 0x20, 0x20, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x62, 0x6f, 0x78, 0x20, + 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x69, 0x7a, 0x65, 0x3a, + 0x20, 0x34, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x69, 0x6e, 0x6c, 0x69, 0x6e, 0x65, + 0x2d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, + 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x72, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, + 0x64, 0x64, 0x69, 0x6e, 0x67, 0x2d, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, + 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, + 0x72, 0x67, 0x69, 0x6e, 0x2d, 0x62, 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3a, + 0x20, 0x31, 0x32, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, + 0x6e, 0x74, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x32, 0x30, 0x70, + 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2d, 0x77, 0x65, 0x62, + 0x6b, 0x69, 0x74, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x73, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x3a, 0x20, 0x6e, 0x6f, 0x6e, 0x65, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x2d, 0x6d, 0x6f, 0x7a, 0x2d, 0x75, 0x73, 0x65, + 0x72, 0x2d, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x3a, 0x20, 0x6e, 0x6f, + 0x6e, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2d, 0x6d, 0x73, + 0x2d, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x3a, 0x20, 0x6e, 0x6f, 0x6e, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x3a, 0x20, 0x6e, 0x6f, 0x6e, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, + 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x7b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x3a, 0x20, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x70, 0x61, 0x63, 0x69, + 0x74, 0x79, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x30, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x20, 0x7b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x3a, 0x20, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x6f, 0x70, 0x3a, 0x20, 0x30, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, + 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, + 0x68, 0x74, 0x3a, 0x20, 0x32, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x32, 0x35, + 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, + 0x72, 0x3a, 0x20, 0x23, 0x65, 0x65, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x3a, 0x68, 0x6f, 0x76, 0x65, 0x72, 0x20, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x7e, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x6d, 0x61, 0x72, 0x6b, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, + 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x63, 0x63, 0x63, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x3a, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x65, 0x64, 0x7e, 0x2e, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x20, 0x7b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, + 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, + 0x32, 0x31, 0x39, 0x36, 0x46, 0x33, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, + 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x6d, 0x61, 0x72, 0x6b, 0x3a, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x7b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x3a, 0x20, 0x22, 0x22, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x61, 0x62, + 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x6e, 0x6f, + 0x6e, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, + 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x3a, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x65, 0x64, 0x7e, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x6d, 0x61, 0x72, + 0x6b, 0x3a, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x7b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, + 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x20, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x6d, 0x61, + 0x72, 0x6b, 0x3a, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x7b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, 0x39, 0x70, + 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x6f, 0x70, 0x3a, + 0x20, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, + 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, + 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x73, 0x6f, 0x6c, 0x69, 0x64, + 0x20, 0x77, 0x68, 0x69, 0x74, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x77, 0x69, 0x64, 0x74, + 0x68, 0x3a, 0x20, 0x30, 0x20, 0x33, 0x70, 0x78, 0x20, 0x33, 0x70, 0x78, + 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2d, 0x77, 0x65, + 0x62, 0x6b, 0x69, 0x74, 0x2d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, + 0x72, 0x6d, 0x3a, 0x20, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x28, 0x34, + 0x35, 0x64, 0x65, 0x67, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x2d, 0x6d, 0x73, 0x2d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, + 0x6d, 0x3a, 0x20, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x28, 0x34, 0x35, + 0x64, 0x65, 0x67, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3a, 0x20, 0x72, 0x6f, + 0x74, 0x61, 0x74, 0x65, 0x28, 0x34, 0x35, 0x64, 0x65, 0x67, 0x29, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, + 0x73, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x20, 0x7b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, + 0x20, 0x66, 0x69, 0x78, 0x65, 0x64, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x74, 0x6f, 0x70, 0x3a, 0x20, 0x34, 0x30, 0x25, 0x25, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x3a, 0x20, 0x34, + 0x34, 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3a, 0x20, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x28, 0x2d, 0x35, 0x30, 0x25, 0x25, + 0x2c, 0x20, 0x2d, 0x35, 0x30, 0x25, 0x25, 0x29, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x30, + 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x31, 0x30, 0x30, 0x70, 0x78, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, + 0x3a, 0x20, 0x31, 0x36, 0x70, 0x78, 0x20, 0x73, 0x6f, 0x6c, 0x69, 0x64, + 0x20, 0x23, 0x66, 0x33, 0x66, 0x33, 0x66, 0x33, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x74, 0x6f, + 0x70, 0x3a, 0x20, 0x31, 0x36, 0x70, 0x78, 0x20, 0x73, 0x6f, 0x6c, 0x69, + 0x64, 0x20, 0x23, 0x33, 0x34, 0x39, 0x38, 0x64, 0x62, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, + 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, 0x35, 0x30, 0x25, 0x25, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x73, 0x70, 0x69, 0x6e, 0x20, 0x32, 0x73, + 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x20, 0x69, 0x6e, 0x66, 0x69, + 0x6e, 0x69, 0x74, 0x65, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, + 0x0d, 0x0a, 0x20, 0x20, 0x40, 0x6b, 0x65, 0x79, 0x66, 0x72, 0x61, 0x6d, + 0x65, 0x73, 0x20, 0x73, 0x70, 0x69, 0x6e, 0x20, 0x7b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x30, 0x25, 0x25, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, + 0x6d, 0x3a, 0x20, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x28, 0x30, 0x64, + 0x65, 0x67, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0d, + 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x31, 0x30, 0x30, 0x25, 0x25, + 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3a, 0x20, 0x72, 0x6f, 0x74, + 0x61, 0x74, 0x65, 0x28, 0x33, 0x36, 0x30, 0x64, 0x65, 0x67, 0x29, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x7d, + 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x23, 0x6c, 0x6f, 0x61, 0x64, 0x69, + 0x6e, 0x67, 0x54, 0x65, 0x78, 0x74, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x66, 0x66, + 0x66, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, + 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x34, 0x30, 0x70, 0x78, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x65, 0x78, 0x74, 0x2d, 0x61, + 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x66, 0x69, 0x78, 0x65, 0x64, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74, 0x6f, 0x70, 0x3a, 0x20, 0x36, 0x35, + 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x65, 0x66, + 0x74, 0x3a, 0x20, 0x35, 0x30, 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3a, + 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x28, 0x2d, + 0x35, 0x30, 0x25, 0x25, 0x2c, 0x20, 0x2d, 0x35, 0x30, 0x25, 0x25, 0x29, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x73, + 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x64, 0x69, + 0x73, 0x70, 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x66, 0x6c, 0x65, 0x78, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x69, 0x66, 0x79, 0x2d, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x63, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x61, 0x6c, 0x69, 0x67, + 0x6e, 0x2d, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x3a, 0x20, 0x63, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x66, 0x69, 0x78, 0x65, 0x64, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x7a, 0x2d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x3a, + 0x20, 0x39, 0x39, 0x39, 0x39, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x6c, 0x65, + 0x66, 0x74, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x74, 0x6f, + 0x70, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x77, 0x69, 0x64, + 0x74, 0x68, 0x3a, 0x20, 0x31, 0x30, 0x30, 0x25, 0x25, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x31, 0x30, + 0x30, 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, + 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x20, 0x72, 0x67, 0x62, 0x61, + 0x28, 0x30, 0x2c, 0x20, 0x30, 0x2c, 0x20, 0x30, 0x2c, 0x20, 0x30, 0x2e, + 0x39, 0x29, 0x3b, 0x20, 0x2f, 0x2a, 0x20, 0x73, 0x65, 0x6d, 0x69, 0x2d, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x20, + 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, + 0x6f, 0x75, 0x6e, 0x64, 0x20, 0x2a, 0x2f, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, + 0x3c, 0x2f, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3e +, 0x00}; + diff --git a/html/config.html b/html/config.html new file mode 100644 index 0000000..5c3d80b --- /dev/null +++ b/html/config.html @@ -0,0 +1,306 @@ + + +
+

ESPMega PRO

+

Device Configurations

+

IP Address

+
+

Network Mask

+
+

Gateway

+
+

DNS Server

+
+

Hostname

+
+

BMS Server - IP Address

+
+

BMS Server - Port

+
+ +

BMS Server - Username

+
+

BMS Server - Password

+
+

BMS Server - Endpoint

+
+

Web Interface - Username

+
+

Web Interface - Password

+
+ +

+ SIWAT SYSTEM 2023 +
+ + \ No newline at end of file diff --git a/html/ota.h b/html/ota.h new file mode 100644 index 0000000..3fb3288 --- /dev/null +++ b/html/ota.h @@ -0,0 +1,363 @@ +const char ota_html[] PROGMEM = { + 0x3c, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x20, 0x73, 0x72, 0x63, 0x3d, + 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x61, 0x6a, 0x61, + 0x78, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x61, 0x70, 0x69, 0x73, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6a, 0x61, 0x78, 0x2f, 0x6c, 0x69, + 0x62, 0x73, 0x2f, 0x6a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2f, 0x33, 0x2e, + 0x32, 0x2e, 0x31, 0x2f, 0x6a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x6d, + 0x69, 0x6e, 0x2e, 0x6a, 0x73, 0x22, 0x3e, 0x3c, 0x2f, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x3e, 0x0d, 0x0a, 0x3c, 0x66, 0x6f, 0x72, 0x6d, 0x20, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x3d, 0x22, 0x50, 0x4f, 0x53, 0x54, + 0x22, 0x20, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x23, 0x22, + 0x20, 0x65, 0x6e, 0x63, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x6d, 0x75, + 0x6c, 0x74, 0x69, 0x70, 0x61, 0x72, 0x74, 0x2f, 0x66, 0x6f, 0x72, 0x6d, + 0x2d, 0x64, 0x61, 0x74, 0x61, 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x75, + 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x68, 0x31, 0x3e, 0x45, 0x53, 0x50, 0x4d, + 0x65, 0x67, 0x61, 0x20, 0x50, 0x52, 0x4f, 0x3c, 0x2f, 0x68, 0x31, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x68, 0x33, 0x3e, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x3c, 0x2f, 0x68, 0x33, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x70, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, + 0x74, 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x6c, 0x65, 0x66, + 0x74, 0x22, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x48, 0x6f, 0x73, + 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, + 0x73, 0x70, 0x61, 0x6e, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, + 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, + 0x22, 0x3e, 0x25, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x25, + 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, 0x20, 0x73, 0x74, + 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2d, 0x61, 0x6c, + 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x22, 0x3e, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x49, 0x50, 0x20, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x70, + 0x61, 0x6e, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x66, 0x6c, + 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x22, 0x3e, + 0x25, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x25, + 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, + 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, 0x20, 0x73, 0x74, + 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2d, 0x61, 0x6c, + 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x22, 0x3e, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x4d, 0x41, 0x43, 0x20, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, + 0x70, 0x61, 0x6e, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x66, + 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x22, + 0x3e, 0x25, 0x6d, 0x61, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x25, 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x70, 0x20, + 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2d, + 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x22, + 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x70, 0x61, 0x6e, + 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x66, 0x6c, 0x6f, 0x61, + 0x74, 0x3a, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x22, 0x3e, 0x25, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x25, 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, + 0x3c, 0x70, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, + 0x78, 0x74, 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x6c, 0x65, + 0x66, 0x74, 0x22, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x41, 0x50, + 0x49, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x3c, 0x73, 0x70, 0x61, 0x6e, 0x20, 0x73, 0x74, 0x79, 0x6c, + 0x65, 0x3d, 0x22, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x72, 0x69, + 0x67, 0x68, 0x74, 0x22, 0x3e, 0x25, 0x6d, 0x71, 0x74, 0x74, 0x5f, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x25, 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, 0x20, + 0x3c, 0x70, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, + 0x78, 0x74, 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, 0x6c, 0x65, + 0x66, 0x74, 0x22, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x41, 0x50, + 0x49, 0x20, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x70, 0x61, 0x6e, 0x20, 0x73, 0x74, + 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, + 0x72, 0x69, 0x67, 0x68, 0x74, 0x22, 0x3e, 0x25, 0x62, 0x61, 0x73, 0x65, + 0x5f, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x25, 0x3c, 0x2f, 0x73, 0x70, 0x61, + 0x6e, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, + 0x20, 0x20, 0x3c, 0x70, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, + 0x74, 0x65, 0x78, 0x74, 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, 0x3a, 0x20, + 0x6c, 0x65, 0x66, 0x74, 0x22, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x43, 0x65, 0x6e, 0x74, 0x72, 0x61, 0x6c, 0x6c, 0x79, 0x20, 0x4d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x64, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, + 0x73, 0x70, 0x61, 0x6e, 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, + 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3a, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, + 0x22, 0x3e, 0x25, 0x6d, 0x71, 0x74, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x25, 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, + 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x70, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x3c, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x20, 0x74, 0x79, 0x70, + 0x65, 0x3d, 0x22, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x22, 0x20, 0x63, + 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x22, 0x20, + 0x6f, 0x6e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x3d, 0x22, 0x77, 0x69, 0x6e, + 0x64, 0x6f, 0x77, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x68, 0x72, 0x65, 0x66, 0x3d, 0x27, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x27, 0x22, 0x3e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x3c, 0x2f, 0x62, 0x75, 0x74, 0x74, 0x6f, 0x6e, 0x3e, 0x3c, 0x62, 0x72, + 0x20, 0x2f, 0x3e, 0x3c, 0x62, 0x72, 0x20, 0x2f, 0x3e, 0x0d, 0x0a, 0x20, + 0x20, 0x3c, 0x68, 0x72, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x68, 0x33, + 0x3e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x20, 0x53, 0x6f, 0x66, 0x74, + 0x77, 0x61, 0x72, 0x65, 0x20, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, + 0x3c, 0x2f, 0x68, 0x33, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x66, 0x69, + 0x6c, 0x65, 0x22, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x22, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x66, 0x69, + 0x6c, 0x65, 0x22, 0x20, 0x6f, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x3d, 0x22, 0x73, 0x75, 0x62, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x22, + 0x20, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x3a, 0x20, 0x6e, 0x6f, 0x6e, 0x65, 0x22, 0x20, 0x2f, + 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x20, + 0x69, 0x64, 0x3d, 0x22, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x22, 0x20, 0x66, 0x6f, 0x72, 0x3d, 0x22, 0x66, 0x69, 0x6c, + 0x65, 0x22, 0x3e, 0x43, 0x68, 0x6f, 0x6f, 0x73, 0x65, 0x20, 0x66, 0x69, + 0x6c, 0x65, 0x2e, 0x2e, 0x2e, 0x3c, 0x2f, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, + 0x74, 0x79, 0x70, 0x65, 0x3d, 0x22, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, + 0x22, 0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x62, 0x74, 0x6e, + 0x22, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3d, 0x22, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x61, 0x6d, 0x22, 0x20, 0x2f, 0x3e, 0x3c, 0x62, 0x72, 0x20, + 0x2f, 0x3e, 0x3c, 0x62, 0x72, 0x20, 0x2f, 0x3e, 0x0d, 0x0a, 0x20, 0x20, + 0x3c, 0x64, 0x69, 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x70, 0x72, 0x67, + 0x22, 0x3e, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0d, 0x0a, 0x20, 0x20, + 0x3c, 0x62, 0x72, 0x20, 0x2f, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x64, + 0x69, 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x70, 0x72, 0x67, 0x62, 0x61, + 0x72, 0x22, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x64, 0x69, + 0x76, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x62, 0x61, 0x72, 0x22, 0x3e, 0x3c, + 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x2f, 0x64, + 0x69, 0x76, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x62, 0x72, 0x20, 0x2f, + 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x3c, 0x62, 0x3e, 0x53, 0x49, 0x57, 0x41, + 0x54, 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x20, 0x32, 0x30, 0x32, + 0x33, 0x3c, 0x2f, 0x62, 0x3e, 0x0d, 0x0a, 0x3c, 0x2f, 0x66, 0x6f, 0x72, + 0x6d, 0x3e, 0x0d, 0x0a, 0x3c, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3e, + 0x0d, 0x0a, 0x20, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x20, 0x73, 0x75, 0x62, 0x28, 0x6f, 0x62, 0x6a, 0x29, 0x20, 0x7b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, 0x20, 0x66, 0x69, 0x6c, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x20, 0x3d, 0x20, 0x6f, 0x62, 0x6a, 0x2e, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x28, + 0x22, 0x5c, 0x5c, 0x22, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x65, 0x74, + 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x64, 0x28, + 0x22, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x22, + 0x29, 0x2e, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x48, 0x54, 0x4d, 0x4c, 0x20, + 0x3d, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x20, 0x20, + 0x20, 0x22, 0x20, 0x2b, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x5b, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x2e, 0x6c, + 0x65, 0x6e, 0x67, 0x74, 0x68, 0x20, 0x2d, 0x20, 0x31, 0x5d, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x24, 0x28, 0x22, 0x66, + 0x6f, 0x72, 0x6d, 0x22, 0x29, 0x2e, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, + 0x28, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, + 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x2e, 0x70, + 0x72, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x28, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, + 0x72, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x3d, 0x20, 0x24, 0x28, 0x22, + 0x23, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x6d, + 0x22, 0x29, 0x5b, 0x30, 0x5d, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x76, 0x61, 0x72, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x3d, 0x20, 0x6e, + 0x65, 0x77, 0x20, 0x46, 0x6f, 0x72, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x28, + 0x66, 0x6f, 0x72, 0x6d, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x24, 0x2e, 0x61, 0x6a, 0x61, 0x78, 0x28, 0x7b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x75, 0x72, 0x6c, 0x3a, 0x20, 0x22, 0x2f, 0x6f, + 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x2c, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x74, 0x79, 0x70, 0x65, 0x3a, + 0x20, 0x22, 0x50, 0x4f, 0x53, 0x54, 0x22, 0x2c, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x64, 0x61, 0x74, 0x61, 0x3a, 0x20, 0x64, 0x61, + 0x74, 0x61, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x3a, 0x20, + 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x44, 0x61, 0x74, + 0x61, 0x3a, 0x20, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x78, 0x68, 0x72, 0x3a, 0x20, 0x66, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x29, 0x20, 0x7b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x76, 0x61, 0x72, + 0x20, 0x78, 0x68, 0x72, 0x20, 0x3d, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x77, + 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x58, 0x4d, 0x4c, 0x48, 0x74, 0x74, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x29, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x78, 0x68, 0x72, + 0x2e, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x61, 0x64, 0x64, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x28, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x22, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x22, 0x2c, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x76, + 0x74, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x65, 0x76, + 0x74, 0x2e, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x43, 0x6f, 0x6d, 0x70, + 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x76, 0x61, 0x72, 0x20, 0x70, 0x65, 0x72, 0x20, 0x3d, 0x20, 0x65, + 0x76, 0x74, 0x2e, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x20, 0x2f, 0x20, + 0x65, 0x76, 0x74, 0x2e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x72, + 0x6f, 0x75, 0x6e, 0x64, 0x28, 0x70, 0x65, 0x72, 0x20, 0x2a, 0x20, 0x31, + 0x30, 0x30, 0x29, 0x20, 0x3c, 0x20, 0x31, 0x30, 0x30, 0x29, 0x20, 0x7b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x28, 0x22, 0x23, 0x70, 0x72, + 0x67, 0x22, 0x29, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x28, 0x22, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x2e, 0x20, 0x2e, 0x20, 0x2e, + 0x20, 0x28, 0x22, 0x20, 0x2b, 0x20, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x72, + 0x6f, 0x75, 0x6e, 0x64, 0x28, 0x70, 0x65, 0x72, 0x20, 0x2a, 0x20, 0x31, + 0x30, 0x30, 0x29, 0x20, 0x2b, 0x20, 0x22, 0x25, 0x25, 0x29, 0x22, 0x29, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x65, 0x6c, + 0x73, 0x65, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, 0x28, + 0x22, 0x23, 0x70, 0x72, 0x67, 0x22, 0x29, 0x2e, 0x68, 0x74, 0x6d, 0x6c, + 0x28, 0x22, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x20, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x2c, 0x20, 0x52, 0x65, 0x62, 0x6f, + 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x2e, 0x20, 0x2e, 0x20, 0x2e, 0x22, + 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x24, + 0x28, 0x22, 0x23, 0x62, 0x61, 0x72, 0x22, 0x29, 0x2e, 0x63, 0x73, 0x73, + 0x28, 0x22, 0x77, 0x69, 0x64, 0x74, 0x68, 0x22, 0x2c, 0x20, 0x4d, 0x61, + 0x74, 0x68, 0x2e, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x28, 0x70, 0x65, 0x72, + 0x20, 0x2a, 0x20, 0x31, 0x30, 0x30, 0x29, 0x20, 0x2b, 0x20, 0x22, 0x25, + 0x25, 0x22, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x61, 0x6c, + 0x73, 0x65, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x78, 0x68, 0x72, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x3a, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, + 0x64, 0x2c, 0x20, 0x73, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, + 0x2e, 0x6c, 0x6f, 0x67, 0x28, 0x22, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x21, 0x22, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x7d, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x3a, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x28, 0x61, 0x2c, 0x20, 0x62, 0x2c, 0x20, 0x63, 0x29, + 0x20, 0x7b, 0x20, 0x7d, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, + 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x29, 0x3b, 0x0d, 0x0a, 0x3c, + 0x2f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x3e, 0x0d, 0x0a, 0x3c, 0x73, + 0x74, 0x79, 0x6c, 0x65, 0x3e, 0x0d, 0x0a, 0x20, 0x20, 0x68, 0x72, 0x20, + 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x3a, 0x20, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, + 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, + 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x61, 0x61, 0x61, 0x61, + 0x61, 0x61, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, + 0x67, 0x69, 0x6e, 0x2d, 0x74, 0x6f, 0x70, 0x3a, 0x20, 0x30, 0x2e, 0x35, + 0x65, 0x6d, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, + 0x67, 0x69, 0x6e, 0x2d, 0x62, 0x6f, 0x74, 0x74, 0x6f, 0x6d, 0x3a, 0x20, + 0x30, 0x2e, 0x35, 0x65, 0x6d, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x2d, 0x6c, 0x65, 0x66, 0x74, 0x3a, + 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x2d, 0x72, 0x69, 0x67, 0x68, 0x74, + 0x3a, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x73, 0x74, 0x79, 0x6c, + 0x65, 0x3a, 0x20, 0x69, 0x6e, 0x73, 0x65, 0x74, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x77, 0x69, + 0x64, 0x74, 0x68, 0x3a, 0x20, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x33, + 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, + 0x20, 0x20, 0x23, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x2c, 0x0d, 0x0a, 0x20, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, + 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, + 0x3a, 0x20, 0x31, 0x30, 0x30, 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, 0x20, 0x34, 0x34, + 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, + 0x34, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, + 0x72, 0x67, 0x69, 0x6e, 0x3a, 0x20, 0x31, 0x30, 0x70, 0x78, 0x20, 0x61, + 0x75, 0x74, 0x6f, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, + 0x6e, 0x74, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x31, 0x35, 0x70, + 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, + 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, + 0x3a, 0x20, 0x23, 0x66, 0x31, 0x66, 0x31, 0x66, 0x31, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, + 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, + 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x30, 0x20, 0x31, 0x35, 0x70, 0x78, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x62, + 0x6f, 0x64, 0x79, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, + 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x3a, 0x20, 0x75, 0x72, 0x6c, 0x28, 0x22, 0x68, 0x74, + 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x66, 0x73, 0x2e, 0x73, 0x69, 0x77, + 0x61, 0x74, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x61, 0x72, 0x6f, 0x6e, 0x61, 0x5f, 0x62, 0x67, 0x2e, 0x70, 0x6e, + 0x67, 0x22, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, + 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x73, 0x69, 0x7a, + 0x65, 0x3a, 0x20, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x66, 0x61, 0x6d, 0x69, + 0x6c, 0x79, 0x3a, 0x20, 0x73, 0x61, 0x6e, 0x73, 0x2d, 0x73, 0x65, 0x72, + 0x69, 0x66, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, + 0x74, 0x2d, 0x73, 0x69, 0x7a, 0x65, 0x3a, 0x20, 0x31, 0x34, 0x70, 0x78, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, + 0x3a, 0x20, 0x23, 0x37, 0x37, 0x37, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, + 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x23, 0x66, 0x69, 0x6c, 0x65, 0x2d, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, + 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x43, 0x43, 0x43, 0x43, + 0x43, 0x43, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6c, + 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x35, 0x45, 0x35, 0x45, 0x35, 0x45, 0x3b, + 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, + 0x67, 0x3a, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x31, 0x70, 0x78, 0x20, 0x73, + 0x6f, 0x6c, 0x69, 0x64, 0x20, 0x23, 0x64, 0x64, 0x64, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x2d, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x3a, 0x20, 0x34, 0x34, 0x70, 0x78, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x74, 0x65, 0x78, 0x74, 0x2d, 0x61, 0x6c, 0x69, + 0x67, 0x6e, 0x3a, 0x20, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x3a, 0x20, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, + 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x23, 0x62, 0x61, 0x72, 0x2c, 0x0d, 0x0a, + 0x20, 0x20, 0x23, 0x70, 0x72, 0x67, 0x62, 0x61, 0x72, 0x20, 0x7b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, + 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, + 0x44, 0x39, 0x44, 0x39, 0x44, 0x39, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, + 0x75, 0x73, 0x3a, 0x20, 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x23, 0x62, 0x61, 0x72, + 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, + 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, + 0x3a, 0x20, 0x23, 0x32, 0x39, 0x43, 0x44, 0x31, 0x46, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x30, + 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x3a, 0x20, 0x31, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x66, 0x6f, 0x72, + 0x6d, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, + 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x20, 0x72, 0x67, 0x62, + 0x61, 0x28, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x32, 0x35, 0x35, 0x2c, 0x20, + 0x32, 0x35, 0x35, 0x2c, 0x20, 0x30, 0x2e, 0x39, 0x35, 0x29, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x77, 0x69, 0x64, + 0x74, 0x68, 0x3a, 0x20, 0x32, 0x35, 0x38, 0x70, 0x78, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x3a, 0x20, + 0x37, 0x35, 0x70, 0x78, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x3a, + 0x20, 0x33, 0x30, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, 0x61, 0x64, 0x69, 0x75, + 0x73, 0x3a, 0x20, 0x31, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x74, 0x65, 0x78, 0x74, 0x2d, 0x61, 0x6c, 0x69, 0x67, 0x6e, + 0x3a, 0x20, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x62, 0x74, 0x6e, + 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, + 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x20, 0x23, 0x43, 0x41, 0x33, + 0x44, 0x33, 0x44, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, + 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x66, 0x66, 0x66, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x7d, 0x0d, 0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x62, 0x61, 0x63, 0x6b, + 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x20, 0x23, 0x34, 0x31, 0x37, + 0x64, 0x66, 0x33, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, + 0x6c, 0x6f, 0x72, 0x3a, 0x20, 0x23, 0x66, 0x66, 0x66, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3b, 0x0d, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3a, 0x20, 0x31, 0x30, 0x30, + 0x25, 0x25, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x3a, 0x20, 0x34, 0x34, 0x70, 0x78, 0x3b, 0x0d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x2d, 0x72, + 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x20, 0x34, 0x70, 0x78, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x3a, + 0x20, 0x31, 0x30, 0x70, 0x78, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x3b, 0x0d, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x2d, 0x73, 0x69, + 0x7a, 0x65, 0x3a, 0x20, 0x31, 0x35, 0x70, 0x78, 0x3b, 0x0d, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x62, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x3a, 0x20, 0x30, + 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x3c, 0x2f, 0x73, 0x74, + 0x79, 0x6c, 0x65, 0x3e +, 0x00}; + diff --git a/html/ota.html b/html/ota.html new file mode 100644 index 0000000..8374677 --- /dev/null +++ b/html/ota.html @@ -0,0 +1,176 @@ + +
+

ESPMega PRO

+

Device Information

+

+ Hostname + $(hostname)$ +

+

+ IP Address + $(ip_address)$ +

+

+ MAC Address + $(mac_address)$ +

+

+ Device + $(model)$ +

+

+ API Server + $(mqtt_connection_string)$ +

+

+ API Endpoint + $(base_topic)$ +

+

+ Centrally Managed + $(mqtt_connected)$ +

+

+
+

Upload Software Package

+ + +

+
+
+
+
+
+
+ SIWAT SYSTEM 2023 +
+ + \ No newline at end of file diff --git a/library.json b/library.json index f16ecd1..a44f11a 100644 --- a/library.json +++ b/library.json @@ -1,36 +1,39 @@ { - "name": "ESPMegaPROR3", - "version": "1.3.2", - "description": "Hardware Library for Siwat INC ESPMegaPRO R3", - "keywords": "espmega,espmegapro", - "repository": + "name": "ESPMegaPROR3", + "version": "2.0.0", + "description": "Hardware Library for Siwat INC ESPMegaPRO R3", + "keywords": "espmega,espmegapro", + "repository": { + "type": "git", + "url": "https://github.com/SiwatINC/ESPMegaPRO3-library" + }, + "authors": [ { - "type": "git", - "url": "https://github.com/SiwatINC/ESPMegaPRO3-library" - }, - "authors": - [ - { - "name": "Siwat Sirichai", - "email": "siwat@siwatinc.com", - "url": "https://siwatinc.com", - "maintainer": true - } - ], - "license": "MIT", - "homepage": "https://siwatinc.com/", - "dependencies": { - "adafruit/Adafruit PWM Servo Driver Library": "^2.4.1", - "robtillaart/PCF8574": "^0.3.7", - "adafruit/Adafruit ADS1X15": "^2.4.0", - "adafruit/Adafruit BusIO": "^1.14.3", - "robtillaart/MCP4725": "^0.3.7", - "arduino-libraries/Arduino_BuiltIn": "^1.0.0", - "SPI": "^2.0.0", - "robtillaart/FRAM_I2C": "^0.6.1", - "paulstoffregen/Time": "^1.6.1", - "paulstoffregen/DS1307RTC": "0.0.0-alpha+sha.c2590c0033" - }, - "frameworks": "arduino", - "platforms": "espressif32" - } \ No newline at end of file + "name": "Siwat Sirichai", + "email": "siwat@siwatinc.com", + "url": "https://siwatinc.com", + "maintainer": true + } + ], + "license": "MIT", + "homepage": "https://siwatinc.com/", + "dependencies": { + "adafruit/Adafruit PWM Servo Driver Library": "^2.4.1", + "robtillaart/PCF8574": "^0.3.7", + "adafruit/Adafruit ADS1X15": "^2.4.0", + "adafruit/Adafruit BusIO": "^1.14.3", + "robtillaart/MCP4725": "^0.3.7", + "arduino-libraries/Arduino_BuiltIn": "^1.0.0", + "SPI": "^2.0.0", + "robtillaart/FRAM_I2C": "^0.6.1", + "paulstoffregen/Time": "^1.6.1", + "paulstoffregen/DS1307RTC": "0.0.0-alpha+sha.c2590c0033", + "knolleary/pubsubclient": "^2.8.0", + "bblanchon/ArduinoJson": "^6.21.4", + "robtillaart/DS18B20": "^0.2.1", + "robtillaart/DHTNEW": "^0.4.18", + "external-repo": "https://github.com/me-no-dev/ESPAsyncWebServer.git" + }, + "frameworks": "arduino", + "platforms": "espressif32" +} \ No newline at end of file diff --git a/library.properties b/library.properties index 3acf141..b45424c 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ESPMega PRO R3 SDK -version=1.3.2 +version=2.0.0 author=Siwat Sirichai maintainer=Siwat Sirichai sentence=Hardware Library for Siwat INC ESPMegaPRO R3