diff --git a/Getting_Started.md b/Getting_Started.md index d66aeca0..e0254c12 100644 --- a/Getting_Started.md +++ b/Getting_Started.md @@ -14,6 +14,8 @@ Hoymiles Inverters | ----- | ----- | ------ | ------- | | ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet | | ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | | +| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | | +| ✔️ | HMT | 1600, 1800, 2250 | | | ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). | ## Table of Contents diff --git a/README.md b/README.md index 73c4184f..e744dde8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Table of approaches: | Board | MI | HM | HMS/HMT | comment | HowTo start | | ------ | -- | -- | ------- | ------- | ---------- | -| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | coming soon✨ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | +| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | ✔️ ✨ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | | [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | | [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | | | [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | diff --git a/User_Manual.md b/User_Manual.md index a7be8f80..1508a60f 100644 --- a/User_Manual.md +++ b/User_Manual.md @@ -48,9 +48,11 @@ The AhoyDTU will publish on the following topics | status code | Remarks | |---|---| -| 0 | not available and not producing | +| 0 | off: not available and not producing | | 1 | available but not producing | | 2 | available and producing | +| 3 | available and was producing | +| 4 | was available | ### `//ch0/#` diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json index 58a2c3c7..e61851fb 100644 --- a/src/.vscode/settings.json +++ b/src/.vscode/settings.json @@ -8,7 +8,7 @@ "files.eol": "\n", "files.trimTrailingWhitespace": true, "diffEditor.ignoreTrimWhitespace": true, - "files.autoSave": "afterDelay", + "files.autoSave": "off", "editor.tabSize": 4, "editor.insertSpaces": true, // `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents. @@ -79,7 +79,8 @@ "mutex": "cpp", "ranges": "cpp", "stop_token": "cpp", - "thread": "cpp" + "thread": "cpp", + "variant": "cpp" }, "cmake.configureOnOpen": false, "editor.formatOnSave": false, diff --git a/src/CHANGES.md b/src/CHANGES.md index 6b97e319..731cd479 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,106 @@ # Development Changes +## 0.7.21 - 2023-07-30 +* fix MqTT YieldDay Total goes to 0 serveral times #1016 + +## 0.7.20 - 2023-07-28 +* merge PR #1048 version and hash in API, fixes #1045 +* fix: no yield day update if yield day reads `0` after inverter reboot (mostly on evening) #848 +* try to fix Wifi override #1047 +* added information after NTP sync to WebUI #1040 + +## 0.7.19 - 2023-07-27 +* next attempt to fix yield day for multiple inverters #1016 +* reduced threshold for inverter state machine from 60min to 15min to go from state `WAS_ON` to `OFF` + +## 0.7.18 - 2023-07-26 +* next attempt to fix yield day for multiple inverters #1016 + +## 0.7.17 - 2023-07-25 +* next attempt to fix yield day for multiple inverters #1016 +* added two more states for the inverter status (also docu) + +## 0.7.16 - 2023-07-24 +* next attempt to fix yield day for multiple inverters #1016 +* fix export settings date #1040 +* fix time on WebUI (timezone was not observed) #913 #1016 + +## 0.7.15 - 2023-07-23 +* add NTP sync interval #1019 +* adjusted range of contrast / luminance setting #1041 +* use only ISO time format in Web-UI #913 + +## 0.7.14 - 2023-07-23 +* fix Contrast for Nokia Display #1041 +* attempt to fix #1016 by improving inverter status +* added option to adjust effiency for yield (day/total) #1028 + +## 0.7.13 - 2023-07-19 +* merged display PR #1027 +* add date, time and version to export json #1024 + +## 0.7.12 - 2023-07-09 +* added inverter status - state-machine #1016 + +## 0.7.11 - 2023-07-09 +* fix MqTT endless loop #1013 + +## 0.7.10 - 2023-07-08 +* fix MqTT endless loop #1013 + +## 0.7.9 - 2023-07-08 +* added 'improve' functions to set wifi password directly with ESP web tools #1014 +* fixed MqTT publish while appling power limit #1013 +* slightly improved HMT live view (Voltage & Current) + +## 0.7.8 - 2023-07-05 +* fix `YieldDay`, `YieldTotal` and `P_AC` in `TotalValues` #929 +* fix some serial debug prints +* merge PR #1005 which fixes issue #889 +* merge homeassistant PR #963 +* merge PR #890 which gives option for scheduled reboot at midnight (default off) + +## 0.7.7 - 2023-07-03 +* attempt to fix MqTT `YieldDay` in `TotalValues` #927 +* attempt to fix MqTT `YieldDay` and `YieldTotal` even if inverters are not completly available #929 +* fix wrong message 'NRF not connected' if it is disabled #1007 + +## 0.7.6 - 2023-06-17 +* fix display of hidden SSID checkbox +* changed yield correction data type to `double`, now decimal places are supported +* corrected name of 0.91" display in settings +* attempt to fix MqTT zero values only if setting is there #980, #957 +* made AP password configurable #951 +* added option to start without time-sync, eg. for AP-only-mode #951 + +## 0.7.5 - 2023-06-16 +* fix yield day reset on midnight #957 +* improved tickers in `app.cpp` + +## 0.7.4 - 2023-06-15 +* fix MqTT `P_AC` send if inverters are available #987 +* fix assignments for HMS 1CH and 2CH devices +* fixed uptime overflow #990 + +## 0.7.3 - 2023-06-09 +* fix hidden SSID scan #983 +* improved NRF24 missing message on home screen #981 +* fix MqTT publishing only updated values #982 + +## 0.7.2 - 2023-06-08 +* fix HMS-800 and HMS-1000 assignments #981 +* make nrf enabled all the time for ESP8266 +* fix menu item `active` highlight for 'API' and 'Doku' +* fix MqTT totals issue #927, #980 +* reduce maximum number of inverters to 4 for ESP8266, increase to 16 for ESP32 + +## 0.7.1 - 2023-06-05 +* enabled power limit control for HMS / HMT devices +* changed NRF24 lib version back to 1.4.5 because of compile problems for EPS8266 + +## 0.7.0 - 2023-06-04 +* HMS / HMT support for ESP32 devices + ## 0.6.15 - 2023-05-25 * improved Prometheus Endpoint PR #958 * fix turn off ePaper only if setting was set #956 diff --git a/src/app.cpp b/src/app.cpp index eaebb8a8..4c1874e0 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -23,12 +23,12 @@ void app::setup() { while (!Serial) yield(); - ah::Scheduler::setup(); resetSystem(); mSettings.setup(); mSettings.getPtr(mConfig); + ah::Scheduler::setup(mConfig->inst.startWithoutTime); DPRINT(DBG_INFO, F("Settings valid: ")); DSERIAL.flush(); if (mSettings.getValid()) @@ -36,8 +36,16 @@ void app::setup() { else DBGPRINTLN(F("false")); - mSys.enableDebug(); - mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); + if(mConfig->nrf.enabled) { + mNrfRadio.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); + mNrfRadio.enableDebug(); + } + #if defined(ESP32) + if(mConfig->cmt.enabled) { + mCmtRadio.setup(mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false); + mCmtRadio.enableDebug(); + } + #endif #ifdef ETHERNET delay(1000); DPRINT(DBG_INFO, F("mEth setup...")); @@ -62,29 +70,34 @@ void app::setup() { #endif #endif /* defined(ETHERNET) */ + mSys.setup(&mTimestamp); mSys.addInverters(&mConfig->inst); - - mPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); + if(mConfig->nrf.enabled) { + mPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); mPayload.enableSerialDebug(mConfig->serial.debug); - mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); + mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); - mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); + mMiPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); mMiPayload.enableSerialDebug(mConfig->serial.debug); - mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); + mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + } - // DBGPRINTLN("--- after payload"); - // DBGPRINTLN(String(ESP.getFreeHeap())); - // DBGPRINTLN(String(ESP.getHeapFragmentation())); - // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); + #if defined(ESP32) + mHmsPayload.setup(this, &mSys, &mCmtRadio, &mStat, 5, &mTimestamp); + mHmsPayload.enableSerialDebug(mConfig->serial.debug); + mHmsPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + #endif - if (!mSys.Radio.isChipConnected()) + if(mConfig->nrf.enabled) { + if (!mNrfRadio.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) mMqttEnabled = (mConfig->mqtt.broker[0] > 0); if (mMqttEnabled) { - mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp); + mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); 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)); mMiPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); @@ -95,7 +108,7 @@ void app::setup() { mWeb.setup(this, &mSys, mConfig); mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); - mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); + mApi.setup(this, &mSys, &mNrfRadio, mWeb.getWebSrvPtr(), mConfig); // Plugins if (mConfig->plugin.display.type != 0) @@ -103,28 +116,29 @@ void app::setup() { mPubSerial.setup(mConfig, &mSys, &mTimestamp); - regularTickers(); - + #if !defined(ETHERNET) + mImprov.setup(this, mConfig->sys.deviceName, mVersion); + #endif - // DBGPRINTLN("--- end setup"); - // DBGPRINTLN(String(ESP.getFreeHeap())); - // DBGPRINTLN(String(ESP.getHeapFragmentation())); - // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); + regularTickers(); } //----------------------------------------------------------------------------- void app::loop(void) { if (mInnerLoopCb) mInnerLoopCb(); + #if !defined(ETHERNET) + mImprov.tickSerial(); + #endif } //----------------------------------------------------------------------------- void app::loopStandard(void) { ah::Scheduler::loop(); - if (mSys.Radio.loop()) { - while (!mSys.Radio.mBufCtrl.empty()) { - packet_t *p = &mSys.Radio.mBufCtrl.front(); + if (mNrfRadio.loop() && mConfig->nrf.enabled) { + while (!mNrfRadio.mBufCtrl.empty()) { + packet_t *p = &mNrfRadio.mBufCtrl.front(); if (mConfig->serial.debug) { DPRINT(DBG_INFO, F("RX ")); @@ -132,7 +146,7 @@ void app::loopStandard(void) { DBGPRINT(F("B Ch")); DBGPRINT(String(p->ch)); DBGPRINT(F(" | ")); - mSys.Radio.dumpBuf(p->packet, p->len); + ah::dumpBuf(p->packet, p->len); } mStat.frmCnt++; @@ -143,14 +157,42 @@ void app::loopStandard(void) { else mMiPayload.add(iv, p); } - mSys.Radio.mBufCtrl.pop(); + mNrfRadio.mBufCtrl.pop(); yield(); } mPayload.process(true); mMiPayload.process(true); } + #if defined(ESP32) + if (mCmtRadio.loop() && mConfig->cmt.enabled) { + while (!mCmtRadio.mBufCtrl.empty()) { + hmsPacket_t *p = &mCmtRadio.mBufCtrl.front(); + if (mConfig->serial.debug) { + DPRINT(DBG_INFO, F("RX ")); + DBGPRINT(String(p->data[0])); + DBGPRINT(F(" RSSI ")); + DBGPRINT(String(p->rssi)); + DBGPRINT(F("dBm | ")); + ah::dumpBuf(&p->data[1], p->data[0]); + } + mStat.frmCnt++; + + Inverter<> *iv = mSys.findInverter(&p->data[2]); + if(NULL != iv) { + if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT)) + mHmsPayload.add(iv, p); + } + mCmtRadio.mBufCtrl.pop(); + yield(); + } + mHmsPayload.process(false); //true + } + #endif mPayload.loop(); mMiPayload.loop(); + #if defined(ESP32) + mHmsPayload.loop(); + #endif if (mMqttEnabled) mMqtt.loop(); @@ -171,6 +213,10 @@ void app::onNetwork(bool gotIp) { regularTickers(); // reinstall regular tickers if (gotIp) { every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); + #if defined(ESP32) + if(mConfig->cmt.enabled) + everySec(std::bind(&CmtRadioType::tickSecond, &mCmtRadio), "tsCmt"); + #endif mMqttReconnect = true; mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); @@ -199,35 +245,10 @@ void app::regularTickers(void) { if (mConfig->plugin.display.type != 0) everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); - + //everySec(std::bind(&Improv::tickSerial, &mImprov), "impro"); // every([this]() {mPayload.simulation();}, 15, "simul"); } -//----------------------------------------------------------------------------- -void app::tickNtpUpdate(void) { - uint32_t nxtTrig = 5; // default: check again in 5 sec - bool isOK = false; -#if defined(ETHERNET) - if (!(isOK = mEth.updateNtpTime())) - once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); -#else /* defined(ETHERNET) */ - isOK = mWifi.getNtpTime(); - if (isOK || mTimestamp != 0) { - this->updateNtp(); - nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min - } - once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); -#endif /* defined(ETHERNET) */ - - // immediately start communicating - // @TODO: leads to reboot loops? not sure #674 - if (isOK && mSendFirst) { - mSendFirst = false; - once(std::bind(&app::tickSend, this), 2, "senOn"); - } - -} - #if defined(ETHERNET) void app::onNtpUpdate(bool gotTime) { @@ -242,30 +263,54 @@ void app::onNtpUpdate(bool gotTime) //----------------------------------------------------------------------------- void app::updateNtp(void) { - if (mMqttReconnect && mMqttEnabled) { - mMqtt.tickerSecond(); - everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); - everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); - } + if (mMqttReconnect && mMqttEnabled) { + mMqtt.tickerSecond(); + everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); + everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); + } - // 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) - everyMin(std::bind(&app::tickMinute, this), "tMin"); - if (mConfig->inst.rstYieldMidNight) { - uint32_t localTime = gTimezone.toLocal(mTimestamp); - uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time - onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); - } + // 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) + everyMin(std::bind(&app::tickMinute, this), "tMin"); + if (mConfig->inst.rstYieldMidNight) { + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); } + } - 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(); + 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(); + } + + mMqttReconnect = false; +} + +//----------------------------------------------------------------------------- +void app::tickNtpUpdate(void) { + uint32_t nxtTrig = 5; // default: check again in 5 sec + #if defined(ETHERNET) + bool isOK = mEth.updateNtpTime(); + #else + bool isOK = mWifi.getNtpTime(); + #endif + if (isOK || mTimestamp != 0) { + this->updateNtp(); + + nxtTrig = isOK ? (mConfig->ntp.interval * 60) : 60; // depending on NTP update success check again in 12h (depends on setting) or in 1 min + + // immediately start communicating + if (isOK && mSendFirst) { + mSendFirst = false; + once(std::bind(&app::tickSend, this), 2, "senOn"); } mMqttReconnect = false; } + once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); +} //----------------------------------------------------------------------------- void app::tickCalcSunrise(void) { @@ -324,43 +369,15 @@ void app::tickComm(void) { //----------------------------------------------------------------------------- void app::tickZeroValues(void) { - Inverter<> *iv; - bool changed = false; - // set values to zero, except yields - for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { - iv = mSys.getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - mPayload.zeroInverterValues(iv); - changed = true; + zeroIvValues(!CHECK_AVAIL, SKIP_YIELD_DAY); } - if(changed) - payloadEventListener(RealTimeRunData_Debug); -} - //----------------------------------------------------------------------------- void app::tickMinute(void) { // only triggered if 'reset values on no avail is enabled' - Inverter<> *iv; - bool changed = false; - // set values to zero, except yields - for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { - iv = mSys.getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) { - mPayload.zeroInverterValues(iv); - changed = true; + zeroIvValues(CHECK_AVAIL, SKIP_YIELD_DAY); } - } - - if(changed) - payloadEventListener(RealTimeRunData_Debug); -} //----------------------------------------------------------------------------- void app::tickMidnight(void) { @@ -369,20 +386,7 @@ void app::tickMidnight(void) { uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); - Inverter<> *iv; - bool changed = false; - // set values to zero, except yield total - for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { - iv = mSys.getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - mPayload.zeroInverterValues(iv, false); - changed = true; - } - - if(changed) - payloadEventListener(RealTimeRunData_Debug); + zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY); if (mMqttEnabled) mMqtt.tickerMidnight(); @@ -390,17 +394,27 @@ void app::tickMidnight(void) { //----------------------------------------------------------------------------- void app::tickSend(void) { - if (!mSys.Radio.isChipConnected()) { + if(mConfig->nrf.enabled) { + if(!mNrfRadio.isChipConnected()) { DPRINTLN(DBG_WARN, F("NRF24 not connected!")); return; } + } if (mIVCommunicationOn) { - if (!mSys.Radio.mBufCtrl.empty()) { + if (!mNrfRadio.mBufCtrl.empty()) { if (mConfig->serial.debug) { DPRINT(DBG_DEBUG, F("recbuf not empty! #")); - DBGPRINTLN(String(mSys.Radio.mBufCtrl.size())); + DBGPRINTLN(String(mNrfRadio.mBufCtrl.size())); + } + } + #if defined(ESP32) + if (!mCmtRadio.mBufCtrl.empty()) { + if (mConfig->serial.debug) { + DPRINT(DBG_INFO, F("recbuf not empty! #")); + DBGPRINTLN(String(mCmtRadio.mBufCtrl.size())); } } + #endif int8_t maxLoop = MAX_NUM_INVERTERS; Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); @@ -411,11 +425,19 @@ void app::tickSend(void) { if (NULL != iv) { if (iv->config->enabled) { + if(mConfig->nrf.enabled) { if (iv->ivGen == IV_HM) mPayload.ivSend(iv); - else + else if(iv->ivGen == IV_MI) mMiPayload.ivSend(iv); } + #if defined(ESP32) + if(mConfig->cmt.enabled) { + if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT)) + mHmsPayload.ivSend(iv); + } + #endif + } } } else { if (mConfig->serial.debug) @@ -426,6 +448,51 @@ void app::tickSend(void) { updateLed(); } +//----------------------------------------------------------------------------- +void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { + Inverter<> *iv; + bool changed = false; + // set values to zero, except yields + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + if (!iv->config->enabled) + continue; // skip to next inverter + + if (checkAvail) { + if (!iv->isAvailable()) + continue; + } + + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + uint8_t pos = 0; + for(uint8_t fld = 0; fld < FLD_EVT; fld++) { + switch(fld) { + case FLD_YD: + if(skipYieldDay) + continue; + else + break; + case FLD_YT: + continue; + } + pos = iv->getPosByChFld(ch, fld, rec); + iv->setValue(pos, rec, 0.0f); + } + iv->doCalculations(); + } + changed = true; + } + + if(changed) { + if(mMqttEnabled && !skipYieldDay) + mMqtt.setZeroValuesEnable(); + payloadEventListener(RealTimeRunData_Debug, NULL); + } +} + //----------------------------------------------------------------------------- void app::resetSystem(void) { snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); @@ -477,7 +544,7 @@ void app::updateLed(void) { if (mConfig->led.led0 != 0xff) { Inverter<> *iv = mSys.getInverterByPos(0); if (NULL != iv) { - if (iv->isProducing(mTimestamp)) + if (iv->isProducing()) digitalWrite(mConfig->led.led0, led_on); else digitalWrite(mConfig->led.led0, led_off); diff --git a/src/app.h b/src/app.h index ba2d5f83..81ef7e75 100644 --- a/src/app.h +++ b/src/app.h @@ -9,19 +9,23 @@ #include #include #include -#include #include "appInterface.h" #include "config/settings.h" #include "defines.h" #include "hm/hmPayload.h" #include "hm/hmSystem.h" +#include "hm/hmRadio.h" +#include "hms/hmsRadio.h" +#include "hms/hmsPayload.h" +#include "hm/hmPayload.h" #include "hm/miPayload.h" #include "publisher/pubMqtt.h" #include "publisher/pubSerial.h" #include "utils/crc.h" #include "utils/dbg.h" #include "utils/scheduler.h" +#include "utils/improv.h" #include "web/RestApi.h" #include "web/web.h" #if defined(ETHERNET) @@ -37,10 +41,14 @@ #define ACOS(x) (degrees(acos(x))) typedef HmSystem HmSystemType; -typedef HmPayload PayloadType; -typedef MiPayload MiPayloadType; +typedef HmPayload> PayloadType; +typedef MiPayload> MiPayloadType; +#ifdef ESP32 +typedef CmtRadio> CmtRadioType; +typedef HmsPayload HmsPayloadType; +#endif typedef Web WebType; -typedef RestApi RestApiType; +typedef RestApi> RestApiType; typedef PubMqtt PubMqttType; typedef PubSerial PubSerialType; @@ -63,8 +71,14 @@ class app : public IApp, public ah::Scheduler { void regularTickers(void); void handleIntr(void) { - mSys.Radio.handleIntr(); + mNrfRadio.handleIntr(); + } + + #ifdef ESP32 + void handleHmsIntr(void) { + mCmtRadio.handleIntr(); } + #endif uint32_t getUptime() { return Scheduler::getUptime(); @@ -78,6 +92,10 @@ class app : public IApp, public ah::Scheduler { mShowRebootRequest = true; // only message on index, no reboot mSavePending = true; mSaveReboot = reboot; + if(reboot) { + onNetwork(false); + ah::Scheduler::resetTicker(); + } once(std::bind(&app::tickSave, this), 3, "save"); return true; } @@ -111,8 +129,8 @@ class app : public IApp, public ah::Scheduler { mWifi.scanAvailNetworks(); } - void getAvailNetworks(JsonObject obj) { - mWifi.getAvailNetworks(obj); + bool getAvailNetworks(JsonObject obj) { + return mWifi.getAvailNetworks(obj); } void setOnUpdate() { @@ -177,10 +195,27 @@ class app : public IApp, public ah::Scheduler { return mWeb.isProtected(request); } - uint8_t getIrqPin(void) { + void getNrfRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) { + *sendCnt = mNrfRadio.mSendCnt; + *retransmits = mNrfRadio.mRetransmits; + } + + bool getNrfEnabled(void) { + return mConfig->nrf.enabled; + } + + bool getCmtEnabled(void) { + return mConfig->cmt.enabled; + } + + uint8_t getNrfIrqPin(void) { return mConfig->nrf.pinIrq; } + uint8_t getCmtIrqPin(void) { + return mConfig->cmt.pinIrq; + } + String getTimeStr(uint32_t offset = 0) { char str[10]; if(0 == mTimestamp) @@ -217,17 +252,19 @@ class app : public IApp, public ah::Scheduler { Scheduler::setTimestamp(newTime); } - HmSystemType mSys; - private: + #define CHECK_AVAIL true + #define SKIP_YIELD_DAY true + typedef std::function innerLoopCb; void resetSystem(void); + void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true); - void payloadEventListener(uint8_t cmd) { + void payloadEventListener(uint8_t cmd, Inverter<> *iv) { #if !defined(AP_ONLY) if (mMqttEnabled) - mMqtt.payloadEventListener(cmd); + mMqtt.payloadEventListener(cmd, iv); #endif if(mConfig->plugin.display.type != 0) mDisplay.payloadEventListener(cmd); @@ -270,23 +307,12 @@ class app : public IApp, public ah::Scheduler { 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; + HmSystemType mSys; + HmRadio<> mNrfRadio; + bool mShowRebootRequest; bool mIVCommunicationOn; @@ -300,6 +326,13 @@ class app : public IApp, public ah::Scheduler { PayloadType mPayload; MiPayloadType mMiPayload; PubSerialType mPubSerial; + #if !defined(ETHERNET) + Improv mImprov; + #endif + #ifdef ESP32 + CmtRadioType mCmtRadio; + HmsPayloadType mHmsPayload; + #endif char mVersion[12]; settings mSettings; diff --git a/src/appInterface.h b/src/appInterface.h index fef97acb..a6f85e84 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -34,7 +34,7 @@ class IApp { #if !defined(ETHERNET) virtual void scanAvailNetworks() = 0; - virtual void getAvailNetworks(JsonObject obj) = 0; + virtual bool getAvailNetworks(JsonObject obj) = 0; #endif /* defined(ETHERNET) */ virtual uint32_t getUptime() = 0; @@ -59,6 +59,9 @@ class IApp { virtual uint32_t getMqttTxCnt() = 0; virtual bool getProtection(AsyncWebServerRequest *request) = 0; + + virtual void getNrfRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) = 0; + //virtual void getCmtRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) = 0; }; #endif /*__IAPP_H__*/ diff --git a/src/config/config.h b/src/config/config.h index b9fc0052..0b44d23d 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -84,7 +84,11 @@ #define PACKET_BUFFER_SIZE 30 // number of configurable inverters -#define MAX_NUM_INVERTERS 10 +#if defined(ESP32) + #define MAX_NUM_INVERTERS 16 +#else + #define MAX_NUM_INVERTERS 4 +#endif // default serial interval #define SERIAL_INTERVAL 5 @@ -105,7 +109,10 @@ #define DEF_MAX_RETRANS_PER_PYLD 5 // number of seconds since last successful response, before inverter is marked inactive -#define INACT_THRES_SEC 300 +#define INVERTER_INACT_THRES_SEC 5*60 + +// number of seconds since last successful response, before inverter is marked offline +#define INVERTER_OFF_THRES_SEC 15*60 // threshold of minimum power on which the inverter is marked as inactive #define INACT_PWR_THRESH 3 diff --git a/src/config/settings.h b/src/config/settings.h index bfdd2fc0..c9d34fc8 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -64,6 +64,7 @@ typedef struct { char adminPwd[PWD_LEN]; uint16_t protectionMask; bool darkMode; + bool schedReboot; #if defined(ETHERNET) // ethernet @@ -72,12 +73,15 @@ typedef struct { // wifi char stationSsid[SSID_LEN]; char stationPwd[PWD_LEN]; + char apPwd[PWD_LEN]; + bool isHidden; #endif /* defined(ETHERNET) */ cfgIp_t ip; } cfgSys_t; typedef struct { + bool enabled; uint16_t sendInterval; uint8_t maxRetransPerPyld; uint8_t pinCs; @@ -89,9 +93,17 @@ typedef struct { uint8_t amplifierPower; } cfgNrf24_t; +typedef struct { + bool enabled; + uint8_t pinCsb; + uint8_t pinFcsb; + uint8_t pinIrq; +} cfgCmt_t; + typedef struct { char addr[NTP_ADDR_LEN]; uint16_t port; + uint16_t interval; // in minutes } cfgNtp_t; typedef struct { @@ -126,9 +138,9 @@ typedef struct { bool enabled; char name[MAX_NAME_LENGTH]; serial_u serial; - uint16_t chMaxPwr[4]; - int32_t yieldCor[4]; // signed YieldTotal correction value - char chName[4][MAX_NAME_LENGTH]; + uint16_t chMaxPwr[6]; + double yieldCor[6]; // YieldTotal correction value + char chName[6][MAX_NAME_LENGTH]; } cfgIv_t; typedef struct { @@ -138,6 +150,8 @@ typedef struct { bool rstYieldMidNight; bool rstValsNotAvail; bool rstValsCommStop; + bool startWithoutTime; + float yieldEffiency; } cfgInst_t; typedef struct { @@ -163,6 +177,7 @@ typedef struct { typedef struct { cfgSys_t sys; cfgNrf24_t nrf; + cfgCmt_t cmt; cfgNtp_t ntp; cfgSun_t sun; cfgSerial_t serial; @@ -257,6 +272,9 @@ class settings { mCfg.valid = true; if(root.containsKey(F("wifi"))) jsonNetwork(root[F("wifi")]); if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]); + #if defined(ESP32) + if(root.containsKey(F("cmt"))) jsonCmt(root[F("cmt")]); + #endif if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]); if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]); if(root.containsKey(F("serial"))) jsonSerial(root[F("serial")]); @@ -281,6 +299,9 @@ class settings { JsonObject root = json.to(); jsonNetwork(root.createNestedObject(F("wifi")), true); jsonNrf(root.createNestedObject(F("nrf")), true); + #if defined(ESP32) + jsonCmt(root.createNestedObject(F("cmt")), true); + #endif jsonNtp(root.createNestedObject(F("ntp")), true); jsonSun(root.createNestedObject(F("sun")), true); jsonSerial(root.createNestedObject(F("serial")), true); @@ -343,7 +364,7 @@ class settings { 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; - + mCfg.sys.schedReboot = false; // restore temp settings #if defined(ETHERNET) memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); @@ -353,6 +374,8 @@ class settings { else { snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); + snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD); + mCfg.sys.isHidden = false; } #endif /* defined(ETHERNET) */ @@ -368,9 +391,16 @@ class settings { mCfg.nrf.pinSclk = DEF_SCLK_PIN; mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; + mCfg.nrf.enabled = true; + + mCfg.cmt.pinCsb = DEF_PIN_OFF; + mCfg.cmt.pinFcsb = DEF_PIN_OFF; + mCfg.cmt.pinIrq = DEF_PIN_OFF; + mCfg.cmt.enabled = false; snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); mCfg.ntp.port = DEF_NTP_PORT; + mCfg.ntp.interval = 720; mCfg.sun.lat = 0.0; mCfg.sun.lon = 0.0; @@ -391,6 +421,8 @@ class settings { mCfg.inst.rstYieldMidNight = false; mCfg.inst.rstValsNotAvail = false; mCfg.inst.rstValsCommStop = false; + mCfg.inst.startWithoutTime = false; + mCfg.inst.yieldEffiency = 0.955f; mCfg.led.led0 = DEF_PIN_OFF; mCfg.led.led1 = DEF_PIN_OFF; @@ -416,11 +448,14 @@ class settings { #if !defined(ETHERNET) obj[F("ssid")] = mCfg.sys.stationSsid; obj[F("pwd")] = mCfg.sys.stationPwd; + obj[F("ap_pwd")] = mCfg.sys.apPwd; + obj[F("hidd")] = (bool) mCfg.sys.isHidden; #endif /* !defined(ETHERNET) */ 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; + obj[F("reb")] = (bool) mCfg.sys.schedReboot; 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); @@ -430,11 +465,14 @@ class settings { #if !defined(ETHERNET) getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN); getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN); + getChar(obj, F("ap_pwd"), mCfg.sys.apPwd, PWD_LEN); + getVal(obj, F("hidd"), &mCfg.sys.isHidden); #endif /* !defined(ETHERNET) */ getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN); getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN); getVal(obj, F("prot_mask"), &mCfg.sys.protectionMask); getVal(obj, F("dark"), &mCfg.sys.darkMode); + getVal(obj, F("reb"), &mCfg.sys.schedReboot); if(obj.containsKey(F("ip"))) ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); if(obj.containsKey(F("mask"))) ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); @@ -458,6 +496,7 @@ class settings { obj[F("mosi")] = mCfg.nrf.pinMosi; obj[F("miso")] = mCfg.nrf.pinMiso; obj[F("pwr")] = mCfg.nrf.amplifierPower; + obj[F("en")] = (bool) mCfg.nrf.enabled; } else { getVal(obj, F("intvl"), &mCfg.nrf.sendInterval); getVal(obj, F("maxRetry"), &mCfg.nrf.maxRetransPerPyld); @@ -468,6 +507,11 @@ class settings { getVal(obj, F("mosi"), &mCfg.nrf.pinMosi); getVal(obj, F("miso"), &mCfg.nrf.pinMiso); getVal(obj, F("pwr"), &mCfg.nrf.amplifierPower); + #if !defined(ESP32) + mCfg.nrf.enabled = true; // ESP8266, read always as enabled + #else + mCfg.nrf.enabled = (bool) obj[F("en")]; + #endif if((obj[F("cs")] == obj[F("ce")])) { mCfg.nrf.pinCs = DEF_CS_PIN; mCfg.nrf.pinCe = DEF_CE_PIN; @@ -479,13 +523,32 @@ class settings { } } + void jsonCmt(JsonObject obj, bool set = false) { + if(set) { + obj[F("csb")] = mCfg.cmt.pinCsb; + obj[F("fcsb")] = mCfg.cmt.pinFcsb; + obj[F("irq")] = mCfg.cmt.pinIrq; + obj[F("en")] = (bool) mCfg.cmt.enabled; + } else { + mCfg.cmt.pinCsb = obj[F("csb")]; + mCfg.cmt.pinFcsb = obj[F("fcsb")]; + mCfg.cmt.pinIrq = obj[F("irq")]; + mCfg.cmt.enabled = (bool) obj[F("en")]; + } + } + void jsonNtp(JsonObject obj, bool set = false) { if(set) { obj[F("addr")] = mCfg.ntp.addr; obj[F("port")] = mCfg.ntp.port; + obj[F("intvl")] = mCfg.ntp.interval; } else { getChar(obj, F("addr"), mCfg.ntp.addr, NTP_ADDR_LEN); getVal(obj, F("port"), &mCfg.ntp.port); + getVal(obj, F("intvl"), &mCfg.ntp.interval); + + if(mCfg.ntp.interval < 5) // minimum 5 minutes + mCfg.ntp.interval = 720; // default -> 12 hours } } @@ -586,12 +649,21 @@ class settings { obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; + obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime; + obj[F("yldEff")] = mCfg.inst.yieldEffiency; } else { getVal(obj, F("en"), &mCfg.inst.enabled); getVal(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight); getVal(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail); getVal(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop); + getVal(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime); + getVal(obj, F("yldEff"), &mCfg.inst.yieldEffiency); + + if(mCfg.inst.yieldEffiency < 0.5) + mCfg.inst.yieldEffiency = 1.0f; + else if(mCfg.inst.yieldEffiency > 1.0f) + mCfg.inst.yieldEffiency = 1.0f; } JsonArray ivArr; @@ -611,7 +683,7 @@ class settings { 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++) { + for(uint8_t i = 0; i < 6; i++) { obj[F("yield")][i] = cfg->yieldCor[i]; obj[F("pwr")][i] = cfg->chMaxPwr[i]; obj[F("chName")][i] = cfg->chName[i]; @@ -620,7 +692,10 @@ class settings { getVal(obj, F("en"), &cfg->enabled); getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH); getVal(obj, F("sn"), &cfg->serial.u64); - for(uint8_t i = 0; i < 4; i++) { + uint8_t size = 4; + if(obj.containsKey(F("pwr"))) + size = obj[F("pwr")].size(); + for(uint8_t i = 0; i < size; i++) { if(obj.containsKey(F("yield"))) cfg->yieldCor[i] = obj[F("yield")][i]; if(obj.containsKey(F("pwr"))) cfg->chMaxPwr[i] = obj[F("pwr")][i]; if(obj.containsKey(F("chName"))) snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); diff --git a/src/defines.h b/src/defines.h index baaea8b6..579a2d8f 100644 --- a/src/defines.h +++ b/src/defines.h @@ -12,8 +12,8 @@ // VERSION //------------------------------------- #define VERSION_MAJOR 0 -#define VERSION_MINOR 6 -#define VERSION_PATCH 15 +#define VERSION_MINOR 7 +#define VERSION_PATCH 21 //------------------------------------- typedef struct { @@ -74,10 +74,6 @@ union serial_u { #define MIN_MQTT_INTERVAL 60 -#define MQTT_STATUS_NOT_AVAIL_NOT_PROD 0 -#define MQTT_STATUS_AVAIL_NOT_PROD 1 -#define MQTT_STATUS_AVAIL_PROD 2 - enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; //------------------------------------- diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index bd12f72b..8b2ca0b7 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://github.com/lumpapu/ahoy +// 2023 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -10,7 +10,7 @@ #include // inverter generations -enum {IV_HM = 0, IV_MI}; +enum {IV_HM = 0, IV_MI, IV_HMS, IV_HMT}; // units enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VAR, UNIT_NONE}; @@ -18,19 +18,22 @@ const char* const units[] = {"V", "A", "W", "Wh", "kWh", "Hz", "°C", "%", "var" // field types enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT, - FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_T, FLD_PF, FLD_EFF, + FLD_UAC, FLD_UAC_1N, FLD_UAC_2N, FLD_UAC_3N, FLD_UAC_12, FLD_UAC_23, FLD_UAC_31, FLD_IAC, + FLD_IAC_1, FLD_IAC_2, FLD_IAC_3, FLD_PAC, FLD_F, FLD_T, FLD_PF, FLD_EFF, FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR, FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_HW_ID, FLD_ACT_ACTIVE_PWR_LIMIT, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE}; const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", - "U_AC", "I_AC", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", + "U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC", + "IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId", "active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"}; const char* const notAvail = "n/a"; const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, - UNIT_V, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, + UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A, + UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE}; // mqtt discovery device classes @@ -53,7 +56,7 @@ const byteAssign_fieldDeviceClass deviceFieldAssignment[] = { {FLD_UAC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT}, {FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT}, {FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT}, - {FLD_F, DEVICE_CLS_FREQ, STATE_CLS_NONE}, + {FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT}, {FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT}, {FLD_PF, DEVICE_CLS_NONE, STATE_CLS_NONE}, {FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE}, @@ -67,9 +70,9 @@ enum {CMD_CALC = 0xffff}; // CH0 is default channel (freq, ac, temp) -enum {CH0 = 0, CH1, CH2, CH3, CH4}; +enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6}; -enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH}; +enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH}; typedef struct { diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index b1390b29..4e6dd77f 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -12,6 +12,7 @@ #endif #include "hmDefines.h" +#include "../hms/hmsDefines.h" #include #include #include "../config/settings.h" @@ -101,6 +102,13 @@ const calcFunc_t calcFunctions[] = { { CALC_IRR_CH, &calcIrradiation } }; +enum class InverterStatus : uint8_t { + OFF, + STARTING, + PRODUCING, + WAS_PRODUCING, + WAS_ON +}; template class Inverter { @@ -122,6 +130,10 @@ class Inverter { //String lastAlarmMsg; bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) + InverterStatus status; // indicates the current inverter status + + static uint32_t *timestamp; // system timestamp + static cfgInst_t *generalConfig; // general inverter configuration from setup Inverter() { ivGen = IV_HM; @@ -134,6 +146,7 @@ class Inverter { //lastAlarmMsg = "nothing"; alarmMesIndex = 0; isConnected = false; + status = InverterStatus::OFF; } ~Inverter() { @@ -265,11 +278,13 @@ class Inverter { val <<= 8; val |= buf[ptr]; } while(++ptr != end); - if (FLD_T == rec->assign[pos].fieldId) { - // temperature is a signed value! - rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div); + if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) { + // temperature, Qvar, and power factor are a signed values + rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div); } else if (FLD_YT == rec->assign[pos].fieldId) { - rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); + rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); + } else if (FLD_YD == rec->assign[pos].fieldId) { + rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency; } else { if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); @@ -318,6 +333,9 @@ class Inverter { } else DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); + + // update status state-machine + isProducing(); } /*inline REC_TYP getPowerLimit(void) { @@ -371,25 +389,42 @@ class Inverter { } } - bool isAvailable(uint32_t timestamp) { - if((timestamp - recordMeas.ts) < INACT_THRES_SEC) - return true; - if((timestamp - recordInfo.ts) < INACT_THRES_SEC) - return true; - if((timestamp - recordConfig.ts) < INACT_THRES_SEC) - return true; - if((timestamp - recordAlarm.ts) < INACT_THRES_SEC) - return true; - return false; + bool isAvailable() { + bool avail = false; + if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC) + avail = true; + if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC) + avail = true; + if((*timestamp - recordConfig.ts) < INVERTER_INACT_THRES_SEC) + avail = true; + if((*timestamp - recordAlarm.ts) < INVERTER_INACT_THRES_SEC) + avail = true; + + if(avail) { + if(status < InverterStatus::PRODUCING) + status = InverterStatus::STARTING; + } else { + if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) + status = InverterStatus::OFF; + else + status = InverterStatus::WAS_ON; + } + + return avail; } - bool isProducing(uint32_t timestamp) { + bool isProducing() { + bool producing = false; DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing")); - if(isAvailable(timestamp)) { - uint8_t pos = getPosByChFld(CH0, FLD_PAC, &recordMeas); - return (getValue(pos, &recordMeas) > INACT_PWR_THRESH); + if(isAvailable()) { + producing = (getChannelFieldValue(CH0, FLD_PAC, &recordMeas) > INACT_PWR_THRESH); + + if(producing) + status = InverterStatus::PRODUCING; + else if(InverterStatus::PRODUCING == status) + status = InverterStatus::WAS_PRODUCING; } - return false; + return producing; } uint16_t getFwVersion() { @@ -421,22 +456,46 @@ class Inverter { switch (cmd) { case RealTimeRunData_Debug: if (INV_TYPE_1CH == type) { - rec->length = (uint8_t)(HM1CH_LIST_LEN); - rec->assign = (byteAssign_t *)hm1chAssignment; - rec->pyldLen = HM1CH_PAYLOAD_LEN; - channels = 1; + if(IV_HM == ivGen) { + rec->length = (uint8_t)(HM1CH_LIST_LEN); + rec->assign = (byteAssign_t *)hm1chAssignment; + rec->pyldLen = HM1CH_PAYLOAD_LEN; + } else if(IV_HMS == ivGen) { + rec->length = (uint8_t)(HMS1CH_LIST_LEN); + rec->assign = (byteAssign_t *)hms1chAssignment; + rec->pyldLen = HMS1CH_PAYLOAD_LEN; + } + channels = 1; } else if (INV_TYPE_2CH == type) { - rec->length = (uint8_t)(HM2CH_LIST_LEN); - rec->assign = (byteAssign_t *)hm2chAssignment; - rec->pyldLen = HM2CH_PAYLOAD_LEN; - channels = 2; + if(IV_HM == ivGen) { + rec->length = (uint8_t)(HM2CH_LIST_LEN); + rec->assign = (byteAssign_t *)hm2chAssignment; + rec->pyldLen = HM2CH_PAYLOAD_LEN; + } else if(IV_HMS == ivGen) { + rec->length = (uint8_t)(HMS2CH_LIST_LEN); + rec->assign = (byteAssign_t *)hms2chAssignment; + rec->pyldLen = HMS2CH_PAYLOAD_LEN; + } + channels = 2; } else if (INV_TYPE_4CH == type) { - rec->length = (uint8_t)(HM4CH_LIST_LEN); - rec->assign = (byteAssign_t *)hm4chAssignment; - rec->pyldLen = HM4CH_PAYLOAD_LEN; - channels = 4; + if(IV_HM == ivGen) { + rec->length = (uint8_t)(HM4CH_LIST_LEN); + rec->assign = (byteAssign_t *)hm4chAssignment; + rec->pyldLen = HM4CH_PAYLOAD_LEN; + } else if(IV_HMS == ivGen) { + rec->length = (uint8_t)(HMS4CH_LIST_LEN); + rec->assign = (byteAssign_t *)hms4chAssignment; + rec->pyldLen = HMS4CH_PAYLOAD_LEN; + } + channels = 4; + } + else if (INV_TYPE_6CH == type) { + rec->length = (uint8_t)(HMT6CH_LIST_LEN); + rec->assign = (byteAssign_t *)hmt6chAssignment; + rec->pyldLen = HMT6CH_PAYLOAD_LEN; + channels = 6; } else { rec->length = 0; @@ -580,6 +639,11 @@ class Inverter { bool mDevControlRequest; // true if change needed }; +template +uint32_t *Inverter::timestamp {0}; +template +cfgInst_t *Inverter::generalConfig {0}; + /** * To calculate values which are not transmitted by the unit there is a generic diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h index 5c66f1f8..2b59cd69 100644 --- a/src/hm/hmPayload.h +++ b/src/hm/hmPayload.h @@ -9,6 +9,7 @@ #include "../utils/dbg.h" #include "../utils/crc.h" #include "../config/config.h" +#include "hmRadio.h" #include typedef struct { @@ -27,18 +28,19 @@ typedef struct { } invPayload_t; -typedef std::function payloadListenerType; +typedef std::function *)> payloadListenerType; typedef std::function alarmListenerType; -template +template class HmPayload { public: HmPayload() {} - void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { mApp = app; mSys = sys; + mRadio = radio; mStat = stat; mMaxRetrans = maxRetransmits; mTimestamp = timestamp; @@ -93,28 +95,6 @@ class HmPayload { notify(0x0b); }*/ - void zeroInverterValues(Inverter<> *iv, bool skipYieldDay = true) { - DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - for(uint8_t ch = 0; ch <= iv->channels; ch++) { - uint8_t pos = 0; - for(uint8_t fld = 0; fld < FLD_EVT; fld++) { - switch(fld) { - case FLD_YD: - if(skipYieldDay) - continue; - else - break; - case FLD_YT: - continue; - } - pos = iv->getPosByChFld(ch, fld, rec); - iv->setValue(pos, rec, 0.0f); - } - iv->doCalculations(); - } - } - void ivSendHighPrio(Inverter<> *iv) { mHighPrioIv = iv; } @@ -163,7 +143,7 @@ class HmPayload { DBGPRINT(F(" power limit ")); DBGPRINTLN(String(iv->powerLimit[0])); } - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); + mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); mPayload[iv->id].txCmd = iv->devControlCmd; //iv->clearCmdQueue(); //iv->enqueCommand(SystemConfigPara); // read back power limit @@ -172,7 +152,7 @@ class HmPayload { DPRINT_IVID(DBG_INFO, iv->id); DBGPRINT(F("prepareDevInformCmd 0x")); DBGHEXLN(cmd); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); + mRadio->prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); mPayload[iv->id].txCmd = cmd; } } @@ -216,7 +196,7 @@ class HmPayload { ok = false; DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("has ")); + DBGPRINT(F(" has ")); if(!ok) DBGPRINT(F("not ")); DBGPRINT(F("accepted power limit set point ")); DBGPRINT(String(iv->powerLimit[0])); @@ -238,7 +218,7 @@ class HmPayload { if (NULL == iv) continue; // skip to next inverter - if (IV_MI == iv->ivGen) // only process HM inverters + if (IV_HM != iv->ivGen) // only process HM inverters continue; // skip to next inverter if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { @@ -261,14 +241,14 @@ class HmPayload { } else if(iv->devControlCmd == ActivePowerContr) { DPRINT_IVID(DBG_INFO, iv->id); DPRINTLN(DBG_INFO, F("retransmit power limit")); - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); + mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); } else { if(false == mPayload[iv->id].gotFragment) { /* DPRINTLN(DBG_WARN, F("nothing received: Request Complete Retransmit")); mPayload[iv->id].txCmd = iv->getQueuedCmd(); DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); */ DPRINT_IVID(DBG_INFO, iv->id); DBGPRINTLN(F("nothing received")); @@ -280,7 +260,7 @@ class HmPayload { DBGPRINT(F("Frame ")); DBGPRINT(String(i + 1)); DBGPRINTLN(F(" missing: Request Retransmit")); - mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); + mRadio->sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); break; // only request retransmit one frame per loop } yield(); @@ -297,7 +277,7 @@ class HmPayload { DPRINT_IVID(DBG_INFO, iv->id); DBGPRINT(F("prepareDevInformCmd 0x")); DBGHEXLN(mPayload[iv->id].txCmd); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); } } else { // payload complete DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); @@ -325,7 +305,7 @@ class HmPayload { DPRINT(DBG_INFO, F("Payload (")); DBGPRINT(String(payloadLen)); DBGPRINT(F("): ")); - mSys->Radio.dumpBuf(payload, payloadLen); + ah::dumpBuf(payload, payloadLen); } if (NULL == rec) { @@ -340,7 +320,7 @@ class HmPayload { yield(); } iv->doCalculations(); - notify(mPayload[iv->id].txCmd); + notify(mPayload[iv->id].txCmd, iv); if(AlarmData == mPayload[iv->id].txCmd) { uint8_t i = 0; @@ -370,9 +350,9 @@ class HmPayload { } private: - void notify(uint8_t val) { + void notify(uint8_t val, Inverter<> *iv) { if(NULL != mCbPayload) - (mCbPayload)(val); + (mCbPayload)(val, iv); } void notify(uint16_t code, uint32_t start, uint32_t endTime) { @@ -425,6 +405,7 @@ class HmPayload { IApp *mApp; HMSYSTEM *mSys; + HMRADIO *mRadio; statistics_t *mStat; uint8_t mMaxRetrans; uint32_t *mTimestamp; diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 35993829..280d1240 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2023 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __RADIO_H__ @@ -24,27 +24,6 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; -//----------------------------------------------------------------------------- -// MACROS -//----------------------------------------------------------------------------- -#define CP_U32_LittleEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[0] = ((v >> 24) & 0xff); \ - b[1] = ((v >> 16) & 0xff); \ - b[2] = ((v >> 8) & 0xff); \ - b[3] = ((v ) & 0xff); \ -}) - -#define CP_U32_BigEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[3] = ((v >> 24) & 0xff); \ - b[2] = ((v >> 16) & 0xff); \ - b[1] = ((v >> 8) & 0xff); \ - b[0] = ((v ) & 0xff); \ -}) - -#define BIT_CNT(x) ((x)<<3) - //----------------------------------------------------------------------------- // HM Radio class //----------------------------------------------------------------------------- @@ -53,7 +32,7 @@ class HmRadio { public: HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { if(mSerialDebug) { - DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); + DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); DBGPRINT(String(CE_PIN)); DBGPRINT(F(", CS_PIN: ")); DBGPRINT(String(CS_PIN)); @@ -105,8 +84,8 @@ class HmRadio { DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; #ifdef ESP32 - #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 - mSpi = new SPIClass(FSPI); + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + mSpi = new SPIClass(HSPI); #else mSpi = new SPIClass(VSPI); #endif @@ -242,7 +221,7 @@ class HmRadio { mTxBuf[18] = (alarmMesId >> 8) & 0xff; mTxBuf[19] = (alarmMesId ) & 0xff; } - sendPacket(invId, 24, isRetransmit, true); + sendPacket(invId, 24, isRetransmit); } void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) { @@ -250,15 +229,6 @@ class HmRadio { sendPacket(invId, 10, isRetransmit, appendCrc16); } - void dumpBuf(uint8_t buf[], uint8_t len) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); - for(uint8_t i = 0; i < len; i++) { - DHEX(buf[i]); - DBGPRINT(" "); - } - DBGPRINTLN(""); - } - uint8_t getDataRate(void) { if(!mNrf24.isChipConnected()) return 3; // unkown @@ -291,17 +261,17 @@ class HmRadio { p.len = len; mNrf24.read(p.packet, len); if (p.packet[0] != 0x00) { - mBufCtrl.push(p); - if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command + mBufCtrl.push(p); + if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received - else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command + else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 && - isLastPackage = true; // response from dev control command + isLastPackage = true; // response from dev control command } } - yield(); - } + yield(); + } return isLastPackage; } @@ -311,7 +281,7 @@ class HmRadio { DHEX(mid); DBGPRINT(F(" pid: ")); DBGHEXLN(pid); - } + } memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); mTxBuf[0] = mid; // message id CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); @@ -344,7 +314,7 @@ class HmRadio { DBGPRINT("B Ch"); DBGPRINT(String(mRfChLst[mTxChIdx])); DBGPRINT(F(" | ")); - dumpBuf(mTxBuf, len); + ah::dumpBuf(mTxBuf, len); } mNrf24.stopListening(); diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index a95d4d24..25d6dd11 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://github.com/lumpapu/ahoy +// 2023 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -7,37 +7,34 @@ #define __HM_SYSTEM_H__ #include "hmInverter.h" -#include "hmRadio.h" template > class HmSystem { public: - HmRadio<> Radio; - HmSystem() {} - void setup() { - mNumInv = 0; - Radio.setup(); - } - - void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin, uint8_t sclkPin, uint8_t mosiPin, uint8_t misoPin) { + void setup(uint32_t *timestamp) { + mInverter[0].timestamp = timestamp; mNumInv = 0; - Radio.setup(ampPwr, irqPin, cePin, csPin, sclkPin, mosiPin, misoPin); } void addInverters(cfgInst_t *config) { + mInverter[0].generalConfig = config; Inverter<> *iv; for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { iv = addInverter(&config->iv[i]); if (0ULL != config->iv[i].serial.u64) { if (NULL != iv) { DPRINT(DBG_INFO, "added inverter "); - if(iv->config->serial.b[5] == 0x11) - DBGPRINT("HM"); - else { + if(iv->config->serial.b[5] == 0x11) { + if((iv->config->serial.b[4] & 0x0f) == 0x04) + DBGPRINT("HMS"); + else + DBGPRINT("HM"); + } else if(iv->config->serial.b[5] == 0x13) + DBGPRINT("HMT"); + else DBGPRINT(((iv->config->serial.b[4] & 0x03) == 0x01) ? " (2nd Gen) " : " (3rd Gen) "); - } DBGPRINTLN(String(iv->config->serial.u64, HEX)); @@ -61,25 +58,40 @@ class HmSystem { DPRINTLN(DBG_VERBOSE, " " + String(p->config->serial.b[4], HEX)); if((p->config->serial.b[5] == 0x11) || (p->config->serial.b[5] == 0x10)) { switch(p->config->serial.b[4]) { + case 0x24: // HMS-500 case 0x22: - case 0x21: p->type = INV_TYPE_1CH; break; + case 0x21: p->type = INV_TYPE_1CH; + break; + + case 0x44: // HMS-1000 case 0x42: - case 0x41: p->type = INV_TYPE_2CH; break; + case 0x41: p->type = INV_TYPE_2CH; + break; + + case 0x64: // HMS-2000 case 0x62: - case 0x61: p->type = INV_TYPE_4CH; break; + case 0x61: p->type = INV_TYPE_4CH; + break; + default: DPRINTLN(DBG_ERROR, F("unknown inverter type")); break; } - if(p->config->serial.b[5] == 0x11) - p->ivGen = IV_HM; + if(p->config->serial.b[5] == 0x11) { + if((p->config->serial.b[4] & 0x0f) == 0x04) + p->ivGen = IV_HMS; + else + p->ivGen = IV_HM; + } else if((p->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM p->ivGen = IV_HM; else // MI 2nd Gen p->ivGen = IV_MI; - } - else if(p->config->serial.u64 != 0ULL) + } else if(p->config->serial.b[5] == 0x13) { + p->ivGen = IV_HMT; + p->type = INV_TYPE_6CH; + } else if(p->config->serial.u64 != 0ULL) DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); p->init(); @@ -124,10 +136,6 @@ class HmSystem { return MAX_NUM_INVERTERS; } - void enableDebug() { - Radio.enableDebug(); - } - private: INVERTERTYPE mInverter[MAX_INVERTER]; uint8_t mNumInv; diff --git a/src/hm/miPayload.h b/src/hm/miPayload.h index fd922c8b..2205ecb9 100644 --- a/src/hm/miPayload.h +++ b/src/hm/miPayload.h @@ -33,15 +33,15 @@ typedef struct { } miPayload_t; -typedef std::function miPayloadListenerType; +typedef std::function *)> miPayloadListenerType; -template +template class MiPayload { public: MiPayload() {} - void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { mApp = app; mSys = sys; mStat = stat; @@ -90,21 +90,21 @@ class MiPayload { DPRINT_IVID(DBG_INFO, iv->id); if (!mPayload[iv->id].gotFragment) { mStat->rxFailNoAnser++; // got nothing - if (mSerialDebug) - DBGPRINTLN(F("enqueued cmd failed/timeout")); + if (mSerialDebug) + DBGPRINTLN(F("enqueued cmd failed/timeout")); } else { mStat->rxFail++; // got "fragments" (part of the required messages) // but no complete set of responses - if (mSerialDebug) { + if (mSerialDebug) { DBGPRINT(F("no complete Payload received! (retransmits: ")); - DBGPRINT(String(mPayload[iv->id].retransmits)); - DBGPRINTLN(F(")")); - } + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); } - iv->setQueuedCmdFinished(); // command failed } + iv->setQueuedCmdFinished(); // command failed } } + } reset(iv->id); mPayload[iv->id].requested = true; @@ -124,7 +124,7 @@ class MiPayload { DBGPRINT(F(" power limit ")); DBGPRINTLN(String(iv->powerLimit[0])); } - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false); + mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false); mPayload[iv->id].txCmd = iv->devControlCmd; mPayload[iv->id].limitrequested = true; @@ -148,10 +148,10 @@ class MiPayload { if (cmd == 0x01 || cmd == SystemConfigPara ) { //0x1 and 0x05 for HM-types cmd = 0x0f; // for MI, these seem to make part of the Polling the device software and hardware version number command cmd2 = cmd == SystemConfigPara ? 0x01 : 0x00; //perhaps we can only try to get second frame? - mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false); + mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false); } else { //mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); - mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false); + mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false); }; mPayload[iv->id].txCmd = cmd; @@ -236,27 +236,27 @@ const byteAssign_t InfoAssignment[] = { mPayload[iv->id].gotFragment = true; if(mSerialDebug) { DPRINT_IVID(DBG_INFO, iv->id); - DPRINT(DBG_INFO,F("HW_VER is ")); - DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25])); + DPRINT(DBG_INFO,F("HW_VER is ")); + DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25])); } } else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10 DPRINT_IVID(DBG_INFO, iv->id); if ( p->packet[9] == 0x01 ) { DBGPRINTLN(F("got 2nd frame (hw info)")); - DPRINT(DBG_INFO,F("HW_PartNo ")); - DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])); + DPRINT(DBG_INFO,F("HW_PartNo ")); + DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])); mPayload[iv->id].gotFragment = true; iv->setValue(iv->getPosByChFld(0, FLD_YT, rec), rec, (float) ((p->packet[20] << 8) + p->packet[21])/1); if(mSerialDebug) { - DPRINT(DBG_INFO,F("HW_FB_TLmValue ")); - DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15])); - DPRINT(DBG_INFO,F("HW_FB_ReSPRT ")); - DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17])); - DPRINT(DBG_INFO,F("HW_GridSamp_ResValule ")); - DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19])); + DPRINT(DBG_INFO,F("HW_FB_TLmValue ")); + DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15])); + DPRINT(DBG_INFO,F("HW_FB_ReSPRT ")); + DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17])); + DPRINT(DBG_INFO,F("HW_GridSamp_ResValule ")); + DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19])); DPRINT(DBG_INFO,F("HW_ECapValue ")); DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21])); - } + } } else { DBGPRINTLN(F("3rd gen. inverter!")); // see table in OpenDTU code, DevInfoParser.cpp devInfo[] } @@ -348,7 +348,7 @@ const byteAssign_t InfoAssignment[] = { DPRINT(DBG_INFO, F("Payload (")); DBGPRINT(String(payloadLen)); DBGPRINT("): "); - mSys->Radio.dumpBuf(payload, payloadLen); + ah::dumpBuf(payload, payloadLen); } if (NULL == rec) { @@ -363,7 +363,7 @@ const byteAssign_t InfoAssignment[] = { yield(); } iv->doCalculations(); - notify(mPayload[iv->id].txCmd); + notify(mPayload[iv->id].txCmd, iv); if(AlarmData == mPayload[iv->id].txCmd) { uint8_t i = 0; @@ -431,7 +431,7 @@ const byteAssign_t InfoAssignment[] = { } else if(iv->devControlCmd == ActivePowerContr) { DPRINT_IVID(DBG_INFO, iv->id); DBGPRINTLN(F("retransmit power limit")); - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false); + mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false); } else { uint8_t cmd = mPayload[iv->id].txCmd; if (mPayload[iv->id].retransmits < mMaxRetrans) { @@ -442,7 +442,7 @@ const byteAssign_t InfoAssignment[] = { mPayload[iv->id].retransmits = mMaxRetrans; } else if ( cmd == 0x0f ) { //hard/firmware request - mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true, false); + mRadio->sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true, false); //iv->setQueuedCmdFinished(); //cmd = iv->getQueuedCmd(); } else { @@ -479,7 +479,7 @@ const byteAssign_t InfoAssignment[] = { } DBGPRINT(F(" 0x")); DBGHEXLN(cmd); - mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true, false); + mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd, true, false); //mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd); yield(); } @@ -497,7 +497,7 @@ const byteAssign_t InfoAssignment[] = { DBGPRINT(F("prepareDevInformCmd 0x")); DBGHEXLN(mPayload[iv->id].txCmd); //mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); - mSys->Radio.sendCmdPacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].txCmd, false, false); + mRadio->sendCmdPacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].txCmd, false, false); } } /*else { // payload complete @@ -556,9 +556,9 @@ const byteAssign_t InfoAssignment[] = { } private: - void notify(uint8_t val) { + void notify(uint8_t val, Inverter<> *iv) { if(NULL != mCbMiPayload) - (mCbMiPayload)(val); + (mCbMiPayload)(val, iv); } void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) { @@ -704,20 +704,20 @@ const byteAssign_t InfoAssignment[] = { } - /* - if(AlarmData == mPayload[iv->id].txCmd) { - uint8_t i = 0; - uint16_t code; - uint32_t start, end; - while(1) { - code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); - if(0 == code) - break; - if (NULL != mCbMiAlarm) - (mCbAlarm)(code, start, end); - yield(); - } - }*/ +/* + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbMiAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + }*/ //if ( mPayload[iv->id].complete || //4ch device if ( p->packet[0] == (0x39 + ALL_FRAMES) || //4ch device - last message @@ -725,12 +725,12 @@ const byteAssign_t InfoAssignment[] = { && mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].stsAB[CH0])) { miComplete(iv); - } + } } void miComplete(Inverter<> *iv) { if ( mPayload[iv->id].complete ) // && iv->type != INV_TYPE_4CH) - return; // if we got second message as well in repreated attempt + return; //if we got second message as well in repreated attempt mPayload[iv->id].complete = true; DPRINT_IVID(DBG_INFO, iv->id); DBGPRINTLN(F("got all msgs")); @@ -752,7 +752,7 @@ const byteAssign_t InfoAssignment[] = { iv->setQueuedCmdFinished(); mStat->rxSuccess++; yield(); - notify(RealTimeRunData_Debug); //iv->type == INV_TYPE_4CH ? 0x36 : 0x09 ); + notify(RealTimeRunData_Debug, iv); //iv->type == INV_TYPE_4CH ? 0x36 : 0x09 ); } bool build(uint8_t id, bool *complete) { @@ -826,6 +826,7 @@ const byteAssign_t InfoAssignment[] = { IApp *mApp; HMSYSTEM *mSys; + HMRADIO *mRadio; statistics_t *mStat; uint8_t mMaxRetrans; uint32_t *mTimestamp; diff --git a/src/hms/cmt2300a.h b/src/hms/cmt2300a.h new file mode 100644 index 00000000..95d62191 --- /dev/null +++ b/src/hms/cmt2300a.h @@ -0,0 +1,437 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __CMT2300A_H__ +#define __CMT2300A_H__ + +#include "esp32_3wSpi.h" + +#define WORK_FREQ_KHZ 865000 // disired work frequency between DTU and + // inverter in kHz +#define HOY_BASE_FREQ_KHZ 860000 // in kHz +#define HOY_MAX_FREQ_KHZ 923500 // 0xFE * 250kHz + Base_freq +#define HOY_BOOT_FREQ_KHZ 868000 // Hoymiles boot/init frequency after power up inverter +#define FREQ_STEP_KHZ 250 // channel step size in kHz +#define FREQ_WARN_MIN_KHZ 863000 // for EU 863 - 870 MHz is allowed +#define FREQ_WARN_MAX_KHZ 870000 // for EU 863 - 870 MHz is allowed + +// detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf + +#define CMT2300A_MASK_CFG_RETAIN 0x10 +#define CMT2300A_MASK_RSTN_IN_EN 0x20 +#define CMT2300A_MASK_LOCKING_EN 0x20 +#define CMT2300A_MASK_CHIP_MODE_STA 0x0F + +#define CMT2300A_CUS_CMT10 0x09 + +#define CMT2300A_CUS_MODE_CTL 0x60 // [7] go_switch + // [6] go_tx + // [5] go_tfs + // [4] go_sleep + // [3] go_rx + // [2] go_rfs + // [1] go_stby + // [0] n/a + +#define CMT2300A_CUS_MODE_STA 0x61 // [3:0] 0x00 IDLE + // 0x01 SLEEP + // 0x02 STBY + // 0x03 RFS + // 0x04 TFS + // 0x05 RX + // 0x06 TX + // 0x08 UNLOCKED/LOW_VDD + // 0x09 CAL +#define CMT2300A_CUS_EN_CTL 0x62 +#define CMT2300A_CUS_FREQ_CHNL 0x63 + +#define CMT2300A_CUS_IO_SEL 0x65 // [5:4] GPIO3 + // 0x00 CLKO + // 0x01 DOUT / DIN + // 0x02 INT2 + // 0x03 DCLK + // [3:2] GPIO2 + // 0x00 INT1 + // 0x01 INT2 + // 0x02 DOUT / DIN + // 0x03 DCLK + // [1:0] GPIO1 + // 0x00 DOUT / DIN + // 0x01 INT1 + // 0x02 INT2 + // 0x03 DCLK + +#define CMT2300A_CUS_INT1_CTL 0x66 // [4:0] INT1_SEL + // 0x00 RX active + // 0x01 TX active + // 0x02 RSSI VLD + // 0x03 Pream OK + // 0x04 SYNC OK + // 0x05 NODE OK + // 0x06 CRC OK + // 0x07 PKT OK + // 0x08 SL TMO + // 0x09 RX TMO + // 0x0A TX DONE + // 0x0B RX FIFO NMTY + // 0x0C RX FIFO TH + // 0x0D RX FIFO FULL + // 0x0E RX FIFO WBYTE + // 0x0F RX FIFO OVF + // 0x10 TX FIFO NMTY + // 0x11 TX FIFO TH + // 0x12 TX FIFO FULL + // 0x13 STATE IS STBY + // 0x14 STATE IS FS + // 0x15 STATE IS RX + // 0x16 STATE IS TX + // 0x17 LED + // 0x18 TRX ACTIVE + // 0x19 PKT DONE + +#define CMT2300A_CUS_INT2_CTL 0x67 // [4:0] INT2_SEL + +#define CMT2300A_CUS_INT_EN 0x68 // [7] SL TMO EN + // [6] RX TMO EN + // [5] TX DONE EN + // [4] PREAM OK EN + // [3] SYNC_OK EN + // [2] NODE OK EN + // [1] CRC OK EN + // [0] PKT DONE EN + +#define CMT2300A_CUS_FIFO_CTL 0x69 // [7] TX DIN EN + // [6:5] TX DIN SEL + // 0x00 SEL GPIO1 + // 0x01 SEL GPIO2 + // 0x02 SEL GPIO3 + // [4] FIFO AUTO CLR DIS + // [3] FIFO TX RD EN + // [2] FIFO RX TX SEL + // [1] FIFO MERGE EN + // [0] SPI FIFO RD WR SEL + +#define CMT2300A_CUS_INT_CLR1 0x6A // clear interrupts Bank1 +#define CMT2300A_CUS_INT_CLR2 0x6B // clear interrupts Bank2 +#define CMT2300A_CUS_FIFO_CLR 0x6C + +#define CMT2300A_CUS_INT_FLAG 0x6D // [7] LBD FLG + // [6] COL ERR FLG + // [5] PKT ERR FLG + // [4] PREAM OK FLG + // [3] SYNC OK FLG + // [2] NODE OK FLG + // [1] CRC OK FLG + // [0] PKT OK FLG + +#define CMT2300A_CUS_RSSI_DBM 0x70 + +#define CMT2300A_GO_SWITCH 0x80 +#define CMT2300A_GO_TX 0x40 +#define CMT2300A_GO_TFS 0x20 +#define CMT2300A_GO_SLEEP 0x10 +#define CMT2300A_GO_RX 0x08 +#define CMT2300A_GO_RFS 0x04 +#define CMT2300A_GO_STBY 0x02 +#define CMT2300A_GO_EEPROM 0x01 + +#define CMT2300A_STA_IDLE 0x00 +#define CMT2300A_STA_SLEEP 0x01 +#define CMT2300A_STA_STBY 0x02 +#define CMT2300A_STA_RFS 0x03 +#define CMT2300A_STA_TFS 0x04 +#define CMT2300A_STA_RX 0x05 +#define CMT2300A_STA_TX 0x06 +#define CMT2300A_STA_EEPROM 0x07 +#define CMT2300A_STA_ERROR 0x08 +#define CMT2300A_STA_CAL 0x09 + +#define CMT2300A_INT_SEL_TX_DONE 0x0A + +#define CMT2300A_MASK_TX_DONE_FLG 0x08 +#define CMT2300A_MASK_PKT_OK_FLG 0x01 + +// default CMT paramters +static uint8_t cmtConfig[0x60] PROGMEM { + // 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm + 0x00, 0x66, 0xEC, 0x1C, 0x70, 0x80, 0x14, 0x08, + 0x11, 0x02, 0x02, 0x00, 0xAE, 0xE0, 0x35, 0x00, + // 0x10 - 0x1f + 0x00, 0xF4, 0x10, 0xE2, 0x42, 0x20, 0x0C, 0x81, + 0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12, // 860MHz as default + // 0x20 - 0x2f + 0xA6, 0xC9, 0x20, 0x20, 0xD2, 0x35, 0x0C, 0x0A, + 0x9F, 0x4B, 0x29, 0x29, 0xC0, 0x14, 0x05, 0x53, + // 0x30 - 0x3f + 0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00, + // 0x40 - 0x4f + 0x00, 0x48, 0x5A, 0x48, 0x4D, 0x01, 0x1D, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x60, + // 0x50 - 0x5f + 0xFF, 0x00, 0x00, 0x1F, 0x10, 0x70, 0x4D, 0x06, + 0x00, 0x07, 0x50, 0x00, 0x42, 0x0C, 0x3F, 0x7F // - TX 13dBm +}; + + +enum {CMT_SUCCESS = 0, CMT_ERR_SWITCH_STATE, CMT_ERR_TX_PENDING, CMT_FIFO_EMPTY, CMT_ERR_RX_IN_FIFO}; + +template +class Cmt2300a { + typedef SPI SpiType; + public: + Cmt2300a() {} + + void setup(uint8_t pinCsb, uint8_t pinFcsb) { + mSpi.setup(pinCsb, pinFcsb); + init(); + } + + void setup() { + mSpi.setup(); + init(); + } + + // call as often as possible + void loop() { + if(mTxPending) { + if(CMT2300A_MASK_TX_DONE_FLG == mSpi.readReg(CMT2300A_CUS_INT_CLR1)) { + if(cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) { + mTxPending = false; + goRx(); + } + } + } + } + + uint8_t goRx(void) { + if(mTxPending) + return CMT_ERR_TX_PENDING; + + if(mInRxMode) + return CMT_SUCCESS; + + mSpi.readReg(CMT2300A_CUS_INT1_CTL); + mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE); + + uint8_t tmp = mSpi.readReg(CMT2300A_CUS_INT_CLR1); + if(0x08 == tmp) // first time after TX a value of 0x08 is read + mSpi.writeReg(CMT2300A_CUS_INT_CLR1, 0x04); + else + mSpi.writeReg(CMT2300A_CUS_INT_CLR1, 0x00); + + if(0x10 == tmp) + mSpi.writeReg(CMT2300A_CUS_INT_CLR2, 0x10); + else + mSpi.writeReg(CMT2300A_CUS_INT_CLR2, 0x00); + + mSpi.writeReg(CMT2300A_CUS_FIFO_CTL, 0x02); + mSpi.writeReg(CMT2300A_CUS_FIFO_CLR, 0x02); + mSpi.writeReg(0x16, 0x0C); // [4:3]: RSSI_DET_SEL, [2:0]: RSSI_AVG_MODE + + if(!cmtSwitchStatus(CMT2300A_GO_RX, CMT2300A_STA_RX)) + return CMT_ERR_SWITCH_STATE; + + mInRxMode = true; + + return CMT_SUCCESS; + } + + uint8_t getRx(uint8_t buf[], uint8_t len, int8_t *rssi) { + if(mTxPending) + return CMT_ERR_TX_PENDING; + + if(0x1b != (mSpi.readReg(CMT2300A_CUS_INT_FLAG) & 0x1b)) + return CMT_FIFO_EMPTY; + + // receive ok (pream, sync, node, crc) + if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) + return CMT_ERR_SWITCH_STATE; + + mSpi.readFifo(buf, len); + *rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128; + + if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP)) + return CMT_ERR_SWITCH_STATE; + + if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) + return CMT_ERR_SWITCH_STATE; + + mInRxMode = false; + mCusIntFlag = mSpi.readReg(CMT2300A_CUS_INT_FLAG); + + return CMT_SUCCESS; + } + + uint8_t tx(uint8_t buf[], uint8_t len) { + if(mTxPending) + return CMT_ERR_TX_PENDING; + + if(mInRxMode) { + mInRxMode = false; + if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) + return CMT_ERR_SWITCH_STATE; + } + + mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE); + + // no data received + mSpi.readReg(CMT2300A_CUS_INT_CLR1); + mSpi.writeReg(CMT2300A_CUS_INT_CLR1, 0x00); + mSpi.writeReg(CMT2300A_CUS_INT_CLR2, 0x00); + + mSpi.writeReg(CMT2300A_CUS_FIFO_CTL, 0x07); + mSpi.writeReg(CMT2300A_CUS_FIFO_CLR, 0x01); + + mSpi.writeReg(0x45, 0x01); + mSpi.writeReg(0x46, len); // payload length + + mSpi.writeFifo(buf, len); + + if(0xff != mRqstCh) { + mCurCh = mRqstCh; + mRqstCh = 0xff; + mSpi.writeReg(CMT2300A_CUS_FREQ_CHNL, mCurCh); + } + + if(!cmtSwitchStatus(CMT2300A_GO_TX, CMT2300A_STA_TX)) + return CMT_ERR_SWITCH_STATE; + + // wait for tx done + mTxPending = true; + + return CMT_SUCCESS; + } + + // initialize CMT2300A, returns true on success + bool reset(void) { + mSpi.writeReg(0x7f, 0xff); // soft reset + delay(30); + + if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) + return false; + + mSpi.writeReg(CMT2300A_CUS_MODE_STA, 0x52); + mSpi.writeReg(0x62, 0x20); + + for(uint8_t i = 0; i < 0x60; i++) { + mSpi.writeReg(i, cmtConfig[i]); + } + + + mSpi.writeReg(CMT2300A_CUS_IO_SEL, 0x20); // -> GPIO3_SEL[1:0] = 0x02 + + // interrupt 1 control selection to TX DONE + if(CMT2300A_INT_SEL_TX_DONE != mSpi.readReg(CMT2300A_CUS_INT1_CTL)) + mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE); + + // select interrupt 2 + if(0x07 != mSpi.readReg(CMT2300A_CUS_INT2_CTL)) + mSpi.writeReg(CMT2300A_CUS_INT2_CTL, 0x07); + + // interrupt enable (TX_DONE, PREAM_OK, SYNC_OK, CRC_OK, PKT_DONE) + mSpi.writeReg(CMT2300A_CUS_INT_EN, 0x3B); + + mSpi.writeReg(0x64, 0x64); + + if(0x00 == mSpi.readReg(CMT2300A_CUS_FIFO_CTL)) + mSpi.writeReg(CMT2300A_CUS_FIFO_CTL, 0x02); // FIFO_MERGE_EN + + if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP)) + return false; + + delayMicroseconds(95); + + if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) + return false; + + if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP)) + return false; + + if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) + return false; + + //switchDtuFreq(WORK_FREQ_KHZ); + + return true; + } + + inline uint8_t freq2Chan(const uint32_t freqKhz) { + if((freqKhz % FREQ_STEP_KHZ) != 0) { + DPRINT(DBG_WARN, F("swtich frequency to ")); + DBGPRINT(String(freqKhz)); + DBGPRINT(F("kHz not possible!")); + return 0xff; // error + // apply the nearest frequency + //freqKhz = (freqKhz + FREQ_STEP_KHZ/2) / FREQ_STEP_KHZ; + //freqKhz *= FREQ_STEP_KHZ; + } + + if((freqKhz < HOY_BASE_FREQ_KHZ) || (freqKhz > HOY_MAX_FREQ_KHZ)) + return 0xff; // error + + if((freqKhz < FREQ_WARN_MIN_KHZ) || (freqKhz > FREQ_WARN_MAX_KHZ)) + DPRINTLN(DBG_WARN, F("Disired frequency is out of EU legal range! (863 - 870MHz)")); + + return (freqKhz - HOY_BASE_FREQ_KHZ) / FREQ_STEP_KHZ; + } + + inline void switchChannel(uint8_t ch) { + mRqstCh = ch; + } + + inline uint32_t getFreqKhz(void) { + if(0xff != mRqstCh) + return HOY_BASE_FREQ_KHZ + (mRqstCh * FREQ_STEP_KHZ); + else + return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ); + } + + private: + void init() { + mTxPending = false; + mInRxMode = false; + mCusIntFlag = 0x00; + mCnt = 0; + mRqstCh = 0xff; + mCurCh = 0x20; + } + + // CMT state machine, wait for next state, true on success + bool cmtSwitchStatus(uint8_t cmd, uint8_t waitFor, uint16_t cycles = 40) { + mSpi.writeReg(CMT2300A_CUS_MODE_CTL, cmd); + while(cycles--) { + yield(); + delayMicroseconds(10); + if(waitFor == (getChipStatus() & waitFor)) + return true; + } + //Serial.println("status wait for: " + String(waitFor, HEX) + " read: " + String(getChipStatus(), HEX)); + return false; + } + + inline bool switchDtuFreq(const uint32_t freqKhz) { + uint8_t toCh = freq2Chan(freqKhz); + if(0xff == toCh) + return false; + + switchChannel(toCh); + + return true; + } + + inline uint8_t getChipStatus(void) { + return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA; + } + + SpiType mSpi; + uint8_t mCnt; + bool mTxPending; + bool mInRxMode; + uint8_t mCusIntFlag; + uint8_t mRqstCh, mCurCh; +}; + +#endif /*__CMT2300A_H__*/ diff --git a/src/hms/esp32_3wSpi.h b/src/hms/esp32_3wSpi.h new file mode 100644 index 00000000..080e2251 --- /dev/null +++ b/src/hms/esp32_3wSpi.h @@ -0,0 +1,189 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __ESP32_3WSPI_H__ +#define __ESP32_3WSPI_H__ + +#include "Arduino.h" +#if defined(ESP32) +#include "driver/spi_master.h" +#include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal + +#if CONFIG_IDF_TARGET_ESP32S3 +#define CLK_PIN 6 +#define MOSI_PIN 5 +#else +#define CLK_PIN 18 +#define MOSI_PIN 23 +#endif + +#define SPI_CLK 1 * 1000 * 1000 // 1MHz + +#define SPI_PARAM_LOCK() \ + do { \ + } while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS) +#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock) + +// for ESP32 this is the so-called HSPI +// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore, +// it is simply the first externally usable hardware SPI master controller +#define SPI_CMT SPI2_HOST + +template //, uint8_t GPIO3_PIN=15> +class esp32_3wSpi { + public: + esp32_3wSpi() { + mInitialized = false; + } + + void setup(uint8_t pinCsb = CSB_PIN, uint8_t pinFcsb = FCSB_PIN) { //, uint8_t pinGpio3 = GPIO3_PIN) { + paramLock = xSemaphoreCreateMutex(); + spi_bus_config_t buscfg = { + .mosi_io_num = MOSI_PIN, + .miso_io_num = -1, // single wire MOSI/MISO + .sclk_io_num = CLK_PIN, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 32, + }; + spi_device_interface_config_t devcfg = { + .command_bits = 1, + .address_bits = 7, + .dummy_bits = 0, + .mode = 0, + .cs_ena_pretrans = 1, + .cs_ena_posttrans = 1, + .clock_speed_hz = SPI_CLK, + .spics_io_num = pinCsb, + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, + .queue_size = 1, + .pre_cb = NULL, + .post_cb = NULL, + }; + + ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED)); + ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg)); + + // FiFo + spi_device_interface_config_t devcfg2 = { + .command_bits = 0, + .address_bits = 0, + .dummy_bits = 0, + .mode = 0, + .cs_ena_pretrans = 2, + .cs_ena_posttrans = (uint8_t)(1 / (SPI_CLK * 10e6 * 2) + 2), // >2 us + .clock_speed_hz = SPI_CLK, + .spics_io_num = pinFcsb, + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, + .queue_size = 1, + .pre_cb = NULL, + .post_cb = NULL, + }; + ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo)); + + esp_rom_gpio_connect_out_signal(MOSI_PIN, spi_periph_signal[SPI_CMT].spid_out, true, false); + delay(100); + + //pinMode(pinGpio3, INPUT); + mInitialized = true; + } + + void writeReg(uint8_t addr, uint8_t reg) { + if(!mInitialized) + return; + + uint8_t tx_data; + tx_data = ~reg; + spi_transaction_t t = { + .cmd = 1, + .addr = (uint64_t)(~addr), + .length = 8, + .tx_buffer = &tx_data, + .rx_buffer = NULL + }; + SPI_PARAM_LOCK(); + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); + SPI_PARAM_UNLOCK(); + delayMicroseconds(100); + } + + uint8_t readReg(uint8_t addr) { + if(!mInitialized) + return 0; + + uint8_t rx_data; + spi_transaction_t t = { + .cmd = 0, + .addr = (uint64_t)(~addr), + .length = 8, + .rxlength = 8, + .tx_buffer = NULL, + .rx_buffer = &rx_data + }; + + SPI_PARAM_LOCK(); + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); + SPI_PARAM_UNLOCK(); + delayMicroseconds(100); + return rx_data; + } + + void writeFifo(uint8_t buf[], uint8_t len) { + if(!mInitialized) + return; + uint8_t tx_data; + + spi_transaction_t t = { + .length = 8, + .tx_buffer = &tx_data, // reference to write data + .rx_buffer = NULL + }; + + SPI_PARAM_LOCK(); + for(uint8_t i = 0; i < len; i++) { + tx_data = ~buf[i]; // negate buffer contents + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); + delayMicroseconds(4); // > 4 us + } + SPI_PARAM_UNLOCK(); + } + + void readFifo(uint8_t buf[], uint8_t len) { + if(!mInitialized) + return; + uint8_t rx_data; + + spi_transaction_t t = { + .length = 8, + .rxlength = 8, + .tx_buffer = NULL, + .rx_buffer = &rx_data + }; + + SPI_PARAM_LOCK(); + for(uint8_t i = 0; i < len; i++) { + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); + delayMicroseconds(4); // > 4 us + buf[i] = rx_data; + } + SPI_PARAM_UNLOCK(); + } + + private: + spi_device_handle_t spi_reg, spi_fifo; + bool mInitialized; + SemaphoreHandle_t paramLock = NULL; +}; +#else + template + class esp32_3wSpi { + public: + esp32_3wSpi() {} + void setup() {} + void loop() {} + }; +#endif + +#endif /*__ESP32_3WSPI_H__*/ diff --git a/src/hms/hmsDefines.h b/src/hms/hmsDefines.h new file mode 100644 index 00000000..7f27a4fd --- /dev/null +++ b/src/hms/hmsDefines.h @@ -0,0 +1,190 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __HMS_DEFINES_H__ +#define __HMS_DEFINES_H__ + +#include "../hm/hmDefines.h" + +//------------------------------------- +// HMS-350, HMS-500 +//------------------------------------- +const byteAssign_t hms1chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 6, 2, 10 }, + { FLD_YT, UNIT_KWH, CH1, 8, 4, 1000 }, + { FLD_YD, UNIT_WH, CH1, 12, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, + + { FLD_UAC, UNIT_V, CH0, 14, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 16, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 18, 2, 10 }, + { FLD_Q, UNIT_VAR, CH0, 20, 2, 10 }, // signed! + { FLD_IAC, UNIT_A, CH0, 22, 2, 100 }, + { FLD_PF, UNIT_NONE, CH0, 24, 2, 1000 }, // signed! + { FLD_T, UNIT_C, CH0, 26, 2, 10 }, // signed! + { FLD_EVT, UNIT_NONE, CH0, 28, 2, 1 }, + + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, + { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, + { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } +}; +#define HMS1CH_LIST_LEN (sizeof(hms1chAssignment) / sizeof(byteAssign_t)) +#define HMS1CH_PAYLOAD_LEN 30 + +//------------------------------------- +// HMS-800, HMS-1000 +//------------------------------------- +const byteAssign_t hms2chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 6, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 10, 2, 10 }, + { FLD_YT, UNIT_KWH, CH1, 14, 4, 1000 }, + { FLD_YD, UNIT_WH, CH1, 22, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH2, 4, 2, 10 }, + { FLD_IDC, UNIT_A, CH2, 8, 2, 100 }, + { FLD_PDC, UNIT_W, CH2, 12, 2, 10 }, + { FLD_YT, UNIT_KWH, CH2, 18, 4, 1000 }, + { FLD_YD, UNIT_WH, CH2, 24, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC }, + + { FLD_UAC, UNIT_V, CH0, 26, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 28, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 30, 2, 10 }, + { FLD_Q, UNIT_VAR, CH0, 32, 2, 10 }, // signed! + { FLD_IAC, UNIT_A, CH0, 34, 2, 100 }, + { FLD_PF, UNIT_NONE, CH0, 36, 2, 1000 }, // signed! + { FLD_T, UNIT_C, CH0, 38, 2, 10 }, // signed! + { FLD_EVT, UNIT_NONE, CH0, 40, 2, 1 }, + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, + { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, + { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } +}; +#define HMS2CH_LIST_LEN (sizeof(hms2chAssignment) / sizeof(byteAssign_t)) +#define HMS2CH_PAYLOAD_LEN 42 + +//------------------------------------- +// HMS-1800, HMS-2000 +//------------------------------------- +const byteAssign_t hms4chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 6, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 10, 2, 10 }, + { FLD_YT, UNIT_KWH, CH1, 14, 4, 1000 }, + { FLD_YD, UNIT_WH, CH1, 22, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH2, 4, 2, 10 }, + { FLD_IDC, UNIT_A, CH2, 8, 2, 100 }, + { FLD_PDC, UNIT_W, CH2, 12, 2, 10 }, + { FLD_YT, UNIT_KWH, CH2, 18, 4, 1000 }, + { FLD_YD, UNIT_WH, CH2, 24, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH3, 26, 2, 10 }, + { FLD_IDC, UNIT_A, CH3, 30, 2, 100 }, + { FLD_PDC, UNIT_W, CH3, 34, 2, 10 }, + { FLD_YT, UNIT_KWH, CH3, 38, 4, 1000 }, + { FLD_YD, UNIT_WH, CH3, 46, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH4, 28, 2, 10 }, + { FLD_IDC, UNIT_A, CH4, 32, 2, 100 }, + { FLD_PDC, UNIT_W, CH4, 36, 2, 10 }, + { FLD_YT, UNIT_KWH, CH4, 42, 4, 1000 }, + { FLD_YD, UNIT_WH, CH4, 48, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC }, + + { FLD_UAC, UNIT_V, CH0, 50, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 52, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 54, 2, 10 }, + { FLD_Q, UNIT_VAR, CH0, 56, 2, 10 }, // signed! + { FLD_IAC, UNIT_A, CH0, 58, 2, 100 }, + { FLD_PF, UNIT_NONE, CH0, 60, 2, 1000 }, // signed! + { FLD_T, UNIT_C, CH0, 62, 2, 10 }, // signed! + { FLD_EVT, UNIT_NONE, CH0, 64, 2, 1 }, + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, + { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, + { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } +}; +#define HMS4CH_LIST_LEN (sizeof(hms4chAssignment) / sizeof(byteAssign_t)) +#define HMS4CH_PAYLOAD_LEN 66 + +//------------------------------------- +// HMT-1800, HMT-2250 +//------------------------------------- +const byteAssign_t hmt6chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 8, 2, 10 }, + { FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 }, + { FLD_YD, UNIT_WH, CH1, 20, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC }, + { FLD_IDC, UNIT_A, CH2, 6, 2, 100 }, + { FLD_PDC, UNIT_W, CH2, 10, 2, 10 }, + { FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 }, + { FLD_YD, UNIT_WH, CH2, 22, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH3, 24, 2, 10 }, + { FLD_IDC, UNIT_A, CH3, 26, 2, 100 }, + { FLD_PDC, UNIT_W, CH3, 30, 2, 10 }, + { FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 }, + { FLD_YD, UNIT_WH, CH3, 42, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC }, + { FLD_IDC, UNIT_A, CH4, 28, 2, 100 }, + { FLD_PDC, UNIT_W, CH4, 32, 2, 10 }, + { FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 }, + { FLD_YD, UNIT_WH, CH4, 44, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH5, 46, 2, 10 }, + { FLD_IDC, UNIT_A, CH5, 48, 2, 100 }, + { FLD_PDC, UNIT_W, CH5, 52, 2, 10 }, + { FLD_YT, UNIT_KWH, CH5, 56, 4, 1000 }, + { FLD_YD, UNIT_WH, CH5, 64, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH5, CALC_IRR_CH, CH5, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH6, CALC_UDC_CH, CH5, CMD_CALC }, + { FLD_IDC, UNIT_A, CH6, 50, 2, 100 }, + { FLD_PDC, UNIT_W, CH6, 54, 2, 10 }, + { FLD_YT, UNIT_KWH, CH6, 60, 4, 1000 }, + { FLD_YD, UNIT_WH, CH6, 66, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH6, CALC_IRR_CH, CH6, CMD_CALC }, + + { FLD_UAC_1N, UNIT_V, CH0, 68, 2, 10 }, + { FLD_UAC_2N, UNIT_V, CH0, 70, 2, 10 }, + { FLD_UAC_3N, UNIT_V, CH0, 72, 2, 10 }, + { FLD_UAC_12, UNIT_V, CH0, 74, 2, 10 }, + { FLD_UAC_23, UNIT_V, CH0, 76, 2, 10 }, + { FLD_UAC_31, UNIT_V, CH0, 78, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 80, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 82, 2, 10 }, + { FLD_Q, UNIT_VAR, CH0, 84, 2, 10 }, + { FLD_IAC_1, UNIT_A, CH0, 86, 2, 100 }, + { FLD_IAC_2, UNIT_A, CH0, 88, 2, 100 }, + { FLD_IAC_3, UNIT_A, CH0, 90, 2, 100 }, + { FLD_PF, UNIT_NONE, CH0, 92, 2, 1000 }, + { FLD_T, UNIT_C, CH0, 94, 2, 10 }, + { FLD_EVT, UNIT_NONE, CH0, 96, 2, 1 }, + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, + { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, + { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } +}; +#define HMT6CH_LIST_LEN (sizeof(hmt6chAssignment) / sizeof(byteAssign_t)) +#define HMT6CH_PAYLOAD_LEN 98 + +#endif /*__HMS_DEFINES_H__*/ diff --git a/src/hms/hmsPayload.h b/src/hms/hmsPayload.h new file mode 100644 index 00000000..5c7a08d2 --- /dev/null +++ b/src/hms/hmsPayload.h @@ -0,0 +1,405 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __HMS_PAYLOAD_H__ +#define __HMS_PAYLOAD_H__ + +#include "../utils/dbg.h" +#include "../utils/crc.h" +#include "../config/config.h" +#include + +#define HMS_TIMEOUT_SEC 30 // 30s * 1000 + +typedef struct { + uint8_t txCmd; + uint8_t txId; + //uint8_t invId; + uint32_t ts; + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + uint8_t maxPackId; + bool lastFound; + uint8_t retransmits; + bool requested; + bool gotFragment; +} hmsPayload_t; + + +typedef std::function *)> payloadListenerType; +typedef std::function alarmListenerType; + + +template +class HmsPayload { + public: + HmsPayload() {} + + void setup(IApp *app, HMSYSTEM *sys, RADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + mApp = app; + mSys = sys; + mRadio = radio; + mStat = stat; + mMaxRetrans = maxRetransmits; + mTimestamp = timestamp; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + reset(i); + mIvCmd56Cnt[i] = 0; + } + mSerialDebug = false; + //mHighPrioIv = NULL; + mCbAlarm = NULL; + mCbPayload = NULL; + //mLastRx = 0; + } + + void enableSerialDebug(bool enable) { + mSerialDebug = enable; + } + + void addPayloadListener(payloadListenerType cb) { + mCbPayload = cb; + } + + void addAlarmListener(alarmListenerType cb) { + mCbAlarm = cb; + } + + void loop() { + /*if(NULL != mHighPrioIv) { + ivSend(mHighPrioIv, true); + mHighPrioIv = NULL; + }*/ + } + + void ivSendHighPrio(Inverter<> *iv) { + //mHighPrioIv = iv; + } + + void ivSend(Inverter<> *iv, bool highPrio = false) { + if ((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) // only process HMS inverters + return; + + if(!highPrio) { + if (mPayload[iv->id].requested) { + if (!mPayload[iv->id].complete) + process(false); // no retransmit + + if (!mPayload[iv->id].complete) { + if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) + mStat->rxFailNoAnser++; // got nothing + else + mStat->rxFail++; // got fragments but not complete response + + iv->setQueuedCmdFinished(); // command failed + if (mSerialDebug) + DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); + /*if (mSerialDebug) { + DPRINT(DBG_INFO, F("(#")); + DBGPRINT(String(iv->id)); + DBGPRINT(F(") no Payload received! (retransmits: ")); + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); + }*/ + } + } + } + + reset(iv->id); + mPayload[iv->id].requested = true; + + yield(); + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + } + + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DBGPRINT(String(iv->devControlCmd, HEX)); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mRadio->sendControlPacket(&iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + //iv->clearCmdQueue(); + //iv->enqueCommand(SystemConfigPara); // read back power limit + } else if(((rec->ts + HMS_TIMEOUT_SEC) < *mTimestamp) && (mIvCmd56Cnt[iv->id] < 3)) { + mRadio->switchFrequency(&iv->radioId.u64, HOY_BOOT_FREQ_KHZ, WORK_FREQ_KHZ); + mIvCmd56Cnt[iv->id]++; + } else { + if(++mIvCmd56Cnt[iv->id] == 10) + mIvCmd56Cnt[iv->id] = 0; + uint8_t cmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(cmd); + mRadio->prepareDevInformCmd(&iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); + mPayload[iv->id].txCmd = cmd; + } + } + + void add(Inverter<> *iv, hmsPacket_t *p) { + if (p->data[1] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command + mPayload[iv->id].txId = p->data[1]; + DPRINTLN(DBG_DEBUG, F("Response from info request received")); + uint8_t *pid = &p->data[10]; + if (*pid == 0x00) { + DPRINT(DBG_DEBUG, F("fragment number zero received and ignored")); + } else { + DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); + if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { + memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->data[11], p->data[0] - 11); + mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->data[0] -11; + mPayload[iv->id].gotFragment = true; + } + + if ((*pid & ALL_FRAMES) == ALL_FRAMES) { + // Last packet + if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { + mPayload[iv->id].maxPackId = (*pid & 0x7f); + if (*pid > 0x81) + mPayload[iv->id].lastFound = true; + } + } + } + } else if (p->data[1] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command + DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); + + mPayload[iv->id].txId = p->data[1]; + iv->clearDevControlRequest(); + + if ((p->data[13] == ActivePowerContr) && (p->data[14] == 0x00)) { + bool ok = true; + if((p->data[11] == 0x00) && (p->data[12] == 0x00)) + mApp->setMqttPowerLimitAck(iv); + else + ok = false; + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F(" has ")); + if(!ok) DBGPRINT(F("not ")); + DBGPRINT(F("accepted power limit set point ")); + DBGPRINT(String(iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(iv->powerLimit[1])); + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // read back power limit + } + iv->devControlCmd = Init; + } + } + + void process(bool retransmit) { + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if ((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) // only process HMS inverters + continue; // skip to next inverter + + if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { + // no processing needed if txId is not 0x95 + mPayload[iv->id].complete = true; + continue; // skip to next inverter + } + + if (!mPayload[iv->id].complete) { + bool crcPass, pyldComplete; + crcPass = build(iv->id, &pyldComplete); + if (!crcPass && !pyldComplete) { // payload not complete + if ((mPayload[iv->id].requested) && (retransmit)) { + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { + // This is required to prevent retransmissions without answer. + DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if(iv->devControlCmd == ActivePowerContr) { + DPRINTLN(DBG_INFO, F("retransmit power limit")); + mRadio->sendControlPacket(&iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); + } else { + if(false == mPayload[iv->id].gotFragment) { + + //DPRINTLN(DBG_WARN, F("nothing received: Request Complete Retransmit")); + //mPayload[iv->id].txCmd = iv->getQueuedCmd(); + //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); + //mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("nothing received")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else { + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { + if (mPayload[iv->id].len[i] == 0) { + DPRINT(DBG_WARN, F("Frame ")); + DBGPRINT(String(i + 1)); + DBGPRINTLN(F(" missing: Request Retransmit")); + //mRadio->sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); + break; // only request retransmit one frame per loop + } + yield(); + } + } + } + } + } + } /*else if(!crcPass && pyldComplete) { // crc error on complete Payload + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINT(DBG_INFO, F("(#")); + DBGPRINT(String(iv->id)); + DBGPRINT(F(") prepareDevInformCmd 0x")); + DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX)); + mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } + }*/ else { // payload complete + DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); + DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX)); + DPRINT(DBG_INFO, F("procPyld: txid: 0x")); + DBGPRINTLN(String(mPayload[iv->id].txId, HEX)); + DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); + record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + mPayload[iv->id].complete = true; + + uint8_t payload[100]; + uint8_t payloadLen = 0; + + memset(payload, 0, 100); + + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + payloadLen += (mPayload[iv->id].len[i]); + yield(); + } + payloadLen -= 2; + + if (mSerialDebug) { + DPRINT(DBG_INFO, F("Payload (")); + DBGPRINT(String(payloadLen)); + DBGPRINT(F("): ")); + ah::dumpBuf(payload, payloadLen); + } + + if (NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + mStat->rxSuccess++; + + rec->ts = mPayload[iv->id].ts; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + notify(mPayload[iv->id].txCmd, iv); + + /*if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + }*/ + } else { + DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); + DBGPRINT(String(rec->pyldLen)); + DBGPRINTLN(F(" bytes")); + mStat->rxFail++; + } + + iv->setQueuedCmdFinished(); + } + } + yield(); + } + } + + private: + void notify(uint8_t val, Inverter<> *iv) { + if(NULL != mCbPayload) + (mCbPayload)(val, iv); + } + + void notify(uint16_t code, uint32_t start, uint32_t endTime) { + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, endTime); + } + + bool build(uint8_t id, bool *complete) { + DPRINTLN(DBG_VERBOSE, F("build")); + uint16_t crc = 0xffff, crcRcv = 0x0000; + if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + + // check if all fragments are there + *complete = true; + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if(mPayload[id].len[i] == 0) + *complete = false; + } + if(!*complete) + return false; + + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if (mPayload[id].len[i] > 0) { + if (i == (mPayload[id].maxPackId - 1)) { + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 1, crc); + crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); + } else + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); + } + yield(); + } + + return (crc == crcRcv) ? true : false; + } + + void reset(uint8_t id) { + DPRINT(DBG_INFO, "resetPayload: id: "); + DBGPRINTLN(String(id)); + memset(&mPayload[id], 0, sizeof(hmsPayload_t)); + //mPayload[id].txCmd = 0; + mPayload[id].gotFragment = false; + //mPayload[id].retransmits = 0; + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + mPayload[id].lastFound = false; + mPayload[id].complete = false; + mPayload[id].requested = false; + mPayload[id].ts = *mTimestamp; + } + + IApp *mApp; + HMSYSTEM *mSys; + RADIO *mRadio; + statistics_t *mStat; + uint8_t mMaxRetrans; + uint32_t *mTimestamp; + //uint32_t mLastRx; + hmsPayload_t mPayload[MAX_NUM_INVERTERS]; + uint8_t mIvCmd56Cnt[MAX_NUM_INVERTERS]; + bool mSerialDebug; + Inverter<> *mHighPrioIv; + + alarmListenerType mCbAlarm; + payloadListenerType mCbPayload; +}; + +#endif /*__HMS_PAYLOAD_H__*/ diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h new file mode 100644 index 00000000..1bce5102 --- /dev/null +++ b/src/hms/hmsRadio.h @@ -0,0 +1,213 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __HMS_RADIO_H__ +#define __HMS_RADIO_H__ + +#include "../utils/dbg.h" +#include "cmt2300a.h" + +typedef struct { + int8_t rssi; + uint8_t data[28]; +} hmsPacket_t; + +#define U32_B3(val) ((uint8_t)((val >> 24) & 0xff)) +#define U32_B2(val) ((uint8_t)((val >> 16) & 0xff)) +#define U32_B1(val) ((uint8_t)((val >> 8) & 0xff)) +#define U32_B0(val) ((uint8_t)((val ) & 0xff)) + +template +class CmtRadio { + typedef SPI SpiType; + typedef Cmt2300a CmtType; + public: + CmtRadio() { + mDtuSn = DTU_SN; + } + + void setup(uint8_t pinCsb, uint8_t pinFcsb, bool genDtuSn = true) { + mCmt.setup(pinCsb, pinFcsb); + reset(genDtuSn); + } + + void setup(bool genDtuSn = true) { + mCmt.setup(); + reset(genDtuSn); + } + + bool loop() { + mCmt.loop(); + + if((!mIrqRcvd) && (!mRqstGetRx)) + return false; + getRx(); + if(CMT_SUCCESS == mCmt.goRx()) { + mIrqRcvd = false; + mRqstGetRx = false; + return true; + } else + return false; + } + + void tickSecond() { + } + + void handleIntr(void) { + mIrqRcvd = true; + } + + void enableDebug() { + mSerialDebug = true; + } + + void sendControlPacket(const uint64_t *ivId, uint8_t cmd, uint16_t *data, bool isRetransmit) { + DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + DBGHEXLN(cmd); + initPacket(ivId, TX_REQ_DEVCONTROL, SINGLE_FRAME); + uint8_t cnt = 10; + + mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor + mTxBuf[cnt++] = 0x00; + if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet + mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit + mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit + mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + } + + sendPacket(cnt, isRetransmit); + } + + bool switchFrequency(const uint64_t *ivId, uint32_t fromkHz, uint32_t tokHz) { + uint8_t fromCh = mCmt.freq2Chan(fromkHz); + uint8_t toCh = mCmt.freq2Chan(tokHz); + + if((0xff == fromCh) || (0xff == toCh)) + return false; + + mCmt.switchChannel(fromCh); + sendSwitchChCmd(ivId, toCh); + mCmt.switchChannel(toCh); + return true; + } + + void prepareDevInformCmd(const uint64_t *ivId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg. + initPacket(ivId, reqfld, ALL_FRAMES); + mTxBuf[10] = cmd; + CP_U32_LittleEndian(&mTxBuf[12], ts); + /*if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { + mTxBuf[18] = (alarmMesId >> 8) & 0xff; + mTxBuf[19] = (alarmMesId ) & 0xff; + }*/ + sendPacket(24, isRetransmit); + } + + void sendPacket(uint8_t len, bool isRetransmit) { + if (len > 14) { + uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); + mTxBuf[len++] = (crc >> 8) & 0xff; + mTxBuf[len++] = (crc ) & 0xff; + } + mTxBuf[len] = ah::crc8(mTxBuf, len); + len++; + + if(mSerialDebug) { + DPRINT(DBG_INFO, F("TX ")); + DBGPRINT(String(mCmt.getFreqKhz()/1000.0f)); + DBGPRINT(F("Mhz | ")); + ah::dumpBuf(mTxBuf, len); + } + + uint8_t status = mCmt.tx(mTxBuf, len); + if(CMT_SUCCESS != status) { + DPRINT(DBG_WARN, F("CMT TX failed, code: ")); + DBGPRINTLN(String(status)); + if(CMT_ERR_RX_IN_FIFO == status) + mIrqRcvd = true; + } + + if(isRetransmit) + mRetransmits++; + else + mSendCnt++; + } + + uint32_t mSendCnt; + uint32_t mRetransmits; + std::queue mBufCtrl; + + private: + inline void reset(bool genDtuSn) { + if(genDtuSn) + generateDtuSn(); + if(!mCmt.reset()) + DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!")); + else + mCmt.goRx(); + + mSendCnt = 0; + mRetransmits = 0; + mSerialDebug = false; + mIrqRcvd = false; + mRqstGetRx = false; + } + + inline void sendSwitchChCmd(const uint64_t *ivId, uint8_t ch) { + /** ch: + * 0x00: 860.00 MHz + * 0x01: 860.25 MHz + * 0x02: 860.50 MHz + * ... + * 0x14: 865.00 MHz + * ... + * 0x28: 870.00 MHz + * */ + initPacket(ivId, 0x56, 0x02); + mTxBuf[10] = 0x15; + mTxBuf[11] = 0x21; + mTxBuf[12] = ch; + mTxBuf[13] = 0x14; + sendPacket(14, false); + mRqstGetRx = true; + } + + void initPacket(const uint64_t *ivId, uint8_t mid, uint8_t pid) { + mTxBuf[0] = mid; + CP_U32_BigEndian(&mTxBuf[1], (*ivId) >> 8); + CP_U32_LittleEndian(&mTxBuf[5], mDtuSn); + mTxBuf[9] = pid; + memset(&mTxBuf[10], 0x00, 17); + } + + inline void generateDtuSn(void) { + uint32_t chipID = 0; + #ifdef ESP32 + uint64_t MAC = ESP.getEfuseMac(); + chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); + #endif + mDtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal + for(int i = 0; i < 7; i++) { + mDtuSn |= (chipID % 10) << (i * 4); + chipID /= 10; + } + } + + inline void getRx(void) { + hmsPacket_t p; + uint8_t status = mCmt.getRx(p.data, 28, &p.rssi); + if(CMT_SUCCESS == status) + mBufCtrl.push(p); + } + + CmtType mCmt; + uint32_t mDtuSn; + uint8_t mTxBuf[27]; + bool mSerialDebug; + bool mIrqRcvd; + bool mRqstGetRx; +}; + +#endif /*__HMS_RADIO_H__*/ diff --git a/src/main.cpp b/src/main.cpp index af10abce..c7d2a52d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,8 +5,6 @@ #include "utils/dbg.h" #include "app.h" -#include "config/config.h" - app myApp; @@ -15,13 +13,27 @@ IRAM_ATTR void handleIntr(void) { myApp.handleIntr(); } +//----------------------------------------------------------------------------- +#ifdef ESP32 +IRAM_ATTR void handleHmsIntr(void) { + myApp.handleHmsIntr(); +} +#endif //----------------------------------------------------------------------------- void setup() { myApp.setup(); - // TODO: move to HmRadio - attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING); + if(myApp.getNrfEnabled()) { + if(DEF_PIN_OFF != myApp.getNrfIrqPin()) + attachInterrupt(digitalPinToInterrupt(myApp.getNrfIrqPin()), handleIntr, FALLING); + } + #ifdef ESP32 + if(myApp.getCmtEnabled()) { + if(DEF_PIN_OFF != myApp.getCmtIrqPin()) + attachInterrupt(digitalPinToInterrupt(myApp.getCmtIrqPin()), handleHmsIntr, RISING); + } + #endif } diff --git a/src/platformio.ini b/src/platformio.ini index 46e46498..6a2739c8 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -24,7 +24,7 @@ extra_scripts = lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 @ ^1.4.5 + nrf24/RF24 @ 1.4.5 paulstoffregen/Time @ ^1.6.1 https://github.com/bertmelis/espMqttClient#v1.4.2 bblanchon/ArduinoJson @ ^6.21.2 @@ -37,7 +37,7 @@ lib_deps = platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = -D RELEASE +build_flags = -D RELEASE -std=gnu++17 ;-Wl,-Map,output.map monitor_filters = ;default ; Remove typical terminal control codes from input @@ -50,7 +50,7 @@ monitor_filters = platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = -D RELEASE -DENABLE_PROMETHEUS_EP +build_flags = -D RELEASE -std=gnu++17 -DENABLE_PROMETHEUS_EP monitor_filters = ;default ; Remove typical terminal control codes from input ;time ; Add timestamp with milliseconds for each new line @@ -61,7 +61,7 @@ monitor_filters = platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 +build_flags = -DDEBUG_LEVEL=DBG_DEBUG -std=gnu++17 -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 build_type = debug monitor_filters = ;default ; Remove typical terminal control codes from input @@ -73,7 +73,7 @@ platform = espressif8266 board = esp8285 board_build.ldscript = eagle.flash.1m64.ld board_build.f_cpu = 80000000L -build_flags = -D RELEASE +build_flags = -D RELEASE -std=gnu++17 monitor_filters = ;default ; Remove typical terminal control codes from input time ; Add timestamp with milliseconds for each new line diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index ba187c7d..2340fd42 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -10,12 +10,15 @@ #include "Display_Mono_128X32.h" #include "Display_Mono_128X64.h" #include "Display_Mono_84X48.h" +#include "Display_Mono_64X48.h" #include "Display_ePaper.h" template class Display { public: - Display() {} + Display() { + mMono = NULL; + } void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { mCfg = cfg; @@ -25,31 +28,28 @@ class Display { mLoopCnt = 0; mVersion = version; - if (mCfg->type == 0) - return; + switch (mCfg->type) { + case 0: mMono = NULL; break; + case 1: // fall-through + case 2: mMono = new DisplayMono128X64(); break; + case 3: mMono = new DisplayMono84X48(); break; + case 4: mMono = new DisplayMono128X32(); break; + case 5: mMono = new DisplayMono64X48(); break; - if ((0 < mCfg->type) && (mCfg->type < 10)) { - switch (mCfg->type) { - case 2: - case 1: - default: - mMono = new DisplayMono128X64(); - break; - case 3: - mMono = new DisplayMono84X48(); - break; - case 4: - mMono = new DisplayMono128X32(); - break; - } - mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); - mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); - } else if (mCfg->type >= 10) { #if defined(ESP32) - mRefreshCycle = 0; - mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline); - mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + case 10: + mMono = NULL; // ePaper does not use this + mRefreshCycle = 0; + mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline); + mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + break; #endif + + default: mMono = NULL; break; + } + if(mMono) { + mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); + mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); } } @@ -89,7 +89,7 @@ class Display { if (iv == NULL) continue; - if (iv->isProducing(*mUtcTs)) + if (iv->isProducing()) isprod++; totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); @@ -97,14 +97,16 @@ class Display { totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); } - if ((0 < mCfg->type) && (mCfg->type < 10) && (mMono != NULL)) { + if (mMono ) { mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod); - } else if (mCfg->type >= 10) { + } #if defined(ESP32) + else if (mCfg->type == 10) { + mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); mRefreshCycle++; -#endif } +#endif #if defined(ESP32) if (mRefreshCycle > 480) { diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index ed9154af..42eea5f3 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -35,8 +35,8 @@ class DisplayMono { uint8_t mLoopCnt; uint32_t* mUtcTs; - uint8_t mLineXOffsets[5]; - uint8_t mLineYOffsets[5]; + uint8_t mLineXOffsets[5] = {}; + uint8_t mLineYOffsets[5] = {}; uint16_t mDispY; diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index 9d5ade7e..e9e09d28 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -21,8 +21,6 @@ class DisplayMono128X32 : public DisplayMono { void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { - if((0 == type) || (type > 4)) - return; u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); mType = type; diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 3d4f91ee..a828816c 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -19,8 +19,6 @@ class DisplayMono128X64 : public DisplayMono { } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { - if((0 == type) || (type > 4)) - return; u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); mType = type; @@ -65,8 +63,7 @@ class DisplayMono128X64 : public DisplayMono { mDisplay->clearBuffer(); // set Contrast of the Display to raise the lifetime - if (3 != mType) - mDisplay->setContrast(mLuminance); + mDisplay->setContrast(mLuminance); if ((totalPower > 0) && (isprod > 0)) { mTimeout = DISP_DEFAULT_TIMEOUT; diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h new file mode 100644 index 00000000..8c355322 --- /dev/null +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -0,0 +1,134 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono64X48 : public DisplayMono { + public: + DisplayMono64X48() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = false; + mLuminance = 20; + mExtra = 0; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + + // Wemos OLed Shield is not defined in u8 lib -> use nearest compatible + mDisplay = new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data); + + mUtcTs = utcTs; + + mDisplay->begin(); + calcLinePositions(); + + mDisplay->clearBuffer(); + mDisplay->setContrast(mLuminance); + + printText("AHOY!", 0); + printText("ahoydtu.de", 1); + printText(version, 2); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "T: %4.0f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mExtra++; + } + + private: + void calcLinePositions() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff; + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_fur11_tf); + break; + case 1: + case 2: + mDisplay->setFont(u8g2_font_6x10_tf); + break; + case 3: + mDisplay->setFont(u8g2_font_4x6_tr); + break; + case 4: + mDisplay->setFont(u8g2_font_4x6_tr); + break; + } + } + + void printText(const char *text, uint8_t line) { + uint8_t dispX = 0; //small display, use all we have + dispX += (mEnScreenSaver) ? (mExtra % 4) : 0; + setFont(line); + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index 82aa83fa..490fdf9e 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -20,8 +20,6 @@ class DisplayMono84X48 : public DisplayMono { } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { - if((0 == type) || (type > 4)) - return; u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); mType = type; @@ -33,8 +31,8 @@ class DisplayMono84X48 : public DisplayMono { calcLinePositions(); mDisplay->clearBuffer(); - if (3 != mType) - mDisplay->setContrast(mLuminance); + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0); printText("ahoydtu.de", 2); printText(version, 3); @@ -58,8 +56,7 @@ class DisplayMono84X48 : public DisplayMono { mDisplay->clearBuffer(); // set Contrast of the Display to raise the lifetime - if (3 != mType) - mDisplay->setContrast(mLuminance); + mDisplay->setContrast(mLuminance); if ((totalPower > 0) && (isprod > 0)) { mTimeout = DISP_DEFAULT_TIMEOUT; @@ -95,7 +92,7 @@ class DisplayMono84X48 : public DisplayMono { mDisplay->sendBuffer(); - mExtra = 1; + mExtra++; } private: diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index 924961a3..74000180 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -26,7 +26,7 @@ DisplayEPaper::DisplayEPaper() { void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char *version) { mUtcTs = utcTs; - if (type > 9) { + if (type == 10) { Serial.begin(115200); _display = new GxEPD2_BW(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); hspi.begin(_SCK, _BUSY, _MOSI, _CS); diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 1f8519f5..fa12e969 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -49,19 +49,21 @@ class PubMqtt { mRxCnt = 0; mTxCnt = 0; mSubscriptionCb = NULL; - memset(mLastIvState, MQTT_STATUS_NOT_AVAIL_NOT_PROD, MAX_NUM_INVERTERS); + memset(mLastIvState, (uint8_t)InverterStatus::OFF, MAX_NUM_INVERTERS); memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4); mLastAnyAvail = false; + mZeroValues = false; } ~PubMqtt() { } - void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) { + void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) { mCfgMqtt = cfg_mqtt; mDevName = devName; mVersion = version; mSys = sys; mUtcTimestamp = utcTs; + mUptime = uptime; mIntervalTimeout = 1; mSendIvData.setup(sys, utcTs, &mSendList); @@ -119,14 +121,14 @@ class PubMqtt { else { // send mqtt data in a fixed interval if(mIntervalTimeout == 0) { mIntervalTimeout = mCfgMqtt->interval; - mSendList.push(RealTimeRunData_Debug); + mSendList.push(sendListCmdIv(RealTimeRunData_Debug, NULL)); sendIvData(); } } } void tickerMinute() { - snprintf(mVal, 40, "%ld", millis() / 1000); + snprintf(mVal, 40, "%d", (*mUptime)); publish(subtopics[MQTT_UPTIME], mVal); publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str()); publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str()); @@ -165,10 +167,10 @@ class PubMqtt { publish(mSubTopic, mVal, true); } - void payloadEventListener(uint8_t cmd) { + void payloadEventListener(uint8_t cmd, Inverter<> *iv) { if(mClient.connected()) { // prevent overflow if MQTT broker is not reachable but set if((0 == mCfgMqtt->interval) || (RealTimeRunData_Debug != cmd)) // no interval or no live data - mSendList.push(cmd); + mSendList.push(sendListCmdIv(cmd, iv)); } } @@ -239,6 +241,10 @@ class PubMqtt { } } + void setZeroValuesEnable(void) { + mZeroValues = true; + } + private: void onConnect(bool sessionPreset) { DPRINTLN(DBG_INFO, F("MQTT connected")); @@ -308,7 +314,7 @@ class PubMqtt { delete[] pyld; } - const char *p = topic; + const char *p = topic + strlen(mCfgMqtt->topic); uint8_t pos = 0; uint8_t elm = 0; char tmp[30]; @@ -482,24 +488,22 @@ class PubMqtt { rec = iv->getRecordStruct(RealTimeRunData_Debug); // inverter status - uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; - if (iv->isAvailable(*mUtcTimestamp)) { + iv->isProducing(); // recalculate status + if (iv->isAvailable()) anyAvail = true; - status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD; - } else // inverter is enabled but not available allAvail = false; - if(mLastIvState[id] != status) { + if(mLastIvState[id] != iv->status) { // if status changed from producing to not producing send last data immediately - if (MQTT_STATUS_AVAIL_PROD == mLastIvState[id]) + if (InverterStatus::WAS_PRODUCING == mLastIvState[id]) sendData(iv, RealTimeRunData_Debug); - mLastIvState[id] = status; + mLastIvState[id] = iv->status; changed = true; snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); - snprintf(mVal, 40, "%d", status); + snprintf(mVal, 40, "%d", (uint8_t)iv->status); publish(mSubTopic, mVal, true); snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name); @@ -545,7 +549,7 @@ class PubMqtt { switch (rec->assign[i].fieldId) { case FLD_YT: case FLD_YD: - if ((rec->assign[i].ch == CH0) && (!iv->isProducing(*mUtcTimestamp))) // avoids returns to 0 on restart + if ((rec->assign[i].ch == CH0) && (!iv->isProducing())) // avoids returns to 0 on restart continue; retained = true; break; @@ -564,12 +568,13 @@ class PubMqtt { void sendIvData() { bool anyAvail = processIvStatus(); if (mLastAnyAvail != anyAvail) - mSendList.push(RealTimeRunData_Debug); // makes sure that total values are calculated + mSendList.push(sendListCmdIv(RealTimeRunData_Debug, NULL)); // makes sure that total values are calculated if(mSendList.empty()) return; - mSendIvData.start(); + mSendIvData.start(mZeroValues); + mZeroValues = false; mLastAnyAvail = anyAvail; } @@ -582,13 +587,14 @@ class PubMqtt { HMSYSTEM *mSys; PubMqttIvData mSendIvData; - uint32_t *mUtcTimestamp; + uint32_t *mUtcTimestamp, *mUptime; uint32_t mRxCnt, mTxCnt; - std::queue mSendList; + std::queue mSendList; std::queue mAlarmList; subscriptionCb mSubscriptionCb; bool mLastAnyAvail; - uint8_t mLastIvState[MAX_NUM_INVERTERS]; + bool mZeroValues; + InverterStatus mLastIvState[MAX_NUM_INVERTERS]; uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; uint16_t mIntervalTimeout; diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 947cd2f4..8de300ca 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -12,14 +12,21 @@ typedef std::function pubMqttPublisherType; +struct sendListCmdIv { + uint8_t cmd; + Inverter<> *iv; + sendListCmdIv(uint8_t a, Inverter<> *i) : cmd(a), iv(i) {} +}; + template class PubMqttIvData { public: - void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue *sendList) { + void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue *sendList) { mSys = sys; mUtcTimestamp = utcTs; mSendList = sendList; mState = IDLE; + mZeroValues = false; memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4); mRTRDataHasBeenSent = false; @@ -36,10 +43,11 @@ class PubMqttIvData { yield(); } - bool start(void) { + bool start(bool zeroValues = false) { if(IDLE != mState) return false; + mZeroValues = zeroValues; mRTRDataHasBeenSent = false; mState = START; return true; @@ -59,10 +67,14 @@ class PubMqttIvData { void stateStart() { mLastIvId = 0; + mTotalFound = false; + mSendTotalYd = true; + mAllTotalFound = true; if(!mSendList->empty()) { - mCmd = mSendList->front(); + mCmd = mSendList->front().cmd; + mIvSend = mSendList->front().iv; - if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) { + if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) { // send RealTimeRunData only once mSendTotals = (RealTimeRunData_Debug == mCmd); memset(mTotal, 0, sizeof(float) * 4); mState = FIND_NXT_IV; @@ -88,12 +100,14 @@ class PubMqttIvData { mLastIvId++; mPos = 0; - if(found) + if(found) { + mIv->isProducing(); // recalculate status mState = SEND_DATA; - else if(mSendTotals) + } else if(mSendTotals && mTotalFound) mState = SEND_TOTALS; else { mSendList->pop(); + mZeroValues = false; mState = START; } } @@ -109,40 +123,43 @@ class PubMqttIvData { if(mPos < rec->length) { bool retained = false; if (mCmd == RealTimeRunData_Debug) { - switch (rec->assign[mPos].fieldId) { - case FLD_YT: - case FLD_YD: - if ((rec->assign[mPos].ch == CH0) && (!mIv->isProducing(*mUtcTimestamp))) { // avoids returns to 0 on restart - mPos++; - return; - } - retained = true; - break; - } + if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId)) + retained = true; // calculate total values for RealTimeRunData_Debug if (CH0 == rec->assign[mPos].ch) { - switch (rec->assign[mPos].fieldId) { - case FLD_PAC: - mTotal[0] += mIv->getValue(mPos, rec); - break; - case FLD_YT: - mTotal[1] += mIv->getValue(mPos, rec); - break; - case FLD_YD: - mTotal[2] += mIv->getValue(mPos, rec); - break; - case FLD_PDC: - mTotal[3] += mIv->getValue(mPos, rec); - break; - } + if(mIv->status > InverterStatus::STARTING) { + mTotalFound = true; + switch (rec->assign[mPos].fieldId) { + case FLD_PAC: + mTotal[0] += mIv->getValue(mPos, rec); + break; + case FLD_YT: + mTotal[1] += mIv->getValue(mPos, rec); + break; + case FLD_YD: { + float val = mIv->getValue(mPos, rec); + if(0 == val) // inverter restarted during day + mSendTotalYd = false; + else + mTotal[2] += val; + break; + } + case FLD_PDC: + mTotal[3] += mIv->getValue(mPos, rec); + break; + } + } else + mAllTotalFound = false; } } else mIvLastRTRpub[mIv->id] = lastTs; - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); - snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); - mPublish(mSubTopic, mVal, retained); + if((mIvSend == mIv) || (NULL == mIvSend)) { // send only updated values, or all if the inverter is NULL + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); + snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); + mPublish(mSubTopic, mVal, retained); + } mPos++; } else mState = FIND_NXT_IV; @@ -152,6 +169,7 @@ class PubMqttIvData { void stateSendTotals() { uint8_t fieldId; + mRTRDataHasBeenSent = true; if(mPos < 4) { bool retained = true; switch (mPos) { @@ -161,9 +179,17 @@ class PubMqttIvData { retained = false; break; case 1: + if(!mAllTotalFound) { + mPos++; + return; + } fieldId = FLD_YT; break; case 2: + if((!mAllTotalFound) || (!mSendTotalYd)) { + mPos++; + return; + } fieldId = FLD_YD; break; case 3: @@ -177,10 +203,9 @@ class PubMqttIvData { mPos++; } else { mSendList->pop(); + mZeroValues = false; mState = START; } - - mRTRDataHasBeenSent = true; } HMSYSTEM *mSys; @@ -191,18 +216,19 @@ class PubMqttIvData { uint8_t mCmd; uint8_t mLastIvId; - bool mSendTotals; + bool mSendTotals, mTotalFound, mAllTotalFound, mSendTotalYd; float mTotal[4]; - Inverter<> *mIv; + Inverter<> *mIv, *mIvSend; uint8_t mPos; uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; bool mRTRDataHasBeenSent; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; char mVal[40]; + bool mZeroValues; // makes sure that yield day is sent even if no inverter is online - std::queue *mSendList; + std::queue *mSendList; }; #endif /*__PUB_MQTT_IV_DATA_H__*/ diff --git a/src/publisher/pubSerial.h b/src/publisher/pubSerial.h index 522a227d..34dc64f2 100644 --- a/src/publisher/pubSerial.h +++ b/src/publisher/pubSerial.h @@ -28,7 +28,7 @@ class PubSerial { Inverter<> *iv = mSys->getInverterByPos(id); if (NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (iv->isAvailable(*mUtcTimestamp)) { + if (iv->isAvailable()) { DPRINTLN(DBG_INFO, "Iv: " + String(id)); for (uint8_t i = 0; i < rec->length; i++) { if (0.0f != iv->getValue(i, rec)) { diff --git a/src/utils/dbg.cpp b/src/utils/dbg.cpp index 9f7c9fd3..e7ae4581 100644 --- a/src/utils/dbg.cpp +++ b/src/utils/dbg.cpp @@ -1,3 +1,4 @@ #include "dbg.h" DBG_CB mCb = NULL; +bool mDebugEn = true; diff --git a/src/utils/dbg.h b/src/utils/dbg.h index a18d2f77..6c861329 100644 --- a/src/utils/dbg.h +++ b/src/utils/dbg.h @@ -39,6 +39,7 @@ #ifdef ARDUINO #define DBG_CB std::function extern DBG_CB mCb; + extern bool mDebugEn; inline void registerDebugCb(DBG_CB cb) { mCb = cb; @@ -48,22 +49,28 @@ #define DSERIAL Serial #endif + inline void setDebugEn(bool en) { + mDebugEn = en; + } + //template - inline void DBGPRINT(String str) { DSERIAL.print(str); if(NULL != mCb) mCb(str); } + inline void DBGPRINT(String str, bool ser = true) { if(ser && mDebugEn) DSERIAL.print(str); if(NULL != mCb) mCb(str); } //template - inline void DBGPRINTLN(String str) { DBGPRINT(str); DBGPRINT(F("\r\n")); } - inline void DHEX(uint8_t b) { - if( b<0x10 ) DSERIAL.print(F("0")); - DSERIAL.print(b,HEX); + inline void DBGPRINTLN(String str, bool ser = true) { DBGPRINT(str); DBGPRINT(F("\r\n")); } + inline void DHEX(uint8_t b, bool ser = true) { + if(ser && mDebugEn) { + if( b<0x10 ) DSERIAL.print(F("0")); + DSERIAL.print(b,HEX); + } if(NULL != mCb) { if( b<0x10 ) mCb(F("0")); mCb(String(b, HEX)); } } - inline void DBGHEXLN(uint8_t b) { - DHEX(b); - DBGPRINT(F("\r\n")); + inline void DBGHEXLN(uint8_t b, bool ser = true) { + DHEX(b, ser); + DBGPRINT(F("\r\n"), ser); } /*inline void DHEX(uint16_t b) { if( b<0x10 ) DSERIAL.print(F("000")); diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp index 97d7418b..26cbdad2 100644 --- a/src/utils/helper.cpp +++ b/src/utils/helper.cpp @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- #include "helper.h" +#include "dbg.h" namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr) { @@ -40,6 +41,15 @@ namespace ah { return String(str); } + String getDateTimeStrFile(time_t t) { + char str[20]; + if(0 == t) + sprintf(str, "na"); + else + sprintf(str, "%04d-%02d-%02d_%02d-%02d-%02d", year(t), month(t), day(t), hour(t), minute(t), second(t)); + return String(str); + } + String getTimeStr(time_t t) { char str[9]; if(0 == t) @@ -64,4 +74,12 @@ namespace ah { } return ret; } + + void dumpBuf(uint8_t buf[], uint8_t len) { + for(uint8_t i = 0; i < len; i++) { + DHEX(buf[i]); + DBGPRINT(" "); + } + DBGPRINTLN(""); + } } diff --git a/src/utils/helper.h b/src/utils/helper.h index efd056d2..054ae0c3 100644 --- a/src/utils/helper.h +++ b/src/utils/helper.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de +// 2023 Ahoy, https://ahoydtu.de // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -20,13 +20,31 @@ static Timezone gTimezone(CEST, CET); #define CHECK_MASK(a,b) ((a & b) == b) +#define CP_U32_LittleEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[0] = ((v >> 24) & 0xff); \ + b[1] = ((v >> 16) & 0xff); \ + b[2] = ((v >> 8) & 0xff); \ + b[3] = ((v ) & 0xff); \ +}) + +#define CP_U32_BigEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[3] = ((v >> 24) & 0xff); \ + b[2] = ((v >> 16) & 0xff); \ + b[1] = ((v >> 8) & 0xff); \ + b[0] = ((v ) & 0xff); \ +}) + namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr); void ip2Char(uint8_t ip[], char *str); double round3(double value); String getDateTimeStr(time_t t); + String getDateTimeStrFile(time_t t); String getTimeStr(time_t t); uint64_t Serial2u64(const char *val); + void dumpBuf(uint8_t buf[], uint8_t len); } #endif /*__HELPER_H__*/ diff --git a/src/utils/improv.h b/src/utils/improv.h new file mode 100644 index 00000000..23850ba9 --- /dev/null +++ b/src/utils/improv.h @@ -0,0 +1,221 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __IMPROV_H__ +#define __IMPROV_H__ + +#include +#include +#include "dbg.h" +#include "AsyncJson.h" + +// https://www.improv-wifi.com/serial/ +// https://github.com/jnthas/improv-wifi-demo/blob/main/src/esp32-wifiimprov/esp32-wifiimprov.ino + +// configure ESP through Serial interface +#if !defined(ETHERNET) +class Improv { + public: + void setup(IApp *app, const char *devName, const char *version) { + mApp = app; + mDevName = devName; + mVersion = version; + + mScanRunning = false; + } + + void tickSerial(void) { + if(mScanRunning) + getNetworks(); + + if(Serial.available() == 0) + return; + + uint8_t buf[40]; + uint8_t len = Serial.readBytes(buf, 40); + + if(!checkPaket(&buf[0], len, [this](uint8_t type, uint8_t buf[], uint8_t len) { + parsePayload(type, buf, len); + })) { + DBGPRINTLN(F("check paket failed")); + } + dumpBuf(buf, len); + } + + private: + enum State : uint8_t { + STATE_STOPPED = 0x00, + STATE_AWAITING_AUTHORIZATION = 0x01, + STATE_AUTHORIZED = 0x02, + STATE_PROVISIONING = 0x03, + STATE_PROVISIONED = 0x04, + }; + + enum Command : uint8_t { + UNKNOWN = 0x00, + WIFI_SETTINGS = 0x01, + IDENTIFY = 0x02, + GET_CURRENT_STATE = 0x02, + GET_DEVICE_INFO = 0x03, + GET_WIFI_NETWORKS = 0x04, + BAD_CHECKSUM = 0xFF, + }; + + enum ImprovSerialType : uint8_t { + TYPE_CURRENT_STATE = 0x01, + TYPE_ERROR_STATE = 0x02, + TYPE_RPC = 0x03, + TYPE_RPC_RESPONSE = 0x04 + }; + + void dumpBuf(uint8_t buf[], uint8_t len) { + for(uint8_t i = 0; i < len; i++) { + DHEX(buf[i], false); + DBGPRINT(" ", false); + } + DBGPRINTLN("", false); + } + + inline uint8_t buildChecksum(uint8_t buf[], uint8_t len) { + uint8_t calc = 0; + for(uint8_t i = 0; i < len; i++) { + calc += buf[i]; + } + return calc; + } + + inline bool checkChecksum(uint8_t buf[], uint8_t len) { + /*DHEX(buf[len], false); + DBGPRINT(F(" == "), false); + DBGHEXLN(buildChecksum(buf, len), false);*/ + return ((buildChecksum(buf, len)) == buf[len]); + } + + bool checkPaket(uint8_t buf[], uint8_t len, std::function cb) { + if(len < 11) + return false; + + if(0 != strncmp((char*)buf, "IMPROV", 6)) + return false; + + // verison check (only version 1 is supported!) + if(0x01 != buf[6]) + return false; + + if(!checkChecksum(buf, (9 + buf[8]))) + return false; + + cb(buf[7], &buf[9], buf[8]); + + return true; + } + + uint8_t char2Improv(const char *str, uint8_t buf[]) { + uint8_t len = strlen(str); + buf[0] = len; + for(uint8_t i = 1; i <= len; i++) { + buf[i] = (uint8_t)str[i-1]; + } + return len + 1; + } + + void sendDevInfo(void) { + uint8_t buf[50]; + buf[7] = TYPE_RPC_RESPONSE; + buf[9] = GET_DEVICE_INFO; // repsonse to cmd + uint8_t p = 11; + // firmware name + p += char2Improv("AhoyDTU", &buf[p]); + // firmware version + p += char2Improv(mVersion, &buf[p]); + // chip variant + #if defined(ESP32) + p += char2Improv("ESP32", &buf[p]); + #else + p += char2Improv("ESP8266", &buf[p]); + #endif + // device name + p += char2Improv(mDevName, &buf[p]); + + buf[10] = p - 11; // sub length + buf[8] = p - 9; // paket length + + sendPaket(buf, p); + } + + void getNetworks(void) { + if(!mScanRunning) + mApp->scanAvailNetworks(); + + JsonObject obj; + if(!mApp->getAvailNetworks(obj)) + return; + + mScanRunning = false; + + uint8_t buf[50]; + buf[7] = TYPE_RPC_RESPONSE; + buf[9] = GET_WIFI_NETWORKS; // repsonse to cmd + uint8_t p = 11; + + JsonArray arr = obj[F("networks")]; + for(uint8_t i = 0; i < arr.size(); i++) { + buf[p++] = strlen(arr[i][F("ssid")]); + // ssid + p += char2Improv(arr[i][F("ssid")], &buf[p]); + buf[p++] = String(arr[i][F("rssi")]).length(); + // rssi + p += char2Improv(String(arr[i][F("rssi")]).c_str(), &buf[p]); + + buf[10] = p - 11; // sub length + buf[8] = p - 9; // paket length + + sendPaket(buf, p); + } + } + + void setState(uint8_t state) { + uint8_t buf[20]; + buf[7] = TYPE_CURRENT_STATE; + buf[8] = 0x01; + buf[9] = state; + sendPaket(buf, 10); + } + + void sendPaket(uint8_t buf[], uint8_t len) { + buf[0] = 'I'; + buf[1] = 'M'; + buf[2] = 'P'; + buf[3] = 'R'; + buf[4] = 'O'; + buf[5] = 'V'; + buf[6] = 1; // protocol version + + buf[len] = buildChecksum(buf, len); + len++; + Serial.write(buf, len); + dumpBuf(buf, len); + } + + void parsePayload(uint8_t type, uint8_t buf[], uint8_t len) { + if(TYPE_RPC == type) { + if(GET_CURRENT_STATE == buf[0]) { + setDebugEn(false); + setState(STATE_AUTHORIZED); + } + else if(GET_DEVICE_INFO == buf[0]) + sendDevInfo(); + else if(GET_WIFI_NETWORKS == buf[0]) + getNetworks(); + } + } + + IApp *mApp; + const char *mDevName, *mVersion; + bool mScanRunning; +}; +#endif + +#endif /*__IMPROV_H__*/ diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h index ca250a3e..954ae18a 100644 --- a/src/utils/scheduler.h +++ b/src/utils/scheduler.h @@ -31,9 +31,9 @@ namespace ah { public: Scheduler() {} - void setup() { + void setup(bool directStart) { mUptime = 0; - mTimestamp = 0; + mTimestamp = (directStart) ? 1 : 0; mMax = 0; mPrevMillis = millis(); resetTicker(); @@ -117,6 +117,7 @@ namespace ah { protected: uint32_t mTimestamp; + uint32_t mUptime; private: inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) { @@ -162,7 +163,6 @@ namespace ah { sP mTicker[MAX_NUM_TICKER]; bool mTickerInUse[MAX_NUM_TICKER]; uint32_t mMillis, mPrevMillis, mDiff; - uint32_t mUptime; uint8_t mDiffSeconds; uint8_t mMax; }; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index f0e34222..00f269c1 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -28,9 +28,10 @@ #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 acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, 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() { @@ -41,10 +42,11 @@ class RestApi { nr = 0; } - void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { + void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, AsyncWebServer *srv, settings_t *config) { mApp = app; mSrv = srv; mSys = sys; + mRadio = radio; 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( @@ -188,9 +190,12 @@ class RestApi { response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); } + String filename = ah::getDateTimeStrFile(gTimezone.toLocal(mApp->getTimestamp())); + filename += "_v" + String(mApp->getVersion()); + response->addHeader("Content-Type", "application/octet-stream"); response->addHeader("Content-Description", "File Transfer"); - response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); + response->addHeader("Content-Disposition", "attachment; filename=" + filename + "_ahoy_setup.json"); request->send(response); fp.close(); } @@ -198,6 +203,9 @@ class RestApi { void getGeneric(AsyncWebServerRequest *request, JsonObject obj) { obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); obj[F("ts_uptime")] = mApp->getUptime(); + obj[F("ts_now")] = mApp->getTimestamp(); + obj[F("version")] = String(mApp->getVersion()); + obj[F("build")] = String(AUTO_GIT_HASH); obj[F("menu_prot")] = mApp->getProtection(request); obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); @@ -212,9 +220,12 @@ class RestApi { void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) { #if !defined(ETHERNET) obj[F("ssid")] = mConfig->sys.stationSsid; + obj[F("ap_pwd")] = mConfig->sys.apPwd; + obj[F("hidd")] = mConfig->sys.isHidden; #endif /* !defined(ETHERNET) */ obj[F("device_name")] = mConfig->sys.deviceName; obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; + obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot; obj[F("mac")] = WiFi.macAddress(); obj[F("hostname")] = mConfig->sys.deviceName; @@ -228,7 +239,7 @@ class RestApi { obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb getGeneric(request, obj); - getRadio(obj.createNestedObject(F("radio"))); + getRadioNrf(obj.createNestedObject(F("radio"))); getStatistics(obj.createNestedObject(F("statistics"))); #if defined(ESP32) @@ -300,8 +311,8 @@ class RestApi { 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("tx_cnt")] = mRadio->mSendCnt; + obj[F("retransmits")] = mRadio->mRetransmits; } void getInverterList(JsonObject obj) { @@ -320,7 +331,7 @@ class RestApi { 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_yield_cor")][j] = (double)iv->config->yieldCor[j]; obj2[F("ch_name")][j] = iv->config->chName[j]; obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; } @@ -332,6 +343,8 @@ class RestApi { obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; + obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime; + obj[F("yldEff")] = mConfig->inst.yieldEffiency; } void getInverter(JsonObject obj, uint8_t id) { @@ -345,6 +358,7 @@ class RestApi { obj[F("version")] = String(iv->getFwVersion()); obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); obj[F("ts_last_success")] = rec->ts; + obj[F("generation")] = iv->ivGen; JsonArray ch = obj.createNestedArray("ch"); @@ -352,10 +366,17 @@ class RestApi { uint8_t pos; obj[F("ch_name")][0] = "AC"; JsonArray ch0 = ch.createNestedArray(); + if(IV_HMT == iv->ivGen) { + for (uint8_t fld = 0; fld < sizeof(acListHmt); fld++) { + pos = (iv->getPosByChFld(CH0, acListHmt[fld], rec)); + ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + } else { 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; } + } // DC for(uint8_t j = 0; j < iv->channels; j ++) { @@ -382,6 +403,7 @@ class RestApi { void getNtp(JsonObject obj) { obj[F("addr")] = String(mConfig->ntp.addr); obj[F("port")] = String(mConfig->ntp.port); + obj[F("interval")] = String(mConfig->ntp.interval); } void getSun(JsonObject obj) { @@ -403,11 +425,19 @@ class RestApi { obj[F("led_high_active")] = mConfig->led.led_high_active; } - void getRadio(JsonObject obj) { + void getRadioCmt(JsonObject obj) { + obj[F("csb")] = mConfig->cmt.pinCsb; + obj[F("fcsb")] = mConfig->cmt.pinFcsb; + obj[F("irq")] = mConfig->cmt.pinIrq; + obj[F("en")] = (bool) mConfig->cmt.enabled; + } + + void getRadioNrf(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(); + obj[F("isconnected")] = mRadio->isChipConnected(); + obj[F("DataRate")] = mRadio->getDataRate(); + obj[F("isPVariant")] = mRadio->isPVariant(); + obj[F("en")] = (bool) mConfig->nrf.enabled; } void getSerial(JsonObject obj) { @@ -458,16 +488,16 @@ class RestApi { 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("is_avail")] = iv->isAvailable(); + invObj[F("is_producing")] = iv->isProducing(); 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()) + if(!mRadio->isChipConnected() && mConfig->nrf.enabled) + warn.add(F("your NRF24 module can't be reached, check the wiring, pinout and enable")); + else if(!mRadio->isPVariant() && mConfig->nrf.enabled) warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); if(!mApp->getSettingsValid()) warn.add(F("your settings are invalid")); @@ -496,7 +526,8 @@ class RestApi { getNtp(obj.createNestedObject(F("ntp"))); getSun(obj.createNestedObject(F("sun"))); getPinout(obj.createNestedObject(F("pinout"))); - getRadio(obj.createNestedObject(F("radio"))); + getRadioCmt(obj.createNestedObject(F("radioCmt"))); + getRadioNrf(obj.createNestedObject(F("radioNrf"))); getSerial(obj.createNestedObject(F("serial"))); getStaticIp(obj.createNestedObject(F("static_ip"))); getDisplay(obj.createNestedObject(F("display"))); @@ -620,6 +651,7 @@ class RestApi { IApp *mApp; HMSYSTEM *mSys; + HMRADIO *mRadio; AsyncWebServer *mSrv; settings_t *mConfig; diff --git a/src/web/html/api.js b/src/web/html/api.js index 1dd5422e..299144a9 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -78,8 +78,10 @@ function parseNav(obj) { if(i == 2) continue; var l = document.getElementById("nav"+i); - if(window.location.pathname == "/" + l.href.substring(0, l.href.indexOf("?")).split('/').pop()) - l.classList.add("active"); + if(window.location.pathname == "/" + l.href.substring(0, l.href.indexOf("?")).split('/').pop()) { + if((i != 8 )&& (i != 9)) + l.classList.add("active"); + } if(obj["menu_protEn"]) { if(obj["menu_prot"]) { @@ -117,6 +119,10 @@ function parseRssi(obj) { document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"])); } +function toIsoDateStr(d) { + return new Date(d.getTime() + (d.getTimezoneOffset() * -60000)).toISOString().substring(0, 19).replace('T', ', '); +} + function setHide(id, hide) { var elm = document.getElementById(id); if(hide) { diff --git a/src/web/html/index.html b/src/web/html/index.html index 72537e5e..f2afe3c0 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -62,10 +62,6 @@ getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj)); } - function ts2Span(ts) { - return span(new Date(ts * 1000).toLocaleString('de-DE')); - } - function parseGeneric(obj) { if(exeOnce) parseESP(obj); @@ -88,8 +84,12 @@ + ("0"+min).substr(-2) + ":" + ("0"+sec).substr(-2); var dSpan = document.getElementById("date"); - if(0 != obj["ts_now"]) - dSpan.innerHTML = date.toLocaleString('de-DE'); + if(0 != obj["ts_now"]) { + if(obj["ts_now"] < 1680000000) + setTime(); + else + dSpan.innerHTML = toIsoDateStr(date); + } else { dSpan.innerHTML = ""; var e = inp("set", "sync from browser", 0, ["btn"], "set", "button"); @@ -153,7 +153,7 @@ if(false == i["is_avail"]) { if(i["ts_last_success"] > 0) { var date = new Date(i["ts_last_success"] * 1000); - p.append(span("-> last successful transmission: " + date.toLocaleString('de-DE')), br()); + p.append(span("-> last successful transmission: " + toIsoDateStr(date)), br()); } } } @@ -186,7 +186,7 @@ function tick() { if(0 != ts) - document.getElementById("date").innerHTML = (new Date((++ts) * 1000)).toLocaleString('de-DE'); + document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000))); if(++tickCnt >= 10) { tickCnt = 0; getAjax('/api/index', parse); diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 427152e3..52c4634d 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -9,6 +9,7 @@
+
@@ -17,10 +18,14 @@
Device Name
+
+
Reboot Ahoy at midnight
+
+
Dark Mode
-
(empty browser cache or use CTRL + F5 after reboot to apply this setting)
+
(empty browser cache or use CTRL + F5 after reboot to apply this setting)
@@ -31,6 +36,9 @@

Radio (NRF24L01+)

+

Radio (CMT2300A)

+
(ESP32 only)
+

Serial Console

print inverter data
@@ -51,6 +59,12 @@
WiFi + +
+
AP Password (min. length: 8)
+
+
+

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

@@ -70,6 +84,10 @@
SSID
+
+
SSID is hidden
+
+
Password
@@ -154,6 +172,14 @@
Reset values when inverter status is 'not available'
+
+
Start without time sync (useful in AP-Only-Mode)
+
+
+
+
Yield Effiency (should be between 0.95 and 0.96)
+
+
@@ -170,13 +196,21 @@
-
set system time
+
NTP Intervall (in Minutes, min. 5 Minutes)
+
+
+
+
set System time
- +
+
+
System Time
+
+
@@ -258,7 +292,7 @@
Luminance
-
+

Pinout

@@ -303,6 +337,7 @@