diff --git a/src/CHANGES.md b/src/CHANGES.md index fc8566cc..7839d420 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,14 @@ # Development Changes +## 0.8.31 - 2023-12-29 +* added class to handle timeouts PR #1298 + +## 0.8.30 - 2023-12-28 +* added info if grid profile was not found +* merge PR #1293 +* merge PR #1295 fix ESP8266 pin settings +* merge PR #1297 fix layout for OLED displays + ## 0.8.29 - 2023-12-27 * fix MqTT generic topic `comm_disabled` #1265 #1286 * potential fix of #1285 (reset yield day) diff --git a/src/defines.h b/src/defines.h index 6613416e..c01104d1 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 29 +#define VERSION_PATCH 31 //------------------------------------- typedef struct { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 23d20004..1ed45831 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -9,6 +9,7 @@ #include "CommQueue.h" #include #include "../utils/crc.h" +#include "../utils/timemonitor.h" #include "Heuristic.h" #define MI_TIMEOUT 250 // timeout for MI type requests @@ -59,8 +60,7 @@ class Communication : public CommQueue<> { mLastEmptyQueueMillis = millis(); mPrintSequenceDuration = true; - uint16_t timeout = (q->iv->ivGen == IV_MI) ? MI_TIMEOUT : (((q->iv->mGotFragment && q->iv->mGotLastMsg) || mIsRetransmit) ? SINGLEFR_TIMEOUT : ((q->cmd != AlarmData) ? DEFAULT_TIMEOUT : (1.5 * DEFAULT_TIMEOUT))); - uint16_t timeout_min = (q->iv->ivGen == IV_MI) ? MI_TIMEOUT : ((q->iv->mGotFragment || mIsRetransmit)) ? SINGLEFR_TIMEOUT : FRSTMSG_TIMEOUT; + uint16_t timeout = (q->iv->ivGen == IV_MI) ? MI_TIMEOUT : (((q->iv->mGotFragment && q->iv->mGotLastMsg) || mIsRetransmit) ? SINGLEFR_TIMEOUT : ((q->cmd != AlarmData) && (q->cmd != GridOnProFilePara) ? DEFAULT_TIMEOUT : (1.5 * DEFAULT_TIMEOUT))); /*if(mDebugState != mState) { DPRINT(DBG_INFO, F("State: ")); @@ -69,8 +69,9 @@ class Communication : public CommQueue<> { }*/ switch(mState) { case States::RESET: - if(millis() < mWaitTimeout) + if (!mWaitTime.isTimeout()) return; + mMaxFrameId = 0; for(uint8_t i = 0; i < MAX_PAYLOAD_ENTRIES; i++) { mLocalBuf[i].len = 0; @@ -108,16 +109,17 @@ class Communication : public CommQueue<> { q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false); q->iv->radioStatistics.txCnt++; - mWaitTimeout = millis() + timeout; - mWaitTimeout_min = millis() + timeout_min; + mWaitTime.startTimeMonitor(timeout); mIsRetransmit = false; - mlastTO_min = timeout_min; setAttempt(); + if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara)) + incrAttempt(q->cmd == AlarmData? 5 : 3); + mState = States::WAIT; break; case States::WAIT: - if(millis() < mWaitTimeout) + if (!mWaitTime.isTimeout()) return; mState = States::CHECK_FRAMES; break; @@ -127,13 +129,13 @@ class Communication : public CommQueue<> { if(*mSerialDebug) { DPRINT_IVID(DBG_INFO, q->iv->id); DBGPRINT(F("request timeout: ")); - DBGPRINT(String(millis() - mWaitTimeout + timeout)); + DBGPRINT(String(mWaitTime.getRunTime())); DBGPRINTLN(F("ms")); } if(!q->iv->mGotFragment) { if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) { q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ)); - mWaitTimeout = millis() + 1000; + mWaitTime.startTimeMonitor(1000); } } closeRequest(q, false); @@ -182,7 +184,7 @@ class Communication : public CommQueue<> { bool fastNext = true; if(q->iv->miMultiParts < 6) { mState = States::WAIT; - if((millis() > mWaitTimeout && mIsRetransmit) || !mIsRetransmit) { + if((mWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) { miRepeatRequest(q); return; } @@ -242,7 +244,6 @@ class Communication : public CommQueue<> { q->iv->mIsSingleframeReq = true; sendRetransmit(q, (framnr-1)); mIsRetransmit = true; - mlastTO_min = timeout_min; return; } @@ -446,7 +447,6 @@ class Communication : public CommQueue<> { if(NULL == rec) { if(GetLossRate == q->cmd) { q->iv->parseGetLossRate(mPayload, len); - //closeRequest(q, true); //@lumapu: Activating would crash most esp's! return; } else { DPRINTLN(DBG_ERROR, F("record is NULL!")); @@ -490,7 +490,7 @@ class Communication : public CommQueue<> { if(q->attempts) { q->iv->radio->sendCmdPacket(q->iv, TX_REQ_INFO, (SINGLE_FRAME + i), true); q->iv->radioStatistics.retransmits++; - mWaitTimeout = millis() + SINGLEFR_TIMEOUT; // timeout + mWaitTime.startTimeMonitor(SINGLEFR_TIMEOUT); // timeout mState = States::WAIT; } else { //add(q, true); @@ -507,7 +507,7 @@ class Communication : public CommQueue<> { q->iv->radioStatistics.rxFail++; // got no complete payload else q->iv->radioStatistics.rxFailNoAnser++; // got nothing - mWaitTimeout = millis() + *mInverterGap; + mWaitTime.startTimeMonitor(*mInverterGap); bool keep = false; if(q->isDevControl) @@ -729,8 +729,7 @@ class Communication : public CommQueue<> { //q->iv->radioStatistics.retransmits++; q->iv->radio->sendCmdPacket(q->iv, cmd, 0x00, true); - mWaitTimeout = millis() + MI_TIMEOUT; - mWaitTimeout_min = mWaitTimeout; + mWaitTime.startTimeMonitor(MI_TIMEOUT); q->iv->miMultiParts = 0; q->iv->mGotFragment = 0; mIsRetransmit = true; @@ -750,8 +749,7 @@ class Communication : public CommQueue<> { q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true); - mWaitTimeout = millis() + MI_TIMEOUT; - mWaitTimeout_min = mWaitTimeout; + mWaitTime.startTimeMonitor(MI_TIMEOUT); //mState = States::WAIT; mIsRetransmit = false; } @@ -885,12 +883,10 @@ class Communication : public CommQueue<> { uint32_t *mTimestamp; bool *mPrivacyMode, *mSerialDebug, *mPrintWholeTrace; uint16_t *mInverterGap; - uint32_t mWaitTimeout = 0; - uint32_t mWaitTimeout_min = 0; + TimeMonitor mWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state) std::array mLocalBuf; - bool mFirstTry = false; // see, if we should do a second try - bool mIsRetransmit = false; // we alrady had waited one complete cycle - uint16_t mlastTO_min = DEFAULT_TIMEOUT; // remember timeout_min for correct calculation + bool mFirstTry = false; // see, if we should do a second try + bool mIsRetransmit = false; // we already had waited one complete cycle uint8_t mMaxFrameId; uint8_t mPayload[MAX_BUFFER]; payloadListenerType mCbPayload = NULL; diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 1c4e26e9..d7ae594b 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -121,9 +121,10 @@ class HmRadio : public Radio { uint32_t startMicros = micros(); uint32_t loopMillis = millis(); - uint32_t outerLoopTimeout = (mLastIv->mIsSingleframeReq) ? 100 : ((mLastIv->mCmd != AlarmData) ? 400 : 600); + uint32_t outerLoopTimeout = (mLastIv->mIsSingleframeReq) ? 100 : ((mLastIv->mCmd != AlarmData) && (mLastIv->mCmd != GridOnProFilePara)) ? 400 : 600; while ((millis() - loopMillis) < outerLoopTimeout) { + startMicros = micros(); while ((micros() - startMicros) < 5110) { // listen (4088us or?) 5110us to each channel if (mIrqRcvd) { mIrqRcvd = false; @@ -137,7 +138,6 @@ class HmRadio : public Radio { // switch to next RX channel mRxChIdx = (mRxChIdx + 1) % RF_CHANNELS; mNrf24->setChannel(mRfChLst[mRxChIdx]); - startMicros = micros(); } // not finished but time is over mRxChIdx = (mRxChIdx + 1) % RF_CHANNELS; diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 3fc691c2..3e998b6d 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -19,6 +19,7 @@ #include "../../utils/helper.h" #include "Display_data.h" #include "../../utils/dbg.h" +#include "../../utils/timemonitor.h" class DisplayMono { public: @@ -37,19 +38,19 @@ class DisplayMono { if (mDisplayActive) { if (!dispConditions) { - if ((millis() - mStarttime) > DISP_DEFAULT_TIMEOUT * 1000ul) { // switch display off after timeout + if (mDisplayTime.isTimeout()) { // switch display off after timeout mDisplayActive = false; mDisplay->setPowerSave(true); DBGPRINTLN("**** Display off ****"); } } else - mStarttime = millis(); // keep display on + mDisplayTime.reStartTimeMonitor(); // keep display on } else { if (dispConditions) { - mDisplayActive = true; // switch display on - mStarttime = millis(); + mDisplayActive = true; + mDisplayTime.reStartTimeMonitor(); // switch display on mDisplay->setPowerSave(false); DBGPRINTLN("**** Display on ****"); } @@ -79,7 +80,7 @@ class DisplayMono { uint8_t mExtra; int8_t mPixelshift=0; - uint32_t mStarttime = millis(); + TimeMonitor mDisplayTime = TimeMonitor(1000 * 15, true); bool mDisplayActive = true; // always start with display on char mFmtText[DISP_FMT_TEXT_LEN]; diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 7b7b7920..afb581dd 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -49,9 +49,9 @@ class DisplayMono128X64 : public DisplayMono { /* mDisplayData->nrSleeping = 10; mDisplayData->nrProducing = 10; - mDisplayData->totalPower = 54321.9; // W - mDisplayData->totalYieldDay = 4321.9; // Wh - mDisplayData->totalYieldTotal = 4321.9; // kWh + mDisplayData->totalPower = 15432.9; // W + mDisplayData->totalYieldDay = 14321.9; // Wh + mDisplayData->totalYieldTotal = 15432.9; // kWh mDisplay->drawPixel(0, 0); mDisplay->drawPixel(mDispWidth-1, 0); mDisplay->drawPixel(0, mDispHeight-1); @@ -63,8 +63,8 @@ class DisplayMono128X64 : public DisplayMono { // print total power if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 999) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f kW", (mDisplayData->totalPower / 1000.0)); + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); @@ -120,16 +120,16 @@ class DisplayMono128X64 : public DisplayMono { mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol - if (mDisplayData->totalYieldDay > 999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f kWh", mDisplayData->totalYieldDay / 1000.0); + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); printText(mFmtText, l_YieldDay, 0xff); - if (mDisplayData->totalYieldTotal > 999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f MWh", mDisplayData->totalYieldTotal / 1000.0); + if (mDisplayData->totalYieldTotal > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f kWh", mDisplayData->totalYieldTotal); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); printText(mFmtText, l_YieldTotal, 0xff); // draw dynamic RSSI bars diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index df27a414..7e5c157f 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -36,8 +36,8 @@ class DisplayMono84X48 : public DisplayMono { /* mDisplayData->nrSleeping = 10; mDisplayData->nrProducing = 10; - mDisplayData->totalPower = 111.91; // W - mDisplayData->totalYieldDay = 54321.9; // Wh + mDisplayData->totalPower = 19897.6; // W + mDisplayData->totalYieldDay = 15521.9; // Wh mDisplayData->totalYieldTotal = 654321.9; // kWh mDisplay->drawPixel(0, 0); mDisplay->drawPixel(mDispWidth-1, 0); @@ -47,8 +47,8 @@ class DisplayMono84X48 : public DisplayMono { // print total power if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 999) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f kW", (mDisplayData->totalPower / 1000)); + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); @@ -84,16 +84,16 @@ class DisplayMono84X48 : public DisplayMono { printText("\x88", l_YieldDay, 10); // day symbol printText("\x83", l_YieldTotal, 10); // total symbol - if (mDisplayData->totalYieldDay > 999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f kWh", mDisplayData->totalYieldDay / 1000.0); + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); printText(mFmtText, l_YieldDay, 0xff); - if (mDisplayData->totalYieldTotal > 999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f MWh", mDisplayData->totalYieldTotal / 1000.0); + if (mDisplayData->totalYieldTotal > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.1f kWh", mDisplayData->totalYieldTotal); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); printText(mFmtText, l_YieldTotal, 0xff); // draw dynamic Nokia RSSI bars diff --git a/src/utils/timemonitor.h b/src/utils/timemonitor.h new file mode 100644 index 00000000..798077d5 --- /dev/null +++ b/src/utils/timemonitor.h @@ -0,0 +1,126 @@ +//----------------------------------------------------------- +// You69Man, 2023 +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 2 as published by the Free Software Foundation. +//----------------------------------------------------------- + +/** + * @file timemonitor.h + * + * Class declaration for TimeMonitor + */ + +#ifndef __TIMEMONITOR_H__ +#define __TIMEMONITOR_H__ + +#include + +class TimeMonitor { + public: + /** + * A constructor for initializing a TimeMonitor + * @note TimeMonitor witch default constructor is stopped + */ + TimeMonitor(void) {} + + /** + * A constructor for initializing a TimeMonitor + * @param timeout timeout in ms + * @param start (optional) if true, start TimeMonitor immediately + * @note TimeMonitor witch default constructor is stopped + */ + TimeMonitor(uint32_t timeout, bool start = false) { + if (start) + startTimeMonitor(timeout); + else + configureTimeMonitor(timeout); + } + + /** + * Start the TimeMonitor with new timeout configuration + * @param timeout timout in ms + */ + void startTimeMonitor(uint32_t timeout) { + mStartTime = millis(); + mTimeout = timeout; + mStarted = true; + } + + /** + * Restart the TimeMonitor with already set timeout configuration + * @note returns nothing + */ + void reStartTimeMonitor(void) { + mStartTime = millis(); + mStarted = true; + } + + /** + * Configure the TimeMonitor to new timeout configuration + * @param timeout timeout in ms + * @note This doesn't restart an already running TimeMonitor. + * If timer is already running and new timeout is longer that current timeout runtime of the running timer is expanded + * If timer is already running and new timeout is shorter that current timeout this can immediately lead to a timeout + */ + void configureTimeMonitor(uint32_t timeout) { + mTimeout = timeout; + } + + /** + * Stop the TimeMonitor + */ + void stopTimeMonitor(void) { + mStarted = false; + } + + /** + * Get timeout status of the TimeMonitor + * @return bool + * true: TimeMonitor already timed out + * false: TimeMonitor still in time or TimeMonitor was stopped + */ + bool isTimeout(void) { + if ((mStarted) && (millis() - mStartTime >= mTimeout)) + return true; + else + return false; + } + + /** + * Get timeout configuration of the TimeMonitor + * @return uint32_t timeout value in ms + */ + uint32_t getTimeout(void) const { + return mTimeout; + } + + /** + * Get residual time of the TimeMonitor until timeout + * @return uint32_t residual time until timeout in ms + * @note in case of a stopped TimeMonitor residual time is always 0xFFFFFFFFUL + * in case of a timed out TimeMonitor residual time is always 0UL (zero) + */ + uint32_t getResidualTime(void) const { + uint32_t delayed = millis() - mStartTime; + return(mStarted ? (delayed < mTimeout ? mTimeout - delayed : 0UL) : 0xFFFFFFFFUL); + } + + /** + * Get overall run time of the TimeMonitor + * @return uint32_t residual time until timeout in ms + * @note in case of a stopped TimeMonitor residual time is always 0xFFFFFFFFUL + * in case of a timed out TimeMonitor residual time is always 0UL (zero) + */ + uint32_t getRunTime(void) const { + return(mStarted ? millis() - mStartTime : 0UL); + } + + private: + uint32_t mStartTime = 0UL; // start time of the TimeMonitor + uint32_t mTimeout = 0UL; // timeout configuration of the TimeMonitor + bool mStarted = false; // start/stop state of the TimeMonitor +}; + +#endif \ No newline at end of file diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json index adb68393..5dcdda65 100644 --- a/src/web/html/grid_info.json +++ b/src/web/html/grid_info.json @@ -159,6 +159,7 @@ { "name": "Nominal Voltage", "div": 10, + "def": 230, "unit": "V" }, { diff --git a/src/web/html/setup.html b/src/web/html/setup.html index a41f50b3..25748695 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -944,10 +944,12 @@ function parsePinout(obj, type, system) { var e = document.getElementById("pinout"); + var pinList = esp8266pins; + /*IF_ESP32*/ var pinList = esp32pins; - if("ESP8266" == type) pinList = esp8266pins; - else if ("ESP32-S3" == system["chip_model"]) pinList = esp32s3pins; + if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*ENDIF_ESP32*/ pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected']]; for(p of pins) { e.append( @@ -974,10 +976,12 @@ var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox"); en.checked = obj["en"]; + var pinList = esp8266pins; + /*IF_ESP32*/ var pinList = esp32pins; - if("ESP8266" == type) pinList = esp8266pins; - else if ("ESP32-S3" == system["chip_model"]) pinList = esp32s3pins; + if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*ENDIF_ESP32*/ e.replaceChildren ( ml("div", {class: "row mb-3"}, [ @@ -1039,10 +1043,12 @@ } function parseDisplay(obj, type, system) { + var pinList = esp8266pins; + /*IF_ESP32*/ var pinList = esp32pins; - if("ESP8266" == type) pinList = esp8266pirpins; - else if ("ESP32-S3" == system["chip_model"]) pinList = esp32s3pins; + if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*ENDIF_ESP32*/ for(var i of ["disp_pwr"]) document.getElementsByName(i)[0].checked = obj[i]; @@ -1108,7 +1114,7 @@ document.getElementById("pirPin").append( ml("div", {class: "row mb-3"}, [ ml("div", {class: "col-12 col-sm-3 my-2"}, "PIR sensor"), - ml("div", {class: "col-12 col-sm-9"}, sel("pir_pin", pinList, obj["pir_pin"])) + ml("div", {class: "col-12 col-sm-9"}, sel("pir_pin", ("ESP8266" == type) ? esp8266pirpins : pinList, obj["pir_pin"])) ]) ); diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index ae8d2298..55819f9f 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -356,12 +356,19 @@ getJSON("/grid_info.json").then(data => { var glob = {offs:0, grid:obj.grid, info: data} var content = []; - content.push(ml("div", {class: "row"}, - ml("div", {class: "col my-3"}, ml("h5", {}, getGridType(glob.info.type, getGridIdentifier(glob)) + " (Version " + getGridValue(glob).toString(16) + ")")) - )) - - while((glob.offs*3) < glob.grid.length) { - content.push(parseGridGroup(glob)) + var g = getGridType(glob.info.type, getGridIdentifier(glob)) + if(null === g) { + content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "Unknown Profile")))) + content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "Please open a new issue at https://github.com/lumapu/ahoy and copy the raw data into it.")))) + content.push(ml("div", {class: "row"}, ml("div", {class: "col my-2"}, ml("pre", {}, obj.grid)))) + } else { + content.push(ml("div", {class: "row"}, + ml("div", {class: "col my-3"}, ml("h5", {}, g + " (Version " + getGridValue(glob).toString(16) + ")")) + )) + + while((glob.offs*3) < glob.grid.length) { + content.push(parseGridGroup(glob)) + } } modal("Grid Profile for inverter " + obj.name, ml("div", {}, ml("div", {class: "col mb-2"}, [...content])))