diff --git a/src/app.cpp b/src/app.cpp index d83e169b..e6b577bc 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4,13 +4,14 @@ //----------------------------------------------------------------------------- #include "app.h" + #include + #include "utils/sun.h" //----------------------------------------------------------------------------- app::app() : ah::Scheduler() {} - //----------------------------------------------------------------------------- void app::setup() { Serial.begin(115200); @@ -24,7 +25,7 @@ void app::setup() { mSettings.setup(); mSettings.getPtr(mConfig); DPRINT(DBG_INFO, F("Settings valid: ")); - if(mSettings.getValid()) + if (mSettings.getValid()) DBGPRINTLN(F("true")); else DBGPRINTLN(F("false")); @@ -32,16 +33,16 @@ void app::setup() { mSys.enableDebug(); mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs); - #if defined(AP_ONLY) +#if defined(AP_ONLY) mInnerLoopCb = std::bind(&app::loopStandard, this); - #else +#else mInnerLoopCb = std::bind(&app::loopWifi, this); - #endif +#endif mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onWifi, this, std::placeholders::_1)); - #if !defined(AP_ONLY) +#if !defined(AP_ONLY) everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); - #endif +#endif mSys.addInverters(&mConfig->inst); @@ -52,23 +53,23 @@ void app::setup() { mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); mMiPayload.enableSerialDebug(mConfig->serial.debug); - //DBGPRINTLN("--- after payload"); - //DBGPRINTLN(String(ESP.getFreeHeap())); - //DBGPRINTLN(String(ESP.getHeapFragmentation())); - //DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); + // DBGPRINTLN("--- after payload"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); - if(!mSys.Radio.isChipConnected()) + if (!mSys.Radio.isChipConnected()) DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); - // when WiFi is in client mode, then enable mqtt broker - #if !defined(AP_ONLY) +// when WiFi is in client mode, then enable mqtt broker +#if !defined(AP_ONLY) mMqttEnabled = (mConfig->mqtt.broker[0] > 0); if (mMqttEnabled) { mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } - #endif +#endif setupLed(); mWeb.setup(this, &mSys, mConfig); @@ -77,18 +78,17 @@ void app::setup() { mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); // Plugins - if(mConfig->plugin.display.type != 0) - mMonoDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion); + if (mConfig->plugin.display.type != 0) + mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion); mPubSerial.setup(mConfig, &mSys, &mTimestamp); regularTickers(); - - //DBGPRINTLN("--- end setup"); - //DBGPRINTLN(String(ESP.getFreeHeap())); - //DBGPRINTLN(String(ESP.getHeapFragmentation())); - //DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); + // DBGPRINTLN("--- end setup"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); } //----------------------------------------------------------------------------- @@ -115,8 +115,8 @@ void app::loopStandard(void) { mStat.frmCnt++; Inverter<> *iv = mSys.findInverter(&p->packet[1]); - if(NULL != iv) { - if(IV_HM == iv->ivGen) + if (NULL != iv) { + if (IV_HM == iv->ivGen) mPayload.add(iv, p); else mMiPayload.add(iv, p); @@ -130,7 +130,7 @@ void app::loopStandard(void) { mPayload.loop(); mMiPayload.loop(); - if(mMqttEnabled) + if (mMqttEnabled) mMqtt.loop(); } @@ -144,19 +144,18 @@ void app::loopWifi(void) { void app::onWifi(bool gotIp) { DPRINTLN(DBG_DEBUG, F("onWifi")); ah::Scheduler::resetTicker(); - regularTickers(); // reinstall regular tickers + regularTickers(); // reinstall regular tickers if (gotIp) { mInnerLoopCb = std::bind(&app::loopStandard, this); every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); mMqttReconnect = true; - mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! + mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); - if(WIFI_AP == WiFi.getMode()) { + if (WIFI_AP == WiFi.getMode()) { mMqttEnabled = false; everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); } - } - else { + } else { mInnerLoopCb = std::bind(&app::loopWifi, this); everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); } @@ -167,8 +166,8 @@ void app::regularTickers(void) { DPRINTLN(DBG_DEBUG, F("regularTickers")); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); // Plugins - if(mConfig->plugin.display.type != 0) - everySec(std::bind(&MonoDisplayType::tickerSecond, &mMonoDisplay), "disp"); + if (mConfig->plugin.display.type != 0) + everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); } @@ -184,26 +183,26 @@ void app::tickNtpUpdate(void) { } // only install schedulers once even if NTP wasn't successful in first loop - if(mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed - if(mConfig->inst.rstValsNotAvail) + if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed + if (mConfig->inst.rstValsNotAvail) everyMin(std::bind(&app::tickMinute, this), "tMin"); - if(mConfig->inst.rstYieldMidNight) { + if (mConfig->inst.rstYieldMidNight) { uint32_t localTime = gTimezone.toLocal(mTimestamp); - uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); } } - nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min + nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min - if((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { + if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; tickCalcSunrise(); } // immediately start communicating // @TODO: leads to reboot loops? not sure #674 - if(isOK && mSendFirst) { + if (isOK && mSendFirst) { mSendFirst = false; once(std::bind(&app::tickSend, this), 2, "senOn"); } @@ -215,15 +214,15 @@ void app::tickNtpUpdate(void) { //----------------------------------------------------------------------------- void app::tickCalcSunrise(void) { - if (mSunrise == 0) // on boot/reboot calc sun values for current time + if (mSunrise == 0) // on boot/reboot calc sun values for current time ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); - if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day + if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); tickIVCommunication(); - uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop + uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); if (mMqttEnabled) tickSun(); @@ -231,15 +230,15 @@ void app::tickCalcSunrise(void) { //----------------------------------------------------------------------------- void app::tickIVCommunication(void) { - mIVCommunicationOn = !mConfig->sun.disNightCom; // if sun.disNightCom is false, communication is always on - if (!mIVCommunicationOn) { // inverter communication only during the day + mIVCommunicationOn = !mConfig->sun.disNightCom; // if sun.disNightCom is false, communication is always on + if (!mIVCommunicationOn) { // inverter communication only during the day uint32_t nxtTrig; - if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start + if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start nxtTrig = mSunrise - mConfig->sun.offsetSec; } else { - if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise + if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise nxtTrig = 0; - } else { // current time lies within communication start/stop time, set next trigger to communication stop + } else { // current time lies within communication start/stop time, set next trigger to communication stop mIVCommunicationOn = true; nxtTrig = mSunset + mConfig->sun.offsetSec; } @@ -254,17 +253,17 @@ void app::tickIVCommunication(void) { void app::tickSun(void) { // only used and enabled by MQTT (see setup()) if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) - once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry + once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry } //----------------------------------------------------------------------------- void app::tickComm(void) { - if((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) + if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); if (mMqttEnabled) { if (!mMqtt.tickerComm(!mIVCommunicationOn)) - once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s + once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s } } @@ -275,7 +274,7 @@ void app::tickZeroValues(void) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) - continue; // skip to next inverter + continue; // skip to next inverter mPayload.zeroInverterValues(iv); } @@ -290,9 +289,9 @@ void app::tickMinute(void) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) - continue; // skip to next inverter + continue; // skip to next inverter - if(!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) + if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) mPayload.zeroInverterValues(iv); } } @@ -301,7 +300,7 @@ void app::tickMinute(void) { void app::tickMidnight(void) { // only triggered if 'reset values at midnight is enabled' uint32_t localTime = gTimezone.toLocal(mTimestamp); - uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); Inverter<> *iv; @@ -309,7 +308,7 @@ void app::tickMidnight(void) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) - continue; // skip to next inverter + continue; // skip to next inverter mPayload.zeroInverterValues(iv); mPayload.zeroYieldDay(iv); @@ -321,7 +320,7 @@ void app::tickMidnight(void) { //----------------------------------------------------------------------------- void app::tickSend(void) { - if(!mSys.Radio.isChipConnected()) { + if (!mSys.Radio.isChipConnected()) { DPRINTLN(DBG_WARN, F("NRF24 not connected!")); return; } @@ -341,8 +340,8 @@ void app::tickSend(void) { } while ((NULL == iv) && ((maxLoop--) > 0)); if (NULL != iv) { - if(iv->config->enabled) { - if(iv->ivGen == IV_HM) + if (iv->config->enabled) { + if (iv->ivGen == IV_HM) mPayload.ivSend(iv); else mMiPayload.ivSend(iv); @@ -368,7 +367,7 @@ void app::resetSystem(void) { mSendFirst = true; mSunrise = 0; - mSunset = 0; + mSunset = 0; mMqttEnabled = false; @@ -391,25 +390,25 @@ void app::setupLed(void) { * PIN ---- |<----- 3.3V * * */ - if(mConfig->led.led0 != 0xff) { + if (mConfig->led.led0 != 0xff) { pinMode(mConfig->led.led0, OUTPUT); - digitalWrite(mConfig->led.led0, HIGH); // LED off + digitalWrite(mConfig->led.led0, HIGH); // LED off } - if(mConfig->led.led1 != 0xff) { + if (mConfig->led.led1 != 0xff) { pinMode(mConfig->led.led1, OUTPUT); - digitalWrite(mConfig->led.led1, HIGH); // LED off + digitalWrite(mConfig->led.led1, HIGH); // LED off } } //----------------------------------------------------------------------------- void app::updateLed(void) { - if(mConfig->led.led0 != 0xff) { + if (mConfig->led.led0 != 0xff) { Inverter<> *iv = mSys.getInverterByPos(0); if (NULL != iv) { - if(iv->isProducing(mTimestamp)) - digitalWrite(mConfig->led.led0, LOW); // LED on + if (iv->isProducing(mTimestamp)) + digitalWrite(mConfig->led.led0, LOW); // LED on else - digitalWrite(mConfig->led.led0, HIGH); // LED off + digitalWrite(mConfig->led.led0, HIGH); // LED off } } } diff --git a/src/app.h b/src/app.h index 68281492..8e7c5617 100644 --- a/src/app.h +++ b/src/app.h @@ -6,30 +6,25 @@ #ifndef __APP_H__ #define __APP_H__ - -#include "utils/dbg.h" #include +#include #include #include -#include #include "appInterface.h" - #include "config/settings.h" #include "defines.h" -#include "utils/crc.h" -#include "utils/scheduler.h" - -#include "hm/hmSystem.h" #include "hm/hmPayload.h" +#include "hm/hmSystem.h" #include "hm/miPayload.h" -#include "wifi/ahoywifi.h" -#include "web/web.h" -#include "web/RestApi.h" - #include "publisher/pubMqtt.h" #include "publisher/pubSerial.h" - +#include "utils/crc.h" +#include "utils/dbg.h" +#include "utils/scheduler.h" +#include "web/RestApi.h" +#include "web/web.h" +#include "wifi/ahoywifi.h" // convert degrees and radians for sun calculation #define SIN(x) (sin(radians(x))) @@ -46,235 +41,234 @@ typedef PubMqtt PubMqttType; typedef PubSerial PubSerialType; // PLUGINS -#include "plugins/MonochromeDisplay/MonochromeDisplay.h" -typedef MonochromeDisplay MonoDisplayType; - +#include "plugins/Display/Display.h" +typedef Display DisplayType; class app : public IApp, public ah::Scheduler { - public: - app(); - ~app() {} - - void setup(void); - void loop(void); - void loopStandard(void); - void loopWifi(void); - void onWifi(bool gotIp); - void regularTickers(void); - - void handleIntr(void) { - mSys.Radio.handleIntr(); - } - - uint32_t getUptime() { - return Scheduler::getUptime(); - } - - uint32_t getTimestamp() { - return Scheduler::getTimestamp(); - } - - bool saveSettings() { - mShowRebootRequest = true; - return mSettings.saveSettings(); - } - - bool readSettings(const char *path) { - return mSettings.readSettings(path); + public: + app(); + ~app() {} + + void setup(void); + void loop(void); + void loopStandard(void); + void loopWifi(void); + void onWifi(bool gotIp); + void regularTickers(void); + + void handleIntr(void) { + mSys.Radio.handleIntr(); + } + + uint32_t getUptime() { + return Scheduler::getUptime(); + } + + uint32_t getTimestamp() { + return Scheduler::getTimestamp(); + } + + bool saveSettings() { + mShowRebootRequest = true; + return mSettings.saveSettings(); + } + + bool readSettings(const char *path) { + return mSettings.readSettings(path); + } + + bool eraseSettings(bool eraseWifi = false) { + return mSettings.eraseSettings(eraseWifi); + } + + statistics_t *getStatistics() { + return &mStat; + } + + void scanAvailNetworks() { + mWifi.scanAvailNetworks(); + } + + void getAvailNetworks(JsonObject obj) { + mWifi.getAvailNetworks(obj); + } + + void setOnUpdate() { + onWifi(false); + } + + void setRebootFlag() { + once(std::bind(&app::tickReboot, this), 3, "rboot"); + } + + const char *getVersion() { + return mVersion; + } + + uint32_t getSunrise() { + return mSunrise; + } + + uint32_t getSunset() { + return mSunset; + } + + bool getSettingsValid() { + return mSettings.getValid(); + } + + bool getRebootRequestState() { + return mShowRebootRequest; + } + + void setMqttDiscoveryFlag() { + once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); + } + + void setMqttPowerLimitAck(Inverter<> *iv) { + mMqtt.setPowerLimitAck(iv); + } + + void ivSendHighPrio(Inverter<> *iv) { + if (mIVCommunicationOn) // only send commands if communcation is enabled + mPayload.ivSendHighPrio(iv); + } + + bool getMqttIsConnected() { + return mMqtt.isConnected(); + } + + uint32_t getMqttTxCnt() { + return mMqtt.getTxCnt(); + } + + uint32_t getMqttRxCnt() { + return mMqtt.getRxCnt(); + } + + bool getProtection() { + return mWeb.getProtection(); + } + + uint8_t getIrqPin(void) { + return mConfig->nrf.pinIrq; + } + + String getTimeStr(uint32_t offset = 0) { + char str[10]; + if (0 == mTimestamp) + sprintf(str, "n/a"); + else + sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp + offset), minute(mTimestamp + offset), second(mTimestamp + offset)); + return String(str); + } + + uint32_t getTimezoneOffset() { + return mApi.getTimezoneOffset(); + } + + void getSchedulerInfo(uint8_t *max) { + getStat(max); + } + + void getSchedulerNames(void) { + printSchedulers(); + } + + void setTimestamp(uint32_t newTime) { + DPRINT(DBG_DEBUG, F("setTimestamp: ")); + DBGPRINTLN(String(newTime)); + if (0 == newTime) + mWifi.getNtpTime(); + else + Scheduler::setTimestamp(newTime); + } + + HmSystemType mSys; + + private: + typedef std::function innerLoopCb; + + void resetSystem(void); + + void payloadEventListener(uint8_t cmd) { +#if !defined(AP_ONLY) + if (mMqttEnabled) + mMqtt.payloadEventListener(cmd); +#endif + if (mConfig->plugin.display.type != 0) + mDisplay.payloadEventListener(cmd); + } + + void mqttSubRxCb(JsonObject obj); + + void setupLed(void); + void updateLed(void); + + void tickReboot(void) { + DPRINTLN(DBG_INFO, F("Rebooting...")); + onWifi(false); + ah::Scheduler::resetTicker(); + WiFi.disconnect(); + ESP.restart(); + } + + void tickNtpUpdate(void); + void tickCalcSunrise(void); + void tickIVCommunication(void); + void tickSun(void); + void tickComm(void); + void tickSend(void); + void tickMinute(void); + void tickZeroValues(void); + void tickMidnight(void); + /*void tickSerial(void) { + if(Serial.available() == 0) + return; + + uint8_t buf[80]; + uint8_t len = Serial.readBytes(buf, 80); + DPRINTLN(DBG_INFO, "got serial data, len: " + String(len)); + for(uint8_t i = 0; i < len; i++) { + if((0 != i) && (i % 8 == 0)) + DBGPRINTLN(""); + DBGPRINT(String(buf[i], HEX) + " "); } + DBGPRINTLN(""); + }*/ - bool eraseSettings(bool eraseWifi = false) { - return mSettings.eraseSettings(eraseWifi); - } + innerLoopCb mInnerLoopCb; - statistics_t *getStatistics() { - return &mStat; - } + bool mShowRebootRequest; + bool mIVCommunicationOn; - void scanAvailNetworks() { - mWifi.scanAvailNetworks(); - } - - void getAvailNetworks(JsonObject obj) { - mWifi.getAvailNetworks(obj); - } + ahoywifi mWifi; + WebType mWeb; + RestApiType mApi; + PayloadType mPayload; + MiPayloadType mMiPayload; + PubSerialType mPubSerial; - void setOnUpdate() { - onWifi(false); - } + char mVersion[12]; + settings mSettings; + settings_t *mConfig; - void setRebootFlag() { - once(std::bind(&app::tickReboot, this), 3, "rboot"); - } + uint8_t mSendLastIvId; + bool mSendFirst; - const char *getVersion() { - return mVersion; - } + statistics_t mStat; - uint32_t getSunrise() { - return mSunrise; - } + // mqtt + PubMqttType mMqtt; + bool mMqttReconnect; + bool mMqttEnabled; - uint32_t getSunset() { - return mSunset; - } - - bool getSettingsValid() { - return mSettings.getValid(); - } - - bool getRebootRequestState() { - return mShowRebootRequest; - } - - void setMqttDiscoveryFlag() { - once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); - } - - void setMqttPowerLimitAck(Inverter<> *iv) { - mMqtt.setPowerLimitAck(iv); - } - - void ivSendHighPrio(Inverter<> *iv) { - if(mIVCommunicationOn) // only send commands if communcation is enabled - mPayload.ivSendHighPrio(iv); - } - - bool getMqttIsConnected() { - return mMqtt.isConnected(); - } - - uint32_t getMqttTxCnt() { - return mMqtt.getTxCnt(); - } - - uint32_t getMqttRxCnt() { - return mMqtt.getRxCnt(); - } - - bool getProtection() { - return mWeb.getProtection(); - } - - uint8_t getIrqPin(void) { - return mConfig->nrf.pinIrq; - } - - String getTimeStr(uint32_t offset = 0) { - char str[10]; - if(0 == mTimestamp) - sprintf(str, "n/a"); - else - sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp + offset), minute(mTimestamp + offset), second(mTimestamp + offset)); - return String(str); - } - - uint32_t getTimezoneOffset() { - return mApi.getTimezoneOffset(); - } - - void getSchedulerInfo(uint8_t *max) { - getStat(max); - } - - void getSchedulerNames(void) { - printSchedulers(); - } - - void setTimestamp(uint32_t newTime) { - DPRINT(DBG_DEBUG, F("setTimestamp: ")); - DBGPRINTLN(String(newTime)); - if(0 == newTime) - mWifi.getNtpTime(); - else - Scheduler::setTimestamp(newTime); - } - - HmSystemType mSys; - - private: - typedef std::function innerLoopCb; - - void resetSystem(void); - - void payloadEventListener(uint8_t cmd) { - #if !defined(AP_ONLY) - if (mMqttEnabled) - mMqtt.payloadEventListener(cmd); - #endif - if(mConfig->plugin.display.type != 0) - mMonoDisplay.payloadEventListener(cmd); - } - - void mqttSubRxCb(JsonObject obj); - - void setupLed(void); - void updateLed(void); - - void tickReboot(void) { - DPRINTLN(DBG_INFO, F("Rebooting...")); - onWifi(false); - ah::Scheduler::resetTicker(); - WiFi.disconnect(); - ESP.restart(); - } + // sun + int32_t mCalculatedTimezoneOffset; + uint32_t mSunrise, mSunset; - void tickNtpUpdate(void); - void tickCalcSunrise(void); - void tickIVCommunication(void); - void tickSun(void); - void tickComm(void); - void tickSend(void); - void tickMinute(void); - void tickZeroValues(void); - void tickMidnight(void); - /*void tickSerial(void) { - if(Serial.available() == 0) - return; - - uint8_t buf[80]; - uint8_t len = Serial.readBytes(buf, 80); - DPRINTLN(DBG_INFO, "got serial data, len: " + String(len)); - for(uint8_t i = 0; i < len; i++) { - if((0 != i) && (i % 8 == 0)) - DBGPRINTLN(""); - DBGPRINT(String(buf[i], HEX) + " "); - } - DBGPRINTLN(""); - }*/ - - innerLoopCb mInnerLoopCb; - - bool mShowRebootRequest; - bool mIVCommunicationOn; - - ahoywifi mWifi; - WebType mWeb; - RestApiType mApi; - PayloadType mPayload; - MiPayloadType mMiPayload; - PubSerialType mPubSerial; - - char mVersion[12]; - settings mSettings; - settings_t *mConfig; - - uint8_t mSendLastIvId; - bool mSendFirst; - - statistics_t mStat; - - // mqtt - PubMqttType mMqtt; - bool mMqttReconnect; - bool mMqttEnabled; - - // sun - int32_t mCalculatedTimezoneOffset; - uint32_t mSunrise, mSunset; - - // plugins - MonoDisplayType mMonoDisplay; + // plugins + DisplayType mDisplay; }; #endif /*__APP_H__*/ diff --git a/src/config/settings.h b/src/config/settings.h index f674dac4..b02b5a61 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -7,44 +7,43 @@ #define __SETTINGS_H__ #include -#include #include +#include + +#include "../defines.h" #include "../utils/dbg.h" #include "../utils/helper.h" -#include "../defines.h" /** * More info: * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * */ -#define DEF_PIN_OFF 255 - - -#define PROT_MASK_INDEX 0x0001 -#define PROT_MASK_LIVE 0x0002 -#define PROT_MASK_SERIAL 0x0004 -#define PROT_MASK_SETUP 0x0008 -#define PROT_MASK_UPDATE 0x0010 -#define PROT_MASK_SYSTEM 0x0020 -#define PROT_MASK_API 0x0040 -#define PROT_MASK_MQTT 0x0080 - -#define DEF_PROT_INDEX 0x0001 -#define DEF_PROT_LIVE 0x0000 -#define DEF_PROT_SERIAL 0x0004 -#define DEF_PROT_SETUP 0x0008 -#define DEF_PROT_UPDATE 0x0010 -#define DEF_PROT_SYSTEM 0x0020 -#define DEF_PROT_API 0x0000 -#define DEF_PROT_MQTT 0x0000 - +#define DEF_PIN_OFF 255 + +#define PROT_MASK_INDEX 0x0001 +#define PROT_MASK_LIVE 0x0002 +#define PROT_MASK_SERIAL 0x0004 +#define PROT_MASK_SETUP 0x0008 +#define PROT_MASK_UPDATE 0x0010 +#define PROT_MASK_SYSTEM 0x0020 +#define PROT_MASK_API 0x0040 +#define PROT_MASK_MQTT 0x0080 + +#define DEF_PROT_INDEX 0x0001 +#define DEF_PROT_LIVE 0x0000 +#define DEF_PROT_SERIAL 0x0004 +#define DEF_PROT_SETUP 0x0008 +#define DEF_PROT_UPDATE 0x0010 +#define DEF_PROT_SYSTEM 0x0020 +#define DEF_PROT_API 0x0000 +#define DEF_PROT_MQTT 0x0000 typedef struct { - uint8_t ip[4]; // ip address - uint8_t mask[4]; // sub mask - uint8_t dns1[4]; // dns 1 - uint8_t dns2[4]; // dns 2 - uint8_t gateway[4]; // standard gateway + uint8_t ip[4]; // ip address + uint8_t mask[4]; // sub mask + uint8_t dns1[4]; // dns 1 + uint8_t dns2[4]; // dns 2 + uint8_t gateway[4]; // standard gateway } cfgIp_t; typedef struct { @@ -77,7 +76,7 @@ typedef struct { typedef struct { float lat; float lon; - bool disNightCom; // disable night communication + bool disNightCom; // disable night communication uint16_t offsetSec; } cfgSun_t; @@ -88,8 +87,8 @@ typedef struct { } cfgSerial_t; typedef struct { - uint8_t led0; // first LED pin - uint8_t led1; // second LED pin + uint8_t led0; // first LED pin + uint8_t led1; // second LED pin } cfgLed_t; typedef struct { @@ -106,7 +105,7 @@ typedef struct { char name[MAX_NAME_LENGTH]; serial_u serial; uint16_t chMaxPwr[4]; - int32_t yieldCor[4]; // signed YieldTotal correction value + int32_t yieldCor[4]; // signed YieldTotal correction value char chName[4][MAX_NAME_LENGTH]; } cfgIv_t; @@ -124,12 +123,17 @@ typedef struct { bool pwrSaveAtIvOffline; bool logoEn; bool pxShift; - bool rot180; + uint8_t rot; + uint16_t period; uint16_t wakeUp; uint16_t sleepAt; uint8_t contrast; - uint8_t pin0; - uint8_t pin1; + uint8_t disp_data; + uint8_t disp_clk; + uint8_t disp_cs; + uint8_t disp_reset; + uint8_t disp_busy; + uint8_t disp_dc; } display_t; typedef struct { @@ -137,409 +141,422 @@ typedef struct { } plugins_t; typedef struct { - cfgSys_t sys; - cfgNrf24_t nrf; - cfgNtp_t ntp; - cfgSun_t sun; + cfgSys_t sys; + cfgNrf24_t nrf; + cfgNtp_t ntp; + cfgSun_t sun; cfgSerial_t serial; - cfgMqtt_t mqtt; - cfgLed_t led; - cfgInst_t inst; - plugins_t plugin; - bool valid; + cfgMqtt_t mqtt; + cfgLed_t led; + cfgInst_t inst; + plugins_t plugin; + bool valid; } settings_t; class settings { - public: - settings() {} - - void setup() { - DPRINTLN(DBG_INFO, F("Initializing FS ..")); - - mCfg.valid = false; - #if !defined(ESP32) - LittleFSConfig cfg; - cfg.setAutoFormat(false); - LittleFS.setConfig(cfg); - #define LITTLFS_TRUE - #define LITTLFS_FALSE - #else - #define LITTLFS_TRUE true - #define LITTLFS_FALSE false - #endif - - if(!LittleFS.begin(LITTLFS_FALSE)) { - DPRINTLN(DBG_INFO, F(".. format ..")); - LittleFS.format(); - if(LittleFS.begin(LITTLFS_TRUE)) { - DPRINTLN(DBG_INFO, F(".. success")); - } else { - DPRINTLN(DBG_INFO, F(".. failed")); - } - + public: + settings() {} + + void setup() { + DPRINTLN(DBG_INFO, F("Initializing FS ..")); + + mCfg.valid = false; +#if !defined(ESP32) + LittleFSConfig cfg; + cfg.setAutoFormat(false); + LittleFS.setConfig(cfg); +#define LITTLFS_TRUE +#define LITTLFS_FALSE +#else +#define LITTLFS_TRUE true +#define LITTLFS_FALSE false +#endif + + if (!LittleFS.begin(LITTLFS_FALSE)) { + DPRINTLN(DBG_INFO, F(".. format ..")); + LittleFS.format(); + if (LittleFS.begin(LITTLFS_TRUE)) { + DPRINTLN(DBG_INFO, F(".. success")); + } else { + DPRINTLN(DBG_INFO, F(".. failed")); } - else - DPRINTLN(DBG_INFO, F(" .. done")); - readSettings("/settings.json"); - } - - // should be used before OTA - void stop() { - LittleFS.end(); - DPRINTLN(DBG_INFO, F("FS stopped")); - } + } else + DPRINTLN(DBG_INFO, F(" .. done")); + + readSettings("/settings.json"); + } + + // should be used before OTA + void stop() { + LittleFS.end(); + DPRINTLN(DBG_INFO, F("FS stopped")); + } + + void getPtr(settings_t *&cfg) { + cfg = &mCfg; + } + + bool getValid(void) { + return mCfg.valid; + } + + void getInfo(uint32_t *used, uint32_t *size) { +#if !defined(ESP32) + FSInfo info; + LittleFS.info(info); + *used = info.usedBytes; + *size = info.totalBytes; + + DPRINTLN(DBG_INFO, F("-- FILESYSTEM INFO --")); + DPRINTLN(DBG_INFO, String(info.usedBytes) + F(" of ") + String(info.totalBytes) + F(" used")); +#else + DPRINTLN(DBG_WARN, F("not supported by ESP32")); +#endif + } + + bool readSettings(const char *path) { + loadDefaults(); + File fp = LittleFS.open(path, "r"); + if (!fp) + DPRINTLN(DBG_WARN, F("failed to load json, using default config")); + else { + // DPRINTLN(DBG_INFO, fp.readString()); + // fp.seek(0, SeekSet); + DynamicJsonDocument root(4500); + DeserializationError err = deserializeJson(root, fp); + if (!err && (root.size() > 0)) { + mCfg.valid = true; + jsonWifi(root[F("wifi")]); + jsonNrf(root[F("nrf")]); + jsonNtp(root[F("ntp")]); + jsonSun(root[F("sun")]); + jsonSerial(root[F("serial")]); + jsonMqtt(root[F("mqtt")]); + jsonLed(root[F("led")]); + jsonPlugin(root[F("plugin")]); + jsonInst(root[F("inst")]); + } else { + Serial.println(F("failed to parse json, using default config")); + } - void getPtr(settings_t *&cfg) { - cfg = &mCfg; + fp.close(); } - - bool getValid(void) { - return mCfg.valid; + return mCfg.valid; + } + + bool saveSettings(void) { + DPRINTLN(DBG_DEBUG, F("save settings")); + File fp = LittleFS.open("/settings.json", "w"); + if (!fp) { + DPRINTLN(DBG_ERROR, F("can't open settings file!")); + return false; } - void getInfo(uint32_t *used, uint32_t *size) { - #if !defined(ESP32) - FSInfo info; - LittleFS.info(info); - *used = info.usedBytes; - *size = info.totalBytes; - - DPRINTLN(DBG_INFO, F("-- FILESYSTEM INFO --")); - DPRINTLN(DBG_INFO, String(info.usedBytes) + F(" of ") + String(info.totalBytes) + F(" used")); - #else - DPRINTLN(DBG_WARN, F("not supported by ESP32")); - #endif + DynamicJsonDocument json(5500); + JsonObject root = json.to(); + jsonWifi(root.createNestedObject(F("wifi")), true); + jsonNrf(root.createNestedObject(F("nrf")), true); + jsonNtp(root.createNestedObject(F("ntp")), true); + jsonSun(root.createNestedObject(F("sun")), true); + jsonSerial(root.createNestedObject(F("serial")), true); + jsonMqtt(root.createNestedObject(F("mqtt")), true); + jsonLed(root.createNestedObject(F("led")), true); + jsonPlugin(root.createNestedObject(F("plugin")), true); + jsonInst(root.createNestedObject(F("inst")), true); + + if (0 == serializeJson(root, fp)) { + DPRINTLN(DBG_ERROR, F("can't write settings file!")); + return false; } - - bool readSettings(const char* path) { - loadDefaults(); - File fp = LittleFS.open(path, "r"); - if(!fp) - DPRINTLN(DBG_WARN, F("failed to load json, using default config")); - else { - //DPRINTLN(DBG_INFO, fp.readString()); - //fp.seek(0, SeekSet); - DynamicJsonDocument root(4500); - DeserializationError err = deserializeJson(root, fp); - if(!err && (root.size() > 0)) { - mCfg.valid = true; - jsonWifi(root[F("wifi")]); - jsonNrf(root[F("nrf")]); - jsonNtp(root[F("ntp")]); - jsonSun(root[F("sun")]); - jsonSerial(root[F("serial")]); - jsonMqtt(root[F("mqtt")]); - jsonLed(root[F("led")]); - jsonPlugin(root[F("plugin")]); - jsonInst(root[F("inst")]); - } - else { - Serial.println(F("failed to parse json, using default config")); - } - - fp.close(); - } - return mCfg.valid; + fp.close(); + + return true; + } + + bool eraseSettings(bool eraseWifi = false) { + if (true == eraseWifi) + return LittleFS.format(); + loadDefaults(!eraseWifi); + return saveSettings(); + } + + private: + void loadDefaults(bool keepWifi = false) { + DPRINTLN(DBG_VERBOSE, F("loadDefaults")); + + cfgSys_t tmp; + if (keepWifi) { + // copy contents which should not be deleted + memset(&tmp.adminPwd, 0, PWD_LEN); + memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t)); } - - bool saveSettings(void) { - DPRINTLN(DBG_DEBUG, F("save settings")); - File fp = LittleFS.open("/settings.json", "w"); - if(!fp) { - DPRINTLN(DBG_ERROR, F("can't open settings file!")); - return false; - } - - DynamicJsonDocument json(5500); - JsonObject root = json.to(); - jsonWifi(root.createNestedObject(F("wifi")), true); - jsonNrf(root.createNestedObject(F("nrf")), true); - jsonNtp(root.createNestedObject(F("ntp")), true); - jsonSun(root.createNestedObject(F("sun")), true); - jsonSerial(root.createNestedObject(F("serial")), true); - jsonMqtt(root.createNestedObject(F("mqtt")), true); - jsonLed(root.createNestedObject(F("led")), true); - jsonPlugin(root.createNestedObject(F("plugin")), true); - jsonInst(root.createNestedObject(F("inst")), true); - - if(0 == serializeJson(root, fp)) { - DPRINTLN(DBG_ERROR, F("can't write settings file!")); - return false; - } - fp.close(); - - return true; + // erase all settings and reset to default + memset(&mCfg, 0, sizeof(settings_t)); + mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; + mCfg.sys.darkMode = false; + // restore temp settings + if (keepWifi) + memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); + else { + snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); + snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); } - bool eraseSettings(bool eraseWifi = false) { - if(true == eraseWifi) - return LittleFS.format(); - loadDefaults(!eraseWifi); - return saveSettings(); + snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); + + mCfg.nrf.sendInterval = SEND_INTERVAL; + mCfg.nrf.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; + mCfg.nrf.pinCs = DEF_CS_PIN; + mCfg.nrf.pinCe = DEF_CE_PIN; + mCfg.nrf.pinIrq = DEF_IRQ_PIN; + mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; + + snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); + mCfg.ntp.port = DEF_NTP_PORT; + + mCfg.sun.lat = 0.0; + mCfg.sun.lon = 0.0; + mCfg.sun.disNightCom = false; + mCfg.sun.offsetSec = 0; + + mCfg.serial.interval = SERIAL_INTERVAL; + mCfg.serial.showIv = false; + mCfg.serial.debug = false; + + mCfg.mqtt.port = DEF_MQTT_PORT; + snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); + snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER); + snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); + snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); + mCfg.mqtt.interval = 0; // off + + mCfg.inst.rstYieldMidNight = false; + mCfg.inst.rstValsNotAvail = false; + mCfg.inst.rstValsCommStop = false; + + mCfg.led.led0 = DEF_PIN_OFF; + mCfg.led.led1 = DEF_PIN_OFF; + + memset(&mCfg.inst, 0, sizeof(cfgInst_t)); + + mCfg.plugin.display.pwrSaveAtIvOffline = false; + mCfg.plugin.display.contrast = 60; + mCfg.plugin.display.logoEn = true; + mCfg.plugin.display.pxShift = true; + mCfg.plugin.display.rot = 0; + mCfg.plugin.display.period = 10000; + mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA + mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL + mCfg.plugin.display.disp_cs = DEF_PIN_OFF; + mCfg.plugin.display.disp_reset = DEF_PIN_OFF; + mCfg.plugin.display.disp_busy = DEF_PIN_OFF; + mCfg.plugin.display.disp_dc = DEF_PIN_OFF; + } + + void jsonWifi(JsonObject obj, bool set = false) { + if (set) { + char buf[16]; + obj[F("ssid")] = mCfg.sys.stationSsid; + obj[F("pwd")] = mCfg.sys.stationPwd; + obj[F("dev")] = mCfg.sys.deviceName; + obj[F("adm")] = mCfg.sys.adminPwd; + obj[F("prot_mask")] = mCfg.sys.protectionMask; + obj[F("dark")] = mCfg.sys.darkMode; + ah::ip2Char(mCfg.sys.ip.ip, buf); + obj[F("ip")] = String(buf); + ah::ip2Char(mCfg.sys.ip.mask, buf); + obj[F("mask")] = String(buf); + ah::ip2Char(mCfg.sys.ip.dns1, buf); + obj[F("dns1")] = String(buf); + ah::ip2Char(mCfg.sys.ip.dns2, buf); + obj[F("dns2")] = String(buf); + ah::ip2Char(mCfg.sys.ip.gateway, buf); + obj[F("gtwy")] = String(buf); + } else { + snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as()); + snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as()); + snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as()); + snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as()); + mCfg.sys.protectionMask = obj[F("prot_mask")]; + mCfg.sys.darkMode = obj[F("dark")]; + ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); + ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); + ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); + ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as()); + ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as()); + + if (mCfg.sys.protectionMask == 0) + mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; } - - private: - void loadDefaults(bool keepWifi = false) { - DPRINTLN(DBG_VERBOSE, F("loadDefaults")); - - cfgSys_t tmp; - if(keepWifi) { - // copy contents which should not be deleted - memset(&tmp.adminPwd, 0, PWD_LEN); - memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t)); - } - // erase all settings and reset to default - memset(&mCfg, 0, sizeof(settings_t)); - mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP - | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; - mCfg.sys.darkMode = false; - // restore temp settings - if(keepWifi) - memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); - else { - snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); - snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); - } - - snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); - - mCfg.nrf.sendInterval = SEND_INTERVAL; - mCfg.nrf.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; - mCfg.nrf.pinCs = DEF_CS_PIN; - mCfg.nrf.pinCe = DEF_CE_PIN; - mCfg.nrf.pinIrq = DEF_IRQ_PIN; - mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; - - snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); - mCfg.ntp.port = DEF_NTP_PORT; - - mCfg.sun.lat = 0.0; - mCfg.sun.lon = 0.0; - mCfg.sun.disNightCom = false; - mCfg.sun.offsetSec = 0; - - mCfg.serial.interval = SERIAL_INTERVAL; - mCfg.serial.showIv = false; - mCfg.serial.debug = false; - - mCfg.mqtt.port = DEF_MQTT_PORT; - snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); - snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER); - snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); - snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); - mCfg.mqtt.interval = 0; // off - - mCfg.inst.rstYieldMidNight = false; - mCfg.inst.rstValsNotAvail = false; - mCfg.inst.rstValsCommStop = false; - - mCfg.led.led0 = DEF_PIN_OFF; - mCfg.led.led1 = DEF_PIN_OFF; - - memset(&mCfg.inst, 0, sizeof(cfgInst_t)); - - mCfg.plugin.display.pwrSaveAtIvOffline = false; - mCfg.plugin.display.contrast = 60; - mCfg.plugin.display.logoEn = true; - mCfg.plugin.display.pxShift = true; - mCfg.plugin.display.rot180 = false; - mCfg.plugin.display.pin0 = DEF_PIN_OFF; // SCL - mCfg.plugin.display.pin1 = DEF_PIN_OFF; // SDA + } + + void jsonNrf(JsonObject obj, bool set = false) { + if (set) { + obj[F("intvl")] = mCfg.nrf.sendInterval; + obj[F("maxRetry")] = mCfg.nrf.maxRetransPerPyld; + obj[F("cs")] = mCfg.nrf.pinCs; + obj[F("ce")] = mCfg.nrf.pinCe; + obj[F("irq")] = mCfg.nrf.pinIrq; + obj[F("pwr")] = mCfg.nrf.amplifierPower; + } else { + mCfg.nrf.sendInterval = obj[F("intvl")]; + mCfg.nrf.maxRetransPerPyld = obj[F("maxRetry")]; + mCfg.nrf.pinCs = obj[F("cs")]; + mCfg.nrf.pinCe = obj[F("ce")]; + mCfg.nrf.pinIrq = obj[F("irq")]; + mCfg.nrf.amplifierPower = obj[F("pwr")]; } - - void jsonWifi(JsonObject obj, bool set = false) { - if(set) { - char buf[16]; - obj[F("ssid")] = mCfg.sys.stationSsid; - obj[F("pwd")] = mCfg.sys.stationPwd; - obj[F("dev")] = mCfg.sys.deviceName; - obj[F("adm")] = mCfg.sys.adminPwd; - obj[F("prot_mask")] = mCfg.sys.protectionMask; - obj[F("dark")] = mCfg.sys.darkMode; - ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf); - ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf); - ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); - ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf); - ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf); - } else { - snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as()); - snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as()); - snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as()); - snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as()); - mCfg.sys.protectionMask = obj[F("prot_mask")]; - mCfg.sys.darkMode = obj[F("dark")]; - ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); - ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); - ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); - ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as()); - ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as()); - - if(mCfg.sys.protectionMask == 0) - mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP - | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; - } + } + + void jsonNtp(JsonObject obj, bool set = false) { + if (set) { + obj[F("addr")] = mCfg.ntp.addr; + obj[F("port")] = mCfg.ntp.port; + } else { + snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", obj[F("addr")].as()); + mCfg.ntp.port = obj[F("port")]; } - - void jsonNrf(JsonObject obj, bool set = false) { - if(set) { - obj[F("intvl")] = mCfg.nrf.sendInterval; - obj[F("maxRetry")] = mCfg.nrf.maxRetransPerPyld; - obj[F("cs")] = mCfg.nrf.pinCs; - obj[F("ce")] = mCfg.nrf.pinCe; - obj[F("irq")] = mCfg.nrf.pinIrq; - obj[F("pwr")] = mCfg.nrf.amplifierPower; - } else { - mCfg.nrf.sendInterval = obj[F("intvl")]; - mCfg.nrf.maxRetransPerPyld = obj[F("maxRetry")]; - mCfg.nrf.pinCs = obj[F("cs")]; - mCfg.nrf.pinCe = obj[F("ce")]; - mCfg.nrf.pinIrq = obj[F("irq")]; - mCfg.nrf.amplifierPower = obj[F("pwr")]; - } + } + + void jsonSun(JsonObject obj, bool set = false) { + if (set) { + obj[F("lat")] = mCfg.sun.lat; + obj[F("lon")] = mCfg.sun.lon; + obj[F("dis")] = mCfg.sun.disNightCom; + obj[F("offs")] = mCfg.sun.offsetSec; + } else { + mCfg.sun.lat = obj[F("lat")]; + mCfg.sun.lon = obj[F("lon")]; + mCfg.sun.disNightCom = obj[F("dis")]; + mCfg.sun.offsetSec = obj[F("offs")]; } - - void jsonNtp(JsonObject obj, bool set = false) { - if(set) { - obj[F("addr")] = mCfg.ntp.addr; - obj[F("port")] = mCfg.ntp.port; - } else { - snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", obj[F("addr")].as()); - mCfg.ntp.port = obj[F("port")]; - } + } + + void jsonSerial(JsonObject obj, bool set = false) { + if (set) { + obj[F("intvl")] = mCfg.serial.interval; + obj[F("show")] = mCfg.serial.showIv; + obj[F("debug")] = mCfg.serial.debug; + } else { + mCfg.serial.interval = obj[F("intvl")]; + mCfg.serial.showIv = obj[F("show")]; + mCfg.serial.debug = obj[F("debug")]; } - - void jsonSun(JsonObject obj, bool set = false) { - if(set) { - obj[F("lat")] = mCfg.sun.lat; - obj[F("lon")] = mCfg.sun.lon; - obj[F("dis")] = mCfg.sun.disNightCom; - obj[F("offs")] = mCfg.sun.offsetSec; - } else { - mCfg.sun.lat = obj[F("lat")]; - mCfg.sun.lon = obj[F("lon")]; - mCfg.sun.disNightCom = obj[F("dis")]; - mCfg.sun.offsetSec = obj[F("offs")]; - } + } + + void jsonMqtt(JsonObject obj, bool set = false) { + if (set) { + obj[F("broker")] = mCfg.mqtt.broker; + obj[F("port")] = mCfg.mqtt.port; + obj[F("user")] = mCfg.mqtt.user; + obj[F("pwd")] = mCfg.mqtt.pwd; + obj[F("topic")] = mCfg.mqtt.topic; + obj[F("intvl")] = mCfg.mqtt.interval; + + } else { + mCfg.mqtt.port = obj[F("port")]; + mCfg.mqtt.interval = obj[F("intvl")]; + snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as()); + snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as()); + snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as()); + snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", obj[F("topic")].as()); } - - void jsonSerial(JsonObject obj, bool set = false) { - if(set) { - obj[F("intvl")] = mCfg.serial.interval; - obj[F("show")] = mCfg.serial.showIv; - obj[F("debug")] = mCfg.serial.debug; - } else { - mCfg.serial.interval = obj[F("intvl")]; - mCfg.serial.showIv = obj[F("show")]; - mCfg.serial.debug = obj[F("debug")]; - } + } + + void jsonLed(JsonObject obj, bool set = false) { + if (set) { + obj[F("0")] = mCfg.led.led0; + obj[F("1")] = mCfg.led.led1; + } else { + mCfg.led.led0 = obj[F("0")]; + mCfg.led.led1 = obj[F("1")]; } - - void jsonMqtt(JsonObject obj, bool set = false) { - if(set) { - obj[F("broker")] = mCfg.mqtt.broker; - obj[F("port")] = mCfg.mqtt.port; - obj[F("user")] = mCfg.mqtt.user; - obj[F("pwd")] = mCfg.mqtt.pwd; - obj[F("topic")] = mCfg.mqtt.topic; - obj[F("intvl")] = mCfg.mqtt.interval; - - } else { - mCfg.mqtt.port = obj[F("port")]; - mCfg.mqtt.interval = obj[F("intvl")]; - snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as()); - snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as()); - snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as()); - snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", obj[F("topic")].as()); - } + } + + void jsonPlugin(JsonObject obj, bool set = false) { + if (set) { + JsonObject disp = obj.createNestedObject("disp"); + disp[F("type")] = mCfg.plugin.display.type; + disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; + disp[F("period")] = mCfg.plugin.display.period; + disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; + disp[F("rotation")] = mCfg.plugin.display.rot; + disp[F("wake")] = mCfg.plugin.display.wakeUp; + disp[F("sleep")] = mCfg.plugin.display.sleepAt; + disp[F("contrast")] = mCfg.plugin.display.contrast; + disp[F("data")] = mCfg.plugin.display.disp_data; + disp[F("clock")] = mCfg.plugin.display.disp_clk; + disp[F("cs")] = mCfg.plugin.display.disp_cs; + disp[F("reset")] = mCfg.plugin.display.disp_reset; + disp[F("busy")] = mCfg.plugin.display.disp_busy; + disp[F("dc")] = mCfg.plugin.display.disp_dc; + } else { + JsonObject disp = obj["disp"]; + mCfg.plugin.display.type = disp[F("type")]; + mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")]; + mCfg.plugin.display.period = disp[F("period")]; + mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")]; + mCfg.plugin.display.rot = disp[F("rotation")]; + mCfg.plugin.display.wakeUp = disp[F("wake")]; + mCfg.plugin.display.sleepAt = disp[F("sleep")]; + mCfg.plugin.display.contrast = disp[F("contrast")]; + mCfg.plugin.display.disp_data = disp[F("data")]; + mCfg.plugin.display.disp_clk = disp[F("clock")]; + mCfg.plugin.display.disp_cs = disp[F("cs")]; + mCfg.plugin.display.disp_reset = disp[F("reset")]; + mCfg.plugin.display.disp_busy = disp[F("busy")]; + mCfg.plugin.display.disp_dc = disp[F("dc")]; } - - void jsonLed(JsonObject obj, bool set = false) { - if(set) { - obj[F("0")] = mCfg.led.led0; - obj[F("1")] = mCfg.led.led1; - } else { - mCfg.led.led0 = obj[F("0")]; - mCfg.led.led1 = obj[F("1")]; - } + } + + void jsonInst(JsonObject obj, bool set = false) { + if (set) { + obj[F("en")] = (bool)mCfg.inst.enabled; + obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; + obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; + } else { + mCfg.inst.enabled = (bool)obj[F("en")]; + mCfg.inst.rstYieldMidNight = (bool)obj["rstMidNight"]; + mCfg.inst.rstValsNotAvail = (bool)obj["rstNotAvail"]; + mCfg.inst.rstValsCommStop = (bool)obj["rstComStop"]; } - void jsonPlugin(JsonObject obj, bool set = false) { - if(set) { - JsonObject disp = obj.createNestedObject("disp"); - disp[F("type")] = mCfg.plugin.display.type; - disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; - disp[F("logo")] = (bool)mCfg.plugin.display.logoEn; - disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; - disp[F("rot180")] = (bool)mCfg.plugin.display.rot180; - disp[F("wake")] = mCfg.plugin.display.wakeUp; - disp[F("sleep")] = mCfg.plugin.display.sleepAt; - disp[F("contrast")] = mCfg.plugin.display.contrast; - disp[F("pin0")] = mCfg.plugin.display.pin0; - disp[F("pin1")] = mCfg.plugin.display.pin1; - } else { - JsonObject disp = obj["disp"]; - mCfg.plugin.display.type = disp[F("type")]; - mCfg.plugin.display.pwrSaveAtIvOffline = (bool) disp[F("pwrSafe")]; - mCfg.plugin.display.logoEn = (bool) disp[F("logo")]; - mCfg.plugin.display.pxShift = (bool) disp[F("pxShift")]; - mCfg.plugin.display.rot180 = (bool) disp[F("rot180")]; - mCfg.plugin.display.wakeUp = disp[F("wake")]; - mCfg.plugin.display.sleepAt = disp[F("sleep")]; - mCfg.plugin.display.contrast = disp[F("contrast")]; - mCfg.plugin.display.pin0 = disp[F("pin0")]; - mCfg.plugin.display.pin1 = disp[F("pin1")]; - } + JsonArray ivArr; + if (set) + ivArr = obj.createNestedArray(F("iv")); + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + if (set) + jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); + else + jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); } - - void jsonInst(JsonObject obj, bool set = false) { - if(set) { - obj[F("en")] = (bool)mCfg.inst.enabled; - obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; - obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; - obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; - } - else { - mCfg.inst.enabled = (bool)obj[F("en")]; - mCfg.inst.rstYieldMidNight = (bool)obj["rstMidNight"]; - mCfg.inst.rstValsNotAvail = (bool)obj["rstNotAvail"]; - mCfg.inst.rstValsCommStop = (bool)obj["rstComStop"]; - } - - JsonArray ivArr; - if(set) - ivArr = obj.createNestedArray(F("iv")); - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - if(set) - jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); - else - jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); + } + + void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) { + if (set) { + obj[F("en")] = (bool)cfg->enabled; + obj[F("name")] = cfg->name; + obj[F("sn")] = cfg->serial.u64; + for (uint8_t i = 0; i < 4; i++) { + obj[F("yield")][i] = cfg->yieldCor[i]; + obj[F("pwr")][i] = cfg->chMaxPwr[i]; + obj[F("chName")][i] = cfg->chName[i]; } - } - - void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) { - if(set) { - obj[F("en")] = (bool)cfg->enabled; - obj[F("name")] = cfg->name; - obj[F("sn")] = cfg->serial.u64; - for(uint8_t i = 0; i < 4; i++) { - obj[F("yield")][i] = cfg->yieldCor[i]; - obj[F("pwr")][i] = cfg->chMaxPwr[i]; - obj[F("chName")][i] = cfg->chName[i]; - } - } else { - cfg->enabled = (bool)obj[F("en")]; - snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as()); - cfg->serial.u64 = obj[F("sn")]; - for(uint8_t i = 0; i < 4; i++) { - cfg->yieldCor[i] = obj[F("yield")][i]; - cfg->chMaxPwr[i] = obj[F("pwr")][i]; - snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); - } + } else { + cfg->enabled = (bool)obj[F("en")]; + snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as()); + cfg->serial.u64 = obj[F("sn")]; + for (uint8_t i = 0; i < 4; i++) { + cfg->yieldCor[i] = obj[F("yield")][i]; + cfg->chMaxPwr[i] = obj[F("pwr")][i]; + snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); } } + } - settings_t mCfg; + settings_t mCfg; }; #endif /*__SETTINGS_H__*/ diff --git a/src/platformio.ini b/src/platformio.ini index caea7b42..08460930 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -41,6 +41,7 @@ lib_deps = bblanchon/ArduinoJson https://github.com/JChristensen/Timezone olikraus/U8g2 + zinggjm/GxEPD2@^1.5.0 ;esp8266/DNSServer ;esp8266/EEPROM ;esp8266/ESP8266WiFi diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h new file mode 100644 index 00000000..8509dacd --- /dev/null +++ b/src/plugins/Display/Display.h @@ -0,0 +1,125 @@ +#ifndef __DISPLAY__ +#define __DISPLAY__ + +#include +#include + +#include "../../hm/hmSystem.h" +#include "../../utils/helper.h" +#include "Display_Mono.h" +#include "Display_ePaper.h" +#include "imagedata.h" + +template +class Display { + public: + Display() {} + + void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, uint8_t disp_reset, const char *version) { + mCfg = cfg; + mSys = sys; + mUtcTs = utcTs; + mNewPayload = false; + mLoopCnt = 0; + mVersion = version; + + if (mCfg->type == 0) { + return; + } else if (1 < mCfg->type < 11) { + switch (mCfg->rot) { + case 0: + DisplayMono.disp_rotation = U8G2_R0; + break; + case 1: + DisplayMono.disp_rotation = U8G2_R1; + break; + case 2: + DisplayMono.disp_rotation = U8G2_R2; + break; + case 3: + DisplayMono.disp_rotation = U8G2_R3; + break; + } + + DisplayMono.enablePowerSafe = mCfg->pwrSaveAtIvOffline; + DisplayMono.enableScreensaver = mCfg->pxShift; + DisplayMono.contrast = mCfg->contrast; + + DisplayMono.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mVersion); + } else if (mCfg->type > 10) { + DisplayEPaper.displayRotation = mCfg->rot; + counterEPaper = 0; + + DisplayEPaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mVersion); + } + } + + void payloadEventListener(uint8_t cmd) { + mNewPayload = true; + } + + void tickerSecond() { + if (mNewPayload || ((++mLoopCnt % 10) == 0)) { + mNewPayload = false; + mLoopCnt = 0; + DataScreen(); + } + } + + private: + void DataScreen() { + if (mCfg->type == 0) + return; + if (*mUtcTs == 0) + return; + + if ((millis() - _lastDisplayUpdate) > mCfg->period) { + float totalPower = 0; + float totalYieldDay = 0; + float totalYieldTotal = 0; + + uint8_t isprod = 0; + + Inverter<> *iv; + record_t<> *rec; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + + if (iv->isProducing(*mUtcTs)) + uint8_t isprod = 0; + + totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); + } + + if (1 < mCfg->type < 11) { + DisplayMono.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + } else if (mCfg->type > 10) { + DisplayEPaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + counterEPaper++; + } + _lastDisplayUpdate = millis(); + } + + if (counterEPaper > 480) { + DisplayEPaper.fullRefresh(); + counterEPaper = 0; + } + } + + // private member variables + bool mNewPayload; + uint8_t mLoopCnt; + uint32_t *mUtcTs; + const char *mVersion; + display_t *mCfg; + HMSYSTEM *mSys; + uint16_t counterEPaper; + uint32_t _lastDisplayUpdate = 0; +}; + +#endif /*__DISPLAY__*/ diff --git a/src/plugins/Display/Display_Mono.cpp b/src/plugins/Display/Display_Mono.cpp new file mode 100644 index 00000000..d6caa1a0 --- /dev/null +++ b/src/plugins/Display/Display_Mono.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "Display_Mono.h" + +#include + +#include + +#include "WiFi.h" +#include "imagedata.h" + +#ifdef U8X8_HAVE_HW_SPI +#include +#endif +#ifdef U8X8_HAVE_HW_I2C +#include +#endif + +std::map> mono_types = { + {1, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs, uint8_t dc) { return new U8G2_PCD8544_84X48_F_4W_SW_SPI(U8G2_R2, clock, data, cs, dc, reset); }}, + {2, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs, uint8_t dc) { return new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); }}, + {3, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs, uint8_t dc) { return new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); }}, +}; + +DisplayMonoClass::DisplayMonoClass() { +} + +DisplayMonoClass::~DisplayMonoClass() { + delete _display; +} + +void DisplayMonoClass::calcLineHeights() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (_display->getMaxCharHeight()); + mLineOffsets[i] = yOff; + } +} + +inline void DisplayMonoClass::setFont(uint8_t line) { + switch (line) { + case 0: + _display->setFont((_mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr); + break; + case 3: + _display->setFont(u8g2_font_5x8_tr); + break; + default: + _display->setFont((_mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); + break; + } +} + +void DisplayMonoClass::printText(const char* text, uint8_t line, uint8_t dispX = 5) { + if (!_mIsLarge) { + dispX = (line == 0) ? 5 : 0; + } else { + dispX = (line == 0) ? 20 : 5; + } + setFont(line); + + dispX += enableScreensaver ? (_mExtra % 7) : 0; + _display->drawStr(dispX, mLineOffsets[line], text); +} + +void DisplayMonoClass::init(uint8_t _type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, const char* version) { + if (0 < _type < 4) { + auto constructor = mono_types[_type]; + _display = constructor(_RST, _SCK, _MOSI, _CS, _DC); + _display->begin(); + _display->setDisplayRotation(disp_rotation); + + _mIsLarge = (_display->getWidth() > 100); + calcLineHeights(); + + _display->clearBuffer(); + if (contrast < 255) { + _display->setContrast(contrast); + } + printText("AHOY!", 0, 35); + printText("ahoydtu.de", 2, 20); + printText(version, 3, 46); + _display->sendBuffer(); + } +} + +void DisplayMonoClass::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + _display->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (contrast < 255) { + _display->setContrast(contrast); + } + + //=====> Actual Production ========== + if ((totalPower > 0) && (isprod > 0)) { + _display->setPowerSave(false); + if (totalPower > 999) { + snprintf(_fmtText, sizeof(_fmtText), "%2.2f kW", (totalPower / 1000)); + } else { + snprintf(_fmtText, sizeof(_fmtText), "%3.0f W", totalPower); + } + printText(_fmtText, 0); + _previousMillis = millis(); + } + //<======================= + + //=====> Offline =========== + else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (millis() - _previousMillis >= (_mTimeout * 2)) { + _display->setPowerSave(enablePowerSafe); + } + } + //<======================= + + //=====> Today & Total Production ======= + snprintf(_fmtText, sizeof(_fmtText), "today: %4.0f Wh", totalYieldDay); + printText(_fmtText, 1); + + snprintf(_fmtText, sizeof(_fmtText), "total: %.1f kWh", totalYieldTotal); + printText(_fmtText, 2); + //<======================= + + //=====> IP or Date-Time ======== + if (!(_mExtra % 10) && WiFi.localIP()) { + printText(WiFi.localIP().toString().c_str(), 3); + } else if (!(_mExtra % 5)) { + snprintf(_fmtText, sizeof(_fmtText), "#%d Inverter online", isprod); + printText(_fmtText, 3); + } else { + time_t now = time(nullptr); + strftime(_fmtText, sizeof(_fmtText), "%d.%m.%Y %H:%M", localtime(&now)); + printText(_fmtText, 3); + } + + _display->sendBuffer(); + + _dispY = 0; + _mExtra++; +} + +DisplayMonoClass DisplayMono; \ No newline at end of file diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h new file mode 100644 index 00000000..81375605 --- /dev/null +++ b/src/plugins/Display/Display_Mono.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#define DISP_DEFAULT_TIMEOUT 60000 // in milliseconds + +class DisplayMonoClass { + public: + DisplayMonoClass(); + ~DisplayMonoClass(); + + void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, const char* version); + + void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + + bool enablePowerSafe = true; + bool enableScreensaver = true; + const u8g2_cb_t* disp_rotation = U8G2_R2; + uint8_t contrast = 60; + + private: + void calcLineHeights(); + void setFont(uint8_t line); + void printText(const char* text, uint8_t line, uint8_t dispX); + + U8G2* _display; + + bool _mIsLarge = false; + uint8_t mLoopCnt; + uint32_t* mUtcTs; + uint8_t mLineOffsets[5]; + + uint16_t _dispY = 0; + uint32_t _previousMillis = 0; + + uint8_t _mExtra; + uint16_t _mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + char _fmtText[32]; +}; + +extern DisplayMonoClass DisplayMono; \ No newline at end of file diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp new file mode 100644 index 00000000..01b641ba --- /dev/null +++ b/src/plugins/Display/Display_ePaper.cpp @@ -0,0 +1,192 @@ +#include "Display_ePaper.h" + +#include "WiFi.h" +#include "imagedata.h" + +static const uint32_t spiClk = 4000000; // 4 MHz + +#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) +SPIClass hspi(HSPI); +#endif + +std::map> _ePaperTypes = { + // DEPG0150BN 200x200, SSD1681, TTGO T5 V2.4.1 + {11, [](uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY) { return new GxEPD2_BW(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); }}, + // GDEW027C44 2.7 " b/w/r 176x264, IL91874 + //{DisplayType_t::ePaper270, [](uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY) + // F { return new GxEPD2_3C(GxEPD2_270c(_CS, _DC, _RST, _BUSY)); }}, +}; + +DisplayEPaperClass::DisplayEPaperClass() { +} + +DisplayEPaperClass::~DisplayEPaperClass() { + delete _display; +} +//*************************************************************************** +void DisplayEPaperClass::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, const char *version) { + if (type > 3) { + Serial.begin(115200); + auto constructor = _ePaperTypes[type]; + _display = constructor(_CS, _DC, _RST, _BUSY); + hspi.begin(_SCK, _BUSY, _MOSI, _CS); + +#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) + _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); +#endif + _display->init(115200, true, 2, false); + _display->setRotation(displayRotation); + _display->setFullWindow(); + + // Logo + _display->fillScreen(GxEPD_BLACK); + _display->drawBitmap(0, 0, AhoyLogo, 200, 200, GxEPD_WHITE); + //_display->drawBitmap(0, 0, OpenDTULogo, 200, 200, GxEPD_WHITE); + while (_display->nextPage()) + ; + + // clean the screen + delay(2000); + _display->fillScreen(GxEPD_WHITE); + while (_display->nextPage()) + ; + + headlineIP(); + + // call the PowerPage to change the PV Power Values + actualPowerPaged(0, 0, 0, 0); + } +} +//*************************************************************************** +void DisplayEPaperClass::fullRefresh() { + // screen complete black + _display->fillScreen(GxEPD_BLACK); + while (_display->nextPage()) + ; + delay(2000); + // screen complete white + _display->fillScreen(GxEPD_WHITE); + while (_display->nextPage()) + ; +} +//*************************************************************************** +void DisplayEPaperClass::headlineIP() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, 0, _display->width(), headfootline); + _display->fillScreen(GxEPD_BLACK); + + do { + if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { + snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str()); + } else { + snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected"); + } + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (headfootline - 2)); + _display->println(_fmtText); + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaperClass::lastUpdatePaged() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, _display->height() - headfootline, _display->width(), headfootline); + _display->fillScreen(GxEPD_BLACK); + do { + time_t now = time(nullptr); + strftime(_fmtText, sizeof(_fmtText), "%d.%m.%Y %H:%M", localtime(&now)); + + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (_display->height() - 3)); + _display->println(_fmtText); + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaperClass::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) { + int16_t tbx, tby; + uint16_t tbw, tbh, x, y; + + _display->setFont(&FreeSans24pt7b); + _display->setTextColor(GxEPD_BLACK); + + _display->setPartialWindow(0, headfootline, _display->width(), _display->height() - (headfootline * 2)); + _display->fillScreen(GxEPD_WHITE); + do { + if (_totalPower > 9999) { + snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000)); + _changed = true; + } else if ((_totalPower > 0) && (_totalPower <= 9999)) { + snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower); + _changed = true; + } else { + snprintf(_fmtText, sizeof(_fmtText), "offline"); + } + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, headfootline + tbh + 10); + _display->print(_fmtText); + + _display->setFont(&FreeSans12pt7b); + y = _display->height() / 2; + _display->setCursor(0, y); + _display->print("today:"); + snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 33, y); + _display->println("Wh"); + + y = y + tbh + 7; + _display->setCursor(0, y); + _display->print("total:"); + snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 45, y); + _display->println("kWh"); + + _display->setCursor(0, _display->height() - (headfootline + 10)); + snprintf(_fmtText, sizeof(_fmtText), "#%d Inverter online", _isprod); + _display->println(_fmtText); + + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaperClass::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + // check if the IP has changed + if (_settedIP != WiFi.localIP().toString().c_str()) { + // save the new IP and call the Headline Funktion to adapt the Headline + _settedIP = WiFi.localIP().toString().c_str(); + headlineIP(); + } + + // call the PowerPage to change the PV Power Values + actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod); + + // if there was an change and the Inverter is producing set a new Timestam in the footline + if ((isprod > 0) && (_changed)) { + _changed = false; + lastUpdatePaged(); + } + + _display->powerOff(); +} +//*************************************************************************** +DisplayEPaperClass DisplayEPaper; \ No newline at end of file diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h new file mode 100644 index 00000000..ed8fb025 --- /dev/null +++ b/src/plugins/Display/Display_ePaper.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +// uncomment next line to use HSPI for EPD (and VSPI for SD), e.g. with Waveshare ESP32 Driver Board +#define USE_HSPI_FOR_EPD + +/// uncomment next line to use class GFX of library GFX_Root instead of Adafruit_GFX, to use less code and ram +// #include +// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code +// enable GxEPD2_GFX base class +#define ENABLE_GxEPD2_GFX 1 + +#include +#include +#include + +#include +// FreeFonts from Adafruit_GFX +#include +#include +#include +#include + +#include "imagedata.h" + +// GDEW027C44 2.7 " b/w/r 176x264, IL91874 +// GDEH0154D67 1.54" b/w 200x200 + +class DisplayEPaperClass { + public: + DisplayEPaperClass(); + ~DisplayEPaperClass(); + void fullRefresh(); + void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, const char* version); + void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + + uint8_t displayRotation = 2; + + private: + void headlineIP(); + void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); + void lastUpdatePaged(); + + bool _changed = false; + char _fmtText[35]; + const char* _settedIP; + uint8_t headfootline = 16; + GxEPD2_GFX* _display; +}; + +extern DisplayEPaperClass DisplayEPaper; \ No newline at end of file diff --git a/src/plugins/Display/imagedata.cpp b/src/plugins/Display/imagedata.cpp new file mode 100644 index 00000000..9cdf352c --- /dev/null +++ b/src/plugins/Display/imagedata.cpp @@ -0,0 +1,642 @@ +// GxEPD2_ESP32_ESP8266_WifiData_V1_und_V2 + +#include "imagedata.h" +#if defined(__AVR__) || defined(ARDUINO_ARCH_SAMD) +#include +#elif defined(ESP8266) || defined(ESP32) +#include +#endif + +// 'OpenDTU', 200x200px +const unsigned char OpenDTULogo[] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf8, 0x03, 0x80, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xf0, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, + 0x7f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xfe, 0x01, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x01, 0xff, 0xfe, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0x07, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, 0xff, + 0xfc, 0x7f, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0xfc, 0x0f, 0xe0, 0x0f, 0xc0, 0xfe, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, + 0xf8, 0x07, 0xc0, 0x0f, 0xc0, 0x3e, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xf0, 0x07, 0xc0, 0x0f, 0x80, 0x1e, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x01, 0xe0, 0x07, 0xc0, 0x07, 0x80, 0x0f, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xe0, 0x07, 0xc0, 0x0f, 0x80, + 0x0f, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x03, 0xe0, 0x07, 0xc0, 0x0f, 0xc0, 0x1f, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x03, 0xe0, 0x0f, 0xe0, + 0x0f, 0xc0, 0x1f, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0x03, 0xf0, 0x3f, 0xf8, 0x3f, 0xf0, 0x7f, 0x81, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x07, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0x07, 0xf0, 0x7f, 0xfc, 0xff, 0xf0, 0x7f, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0x80, 0x0f, 0xe0, 0x1f, 0xc0, 0x07, + 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x0f, 0x80, 0x07, 0xe0, 0x0f, 0xc0, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0f, 0x00, 0x0f, 0xc0, 0x0f, + 0xc0, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x0f, 0x00, 0x0f, 0xc0, 0x07, 0xc0, 0x03, 0xe0, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0x00, 0x0f, + 0xc0, 0x07, 0xe0, 0x03, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0x80, 0x0f, 0xc0, 0x0f, 0xe0, 0x03, 0xe0, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x1f, + 0x80, 0x1f, 0xe0, 0x0f, 0xf0, 0x07, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xe0, 0x3f, 0xf8, 0x3f, 0xfc, 0x1f, 0xf0, + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x03, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x1f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x3f, 0xff, 0xff, 0xff, 0x80, 0x00, + 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x7f, 0x8f, 0xff, + 0xf3, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x01, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0x07, 0xff, 0xe1, 0xfc, 0x00, 0x03, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x1f, + 0x03, 0xff, 0xc0, 0xf8, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x3f, 0xe0, 0x00, 0x00, 0x1f, 0x03, 0xff, 0xc0, 0xf0, 0x00, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x1f, 0xf0, 0x00, + 0x00, 0x1f, 0x03, 0xff, 0xc0, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x7f, 0x03, 0xff, 0xc0, 0xc0, 0x3f, + 0x80, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xf0, 0x0f, + 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0xc0, 0x7f, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xfc, 0x07, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, + 0xc0, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, + 0xfe, 0x03, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x81, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0x03, 0xff, 0xf0, 0x3f, 0xff, 0x03, + 0xff, 0xc0, 0x81, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x03, 0xff, 0x03, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x81, 0xff, 0xf8, 0x1f, 0xf0, + 0x00, 0x7f, 0xff, 0xf0, 0x03, 0xff, 0xf0, 0x00, 0x7f, 0xff, 0x03, 0xff, 0x81, 0xff, 0xf0, 0x3f, + 0xff, 0x03, 0xff, 0xc0, 0x83, 0xff, 0xf8, 0x1f, 0xe0, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0xff, 0xe0, + 0x00, 0x1f, 0xff, 0x03, 0xff, 0x81, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xf8, + 0x1f, 0xe0, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x0f, 0xff, 0x03, 0xff, 0x81, 0xff, + 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe0, 0x00, 0x07, 0xff, 0x00, 0x00, + 0x3f, 0xe0, 0x00, 0x07, 0xff, 0x03, 0xff, 0x81, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, + 0xff, 0xfc, 0x1f, 0xe0, 0x00, 0x03, 0xfe, 0x00, 0x00, 0x3f, 0xe0, 0x00, 0x07, 0xff, 0x03, 0xff, + 0xc0, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe0, 0x3c, 0x01, 0xfe, + 0x01, 0xe0, 0x1f, 0xe0, 0x1c, 0x03, 0xff, 0x03, 0xff, 0xc0, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, + 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe0, 0x7f, 0x01, 0xfc, 0x07, 0xf8, 0x1f, 0xe0, 0x7f, 0x03, 0xff, + 0x03, 0xff, 0xc0, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe0, 0x7f, + 0x80, 0xfc, 0x0f, 0xf8, 0x1f, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xff, 0xc0, 0xff, 0xf0, 0x3f, 0xff, + 0x03, 0xff, 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe0, 0x7f, 0xc0, 0xfc, 0x0f, 0xe0, 0x1f, 0xe0, 0x7f, + 0x83, 0xff, 0x03, 0xff, 0xc0, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xfc, 0x1f, + 0xe0, 0x7f, 0xc0, 0xfc, 0x0c, 0x00, 0x1f, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xff, 0xc0, 0xff, 0xf0, + 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe0, 0x7f, 0xc0, 0xfc, 0x00, 0x00, 0x1f, + 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xff, 0xc0, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, + 0xfc, 0x1f, 0xe0, 0x7f, 0xc0, 0xfc, 0x00, 0x00, 0x1f, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xff, 0xc1, + 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xfc, 0x1f, 0xe0, 0x7f, 0xc0, 0xfc, 0x00, + 0x00, 0x3f, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xff, 0x81, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, + 0x03, 0xff, 0xf8, 0x1f, 0xe0, 0x7f, 0xc0, 0xfc, 0x00, 0x03, 0xff, 0xe0, 0x7f, 0x83, 0xff, 0x03, + 0xff, 0x81, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x83, 0xff, 0xf8, 0x1f, 0xe0, 0x7f, 0xc0, + 0xfc, 0x00, 0x7f, 0xff, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xff, 0x81, 0xff, 0xf0, 0x3f, 0xff, 0x03, + 0xff, 0xc0, 0x81, 0xff, 0xf8, 0x1f, 0xe0, 0x7f, 0xc0, 0xfc, 0x07, 0xff, 0xff, 0xe0, 0x7f, 0x83, + 0xff, 0x03, 0xff, 0x81, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0x81, 0xff, 0xf8, 0x3f, 0xe0, + 0x7f, 0xc0, 0xfc, 0x0f, 0xff, 0xff, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xff, 0x03, 0xff, 0xf0, 0x3f, + 0xff, 0x03, 0xff, 0xc0, 0x81, 0xff, 0xf0, 0x3f, 0xe0, 0x7f, 0xc0, 0xfc, 0x0f, 0xff, 0xff, 0xe0, + 0x7f, 0x83, 0xff, 0x03, 0xff, 0x03, 0xff, 0xf0, 0x3f, 0xff, 0x03, 0xff, 0xc0, 0xc0, 0xff, 0xf0, + 0x3f, 0xe0, 0x7f, 0xc0, 0xfc, 0x0f, 0xff, 0xff, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xfe, 0x07, 0xff, + 0xf0, 0x3f, 0xff, 0x03, 0xff, 0x80, 0xc0, 0x7f, 0xe0, 0x3f, 0xe0, 0x7f, 0x81, 0xfc, 0x0f, 0xff, + 0xff, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xfc, 0x07, 0xff, 0xf0, 0x3f, 0xff, 0x01, 0xff, 0x81, 0xc0, + 0x3f, 0x80, 0x7f, 0xe0, 0x7e, 0x01, 0xfe, 0x07, 0xf0, 0x7f, 0xe0, 0x7f, 0x83, 0xff, 0x03, 0xe0, + 0x0f, 0xff, 0xf0, 0x3f, 0xff, 0x80, 0xfe, 0x01, 0xe0, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x03, 0xfe, + 0x00, 0x00, 0x3f, 0xe0, 0x7f, 0x83, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x3f, 0xff, 0x80, 0x00, + 0x03, 0xf0, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x03, 0xff, 0x00, 0x00, 0x3f, 0xe0, 0x7f, 0x83, 0xff, + 0x00, 0x00, 0x1f, 0xff, 0xf0, 0x3f, 0xff, 0xc0, 0x00, 0x03, 0xf8, 0x00, 0x01, 0xff, 0xe0, 0x00, + 0x07, 0xff, 0x00, 0x00, 0x7f, 0xe0, 0x7f, 0x83, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x3f, 0xff, + 0xe0, 0x00, 0x07, 0xfc, 0x00, 0x03, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0x80, 0x00, 0x7f, 0xe0, 0x7f, + 0x83, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xf0, 0x3f, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0x00, 0x0f, 0xff, + 0xe0, 0x00, 0x3f, 0xff, 0xe0, 0x01, 0xff, 0xf0, 0xff, 0x83, 0xff, 0x00, 0x03, 0xff, 0xff, 0xf8, + 0x7f, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xc0, 0x3f, 0xff, 0xe0, 0x00, 0xff, 0xff, 0xf8, 0x07, 0xff, + 0xf9, 0xff, 0xe7, 0xff, 0xc0, 0x1f, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +// 'Logo', 200x200px +const unsigned char AhoyLogo[] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0x0b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x06, + 0x0f, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x7e, 0x0f, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x03, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x19, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfe, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x70, 0x7f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe0, 0x3f, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x0f, 0xe0, 0x3f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xe0, 0x1f, 0x83, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x1f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, + 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0xff, 0xc1, 0x07, 0x80, 0x07, 0xfe, 0xff, 0xff, 0xfc, 0x07, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xe1, 0x07, 0xc0, 0x01, 0xe0, 0x0f, + 0xff, 0xfc, 0x0f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xe1, 0x83, 0xc0, 0x01, 0xc0, 0x07, 0xff, 0xf8, 0x0f, 0xfc, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe1, 0x83, 0xc0, 0x00, + 0xc0, 0x07, 0x8f, 0xf8, 0x1f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe0, 0x01, 0xc0, 0x00, 0x81, 0x83, 0x07, 0xf0, 0x3f, 0xf9, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xe0, 0x01, + 0xe0, 0xe0, 0x87, 0xe3, 0x0f, 0xf0, 0x3f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xe0, 0x00, 0xe0, 0xe0, 0x87, 0xe1, 0x0c, 0x60, 0x7f, + 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, + 0xe0, 0x00, 0xe1, 0xf0, 0x87, 0xe1, 0x08, 0x60, 0x7f, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xe0, 0xe0, 0xe0, 0xe0, 0x87, 0xc2, 0x00, + 0x40, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x8f, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, 0x82, 0x00, 0xc0, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, + 0x06, 0x01, 0x81, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xcf, 0xe0, 0xe0, 0x20, 0xe0, 0xe0, 0x0c, 0x03, 0x81, 0xff, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x30, + 0xe1, 0xf8, 0x18, 0x07, 0xe1, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x7f, 0xff, 0xff, 0xf0, 0x1f, 0xf3, 0xfe, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xc0, + 0xfb, 0xff, 0xff, 0xff, 0xe0, 0x3e, 0x1f, 0xfc, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xfc, 0x0f, + 0xf8, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0x33, 0xef, 0xff, 0xff, 0xff, 0xff, 0x81, 0xfc, 0x0f, 0xf1, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xf1, 0xff, 0xff, 0xa0, 0x00, 0x7f, 0xe3, + 0xfc, 0x0f, 0xf3, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf1, 0xf9, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x0f, 0xe7, 0xff, 0xe0, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf9, 0xff, 0x80, 0x3f, 0xff, + 0xe0, 0x0f, 0xfe, 0x1f, 0xc7, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xcf, 0xf8, 0xf0, 0x07, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0x8f, 0xff, 0xfc, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x70, 0x3f, + 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0x1f, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, 0x63, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0x3f, + 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfe, + 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7e, 0x3f, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x0c, 0x7f, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x7f, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x87, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x01, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x55, 0x00, 0x3f, 0xf8, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x01, 0xff, 0xff, 0xf8, 0x0f, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x9f, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0x03, + 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe3, 0xf1, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe7, 0xf9, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x7e, 0x06, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xcf, + 0xf8, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0x03, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x1f, 0x23, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xf3, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xf1, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xf8, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x0f, 0xff, 0x8f, 0xf1, 0xff, 0xff, 0xff, 0xfe, 0xf5, 0x90, 0x07, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x03, 0xff, + 0x1f, 0xe3, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0xfe, 0x31, 0xfe, 0x7f, 0xe7, 0xff, 0x80, 0x00, 0x40, 0x00, + 0x07, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0x3c, + 0xf9, 0xfc, 0xff, 0xe7, 0xfe, 0x3f, 0xc9, 0xff, 0xf1, 0x1f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x3c, 0xf9, 0xf9, 0xff, 0xc7, 0xfc, 0xff, 0x90, + 0x7f, 0xf3, 0x03, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, + 0x3f, 0x39, 0xfd, 0xf3, 0xff, 0xcf, 0xfc, 0xff, 0x90, 0x3f, 0xf3, 0x83, 0xf8, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x39, 0xf9, 0xc7, 0xff, 0xcf, 0xfc, + 0xff, 0x32, 0x7f, 0xe4, 0x77, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0xff, 0xff, 0x7f, 0x33, 0xf9, 0x8f, 0xff, 0xcf, 0xf9, 0xff, 0x00, 0x7f, 0xe0, 0x67, 0xfc, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xb3, 0xf3, 0xbf, 0xff, + 0xcf, 0xf9, 0xff, 0x00, 0xff, 0xfe, 0x47, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xf7, 0xf3, 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe0, 0xff, 0xfc, 0x0f, + 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xe7, 0xe7, + 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe1, 0xff, 0xfc, 0x1f, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xe7, 0xef, 0xff, 0xc7, 0xf9, 0xff, 0xc3, 0xff, + 0xfc, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, + 0xef, 0xef, 0xc0, 0xff, 0xe7, 0xf9, 0xff, 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xcf, 0xf0, 0x01, 0xe7, 0xf1, 0xff, + 0x87, 0xff, 0xf8, 0x7f, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, + 0xff, 0xbf, 0xcf, 0xe7, 0xff, 0xc1, 0xe3, 0xe1, 0xff, 0x8f, 0xff, 0xf0, 0xff, 0xff, 0x9f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x9f, 0xef, 0xe7, 0xff, 0xff, 0xf3, + 0xc1, 0xff, 0x96, 0xaf, 0xf9, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf9, 0xff, 0xff, 0x9f, 0xe7, 0xe3, 0xff, 0xff, 0xf1, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, + 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xcf, 0xe7, 0xf3, 0xff, + 0xff, 0xf8, 0xc0, 0x00, 0x4a, 0x90, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf9, 0xff, 0xff, 0xef, 0xf3, 0xf3, 0x9f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe7, 0xf1, + 0xe7, 0xc7, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf3, 0xf0, 0x07, 0xe3, 0xff, 0xff, 0x81, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, + 0xf8, 0x07, 0x1f, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xfc, 0x1f, 0x9f, 0xf8, 0xff, 0xff, 0xc3, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, + 0xff, 0xff, 0xf8, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf9, 0xff, 0x9f, 0xfe, 0x3f, + 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfd, 0xff, 0xff, 0xf1, 0xff, 0x9f, 0xff, 0x9f, 0xff, 0xf3, 0xff, 0x3f, 0x3f, 0xff, 0xff, + 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe1, 0xff, 0xcf, + 0xff, 0xc7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xe1, 0xff, 0x8f, 0xff, 0xe7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, + 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xc1, + 0xff, 0xcf, 0xff, 0xf3, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x81, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xf3, 0xff, + 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, + 0xff, 0x91, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0x11, 0xff, 0x9f, 0xff, 0xff, 0xff, + 0xf3, 0xff, 0x1f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x7f, 0xff, 0x21, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xbf, 0x9f, 0xff, 0xff, 0xfe, + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x20, 0xff, 0x9f, 0xff, + 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x60, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0x64, 0x3f, + 0x1f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0xe7, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, + 0xff, 0x3f, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, + 0xe7, 0x80, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xf8, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, + 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x9f, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xe7, 0xff, 0xfe, 0x7f, 0xff, 0xc3, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf3, 0xf3, 0xff, 0xfc, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xff, 0xf8, 0xff, 0xff, + 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xf9, 0xe7, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf3, 0xf9, 0xff, 0xe1, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe7, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0x3f, 0x07, + 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf3, 0xe7, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x00, 0x1f, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, + 0xe0, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, + 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf7, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x83, 0xe7, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x13, 0xe7, 0xff, 0xfc, 0x03, + 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfc, 0x31, 0xe7, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfe, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x39, 0xe3, 0xff, + 0xfc, 0x00, 0x1f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x31, 0xf3, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xc7, 0xff, 0xff, + 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x19, + 0xf3, 0xff, 0xfc, 0x00, 0x07, 0xff, 0x87, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xf3, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x83, 0xf3, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xf3, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x07, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xe3, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xfe, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe1, 0xff, 0xfe, + 0x01, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xf0, 0x00, 0x3f, 0x80, 0x07, 0xff, 0xff, 0xf0, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x4c, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0c, 0xff, 0xf0, 0x00, 0x00, 0x0b, 0x87, 0xff, + 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x0e, 0x7f, 0xf8, 0x00, 0x3f, 0xff, 0xc7, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x86, 0x7f, 0xfe, 0x00, 0xff, 0xff, + 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x80, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xf3, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xf3, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xf3, 0xc0, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xe3, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x03, 0xff, + 0xfe, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x17, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/src/plugins/Display/imagedata.h b/src/plugins/Display/imagedata.h new file mode 100644 index 00000000..ebdb918e --- /dev/null +++ b/src/plugins/Display/imagedata.h @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +extern const unsigned char AhoyLogo[]; +extern const unsigned char OpenDTULogo[]; diff --git a/src/plugins/MonochromeDisplay/MonochromeDisplay.h b/src/plugins/MonochromeDisplay/MonochromeDisplay.h deleted file mode 100644 index d7880432..00000000 --- a/src/plugins/MonochromeDisplay/MonochromeDisplay.h +++ /dev/null @@ -1,217 +0,0 @@ -#ifndef __MONOCHROME_DISPLAY__ -#define __MONOCHROME_DISPLAY__ - -#include -#include - -#include "../../utils/helper.h" -#include "../../hm/hmSystem.h" - -#define DISP_DEFAULT_TIMEOUT 60 // in seconds - - -static uint8_t bmp_logo[] PROGMEM = { - B00000000, B00000000, // ................ - B11101100, B00110111, // ..##.######.##.. - B11101100, B00110111, // ..##.######.##.. - B11100000, B00000111, // .....######..... - B11010000, B00001011, // ....#.####.#.... - B10011000, B00011001, // ...##..##..##... - B10000000, B00000001, // .......##....... - B00000000, B00000000, // ................ - B01111000, B00011110, // ...####..####... - B11111100, B00111111, // ..############.. - B01111100, B00111110, // ..#####..#####.. - B00000000, B00000000, // ................ - B11111100, B00111111, // ..############.. - B11111110, B01111111, // .##############. - B01111110, B01111110, // .######..######. - B00000000, B00000000 // ................ -}; - - -static uint8_t bmp_arrow[] PROGMEM = { - B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111, - B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111, - B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000 -}; - - -template -class MonochromeDisplay { - public: - MonochromeDisplay() {} - - void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, uint8_t disp_reset, const char *version) { - mCfg = cfg; - mSys = sys; - mUtcTs = utcTs; - mNewPayload = false; - mLoopCnt = 0; - mTimeout = DISP_DEFAULT_TIMEOUT; // power off timeout (after inverters go offline) - - u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot180) ? U8G2_R2 : U8G2_R0); - - if(mCfg->type) { - switch(mCfg->type) { - case 1: - mDisplay = new U8G2_PCD8544_84X48_F_4W_HW_SPI(rot, mCfg->pin0, mCfg->pin1, disp_reset); - break; - case 2: - mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, disp_reset, mCfg->pin0, mCfg->pin1); - break; - case 3: - mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, disp_reset, mCfg->pin0, mCfg->pin1); - break; - } - mDisplay->begin(); - - mIsLarge = ((mDisplay->getWidth() > 120) && (mDisplay->getHeight() > 60)); - calcLineHeights(); - - mDisplay->clearBuffer(); - mDisplay->setContrast(mCfg->contrast); - printText("Ahoy!", 0, 35); - printText("ahoydtu.de", 2, 20); - printText(version, 3, 46); - mDisplay->sendBuffer(); - } - } - - void payloadEventListener(uint8_t cmd) { - mNewPayload = true; - } - - void tickerSecond() { - if(mCfg->pwrSaveAtIvOffline) { - if(mTimeout != 0) - mTimeout--; - } - if(mNewPayload || ((++mLoopCnt % 10) == 0)) { - mNewPayload = false; - mLoopCnt = 0; - DataScreen(); - } - } - - private: - void DataScreen() { - if (mCfg->type == 0) - return; - if(*mUtcTs == 0) - return; - - float totalPower = 0; - float totalYieldDay = 0; - float totalYieldTotal = 0; - - bool isprod = false; - - Inverter<> *iv; - record_t<> *rec; - for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { - iv = mSys->getInverterByPos(i); - rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (iv == NULL) - continue; - - if (iv->isProducing(*mUtcTs)) - isprod = true; - - totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); - totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); - totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); - } - - mDisplay->clearBuffer(); - - // Logos - // pxMovement +x (0 - 6 px) - uint8_t ex = (_mExtra % 7); - if (isprod) { - mDisplay->drawXBMP(5 + ex, 1, 8, 17, bmp_arrow); - if (mCfg->logoEn) - mDisplay->drawXBMP(mDisplay->getWidth() - 24 + ex, 2, 16, 16, bmp_logo); - } - - if ((totalPower > 0) && isprod) { - mTimeout = DISP_DEFAULT_TIMEOUT; - mDisplay->setPowerSave(false); - mDisplay->setContrast(mCfg->contrast); - if (totalPower > 999) - snprintf(_fmtText, sizeof(_fmtText), "%2.1f kW", (totalPower / 1000)); - else - snprintf(_fmtText, sizeof(_fmtText), "%3.0f W", totalPower); - printText(_fmtText, 0, 20); - } else { - printText("offline", 0, 25); - if(mCfg->pwrSaveAtIvOffline) { - if(mTimeout == 0) - mDisplay->setPowerSave(true); - } - } - - snprintf(_fmtText, sizeof(_fmtText), "today: %4.0f Wh", totalYieldDay); - printText(_fmtText, 1); - - snprintf(_fmtText, sizeof(_fmtText), "total: %.1f kWh", totalYieldTotal); - printText(_fmtText, 2); - - IPAddress ip = WiFi.localIP(); - if (!(_mExtra % 10) && (ip)) { - printText(ip.toString().c_str(), 3); - } else { - // Get current time - if(mIsLarge) - printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); - else - printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); - } - mDisplay->sendBuffer(); - - _mExtra++; - } - - void calcLineHeights() { - uint8_t yOff = 0; - for(uint8_t i = 0; i < 4; i++) { - setFont(i); - yOff += (mDisplay->getMaxCharHeight() + 1); - mLineOffsets[i] = yOff; - } - } - - inline void setFont(uint8_t line) { - switch (line) { - case 0: mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_lubBI14_tr); break; - case 3: mDisplay->setFont(u8g2_font_5x8_tr); break; - default: mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); break; - } - } - - void printText(const char* text, uint8_t line, uint8_t dispX = 5) { - if(!mIsLarge) - dispX = (line == 0) ? 10 : 5; - setFont(line); - if(mCfg->pxShift) - dispX += (_mExtra % 7); // add pixel movement - mDisplay->drawStr(dispX, mLineOffsets[line], text); - } - - // private member variables - U8G2* mDisplay; - - uint8_t _mExtra; - uint16_t mTimeout; // interval at which to power save (milliseconds) - char _fmtText[32]; - - bool mNewPayload; - bool mIsLarge; - uint8_t mLoopCnt; - uint32_t *mUtcTs; - uint8_t mLineOffsets[5]; - display_t *mCfg; - HMSYSTEM *mSys; -}; - -#endif /*__MONOCHROME_DISPLAY__*/ diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 0f18728d..616244fa 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -8,646 +8,669 @@ #include "../utils/dbg.h" #ifdef ESP32 - #include "AsyncTCP.h" +#include "AsyncTCP.h" #else - #include "ESPAsyncTCP.h" +#include "ESPAsyncTCP.h" #endif -#include "ESPAsyncWebServer.h" -#include "AsyncJson.h" +#include "../appInterface.h" #include "../hm/hmSystem.h" #include "../utils/helper.h" - -#include "../appInterface.h" +#include "AsyncJson.h" +#include "ESPAsyncWebServer.h" #if defined(F) && defined(ESP32) - #undef F - #define F(sl) (sl) +#undef F +#define F(sl) (sl) #endif const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR}; -template +template class RestApi { - public: - RestApi() { - mTimezoneOffset = 0; - mHeapFree = 0; - mHeapFreeBlk = 0; - mHeapFrag = 0; - nr = 0; - } - - void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { - mApp = app; - mSrv = srv; - mSys = sys; - mConfig = config; - mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); - mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( - std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); - - mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); - } - - uint32_t getTimezoneOffset(void) { - return mTimezoneOffset; - } - - void ctrlRequest(JsonObject obj) { - /*char out[128]; - serializeJson(obj, out, 128); - DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ - DynamicJsonDocument json(128); - JsonObject dummy = json.as(); - if(obj[F("path")] == "ctrl") - setCtrl(obj, dummy); - else if(obj[F("path")] == "setup") - setSetup(obj, dummy); - } - - private: - void onApi(AsyncWebServerRequest *request) { - mHeapFree = ESP.getFreeHeap(); - #ifndef ESP32 - mHeapFreeBlk = ESP.getMaxFreeBlockSize(); - mHeapFrag = ESP.getHeapFragmentation(); - #endif - - AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); - JsonObject root = response->getRoot(); + public: + RestApi() { + mTimezoneOffset = 0; + mHeapFree = 0; + mHeapFreeBlk = 0; + mHeapFrag = 0; + nr = 0; + } + + void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { + mApp = app; + mSrv = srv; + mSys = sys; + mConfig = config; + mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); + mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody(std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); + + mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); + } + + uint32_t getTimezoneOffset(void) { + return mTimezoneOffset; + } + + void ctrlRequest(JsonObject obj) { + /*char out[128]; + serializeJson(obj, out, 128); + DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ + DynamicJsonDocument json(128); + JsonObject dummy = json.as(); + if (obj[F("path")] == "ctrl") + setCtrl(obj, dummy); + else if (obj[F("path")] == "setup") + setSetup(obj, dummy); + } + + private: + void onApi(AsyncWebServerRequest *request) { + mHeapFree = ESP.getFreeHeap(); +#ifndef ESP32 + mHeapFreeBlk = ESP.getMaxFreeBlockSize(); + mHeapFrag = ESP.getHeapFragmentation(); +#endif + AsyncJsonResponse *response = new AsyncJsonResponse(false, 6000); + JsonObject root = response->getRoot(); + + String path = request->url().substring(5); + if (path == "html/system") + getHtmlSystem(root); + else if (path == "html/logout") + getHtmlLogout(root); + else if (path == "html/save") + getHtmlSave(root); + else if (path == "system") + getSysInfo(root); + else if (path == "generic") + getGeneric(root); + else if (path == "reboot") + getReboot(root); + else if (path == "statistics") + getStatistics(root); + else if (path == "inverter/list") + getInverterList(root); + else if (path == "index") + getIndex(root); + else if (path == "setup") + getSetup(root); + else if (path == "setup/networks") + getNetworks(root); + else if (path == "live") + getLive(root); + else if (path == "record/info") + getRecord(root, InverterDevInform_All); + else if (path == "record/alarm") + getRecord(root, AlarmData); + else if (path == "record/config") + getRecord(root, SystemConfigPara); + else if (path == "record/live") + getRecord(root, RealTimeRunData_Debug); + else { + if (path.substring(0, 12) == "inverter/id/") + getInverter(root, request->url().substring(17).toInt()); + else + getNotFound(root, F("http://") + request->host() + F("/api/")); + } + + // DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); + response->addHeader("Access-Control-Allow-Origin", "*"); + response->addHeader("Access-Control-Allow-Headers", "content-type"); + response->setLength(); + request->send(response); + } + + void onApiPost(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, "onApiPost"); + } + + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + DPRINTLN(DBG_VERBOSE, "onApiPostBody"); + DynamicJsonDocument json(200); + AsyncJsonResponse *response = new AsyncJsonResponse(false, 200); + JsonObject root = response->getRoot(); + + DeserializationError err = deserializeJson(json, (const char *)data, len); + JsonObject obj = json.as(); + root[F("success")] = (err) ? false : true; + if (!err) { String path = request->url().substring(5); - if(path == "html/system") getHtmlSystem(root); - else if(path == "html/logout") getHtmlLogout(root); - else if(path == "html/save") getHtmlSave(root); - else if(path == "system") getSysInfo(root); - else if(path == "generic") getGeneric(root); - else if(path == "reboot") getReboot(root); - else if(path == "statistics") getStatistics(root); - else if(path == "inverter/list") getInverterList(root); - else if(path == "index") getIndex(root); - else if(path == "setup") getSetup(root); - else if(path == "setup/networks") getNetworks(root); - else if(path == "live") getLive(root); - else if(path == "record/info") getRecord(root, InverterDevInform_All); - else if(path == "record/alarm") getRecord(root, AlarmData); - else if(path == "record/config") getRecord(root, SystemConfigPara); - else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); + if (path == "ctrl") + root[F("success")] = setCtrl(obj, root); + else if (path == "setup") + root[F("success")] = setSetup(obj, root); else { - if(path.substring(0, 12) == "inverter/id/") - getInverter(root, request->url().substring(17).toInt()); - else - getNotFound(root, F("http://") + request->host() + F("/api/")); + root[F("success")] = false; + root[F("error")] = "Path not found: " + path; + } + } else { + switch (err.code()) { + case DeserializationError::Ok: + break; + case DeserializationError::InvalidInput: + root[F("error")] = F("Invalid input"); + break; + case DeserializationError::NoMemory: + root[F("error")] = F("Not enough memory"); + break; + default: + root[F("error")] = F("Deserialization failed"); + break; } - - //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); - response->addHeader("Access-Control-Allow-Origin", "*"); - response->addHeader("Access-Control-Allow-Headers", "content-type"); - response->setLength(); - request->send(response); - } - - void onApiPost(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, "onApiPost"); } - void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - DPRINTLN(DBG_VERBOSE, "onApiPostBody"); - DynamicJsonDocument json(200); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); - JsonObject root = response->getRoot(); - - DeserializationError err = deserializeJson(json, (const char *)data, len); - JsonObject obj = json.as(); - root[F("success")] = (err) ? false : true; - if(!err) { - String path = request->url().substring(5); - if(path == "ctrl") - root[F("success")] = setCtrl(obj, root); - else if(path == "setup") - root[F("success")] = setSetup(obj, root); - else { - root[F("success")] = false; - root[F("error")] = "Path not found: " + path; + response->setLength(); + request->send(response); + } + + void getNotFound(JsonObject obj, String url) { + JsonObject ep = obj.createNestedObject("avail_endpoints"); + ep[F("system")] = url + F("system"); + ep[F("statistics")] = url + F("statistics"); + ep[F("inverter/list")] = url + F("inverter/list"); + ep[F("index")] = url + F("index"); + ep[F("setup")] = url + F("setup"); + ep[F("live")] = url + F("live"); + ep[F("record/info")] = url + F("record/info"); + ep[F("record/alarm")] = url + F("record/alarm"); + ep[F("record/config")] = url + F("record/config"); + ep[F("record/live")] = url + F("record/live"); + } + + void onDwnldSetup(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + + File fp = LittleFS.open("/settings.json", "r"); + if (!fp) { + DPRINTLN(DBG_ERROR, F("failed to load settings")); + response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); + } else { + String tmp = fp.readString(); + int i = 0; + // remove all passwords + while (i != -1) { + i = tmp.indexOf("\"pwd\":", i); + if (-1 != i) { + i += 7; + tmp.remove(i, tmp.indexOf("\"", i) - i); } } - else { - switch (err.code()) { - case DeserializationError::Ok: break; - case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; - case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; - default: root[F("error")] = F("Deserialization failed"); break; - } - } - - response->setLength(); - request->send(response); + response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); } - void getNotFound(JsonObject obj, String url) { - JsonObject ep = obj.createNestedObject("avail_endpoints"); - ep[F("system")] = url + F("system"); - ep[F("statistics")] = url + F("statistics"); - ep[F("inverter/list")] = url + F("inverter/list"); - ep[F("index")] = url + F("index"); - ep[F("setup")] = url + F("setup"); - ep[F("live")] = url + F("live"); - ep[F("record/info")] = url + F("record/info"); - ep[F("record/alarm")] = url + F("record/alarm"); - ep[F("record/config")] = url + F("record/config"); - ep[F("record/live")] = url + F("record/live"); - } + response->addHeader("Content-Type", "application/octet-stream"); + response->addHeader("Content-Description", "File Transfer"); + response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); + request->send(response); + fp.close(); + } - void onDwnldSetup(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response; + void getGeneric(JsonObject obj) { + obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); + obj[F("ts_uptime")] = mApp->getUptime(); + obj[F("menu_prot")] = mApp->getProtection(); + obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask); + obj[F("menu_protEn")] = (bool)(strlen(mConfig->sys.adminPwd) > 0); - File fp = LittleFS.open("/settings.json", "r"); - if(!fp) { - DPRINTLN(DBG_ERROR, F("failed to load settings")); - response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); - } - else { - String tmp = fp.readString(); - int i = 0; - // remove all passwords - while (i != -1) { - i = tmp.indexOf("\"pwd\":", i); - if(-1 != i) { - i+=7; - tmp.remove(i, tmp.indexOf("\"", i)-i); - } +#if defined(ESP32) + obj[F("esp_type")] = F("ESP32"); +#else + obj[F("esp_type")] = F("ESP8266"); +#endif + } + + void getSysInfo(JsonObject obj) { + obj[F("ssid")] = mConfig->sys.stationSsid; + obj[F("device_name")] = mConfig->sys.deviceName; + obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; + + obj[F("mac")] = WiFi.macAddress(); + obj[F("hostname")] = mConfig->sys.deviceName; + obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); + obj[F("prot_mask")] = mConfig->sys.protectionMask; + + obj[F("sdk")] = ESP.getSdkVersion(); + obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); + obj[F("heap_free")] = mHeapFree; + obj[F("sketch_total")] = ESP.getFreeSketchSpace(); + obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb + getGeneric(obj); + + getRadio(obj.createNestedObject(F("radio"))); + getStatistics(obj.createNestedObject(F("statistics"))); + +#if defined(ESP32) + obj[F("heap_total")] = ESP.getHeapSize(); + obj[F("chip_revision")] = ESP.getChipRevision(); + obj[F("chip_model")] = ESP.getChipModel(); + obj[F("chip_cores")] = ESP.getChipCores(); + // obj[F("core_version")] = F("n/a"); + // obj[F("flash_size")] = F("n/a"); + // obj[F("heap_frag")] = F("n/a"); + // obj[F("max_free_blk")] = F("n/a"); + // obj[F("reboot_reason")] = F("n/a"); +#else + // obj[F("heap_total")] = F("n/a"); + // obj[F("chip_revision")] = F("n/a"); + // obj[F("chip_model")] = F("n/a"); + // obj[F("chip_cores")] = F("n/a"); + obj[F("core_version")] = ESP.getCoreVersion(); + obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb + obj[F("heap_frag")] = mHeapFrag; + obj[F("max_free_blk")] = mHeapFreeBlk; + obj[F("reboot_reason")] = ESP.getResetReason(); +#endif + // obj[F("littlefs_total")] = LittleFS.totalBytes(); + // obj[F("littlefs_used")] = LittleFS.usedBytes(); + + uint8_t max; + mApp->getSchedulerInfo(&max); + obj[F("schMax")] = max; + } + + void getHtmlSystem(JsonObject obj) { + getSysInfo(obj.createNestedObject(F("system"))); + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("html")] = F("Factory Reset

Reboot"); + } + + void getHtmlLogout(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 3; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("succesfully logged out"); + } + + void getHtmlSave(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 2; + obj[F("refresh_url")] = "/setup"; + obj[F("html")] = F("settings succesfully save"); + } + + void getReboot(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 10; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("reboot. Autoreload after 10 seconds"); + } + + void getStatistics(JsonObject obj) { + statistics_t *stat = mApp->getStatistics(); + obj[F("rx_success")] = stat->rxSuccess; + obj[F("rx_fail")] = stat->rxFail; + obj[F("rx_fail_answer")] = stat->rxFailNoAnser; + obj[F("frame_cnt")] = stat->frmCnt; + obj[F("tx_cnt")] = mSys->Radio.mSendCnt; + obj[F("retransmits")] = mSys->Radio.mRetransmits; + } + + void getInverterList(JsonObject obj) { + JsonArray invArr = obj.createNestedArray(F("inverter")); + + Inverter<> *iv; + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i); + if (NULL != iv) { + JsonObject obj2 = invArr.createNestedObject(); + obj2[F("enabled")] = (bool)iv->config->enabled; + obj2[F("id")] = i; + obj2[F("name")] = String(iv->config->name); + obj2[F("serial")] = String(iv->config->serial.u64, HEX); + obj2[F("channels")] = iv->channels; + obj2[F("version")] = String(iv->getFwVersion()); + + for (uint8_t j = 0; j < iv->channels; j++) { + obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j]; + obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j]; + obj2[F("ch_name")][j] = iv->config->chName[j]; } - response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); } - - response->addHeader("Content-Type", "application/octet-stream"); - response->addHeader("Content-Description", "File Transfer"); - response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); - request->send(response); - fp.close(); - } - - void getGeneric(JsonObject obj) { - obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); - obj[F("ts_uptime")] = mApp->getUptime(); - obj[F("menu_prot")] = mApp->getProtection(); - obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); - obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); - - #if defined(ESP32) - obj[F("esp_type")] = F("ESP32"); - #else - obj[F("esp_type")] = F("ESP8266"); - #endif - } - - void getSysInfo(JsonObject obj) { - obj[F("ssid")] = mConfig->sys.stationSsid; - obj[F("device_name")] = mConfig->sys.deviceName; - obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; - - obj[F("mac")] = WiFi.macAddress(); - obj[F("hostname")] = mConfig->sys.deviceName; - obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); - obj[F("prot_mask")] = mConfig->sys.protectionMask; - - obj[F("sdk")] = ESP.getSdkVersion(); - obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); - obj[F("heap_free")] = mHeapFree; - obj[F("sketch_total")] = ESP.getFreeSketchSpace(); - obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb - getGeneric(obj); - - getRadio(obj.createNestedObject(F("radio"))); - getStatistics(obj.createNestedObject(F("statistics"))); - - #if defined(ESP32) - obj[F("heap_total")] = ESP.getHeapSize(); - obj[F("chip_revision")] = ESP.getChipRevision(); - obj[F("chip_model")] = ESP.getChipModel(); - obj[F("chip_cores")] = ESP.getChipCores(); - //obj[F("core_version")] = F("n/a"); - //obj[F("flash_size")] = F("n/a"); - //obj[F("heap_frag")] = F("n/a"); - //obj[F("max_free_blk")] = F("n/a"); - //obj[F("reboot_reason")] = F("n/a"); - #else - //obj[F("heap_total")] = F("n/a"); - //obj[F("chip_revision")] = F("n/a"); - //obj[F("chip_model")] = F("n/a"); - //obj[F("chip_cores")] = F("n/a"); - obj[F("core_version")] = ESP.getCoreVersion(); - obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb - obj[F("heap_frag")] = mHeapFrag; - obj[F("max_free_blk")] = mHeapFreeBlk; - obj[F("reboot_reason")] = ESP.getResetReason(); - #endif - //obj[F("littlefs_total")] = LittleFS.totalBytes(); - //obj[F("littlefs_used")] = LittleFS.usedBytes(); - - uint8_t max; - mApp->getSchedulerInfo(&max); - obj[F("schMax")] = max; - } - - void getHtmlSystem(JsonObject obj) { - getSysInfo(obj.createNestedObject(F("system"))); - getGeneric(obj.createNestedObject(F("generic"))); - obj[F("html")] = F("Factory Reset

Reboot"); - - } - - void getHtmlLogout(JsonObject obj) { - getGeneric(obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 3; - obj[F("refresh_url")] = "/"; - obj[F("html")] = F("succesfully logged out"); - } - - void getHtmlSave(JsonObject obj) { - getGeneric(obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 2; - obj[F("refresh_url")] = "/setup"; - obj[F("html")] = F("settings succesfully save"); - } - - void getReboot(JsonObject obj) { - getGeneric(obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 10; - obj[F("refresh_url")] = "/"; - obj[F("html")] = F("reboot. Autoreload after 10 seconds"); - } - - void getStatistics(JsonObject obj) { - statistics_t *stat = mApp->getStatistics(); - obj[F("rx_success")] = stat->rxSuccess; - obj[F("rx_fail")] = stat->rxFail; - obj[F("rx_fail_answer")] = stat->rxFailNoAnser; - obj[F("frame_cnt")] = stat->frmCnt; - obj[F("tx_cnt")] = mSys->Radio.mSendCnt; - obj[F("retransmits")] = mSys->Radio.mRetransmits; } + obj[F("interval")] = String(mConfig->nrf.sendInterval); + obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld); + obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; + obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; + obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; + } + + void getInverter(JsonObject obj, uint8_t id) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + obj[F("id")] = id; + obj[F("enabled")] = (bool)iv->config->enabled; + obj[F("name")] = String(iv->config->name); + obj[F("serial")] = String(iv->config->serial.u64, HEX); + obj[F("version")] = String(iv->getFwVersion()); + obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); + obj[F("ts_last_success")] = rec->ts; + + JsonArray ch = obj.createNestedArray("ch"); + + // AC + uint8_t pos; + obj[F("ch_name")][0] = "AC"; + JsonArray ch0 = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + pos = (iv->getPosByChFld(CH0, acList[fld], rec)); + ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } - void getInverterList(JsonObject obj) { - JsonArray invArr = obj.createNestedArray(F("inverter")); - - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("enabled")] = (bool)iv->config->enabled; - obj2[F("id")] = i; - obj2[F("name")] = String(iv->config->name); - obj2[F("serial")] = String(iv->config->serial.u64, HEX); - obj2[F("channels")] = iv->channels; - obj2[F("version")] = String(iv->getFwVersion()); - - for(uint8_t j = 0; j < iv->channels; j ++) { - obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j]; - obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j]; - obj2[F("ch_name")][j] = iv->config->chName[j]; - } + // DC + for (uint8_t j = 0; j < iv->channels; j++) { + obj[F("ch_name")][j + 1] = iv->config->chName[j]; + JsonArray cur = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + pos = (iv->getPosByChFld((j + 1), dcList[fld], rec)); + cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; } } - obj[F("interval")] = String(mConfig->nrf.sendInterval); - obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld); - obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; - obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; - obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; - obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; + } + } + + void getMqtt(JsonObject obj) { + obj[F("broker")] = String(mConfig->mqtt.broker); + obj[F("port")] = String(mConfig->mqtt.port); + obj[F("user")] = String(mConfig->mqtt.user); + obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); + obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("interval")] = String(mConfig->mqtt.interval); + } + + void getNtp(JsonObject obj) { + obj[F("addr")] = String(mConfig->ntp.addr); + obj[F("port")] = String(mConfig->ntp.port); + } + + void getSun(JsonObject obj) { + obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; + obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; + obj[F("disnightcom")] = mConfig->sun.disNightCom; + obj[F("offs")] = mConfig->sun.offsetSec; + } + + void getPinout(JsonObject obj) { + obj[F("cs")] = mConfig->nrf.pinCs; + obj[F("ce")] = mConfig->nrf.pinCe; + obj[F("irq")] = mConfig->nrf.pinIrq; + obj[F("led0")] = mConfig->led.led0; + obj[F("led1")] = mConfig->led.led1; + } + + void getRadio(JsonObject obj) { + obj[F("power_level")] = mConfig->nrf.amplifierPower; + obj[F("isconnected")] = mSys->Radio.isChipConnected(); + obj[F("DataRate")] = mSys->Radio.getDataRate(); + obj[F("isPVariant")] = mSys->Radio.isPVariant(); + } + + void getSerial(JsonObject obj) { + obj[F("interval")] = (uint16_t)mConfig->serial.interval; + obj[F("show_live_data")] = mConfig->serial.showIv; + obj[F("debug")] = mConfig->serial.debug; + } + + void getStaticIp(JsonObject obj) { + char buf[16]; + ah::ip2Char(mConfig->sys.ip.ip, buf); + obj[F("ip")] = String(buf); + ah::ip2Char(mConfig->sys.ip.mask, buf); + obj[F("mask")] = String(buf); + ah::ip2Char(mConfig->sys.ip.dns1, buf); + obj[F("dns1")] = String(buf); + ah::ip2Char(mConfig->sys.ip.dns2, buf); + obj[F("dns2")] = String(buf); + ah::ip2Char(mConfig->sys.ip.gateway, buf); + obj[F("gateway")] = String(buf); + } + + void getDisplay(JsonObject obj) { + obj[F("disp_type")] = (uint8_t)mConfig->plugin.display.type; + obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; + obj[F("px_shift")] = (bool)mConfig->plugin.display.pxShift; + obj[F("rotation")] = (uint8_t)mConfig->plugin.display.rot; + obj[F("period")] = (uint16_t)mConfig->plugin.display.period; + obj[F("contrast")] = (uint8_t)mConfig->plugin.display.contrast; + obj[F("data")] = mConfig->plugin.display.disp_data; + obj[F("clock")] = mConfig->plugin.display.disp_clk; + obj[F("cs")] = mConfig->plugin.display.disp_cs; + obj[F("reset")] = mConfig->plugin.display.disp_reset; + obj[F("busy")] = mConfig->plugin.display.disp_busy; + obj[F("dc")] = mConfig->plugin.display.disp_dc; + } + + void getIndex(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("ts_now")] = mApp->getTimestamp(); + obj[F("ts_sunrise")] = mApp->getSunrise(); + obj[F("ts_sunset")] = mApp->getSunset(); + obj[F("ts_offset")] = mConfig->sun.offsetSec; + obj[F("disNightComm")] = mConfig->sun.disNightCom; + + JsonArray inv = obj.createNestedArray(F("inverter")); + Inverter<> *iv; + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i); + if (NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + JsonObject invObj = inv.createNestedObject(); + invObj[F("enabled")] = (bool)iv->config->enabled; + invObj[F("id")] = i; + invObj[F("name")] = String(iv->config->name); + invObj[F("version")] = String(iv->getFwVersion()); + invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp()); + invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp()); + invObj[F("ts_last_success")] = iv->getLastTs(rec); + } } - void getInverter(JsonObject obj, uint8_t id) { - Inverter<> *iv = mSys->getInverterByPos(id); + JsonArray warn = obj.createNestedArray(F("warnings")); + if (!mSys->Radio.isChipConnected()) + warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); + else if (!mSys->Radio.isPVariant()) + warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); + if (!mApp->getSettingsValid()) + warn.add(F("your settings are invalid")); + if (mApp->getRebootRequestState()) + warn.add(F("reboot your ESP to apply all your configuration changes")); + if (0 == mApp->getTimestamp()) + warn.add(F("time not set. No communication to inverter possible")); + /*if(0 == mSys->getNumInverters()) + warn.add(F("no inverter configured"));*/ + + if ((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0)) + warn.add(F("MQTT is not connected")); + + JsonArray info = obj.createNestedArray(F("infos")); + if (mApp->getMqttIsConnected()) + info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); + if (mConfig->mqtt.interval > 0) + info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds")); + } + + void getSetup(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + getSysInfo(obj.createNestedObject(F("system"))); + // getInverterList(obj.createNestedObject(F("inverter"))); + getMqtt(obj.createNestedObject(F("mqtt"))); + getNtp(obj.createNestedObject(F("ntp"))); + getSun(obj.createNestedObject(F("sun"))); + getPinout(obj.createNestedObject(F("pinout"))); + getRadio(obj.createNestedObject(F("radio"))); + getSerial(obj.createNestedObject(F("serial"))); + getStaticIp(obj.createNestedObject(F("static_ip"))); + getDisplay(obj.createNestedObject(F("display"))); + } + + void getNetworks(JsonObject obj) { + mApp->getAvailNetworks(obj); + } + + void getLive(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + // JsonArray invArr = obj.createNestedArray(F("inverter")); + obj[F("refresh")] = mConfig->nrf.sendInterval; + + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); + obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); + } + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); + obj[F("fld_names")][fld] = String(fields[dcList[fld]]); + } + + Inverter<> *iv; + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i); + bool parse = false; + if (NULL != iv) + parse = iv->config->enabled; + obj[F("iv")][i] = parse; + } + + /*Inverter<> *iv; + uint8_t pos; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - obj[F("id")] = id; - obj[F("enabled")] = (bool)iv->config->enabled; - obj[F("name")] = String(iv->config->name); - obj[F("serial")] = String(iv->config->serial.u64, HEX); - obj[F("version")] = String(iv->getFwVersion()); - obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); - obj[F("ts_last_success")] = rec->ts; - - JsonArray ch = obj.createNestedArray("ch"); - - // AC - uint8_t pos; - obj[F("ch_name")][0] = "AC"; + JsonObject obj2 = invArr.createNestedObject(); + obj2[F("enabled")] = (bool)iv->config->enabled; + obj2[F("name")] = String(iv->config->name); + obj2[F("channels")] = iv->channels; + obj2[F("power_limit_read")] = ah::round3(iv->actPowerLimit); + //obj2[F("last_alarm")] = String(iv->lastAlarmMsg); + obj2[F("ts_last_success")] = rec->ts; + + JsonArray ch = obj2.createNestedArray("ch"); JsonArray ch0 = ch.createNestedArray(); - for (uint8_t fld = 0; fld < sizeof(acList); fld++) { - pos = (iv->getPosByChFld(CH0, acList[fld], rec)); + obj2[F("ch_names")][0] = "AC"; + for (uint8_t fld = 0; fld < sizeof(list); fld++) { + pos = (iv->getPosByChFld(CH0, list[fld], rec)); ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; + obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; } - // DC - for(uint8_t j = 0; j < iv->channels; j ++) { - obj[F("ch_name")][j+1] = iv->config->chName[j]; + for(uint8_t j = 1; j <= iv->channels; j ++) { + obj2[F("ch_names")][j] = String(iv->config->chName[j-1]); JsonArray cur = ch.createNestedArray(); - for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { - pos = (iv->getPosByChFld((j+1), dcList[fld], rec)); - cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - } - } - } - } - - void getMqtt(JsonObject obj) { - obj[F("broker")] = String(mConfig->mqtt.broker); - obj[F("port")] = String(mConfig->mqtt.port); - obj[F("user")] = String(mConfig->mqtt.user); - obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); - obj[F("topic")] = String(mConfig->mqtt.topic); - obj[F("interval")] = String(mConfig->mqtt.interval); - } - - void getNtp(JsonObject obj) { - obj[F("addr")] = String(mConfig->ntp.addr); - obj[F("port")] = String(mConfig->ntp.port); - } - - void getSun(JsonObject obj) { - obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; - obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; - obj[F("disnightcom")] = mConfig->sun.disNightCom; - obj[F("offs")] = mConfig->sun.offsetSec; - } - - void getPinout(JsonObject obj) { - obj[F("cs")] = mConfig->nrf.pinCs; - obj[F("ce")] = mConfig->nrf.pinCe; - obj[F("irq")] = mConfig->nrf.pinIrq; - obj[F("led0")] = mConfig->led.led0; - obj[F("led1")] = mConfig->led.led1; - } - - void getRadio(JsonObject obj) { - obj[F("power_level")] = mConfig->nrf.amplifierPower; - obj[F("isconnected")] = mSys->Radio.isChipConnected(); - obj[F("DataRate")] = mSys->Radio.getDataRate(); - obj[F("isPVariant")] = mSys->Radio.isPVariant(); - } - - void getSerial(JsonObject obj) { - obj[F("interval")] = (uint16_t)mConfig->serial.interval; - obj[F("show_live_data")] = mConfig->serial.showIv; - obj[F("debug")] = mConfig->serial.debug; - } - - void getStaticIp(JsonObject obj) { - char buf[16]; - ah::ip2Char(mConfig->sys.ip.ip, buf); obj[F("ip")] = String(buf); - ah::ip2Char(mConfig->sys.ip.mask, buf); obj[F("mask")] = String(buf); - ah::ip2Char(mConfig->sys.ip.dns1, buf); obj[F("dns1")] = String(buf); - ah::ip2Char(mConfig->sys.ip.dns2, buf); obj[F("dns2")] = String(buf); - ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf); - } - - void getDisplay(JsonObject obj) { - obj[F("disp_type")] = (uint8_t)mConfig->plugin.display.type; - obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; - obj[F("logo_en")] = (bool)mConfig->plugin.display.logoEn; - obj[F("px_shift")] = (bool)mConfig->plugin.display.pxShift; - obj[F("rot180")] = (bool)mConfig->plugin.display.rot180; - obj[F("contrast")] = (uint8_t)mConfig->plugin.display.contrast; - obj[F("pinDisp0")] = mConfig->plugin.display.pin0; - obj[F("pinDisp1")] = mConfig->plugin.display.pin1; - } - - - void getIndex(JsonObject obj) { - getGeneric(obj.createNestedObject(F("generic"))); - obj[F("ts_now")] = mApp->getTimestamp(); - obj[F("ts_sunrise")] = mApp->getSunrise(); - obj[F("ts_sunset")] = mApp->getSunset(); - obj[F("ts_offset")] = mConfig->sun.offsetSec; - obj[F("disNightComm")] = mConfig->sun.disNightCom; - - JsonArray inv = obj.createNestedArray(F("inverter")); - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject invObj = inv.createNestedObject(); - invObj[F("enabled")] = (bool)iv->config->enabled; - invObj[F("id")] = i; - invObj[F("name")] = String(iv->config->name); - invObj[F("version")] = String(iv->getFwVersion()); - invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp()); - invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp()); - invObj[F("ts_last_success")] = iv->getLastTs(rec); - } - } - - JsonArray warn = obj.createNestedArray(F("warnings")); - if(!mSys->Radio.isChipConnected()) - warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); - else if(!mSys->Radio.isPVariant()) - warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); - if(!mApp->getSettingsValid()) - warn.add(F("your settings are invalid")); - if(mApp->getRebootRequestState()) - warn.add(F("reboot your ESP to apply all your configuration changes")); - if(0 == mApp->getTimestamp()) - warn.add(F("time not set. No communication to inverter possible")); - /*if(0 == mSys->getNumInverters()) - warn.add(F("no inverter configured"));*/ - - if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0)) - warn.add(F("MQTT is not connected")); - - JsonArray info = obj.createNestedArray(F("infos")); - if(mApp->getMqttIsConnected()) - info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); - if(mConfig->mqtt.interval > 0) - info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds")); - } - - void getSetup(JsonObject obj) { - getGeneric(obj.createNestedObject(F("generic"))); - getSysInfo(obj.createNestedObject(F("system"))); - //getInverterList(obj.createNestedObject(F("inverter"))); - getMqtt(obj.createNestedObject(F("mqtt"))); - getNtp(obj.createNestedObject(F("ntp"))); - getSun(obj.createNestedObject(F("sun"))); - getPinout(obj.createNestedObject(F("pinout"))); - getRadio(obj.createNestedObject(F("radio"))); - getSerial(obj.createNestedObject(F("serial"))); - getStaticIp(obj.createNestedObject(F("static_ip"))); - getDisplay(obj.createNestedObject(F("display"))); - } - - void getNetworks(JsonObject obj) { - mApp->getAvailNetworks(obj); - } - - void getLive(JsonObject obj) { - getGeneric(obj.createNestedObject(F("generic"))); - //JsonArray invArr = obj.createNestedArray(F("inverter")); - obj[F("refresh")] = mConfig->nrf.sendInterval; - - for (uint8_t fld = 0; fld < sizeof(acList); fld++) { - obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); - obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); - } - for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { - obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); - obj[F("fld_names")][fld] = String(fields[dcList[fld]]); - } - - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - bool parse = false; - if(NULL != iv) - parse = iv->config->enabled; - obj[F("iv")][i] = parse; - } - - /*Inverter<> *iv; - uint8_t pos; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("enabled")] = (bool)iv->config->enabled; - obj2[F("name")] = String(iv->config->name); - obj2[F("channels")] = iv->channels; - obj2[F("power_limit_read")] = ah::round3(iv->actPowerLimit); - //obj2[F("last_alarm")] = String(iv->lastAlarmMsg); - obj2[F("ts_last_success")] = rec->ts; - - JsonArray ch = obj2.createNestedArray("ch"); - JsonArray ch0 = ch.createNestedArray(); - obj2[F("ch_names")][0] = "AC"; - for (uint8_t fld = 0; fld < sizeof(list); fld++) { - pos = (iv->getPosByChFld(CH0, list[fld], rec)); - ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } - - for(uint8_t j = 1; j <= iv->channels; j ++) { - obj2[F("ch_names")][j] = String(iv->config->chName[j-1]); - JsonArray cur = ch.createNestedArray(); - for (uint8_t k = 0; k < 6; k++) { - switch(k) { - default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break; - case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break; - case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break; - case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break; - case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break; - case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; - } - cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - if(1 == j) { - obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } + for (uint8_t k = 0; k < 6; k++) { + switch(k) { + default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break; + case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break; + case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break; + case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break; + case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break; + case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; + } + cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + if(1 == j) { + obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; + obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; } } } - }*/ - } - - void getRecord(JsonObject obj, uint8_t recType) { - JsonArray invArr = obj.createNestedArray(F("inverter")); - - Inverter<> *iv; - record_t<> *rec; - uint8_t pos; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - rec = iv->getRecordStruct(recType); - JsonArray obj2 = invArr.createNestedArray(); - for(uint8_t j = 0; j < rec->length; j++) { - byteAssign_t *assign = iv->getByteAssign(j, rec); - pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); - obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; - } - } - } - } - - bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { - Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); - bool accepted = true; - if(NULL == iv) { - jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); - return false; } - - if(F("power") == jsonIn[F("cmd")]) - accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); - else if(F("restart") == jsonIn[F("restart")]) - accepted = iv->setDevControlRequest(Restart); - else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { - iv->powerLimit[0] = jsonIn["val"]; - if(F("limit_persistent_relative") == jsonIn[F("cmd")]) - iv->powerLimit[1] = RelativPersistent; - else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) - iv->powerLimit[1] = AbsolutPersistent; - else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")]) - iv->powerLimit[1] = RelativNonPersistent; - else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) - iv->powerLimit[1] = AbsolutNonPersistent; - - accepted = iv->setDevControlRequest(ActivePowerContr); - } - else if(F("dev") == jsonIn[F("cmd")]) { - DPRINTLN(DBG_INFO, F("dev cmd")); - iv->enqueCommand(jsonIn[F("val")].as()); - } - else { - jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; - return false; - } - - if(!accepted) { - jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); - return false; - } else - mApp->ivSendHighPrio(iv); - - return true; - } - - bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { - if(F("scan_wifi") == jsonIn[F("cmd")]) - mApp->scanAvailNetworks(); - else if(F("set_time") == jsonIn[F("cmd")]) - mApp->setTimestamp(jsonIn[F("val")]); - else if(F("sync_ntp") == jsonIn[F("cmd")]) - mApp->setTimestamp(0); // 0: update ntp flag - else if(F("serial_utc_offset") == jsonIn[F("cmd")]) - mTimezoneOffset = jsonIn[F("val")]; - else if(F("discovery_cfg") == jsonIn[F("cmd")]) { - mApp->setMqttDiscoveryFlag(); // for homeassistant - } - else { - jsonOut[F("error")] = F("unknown cmd"); - return false; + }*/ + } + + void getRecord(JsonObject obj, uint8_t recType) { + JsonArray invArr = obj.createNestedArray(F("inverter")); + + Inverter<> *iv; + record_t<> *rec; + uint8_t pos; + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i); + if (NULL != iv) { + rec = iv->getRecordStruct(recType); + JsonArray obj2 = invArr.createNestedArray(); + for (uint8_t j = 0; j < rec->length; j++) { + byteAssign_t *assign = iv->getByteAssign(j, rec); + pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); + obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; + obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; + obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; + } } - - return true; } - - IApp *mApp; - HMSYSTEM *mSys; - AsyncWebServer *mSrv; - settings_t *mConfig; - - uint32_t mTimezoneOffset; - uint32_t mHeapFree, mHeapFreeBlk; - uint8_t mHeapFrag; - uint16_t nr; + } + + bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { + Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); + bool accepted = true; + if (NULL == iv) { + jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); + return false; + } + + if (F("power") == jsonIn[F("cmd")]) + accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); + else if (F("restart") == jsonIn[F("restart")]) + accepted = iv->setDevControlRequest(Restart); + else if (0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { + iv->powerLimit[0] = jsonIn["val"]; + if (F("limit_persistent_relative") == jsonIn[F("cmd")]) + iv->powerLimit[1] = RelativPersistent; + else if (F("limit_persistent_absolute") == jsonIn[F("cmd")]) + iv->powerLimit[1] = AbsolutPersistent; + else if (F("limit_nonpersistent_relative") == jsonIn[F("cmd")]) + iv->powerLimit[1] = RelativNonPersistent; + else if (F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) + iv->powerLimit[1] = AbsolutNonPersistent; + + accepted = iv->setDevControlRequest(ActivePowerContr); + } else if (F("dev") == jsonIn[F("cmd")]) { + DPRINTLN(DBG_INFO, F("dev cmd")); + iv->enqueCommand(jsonIn[F("val")].as()); + } else { + jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; + return false; + } + + if (!accepted) { + jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); + return false; + } else + mApp->ivSendHighPrio(iv); + + return true; + } + + bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { + if (F("scan_wifi") == jsonIn[F("cmd")]) + mApp->scanAvailNetworks(); + else if (F("set_time") == jsonIn[F("cmd")]) + mApp->setTimestamp(jsonIn[F("val")]); + else if (F("sync_ntp") == jsonIn[F("cmd")]) + mApp->setTimestamp(0); // 0: update ntp flag + else if (F("serial_utc_offset") == jsonIn[F("cmd")]) + mTimezoneOffset = jsonIn[F("val")]; + else if (F("discovery_cfg") == jsonIn[F("cmd")]) { + mApp->setMqttDiscoveryFlag(); // for homeassistant + } else { + jsonOut[F("error")] = F("unknown cmd"); + return false; + } + + return true; + } + + IApp *mApp; + HMSYSTEM *mSys; + AsyncWebServer *mSrv; + settings_t *mConfig; + + uint32_t mTimezoneOffset; + uint32_t mHeapFree, mHeapFreeBlk; + uint8_t mHeapFrag; + uint16_t nr; }; #endif /*__WEB_API_H__*/ diff --git a/src/web/web.h b/src/web/web.h index d79ace82..5eed3688 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -8,932 +8,947 @@ #include "../utils/dbg.h" #ifdef ESP32 - #include "AsyncTCP.h" - #include "Update.h" +#include "AsyncTCP.h" +#include "Update.h" #else - #include "ESPAsyncTCP.h" +#include "ESPAsyncTCP.h" #endif -#include "ESPAsyncWebServer.h" - #include "../appInterface.h" - #include "../hm/hmSystem.h" #include "../utils/helper.h" - -#include "html/h/index_html.h" -#include "html/h/login_html.h" -#include "html/h/style_css.h" -#include "html/h/colorDark_css.h" -#include "html/h/colorBright_css.h" +#include "ESPAsyncWebServer.h" #include "html/h/api_js.h" +#include "html/h/colorBright_css.h" +#include "html/h/colorDark_css.h" #include "html/h/favicon_ico.h" -#include "html/h/setup_html.h" -#include "html/h/visualization_html.h" -#include "html/h/update_html.h" +#include "html/h/index_html.h" +#include "html/h/login_html.h" #include "html/h/serial_html.h" +#include "html/h/setup_html.h" +#include "html/h/style_css.h" #include "html/h/system_html.h" +#include "html/h/update_html.h" +#include "html/h/visualization_html.h" #define WEB_SERIAL_BUF_SIZE 2048 -const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1"}; +const char *const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1"}; -template +template class Web { - public: - Web(void) : mWeb(80), mEvts("/events") { - mProtected = true; - mLogoutTimeout = 0; - - memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); - mSerialBufFill = 0; - mSerialAddTime = true; - mSerialClientConnnected = false; - } - - void setup(IApp *app, HMSYSTEM *sys, settings_t *config) { - mApp = app; - mSys = sys; - mConfig = config; - - DPRINTLN(DBG_VERBOSE, F("app::setup-on")); - mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); - mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); - mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); - mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1)); - mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1)); - mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1)); - mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1)); - mWeb.onNotFound ( std::bind(&Web::showNotFound, this, std::placeholders::_1)); - mWeb.on("/reboot", HTTP_ANY, std::bind(&Web::onReboot, this, std::placeholders::_1)); - mWeb.on("/system", HTTP_ANY, std::bind(&Web::onSystem, this, std::placeholders::_1)); - mWeb.on("/erase", HTTP_ANY, std::bind(&Web::showErase, this, std::placeholders::_1)); - mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1)); - - mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); - mWeb.on("/save", HTTP_ANY, std::bind(&Web::showSave, this, std::placeholders::_1)); - - mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); - //mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1)); - - #ifdef ENABLE_JSON_EP - mWeb.on("/json", HTTP_ANY, std::bind(&Web::showJson, this, std::placeholders::_1)); - #endif - #ifdef ENABLE_PROMETHEUS_EP - mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1)); - #endif - - mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1)); - mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1), - std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); - mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1), - std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); - mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1)); - mWeb.on("/debug", HTTP_GET, std::bind(&Web::onDebug, this, std::placeholders::_1)); - - - mEvts.onConnect(std::bind(&Web::onConnect, this, std::placeholders::_1)); - mWeb.addHandler(&mEvts); - - mWeb.begin(); - - registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h + public: + Web(void) : mWeb(80), mEvts("/events") { + mProtected = true; + mLogoutTimeout = 0; + + memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); + mSerialBufFill = 0; + mSerialAddTime = true; + mSerialClientConnnected = false; + } + + void setup(IApp *app, HMSYSTEM *sys, settings_t *config) { + mApp = app; + mSys = sys; + mConfig = config; + + DPRINTLN(DBG_VERBOSE, F("app::setup-on")); + mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); + mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); + mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); + mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1)); + mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1)); + mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1)); + mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1)); + mWeb.onNotFound(std::bind(&Web::showNotFound, this, std::placeholders::_1)); + mWeb.on("/reboot", HTTP_ANY, std::bind(&Web::onReboot, this, std::placeholders::_1)); + mWeb.on("/system", HTTP_ANY, std::bind(&Web::onSystem, this, std::placeholders::_1)); + mWeb.on("/erase", HTTP_ANY, std::bind(&Web::showErase, this, std::placeholders::_1)); + mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1)); + + mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); + mWeb.on("/save", HTTP_ANY, std::bind(&Web::showSave, this, std::placeholders::_1)); + + mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); + // mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1)); - mUploadFail = false; - } +#ifdef ENABLE_JSON_EP + mWeb.on("/json", HTTP_ANY, std::bind(&Web::showJson, this, std::placeholders::_1)); +#endif +#ifdef ENABLE_PROMETHEUS_EP + mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1)); +#endif - void tickSecond() { - if(0 != mLogoutTimeout) { - mLogoutTimeout -= 1; - if(0 == mLogoutTimeout) { - if(strlen(mConfig->sys.adminPwd) > 0) - mProtected = true; - } + mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1)); + mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1), + std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1), + std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1)); + mWeb.on("/debug", HTTP_GET, std::bind(&Web::onDebug, this, std::placeholders::_1)); - DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout)); - } + mEvts.onConnect(std::bind(&Web::onConnect, this, std::placeholders::_1)); + mWeb.addHandler(&mEvts); - if(mSerialClientConnnected) { - if(mSerialBufFill > 0) { - mEvts.send(mSerialBuf, "serial", millis()); - memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); - mSerialBufFill = 0; - } + mWeb.begin(); + + registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h + + mUploadFail = false; + } + + void tickSecond() { + if (0 != mLogoutTimeout) { + mLogoutTimeout -= 1; + if (0 == mLogoutTimeout) { + if (strlen(mConfig->sys.adminPwd) > 0) + mProtected = true; } - } - AsyncWebServer *getWebSrvPtr(void) { - return &mWeb; + DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout)); } - void setProtection(bool protect) { - mProtected = protect; + if (mSerialClientConnnected) { + if (mSerialBufFill > 0) { + mEvts.send(mSerialBuf, "serial", millis()); + memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); + mSerialBufFill = 0; + } } + } - bool getProtection() { - return mProtected; - } + AsyncWebServer *getWebSrvPtr(void) { + return &mWeb; + } - void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - mApp->setOnUpdate(); + void setProtection(bool protect) { + mProtected = protect; + } - if(!index) { - Serial.printf("Update Start: %s\n", filename.c_str()); - #ifndef ESP32 - Update.runAsync(true); - #endif - if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { - Update.printError(Serial); - } + bool getProtection() { + return mProtected; + } + + void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + mApp->setOnUpdate(); + + if (!index) { + Serial.printf("Update Start: %s\n", filename.c_str()); +#ifndef ESP32 + Update.runAsync(true); +#endif + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + Update.printError(Serial); } - if(!Update.hasError()) { - if(Update.write(data, len) != len){ - Update.printError(Serial); - } + } + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + Update.printError(Serial); } - if(final) { - if(Update.end(true)) { - Serial.printf("Update Success: %uB\n", index+len); - } else { - Update.printError(Serial); - } + } + if (final) { + if (Update.end(true)) { + Serial.printf("Update Success: %uB\n", index + len); + } else { + Update.printError(Serial); } } + } - void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - if(!index) { - mUploadFail = false; - mUploadFp = LittleFS.open("/tmp.json", "w"); - if(!mUploadFp) { - DPRINTLN(DBG_ERROR, F("can't open file!")); - mUploadFail = true; - mUploadFp.close(); - } - } - mUploadFp.write(data, len); - if(final) { + void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + if (!index) { + mUploadFail = false; + mUploadFp = LittleFS.open("/tmp.json", "w"); + if (!mUploadFp) { + DPRINTLN(DBG_ERROR, F("can't open file!")); + mUploadFail = true; mUploadFp.close(); - File fp = LittleFS.open("/tmp.json", "r"); - if(!fp) + } + } + mUploadFp.write(data, len); + if (final) { + mUploadFp.close(); + File fp = LittleFS.open("/tmp.json", "r"); + if (!fp) + mUploadFail = true; + else { + if (!mApp->readSettings("tmp.json")) { mUploadFail = true; - else { - if(!mApp->readSettings("tmp.json")) { - mUploadFail = true; - DPRINTLN(DBG_ERROR, F("upload JSON error!")); - } - else - mApp->saveSettings(); - } - DPRINTLN(DBG_INFO, F("upload finished!")); + DPRINTLN(DBG_ERROR, F("upload JSON error!")); + } else + mApp->saveSettings(); } + DPRINTLN(DBG_INFO, F("upload finished!")); } + } - void serialCb(String msg) { - if(!mSerialClientConnnected) - return; + void serialCb(String msg) { + if (!mSerialClientConnnected) + return; - msg.replace("\r\n", ""); - if(mSerialAddTime) { - if((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { - if(mApp->getTimestamp() > 0) { - strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9); - mSerialBufFill += 9; - } - } - else { - mSerialBufFill = 0; - mEvts.send("webSerial, buffer overflow!", "serial", millis()); - return; + msg.replace("\r\n", ""); + if (mSerialAddTime) { + if ((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { + if (mApp->getTimestamp() > 0) { + strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9); + mSerialBufFill += 9; } - mSerialAddTime = false; - } - - if(msg.endsWith("")) - mSerialAddTime = true; - - uint16_t length = msg.length(); - if((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { - strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length); - mSerialBufFill += length; - } - else { + } else { mSerialBufFill = 0; mEvts.send("webSerial, buffer overflow!", "serial", millis()); + return; } - } - - private: - void checkRedirect(AsyncWebServerRequest *request) { - if((mConfig->sys.protectionMask & PROT_MASK_INDEX) != PROT_MASK_INDEX) - request->redirect(F("/index")); - else if((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE) - request->redirect(F("/live")); - else if((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL) - request->redirect(F("/serial")); - else if((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM) - request->redirect(F("/system")); - else - request->redirect(F("/login")); - } - - void onUpdate(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onUpdate")); - - if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) { - if(mProtected) { - checkRedirect(request); - return; - } + mSerialAddTime = false; + } + + if (msg.endsWith("")) + mSerialAddTime = true; + + uint16_t length = msg.length(); + if ((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { + strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length); + mSerialBufFill += length; + } else { + mSerialBufFill = 0; + mEvts.send("webSerial, buffer overflow!", "serial", millis()); + } + } + + private: + void checkRedirect(AsyncWebServerRequest *request) { + if ((mConfig->sys.protectionMask & PROT_MASK_INDEX) != PROT_MASK_INDEX) + request->redirect(F("/index")); + else if ((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE) + request->redirect(F("/live")); + else if ((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL) + request->redirect(F("/serial")); + else if ((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM) + request->redirect(F("/system")); + else + request->redirect(F("/login")); + } + + void onUpdate(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onUpdate")); + + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) { + if (mProtected) { + checkRedirect(request); + return; } - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), update_html, update_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); } - void showUpdate(AsyncWebServerRequest *request) { - bool reboot = !Update.hasError(); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), update_html, update_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } - String html = F("UpdateUpdate: "); - if(reboot) - html += "success"; - else - html += "failed"; - html += F("

rebooting ... auto reload after 20s"); + void showUpdate(AsyncWebServerRequest *request) { + bool reboot = !Update.hasError(); - AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); - response->addHeader("Connection", "close"); - request->send(response); - mApp->setRebootFlag(); - } + String html = F("UpdateUpdate: "); + if (reboot) + html += "success"; + else + html += "failed"; + html += F("

rebooting ... auto reload after 20s"); - void onUpload(AsyncWebServerRequest *request) { - bool reboot = !mUploadFail; + AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); + response->addHeader("Connection", "close"); + request->send(response); + mApp->setRebootFlag(); + } - String html = F("UploadUpload: "); - if(reboot) - html += "success"; - else - html += "failed"; - html += F("

rebooting ... auto reload after 20s"); + void onUpload(AsyncWebServerRequest *request) { + bool reboot = !mUploadFail; - AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); - response->addHeader("Connection", "close"); - request->send(response); - mApp->setRebootFlag(); - } + String html = F("UploadUpload: "); + if (reboot) + html += "success"; + else + html += "failed"; + html += F("

rebooting ... auto reload after 20s"); - void onConnect(AsyncEventSourceClient *client) { - DPRINTLN(DBG_VERBOSE, "onConnect"); + AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); + response->addHeader("Connection", "close"); + request->send(response); + mApp->setRebootFlag(); + } - mSerialClientConnnected = true; + void onConnect(AsyncEventSourceClient *client) { + DPRINTLN(DBG_VERBOSE, "onConnect"); - if(client->lastId()) - DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId())); + mSerialClientConnnected = true; - client->send("hello!", NULL, millis(), 1000); - } + if (client->lastId()) + DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId())); - void onIndex(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onIndex")); + client->send("hello!", NULL, millis(), 1000); + } - if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) { - if(mProtected) { - checkRedirect(request); - return; - } - } + void onIndex(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onIndex")); - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), index_html, index_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } - - void onLogin(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onLogin")); - - if(request->args() > 0) { - if(String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { - mProtected = false; - request->redirect("/"); - } - } - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), login_html, login_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } - - void onLogout(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onLogout")); - - if(mProtected) { + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) { + if (mProtected) { checkRedirect(request); return; } - - mProtected = true; - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } - - void onColor(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onColor")); - AsyncWebServerResponse *response; - if(mConfig->sys.darkMode) - response = request->beginResponse_P(200, F("text/css"), colorDark_css, colorDark_css_len); - else - response = request->beginResponse_P(200, F("text/css"), colorBright_css, colorBright_css_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); } - void onCss(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onCss")); - mLogoutTimeout = LOGOUT_TIMEOUT; - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), index_html, index_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onLogin(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onLogin")); + + if (request->args() > 0) { + if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { + mProtected = false; + request->redirect("/"); + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), login_html, login_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onLogout(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onLogout")); + + if (mProtected) { + checkRedirect(request); + return; + } + + mProtected = true; + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onColor(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onColor")); + AsyncWebServerResponse *response; + if (mConfig->sys.darkMode) + response = request->beginResponse_P(200, F("text/css"), colorDark_css, colorDark_css_len); + else + response = request->beginResponse_P(200, F("text/css"), colorBright_css, colorBright_css_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onCss(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onCss")); + mLogoutTimeout = LOGOUT_TIMEOUT; + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onApiJs(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onApiJs")); + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onFavicon(AsyncWebServerRequest *request) { + static const char favicon_type[] PROGMEM = "image/x-icon"; + AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void showNotFound(AsyncWebServerRequest *request) { + if (mProtected) + checkRedirect(request); + else + request->redirect("/setup"); + } + + void onReboot(AsyncWebServerRequest *request) { + mApp->setRebootFlag(); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void showErase(AsyncWebServerRequest *request) { + if (mProtected) { + checkRedirect(request); + return; + } + + DPRINTLN(DBG_VERBOSE, F("showErase")); + mApp->eraseSettings(false); + onReboot(request); + } + + void showFactoryRst(AsyncWebServerRequest *request) { + if (mProtected) { + checkRedirect(request); + return; + } + + DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); + String content = ""; + int refresh = 3; + if (request->args() > 0) { + if (request->arg("reset").toInt() == 1) { + refresh = 10; + if (mApp->eraseSettings(true)) + content = F("factory reset: success\n\nrebooting ... "); + else + content = F("factory reset: failed\n\nrebooting ... "); + } else { + content = F("factory reset: aborted"); + refresh = 3; + } + } else { + content = F( + "

Factory Reset

" + "

RESET

CANCEL

"); + refresh = 120; } - - void onApiJs(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onApiJs")); - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); + request->send(200, F("text/html; charset=UTF-8"), F("Factory Reset") + content + F("")); + if (refresh == 10) { + delay(1000); + ESP.restart(); } + } - void onFavicon(AsyncWebServerRequest *request) { - static const char favicon_type[] PROGMEM = "image/x-icon"; - AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } + void onSetup(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onSetup")); - void showNotFound(AsyncWebServerRequest *request) { - if(mProtected) + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) { + if (mProtected) { checkRedirect(request); - else - request->redirect("/setup"); + return; + } } - void onReboot(AsyncWebServerRequest *request) { - mApp->setRebootFlag(); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), setup_html, setup_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void showSave(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("showSave")); + + if (mProtected) { + checkRedirect(request); + return; + } + + if (request->args() == 0) + return; + + char buf[20] = {0}; + + // general + if (request->arg("ssid") != "") + request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN); + if (request->arg("pwd") != "{PWD}") + request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); + if (request->arg("device") != "") + request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); + mConfig->sys.darkMode = (request->arg("darkMode") == "on"); + + // protection + if (request->arg("adminpwd") != "{PWD}") { + request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); + mProtected = (strlen(mConfig->sys.adminPwd) > 0); + } + mConfig->sys.protectionMask = 0x0000; + for (uint8_t i = 0; i < 6; i++) { + if (request->arg("protMask" + String(i)) == "on") + mConfig->sys.protectionMask |= (1 << i); + } + + // static ip + request->arg("ipAddr").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.ip, buf); + request->arg("ipMask").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.mask, buf); + request->arg("ipDns1").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.dns1, buf); + request->arg("ipDns2").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.dns2, buf); + request->arg("ipGateway").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.gateway, buf); + + // inverter + Inverter<> *iv; + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i, false); + // enable communication + iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on"); + // address + request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); + if (strlen(buf) == 0) + memset(buf, 0, 20); + iv->config->serial.u64 = ah::Serial2u64(buf); + switch (iv->config->serial.b[4]) { + case 0x21: + iv->type = INV_TYPE_1CH; + iv->channels = 1; + break; + case 0x41: + iv->type = INV_TYPE_2CH; + iv->channels = 2; + break; + case 0x61: + iv->type = INV_TYPE_4CH; + iv->channels = 4; + break; + default: + break; + } + + // name + request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH); + + // max channel power / name + for (uint8_t j = 0; j < 4; j++) { + iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toInt(); + iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; + request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH); + } + iv->initialized = true; + } + + if (request->arg("invInterval") != "") + mConfig->nrf.sendInterval = request->arg("invInterval").toInt(); + if (request->arg("invRetry") != "") + mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt(); + mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); + mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); + mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); + + // pinout + uint8_t pin; + for (uint8_t i = 0; i < 5; i++) { + pin = request->arg(String(pinArgNames[i])).toInt(); + switch (i) { + default: + mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); + break; + case 1: + mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); + break; + case 2: + mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); + break; + case 3: + mConfig->led.led0 = pin; + break; + case 4: + mConfig->led.led1 = pin; + break; + } + } + + // nrf24 amplifier power + mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03; + + // ntp + if (request->arg("ntpAddr") != "") { + request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN); + mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff; + } + + // sun + if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { + mConfig->sun.lat = 0.0; + mConfig->sun.lon = 0.0; + mConfig->sun.disNightCom = false; + mConfig->sun.offsetSec = 0; + } else { + mConfig->sun.lat = request->arg("sunLat").toFloat(); + mConfig->sun.lon = request->arg("sunLon").toFloat(); + mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on"); + mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; + } + + // mqtt + if (request->arg("mqttAddr") != "") { + String addr = request->arg("mqttAddr"); + addr.trim(); + addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); + } else + mConfig->mqtt.broker[0] = '\0'; + request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); + if (request->arg("mqttPwd") != "{PWD}") + request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); + request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); + mConfig->mqtt.port = request->arg("mqttPort").toInt(); + mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); + + // serial console + if (request->arg("serIntvl") != "") { + mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff; + + mConfig->serial.debug = (request->arg("serDbg") == "on"); + mConfig->serial.showIv = (request->arg("serEn") == "on"); + // Needed to log TX buffers to serial console + mSys->Radio.mSerialDebug = mConfig->serial.debug; + } + + // display + mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("dispPwr") == "on"); + mConfig->plugin.display.pxShift = (request->arg("dispPxSh") == "on"); + mConfig->plugin.display.rot = request->arg("rotation").toInt(); + mConfig->plugin.display.type = request->arg("dispType").toInt(); + mConfig->plugin.display.contrast = request->arg("dispCont").toInt(); + mConfig->plugin.display.period = request->arg("period").toInt(); + mConfig->plugin.display.disp_data = request->arg("data").toInt(); + mConfig->plugin.display.disp_clk = request->arg("clock").toInt(); + mConfig->plugin.display.disp_cs = request->arg("cs").toInt(); + mConfig->plugin.display.disp_reset = request->arg("reset").toInt(); + mConfig->plugin.display.disp_busy = request->arg("busy").toInt(); + mConfig->plugin.display.disp_dc = request->arg("dc").toInt(); + + mApp->saveSettings(); + + if (request->arg("reboot") == "on") + onReboot(request); + else { AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); response->addHeader(F("Content-Encoding"), "gzip"); request->send(response); } + } - void showErase(AsyncWebServerRequest *request) { - if(mProtected) { - checkRedirect(request); - return; - } - - DPRINTLN(DBG_VERBOSE, F("showErase")); - mApp->eraseSettings(false); - onReboot(request); - } + void onLive(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onLive")); - void showFactoryRst(AsyncWebServerRequest *request) { - if(mProtected) { + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { + if (mProtected) { checkRedirect(request); return; } - - DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); - String content = ""; - int refresh = 3; - if(request->args() > 0) { - if(request->arg("reset").toInt() == 1) { - refresh = 10; - if(mApp->eraseSettings(true)) - content = F("factory reset: success\n\nrebooting ... "); - else - content = F("factory reset: failed\n\nrebooting ... "); - } - else { - content = F("factory reset: aborted"); - refresh = 3; - } - } - else { - content = F("

Factory Reset

" - "

RESET

CANCEL

"); - refresh = 120; - } - request->send(200, F("text/html; charset=UTF-8"), F("Factory Reset") + content + F("")); - if(refresh == 10) { - delay(1000); - ESP.restart(); - } } - void onSetup(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onSetup")); - - if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) { - if(mProtected) { - checkRedirect(request); - return; + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), visualization_html, visualization_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + response->addHeader(F("content-type"), "text/html; charset=UTF-8"); + + request->send(response); + } + + /*void showWebApi(AsyncWebServerRequest *request) { + // TODO: remove + DPRINTLN(DBG_VERBOSE, F("web::showWebApi")); + DPRINTLN(DBG_DEBUG, request->arg("plain")); + const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity. + DynamicJsonDocument response(capacity); + + // Parse JSON object + deserializeJson(response, request->arg("plain")); + // ToDo: error handling for payload + uint8_t iv_id = response["inverter"]; + uint8_t cmd = response["cmd"]; + Inverter<> *iv = mSys->getInverterByPos(iv_id); + if (NULL != iv) { + if (response["tx_request"] == (uint8_t)TX_REQ_INFO) { + // if the AlarmData is requested set the Alarm Index to the requested one + if (cmd == AlarmData || cmd == AlarmUpdate) { + // set the AlarmMesIndex for the request from user input + iv->alarmMesIndex = response["payload"]; } - } - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), setup_html, setup_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } - - void showSave(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("showSave")); - - if(mProtected) { - checkRedirect(request); - return; - } - - if(request->args() == 0) - return; - - char buf[20] = {0}; - - // general - if(request->arg("ssid") != "") - request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN); - if(request->arg("pwd") != "{PWD}") - request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); - if(request->arg("device") != "") - request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); - mConfig->sys.darkMode = (request->arg("darkMode") == "on"); - - // protection - if(request->arg("adminpwd") != "{PWD}") { - request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); - mProtected = (strlen(mConfig->sys.adminPwd) > 0); - } - mConfig->sys.protectionMask = 0x0000; - for(uint8_t i = 0; i < 6; i++) { - if(request->arg("protMask" + String(i)) == "on") - mConfig->sys.protectionMask |= (1 << i); - } - - // static ip - request->arg("ipAddr").toCharArray(buf, 20); - ah::ip2Arr(mConfig->sys.ip.ip, buf); - request->arg("ipMask").toCharArray(buf, 20); - ah::ip2Arr(mConfig->sys.ip.mask, buf); - request->arg("ipDns1").toCharArray(buf, 20); - ah::ip2Arr(mConfig->sys.ip.dns1, buf); - request->arg("ipDns2").toCharArray(buf, 20); - ah::ip2Arr(mConfig->sys.ip.dns2, buf); - request->arg("ipGateway").toCharArray(buf, 20); - ah::ip2Arr(mConfig->sys.ip.gateway, buf); - - - // inverter - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i, false); - // enable communication - iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on"); - // address - request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); - if(strlen(buf) == 0) - memset(buf, 0, 20); - iv->config->serial.u64 = ah::Serial2u64(buf); - switch(iv->config->serial.b[4]) { - case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break; - case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break; - case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break; - default: break; + DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"])); + // process payload from web request corresponding to the cmd + iv->enqueCommand(cmd); + } + + + if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) { + if (response["cmd"] == (uint8_t)ActivePowerContr) { + uint16_t webapiPayload = response["payload"]; + uint16_t webapiPayload2 = response["payload2"]; + if (webapiPayload > 0 && webapiPayload < 10000) { + iv->devControlCmd = ActivePowerContr; + iv->powerLimit[0] = webapiPayload; + if (webapiPayload2 > 0) + iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check + else // if not set, set it to 0x0000 default + iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut + if (iv->powerLimit[1] & 0x0001) + DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API")); + else + DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API")); + iv->devControlRequest = true; // queue it in the request loop + } } - - // name - request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH); - - // max channel power / name - for(uint8_t j = 0; j < 4; j++) { - iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toInt(); - iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; - request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH); + if (response["cmd"] == (uint8_t)TurnOff) { + iv->devControlCmd = TurnOff; + iv->devControlRequest = true; // queue it in the request loop } - iv->initialized = true; - } - - if(request->arg("invInterval") != "") - mConfig->nrf.sendInterval = request->arg("invInterval").toInt(); - if(request->arg("invRetry") != "") - mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt(); - mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); - mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); - mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); - - // pinout - uint8_t pin; - for(uint8_t i = 0; i < 5; i ++) { - pin = request->arg(String(pinArgNames[i])).toInt(); - switch(i) { - default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break; - case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break; - case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break; - case 3: mConfig->led.led0 = pin; break; - case 4: mConfig->led.led1 = pin; break; + if (response["cmd"] == (uint8_t)TurnOn) { + iv->devControlCmd = TurnOn; + iv->devControlRequest = true; // queue it in the request loop } - } - - // nrf24 amplifier power - mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03; - - // ntp - if(request->arg("ntpAddr") != "") { - request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN); - mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff; - } - - // sun - if(request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { - mConfig->sun.lat = 0.0; - mConfig->sun.lon = 0.0; - mConfig->sun.disNightCom = false; - mConfig->sun.offsetSec = 0; - } else { - mConfig->sun.lat = request->arg("sunLat").toFloat(); - mConfig->sun.lon = request->arg("sunLon").toFloat(); - mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on"); - mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; - } - - // mqtt - if(request->arg("mqttAddr") != "") { - String addr = request->arg("mqttAddr"); - addr.trim(); - addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); - } - else - mConfig->mqtt.broker[0] = '\0'; - request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); - if(request->arg("mqttPwd") != "{PWD}") - request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); - request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); - mConfig->mqtt.port = request->arg("mqttPort").toInt(); - mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); - - // serial console - if(request->arg("serIntvl") != "") { - mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff; - - mConfig->serial.debug = (request->arg("serDbg") == "on"); - mConfig->serial.showIv = (request->arg("serEn") == "on"); - // Needed to log TX buffers to serial console - mSys->Radio.mSerialDebug = mConfig->serial.debug; - } - - // display - mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("dispPwr") == "on"); - mConfig->plugin.display.logoEn = (request->arg("logoEn") == "on"); - mConfig->plugin.display.pxShift = (request->arg("dispPxSh") == "on"); - mConfig->plugin.display.rot180 = (request->arg("disp180") == "on"); - mConfig->plugin.display.type = request->arg("dispType").toInt(); - mConfig->plugin.display.contrast = request->arg("dispCont").toInt(); - mConfig->plugin.display.pin0 = request->arg("pinDisp0").toInt(); - mConfig->plugin.display.pin1 = request->arg("pinDisp1").toInt(); - - - mApp->saveSettings(); - - if(request->arg("reboot") == "on") - onReboot(request); - else { - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } - } - - void onLive(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onLive")); - - if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { - if(mProtected) { - checkRedirect(request); - return; + if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) { + iv->devControlCmd = CleanState_LockAndAlarm; + iv->devControlRequest = true; // queue it in the request loop + } + if (response["cmd"] == (uint8_t)Restart) { + iv->devControlCmd = Restart; + iv->devControlRequest = true; // queue it in the request loop } } - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), visualization_html, visualization_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - response->addHeader(F("content-type"), "text/html; charset=UTF-8"); - - request->send(response); } + request->send(200, "text/json", "{success:true}"); + }*/ - /*void showWebApi(AsyncWebServerRequest *request) { - // TODO: remove - DPRINTLN(DBG_VERBOSE, F("web::showWebApi")); - DPRINTLN(DBG_DEBUG, request->arg("plain")); - const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity. - DynamicJsonDocument response(capacity); - - // Parse JSON object - deserializeJson(response, request->arg("plain")); - // ToDo: error handling for payload - uint8_t iv_id = response["inverter"]; - uint8_t cmd = response["cmd"]; - Inverter<> *iv = mSys->getInverterByPos(iv_id); - if (NULL != iv) { - if (response["tx_request"] == (uint8_t)TX_REQ_INFO) { - // if the AlarmData is requested set the Alarm Index to the requested one - if (cmd == AlarmData || cmd == AlarmUpdate) { - // set the AlarmMesIndex for the request from user input - iv->alarmMesIndex = response["payload"]; - } - DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"])); - // process payload from web request corresponding to the cmd - iv->enqueCommand(cmd); - } + void onDebug(AsyncWebServerRequest *request) { + mApp->getSchedulerNames(); + AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok"); + request->send(response); + } + void onSerial(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onSerial")); - if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) { - if (response["cmd"] == (uint8_t)ActivePowerContr) { - uint16_t webapiPayload = response["payload"]; - uint16_t webapiPayload2 = response["payload2"]; - if (webapiPayload > 0 && webapiPayload < 10000) { - iv->devControlCmd = ActivePowerContr; - iv->powerLimit[0] = webapiPayload; - if (webapiPayload2 > 0) - iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check - else // if not set, set it to 0x0000 default - iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut - if (iv->powerLimit[1] & 0x0001) - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API")); - else - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API")); - iv->devControlRequest = true; // queue it in the request loop - } - } - if (response["cmd"] == (uint8_t)TurnOff) { - iv->devControlCmd = TurnOff; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)TurnOn) { - iv->devControlCmd = TurnOn; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) { - iv->devControlCmd = CleanState_LockAndAlarm; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)Restart) { - iv->devControlCmd = Restart; - iv->devControlRequest = true; // queue it in the request loop - } - } + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) { + if (mProtected) { + checkRedirect(request); + return; } - request->send(200, "text/json", "{success:true}"); - }*/ - - void onDebug(AsyncWebServerRequest *request) { - mApp->getSchedulerNames(); - AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok"); - request->send(response); } - void onSerial(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onSerial")); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), serial_html, serial_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } - if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) { - if(mProtected) { - checkRedirect(request); - return; - } - } - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), serial_html, serial_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } - - void onSystem(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onSystem")); + void onSystem(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onSystem")); - if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) { - if(mProtected) { - checkRedirect(request); - return; - } + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) { + if (mProtected) { + checkRedirect(request); + return; } - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); } -#ifdef ENABLE_JSON_EP - void showJson(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("web::showJson")); - String modJson; - Inverter<> *iv; - record_t<> *rec; - char topic[40], val[25]; - - modJson = F("{\n"); - for(uint8_t id = 0; id < mSys->getNumInverters(); id++) { - iv = mSys->getInverterByPos(id); - if(NULL == iv) - continue; - - rec = iv->getRecordStruct(RealTimeRunData_Debug); - snprintf(topic, 30, "\"%s\": {\n", iv->config->name); - modJson += String(topic); - for(uint8_t i = 0; i < rec->length; i++) { - snprintf(topic, 40, "\t\"ch%d/%s\"", rec->assign[i].ch, iv->getFieldName(i, rec)); - snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i, rec), iv->getUnit(i, rec)); - modJson += String(topic) + ": " + String(val) + F(",\n"); - } - modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(rec->ts) + F("\"\n\t},\n"); - } - modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mApp->getTimestamp())) + F("\"\n}\n"); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } - AsyncWebServerResponse *response = request->beginResponse(200, F("application/json"), modJson); - request->send(response); - } +#ifdef ENABLE_JSON_EP + void showJson(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("web::showJson")); + String modJson; + Inverter<> *iv; + record_t<> *rec; + char topic[40], val[25]; + + modJson = F("{\n"); + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; + + rec = iv->getRecordStruct(RealTimeRunData_Debug); + snprintf(topic, 30, "\"%s\": {\n", iv->config->name); + modJson += String(topic); + for (uint8_t i = 0; i < rec->length; i++) { + snprintf(topic, 40, "\t\"ch%d/%s\"", rec->assign[i].ch, iv->getFieldName(i, rec)); + snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i, rec), iv->getUnit(i, rec)); + modJson += String(topic) + ": " + String(val) + F(",\n"); + } + modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(rec->ts) + F("\"\n\t},\n"); + } + modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mApp->getTimestamp())) + F("\"\n}\n"); + + AsyncWebServerResponse *response = request->beginResponse(200, F("application/json"), modJson); + request->send(response); + } #endif #ifdef ENABLE_PROMETHEUS_EP - enum { - metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd - } metricsStep; - int metricsInverterId,metricsChannelId; - - void showMetrics(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); - - metricsStep = metricsStateStart; - AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), - [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t - { - Inverter<> *iv; - record_t<> *rec; - statistics_t *stat; - String promUnit, promType; - String metrics; - char type[60], topic[100], val[25]; - size_t len = 0; - int alarmChannelId; - - switch (metricsStep) { - case metricsStateStart: // System Info & NRF Statistics : fit to one packet - snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n", - mApp->getVersion(), mConfig->sys.deviceName); - metrics = String(type) + String(topic); - - snprintf(topic,sizeof(topic),"# TYPE ahoy_solar_freeheap gauge\nahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap()); - metrics += String(topic); - - // NRF Statistics - stat = mApp->getStatistics(); - metrics += radioStatistic(F("rx_success"), stat->rxSuccess); - metrics += radioStatistic(F("rx_fail"), stat->rxFail); - metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser); - metrics += radioStatistic(F("frame_cnt"), stat->frmCnt); - metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); - - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - // Start Inverter loop - metricsInverterId = 0; - metricsStep = metricsStateInverter; - break; - - case metricsStateInverter: // Inverter loop - if (metricsInverterId < mSys->getNumInverters()) { - iv = mSys->getInverterByPos(metricsInverterId); - if(NULL != iv) { - // Inverter info : fit to one packet - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", - iv->config->name, iv->config->serial.u64); - metrics = String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); - metrics += String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); - metrics += String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); - metrics += String(type) + String(topic); - - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - - // Start Realtime Data Channel loop for this inverter - metricsChannelId = 0; - metricsStep = metricStateRealtimeData; - } - } else { - metricsStep = metricsStateEnd; - } - break; - - case metricStateRealtimeData: // Realtime Data Channel loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (metricsChannelId < rec->length) { - uint8_t channel = rec->assign[metricsChannelId].ch; - std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); - if (0 == channel) { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); - } else { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); - } - snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); - - metricsChannelId++; - } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. - - // All realtime data channels processed --> try alarm data - metricsStep = metricsStateAlarmData; - } - break; - - case metricsStateAlarmData: // Alarm Info loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(AlarmData); - // simple hack : there is only one channel with alarm data - // TODO: find the right one channel with the alarm id - alarmChannelId = 0; - // printf("AlarmData Length %d\n",rec->length); - if (alarmChannelId < rec->length) - { - //uint8_t channel = rec->assign[alarmChannelId].ch; - std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); - snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); - } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. - } - // alarm channel processed --> try next inverter - metricsInverterId++; - metricsStep = metricsStateInverter; - break; - - case metricsStateEnd: - default: // end of transmission - len = 0; - break; - } - return len; - }); - request->send(response); - } - - String radioStatistic(String statistic, uint32_t value) { - char type[60], topic[80], val[25]; - snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s gauge",statistic.c_str()); - snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str()); - snprintf(val, sizeof(val), "%d", value); - return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n"); - } - - std::pair convertToPromUnits(String shortUnit) { - if(shortUnit == "A") return {"_ampere", "gauge"}; - if(shortUnit == "V") return {"_volt", "gauge"}; - if(shortUnit == "%") return {"_ratio", "gauge"}; - if(shortUnit == "W") return {"_watt", "gauge"}; - if(shortUnit == "Wh") return {"_wattHours", "counter"}; - if(shortUnit == "kWh") return {"_kilowattHours", "counter"}; - if(shortUnit == "°C") return {"_celsius", "gauge"}; - if(shortUnit == "var") return {"_var", "gauge"}; - if(shortUnit == "Hz") return {"_hertz", "gauge"}; - return {"", "gauge"}; - } + enum { + metricsStateStart, + metricsStateInverter, + metricStateRealtimeData, + metricsStateAlarmData, + metricsStateEnd + } metricsStep; + int metricsInverterId, metricsChannelId; + + void showMetrics(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); + + metricsStep = metricsStateStart; + AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), + [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t { + Inverter<> *iv; + record_t<> *rec; + statistics_t *stat; + String promUnit, promType; + String metrics; + char type[60], topic[100], val[25]; + size_t len = 0; + int alarmChannelId; + + switch (metricsStep) { + case metricsStateStart: // System Info & NRF Statistics : fit to one packet + snprintf(type, sizeof(type), "# TYPE ahoy_solar_info gauge\n"); + snprintf(topic, sizeof(topic), "ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n", + mApp->getVersion(), mConfig->sys.deviceName); + metrics = String(type) + String(topic); + + snprintf(topic, sizeof(topic), "# TYPE ahoy_solar_freeheap gauge\nahoy_solar_freeheap{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, ESP.getFreeHeap()); + metrics += String(topic); + + // NRF Statistics + stat = mApp->getStatistics(); + metrics += radioStatistic(F("rx_success"), stat->rxSuccess); + metrics += radioStatistic(F("rx_fail"), stat->rxFail); + metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser); + metrics += radioStatistic(F("frame_cnt"), stat->frmCnt); + metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); + + len = snprintf((char *)buffer, maxLen, "%s", metrics.c_str()); + // Start Inverter loop + metricsInverterId = 0; + metricsStep = metricsStateInverter; + break; + + case metricsStateInverter: // Inverter loop + if (metricsInverterId < mSys->getNumInverters()) { + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + // Inverter info : fit to one packet + snprintf(type, sizeof(type), "# TYPE ahoy_solar_inverter_info gauge\n"); + snprintf(topic, sizeof(topic), "ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", + iv->config->name, iv->config->serial.u64); + metrics = String(type) + String(topic); + + snprintf(type, sizeof(type), "# TYPE ahoy_solar_inverter_is_enabled gauge\n"); + snprintf(topic, sizeof(topic), "ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n", iv->config->name, iv->config->enabled); + metrics += String(type) + String(topic); + + snprintf(type, sizeof(type), "# TYPE ahoy_solar_inverter_is_available gauge\n"); + snprintf(topic, sizeof(topic), "ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n", iv->config->name, iv->isAvailable(mApp->getTimestamp())); + metrics += String(type) + String(topic); + + snprintf(type, sizeof(type), "# TYPE ahoy_solar_inverter_is_producing gauge\n"); + snprintf(topic, sizeof(topic), "ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n", iv->config->name, iv->isProducing(mApp->getTimestamp())); + metrics += String(type) + String(topic); + + len = snprintf((char *)buffer, maxLen, "%s", metrics.c_str()); + + // Start Realtime Data Channel loop for this inverter + metricsChannelId = 0; + metricsStep = metricStateRealtimeData; + } + } else { + metricsStep = metricsStateEnd; + } + break; + + case metricStateRealtimeData: // Realtime Data Channel loop + iv = mSys->getInverterByPos(metricsInverterId); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (metricsChannelId < rec->length) { + uint8_t channel = rec->assign[metricsChannelId].ch; + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); + snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + if (0 == channel) { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); + } else { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name, iv->config->chName[channel - 1]); + } + snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); + len = snprintf((char *)buffer, maxLen, "%s\n%s %s\n", type, topic, val); + + metricsChannelId++; + } else { + len = snprintf((char *)buffer, maxLen, "#\n"); // At least one char to send otherwise the transmission ends. + + // All realtime data channels processed --> try alarm data + metricsStep = metricsStateAlarmData; + } + break; + + case metricsStateAlarmData: // Alarm Info loop + iv = mSys->getInverterByPos(metricsInverterId); + rec = iv->getRecordStruct(AlarmData); + // simple hack : there is only one channel with alarm data + // TODO: find the right one channel with the alarm id + alarmChannelId = 0; + // printf("AlarmData Length %d\n",rec->length); + if (alarmChannelId < rec->length) { + // uint8_t channel = rec->assign[alarmChannelId].ch; + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); + snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); + len = snprintf((char *)buffer, maxLen, "%s\n%s %s\n", type, topic, val); + } else { + len = snprintf((char *)buffer, maxLen, "#\n"); // At least one char to send otherwise the transmission ends. + } + // alarm channel processed --> try next inverter + metricsInverterId++; + metricsStep = metricsStateInverter; + break; + + case metricsStateEnd: + default: // end of transmission + len = 0; + break; + } + return len; + }); + request->send(response); + } + + String radioStatistic(String statistic, uint32_t value) { + char type[60], topic[80], val[25]; + snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s gauge", statistic.c_str()); + snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s", statistic.c_str()); + snprintf(val, sizeof(val), "%d", value); + return (String(type) + "\n" + String(topic) + " " + String(val) + "\n"); + } + + std::pair convertToPromUnits(String shortUnit) { + if (shortUnit == "A") return {"_ampere", "gauge"}; + if (shortUnit == "V") return {"_volt", "gauge"}; + if (shortUnit == "%") return {"_ratio", "gauge"}; + if (shortUnit == "W") return {"_watt", "gauge"}; + if (shortUnit == "Wh") return {"_wattHours", "counter"}; + if (shortUnit == "kWh") return {"_kilowattHours", "counter"}; + if (shortUnit == "°C") return {"_celsius", "gauge"}; + if (shortUnit == "var") return {"_var", "gauge"}; + if (shortUnit == "Hz") return {"_hertz", "gauge"}; + return {"", "gauge"}; + } #endif - AsyncWebServer mWeb; - AsyncEventSource mEvts; - bool mProtected; - uint32_t mLogoutTimeout; - IApp *mApp; - HMSYSTEM *mSys; - - settings_t *mConfig; - - bool mSerialAddTime; - char mSerialBuf[WEB_SERIAL_BUF_SIZE]; - uint16_t mSerialBufFill; - bool mSerialClientConnnected; - - File mUploadFp; - bool mUploadFail; + AsyncWebServer mWeb; + AsyncEventSource mEvts; + bool mProtected; + uint32_t mLogoutTimeout; + IApp *mApp; + HMSYSTEM *mSys; + + settings_t *mConfig; + + bool mSerialAddTime; + char mSerialBuf[WEB_SERIAL_BUF_SIZE]; + uint16_t mSerialBufFill; + bool mSerialClientConnnected; + + File mUploadFp; + bool mUploadFail; }; #endif /*__WEB_H__*/