diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 65a22730..af81492f 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -39,7 +39,7 @@ jobs: - name: Install PlatformIO run: | - python -m pip install --upgrade pip + python -m pip install setuptools --upgrade pip pip install --upgrade platformio - name: Run PlatformIO diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml index cb4979b4..70eb5f29 100644 --- a/.github/workflows/compile_release.yml +++ b/.github/workflows/compile_release.yml @@ -43,7 +43,7 @@ jobs: - name: Install PlatformIO run: | - python -m pip install --upgrade pip + python -m pip install setuptools --upgrade pip pip install --upgrade platformio - name: Run PlatformIO diff --git a/.gitignore b/.gitignore index 2ee4b679..21ae2a57 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ src/web/html/tmp/* *.suo *.ipch src/output.map + +/.venv diff --git a/patches/GxEPD2_SW_SPI.patch b/patches/GxEPD2_SW_SPI.patch index 3eb6a32c..9697eec8 100644 --- a/patches/GxEPD2_SW_SPI.patch +++ b/patches/GxEPD2_SW_SPI.patch @@ -14,7 +14,7 @@ index 1588444..592869b 100644 { _initial_write = true; _initial_refresh = true; -@@ -67,27 +67,30 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset +@@ -71,27 +71,30 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset { pinMode(_busy, INPUT); } @@ -58,7 +58,7 @@ index 1588444..592869b 100644 } void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter) -@@ -96,12 +99,6 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* +@@ -100,12 +103,6 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* _busy_callback_parameter = busy_callback_parameter; } @@ -71,7 +71,7 @@ index 1588444..592869b 100644 void GxEPD2_EPD::_reset() { if (_rst >= 0) -@@ -168,115 +165,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) +@@ -174,115 +169,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) void GxEPD2_EPD::_writeCommand(uint8_t c) { @@ -334,7 +334,7 @@ index ef2318f..50aa961 100644 protected: void _reset(); void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000); -@@ -111,16 +115,21 @@ class GxEPD2_EPD +@@ -111,9 +115,14 @@ class GxEPD2_EPD void _startTransfer(); void _transfer(uint8_t value); void _endTransfer(); @@ -351,8 +351,7 @@ index ef2318f..50aa961 100644 bool _diag_enabled, _pulldown_rst_mode; - SPIClass* _pSPIx; SPISettings _spi_settings; - bool _initial_write, _initial_refresh; - bool _power_is_on, _using_partial_mode, _hibernating; +@@ -123,5 +124,5 @@ class GxEPD2_EPD uint16_t _reset_duration; - void (*_busy_callback)(const void*); + void (*_busy_callback)(const void*); diff --git a/src/CHANGES.md b/src/CHANGES.md index 1b6ec262..380458fc 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,53 @@ # Development Changes +## 0.8.8 - 2023-11-16 +* fix ESP8266 save inverter #1232 + +## 0.8.7 - 2023-11-13 +* fix ESP8266 inverter settings #1226 +* send radio statistics via MqTT #1227 +* made night communication inverter depended +* added option to prevent adding values of inverter to total values (MqTT only) #1199 + +## 0.8.6 - 2023-11-12 +* merged PR #1225 +* improved heuristics (prevent update of statitistic during testing) + +## 0.8.5 - 2023-11-12 +* fixed endless loop while switching CMT frequency +* removed obsolete "retries" field from settings #1224 +* fixed crash while defining new invertes #1224 +* fixed default frequency settings +* added default input power to `400` while adding new inverters +* fixed color of wifi RSSI icon #1224 + +## 0.8.4 - 2023-11-10 +* changed MqTT alarm topic, removed retained flag #1212 +* reduce last_success MQTT messages (#1124) +* introduced tabs in WebGUI (inverter settings) +* added inverter-wise power level and frequency + +## 0.8.3 - 2023-11-09 +* fix yield day reset during day #848 +* add total AC Max Power to WebUI +* fix opendtufusion build (GxEPD patch) +* fix null ptr PR #1222 + +## 0.8.2 - 2023-11-08 +* beautified inverter settings in `setup` (preperation for future, settings become more inverter dependent) + +## 0.8.1 - 2023-11-05 +* added tx channel heuristics (per inverter) +* fix statistics counter + +## 0.8.0 - 2023-10-?? +* switched to new communication scheme + +## 0.7.66 - 2023-10-04 +* prepared PA-Level for CMT +* removed settings for number of retransmits, its fixed to `5` now +* added parentheses to have a excactly defined behaviour + ## 0.7.65 - 2023-10-02 * MI control command review #1197 diff --git a/src/app.cpp b/src/app.cpp index 82ea1e2c..c73281c5 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4,9 +4,7 @@ //----------------------------------------------------------------------------- #include - #include "app.h" - #include "utils/sun.h" @@ -34,7 +32,7 @@ void app::setup() { DBGPRINTLN(F("false")); 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.setup(mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); mNrfRadio.enableDebug(); } #if defined(ESP32) @@ -59,25 +57,11 @@ void app::setup() { #endif #endif /* defined(ETHERNET) */ + mCommunication.setup(&mTimestamp); + mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); mSys.setup(&mTimestamp, &mConfig->inst); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - mSys.addInverter(i, [this](Inverter<> *iv) { - if((IV_MI == iv->ivGen) || (IV_HM == iv->ivGen)) - iv->radio = &mNrfRadio; - #if defined(ESP32) - else if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) - iv->radio = &mCmtRadio; - #endif - }); - } - - mPayload.setup(this, &mSys, mConfig->nrf.maxRetransPerPyld, &mTimestamp); - mPayload.enableSerialDebug(mConfig->serial.debug); - mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); - if (mConfig->nrf.enabled) { - mMiPayload.setup(this, &mSys, mConfig->nrf.maxRetransPerPyld, &mTimestamp); - mMiPayload.enableSerialDebug(mConfig->serial.debug); - mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + initInverter(i); } if(mConfig->nrf.enabled) { @@ -91,8 +75,7 @@ void app::setup() { if (mMqttEnabled) { mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); - mPayload.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); - mMiPayload.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); + mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); } #endif setupLed(); @@ -103,8 +86,10 @@ void app::setup() { mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); // Plugins + #if defined(PLUGIN_DISPLAY) if (mConfig->plugin.display.type != 0) mDisplay.setup(this, &mConfig->plugin.display, &mSys, &mNrfRadio, &mTimestamp); + #endif mPubSerial.setup(mConfig, &mSys, &mTimestamp); @@ -123,67 +108,12 @@ void app::setup() { //----------------------------------------------------------------------------- void app::loop(void) { ah::Scheduler::loop(); - bool processPayload = false; - - 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 ")); - DBGPRINT(String(p->len)); - DBGPRINT(F(" CH")); - DBGPRINT(String(p->ch)); - DBGPRINT(F(", ")); - DBGPRINT(String(p->rssi)); - DBGPRINT(F("dBm | ")); - ah::dumpBuf(p->packet, p->len); - } - Inverter<> *iv = mSys.findInverter(&p->packet[1]); - if (NULL != iv) { - iv->radioStatistics.frmCnt++; - if (IV_MI == iv->ivGen) - mMiPayload.add(iv, p); - else - mPayload.add(iv, p); - } - mNrfRadio.mBufCtrl.pop(); - processPayload = true; - yield(); - } - mMiPayload.process(true); - } + mNrfRadio.loop(); #if defined(ESP32) - if (mCmtRadio.loop() && mConfig->cmt.enabled) { - while (!mCmtRadio.mBufCtrl.empty()) { - packet_t *p = &mCmtRadio.mBufCtrl.front(); - if (mConfig->serial.debug) { - DPRINT(DBG_INFO, F("RX ")); - DBGPRINT(String(p->len)); - DBGPRINT(F(", ")); - DBGPRINT(String(p->rssi)); - DBGPRINT(F("dBm | ")); - ah::dumpBuf(p->packet, p->len); - } - - Inverter<> *iv = mSys.findInverter(&p->packet[1]); - if(NULL != iv) { - iv->radioStatistics.frmCnt++; - if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT)) - mPayload.add(iv, p); - } - mCmtRadio.mBufCtrl.pop(); - processPayload = true; - yield(); - } - } + mCmtRadio.loop(); #endif - - if(processPayload) - mPayload.process(true); - - mPayload.loop(); - mMiPayload.loop(); + mCommunication.loop(); if (mMqttEnabled && mNetworkConnected) mMqtt.loop(); @@ -198,8 +128,8 @@ void app::onNetwork(bool gotIp) { every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); mMqttReconnect = true; mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! - //once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); - tickNtpUpdate(); + once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); + //tickNtpUpdate(); #if !defined(ETHERNET) if (WIFI_AP == WiFi.getMode()) { mMqttEnabled = false; @@ -213,11 +143,16 @@ void app::regularTickers(void) { DPRINTLN(DBG_DEBUG, F("regularTickers")); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); // Plugins + #if defined(PLUGIN_DISPLAY) if (mConfig->plugin.display.type != 0) everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); - // Plugins + #endif + + // ZeroExport + #if defined(PLUGIN_ZEROEXPORT) if (mConfig->plugin.zexport.enabled) everySec(std::bind(&ZeroExportType::tickerSecond, &mzExport), "zExport"); + #endif every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); #if !defined(ETHERNET) @@ -288,7 +223,7 @@ void app::tickNtpUpdate(void) { // immediately start communicating if (isOK && mSendFirst) { mSendFirst = false; - once(std::bind(&app::tickSend, this), 2, "senOn"); + once(std::bind(&app::tickSend, this), 1, "senOn"); } mMqttReconnect = false; @@ -314,43 +249,50 @@ void app::tickCalcSunrise(void) { //----------------------------------------------------------------------------- void app::tickIVCommunication(void) { - mIVCommunicationOn = !mConfig->sun.disNightCom; // if sun.disNightCom is false, communication is always on - if (!mIVCommunicationOn) { // inverter communication only during the day - uint32_t nxtTrig; - if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start - nxtTrig = mSunrise - mConfig->sun.offsetSec; - } else { - if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise - nxtTrig = 0; - } else { // current time lies within communication start/stop time, set next trigger to communication stop - mIVCommunicationOn = true; - nxtTrig = mSunset + mConfig->sun.offsetSec; + bool restartTick = false; + bool zeroValues = false; + uint32_t nxtTrig = 0; + + Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys.getInverterByPos(i); + if(NULL == iv) + continue; + + iv->commEnabled = !iv->config->disNightCom; // if sun.disNightCom is false, communication is always on + if (!iv->commEnabled) { // inverter communication only during the day + if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start + nxtTrig = mSunrise - mConfig->sun.offsetSec; + } else { + if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise + nxtTrig = 0; + } else { // current time lies within communication start/stop time, set next trigger to communication stop + iv->commEnabled = true; + nxtTrig = mSunset + mConfig->sun.offsetSec; + } } + if (nxtTrig != 0) + restartTick = true; } - if (nxtTrig != 0) - onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); + + if ((!iv->commEnabled) && (mConfig->inst.rstValsCommStop)) + zeroValues = true; } - tickComm(); + + if(restartTick) // at least one inverter + onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); + + if (zeroValues) // at least one inverter + once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); } //----------------------------------------------------------------------------- void app::tickSun(void) { // only used and enabled by MQTT (see setup()) - if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) + if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec)) once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry } -//----------------------------------------------------------------------------- -void app::tickComm(void) { - if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) - once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); - - if (mMqttEnabled) { - if (!mMqtt.tickerComm(!mIVCommunicationOn)) - once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s - } -} - //----------------------------------------------------------------------------- void app::tickZeroValues(void) { zeroIvValues(!CHECK_AVAIL, SKIP_YIELD_DAY); @@ -400,57 +342,30 @@ void app::tickMidnight(void) { //----------------------------------------------------------------------------- void app::tickSend(void) { - if(mConfig->nrf.enabled) { - if(!mNrfRadio.isChipConnected()) { - DPRINTLN(DBG_WARN, F("NRF24 not connected!")); - } - } - - if (mIVCommunicationOn) { - if (!mNrfRadio.mBufCtrl.empty()) { - if (mConfig->serial.debug) { - DPRINT(DBG_DEBUG, F("recbuf not empty! #")); - 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())); + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + Inverter<> *iv = mSys.getInverterByPos(i); + if(NULL == iv) + continue; + + if(iv->config->enabled) { + if(!iv->commEnabled) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("no communication to the inverter (night time)")); + continue; } - } - #endif - - int8_t maxLoop = MAX_NUM_INVERTERS; - Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); - while(maxLoop > 0) { - do { - mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; - iv = mSys.getInverterByPos(mSendLastIvId); - } while ((NULL == iv) && ((maxLoop--) > 0)); - if(NULL != iv) - if(iv->config->enabled) - break; - } - if (NULL != iv) { - if (iv->config->enabled) { - if((iv->ivGen == IV_MI) && mConfig->nrf.enabled) - mMiPayload.ivSend(iv); + iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) { + if(isDevControl) + mCommunication.addImportant(iv, cmd); else - mPayload.ivSend(iv); - } + mCommunication.add(iv, cmd); + }); #if defined(ESP32) if(mConfig->nrf.enabled || mConfig->cmt.enabled) zeroexport(); #endif } - } else { - if (mConfig->serial.debug) - DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!")); } - yield(); updateLed(); } @@ -466,6 +381,8 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { continue; // skip to next inverter if (!iv->config->enabled) continue; // skip to next inverter + if (iv->commEnabled) + continue; // skip to next inverter if (checkAvail) { if (!iv->isAvailable()) @@ -523,7 +440,6 @@ void app::resetSystem(void) { mSendLastIvId = 0; mShowRebootRequest = false; - mIVCommunicationOn = true; mSavePending = false; mSaveReboot = false; diff --git a/src/app.h b/src/app.h index 8cf017bd..8ce34465 100644 --- a/src/app.h +++ b/src/app.h @@ -12,12 +12,9 @@ #include "config/settings.h" #include "defines.h" #include "appInterface.h" -#include "hm/hmPayload.h" #include "hm/hmSystem.h" #include "hm/hmRadio.h" #include "hms/hmsRadio.h" -#include "hm/hmPayload.h" -#include "hm/miPayload.h" #include "publisher/pubMqtt.h" #include "publisher/pubSerial.h" #include "utils/crc.h" @@ -25,6 +22,7 @@ #include "utils/scheduler.h" #include "web/RestApi.h" #include "web/web.h" +#include "hm/Communication.h" #if defined(ETHERNET) #include "eth/ahoyeth.h" #else /* defined(ETHERNET) */ @@ -41,8 +39,6 @@ #define ACOS(x) (degrees(acos(x))) typedef HmSystem HmSystemType; -typedef HmPayload PayloadType; -typedef MiPayload MiPayloadType; #ifdef ESP32 typedef CmtRadio CmtRadioType; #endif @@ -52,13 +48,16 @@ typedef PubMqtt PubMqttType; typedef PubSerial PubSerialType; // PLUGINS +#if defined(PLUGIN_DISPLAY) #include "plugins/Display/Display.h" #include "plugins/Display/Display_data.h" typedef Display> DisplayType; +#endif +#if defined(PLUGIN_ZEROEXPORT) #include "plugins/zeroExport/zeroExport.h" typedef ZeroExport ZeroExportType; - +#endif class app : public IApp, public ah::Scheduler { public: @@ -110,6 +109,17 @@ class app : public IApp, public ah::Scheduler { return true; } + void initInverter(uint8_t id) { + mSys.addInverter(id, [this](Inverter<> *iv) { + if((IV_MI == iv->ivGen) || (IV_HM == iv->ivGen)) + iv->radio = &mNrfRadio; + #if defined(ESP32) + else if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) + iv->radio = &mCmtRadio; + #endif + }); + } + bool readSettings(const char *path) { return mSettings.readSettings(path); } @@ -173,15 +183,6 @@ class app : public IApp, public ah::Scheduler { mMqtt.setPowerLimitAck(iv); } - void ivSendHighPrio(Inverter<> *iv) { - if(mIVCommunicationOn) { // only send commands if communication is enabled - if (iv->ivGen == IV_MI) - mMiPayload.ivSendHighPrio(iv); - else - mPayload.ivSendHighPrio(iv); - } - } - bool getMqttIsConnected() { return mMqtt.isConnected(); } @@ -262,8 +263,10 @@ class app : public IApp, public ah::Scheduler { if (mMqttEnabled) mMqtt.payloadEventListener(cmd, iv); #endif + #if defined(PLUGIN_DISPLAY) if(mConfig->plugin.display.type != 0) mDisplay.payloadEventListener(cmd); + #endif updateLed(); } @@ -310,9 +313,9 @@ class app : public IApp, public ah::Scheduler { HmSystemType mSys; HmRadio<> mNrfRadio; + Communication mCommunication; bool mShowRebootRequest; - bool mIVCommunicationOn; #if defined(ETHERNET) ahoyeth mEth; @@ -321,8 +324,8 @@ class app : public IApp, public ah::Scheduler { #endif /* defined(ETHERNET) */ WebType mWeb; RestApiType mApi; - PayloadType mPayload; - MiPayloadType mMiPayload; + //PayloadType mPayload; + //MiPayloadType mMiPayload; PubSerialType mPubSerial; #if !defined(ETHERNET) //Improv mImprov; @@ -352,10 +355,14 @@ class app : public IApp, public ah::Scheduler { uint32_t mSunrise, mSunset; // plugins + #if defined(PLUGIN_DISPLAY) DisplayType mDisplay; DisplayData mDispData; + #endif + #if defined(PLUGIN_ZEROEXPORT) ZeroExportType mzExport; + #endif }; #endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h index d3bc8346..802f21e6 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -25,6 +25,7 @@ class IApp { public: virtual ~IApp() {} virtual bool saveSettings(bool stopFs) = 0; + virtual void initInverter(uint8_t id) = 0; virtual bool readSettings(const char *path) = 0; virtual bool eraseSettings(bool eraseWifi) = 0; virtual bool getSavePending() = 0; @@ -53,8 +54,6 @@ class IApp { virtual void setMqttDiscoveryFlag() = 0; virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0; - virtual void ivSendHighPrio(Inverter<> *iv) = 0; - virtual bool getMqttIsConnected() = 0; virtual uint32_t getMqttRxCnt() = 0; virtual uint32_t getMqttTxCnt() = 0; diff --git a/src/config/config.h b/src/config/config.h index 4cd333f6..727829cf 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -132,9 +132,6 @@ #define LED_HIGH_ACTIVE false #endif -// default NRF24 power, possible values (0 - 3) -#define DEF_AMPLIFIERPOWER 1 - // number of packets hold in buffer #define PACKET_BUFFER_SIZE 30 @@ -149,7 +146,7 @@ #define SERIAL_INTERVAL 5 // default send interval -#define SEND_INTERVAL 30 +#define SEND_INTERVAL 15 // maximum human readable inverter name length #define MAX_NAME_LENGTH 16 @@ -158,10 +155,7 @@ #define MAX_RF_PAYLOAD_SIZE 32 // maximum total payload buffers (must be greater than the number of received frame fragments) -#define MAX_PAYLOAD_ENTRIES 10 - -// maximum requests for retransmits per payload (per inverter) -#define DEF_MAX_RETRANS_PER_PYLD 5 +#define MAX_PAYLOAD_ENTRIES 20 // number of seconds since last successful response, before inverter is marked inactive #define INVERTER_INACT_THRES_SEC 5*60 diff --git a/src/config/settings.h b/src/config/settings.h index 10288e0c..97f33cc8 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -30,6 +30,8 @@ * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * */ +#define CONFIG_VERSION 2 + #define PROT_MASK_INDEX 0x0001 #define PROT_MASK_LIVE 0x0002 @@ -82,14 +84,12 @@ typedef struct { typedef struct { bool enabled; uint16_t sendInterval; - uint8_t maxRetransPerPyld; uint8_t pinCs; uint8_t pinCe; uint8_t pinIrq; uint8_t pinMiso; uint8_t pinMosi; uint8_t pinSclk; - uint8_t amplifierPower; } cfgNrf24_t; typedef struct { @@ -110,7 +110,6 @@ typedef struct { typedef struct { float lat; float lon; - bool disNightCom; // disable night communication uint16_t offsetSec; } cfgSun_t; @@ -160,6 +159,10 @@ typedef struct { uint16_t chMaxPwr[6]; double yieldCor[6]; // YieldTotal correction value char chName[6][MAX_NAME_LENGTH]; + uint8_t frequency; + uint8_t powerLevel; + bool disNightCom; // disable night communication + bool add2Total; // add values to total values - useful if one inverter is on battery to turn off } cfgIv_t; typedef struct { @@ -209,6 +212,7 @@ typedef struct { cfgInst_t inst; plugins_t plugin; bool valid; + uint16_t configVersion; } settings_t; class settings { @@ -306,6 +310,7 @@ class settings { if(root.containsKey(F("led"))) jsonLed(root[F("led")]); if(root.containsKey(F("plugin"))) jsonPlugin(root[F("plugin")]); if(root.containsKey(F("inst"))) jsonInst(root[F("inst")]); + getConfigVersion(root.as()); } else { Serial.println(F("failed to parse json, using default config")); @@ -321,6 +326,7 @@ class settings { DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); JsonObject root = json.to(); + json[F("version")] = CONFIG_VERSION; jsonNetwork(root.createNestedObject(F("wifi")), true); jsonNrf(root.createNestedObject(F("nrf")), true); #if defined(ESP32) @@ -408,7 +414,6 @@ class settings { snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); mCfg.nrf.sendInterval = SEND_INTERVAL; - mCfg.nrf.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; mCfg.nrf.pinCs = DEF_NRF_CS_PIN; mCfg.nrf.pinCe = DEF_NRF_CE_PIN; mCfg.nrf.pinIrq = DEF_NRF_IRQ_PIN; @@ -416,7 +421,6 @@ class settings { mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN; mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN; - mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; mCfg.nrf.enabled = true; #if defined(ESP32) @@ -440,7 +444,6 @@ class settings { mCfg.sun.lat = 0.0; mCfg.sun.lon = 0.0; - mCfg.sun.disNightCom = false; mCfg.sun.offsetSec = 0; mCfg.serial.interval = SERIAL_INTERVAL; @@ -476,6 +479,13 @@ class settings { mCfg.inst.rstMaxValsMidNight = false; mCfg.inst.yieldEffiency = 0.955f; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value + mCfg.inst.iv[i].frequency = 0x12; // 863MHz (minimum allowed frequency) + mCfg.inst.iv[i].disNightCom = false; + mCfg.inst.iv[i].add2Total = true; + } + mCfg.led.led0 = DEF_LED0; mCfg.led.led1 = DEF_LED1; mCfg.led.led_high_active = LED_HIGH_ACTIVE; @@ -486,13 +496,34 @@ class settings { mCfg.plugin.display.contrast = 60; mCfg.plugin.display.pxShift = true; mCfg.plugin.display.rot = 0; - mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA - mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL + mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA + mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL mCfg.plugin.display.disp_cs = DEF_PIN_OFF; mCfg.plugin.display.disp_reset = DEF_PIN_OFF; mCfg.plugin.display.disp_busy = DEF_PIN_OFF; mCfg.plugin.display.disp_dc = DEF_PIN_OFF; - } + } + + void loadAddedDefaults() { + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + if(mCfg.configVersion < 1) { + mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value + mCfg.inst.iv[i].frequency = 0x0; // 860MHz (backward compatibility) + } + if(mCfg.configVersion < 2) { + mCfg.inst.iv[i].disNightCom = false; + mCfg.inst.iv[i].add2Total = true; + } + } + } + + void getConfigVersion(JsonObject obj) { + getVal(obj, F("version"), &mCfg.configVersion); + DPRINT(DBG_INFO, F("Config Version: ")); + DBGPRINTLN(String(mCfg.configVersion)); + if(CONFIG_VERSION != mCfg.configVersion) + loadAddedDefaults(); + } void jsonNetwork(JsonObject obj, bool set = false) { if(set) { @@ -541,25 +572,21 @@ class settings { void jsonNrf(JsonObject obj, bool set = false) { if(set) { obj[F("intvl")] = mCfg.nrf.sendInterval; - obj[F("maxRetry")] = mCfg.nrf.maxRetransPerPyld; obj[F("cs")] = mCfg.nrf.pinCs; obj[F("ce")] = mCfg.nrf.pinCe; obj[F("irq")] = mCfg.nrf.pinIrq; obj[F("sclk")] = mCfg.nrf.pinSclk; 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); getVal(obj, F("cs"), &mCfg.nrf.pinCs); getVal(obj, F("ce"), &mCfg.nrf.pinCe); getVal(obj, F("irq"), &mCfg.nrf.pinIrq); getVal(obj, F("sclk"), &mCfg.nrf.pinSclk); 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 @@ -619,12 +646,10 @@ class settings { if(set) { obj[F("lat")] = mCfg.sun.lat; obj[F("lon")] = mCfg.sun.lon; - obj[F("dis")] = mCfg.sun.disNightCom; obj[F("offs")] = mCfg.sun.offsetSec; } else { getVal(obj, F("lat"), &mCfg.sun.lat); getVal(obj, F("lon"), &mCfg.sun.lon); - getVal(obj, F("dis"), &mCfg.sun.disNightCom); getVal(obj, F("offs"), &mCfg.sun.offsetSec); } } @@ -782,6 +807,10 @@ class settings { obj[F("en")] = (bool)cfg->enabled; obj[F("name")] = cfg->name; obj[F("sn")] = cfg->serial.u64; + obj[F("freq")] = cfg->frequency; + obj[F("pa")] = cfg->powerLevel; + obj[F("dis")] = cfg->disNightCom; + obj[F("add")] = cfg->add2Total; for(uint8_t i = 0; i < 6; i++) { obj[F("yield")][i] = cfg->yieldCor[i]; obj[F("pwr")][i] = cfg->chMaxPwr[i]; @@ -791,6 +820,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); + getVal(obj, F("freq"), &cfg->frequency); + getVal(obj, F("pa"), &cfg->powerLevel); + getVal(obj, F("dis"), &cfg->disNightCom); + getVal(obj, F("add"), &cfg->add2Total); uint8_t size = 4; if(obj.containsKey(F("pwr"))) size = obj[F("pwr")].size(); diff --git a/src/defines.h b/src/defines.h index 21d7d37d..684bec9e 100644 --- a/src/defines.h +++ b/src/defines.h @@ -12,8 +12,8 @@ // VERSION //------------------------------------- #define VERSION_MAJOR 0 -#define VERSION_MINOR 7 -#define VERSION_PATCH 65 +#define VERSION_MINOR 8 +#define VERSION_PATCH 8 //------------------------------------- typedef struct { @@ -21,6 +21,7 @@ typedef struct { uint8_t len; int8_t rssi; uint8_t packet[MAX_RF_PAYLOAD_SIZE]; + uint16_t millis; } packet_t; typedef enum { @@ -95,6 +96,8 @@ enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; #define MQTT_MAX_PACKET_SIZE 384 +#define PLUGIN_DISPLAY + typedef struct { uint32_t rxFail; uint32_t rxFailNoAnser; diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h new file mode 100644 index 00000000..8d0bb0f8 --- /dev/null +++ b/src/hm/CommQueue.h @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __COMM_QUEUE_H__ +#define __COMM_QUEUE_H__ + +#include +#include +#include "hmInverter.h" + +template +class CommQueue { + public: + CommQueue() {} + + void addImportant(Inverter<> *iv, uint8_t cmd, bool delOnPop = true) { + dec(&mRdPtr); + mQueue[mRdPtr] = queue_s(iv, cmd, delOnPop, true); + } + + void add(Inverter<> *iv, uint8_t cmd, bool delOnPop = true) { + mQueue[mWrPtr] = queue_s(iv, cmd, delOnPop, false); + inc(&mWrPtr); + } + + void chgCmd(Inverter<> *iv, uint8_t cmd, bool delOnPop = true) { + mQueue[mWrPtr] = queue_s(iv, cmd, delOnPop, false); + } + + protected: + struct queue_s { + Inverter<> *iv; + uint8_t cmd; + uint8_t attempts; + uint32_t ts; + bool delOnPop; + bool isDevControl; + queue_s() {} + queue_s(Inverter<> *i, uint8_t c, bool d, bool dev) : + iv(i), cmd(c), attempts(5), ts(0), delOnPop(d), isDevControl(dev) {} + }; + + protected: + void add(queue_s q) { + mQueue[mWrPtr] = q; + inc(&mWrPtr); + } + + void add(const queue_s *q, bool rstAttempts = false) { + mQueue[mWrPtr] = *q; + if(rstAttempts) + mQueue[mWrPtr].attempts = 5; + inc(&mWrPtr); + } + + void chgCmd(uint8_t cmd) { + mQueue[mRdPtr].cmd = cmd; + mQueue[mRdPtr].isDevControl = false; + } + + void get(std::function cb) { + if(mRdPtr == mWrPtr) { + cb(false, &mQueue[mRdPtr]); // empty + return; + } + cb(true, &mQueue[mRdPtr]); + } + + void cmdDone(bool force = false) { + if(!mQueue[mRdPtr].delOnPop && !force) { + mQueue[mRdPtr].attempts = 5; + add(mQueue[mRdPtr]); // add to the end again + } + inc(&mRdPtr); + } + + void setTs(uint32_t *ts) { + mQueue[mRdPtr].ts = *ts; + } + + void setAttempt(void) { + if(mQueue[mRdPtr].attempts) + mQueue[mRdPtr].attempts--; + } + + void inc(uint8_t *ptr) { + if(++(*ptr) >= N) + *ptr = 0; + } + void dec(uint8_t *ptr) { + if((*ptr) == 0) + *ptr = N-1; + else + --(*ptr); + } + + protected: + std::array mQueue; + uint8_t mWrPtr = 0; + uint8_t mRdPtr = 0; +}; + + +#endif /*__COMM_QUEUE_H__*/ diff --git a/src/hm/Communication.h b/src/hm/Communication.h new file mode 100644 index 00000000..a4eb43eb --- /dev/null +++ b/src/hm/Communication.h @@ -0,0 +1,765 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __COMMUNICATION_H__ +#define __COMMUNICATION_H__ + +#include "CommQueue.h" +#include +#include "../utils/crc.h" +#include "Heuristic.h" + +#define MI_TIMEOUT 250 +#define DEFAULT_TIMEOUT 500 +#define SINGLEFR_TIMEOUT 60 +#define MAX_BUFFER 250 + +typedef std::function *)> payloadListenerType; +typedef std::function *)> alarmListenerType; + +class Communication : public CommQueue<> { + public: + void setup(uint32_t *timestamp) { + mTimestamp = timestamp; + } + + void addImportant(Inverter<> *iv, uint8_t cmd, bool delOnPop = true) { + mState = States::RESET; // cancel current operation + CommQueue::addImportant(iv, cmd, delOnPop); + } + + void addPayloadListener(payloadListenerType cb) { + mCbPayload = cb; + } + + void addAlarmListener(alarmListenerType cb) { + mCbAlarm = cb; + } + + void loop() { + get([this](bool valid, const queue_s *q) { + if(!valid) + return; // empty + + uint16_t timeout = q->iv->ivGen != IV_MI ? DEFAULT_TIMEOUT : MI_TIMEOUT; + bool testMode = false; + + switch(mState) { + case States::RESET: + if(millis() < mWaitTimeout) + return; + mMaxFrameId = 0; + for(uint8_t i = 0; i < MAX_PAYLOAD_ENTRIES; i++) { + mLocalBuf[i].len = 0; + } + + mHeu.printStatus(q->iv); + mHeu.getTxCh(q->iv); + testMode = mHeu.getTestModeEnabled(); + mGotFragment = false; + mFirstTry = mFirstTry ? false : ( (IV_HM == q->iv->ivGen) || (IV_MI == q->iv->ivGen) ) && (q->iv->isAvailable()); //) || (millis() < 120000));} + if(NULL == q->iv->radio) + cmdDone(true); // can't communicate while radio is not defined! + mState = States::START; + break; + + case States::START: + setTs(mTimestamp); + if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) { + // frequency was changed during runtime + if(q->iv->curCmtFreq != q->iv->config->frequency) { + if(q->iv->radio->switchFrequencyCh(q->iv, q->iv->curCmtFreq, q->iv->config->frequency)) + q->iv->curCmtFreq = q->iv->config->frequency; + } + } + + if(q->isDevControl) { + if(ActivePowerContr == q->cmd) + q->iv->powerLimitAck = false; + q->iv->radio->sendControlPacket(q->iv, q->cmd, q->iv->powerLimit, false); + } else + q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false); + if(!testMode) + q->iv->radioStatistics.txCnt++; + mWaitTimeout = millis() + timeout; + setAttempt(); + mState = States::WAIT; + break; + + case States::WAIT: + if(millis() < mWaitTimeout) + return; + mState = States::CHECK_FRAMES; + break; + + case States::CHECK_FRAMES: { + if(!q->iv->radio->get()) { // radio buffer empty + cmdDone(); + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINT(F("request timeout: ")); + DBGPRINT(String(millis() - mWaitTimeout + timeout)); + DBGPRINTLN(F("ms")); + + if(!mGotFragment && !mFirstTry) { + if(!testMode) + q->iv->radioStatistics.rxFailNoAnser++; // got nothing + mHeu.setGotNothing(q->iv); + if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) { + q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ)); + mWaitTimeout = millis() + 1000; + } + } else { + if(!testMode) + q->iv->radioStatistics.rxFail++; + } + mState = States::RESET; + break; + } + + mGotFragment = true; + mFirstTry = false; // for correct reset + States nextState = States::RESET; + while(!q->iv->radio->mBufCtrl.empty()) { + packet_t *p = &q->iv->radio->mBufCtrl.front(); + + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINT(F("RX ")); + if(p->millis < 100) + DBGPRINT(F(" ")); + DBGPRINT(String(p->millis)); + DBGPRINT(F("ms ")); + DBGPRINT(String(p->len)); + if((IV_HM == q->iv->ivGen) || (IV_MI == q->iv->ivGen)) { + DBGPRINT(F(" CH")); + if(3 == p->ch) + DBGPRINT(F("0")); + DBGPRINT(String(p->ch)); + } + DBGPRINT(F(", ")); + DBGPRINT(String(p->rssi)); + DBGPRINT(F("dBm | ")); + ah::dumpBuf(p->packet, p->len); + + if(checkIvSerial(&p->packet[1], q->iv)) { + if(!testMode) + q->iv->radioStatistics.frmCnt++; + + if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command + parseFrame(p); + nextState = States::CHECK_PACKAGE; + } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command + parseDevCtrl(p, q); + cmdDone(true); // remove done request + } else if(IV_MI == q->iv->ivGen) { + parseMiFrame(p, q); + } + } else { + if(!testMode) + q->iv->radioStatistics.rxFail++; // got no complete payload + DPRINTLN(DBG_WARN, F("Inverter serial does not match")); + mWaitTimeout = millis() + timeout; + } + + q->iv->radio->mBufCtrl.pop(); + yield(); + } + if(0 == q->attempts) { + if(!testMode) + q->iv->radioStatistics.rxFail++; // got no complete payload + mHeu.setGotFragment(q->iv); + cmdDone(true); + mState = States::RESET; + } else + mState = nextState; + + } + break; + + case States::CHECK_PACKAGE: + if(0 == mMaxFrameId) { + setAttempt(); + + DPRINT_IVID(DBG_WARN, q->iv->id); + DBGPRINT(F("frame missing: request retransmit (")); + DBGPRINT(String(q->attempts)); + DBGPRINTLN(F(" attempts left)")); + + uint8_t i = 0; + while(i < MAX_PAYLOAD_ENTRIES) { + if(mLocalBuf[i].len == 0) + break; + i++; + } + + sendRetransmit(q, i); + return; + } + + for(uint8_t i = 0; i < mMaxFrameId; i++) { + if(mLocalBuf[i].len == 0) { + setAttempt(); + + DPRINT_IVID(DBG_WARN, q->iv->id); + DBGPRINT(F("frame ")); + DBGPRINT(String(i + 1)); + DBGPRINT(F(" missing: request retransmit (")); + DBGPRINT(String(q->attempts)); + DBGPRINTLN(F(" attempts left)")); + + sendRetransmit(q, i); + return; + } + } + + mHeu.setGotAll(q->iv); + compilePayload(q, testMode); + + if(NULL != mCbPayload) + (mCbPayload)(q->cmd, q->iv); + + cmdDone(true); // remove done request + mState = States::RESET; // everything ok, next request + break; + } + }); + } + + private: + inline bool checkIvSerial(uint8_t buf[], Inverter<> *iv) { + uint8_t tmp[4]; + CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8); + for(uint8_t i = 0; i < 4; i++) { + if(tmp[i] != buf[i]) + return false; + } + return true; + } + + inline bool checkFrameCrc(uint8_t buf[], uint8_t len) { + return (ah::crc8(buf, len - 1) == buf[len-1]); + } + + inline void parseFrame(packet_t *p) { + uint8_t *frameId = &p->packet[9]; + if(0x00 == *frameId) { + DPRINTLN(DBG_WARN, F("invalid frameId 0x00")); + return; // skip current packet + } + if((*frameId & 0x7f) > MAX_PAYLOAD_ENTRIES) { + DPRINTLN(DBG_WARN, F("local buffer to small for payload fragments")); + return; // local storage is to small for id + } + + if(!checkFrameCrc(p->packet, p->len)) { + DPRINTLN(DBG_WARN, F("frame CRC is wrong")); + return; // CRC8 is wrong, frame invalid + } + + if((*frameId & ALL_FRAMES) == ALL_FRAMES) + mMaxFrameId = (*frameId & 0x7f); + + frame_t *f = &mLocalBuf[(*frameId & 0x7f) - 1]; + memcpy(f->buf, &p->packet[10], p->len-11); + f->len = p->len - 11; + f->rssi = p->rssi; + } + + inline void parseMiFrame(packet_t *p, const queue_s *q) { + if ((p->packet[0] == MI_REQ_CH1 + ALL_FRAMES) + || (p->packet[0] == MI_REQ_CH2 + ALL_FRAMES) + || ((p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES)) + && (p->packet[0] < (0x39 + SINGLE_FRAME)) + )) { //&& (p->packet[0] != (0x0f + ALL_FRAMES)))) { + // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 + //mPayload[iv->id].txId = p->packet[0]; + miDataDecode(p, q); + } else if (p->packet[0] == (0x0f + ALL_FRAMES)) + miHwDecode(p, q); + else if ((p->packet[0] == 0x88) || (p->packet[0] == 0x92)) { + record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure + rec->ts = q->ts; + miStsConsolidate(q, ((p->packet[0] == 0x88) ? 1 : 2), rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]); + mHeu.setGotFragment(q->iv); + } + } + + inline void parseDevCtrl(packet_t *p, const queue_s *q) { + if((p->packet[12] != ActivePowerContr) || (p->packet[13] != 0x00)) + return; + bool accepted = true; + if((p->packet[10] == 0x00) && (p->packet[11] == 0x00)) + q->iv->powerLimitAck = true; + else + accepted = false; + + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINT(F(" has ")); + if(!accepted) DBGPRINT(F("not ")); + DBGPRINT(F("accepted power limit set point ")); + DBGPRINT(String(q->iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(q->iv->powerLimit[1])); + q->iv->actPowerLimit = 0xffff; // unknown, readback current value + } + + inline void compilePayload(const queue_s *q, bool testMode) { + uint16_t crc = 0xffff, crcRcv = 0x0000; + for(uint8_t i = 0; i < mMaxFrameId; i++) { + if(i == (mMaxFrameId - 1)) { + crc = ah::crc16(mLocalBuf[i].buf, mLocalBuf[i].len - 2, crc); + crcRcv = (mLocalBuf[i].buf[mLocalBuf[i].len-2] << 8); + crcRcv |= mLocalBuf[i].buf[mLocalBuf[i].len-1]; + } else + crc = ah::crc16(mLocalBuf[i].buf, mLocalBuf[i].len, crc); + } + + if(crc != crcRcv) { + DPRINT_IVID(DBG_WARN, q->iv->id); + DBGPRINT(F("CRC Error ")); + if(q->attempts == 0) { + DBGPRINTLN(F("-> Fail")); + if(!testMode) + q->iv->radioStatistics.rxFail++; // got fragments but not complete response + cmdDone(); + } else + DBGPRINTLN(F("-> complete retransmit")); + mState = States::RESET; + return; + } + + /*DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINT(F("procPyld: cmd: 0x")); + DBGHEXLN(q->cmd);*/ + + memset(mPayload, 0, MAX_BUFFER); + int8_t rssi = -127; + uint8_t len = 0; + + for(uint8_t i = 0; i < mMaxFrameId; i++) { + if(mLocalBuf[i].len + len > MAX_BUFFER) { + DPRINTLN(DBG_ERROR, F("payload buffer to small!")); + return; + } + memcpy(&mPayload[len], mLocalBuf[i].buf, mLocalBuf[i].len); + len += mLocalBuf[i].len; + // get worst RSSI (high value is better) + if(mLocalBuf[i].rssi > rssi) + rssi = mLocalBuf[i].rssi; + } + + len -= 2; + + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINT(F("Payload (")); + DBGPRINT(String(len)); + DBGPRINT(F("): ")); + ah::dumpBuf(mPayload, len); + + record_t<> *rec = q->iv->getRecordStruct(q->cmd); + if(NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + return; + } + if((rec->pyldLen != len) && (0 != rec->pyldLen)) { + DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); + DBGPRINT(String(rec->pyldLen)); + DBGPRINTLN(F(" bytes")); + if(!testMode) + q->iv->radioStatistics.rxFail++; + return; + } + + if(!testMode) + q->iv->radioStatistics.rxSuccess++; + + rec->ts = q->ts; + for (uint8_t i = 0; i < rec->length; i++) { + q->iv->addValue(i, mPayload, rec); + } + + q->iv->rssi = rssi; + q->iv->doCalculations(); + + if(AlarmData == q->cmd) { + uint8_t i = 0; + while(1) { + if(0 == q->iv->parseAlarmLog(i++, mPayload, len)) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(q->iv); + yield(); + } + } + } + + void sendRetransmit(const queue_s *q, uint8_t i) { + if(q->attempts) { + q->iv->radio->sendCmdPacket(q->iv, TX_REQ_INFO, (SINGLE_FRAME + i), true); + q->iv->radioStatistics.retransmits++; + mWaitTimeout = millis() + SINGLEFR_TIMEOUT; // timeout + mState = States::WAIT; + } else { + mHeu.setGotFragment(q->iv); + q->iv->radioStatistics.rxFail++; // got no complete payload + add(q, true); + cmdDone(true); + mState = States::RESET; + } + } + + private: + inline void miHwDecode(packet_t *p, const queue_s *q) { + record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_All); // choose the record structure + rec->ts = q->ts; + //mPayload[iv->id].gotFragment = true; + uint8_t multi_parts = 0; + + /* + Polling the device software and hardware version number command + start byte Command word routing address target address User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] + 0x7e 0x0f xx xx xx xx YY YY YY YY 0x00 CRC 0x7f + Command Receipt - First Frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x00 USFWBuild_VER APPFWBuild_VER APPFWBuild_YYYY APPFWBuild_MMDD APPFWBuild_HHMM APPFW_PN HW_VER CRC 0x7f + Command Receipt - Second Frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x01 HW_PN HW_FB_TLmValue HW_FB_ReSPRT HW_GridSamp_ResValule HW_ECapValue Matching_APPFW_PN CRC 0x7f + Command receipt - third frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[15] byte[16] byte[17] byte[18] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x12 APPFW_MINVER HWInfoAddr PNInfoCRC_gusv PNInfoCRC_gusv CRC 0x7f + */ + + /* + case InverterDevInform_All: + rec->length = (uint8_t)(HMINFO_LIST_LEN); + rec->assign = (byteAssign_t *)InfoAssignment; + rec->pyldLen = HMINFO_PAYLOAD_LEN; + break; + const byteAssign_t InfoAssignment[] = { + { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, + { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, + { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, + { FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 }, + { FLD_BOOTLOADER_VER, UNIT_NONE, CH0, 8, 2, 1 } + }; + */ + + if ( p->packet[9] == 0x00 ) {//first frame + //FLD_FW_VERSION + for (uint8_t i = 0; i < 5; i++) { + q->iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); + } + q->iv->isConnected = true; + //if(mSerialDebug) { + DPRINT_IVID(DBG_INFO, q->iv->id); + DPRINT(DBG_INFO,F("HW_VER is ")); + DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25])); + //} + record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure + rec->ts = q->ts; + q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1); + //mPayload[iv->id].multi_parts +=4; + multi_parts +=4; + } else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10 + DPRINT_IVID(DBG_INFO, q->iv->id); + if ( p->packet[9] == 0x01 ) { + DBGPRINTLN(F("got 2nd frame (hw info)")); + /* according to xlsx (different start byte -1!) + byte[11] to byte[14] HW_PN + byte[15] byte[16] HW_FB_TLmValue + byte[17] byte[18] HW_FB_ReSPRT + byte[19] byte[20] HW_GridSamp_ResValule + byte[21] byte[22] HW_ECapValue + byte[23] to byte[26] Matching_APPFW_PN*/ + 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])); + record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure + rec->ts = q->ts; + q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/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_ECapValue ")); + DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21])); + DPRINT(DBG_INFO,F("Matching_APPFW_PN ")); + DBGPRINTLN(String((uint32_t) (((p->packet[22] << 8) | p->packet[23]) << 8 | p->packet[24]) << 8 | p->packet[25])); + //} + //notify(InverterDevInform_Simple, iv); + //mPayload[iv->id].multi_parts +=2; + multi_parts +=2; + //notify(InverterDevInform_All, iv); + } else { + DBGPRINTLN(F("3rd gen. inverter!")); + } + + } else if ( p->packet[9] == 0x12 ) {//3rd frame + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINTLN(F("got 3rd frame (hw info)")); + /* according to xlsx (different start byte -1!) + byte[11] byte[12] APPFW_MINVER + byte[13] byte[14] HWInfoAddr + byte[15] byte[16] PNInfoCRC_gusv + byte[15] byte[16] PNInfoCRC_gusv (this really is double mentionned in xlsx...) + */ + //if(mSerialDebug) { + DPRINT(DBG_INFO,F("APPFW_MINVER ")); + DBGPRINTLN(String((p->packet[10] << 8) + p->packet[11])); + DPRINT(DBG_INFO,F("HWInfoAddr ")); + DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13])); + DPRINT(DBG_INFO,F("PNInfoCRC_gusv ")); + DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15])); + //} + //mPayload[iv->id].multi_parts++; + multi_parts++; + } + if(multi_parts > 5) { + cmdDone(true); + mState = States::RESET; + q->iv->radioStatistics.rxSuccess++; + mHeu.setGotAll(q->iv); + } else { + mHeu.setGotFragment(q->iv); + } + + /*if (mPayload[iv->id].multi_parts > 5) { + iv->setQueuedCmdFinished(); + mPayload[iv->id].complete = true; + mPayload[iv->id].rxTmo = true; + mPayload[iv->id].requested= false; + iv->radioStatistics.rxSuccess++; + } + if (mHighPrioIv == NULL) + mHighPrioIv = iv; + */ + } + + inline void miDataDecode(packet_t *p, const queue_s *q) { + record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser + rec->ts = q->ts; + q->iv->radioStatistics.rxSuccess++; + mState = States::RESET; + + uint8_t datachan = ( p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES) || p->packet[0] == (MI_REQ_4CH + ALL_FRAMES) ) ? CH1 : + ( p->packet[0] == (MI_REQ_CH2 + ALL_FRAMES) || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 : + p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 : + CH4; + // count in RF_communication_protocol.xlsx is with offset = -1 + q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10); + + q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_IDC, rec), rec, (float)((p->packet[11] << 8) + p->packet[12])/10); + + q->iv->setValue(q->iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[13] << 8) + p->packet[14])/10); + + q->iv->setValue(q->iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[15] << 8) + p->packet[16])/100); + q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_PDC, rec), rec, (float)((p->packet[17] << 8) + p->packet[18])/10); + + q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_YD, rec), rec, (float)((p->packet[19] << 8) + p->packet[20])/1); + + q->iv->setValue(q->iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[21] << 8) + p->packet[22])/10); + q->iv->setValue(q->iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(q->iv, datachan))); + //mPayload[q->iv->id].rssi[(datachan-1)] = p->rssi; + + if (datachan == 1) //mPayload[q->iv->id].rssi[(datachan-1)] = p->rssi; + q->iv->rssi = p->rssi; + else if(q->iv->rssi > p->rssi) + q->iv->rssi = p->rssi; + + if (p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES) ) { + /*For MI1500: + if (MI1500) { + STAT = (uint8_t)(p->packet[25] ); + FCNT = (uint8_t)(p->packet[26]); + FCODE = (uint8_t)(p->packet[27]); + }*/ + miStsConsolidate(q, datachan, rec, p->packet[23], p->packet[24]); + + if (p->packet[0] < (0x39 + ALL_FRAMES) ) { + //addImportant(q->iv, (q->cmd + 1)); + //mPayload[iv->id].txCmd++; + //mPayload[iv->id].retransmits = 0; // reserve retransmissions for each response + //mPayload[iv->id].complete = false; + miNextRequest((p->packet[0] - ALL_FRAMES + 1), q); + mHeu.setGotFragment(q->iv); + } else { + miComplete(q->iv); + } + } else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) { + //addImportant(q->iv, MI_REQ_CH2); + miNextRequest(MI_REQ_CH2, q); + mHeu.setGotFragment(q->iv); + } else { // first data msg for 1ch, 2nd for 2ch + miComplete(q->iv); + } + } + + inline void miNextRequest(uint8_t cmd, const queue_s *q) { + //setAttempt(); + DPRINT_IVID(DBG_WARN, q->iv->id); + DBGPRINT(F("next request (")); + DBGPRINT(String(q->attempts)); + DBGPRINT(F(" attempts left): 0x")); + DBGHEXLN(cmd); + + if(q->attempts) { + q->iv->radio->sendCmdPacket(q->iv, cmd, 0x00, true); + q->iv->radioStatistics.retransmits++; + mWaitTimeout = millis() + MI_TIMEOUT; + //chgCmd(Inverter<> *iv, uint8_t cmd, bool delOnPop = true) + chgCmd(cmd); + mState = States::WAIT; + } else { + add(q, true); + cmdDone(); + mState = States::RESET; + } + } + + inline void miStsConsolidate(const queue_s *q, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) { + //uint8_t status = (p->packet[11] << 8) + p->packet[12]; + uint16_t statusMi = 3; // regular status for MI, change to 1 later? + if ( uState == 2 ) { + statusMi = 5050 + stschan; //first approach, needs review! + if (lState) + statusMi += lState*10; + } else if ( uState > 3 ) { + statusMi = uState*1000 + uEnum*10; + if (lState) + statusMi += lState*100; //needs review, esp. for 4ch-8310 state! + //if (lEnum) + statusMi += lEnum; + if (uEnum < 6) { + statusMi += stschan; + } + if (statusMi == 8000) + statusMi = 8310; //trick? + } + + uint16_t prntsts = statusMi == 3 ? 1 : statusMi; + bool stsok = true; + if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed? + q->iv->alarmCnt = 1; // minimum... + stsok = false; + //sth is or was wrong? + if ( (q->iv->type != INV_TYPE_1CH) && ( (statusMi != 3) + || ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1))) + ) { + q->iv->lastAlarm[stschan+q->iv->type==INV_TYPE_2CH ? 2: 4] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts); + q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0); + q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5; + } else if ( (q->iv->type == INV_TYPE_1CH) && ( (statusMi != 3) + || ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1))) + ) { + q->iv->lastAlarm[stschan] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts); + } else if (q->iv->type == INV_TYPE_1CH) + stsok = true; + + q->iv->alarmLastId = prntsts; //iv->alarmMesIndex; + + if (q->iv->alarmCnt > 1) { //more than one channel + for (uint8_t ch = 0; ch < (q->iv->alarmCnt); ++ch) { //start with 1 + if (q->iv->lastAlarm[ch].code == 1) { + stsok = true; + break; + } + } + } + //if (mSerialDebug) { + DPRINT(DBG_WARN, F("New state on CH")); + DBGPRINT(String(stschan)); DBGPRINT(F(" (")); + DBGPRINT(String(prntsts)); DBGPRINT(F("): ")); + DBGPRINTLN(q->iv->getAlarmStr(prntsts)); + //} + } + + if (!stsok) { + q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); + q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0); + } + + if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) { + q->iv->alarmMesIndex = rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!? + //if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINT(F("alarm ID incremented to ")); + DBGPRINTLN(String(q->iv->alarmMesIndex)); + //} + } + } + + + inline void miComplete(Inverter<> *iv) { + //if ( mPayload[iv->id].complete ) + // return; //if we got second message as well in repreated attempt + //mPayload[iv->id].complete = true; + //if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("got all data msgs")); + //} + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); + + //preliminary AC calculation... + float ac_pow = 0; + if (iv->type == INV_TYPE_1CH) { + if ((!iv->lastAlarm[0].code) || (iv->lastAlarm[0].code == 1)) + ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec); + } else { + for(uint8_t i = 1; i <= iv->channels; i++) { + if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) { + uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); + ac_pow += iv->getValue(pos, rec); + } + } + } + ac_pow = (int) (ac_pow*9.5); + iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10); + + iv->doCalculations(); + // update status state-machine, + if (ac_pow) + iv->isProducing(); + mHeu.setGotAll(iv); + cmdDone(true); + if(NULL != mCbPayload) + (mCbPayload)(RealTimeRunData_Debug, iv); + + mState = States::RESET; // everything ok, next request + } + + private: + enum class States : uint8_t { + RESET, START, WAIT, CHECK_FRAMES, CHECK_PACKAGE + }; + + typedef struct { + uint8_t buf[MAX_RF_PAYLOAD_SIZE]; + uint8_t len; + int8_t rssi; + } frame_t; + + private: + States mState = States::RESET; + uint32_t *mTimestamp; + uint32_t mWaitTimeout = 0; + std::array mLocalBuf; + bool mGotFragment = false; + bool mFirstTry = false; + uint8_t mMaxFrameId; + uint8_t mPayload[MAX_BUFFER]; + payloadListenerType mCbPayload = NULL; + alarmListenerType mCbAlarm = NULL; + Heuristic mHeu; +}; + +#endif /*__COMMUNICATION_H__*/ diff --git a/src/hm/Heuristic.h b/src/hm/Heuristic.h new file mode 100644 index 00000000..7bf9f4a9 --- /dev/null +++ b/src/hm/Heuristic.h @@ -0,0 +1,111 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __HEURISTIC_H__ +#define __HEURISTIC_H__ + +#include "../utils/dbg.h" +#include "hmInverter.h" + +#define RF_MAX_CHANNEL_ID 5 +#define RF_MAX_QUALITY 4 +#define RF_MIN_QUALTIY -6 + +class Heuristic { + public: + uint8_t getTxCh(Inverter<> *iv) { + if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) + return 0; // not used for these inverter types + + mCycle++; // intended to overflow from time to time + if(mTestEn) { + iv->txRfChId = mCycle % RF_MAX_CHANNEL_ID; + DPRINTLN(DBG_INFO, F("heuristic test mode")); + return id2Ch(iv->txRfChId); + } + + uint8_t id = 0; + int8_t bestQuality = -6; + for(uint8_t i = 0; i < RF_MAX_CHANNEL_ID; i++) { + if(iv->txRfQuality[i] > bestQuality) { + bestQuality = iv->txRfQuality[i]; + id = i; + } + } + if(bestQuality == -6) + iv->txRfChId = (iv->txRfChId + 1) % RF_MAX_CHANNEL_ID; // next channel + else + iv->txRfChId = id; // best quality channel + + return id2Ch(iv->txRfChId); + } + + void setGotAll(Inverter<> *iv) { + updateQuality(iv, 2); // GOOD + mTestEn = false; + } + + void setGotFragment(Inverter<> *iv) { + updateQuality(iv, 1); // OK + mTestEn = false; + } + + void setGotNothing(Inverter<> *iv) { + if(!mTestEn) { + updateQuality(iv, -2); // BAD + mTestEn = true; + iv->txRfChId = (iv->txRfChId + 1) % RF_MAX_CHANNEL_ID; + } + } + + void printStatus(Inverter<> *iv) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("CH qualities:")); + for(uint8_t i = 0; i < RF_MAX_CHANNEL_ID; i++) { + DBGPRINT(F(" ")); + DBGPRINT(String(iv->txRfQuality[i])); + } + DBGPRINT(F(" | t: ")); + DBGPRINT(String(iv->radioStatistics.txCnt)); + DBGPRINT(F(", s: ")); + DBGPRINT(String(iv->radioStatistics.rxSuccess)); + DBGPRINT(F(", f: ")); + DBGPRINT(String(iv->radioStatistics.rxFail)); + DBGPRINT(F(", n: ")); + DBGPRINTLN(String(iv->radioStatistics.rxFailNoAnser)); + } + + bool getTestModeEnabled(void) { + return mTestEn; + } + + private: + void updateQuality(Inverter<> *iv, uint8_t quality) { + iv->txRfQuality[iv->txRfChId] += quality; + if(iv->txRfQuality[iv->txRfChId] > RF_MAX_QUALITY) + iv->txRfQuality[iv->txRfChId] = RF_MAX_QUALITY; + else if(iv->txRfQuality[iv->txRfChId] < RF_MIN_QUALTIY) + iv->txRfQuality[iv->txRfChId] = RF_MIN_QUALTIY; + } + + inline uint8_t id2Ch(uint8_t id) { + switch(id) { + case 0: return 3; + case 1: return 23; + case 2: return 40; + case 3: return 61; + case 4: return 75; + } + return 0; // standard + } + + private: + uint8_t mChList[5] = {03, 23, 40, 61, 75}; + bool mTestEn = false; + uint8_t mCycle = 0; +}; + + +#endif /*__HEURISTIC_H__*/ diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 72ab5c4c..c00f4940 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -77,6 +77,15 @@ enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6}; enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH}; +#define WORK_FREQ_KHZ 865000 // desired 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 + typedef struct { uint8_t fieldId; // field id @@ -337,4 +346,8 @@ const devInfo_t devInfo[] = { { 0x103331, 2250 } }; +#define MI_REQ_CH1 0x09 +#define MI_REQ_CH2 0x11 +#define MI_REQ_4CH 0x36 + #endif /*__HM_DEFINES_H__*/ diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 238e80e4..f3d149c4 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -15,6 +15,7 @@ #include "../hms/hmsDefines.h" #include #include +#include #include "../config/settings.h" #include "radio.h" @@ -77,31 +78,6 @@ struct alarm_t { alarm_t() : code(0), start(0), end(0) {} }; -class CommandAbstract { - public: - CommandAbstract(uint8_t txType = 0, uint8_t cmd = 0) { - _TxType = txType; - _Cmd = cmd; - }; - virtual ~CommandAbstract() {}; - - uint8_t getCmd() const { - return _Cmd; - } - - protected: - uint8_t _TxType; - uint8_t _Cmd; -}; - -class InfoCommand : public CommandAbstract { - public: - InfoCommand(uint8_t cmd){ - _TxType = 0x15; - _Cmd = cmd; - } -}; - // list of all available functions, mapped in hmDefines.h template const calcFunc_t calcFunctions[] = { @@ -142,7 +118,6 @@ class Inverter { record_t recordHwInfo; // structure for simple (hardware) info values record_t recordConfig; // structure for system config values record_t recordAlarm; // structure for alarm values - 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 std::array lastAlarm; // holds last 10 alarms @@ -152,7 +127,10 @@ class Inverter { int8_t rssi; // RSSI Radio *radio; // pointer to associated radio class statistics_t radioStatistics; // information about transmitted, failed, ... packets - + int8_t txRfQuality[5]; // heuristics tx quality (check 'Heuristics.h') + uint8_t txRfChId; // RF TX channel id + uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime + bool commEnabled; // 'pause night communication' sets this field to false static uint32_t *timestamp; // system timestamp static cfgInst_t *generalConfig; // general inverter configuration from setup @@ -165,7 +143,6 @@ class Inverter { actPowerLimit = 0xffff; // init feedback from inverter to -1 mDevControlRequest = false; devControlCmd = InitDataState; - initialized = false; alarmMesIndex = 0; isConnected = false; status = InverterStatus::OFF; @@ -173,64 +150,46 @@ class Inverter { alarmCnt = 0; alarmLastId = 0; rssi = -127; + radio = NULL; + commEnabled = true; memset(&radioStatistics, 0, sizeof(statistics_t)); - } - - ~Inverter() { - // TODO: cleanup - } - - template - void enqueCommand(uint8_t cmd) { - _commandQueue.push(std::make_shared(cmd)); - DPRINT_IVID(DBG_INFO, id); - DBGPRINT(F("enqueCommand: 0x")); - DBGHEXLN(cmd); - } - - void setQueuedCmdFinished() { - if (!_commandQueue.empty()) { - _commandQueue.pop(); - } - } - - void clearCmdQueue() { - DPRINTLN(DBG_INFO, F("clearCmdQueue")); - while (!_commandQueue.empty()) { - _commandQueue.pop(); - } - } - - uint8_t getQueuedCmd() { - if (_commandQueue.empty()) { - if (ivGen != IV_MI) { - if (getFwVersion() == 0) - enqueCommand(InverterDevInform_All); // firmware version - else if (getHwVersion() == 0) - enqueCommand(InverterDevInform_Simple); // hardware version - else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) - enqueCommand(AlarmData); // alarm not answered - enqueCommand(RealTimeRunData_Debug); // live data - } else { // if (ivGen == IV_MI){ - if (getFwVersion() == 0) { - enqueCommand(InverterDevInform_All); // hard- and firmware version - } else { - record_t<> *rec = getRecordStruct(InverterDevInform_Simple); - if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) { - enqueCommand(InverterDevInform_All); // hard- and firmware version for missing HW part nr, delivered by frame 1 - } else { - enqueCommand( type == INV_TYPE_4CH ? 0x36 : 0x09 ); - } - } + memset(txRfQuality, -6, 5); + + memset(mOffYD, 0, sizeof(float) * 6); + memset(mLastYD, 0, sizeof(float) * 6); + } + + void tickSend(std::function cb) { + if(mDevControlRequest) { + cb(devControlCmd, true); + mDevControlRequest = false; + } else if (IV_MI != ivGen) { + if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) + cb(AlarmData, false); // get last alarms + else if(0 == getFwVersion()) + cb(InverterDevInform_All, false); // get firmware version + else if(0 == getHwVersion()) + cb(InverterDevInform_Simple, false); // get hardware version + else if(actPowerLimit == 0xffff) + cb(SystemConfigPara, false); // power limit info + else if(InitDataState != devControlCmd) { + cb(devControlCmd, false); // custom command which was received by API + devControlCmd = InitDataState; + } else + cb(RealTimeRunData_Debug, false); // get live data + } else { + if(0 == getFwVersion()) + cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number + else { + record_t<> *rec = getRecordStruct(InverterDevInform_Simple); + if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) + cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1 + else + cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false); } - - if ((actPowerLimit == 0xffff) && isConnected) - enqueCommand(SystemConfigPara); // power limit info } - return _commandQueue.front().get()->getCmd(); } - void init(void) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init")); initAssignment(&recordMeas, RealTimeRunData_Debug); @@ -239,7 +198,7 @@ class Inverter { initAssignment(&recordConfig, SystemConfigPara); initAssignment(&recordAlarm, AlarmData); toRadioId(); - initialized = true; + curCmtFreq = this->config->frequency; // update to frequency read from settings } uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) { @@ -289,12 +248,10 @@ class Inverter { return isConnected; } - void clearDevControlRequest() { - mDevControlRequest = false; - } - - inline bool getDevControlRequest() { - return mDevControlRequest; + bool setDevCommand(uint8_t cmd) { + if(isConnected) + devControlCmd = cmd; + return isConnected; } void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) { @@ -317,7 +274,12 @@ class Inverter { } else if (FLD_YT == rec->assign[pos].fieldId) { 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; + float actYD = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency; + uint8_t idx = rec->assign[pos].ch - 1; + if (mLastYD[idx] > actYD) + mOffYD[idx] += mLastYD[idx]; + mLastYD[idx] = actYD; + rec->record[pos] = mOffYD[idx] + actYD; } else { if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); @@ -334,11 +296,9 @@ class Inverter { if (getPosByChFld(0, FLD_EVT, rec) == pos) { if (alarmMesIndex < rec->record[pos]) { alarmMesIndex = rec->record[pos]; - //enqueCommand(AlarmUpdate); // What is the function of AlarmUpdate? DPRINT(DBG_INFO, "alarm ID incremented to "); DBGPRINTLN(String(alarmMesIndex)); - enqueCommand(AlarmData); } } } @@ -593,6 +553,9 @@ class Inverter { alarmNxtWrPos = 0; alarmCnt = 0; alarmLastId = 0; + + memset(mOffYD, 0, sizeof(float) * 6); + memset(mLastYD, 0, sizeof(float) * 6); } uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len) { @@ -604,9 +567,9 @@ class Inverter { uint32_t startTimeOffset = 0, endTimeOffset = 0; uint32_t start, endTime; - if (((wCode >> 13) & 0x01) == 1) // check if is AM or PM - startTimeOffset = 12 * 60 * 60; - if (((wCode >> 12) & 0x01) == 1) // check if is AM or PM + // check if is AM or PM + startTimeOffset = ((wCode >> 13) & 0x01) * 12 * 60 * 60; + if (((wCode >> 12) & 0x03) != 0) endTimeOffset = 12 * 60 * 60; start = (((uint16_t)pyld[startOff + 4] << 8) | ((uint16_t)pyld[startOff + 5])) + startTimeOffset; @@ -715,8 +678,9 @@ class Inverter { radioId.b[0] = 0x01; } - std::queue> _commandQueue; - bool mDevControlRequest; // true if change needed + private: + float mOffYD[6], mLastYD[6]; + bool mDevControlRequest; // true if change needed }; template diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h deleted file mode 100644 index 0efa28a4..00000000 --- a/src/hm/hmPayload.h +++ /dev/null @@ -1,494 +0,0 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de -// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed -//----------------------------------------------------------------------------- - -#ifndef __HM_PAYLOAD_H__ -#define __HM_PAYLOAD_H__ - -#include "../utils/dbg.h" -#include "../utils/crc.h" -#include "../config/config.h" -#include "hmRadio.h" -#if defined(ESP32) -#include "../hms/cmt2300a.h" -#endif -#include - -#define HMS_TIMEOUT_SEC 30 - -typedef struct { - uint8_t txCmd; - uint8_t txId; - uint32_t ts; - uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; - int8_t rssi[MAX_PAYLOAD_ENTRIES]; - uint8_t len[MAX_PAYLOAD_ENTRIES]; - bool complete; - uint8_t maxPackId; - bool lastFound; - uint8_t retransmits; - bool requested; - bool gotFragment; - bool rxTmo; - uint32_t sendMillis; -} invPayload_t; - - -typedef std::function *)> payloadListenerType; -typedef std::function *)> alarmListenerType; - - -template -class HmPayload { - public: - HmPayload() {} - - void setup(IApp *app, HMSYSTEM *sys, uint8_t maxRetransmits, uint32_t *timestamp) { - mApp = app; - mSys = sys; - 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; - } - - 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); // for e.g. devcontrol commands - mHighPrioIv = NULL; - } - } - - /*void simulation() { - uint8_t pay[] = { - 0x00, 0x01, 0x01, 0x24, 0x02, 0x28, 0x02, 0x33, - 0x06, 0x49, 0x06, 0x6a, 0x00, 0x05, 0x5f, 0x1b, - 0x00, 0x06, 0x66, 0x9a, 0x03, 0xfd, 0x04, 0x0b, - 0x01, 0x23, 0x02, 0x28, 0x02, 0x28, 0x06, 0x41, - 0x06, 0x43, 0x00, 0x05, 0xdc, 0x2c, 0x00, 0x06, - 0x2e, 0x3f, 0x04, 0x01, 0x03, 0xfb, 0x09, 0x78, - 0x13, 0x86, 0x18, 0x15, 0x00, 0xcf, 0x00, 0xfe, - 0x03, 0xe7, 0x01, 0x42, 0x00, 0x03 - }; - - Inverter<> *iv = mSys->getInverterByPos(0); - record_t<> *rec = iv->getRecordStruct(0x0b); - rec->ts = *mTimestamp; - for (uint8_t i = 0; i < rec->length; i++) { - iv->addValue(i, pay, rec); - yield(); - } - iv->doCalculations(); - notify(0x0b, iv); - }*/ - - void ivSendHighPrio(Inverter<> *iv) { - mHighPrioIv = iv; - } - - void ivSend(Inverter<> *iv, bool highPrio = false) { - if(!highPrio) { - if (mPayload[iv->id].requested) { - if (!mPayload[iv->id].complete) - process(false); // no retransmit - - if (!mPayload[iv->id].complete) { - if (mSerialDebug) - DPRINT_IVID(DBG_INFO, iv->id); - if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) { - iv->radioStatistics.rxFailNoAnser++; // got nothing - if (mSerialDebug) - DBGPRINTLN(F("enqueued cmd failed/timeout")); - } else { - iv->radioStatistics.rxFail++; // got fragments but not complete response - if (mSerialDebug) { - DBGPRINT(F("no complete Payload received! (retransmits: ")); - DBGPRINT(String(mPayload[iv->id].retransmits)); - DBGPRINTLN(F(")")); - } - } - iv->setQueuedCmdFinished(); // command failed - } - } - } - - reset(iv->id, !iv->isAvailable()); - 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)); - } - - if (iv->getDevControlRequest()) { - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("Devcontrol request 0x")); - DHEX(iv->devControlCmd); - DBGPRINT(F(" power limit ")); - DBGPRINTLN(String(iv->powerLimit[0])); - } - iv->powerLimitAck = false; - iv->radio->sendControlPacket(iv, iv->devControlCmd, iv->powerLimit, false); - mPayload[iv->id].txCmd = iv->devControlCmd; - //iv->clearCmdQueue(); - //iv->enqueCommand(SystemConfigPara); // read back power limit - } else { - #if defined(ESP32) - if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - if(((rec->ts + HMS_TIMEOUT_SEC) < *mTimestamp) && (mIvCmd56Cnt[iv->id] < 3)) { - iv->radio->switchFrequency(iv, HOY_BOOT_FREQ_KHZ, WORK_FREQ_KHZ); - mIvCmd56Cnt[iv->id]++; - return; - } else if(++mIvCmd56Cnt[iv->id] == 10) - mIvCmd56Cnt[iv->id] = 0; - } - #endif - uint8_t cmd = iv->getQueuedCmd(); - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(cmd); - } - iv->radio->prepareDevInformCmd(iv, cmd, mPayload[iv->id].ts, iv->alarmLastId, false); - mPayload[iv->id].txCmd = cmd; - } - } - - void add(Inverter<> *iv, packet_t *p) { - if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command - mPayload[iv->id].txId = p->packet[0]; - DPRINTLN(DBG_DEBUG, F("Response from info request received")); - uint8_t *pid = &p->packet[9]; - if (*pid == 0x00) { - DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored")); - } else { - DPRINT(DBG_DEBUG, F("PID: 0x")); - DPRINTLN(DBG_DEBUG, String(*pid, HEX)); - if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { - memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); - mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; - mPayload[iv->id].gotFragment = true; - mPayload[iv->id].rssi[(*pid & 0x7F) - 1] = p->rssi; - } - - 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->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command - DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); - - mPayload[iv->id].txId = p->packet[0]; - iv->clearDevControlRequest(); - - if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { - bool ok = true; - if((p->packet[10] == 0x00) && (p->packet[11] == 0x00)) { - mApp->setMqttPowerLimitAck(iv); - iv->powerLimitAck = true; - } else - ok = false; - - if (mSerialDebug) { - 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 - if(mHighPrioIv == NULL) // do it immediately if possible - mHighPrioIv = iv; - } - 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 ((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((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) { - if((mPayload[iv->id].sendMillis + 400) > millis()) - return; // to fast, wait until packets are received! - } - - if (!mPayload[iv->id].complete) { - bool crcPass, pyldComplete, fastNext; - - crcPass = build(iv, &pyldComplete, &fastNext); - 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) { - DPRINT_IVID(DBG_INFO, iv->id); - DPRINTLN(DBG_INFO, F("retransmit power limit")); - iv->radio->sendControlPacket(iv, iv->devControlCmd, iv->powerLimit, true); - } else { - if(false == mPayload[iv->id].gotFragment) { - DPRINT_IVID(DBG_WARN, iv->id); - if (mPayload[iv->id].rxTmo) { - DBGPRINTLN(F("nothing received")); - mPayload[iv->id].retransmits = mMaxRetrans; - } else { - DBGPRINTLN(F("nothing received: complete retransmit")); - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); - iv->radio->prepareDevInformCmd(iv, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); - } - } else { - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { - if (mPayload[iv->id].len[i] == 0) { - if (mSerialDebug) { - DPRINT_IVID(DBG_WARN, iv->id); - DBGPRINT(F("Frame ")); - DBGPRINT(String(i + 1)); - DBGPRINTLN(F(" missing: Request Retransmit")); - } - iv->radio->sendCmdPacket(iv, TX_REQ_INFO, (SINGLE_FRAME + i), true); - break; // only request retransmit one frame per loop - } - yield(); - } - } - } - } - } else if (false == mPayload[iv->id].gotFragment) { - // only if there is no sign of life - mPayload[iv->id].rxTmo = true; // inv might be down, no complete retransmit anymore - } - } else if(!crcPass && pyldComplete) { // crc error on complete Payload - if (mPayload[iv->id].retransmits < mMaxRetrans) { - mPayload[iv->id].retransmits++; - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - if (mSerialDebug) { - DPRINT_IVID(DBG_WARN, iv->id); - DBGPRINTLN(F("CRC Error: Request Complete Retransmit")); - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - } - iv->radio->prepareDevInformCmd(iv, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmLastId, true); - } - } else { // payload complete - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("procPyld: cmd: 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - //DPRINT(DBG_DEBUG, F("procPyld: txid: 0x")); - //DBGHEXLN(mPayload[iv->id].txId); - DPRINT(DBG_DEBUG, F("procPyld: max: ")); - DPRINTLN(DBG_DEBUG, String(mPayload[iv->id].maxPackId)); - } - record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser - mPayload[iv->id].complete = true; - mPayload[iv->id].requested = false; - mPayload[iv->id].rxTmo = false; - - uint8_t payload[150]; - uint8_t payloadLen = 0; - - memset(payload, 0, 150); - - int8_t rssi = -127; - - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { - if((mPayload[iv->id].len[i] + payloadLen) > 150) { - DPRINTLN(DBG_ERROR, F("payload buffer to small!")); - break; - } - memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); - payloadLen += (mPayload[iv->id].len[i]); - // get worst RSSI - if(mPayload[iv->id].rssi[i] > rssi) - rssi = mPayload[iv->id].rssi[i]; - yield(); - } - payloadLen -= 2; - - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(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)) - iv->radioStatistics.rxSuccess++; - - rec->ts = mPayload[iv->id].ts; - for (uint8_t i = 0; i < rec->length; i++) { - iv->addValue(i, payload, rec); - yield(); - } - iv->rssi = rssi; - iv->doCalculations(); - notify(mPayload[iv->id].txCmd, iv); - - if(AlarmData == mPayload[iv->id].txCmd) { - uint8_t i = 0; - while(1) { - if(0 == iv->parseAlarmLog(i++, payload, payloadLen)) - break; - if (NULL != mCbAlarm) - (mCbAlarm)(iv); - yield(); - } - } - if (fastNext && (mHighPrioIv == NULL)) { - /*iv->setQueuedCmdFinished(); - uint8_t cmd = iv->getQueuedCmd(); - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("fast mode ")); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(cmd); - } - iv->radioStatistics.rxSuccess++; - iv->radio->prepareDevInformCmd(iv, cmd, mPayload[iv->id].ts, iv->alarmLastId, false); - mPayload[iv->id].txCmd = cmd; - */ - mHighPrioIv = iv; - } - - } else { - if (mSerialDebug) { - DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); - DBGPRINT(String(rec->pyldLen)); - DBGPRINTLN(F(" bytes")); - } - iv->radioStatistics.rxFail++; - } - - iv->setQueuedCmdFinished(); - } - } - yield(); - } - } - - private: - void notify(uint8_t val, Inverter<> *iv) { - if(NULL != mCbPayload) - (mCbPayload)(val, iv); - } - - bool build(Inverter<> *iv, bool *complete, bool *fastNext ) { - DPRINTLN(DBG_VERBOSE, F("build")); - uint16_t crc = 0xffff, crcRcv = 0x0000; - if (mPayload[iv->id].maxPackId > MAX_PAYLOAD_ENTRIES) - mPayload[iv->id].maxPackId = MAX_PAYLOAD_ENTRIES; - - // check if all fragments are there - *complete = true; - *fastNext = false; - for (uint8_t i = 0; i < mPayload[iv->id].maxPackId; i++) { - if(mPayload[iv->id].len[i] == 0) { - *complete = false; - } - } - if(!*complete) - return false; - - for (uint8_t i = 0; i < mPayload[iv->id].maxPackId; i++) { - if (mPayload[iv->id].len[i] > 0) { - if (i == (mPayload[iv->id].maxPackId - 1)) { - crc = ah::crc16(mPayload[iv->id].data[i], mPayload[iv->id].len[i] - 2, crc); - crcRcv = (mPayload[iv->id].data[i][mPayload[iv->id].len[i] - 2] << 8) | (mPayload[iv->id].data[i][mPayload[iv->id].len[i] - 1]); - } else - crc = ah::crc16(mPayload[iv->id].data[i], mPayload[iv->id].len[i], crc); - } - yield(); - } - - //return (crc == crcRcv) ? true : false; - if (crc != crcRcv) - return false; - - //requests to cause the next request to be executed immediately - if (mPayload[iv->id].gotFragment && ((mPayload[iv->id].txCmd < RealTimeRunData_Debug) || (mPayload[iv->id].txCmd >= AlarmData))) { - *fastNext = true; - } - - return true; - } - - void reset(uint8_t id, bool setTxTmo = true) { - //DPRINT_IVID(DBG_INFO, id); - //DBGPRINTLN(F("resetPayload")); - memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); - 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; - mPayload[id].rxTmo = setTxTmo; // design: don't start with complete retransmit - mPayload[id].sendMillis = millis(); - } - - IApp *mApp; - HMSYSTEM *mSys; - uint8_t mMaxRetrans; - uint32_t *mTimestamp; - invPayload_t mPayload[MAX_NUM_INVERTERS]; - uint8_t mIvCmd56Cnt[MAX_NUM_INVERTERS]; - bool mSerialDebug; - Inverter<> *mHighPrioIv; - - alarmListenerType mCbAlarm; - payloadListenerType mCbPayload; -}; - -#endif /*__HM_PAYLOAD_H__*/ diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index a3368c0f..a55c7d3c 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -40,24 +40,12 @@ class HmRadio : public Radio { } mDtuSn = DTU_SN; - // Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz. - // Channel List 2403, 2423, 2440, 2461, 2475MHz - mRfChLst[0] = 03; - mRfChLst[1] = 23; - mRfChLst[2] = 40; - mRfChLst[3] = 61; - mRfChLst[4] = 75; - - // default channels - mTxChIdx = 2; // Start TX with 40 - mRxChIdx = 0; // Start RX with 03 - mSerialDebug = false; mIrqRcvd = false; } ~HmRadio() {} - void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { + void setup(uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); pinMode(irq, INPUT_PULLUP); @@ -76,6 +64,7 @@ class HmRadio : public Radio { mSpi = new SPIClass(); mSpi->begin(); #endif + mNrf24.begin(mSpi, ce, cs); mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms @@ -92,9 +81,7 @@ class HmRadio : public Radio { // enable all receiving interrupts mNrf24.maskIRQ(false, false, false); - DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); - DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); - mNrf24.setPALevel(ampPwr & 0x03); + mNrf24.setPALevel(1); // low is default if(mNrf24.isChipConnected()) { DPRINTLN(DBG_INFO, F("Radio Config:")); @@ -105,39 +92,47 @@ class HmRadio : public Radio { DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); } - bool loop(void) { + void loop(void) { if (!mIrqRcvd) - return false; // nothing to do + return; // nothing to do mIrqRcvd = false; bool tx_ok, tx_fail, rx_ready; mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH mNrf24.flush_tx(); // empty TX FIFO // start listening + //mNrf24.setChannel(23); + //mRxChIdx = 0; mNrf24.setChannel(mRfChLst[mRxChIdx]); mNrf24.startListening(); - uint32_t startMicros = micros(); - uint32_t loopMillis = millis(); - while (millis()-loopMillis < 400) { - while (micros()-startMicros < 5110) { // listen (4088us or?) 5110us to each channel + if(NULL == mLastIv) // prevent reading on NULL object! + return; + + uint32_t startMicros = micros() + 5110; + uint32_t loopMillis = millis() + 400; + while (millis() < loopMillis) { + while (micros() < startMicros) { // listen (4088us or?) 5110us to each channel if (mIrqRcvd) { mIrqRcvd = false; - if (getReceived()) { // everything received - return true; + + if (getReceived()) { // everything received + return; } } yield(); } // switch to next RX channel - startMicros = micros(); if(++mRxChIdx >= RF_CHANNELS) mRxChIdx = 0; mNrf24.setChannel(mRfChLst[mRxChIdx]); - yield(); + startMicros = micros() + 5110; } // not finished but time is over - return true; + if(++mRxChIdx >= RF_CHANNELS) + mRxChIdx = 0; + + return; } bool isChipConnected(void) { @@ -145,12 +140,13 @@ class HmRadio : public Radio { return mNrf24.isChipConnected(); } - void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true, uint16_t powerMax = 0) { - DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("sendControlPacket cmd: 0x")); DBGHEXLN(cmd); initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME); uint8_t cnt = 10; - if (isNoMI) { + if (IV_MI != iv->ivGen) { 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 @@ -160,6 +156,7 @@ class HmRadio : public Radio { mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling } } else { //MI 2nd gen. specific + uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower()); switch (cmd) { case Restart: case TurnOn: @@ -225,7 +222,7 @@ class HmRadio : public Radio { } cnt++; } - sendPacket(iv, cnt, isRetransmit, isNoMI); + sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen)); } uint8_t getDataRate(void) { @@ -238,8 +235,6 @@ class HmRadio : public Radio { return mNrf24.isPVariant(); } - std::queue mBufCtrl; - private: inline bool getReceived(void) { bool tx_ok, tx_fail, rx_ready; @@ -254,6 +249,7 @@ class HmRadio : public Radio { p.ch = mRfChLst[mRxChIdx]; p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len; p.rssi = mNrf24.testRPD() ? -64 : -75; + p.millis = millis() - mMillis; mNrf24.read(p.packet, p.len); if (p.packet[0] != 0x00) { mBufCtrl.push(p); @@ -271,43 +267,48 @@ class HmRadio : public Radio { } void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) { + mNrf24.setPALevel(iv->config->powerLevel & 0x03); updateCrcs(&len, appendCrc16); // set TX and RX channels - mTxChIdx = (mTxChIdx + 1) % RF_CHANNELS; - mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; + mTxChIdx = mRfChLst[iv->txRfChId]; if(mSerialDebug) { - DPRINT(DBG_INFO, F("TX ")); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("TX ")); DBGPRINT(String(len)); DBGPRINT(" CH"); - DBGPRINT(String(mRfChLst[mTxChIdx])); + DBGPRINT(String(mTxChIdx)); DBGPRINT(F(" | ")); ah::dumpBuf(mTxBuf, len); } mNrf24.stopListening(); - mNrf24.setChannel(mRfChLst[mTxChIdx]); + mNrf24.setChannel(mTxChIdx); mNrf24.openWritingPipe(reinterpret_cast(&iv->radioId.u64)); mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response + mMillis = millis(); - if(isRetransmit) - iv->radioStatistics.retransmits++; - else - iv->radioStatistics.txCnt++; + mLastIv = iv; } uint64_t getIvId(Inverter<> *iv) { return iv->radioId.u64; } + uint8_t getIvGen(Inverter<> *iv) { + return iv->ivGen; + } + uint64_t DTU_RADIO_ID; - uint8_t mRfChLst[RF_CHANNELS]; - uint8_t mTxChIdx; - uint8_t mRxChIdx; + uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz + uint8_t mTxChIdx = 0; + uint8_t mRxChIdx = 0; + uint32_t mMillis; SPIClass* mSpi; RF24 mNrf24; + Inverter<> *mLastIv = NULL; }; #endif /*__HM_RADIO_H__*/ diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index a8ca2371..b86f8d08 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -17,17 +17,12 @@ class HmSystem { void setup(uint32_t *timestamp, cfgInst_t *config) { mInverter[0].timestamp = timestamp; mInverter[0].generalConfig = config; - mNumInv = 0; } void addInverter(uint8_t id, std::function *iv)> cb) { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); - if(MAX_INVERTER <= mNumInv) { - DPRINT(DBG_WARN, F("max number of inverters reached!")); - return; - } - INVERTERTYPE *iv = &mInverter[mNumInv]; - iv->id = mNumInv; + INVERTERTYPE *iv = &mInverter[id]; + iv->id = id; iv->config = &mInverter[0].generalConfig->iv[id]; DPRINT(DBG_VERBOSE, "SERIAL: " + String(iv->config->serial.b[5], HEX)); DPRINTLN(DBG_VERBOSE, " " + String(iv->config->serial.b[4], HEX)); @@ -73,7 +68,6 @@ class HmSystem { iv->ivGen = IV_UNKNOWN; iv->init(); - mNumInv ++; if(IV_UNKNOWN == iv->ivGen) return; // serial is 0 @@ -99,7 +93,7 @@ class HmSystem { INVERTERTYPE *findInverter(uint8_t buf[]) { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); INVERTERTYPE *p; - for(uint8_t i = 0; i < mNumInv; i++) { + for(uint8_t i = 0; i < MAX_INVERTER; i++) { p = &mInverter[i]; if((p->config->serial.b[3] == buf[0]) && (p->config->serial.b[2] == buf[1]) @@ -114,7 +108,7 @@ class HmSystem { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); if(pos >= MAX_INVERTER) return NULL; - else if((mInverter[pos].initialized && mInverter[pos].config->serial.u64 != 0ULL) || false == check) + else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check)) return &mInverter[pos]; else return NULL; @@ -134,7 +128,6 @@ class HmSystem { private: INVERTERTYPE mInverter[MAX_INVERTER]; - uint8_t mNumInv; }; #endif /*__HM_SYSTEM_H__*/ diff --git a/src/hm/miPayload.h b/src/hm/miPayload.h deleted file mode 100644 index 32b16696..00000000 --- a/src/hm/miPayload.h +++ /dev/null @@ -1,844 +0,0 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de -// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed -//----------------------------------------------------------------------------- - -#ifndef __MI_PAYLOAD_H__ -#define __MI_PAYLOAD_H__ -#include "../utils/dbg.h" -#include "../utils/crc.h" -#include "../config/config.h" -#include - -#define MI_REQ_CH1 0x09 -#define MI_REQ_CH2 0x11 -#define MI_REQ_4CH 0x36 - -typedef struct { - uint32_t ts; - bool requested; - bool limitrequested; - uint8_t txCmd; - uint8_t len[MAX_PAYLOAD_ENTRIES]; - int8_t rssi[4]; - bool complete; - bool dataAB[3]; - bool stsAB[3]; - uint16_t sts[5]; - uint8_t txId; - uint8_t invId; - uint8_t retransmits; - bool gotFragment; - bool gotGPF; - uint8_t rtrRes; // for limiting resets - uint8_t multi_parts; // for quality - bool rxTmo; -} miPayload_t; - - -typedef std::function *)> miPayloadListenerType; - - -template -class MiPayload { - public: - MiPayload() {} - - void setup(IApp *app, HMSYSTEM *sys, uint8_t maxRetransmits, uint32_t *timestamp) { - mApp = app; - mSys = sys; - mMaxRetrans = maxRetransmits; - mTimestamp = timestamp; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - reset(i, false, true); - mPayload[i].limitrequested = false; - mPayload[i].gotGPF = false; - } - mSerialDebug = false; - mHighPrioIv = NULL; - mCbPayload = NULL; - } - - void enableSerialDebug(bool enable) { - mSerialDebug = enable; - } - - void addPayloadListener(miPayloadListenerType cb) { - mCbPayload = cb; - } - - void addAlarmListener(alarmListenerType cb) { - mCbAlarm = cb; - } - - void loop() { - if (NULL != mHighPrioIv) { - ivSend(mHighPrioIv, true); // for e.g. devcontrol commands - mHighPrioIv = NULL; - } - } - - void ivSendHighPrio(Inverter<> *iv) { - mHighPrioIv = iv; - } - - void ivSend(Inverter<> *iv, bool highPrio = false) { - if(!highPrio) { - if (mPayload[iv->id].requested) { - if (!mPayload[iv->id].complete) - process(false); // no retransmit - - if (!mPayload[iv->id].complete && mPayload[iv->id].rxTmo) { - if (mSerialDebug) - DPRINT_IVID(DBG_INFO, iv->id); - if (!mPayload[iv->id].gotFragment) { - iv->radioStatistics.rxFailNoAnser++; // got nothing - if (mSerialDebug) - DBGPRINTLN(F("enqueued cmd failed/timeout")); - } else { - iv->radioStatistics.rxFail++; // got "fragments" (part of the required messages) - // but no complete set of responses - if (mSerialDebug) { - DBGPRINT(F("no complete Payload received! (retransmits: ")); - DBGPRINT(String(mPayload[iv->id].retransmits)); - DBGPRINTLN(F(")")); - } - } - mPayload[iv->id].rxTmo = true; - mPayload[iv->id].complete = true; - iv->setQueuedCmdFinished(); // command failed - } - } - } - - reset(iv->id, !iv->isAvailable()); - 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)); - } - - if (iv->getDevControlRequest()) { - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("Devcontrol request 0x")); - DHEX(iv->devControlCmd); - DBGPRINT(F(" power limit ")); - DBGPRINT(String(iv->powerLimit[0])); - DBGPRINT(F(" with PowerLimitControl ")); - DBGPRINTLN(String(iv->powerLimit[1])); - } - iv->powerLimitAck = false; - iv->radio->sendControlPacket(iv, iv->devControlCmd, iv->powerLimit, false, false, (iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower()); - mPayload[iv->id].txCmd = iv->devControlCmd; - mPayload[iv->id].limitrequested = true; - - iv->clearCmdQueue(); - } else { - uint8_t cmd = iv->getQueuedCmd(); - uint8_t cmd2 = cmd; - if ( cmd == SystemConfigPara ) { //0x05 for HM-types - if (!mPayload[iv->id].gotGPF) { - iv->setQueuedCmdFinished(); - cmd = iv->getQueuedCmd(); - } - } - - if (cmd == 0x01) { //0x1 for HM-types - cmd2 = 0x00; - cmd = 0x0f; // for MI, these seem to make part of polling the device software and hardware version number command - } else if (cmd == SystemConfigPara ) { // 0x05 for HM-types - cmd2 = 0x00; - cmd = 0x10; // legacy GPF request - } - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("legacy cmd 0x")); - DBGHEXLN(cmd); - } - iv->radio->sendCmdPacket(iv, cmd, cmd2, false, false); - - mPayload[iv->id].txCmd = cmd; - if (iv->type == INV_TYPE_1CH || iv->type == INV_TYPE_2CH) { - mPayload[iv->id].dataAB[CH1] = false; - mPayload[iv->id].stsAB[CH1] = false; - mPayload[iv->id].dataAB[CH0] = false; - mPayload[iv->id].stsAB[CH0] = false; - if (iv->type == INV_TYPE_2CH) { - mPayload[iv->id].dataAB[CH2] = false; - mPayload[iv->id].stsAB[CH2] = false; - } - } - } - } - - void add(Inverter<> *iv, packet_t *p) { - //DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX)); - if (p->packet[0] == (0x88)) { // 0x88 is MI status response to 0x09 - miStsDecode(iv, p); - } - - else if (p->packet[0] == (MI_REQ_CH2 + SINGLE_FRAME)) { // 0x92; MI status response to 0x11 - miStsDecode(iv, p, CH2); - - } else if ( p->packet[0] == MI_REQ_CH1 + ALL_FRAMES || - p->packet[0] == MI_REQ_CH2 + ALL_FRAMES || - ( p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES) && p->packet[0] < (0x39 + SINGLE_FRAME) - && mPayload[iv->id].txCmd != 0x0f) ) { // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 - mPayload[iv->id].txId = p->packet[0]; - miDataDecode(iv,p); - - } else if (p->packet[0] == ( 0x0f + ALL_FRAMES)) { - // MI response from get hardware information request - miHwDecode(iv, p); - mPayload[iv->id].txId = p->packet[0]; - - } else if (p->packet[0] == ( 0x10 + ALL_FRAMES)) { - // MI response from get Grid Profile information request - miGPFDecode(iv, p); - mPayload[iv->id].txId = p->packet[0]; - - } else if ( p->packet[0] == (TX_REQ_INFO + ALL_FRAMES) // response from get information command - || (p->packet[0] == 0xB6 && mPayload[iv->id].txCmd != MI_REQ_4CH)) { // strange short response from MI-1500 3rd gen; might be misleading! - // atm, we just do nothing else than print out what we got... - // for decoding see xls- Data collection instructions - #147ff - //mPayload[iv->id].txId = p->packet[0]; - DPRINTLN(DBG_DEBUG, F("Response from info request received")); - uint8_t *pid = &p->packet[9]; - if (*pid == 0x00) { - DPRINT(DBG_DEBUG, F("fragment number zero received")); - iv->setQueuedCmdFinished(); - } else if (p->packet[9] == 0x81) { // might need some additional check, as this is only meant for short answers! - DPRINT_IVID(DBG_WARN, iv->id); - DBGPRINTLN(F("seems to use 3rd gen. protocol - switching ivGen!")); - iv->ivGen = IV_HM; - iv->setQueuedCmdFinished(); - iv->clearCmdQueue(); - } - } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES ) // response from dev control command - || p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES -1)) { // response from DRED instruction -#if DEBUG_LEVEL >= DBG_DEBUG - if (mSerialDebug) { - DPRINT_IVID(DBG_DEBUG, iv->id); - DBGPRINTLN(F("Response from devcontrol request received")); - } -#endif - mPayload[iv->id].txId = p->packet[0]; - iv->clearDevControlRequest(); - - if ((p->packet[9] == 0x5a) && (p->packet[10] == 0x5a)) { - mApp->setMqttPowerLimitAck(iv); - iv->powerLimitAck = true; - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("has accepted power limit set point ")); - DBGPRINT(String(iv->powerLimit[0])); - DBGPRINT(F(" with PowerLimitControl ")); - DBGPRINTLN(String(iv->powerLimit[1])); - } - iv->clearCmdQueue(); - //does not work for MI - //iv->enqueCommand(SystemConfigPara); // read back power limit - } - iv->devControlCmd = Init; - } else { // some other response; copied from hmPayload:process; might not be correct to do that here!!! - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO,iv->id); - DBGPRINT(F("procPyld: cmd: 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - DPRINT_IVID(DBG_INFO,iv->id); - DBGPRINT(F("procPyld: txid: 0x")); - DBGHEXLN(mPayload[iv->id].txId); - } - - record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser - mPayload[iv->id].complete = true; - - uint8_t payload[128]; - uint8_t payloadLen = 0; - - memset(payload, 0, 128); - - payloadLen -= 2; - - if (mSerialDebug) { - DPRINT(DBG_INFO, F("Payload (")); - DBGPRINT(String(payloadLen)); - DBGPRINT("): "); - 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)) - iv->radioStatistics.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; - while(1) { - if(0 == iv->parseAlarmLog(i++, payload, payloadLen)) - break; - if (NULL != mCbAlarm) - (mCbAlarm)(iv); - yield(); - } - } - } else { - DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); - iv->radioStatistics.rxFail++; - } - - iv->setQueuedCmdFinished(); - } - } - - 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_MI != iv->ivGen) // only process MI inverters - continue; // skip to next inverter - - if ( !mPayload[iv->id].complete && - (mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && - (mPayload[iv->id].txId < (MI_REQ_4CH + ALL_FRAMES)) && - (mPayload[iv->id].txId > (0x39 + ALL_FRAMES)) && - (mPayload[iv->id].txId != (MI_REQ_CH1 + ALL_FRAMES)) && - (mPayload[iv->id].txId != (MI_REQ_CH2 + ALL_FRAMES)) && - (mPayload[iv->id].txId != (0x88)) && - (mPayload[iv->id].txId != (0x92)) && - (mPayload[iv->id].txId != 0) && - mPayload[iv->id].txCmd != 0x0f && - !iv->getDevControlRequest()) { - // no processing needed if txId is not one of 0x95, 0x88, 0x89, 0x91, 0x92 or response to 0x36ff - mPayload[iv->id].complete = true; - mPayload[iv->id].rxTmo = true; - continue; // skip to next inverter - } - - if (!mPayload[iv->id].complete) { - bool gotAllMsgParts, pyldComplete, fastNext; - gotAllMsgParts = build(iv, &pyldComplete, &fastNext); - if (!gotAllMsgParts && !pyldComplete) { // payload not complete - if ((mPayload[iv->id].requested) && (retransmit)) { - if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { - // This is required to prevent retransmissions without answer. - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINTLN(F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); - mPayload[iv->id].retransmits = mMaxRetrans; - mPayload[iv->id].rxTmo = true; - } else if(iv->devControlCmd == ActivePowerContr) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINTLN(F("retransmit power limit")); - iv->radio->sendControlPacket(iv, iv->devControlCmd, iv->powerLimit, true, false, (iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower()); - } else { - uint8_t cmd = mPayload[iv->id].txCmd; - if (mPayload[iv->id].retransmits < mMaxRetrans) { - mPayload[iv->id].retransmits++; - if( !mPayload[iv->id].gotFragment && mPayload[iv->id].rxTmo ) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINTLN(F("nothing received")); - mPayload[iv->id].retransmits = mMaxRetrans; - mPayload[iv->id].requested = false; //close failed request - } else if( !mPayload[iv->id].gotFragment && !mPayload[iv->id].rxTmo ) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINTLN(F("retransmit on failed first request")); - mPayload[iv->id].rxTmo = true; - iv->radio->sendCmdPacket(iv, cmd, cmd, true, false); - } else if ( cmd == 0x0f ) { - //hard/firmware request - iv->radio->sendCmdPacket(iv, 0x0f, 0x00, true, false); - mPayload[id].multi_parts = 0; - } else { - bool change = false; - if ( cmd >= MI_REQ_4CH && cmd < 0x39 ) { // MI-1500 Data command - if (cmd > MI_REQ_4CH && mPayload[iv->id].retransmits==1) // first request for the upper channels - change = true; - } else if ( cmd == MI_REQ_CH1 ) {//MI single or dual channel device - if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) { - if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {} - //first try to get missing sts for first channel a second time - else if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) { - cmd = MI_REQ_CH2; - change = true; - if (mPayload[iv->id].rtrRes < 3) //only get back to first channel twice - mPayload[iv->id].retransmits = 0; //reset counter - } - } - } else if ( cmd == MI_REQ_CH2) { - if ( mPayload[iv->id].dataAB[CH2] ) { // data + status ch2 are there? - if (mPayload[iv->id].stsAB[CH2] && (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1])) { - cmd = MI_REQ_CH1; - change = true; - } - } - } - DPRINT_IVID(DBG_INFO, iv->id); - if (change) { - DBGPRINT(F("next request is")); - mPayload[iv->id].txCmd = cmd; - mPayload[iv->id].rtrRes++; - } else { - DBGPRINT(F("sth.")); - DBGPRINT(F(" missing: Request Retransmit")); - } - DBGPRINT(F(" 0x")); - DBGHEXLN(cmd); - mPayload[id].multi_parts = 0; - iv->radio->sendCmdPacket(iv, cmd, cmd, true, false); - yield(); - } - } else { - mPayload[iv->id].rxTmo = true; - } - } - } - } else if(!gotAllMsgParts && pyldComplete) { // crc error on complete Payload - if (mPayload[iv->id].retransmits < mMaxRetrans) { - mPayload[iv->id].retransmits++; - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - mPayload[id].multi_parts = 0; - if (mSerialDebug) { - DPRINT_IVID(DBG_WARN, iv->id); - DBGPRINTLN(F("CRC Error: Request Complete Retransmit")); - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - } - iv->radio->sendCmdPacket(iv, mPayload[iv->id].txCmd, mPayload[iv->id].txCmd, false, false); - } else { - mPayload[iv->id].rxTmo = true; - } - } else { - if (!fastNext) { - mPayload[iv->id].rxTmo = true; - } else { - if (mHighPrioIv == NULL) - mHighPrioIv = iv; - } - } - } else { - mPayload[iv->id].rxTmo = true; - } - yield(); - } - } - - private: - void notify(uint8_t val, Inverter<> *iv) { - if(NULL != mCbPayload) - (mCbPayload)(val, iv); - } - - void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure - rec->ts = mPayload[iv->id].ts; - mPayload[iv->id].gotFragment = true; - mPayload[iv->id].multi_parts += 3; - mPayload[iv->id].txId = p->packet[0]; - miStsConsolidate(iv, stschan, rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]); - mPayload[iv->id].stsAB[stschan] = true; - if (mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].stsAB[CH2]) - mPayload[iv->id].stsAB[CH0] = true; - } - - void miStsConsolidate(Inverter<> *iv, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) { - //uint8_t status = (p->packet[11] << 8) + p->packet[12]; - uint16_t statusMi = 3; // regular status for MI, change to 1 later? - if ( uState == 2 ) { - statusMi = 5050 + stschan; //first approach, needs review! - if (lState) - statusMi += lState*10; - } else if ( uState > 3 ) { - statusMi = uState*1000 + uEnum*10; - if (lState) - statusMi += lState*100; //needs review, esp. for 4ch-8310 state! - //if (lEnum) - statusMi += lEnum; - if (uEnum < 6) { - statusMi += stschan; - } - if (statusMi == 8000) - statusMi = 8310; //trick? - } - - uint16_t prntsts = statusMi == 3 ? 1 : statusMi; - bool stsok = true; - if ( statusMi != mPayload[iv->id].sts[stschan] ) { //sth.'s changed? - iv->alarmCnt = 1; // minimum... - //sth is or was wrong? - if ( (iv->type != INV_TYPE_1CH) && ( (statusMi != 3) - || ((mPayload[iv->id].sts[stschan]) && (statusMi == 3) && (mPayload[iv->id].sts[stschan] != 3))) - ) { - iv->lastAlarm[stschan] = alarm_t(prntsts, mPayload[iv->id].ts,0); - iv->alarmCnt = iv->type == INV_TYPE_2CH ? 3 : 5; - } - - iv->alarmLastId = prntsts; //iv->alarmMesIndex; - - mPayload[iv->id].sts[stschan] = statusMi; - stsok = false; - if (iv->alarmCnt > 1) { //more than one channel - for (uint8_t ch = 0; ch < (iv->alarmCnt); ++ch) { //start with 1 - if (mPayload[iv->id].sts[ch] == 3) { - stsok = true; - break; - } - } - } - if (mSerialDebug) { - DPRINT(DBG_WARN, F("New state on CH")); - DBGPRINT(String(stschan)); DBGPRINT(F(" (")); - DBGPRINT(String(prntsts)); DBGPRINT(F("): ")); - DBGPRINTLN(iv->getAlarmStr(prntsts)); - } - } - - if (!stsok) { - iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); - iv->lastAlarm[0] = alarm_t(prntsts, mPayload[iv->id].ts, 0); - } - - if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]) { - iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!? - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("alarm ID incremented to ")); - DBGPRINTLN(String(iv->alarmMesIndex)); - } - } - } - - void miDataDecode(Inverter<> *iv, packet_t *p) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser - rec->ts = mPayload[iv->id].ts; - mPayload[iv->id].gotFragment = true; - mPayload[iv->id].multi_parts += 4; - - uint8_t datachan = ( p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES) || p->packet[0] == (MI_REQ_4CH + ALL_FRAMES) ) ? CH1 : - ( p->packet[0] == (MI_REQ_CH2 + ALL_FRAMES) || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 : - p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 : - CH4; - // count in RF_communication_protocol.xlsx is with offset = -1 - iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10); - yield(); - iv->setValue(iv->getPosByChFld(datachan, FLD_IDC, rec), rec, (float)((p->packet[11] << 8) + p->packet[12])/10); - yield(); - iv->setValue(iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[13] << 8) + p->packet[14])/10); - yield(); - iv->setValue(iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[15] << 8) + p->packet[16])/100); - iv->setValue(iv->getPosByChFld(datachan, FLD_PDC, rec), rec, (float)((p->packet[17] << 8) + p->packet[18])/10); - yield(); - iv->setValue(iv->getPosByChFld(datachan, FLD_YD, rec), rec, (float)((p->packet[19] << 8) + p->packet[20])/1); - yield(); - iv->setValue(iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[21] << 8) + p->packet[22])/10); - iv->setValue(iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(iv, datachan))); - mPayload[iv->id].rssi[(datachan-1)] = p->rssi; - - if ( datachan < 3 ) { - mPayload[iv->id].dataAB[datachan] = true; - } - if ( !mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].dataAB[CH1] && mPayload[iv->id].dataAB[CH2] ) { - mPayload[iv->id].dataAB[CH0] = true; - } - - if (p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES) ) { - /*For MI1500: - if (MI1500) { - STAT = (uint8_t)(p->packet[25] ); - FCNT = (uint8_t)(p->packet[26]); - FCODE = (uint8_t)(p->packet[27]); - }*/ - miStsConsolidate(iv, datachan, rec, p->packet[23], p->packet[24]); - - if (p->packet[0] < (0x39 + ALL_FRAMES) ) { - mPayload[iv->id].txCmd++; - mPayload[iv->id].retransmits = 0; // reserve retransmissions for each response - mPayload[iv->id].complete = false; - } else { - miComplete(iv); - } - } - - } - - void miComplete(Inverter<> *iv) { - if ( mPayload[iv->id].complete ) - return; //if we got second message as well in repreated attempt - mPayload[iv->id].complete = true; - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINTLN(F("got all msgs")); - } - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); - - //preliminary AC calculation... - float ac_pow = 0; - for(uint8_t i = 1; i <= iv->channels; i++) { - if (mPayload[iv->id].sts[i] == 3) { - uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); - ac_pow += iv->getValue(pos, rec); - } - } - ac_pow = (int) (ac_pow*9.5); - iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10); - int8_t rssi = -127; - for (uint8_t i = 0; i < 4; i++) { - // get best RSSI - if(mPayload[iv->id].rssi[i] > rssi) - rssi = mPayload[iv->id].rssi[i]; - yield(); - } - iv->rssi = rssi; - - iv->doCalculations(); - // update status state-machine, - iv->isProducing(); - - iv->setQueuedCmdFinished(); - iv->radioStatistics.rxSuccess++; - yield(); - notify(RealTimeRunData_Debug, iv); - } - - bool build(Inverter<> *iv, bool *complete, bool *fastNext ) { - DPRINTLN(DBG_VERBOSE, F("build")); - // check if all messages are there - - *complete = mPayload[iv->id].complete; - *fastNext = false; - uint8_t txCmd = mPayload[iv->id].txCmd; - - if(!*complete) { - DPRINTLN(DBG_VERBOSE, F("incomlete, txCmd is 0x") + String(txCmd, HEX)); - //we got some delayed status msgs?!? - if ((txCmd == MI_REQ_CH1) || (txCmd == MI_REQ_CH2)) { - if (mPayload[iv->id].stsAB[CH0] && mPayload[iv->id].dataAB[CH0]) { - miComplete(iv); - return true; - } - return false; - } - if (txCmd >= MI_REQ_4CH && txCmd <= 0x39) { - return false; - } - if (txCmd == 0x0f) { //hw info request, at least hw part nr. and version have to be there... - bool gotRelevant = iv->getFwVersion() - && iv->getChannelFieldValue(CH0, FLD_PART_NUM, iv->getRecordStruct(InverterDevInform_Simple)); - if (gotRelevant) - *fastNext = true; - return gotRelevant; - } - } - - //check if we want the next request to be executed faster - if (mPayload[iv->id].gotFragment && txCmd == 0x0f) - *fastNext = true; - return true; - } - - void miHwDecode(Inverter<> *iv, packet_t *p ) { - record_t<> *rec = iv->getRecordStruct(InverterDevInform_All); // choose the record structure - rec->ts = mPayload[iv->id].ts; - mPayload[iv->id].gotFragment = true; - -/* -Polling the device software and hardware version number command -start byte Command word routing address target address User data check end byte -byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] -0x7e 0x0f xx xx xx xx YY YY YY YY 0x00 CRC 0x7f -Command Receipt - First Frame -start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte -byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] -0x7e 0x8f YY YY YY YY xx xx xx xx 0x00 USFWBuild_VER APPFWBuild_VER APPFWBuild_YYYY APPFWBuild_MMDD APPFWBuild_HHMM APPFW_PN HW_VER CRC 0x7f -Command Receipt - Second Frame -start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte -byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] -0x7e 0x8f YY YY YY YY xx xx xx xx 0x01 HW_PN HW_FB_TLmValue HW_FB_ReSPRT HW_GridSamp_ResValule HW_ECapValue Matching_APPFW_PN CRC 0x7f -Command receipt - third frame -start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data check end byte -byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[15] byte[16] byte[17] byte[18] -0x7e 0x8f YY YY YY YY xx xx xx xx 0x12 APPFW_MINVER HWInfoAddr PNInfoCRC_gusv PNInfoCRC_gusv CRC 0x7f -*/ - -/* -case InverterDevInform_All: - rec->length = (uint8_t)(HMINFO_LIST_LEN); - rec->assign = (byteAssign_t *)InfoAssignment; - rec->pyldLen = HMINFO_PAYLOAD_LEN; - break; - -const byteAssign_t InfoAssignment[] = { -{ FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, -{ FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, -{ FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, -{ FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 }, -{ FLD_BOOTLOADER_VER, UNIT_NONE, CH0, 8, 2, 1 } -}; -*/ - - if ( p->packet[9] == 0x00 ) {//first frame - //FLD_FW_VERSION - for (uint8_t i = 0; i < 5; i++) { - iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); - } - iv->isConnected = 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])); - } - record_t<> *rec = iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure - rec->ts = mPayload[iv->id].ts; - iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1); - mPayload[iv->id].multi_parts +=4; - } 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)")); - /* according to xlsx (different start byte -1!) - byte[11] to byte[14] HW_PN - byte[15] byte[16] HW_FB_TLmValue - byte[17] byte[18] HW_FB_ReSPRT - byte[19] byte[20] HW_GridSamp_ResValule - byte[21] byte[22] HW_ECapValue - byte[23] to byte[26] Matching_APPFW_PN*/ - 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])); - record_t<> *rec = iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure - rec->ts = mPayload[iv->id].ts; - iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/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_ECapValue ")); - DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21])); - DPRINT(DBG_INFO,F("Matching_APPFW_PN ")); - DBGPRINTLN(String((uint32_t) (((p->packet[22] << 8) | p->packet[23]) << 8 | p->packet[24]) << 8 | p->packet[25])); - } - //notify(InverterDevInform_Simple, iv); - mPayload[iv->id].multi_parts +=2; - notify(InverterDevInform_All, iv); - } else { - DBGPRINTLN(F("3rd gen. inverter!")); - } - - } else if ( p->packet[9] == 0x12 ) {//3rd frame - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINTLN(F("got 3rd frame (hw info)")); - /* according to xlsx (different start byte -1!) - byte[11] byte[12] APPFW_MINVER - byte[13] byte[14] HWInfoAddr - byte[15] byte[16] PNInfoCRC_gusv - byte[15] byte[16] PNInfoCRC_gusv (this really is double mentionned in xlsx...) - */ - if(mSerialDebug) { - DPRINT(DBG_INFO,F("APPFW_MINVER ")); - DBGPRINTLN(String((p->packet[10] << 8) + p->packet[11])); - DPRINT(DBG_INFO,F("HWInfoAddr ")); - DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13])); - DPRINT(DBG_INFO,F("PNInfoCRC_gusv ")); - DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15])); - } - mPayload[iv->id].multi_parts++; - } - if (mPayload[iv->id].multi_parts > 5) { - iv->setQueuedCmdFinished(); - mPayload[iv->id].complete = true; - mPayload[iv->id].rxTmo = true; - mPayload[iv->id].requested= false; - iv->radioStatistics.rxSuccess++; - } - if (mHighPrioIv == NULL) - mHighPrioIv = iv; - } - - void miGPFDecode(Inverter<> *iv, packet_t *p ) { - mPayload[iv->id].gotFragment = true; - mPayload[iv->id].gotGPF = true; - - record_t<> *rec = iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure - rec->ts = mPayload[iv->id].ts; - iv->setValue(2, rec, (uint32_t) (((p->packet[10] << 8) | p->packet[11]))); //FLD_GRID_PROFILE_CODE - iv->setValue(3, rec, (uint32_t) (((p->packet[12] << 8) | p->packet[13]))); //FLD_GRID_PROFILE_VERSION - iv->setQueuedCmdFinished(); - iv->radioStatistics.rxSuccess++; - -/* according to xlsx (different start byte -1!) - Polling Grid-connected Protection Parameter File Command - Receipt - byte[10] ST1 indicates the status of the grid-connected protection file. ST1=1 indicates the default grid-connected protection file, ST=2 indicates that the grid-connected protection file is configured and normal, ST=3 indicates that the grid-connected protection file cannot be recognized, ST=4 indicates that the grid-connected protection file is damaged - byte[11] byte[12] CountryStd variable indicates the national standard code of the grid-connected protection file - byte[13] byte[14] Version indicates the version of the grid-connected protection file - byte[15] byte[16] -*/ - if(mSerialDebug) { - DPRINT(DBG_INFO,F("ST1 ")); - DBGPRINTLN(String(p->packet[9])); - DPRINT(DBG_INFO,F("CountryStd ")); - DBGPRINTLN(String((p->packet[10] << 8) + p->packet[11])); - DPRINT(DBG_INFO,F("Version ")); - DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13])); - } - if (mHighPrioIv == NULL) - mHighPrioIv = iv; - } - - void reset(uint8_t id, bool setTxTmo = true, bool clrSts = false) { - memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); - mPayload[id].gotFragment = false; - mPayload[id].rxTmo = setTxTmo;// design: don't start with complete retransmit - mPayload[id].rtrRes = 0; - mPayload[id].multi_parts = 0; - mPayload[id].retransmits = 0; - mPayload[id].complete = false; - mPayload[id].dataAB[CH0] = true; //required for 1CH and 2CH devices - mPayload[id].dataAB[CH1] = true; //required for 1CH and 2CH devices - mPayload[id].dataAB[CH2] = true; //only required for 2CH devices - mPayload[id].stsAB[CH0] = true; //required for 1CH and 2CH devices - mPayload[id].stsAB[CH1] = true; //required for 1CH and 2CH devices - mPayload[id].stsAB[CH2] = true; //only required for 2CH devices - mPayload[id].txCmd = 0; - mPayload[id].requested = false; - mPayload[id].ts = *mTimestamp; - if (clrSts) { // only clear channel states at startup - mPayload[id].sts[0] = 0; - mPayload[id].sts[CH1] = 0; - mPayload[id].sts[CH2] = 0; - mPayload[id].sts[CH3] = 0; - mPayload[id].sts[CH4] = 0; - } - } - - IApp *mApp; - HMSYSTEM *mSys; - uint8_t mMaxRetrans; - uint32_t *mTimestamp; - miPayload_t mPayload[MAX_NUM_INVERTERS]; - bool mSerialDebug; - Inverter<> *mHighPrioIv; - - alarmListenerType mCbAlarm; - payloadListenerType mCbPayload; -}; - -#endif /*__MI_PAYLOAD_H__*/ diff --git a/src/hm/radio.h b/src/hm/radio.h index 5f95fc8a..07317e8c 100644 --- a/src/hm/radio.h +++ b/src/hm/radio.h @@ -21,8 +21,14 @@ class Inverter; // abstract radio interface class Radio { public: - virtual void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true, uint16_t powerMax = 0) = 0; + virtual void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) = 0; virtual bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) { return true; } + virtual bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { return true; } + virtual void loop(void) {}; + + bool get() { + return !mBufCtrl.empty(); + } void handleIntr(void) { mIrqRcvd = true; @@ -38,6 +44,13 @@ class Radio { } void prepareDevInformCmd(Inverter<> *iv, 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. + if(IV_MI == getIvGen(iv)) { + DPRINT(DBG_DEBUG, F("legacy cmd 0x")); + DPRINTLN(DBG_DEBUG,String(cmd, HEX)); + sendCmdPacket(iv, cmd, cmd, false, false); + return; + } + if(mSerialDebug) { DPRINT(DBG_DEBUG, F("prepareDevInformCmd 0x")); DPRINTLN(DBG_DEBUG,String(cmd, HEX)); @@ -52,9 +65,13 @@ class Radio { sendPacket(iv, 24, isRetransmit); } + public: + std::queue mBufCtrl; + protected: virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0; virtual uint64_t getIvId(Inverter<> *iv) = 0; + virtual uint8_t getIvGen(Inverter<> *iv) = 0; void initPacket(uint64_t ivId, uint8_t mid, uint8_t pid) { mTxBuf[0] = mid; @@ -66,9 +83,9 @@ class Radio { void updateCrcs(uint8_t *len, bool appendCrc16=true) { // append crc's - if (appendCrc16 && (*len > 10)) { + if (appendCrc16 && ((*len) > 10)) { // crc control data - uint16_t crc = ah::crc16(&mTxBuf[10], *len - 10); + uint16_t crc = ah::crc16(&mTxBuf[10], (*len) - 10); mTxBuf[(*len)++] = (crc >> 8) & 0xff; mTxBuf[(*len)++] = (crc ) & 0xff; } diff --git a/src/hms/cmt2300a.h b/src/hms/cmt2300a.h index a4308519..75d2ce5c 100644 --- a/src/hms/cmt2300a.h +++ b/src/hms/cmt2300a.h @@ -8,15 +8,6 @@ #include "esp32_3wSpi.h" -#define WORK_FREQ_KHZ 865000 // desired 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 @@ -25,6 +16,10 @@ #define CMT2300A_MASK_CHIP_MODE_STA 0x0F #define CMT2300A_CUS_CMT10 0x09 +#define CMT2300A_CUS_TX5 0x59 +#define CMT2300A_CUS_TX8 0x5C +#define CMT2300A_CUS_TX9 0x5D +#define CMT2300A_CUS_TX10 0x5E #define CMT2300A_CUS_MODE_CTL 0x60 // [7] go_switch // [6] go_tx @@ -153,6 +148,42 @@ #define CMT2300A_MASK_TX_DONE_FLG 0x08 #define CMT2300A_MASK_PKT_OK_FLG 0x01 +// this list and the TX5, TX10 registers were compiled from the output of +// HopeRF RFPDK Tool v1.54 +static uint8_t paLevelList[31][2] PROGMEM = { + {0x17, 0x01}, // -10dBm + {0x1a, 0x01}, // -09dBm + {0x1d, 0x01}, // -08dBm + {0x21, 0x01}, // -07dBm + {0x25, 0x01}, // -06dBm + {0x29, 0x01}, // -05dBm + {0x2d, 0x01}, // -04dBm + {0x33, 0x01}, // -03dBm + {0x39, 0x02}, // -02dBm + {0x41, 0x02}, // -01dBm + {0x4b, 0x02}, // 00dBm + {0x56, 0x03}, // 01dBm + {0x63, 0x03}, // 02dBm + {0x71, 0x04}, // 03dBm + {0x80, 0x04}, // 04dBm + {0x22, 0x01}, // 05dBm + {0x27, 0x04}, // 06dBm + {0x2c, 0x05}, // 07dBm + {0x31, 0x06}, // 08dBm + {0x38, 0x06}, // 09dBm + {0x3f, 0x07}, // 10dBm + {0x48, 0x08}, // 11dBm + {0x52, 0x09}, // 12dBm + {0x5d, 0x0b}, // 13dBm + {0x6a, 0x0c}, // 14dBm + {0x79, 0x0d}, // 15dBm + {0x46, 0x10}, // 16dBm + {0x51, 0x10}, // 17dBm + {0x60, 0x12}, // 18dBm + {0x71, 0x14}, // 19dBm + {0x8c, 0x1c} // 20dBm +}; + // default CMT parameters static uint8_t cmtConfig[0x60] PROGMEM { // 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm @@ -168,11 +199,11 @@ static uint8_t cmtConfig[0x60] PROGMEM { 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, 0x48, 0x5A, 0x48, 0x4D, 0x01, 0x1F, 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 + 0x00, 0x07, 0x50, 0x00, 0x5D, 0x0B, 0x3F, 0x7F // - TX 13dBm }; @@ -391,6 +422,30 @@ class Cmt2300a { return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ); } + uint8_t getCurrentChannel(void) { + return mCurCh; + } + + void setPaLevel(int8_t level) { + if(level < -10) + level = -10; + if(level > 20) + level = 20; + + level += 10; // unsigned value + + if(level >= 15) { + mSpi.writeReg(CMT2300A_CUS_TX5, 0x07); + mSpi.writeReg(CMT2300A_CUS_TX10, 0x3f); + } else { + mSpi.writeReg(CMT2300A_CUS_TX5, 0x13); + mSpi.writeReg(CMT2300A_CUS_TX10, 0x18); + } + + mSpi.writeReg(CMT2300A_CUS_TX8, paLevelList[level][0]); + mSpi.writeReg(CMT2300A_CUS_TX9, paLevelList[level][1]); + } + private: void init() { mTxPending = false; diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h index 42eee1bc..bcb08c3f 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/hmsRadio.h @@ -29,25 +29,22 @@ class CmtRadio : public Radio { reset(genDtuSn); } - bool loop() { + void loop() { mCmt.loop(); - if((!mIrqRcvd) && (!mRqstGetRx)) - return false; + return; getRx(); if(CMT_SUCCESS == mCmt.goRx()) { mIrqRcvd = false; mRqstGetRx = false; - return true; - } else - return false; + } } bool isConnected() { return mCmtAvail; } - void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true, uint16_t powerMax = 0) { + void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) { DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); DBGHEXLN(cmd); initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME); @@ -69,6 +66,10 @@ class CmtRadio : public Radio { uint8_t fromCh = mCmt.freq2Chan(fromkHz); uint8_t toCh = mCmt.freq2Chan(tokHz); + return switchFrequencyCh(iv, fromCh, toCh); + } + + bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { if((0xff == fromCh) || (0xff == toCh)) return false; @@ -78,37 +79,41 @@ class CmtRadio : public Radio { return true; } - std::queue mBufCtrl; - private: + void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) { + // inverters have maybe different settings regarding frequency + if(mCmt.getCurrentChannel() != iv->config->frequency) + mCmt.switchChannel(iv->config->frequency); + updateCrcs(&len, appendCrc16); if(mSerialDebug) { - DPRINT(DBG_INFO, F("TX ")); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("TX ")); DBGPRINT(String(mCmt.getFreqKhz()/1000.0f)); DBGPRINT(F("Mhz | ")); ah::dumpBuf(mTxBuf, len); } uint8_t status = mCmt.tx(mTxBuf, len); + mMillis = millis(); 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) - iv->radioStatistics.retransmits++; - else - iv->radioStatistics.txCnt++; } uint64_t getIvId(Inverter<> *iv) { return iv->radioId.u64; } + uint8_t getIvGen(Inverter<> *iv) { + return iv->ivGen; + } + inline void reset(bool genDtuSn) { if(genDtuSn) generateDtuSn(); @@ -146,6 +151,7 @@ class CmtRadio : public Radio { inline void getRx(void) { packet_t p; + p.millis = millis() - mMillis; uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi); if(CMT_SUCCESS == status) mBufCtrl.push(p); @@ -154,6 +160,7 @@ class CmtRadio : public Radio { CmtType mCmt; bool mRqstGetRx; bool mCmtAvail; + uint32_t mMillis; }; #endif /*__HMS_RADIO_H__*/ diff --git a/src/platformio.ini b/src/platformio.ini index 942d2e15..d3c3c43c 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -30,7 +30,7 @@ lib_deps = https://github.com/bertmelis/espMqttClient#v1.4.5 bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.34.17 + olikraus/U8g2 @ ^2.35.7 https://github.com/zinggjm/GxEPD2 @ ^1.5.2 build_flags = -std=c++17 @@ -98,7 +98,7 @@ lib_deps = https://github.com/bertmelis/espMqttClient#v1.4.4 bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.34.17 + olikraus/U8g2 @ ^2.35.7 zinggjm/GxEPD2 @ ^1.5.2 build_flags = ${env.build_flags} -D ETHERNET diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index c4853bab..932c1a8f 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -91,12 +91,14 @@ class Display { Inverter<> *iv; record_t<> *rec; bool allOff = true; - for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + uint8_t nInv = mSys->getNumInverters(); + for (uint8_t i = 0; i < nInv; i++) { iv = mSys->getInverterByPos(i); - rec = iv->getRecordStruct(RealTimeRunData_Debug); if (iv == NULL) continue; + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv->isProducing()) nrprod++; else diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index a65a6b61..2c23c719 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -134,7 +134,7 @@ class PubMqtt { #endif } - bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { + bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs) { if (!mClient.connected()) return false; @@ -142,7 +142,16 @@ class PubMqtt { publish(subtopics[MQTT_SUNSET], String(sunset).c_str(), true); publish(subtopics[MQTT_COMM_START], String(sunrise - offs).c_str(), true); publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true); - publish(subtopics[MQTT_DIS_NIGHT_COMM], ((disNightCom) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + + Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i); + if(NULL == iv) + continue; + + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/dis_night_comm", iv->config->name); + publish(mSubTopic, ((iv->commEnabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + } return true; } @@ -516,25 +525,17 @@ class PubMqtt { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/cnt", iv->config->name); snprintf(mVal, 40, "%d", iv->alarmCnt); - publish(mSubTopic, mVal, true); + publish(mSubTopic, mVal, false); for(uint8_t j = 0; j < 10; j++) { if(0 != iv->lastAlarm[j].code) { - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/code", iv->config->name, j); - snprintf(mVal, 40, "%d", iv->lastAlarm[j].code); - publish(mSubTopic, mVal, true); - - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/str", iv->config->name, j); - snprintf(mVal, 40, "%s", iv->getAlarmStr(iv->lastAlarm[j].code).c_str()); - publish(mSubTopic, mVal, true); - - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/start", iv->config->name, j); - snprintf(mVal, 40, "%d", iv->lastAlarm[j].start + lastMidnight); - publish(mSubTopic, mVal, true); - - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/end", iv->config->name, j); - snprintf(mVal, 40, "%d", iv->lastAlarm[j].end + lastMidnight); - publish(mSubTopic, mVal, true); + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d", iv->config->name, j); + snprintf(mVal, 100, "{\"code\":%d,\"str\":\"%s\",\"start\":%d,\"end\":%d}", + iv->lastAlarm[j].code, + iv->getAlarmStr(iv->lastAlarm[j].code).c_str(), + iv->lastAlarm[j].start + lastMidnight, + iv->lastAlarm[j].end + lastMidnight); + publish(mSubTopic, mVal, false); yield(); } } @@ -613,7 +614,7 @@ class PubMqtt { // global buffer for mqtt topic. Used when publishing mqtt messages. char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; - char mVal[40]; + char mVal[100]; discovery_t mDiscovery; }; diff --git a/src/publisher/pubMqttDefs.h b/src/publisher/pubMqttDefs.h index ca8bf01e..c97daa78 100644 --- a/src/publisher/pubMqttDefs.h +++ b/src/publisher/pubMqttDefs.h @@ -48,7 +48,6 @@ enum { MQTT_SUNSET, MQTT_COMM_START, MQTT_COMM_STOP, - MQTT_DIS_NIGHT_COMM, MQTT_COMM_DISABLED, MQTT_COMM_DIS_TS, MQTT_VERSION, @@ -69,7 +68,6 @@ const char* const subtopics[] PROGMEM = { "sunset", "comm_start", "comm_stop", - "dis_night_comm", "comm_disabled", "comm_dis_ts", "version", diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index a1c03c86..0ac6d306 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -29,14 +29,13 @@ class PubMqttIvData { mZeroValues = false; memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * sizeof(uint32_t)); - memset(mIvLastPublish, 0, MAX_NUM_INVERTERS * sizeof(uint32_t)); mRTRDataHasBeenSent = false; - mTable[IDLE] = &PubMqttIvData::stateIdle; - mTable[START] = &PubMqttIvData::stateStart; - mTable[FIND_NXT_IV] = &PubMqttIvData::stateFindNxtIv; - mTable[SEND_DATA] = &PubMqttIvData::stateSend; - mTable[SEND_TOTALS] = &PubMqttIvData::stateSendTotals; + mTable[IDLE] = &PubMqttIvData::stateIdle; + mTable[START] = &PubMqttIvData::stateStart; + mTable[FIND_NXT_IV] = &PubMqttIvData::stateFindNxtIv; + mTable[SEND_DATA] = &PubMqttIvData::stateSend; + mTable[SEND_TOTALS] = &PubMqttIvData::stateSendTotals; } void loop() { @@ -103,7 +102,7 @@ class PubMqttIvData { mPos = 0; if(found) { record_t<> *rec = mIv->getRecordStruct(mCmd); - if((mIv->getLastTs(rec) != mIvLastPublish[mIv->id]) && mIv->getLastTs(rec) != 0 ) { //workaround for startup. Suspect, mCmd might cause to much messages.... + if((RealTimeRunData_Debug == mCmd) && mIv->getLastTs(rec) != 0 ) { //workaround for startup. Suspect, mCmd might cause to much messages.... snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name); snprintf(mVal, 40, "%d", mIv->getLastTs(rec)); mPublish(mSubTopic, mVal, true, QOS_0); @@ -134,7 +133,6 @@ class PubMqttIvData { pubData &= (lastTs != mIvLastRTRpub[mIv->id]); if (pubData) { - mIvLastPublish[mIv->id] = lastTs; if(mPos < rec->length) { bool retained = false; if (mCmd == RealTimeRunData_Debug) { @@ -144,25 +142,27 @@ class PubMqttIvData { // calculate total values for RealTimeRunData_Debug if (CH0 == rec->assign[mPos].ch) { 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; + if(mIv->config->add2Total) { + 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; } - case FLD_PDC: - mTotal[3] += mIv->getValue(mPos, rec); - break; } } else mAllTotalFound = false; @@ -180,12 +180,25 @@ class PubMqttIvData { mPublish(mSubTopic, mVal, retained, qos); } mPos++; - } else + } else { + sendRadioStat(rec->length); mState = FIND_NXT_IV; + } } else mState = FIND_NXT_IV; } + inline void sendRadioStat(uint8_t start) { + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/radio_stat", mIv->config->name); + snprintf(mVal, 100, "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d}", + mIv->radioStatistics.txCnt, + mIv->radioStatistics.rxSuccess, + mIv->radioStatistics.rxFail, + mIv->radioStatistics.rxFailNoAnser, + mIv->radioStatistics.retransmits); + mPublish(mSubTopic, mVal, false, QOS_0); + } + void stateSendTotals() { uint8_t fieldId; mRTRDataHasBeenSent = true; @@ -223,7 +236,8 @@ class PubMqttIvData { } else { mSendList->pop(); mZeroValues = false; - mState = START; + mPos = 0; + mState = IDLE; } } @@ -241,11 +255,10 @@ class PubMqttIvData { Inverter<> *mIv, *mIvSend; uint8_t mPos; uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; - uint32_t mIvLastPublish[MAX_NUM_INVERTERS]; bool mRTRDataHasBeenSent; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; - char mVal[40]; + char mVal[100]; bool mZeroValues; // makes sure that yield day is sent even if no inverter is online std::queue *mSendList; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index caa9512f..fbb75b7d 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -124,12 +124,26 @@ class RestApi { void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DPRINTLN(DBG_VERBOSE, "onApiPostBody"); - DynamicJsonDocument json(200); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); - JsonObject root = response->getRoot(); - DeserializationError err = deserializeJson(json, (const char *)data, len); + if(0 == index) { + if(NULL != mTmpBuf) + delete[] mTmpBuf; + mTmpBuf = new uint8_t[total+1]; + mTmpSize = total; + } + if(mTmpSize >= (len + index)) + memcpy(&mTmpBuf[index], data, len); + + if((len + index) != total) + return; // not last frame - nothing to do + + DynamicJsonDocument json(1000); + + DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize); JsonObject obj = json.as(); + + AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); + JsonObject root = response->getRoot(); root[F("success")] = (err) ? false : true; if(!err) { String path = request->url().substring(5); @@ -141,18 +155,20 @@ class RestApi { root[F("success")] = false; root[F("error")] = "Path not found: " + path; } - } - else { + } else { switch (err.code()) { case DeserializationError::Ok: break; - case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; - case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; - default: root[F("error")] = F("Deserialization failed"); break; + case DeserializationError::IncompleteInput: root[F("error")] = F("Incomplete input"); break; + case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; + case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; + default: root[F("error")] = F("Deserialization failed"); break; } } response->setLength(); request->send(response); + delete[] mTmpBuf; + mTmpBuf = NULL; } void getNotFound(JsonObject obj, String url) { @@ -339,24 +355,33 @@ class RestApi { Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mSys->getInverterByPos(i); - if(NULL != iv) { - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("enabled")] = (bool)iv->config->enabled; - obj2[F("id")] = i; - obj2[F("name")] = String(iv->config->name); - obj2[F("serial")] = String(iv->config->serial.u64, HEX); - obj2[F("channels")] = iv->channels; - obj2[F("version")] = String(iv->getFwVersion()); - - for(uint8_t j = 0; j < iv->channels; j ++) { - obj2[F("ch_yield_cor")][j] = (double)iv->config->yieldCor[j]; - obj2[F("ch_name")][j] = iv->config->chName[j]; - obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; - } + if(NULL == iv) + continue; + + JsonObject obj2 = invArr.createNestedObject(); + obj2[F("enabled")] = (bool)iv->config->enabled; + obj2[F("id")] = i; + obj2[F("name")] = String(iv->config->name); + obj2[F("serial")] = String(iv->config->serial.u64, HEX); + obj2[F("channels")] = iv->channels; + obj2[F("freq")] = iv->config->frequency; + obj2[F("disnightcom")] = (bool)iv->config->disNightCom; + obj2[F("add2total")] = (bool)iv->config->add2Total; + if(0xff == iv->config->powerLevel) { + if((IV_HMT == iv->ivGen) || (IV_HMS == iv->ivGen)) + obj2[F("pa")] = 30; // 20dBm + else + obj2[F("pa")] = 1; // low + } else + obj2[F("pa")] = iv->config->powerLevel; + + for(uint8_t j = 0; j < iv->channels; 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]; } } obj[F("interval")] = String(mConfig->nrf.sendInterval); - obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld); obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; obj[F("rstNotAvail")] = (bool)mConfig->inst.rstValsNotAvail; @@ -510,7 +535,6 @@ class RestApi { void getSun(JsonObject obj) { obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; - obj[F("disnightcom")] = mConfig->sun.disNightCom; obj[F("offs")] = mConfig->sun.offsetSec; } @@ -545,7 +569,6 @@ class RestApi { void getRadioNrf(JsonObject obj) { obj[F("en")] = (bool) mConfig->nrf.enabled; obj[F("isconnected")] = mRadioNrf->isChipConnected(); - obj[F("power_level")] = mConfig->nrf.amplifierPower; obj[F("dataRate")] = mRadioNrf->getDataRate(); //obj[F("isPVariant")] = mRadioNrf->isPVariant(); } @@ -593,24 +616,28 @@ class RestApi { obj[F("ts_sunrise")] = mApp->getSunrise(); obj[F("ts_sunset")] = mApp->getSunset(); obj[F("ts_offset")] = mConfig->sun.offsetSec; - obj[F("disNightComm")] = mConfig->sun.disNightCom; JsonArray inv = obj.createNestedArray(F("inverter")); Inverter<> *iv; + bool disNightCom = false; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject invObj = inv.createNestedObject(); - invObj[F("enabled")] = (bool)iv->config->enabled; - invObj[F("id")] = i; - invObj[F("name")] = String(iv->config->name); - invObj[F("cur_pwr")] = ah::round3(iv->getChannelFieldValue(CH0, FLD_PAC, rec)); - invObj[F("is_avail")] = iv->isAvailable(); - invObj[F("is_producing")] = iv->isProducing(); - invObj[F("ts_last_success")] = iv->getLastTs(rec); - } + if(NULL == iv) + continue; + + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + JsonObject invObj = inv.createNestedObject(); + invObj[F("enabled")] = (bool)iv->config->enabled; + invObj[F("id")] = i; + invObj[F("name")] = String(iv->config->name); + invObj[F("cur_pwr")] = ah::round3(iv->getChannelFieldValue(CH0, FLD_PAC, rec)); + invObj[F("is_avail")] = iv->isAvailable(); + invObj[F("is_producing")] = iv->isProducing(); + invObj[F("ts_last_success")] = iv->getLastTs(rec); + if(iv->config->disNightCom) + disNightCom = true; } + obj[F("disNightComm")] = disNightCom; JsonArray warn = obj.createNestedArray(F("warnings")); if(!mRadioNrf->isChipConnected() && mConfig->nrf.enabled) @@ -700,12 +727,10 @@ class RestApi { iv->powerLimit[1] = AbsolutNonPersistent; accepted = iv->setDevControlRequest(ActivePowerContr); - } - else if(F("dev") == jsonIn[F("cmd")]) { + } else if(F("dev") == jsonIn[F("cmd")]) { DPRINTLN(DBG_INFO, F("dev cmd")); - iv->enqueCommand(jsonIn[F("val")].as()); - } - else { + iv->setDevCommand(jsonIn[F("val")].as()); + } else { jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; return false; } @@ -713,8 +738,7 @@ class RestApi { if(!accepted) { jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); return false; - } else - mApp->ivSendHighPrio(iv); + } return true; } @@ -731,8 +755,26 @@ class RestApi { mApp->setTimestamp(0); // 0: update ntp flag else if(F("serial_utc_offset") == jsonIn[F("cmd")]) mTimezoneOffset = jsonIn[F("val")]; - else if(F("discovery_cfg") == jsonIn[F("cmd")]) { + else if(F("discovery_cfg") == jsonIn[F("cmd")]) mApp->setMqttDiscoveryFlag(); // for homeassistant + else if(F("save_iv") == jsonIn[F("cmd")]) { + Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false); + iv->config->enabled = jsonIn[F("en")]; + iv->config->serial.u64 = jsonIn[F("ser")]; + snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as()); + + for(uint8_t i = 0; i < 6; i++) { + iv->config->chMaxPwr[i] = jsonIn[F("ch")][i][F("pwr")]; + iv->config->yieldCor[i] = jsonIn[F("ch")][i][F("yld")]; + snprintf(iv->config->chName[i], MAX_NAME_LENGTH, "%s", jsonIn[F("ch")][i][F("name")].as()); + } + + mApp->initInverter(jsonIn[F("id")]); + iv->config->frequency = jsonIn[F("freq")]; + iv->config->powerLevel = jsonIn[F("pa")]; + iv->config->disNightCom = jsonIn[F("disnightcom")]; + iv->config->add2Total = jsonIn[F("add2total")]; + mApp->saveSettings(false); // without reboot } else { jsonOut[F("error")] = F("unknown cmd"); return false; @@ -753,6 +795,8 @@ class RestApi { uint32_t mTimezoneOffset; uint32_t mHeapFree, mHeapFreeBlk; uint8_t mHeapFrag; + uint8_t *mTmpBuf = NULL; + uint32_t mTmpSize; }; #endif /*__WEB_API_H__*/ diff --git a/src/web/html/api.js b/src/web/html/api.js index c9590d89..e76aab2c 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -1,6 +1,4 @@ -/** - * SVG ICONS - */ +/* SVG ICONS - https://icons.getbootstrap.com */ iconWifi1 = [ "M11.046 10.454c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.708-.707z" @@ -34,6 +32,15 @@ iconSuccessFull = [ "M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" ]; +iconGear = [ + "M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z" +]; + +iconDel = [ + "M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z", + "M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" +]; + /** * GENERIC FUNCTIONS */ @@ -119,7 +126,7 @@ function parseRssi(obj) { icon = iconWifi1; else if(obj["wifi_rssi"] <= -70) icon = iconWifi2; - document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"])); + document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj["wifi_rssi"])); } function toIsoDateStr(d) { @@ -181,6 +188,51 @@ function tr(val1, val2) { ]); } +function tr2(cols) { + var t = []; + for(val of cols) { + if(typeof val == "number") + val = String(val); + if(t.length == 0) + t.push(ml("th", {}, val)); + else + t.push(ml("td", {}, val)); + } + return ml("tr", {}, t); +} + +function badge(success, text, second="error") { + return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text); +} + +function tabChange(id) { + var els = document.getElementsByClassName("nav-link"); + [].forEach.call(els, function(e) { + if(e.id != id) + e.classList.remove('active'); + else + e.classList.add('active'); + }); + + els = document.getElementsByClassName("tab-content"); + [].forEach.call(els, function(e) { + if(e.id == ("div"+id.substring(3))) + e.classList.remove('hide'); + else + e.classList.add('hide'); + }); +} + +function tabs(items) { + var li = []; + var cl = " active"; + for(it of items) { + li.push(ml("li", {class: "nav-item"},ml("a", {id: "tab"+it, class: "nav-link" + cl, href: "#", onclick: function(){tabChange(this.id)}}, it))) + cl = ""; + } + return ml("ul", {class: "nav nav-tabs mb-4"}, li); +} + function des(val) { e = document.createElement('p'); e.classList.add("subdes"); @@ -212,13 +264,11 @@ function inp(name, val, max=32, cl=["text"], id=null, type=null, pattern=null, t } function sel(name, options, selId) { - e = document.createElement('select'); - e.name = name; + var o = []; for(it of options) { - o = opt(it[0], it[1], (it[0] == selId)); - e.appendChild(o); + o.push(opt(it[0], it[1], (it[0] == selId))); } - return e; + return ml("select", {name: name}, o); } function selDelAllOpt(sel) { @@ -229,9 +279,7 @@ function selDelAllOpt(sel) { } function opt(val, html, sel=false) { - o = document.createElement('option'); - o.value = val; - o.innerHTML = html; + var o = ml("option", {value: val}, html); if(sel) o.selected = true; return o; @@ -290,7 +338,7 @@ function svg(data=null, w=24, h=24, cl=null, tooltip=null) { function modal(title, body) { if(null == document.getElementById("modal")) { document.getElementById("wrapper").append( - ml("div", {id: "modal-wrapper", class: "modal", onclick: modalClose}), + ml("div", {id: "modal-wrapper", onclick: modalClose}), ml("div", {id: "modal", class: "modal"}, ml("div", {class: "modal-content"}, [ ml("div", {class: "modal-header"}, [ diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css index 3d9d167a..65100721 100644 --- a/src/web/html/colorDark.css +++ b/src/web/html/colorDark.css @@ -16,7 +16,7 @@ --secondary: #0072c8; --nav-active: #555; --footer-bg: #282828; - --modal-bg: #666; + --modal-bg: #282828; --invalid-bg: #400; diff --git a/src/web/html/serial.html b/src/web/html/serial.html index 10d1769e..3f9b4d94 100644 --- a/src/web/html/serial.html +++ b/src/web/html/serial.html @@ -7,9 +7,9 @@ {#HTML_NAV}
-
+
- +
console active:
@@ -42,7 +42,7 @@ if(true == exeOnce) { parseNav(obj); parseESP(obj); - window.setInterval("getAjax('/api/generic', parseGeneric)", 10000); + window.setInterval("getAjax('/api/generic', parseGeneric)", 5000); exeOnce = false; setTimeOffset(); } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 132dc81d..d6637510 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -31,7 +31,7 @@
System Config -

Pinout

+

Status LEDs

Radio (NRF24L01+)

@@ -144,49 +144,33 @@
Inverter
-
-
-
-
-
-

Note

-

A 'max module power' value of '0' disables the channel in 'live' view

-
-
-

General

-
-
-
-
Interval [s]
-
-
-
Max retries per Payload
-
+
Interval [s]
+
-
Reset values and YieldDay at midnight
-
+
Reset values and YieldDay at midnight
+
-
Reset values when inverter polling pauses at sunset
-
+
Reset values when inverter polling pauses at sunset
+
-
Reset values when inverter status is 'not available'
-
+
Reset values when inverter status is 'not available'
+
-
Reset 'max' values at midnight
-
+
Reset 'max' values at midnight
+
-
Start without time sync (useful in AP-Only-Mode)
-
+
Start without time sync (useful in AP-Only-Mode)
+
-
Yield Efficiency (should be between 0.95 and 0.96)
-
+
Yield Efficiency (should be between 0.95 and 0.96)
+
@@ -240,10 +224,6 @@
Offset (pre sunrise, post sunset)
-
-
Pause polling inverters during night
-
-
@@ -491,13 +471,31 @@ [48, "GPIO48"], ]; /*ENDIF_ESP32*/ + var nrfPa = [ + [0, "MIN (recommended)"], + [1, "LOW"], + [2, "HIGH"], + [3, "MAX (experimental)"] + ]; + var esp32cmtPa = []; + var esp32cmtFreq = []; + + /*IF_ESP32*/ + var freqFmt = new Intl.NumberFormat('en-US', { + minimumIntegerDigits: 3, + minimumFractionDigits: 2 + }); + for(var i = 0; i < 31; i++) { + esp32cmtPa.push([i, String(i-10) + " dBm"]); + if(i < 29) + esp32cmtFreq.push([i, freqFmt.format(860 + i*0.25) + " MHz"]); + } + /*ENDIF_ESP32*/ var led_high_active = [ [0, "low active"], [1, "high active"], ]; - const re = /1[0,1,3][2,4,6,8][1,2,4].*/; - window.onload = function() { for(it of document.getElementsByClassName("s_collapsible")) { it.addEventListener("click", function() { @@ -516,10 +514,6 @@ }); } - document.getElementById("btnAdd").addEventListener("click", function() { - ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":6,"ch_max_pwr":[0,0,0,0,0,0],"ch_name":["","","","","",""],"ch_yield_cor":[0,0,0,0,0,0]}')); - }); - function apiCbWifi(obj) { var e = document.getElementById("networks"); selDelAllOpt(e); @@ -618,100 +612,8 @@ return null; } - function ivHtml(obj) { - var id = getFreeId(); - if(null == id) { - setHide("btnAdd", true); - return; - } - - var iv = ml("div", {id: "inv" + id}, null); - document.getElementById("inverter").appendChild(iv); - iv.appendChild(des("Inverter " + id)); - id = "inv" + id; - - var addr = ml("input", {name: id + "Addr", class: "text", type: "number", max: 138999999999, value: obj["serial"]}, null); - - iv.append( - mlCb(id + "Enable", "Communication Enable", obj["enabled"]), - mlE("Serial Number (12 digits)*", addr) - ); - - ['keyup', 'change'].forEach(function(evt) { - addr.addEventListener(evt, (e) => { - var serial = addr.value.substring(0,4); - var max = 0; - for(var i=0;i<6;i++) { - setHide(id+"ModPwr"+i, true); - setHide(id+"ModName"+i, true); - setHide(id+"YieldCor"+i, true); - } - setHide("row"+id+"ModPwr", true); - setHide("row"+id+"ModName", true); - setHide("row"+id+"YieldCor", true); - - if(serial.charAt(0) == 1) { - if((serial.charAt(1) == 0) || (serial.charAt(1) == 1) || (serial.charAt(1) == 3)) { - if((serial.charAt(3) == 1) || (serial.charAt(3) == 2) || (serial.charAt(3) == 4)) { - switch(serial.charAt(2)) { - default: - case "2": max = 1; break; - case "4": max = 2; break; - case "6": max = 4; break; - case "8": max = 6; break; - } - } - } - } - - if(max != 0) { - for(var i=0;i obj.inverter.length) + e.append(ml("div", {class: "row my-3"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "add Inverter", class: "btn", onclick: function() { ivModal(add); }}, null)))); + ivGlob(obj); } + function ivModal(obj) { + var lines = []; + lines.push(ml("tr", {}, [ + ml("th", {style: "width: 10%;"}, "Input"), + ml("th", {}, "Max Module Power [Wp]"), + ml("th", {}, "Name (optional)"), + ml("th", {}, "Yield Correction [kWh] (optional)") + ])); + + for(let i = 0; i < 6; i++) { + lines.push(ml("tr", {id: "ch"+i}, [ + ml("td", {}, String(i+1)), + ml("td", {}, ml("input", {name: "ch_p"+i, class: "text", type: "number", max: 999, value: obj.ch_max_pwr[i]}, null)), + ml("td", {}, ml("input", {name: "ch_n"+i, class: "text", type: "text", maxlength: 15, value: (undefined === obj.ch_name[i]) ? "" : obj.ch_name[i]}, null)), + ml("td", {}, ml("input", {name: "yld_c"+i, class: "text", type: "number", max: 999999, value: obj.ch_yield_cor[i]}, null)) + ])); + } + + var cbEn = ml("input", {name: "enable", type: "checkbox"}, null); + var cbDisNightCom = ml("input", {name: "disnightcom", type: "checkbox"}, null); + var cbAddTotal = ml("input", {name: "add2total", type: "checkbox"}, null); + cbEn.checked = (obj.enabled); + cbDisNightCom.checked = (obj.disnightcom); + cbAddTotal.checked = (obj.add2total); + + var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null); + var html = ml("div", {}, [ + tabs(["General", "Inputs", "Radio", "Advanced"]), + ml("div", {id: "divGeneral", class: "tab-content"}, [ + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-2"}, "Enable"), + ml("div", {class: "col-10"}, cbEn) + ]), + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-2 mt-2"}, "Serial"), + ml("div", {class: "col-10"}, ser) + ]), + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-2 mt-2"}, "Name"), + ml("div", {class: "col-10"}, ml("input", {name: "name", class: "text", type: "text", value: obj.name}, null)) + ]) + ]), + ml("div", {id: "divInputs", class: "tab-content hide"}, [ + ml("div", {class: "row mb-3"}, + ml("table", {class: "table"}, + ml("tbody", {}, lines) + ) + ) + ]), + ml("div", {id: "divRadio", class: "tab-content hide"}, [ + ml("input", {type: "hidden", name: "isnrf"}, null), + ml("div", {id: "setcmt"}, [ + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-3 mt-2"}, "Frequency"), + ml("div", {class: "col-9"}, sel("freq", esp32cmtFreq, obj.freq)) + ]), + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-3 mt-2"}, "Power Level"), + ml("div", {class: "col-9"}, sel("cmtpa", esp32cmtPa, obj.pa)) + ]), + ]), + ml("div", {id: "setnrf"}, + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-3 mt-2"}, "Power Level"), + ml("div", {class: "col-9"}, sel("nrfpa", nrfPa, obj.pa)) + ]), + ), + ]), + ml("div", {id: "divAdvanced", class: "tab-content hide"}, [ + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-10"}, "Pause communication during night (lat. and lon. need to be set)"), + ml("div", {class: "col-2"}, cbDisNightCom) + ]), + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-10"}, "Include inverter to sum of total (should be checked by default)"), + ml("div", {class: "col-2"}, cbAddTotal) + ]) + ]), + ml("div", {class: "row mt-5"}, [ + ml("div", {class: "col-8", id: "res"}, ""), + ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "save", class: "btn", onclick: function() { ivSave(); }}, null)) + ]) + ]); + + ['keyup', 'change'].forEach(function(evt) { + ser.addEventListener(evt, (e) => { + var sn = parseInt(ser.value, 16); + sn = Math.floor(sn / Math.pow(2, 32)); + + var max = 1; + switch(sn & 0x00f0) { + case 0x0010: max = 1; break; + case 0x0040: max = 2; break; + case 0x0060: max = 4; break; + case 0x0080: max = 6; break; + } + for(var i = 0; i < 6; i++) { + setHide("ch"+i, (i >= max)); + } + + var nrf = true; + switch(sn & 0xff00) { + case 0x1000: nrf = true; break; + case 0x1100: + switch(sn & 0x000f) { + case 0x0004: nrf = false; break; + default: nrf = true; break; + } + break; + case 0x1300: nrf = false; break; + } + setHide("setcmt", nrf); + setHide("setnrf", !nrf); + document.getElementsByName("isnrf")[0].value = nrf; + }) + }); + + modal("Edit inverter " + obj.name, html); + ser.dispatchEvent(new Event('change')); + + function ivSave() { + var o = new Object(); + o.cmd = "save_iv"; + o.id = obj.id; + o.ser = parseInt(document.getElementsByName("ser")[0].value, 16); + o.name = document.getElementsByName("name")[0].value; + o.en = document.getElementsByName("enable")[0].checked; + o.ch = []; + for(let i = 0; i < 6; i++) { + var q = new Object(); + q.pwr = document.getElementsByName("ch_p"+i)[0].value; + q.name = document.getElementsByName("ch_n"+i)[0].value; + q.yld = document.getElementsByName("yld_c"+i)[0].value; + o.ch.push(q); + } + if("true" == document.getElementsByName("isnrf")[0].value) + o.pa = document.getElementsByName("nrfpa")[0].value; + else + o.pa = document.getElementsByName("cmtpa")[0].value; + o.freq = document.getElementsByName("freq")[0].value; + o.disnightcom = document.getElementsByName("disnightcom")[0].checked; + o.add2total = document.getElementsByName("add2total")[0].checked; + getAjax("/api/setup", cb, "POST", JSON.stringify(o)); + } + + function cb(obj) { + var e = document.getElementById("res"); + if(!obj.success) + e.innerHTML = "error: " + obj.error; + else { + modalClose(); + getAjax("/api/inverter/list", parseIv); + } + } + } + + function ivDel(obj) { + var html = ml("div", {class: "row"}, [ + ml("div", {class: "col-9"}, "do you realy want to delete inverter " + obj.name + "?"), + ml("div", {class: "col-3 a-r"}, ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "yes", class: "btn", onclick: function() { del(); }}, null))) + ]); + modal("Delete inverter " + obj.name, html); + + function del() { + var o = new Object(); + o.cmd = "save_iv"; + o.id = obj.id; + o.ser = 0; + o.name = ""; + o.en = false; + o.ch = []; + for(let i = 0; i < 6; i++) { + var q = new Object(); + q.pwr = 0; + q.name = ""; + q.yld = 0; + o.ch.push(q); + } + getAjax("/api/setup", cb, "POST", JSON.stringify(o)); + } + + function cb(obj) { + if(obj.success) { + modalClose(); + getAjax("/api/inverter/list", parseIv); + } + } + } + function parseMqtt(obj) { for(var i of [["Addr", "broker"], ["Port", "port"], ["ClientId", "clientId"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]]) document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]]; @@ -772,7 +897,6 @@ function parseSun(obj) { document.getElementsByName("sunLat")[0].value = obj["lat"]; document.getElementsByName("sunLon")[0].value = obj["lon"]; - document.getElementsByName("sunDisNightCom")[0].checked = obj["disnightcom"]; const sel = document.getElementsByName("sunOffs")[0]; for(var i = 0; i <= 60; i++) { sel.appendChild(opt(i, i + " minutes", (i == (obj["offs"] / 60)))); @@ -781,11 +905,11 @@ function parsePinout(obj, type, system) { var e = document.getElementById("pinout"); - pins = [['led0', 'pinLed0'], ['led1', 'pinLed1']]; + pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected']]; for(p of pins) { e.append( ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), + ml("div", {class: "col-12 col-sm-3 my-2"}, p[2]), ml("div", {class: "col-12 col-sm-9"}, sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[0]]) ) @@ -829,19 +953,6 @@ ]) ); } - e.append( - ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-12 col-sm-3 my-2"}, "Power Level"), - ml("div", {class: "col-12 col-sm-9"}, - sel("rf24Power", [ - [0, "MIN (recommended)"], - [1, "LOW"], - [2, "HIGH"], - [3, "MAX (experimental)"] - ], obj["power_level"]) - ) - ]) - ); } /*IF_ESP32*/ diff --git a/src/web/html/style.css b/src/web/html/style.css index 15b35cef..e0eae098 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -24,6 +24,11 @@ input[type=file] { width: 100%; } +textarea { + color: var(--fg); + background-color: var(--bg); +} + #live span { color: var(--fg2); } @@ -89,7 +94,11 @@ svg.icon { fill: var(--success); } -.wifi { +.icon-fg { + fill: var(--fg); +} + +.icon-fg2 { fill: var(--fg2); } @@ -685,7 +694,7 @@ div.hr { margin: 1.75rem auto; } -.modal { +.modal, #modal-wrapper { position: fixed; top: 0; right: 0; @@ -694,6 +703,10 @@ div.hr { display: block; } +.modal { + height: calc(100% - 3.5rem); +} + #modal-wrapper { background-color: #000; opacity: 0.5; @@ -707,8 +720,10 @@ div.hr { width: 100%; background-color: var(--modal-bg); background-clip: padding-box; - border: 1px solid rgba(0,0,0,.2); + border: 1px solid var(--fg); flex-direction: column; + max-height: 100%; + overflow: hidden; } .modal-header { @@ -716,7 +731,7 @@ div.hr { align-items: flex-start; justify-content: space-between; padding: 1rem; - border-bottom: 1px solid #e9ecef; + border-bottom: 1px solid var(--table-border); } .modal-header .close { @@ -725,7 +740,8 @@ div.hr { } .modal-body { - padding: 1rem 1rem 2rem 1rem; + padding: 1rem; + overflow-y: auto; } .close { @@ -778,3 +794,42 @@ h5 { vertical-align: baseline; border-radius: .25rem; } + +ul { + margin-top: 0; +} + +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + list-style: none; +} + +.nav-tabs { + border-bottom: 1px solid var(--fg); +} + +.nav-tabs .nav-link { + margin-bottom: -1px; + border: 1px solid transparent; + border-top-left-radius: .25rem; + border-top-right-radius: .25rem; +} + +.nav-link { + display: block; + padding: .5rem 1rem; + text-decoration: none; + color: var(--fg); +} + +.nav-tabs .nav-link.active { + border-color: var(--primary) var(--fg) var(--bg); + border-top-width: 4px; +} + +.nav-link:hover, .nav-link:visited { + background-color: var(--input-bg); + color: var(--fg); +} diff --git a/src/web/html/system.html b/src/web/html/system.html index 27ad1801..a310890d 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -40,22 +40,16 @@ ); } - function badge(success, text, second="error") { - return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text); - } - function headline(text) { return ml("div", {class: "head p-2 mt-3"}, ml("div", {class: "row"}, ml("div", {class: "col a-c"}, text))) } function parseRadio(obj) { - const pa = ["MIN (recommended)", "LOW", "HIGH", "MAX"]; const dr = ["1 M", "2 M", "250 k"] if(obj.radioNrf.en) { lines = [ tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "not ") + "connected")), - tr("NRF24 Power Level", pa[obj.radioNrf.power_level]), tr("NRF24 Data Rate", dr[obj.radioNrf.dataRate] + "bps") ]; } else diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 5002387b..6c0002cf 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -20,7 +20,7 @@ var units, ivEn; var mIvHtml = []; var mNum = 0; - var total = Array(5).fill(0); + var total = Array(6).fill(0); var tPwrAck; function parseGeneric(obj) { @@ -62,7 +62,7 @@ } function totals() { - for(var i = 0; i < 5; i++) { + for(var i = 0; i < 6; i++) { total[i] = Math.round(total[i] * 100) / 100; } @@ -81,8 +81,9 @@ ]), ml("div", {class: "hr"}), ml("div", {class: "row"}, [ - numMid(total[3], "W", "DC Power"), - numMid(total[4], "var", "Reactive Power") + numMid(total[3], "W", "Max Power"), + numMid(total[4], "W", "DC Power"), + numMid(total[5], "var", "Reactive Power") ]) ]) ]) @@ -91,8 +92,9 @@ function ivHead(obj) { if(0 != obj.status) { // only add totals if inverter is online total[0] += obj.ch[0][2]; // P_AC - total[3] += obj.ch[0][8]; // P_DC - total[4] += obj.ch[0][10]; // Q_AC + total[3] += obj.ch[0][11]; // MAX P_AC + total[4] += obj.ch[0][8]; // P_DC + total[5] += obj.ch[0][10]; // Q_AC } total[1] += obj.ch[0][7]; // YieldDay total[2] += obj.ch[0][6]; // YieldTotal @@ -295,15 +297,17 @@ modal("Info for inverter " + obj.name, ml("div", {}, html)); } + + function parseIvRadioStats(obj) { var html = ml("table", {class: "table"}, [ ml("tbody", {}, [ - tr("TX count", obj.tx_cnt), - tr("RX success", obj.rx_success), - tr("RX fail", obj.rx_fail), - tr("RX no answer", obj.rx_fail_answer), - tr("RX fragments", obj.frame_cnt), - tr("TX retransmits", obj.retransmits) + tr2(["TX count", obj.tx_cnt, ""]), + tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "%"]), + tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "%"]), + tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "%"]), + tr2(["RX fragments", obj.frame_cnt, ""]), + tr2(["TX retransmits", obj.retransmits, ""]) ]) ]); modal("Radio statistics for inverter " + obj.name, ml("div", {}, html)); diff --git a/src/web/web.h b/src/web/web.h index de013d19..5a83e353 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -487,48 +487,8 @@ class Web { request->arg("ipGateway").toCharArray(buf, 20); ah::ip2Arr(mConfig->sys.ip.gateway, buf); - // inverter - Inverter<> *iv; - for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - iv = mSys->getInverterByPos(i, false); - // enable communication - iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on"); - // address - request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); - if (strlen(buf) == 0) - memset(buf, 0, 20); - iv->config->serial.u64 = ah::Serial2u64(buf); - switch(iv->config->serial.b[4]) { - case 0x24: - case 0x22: - case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break; - - case 0x44: - case 0x42: - case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break; - - case 0x64: - case 0x62: - case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break; - default: break; - } - - // name - request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH); - - // max channel power / name - for (uint8_t j = 0; j < 6; j++) { - iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toDouble(); - iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; - request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH); - } - iv->initialized = true; - } - if (request->arg("invInterval") != "") mConfig->nrf.sendInterval = request->arg("invInterval").toInt(); - if (request->arg("invRetry") != "") - mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt(); mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); @@ -559,8 +519,6 @@ class Web { } } - // nrf24 amplifier power - mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03; mConfig->nrf.enabled = (request->arg("nrfEnable") == "on"); // cmt @@ -577,12 +535,10 @@ class Web { if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { mConfig->sun.lat = 0.0; mConfig->sun.lon = 0.0; - mConfig->sun.disNightCom = false; mConfig->sun.offsetSec = 0; } else { mConfig->sun.lat = request->arg("sunLat").toFloat(); mConfig->sun.lon = request->arg("sunLon").toFloat(); - mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on"); mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; } diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index dbe564cc..46a9275c 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -344,6 +344,9 @@ class HoymilesNRF: if not radio.begin(): raise RuntimeError('Can\'t open radio') + + if not radio.isChipConnected(): + logging.warning("could not connect to NRF24 radio") self.txpower = radio_config.get('txpower', 'max') @@ -411,7 +414,7 @@ class HoymilesNRF: self.radio.startListening() fragments = [] - + received_sth=False # Receive: Loop t_end = time.monotonic_ns()+timeout while time.monotonic_ns() < t_end: @@ -431,7 +434,7 @@ class HoymilesNRF: ch_rx=self.rx_channel, ch_tx=self.tx_channel, time_rx=datetime.now() ) - + received_sth=True yield fragment else: @@ -447,7 +450,11 @@ class HoymilesNRF: self.radio.setChannel(self.rx_channel) self.radio.startListening() - time.sleep(0.004) + time.sleep(0.005) + + if not received_sth: + raise TimeoutError + def next_rx_channel(self): """ diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index fef496f7..581b5acb 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -103,10 +103,11 @@ class SunsetHandler: def sun_status2mqtt(self, dtu_ser, dtu_name): if not mqtt_client or not self.suntimes: return - local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M") - local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M") - local_zone = self.suntimes.setlocal(datetime.now()).tzinfo._key + if self.suntimes: + local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M") + local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M") + local_zone = self.suntimes.setlocal(datetime.now()).tzinfo.key mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \ {'dis_night_comm' : 'True', \ 'local_sunrise' : local_sunrise, \ @@ -235,14 +236,14 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): if isinstance(result, hoymiles.decoders.StatusResponse): data = result.__dict__() - if 'event_count' in data: + if data is not None and 'event_count' in data: if event_message_index[inv_str] < data['event_count']: event_message_index[inv_str] = data['event_count'] command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) if mqtt_client: mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) - + if influx_client: influx_client.store_status(result) @@ -409,7 +410,7 @@ if __name__ == '__main__': str(g_inverter_ser), g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command' ) - mqtt_client.subscribe(topic_item[1]) + mqtt_client.client.subscribe(topic_item[1]) mqtt_command_topic_subs.append(topic_item) # start main-loop diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 11971a85..aa574f2d 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -227,6 +227,11 @@ class MqttOutputPlugin(OutputPluginFactory): """ data = response.__dict__() + + if data is None: + logging.warn("received data object is empty") + return + topic = params.get('topic', None) if not topic: topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' @@ -243,31 +248,33 @@ class MqttOutputPlugin(OutputPluginFactory): # AC Data phase_id = 0 phase_sum_power = 0 - for phase in data['phases']: - self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency'], self.qos, self.ret) - phase_id = phase_id + 1 - phase_sum_power += phase['power'] + if data['phases'] is not None: + for phase in data['phases']: + self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency'], self.qos, self.ret) + phase_id = phase_id + 1 + phase_sum_power += phase['power'] # DC Data string_id = 0 string_sum_power = 0 - for string in data['strings']: - if 'name' in string: - string_name = string['name'].replace(" ","_") - else: - string_name = string_id - self.client.publish(f'{topic}/emeter-dc/{string_name}/voltage', string['voltage'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter-dc/{string_name}/current', string['current'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter-dc/{string_name}/power', string['power'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldDay', string['energy_daily'], self.qos, self.ret) - self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldTotal', string['energy_total']/1000, self.qos, self.ret) - self.client.publish(f'{topic}/emeter-dc/{string_name}/Irradiation', string['irradiation'], self.qos, self.ret) - string_id = string_id + 1 - string_sum_power += string['power'] + if data['strings'] is not None: + for string in data['strings']: + if 'name' in string: + string_name = string['name'].replace(" ","_") + else: + string_name = string_id + self.client.publish(f'{topic}/emeter-dc/{string_name}/voltage', string['voltage'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/current', string['current'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/power', string['power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldDay', string['energy_daily'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldTotal', string['energy_total']/1000, self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/Irradiation', string['irradiation'], self.qos, self.ret) + string_id = string_id + 1 + string_sum_power += string['power'] # Global if data['event_count'] is not None: @@ -279,19 +286,23 @@ class MqttOutputPlugin(OutputPluginFactory): self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000, self.qos, self.ret) if data['yield_today'] is not None: self.client.publish(f'{topic}/YieldToday', data['yield_today']/1000, self.qos, self.ret) - self.client.publish(f'{topic}/Efficiency', data['efficiency'], self.qos, self.ret) + if data['efficiency'] is not None: + self.client.publish(f'{topic}/Efficiency', data['efficiency'], self.qos, self.ret) elif isinstance(response, HardwareInfoResponse): - self.client.publish(f'{topic}/Firmware/Version',\ - f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}', self.qos, self.ret) - - self.client.publish(f'{topic}/Firmware/Build_at',\ - f'{data["FW_build_dd"]}/{data["FW_build_mm"]}/{data["FW_build_yy"]}T{data["FW_build_HH"]}:{data["FW_build_MM"]}',\ - self.qos, self.ret) - - self.client.publish(f'{topic}/Firmware/HWPartId',\ - f'{data["FW_HW_ID"]}', self.qos, self.ret) + if data["FW_ver_maj"] is not None and data["FW_ver_min"] is not None and data["FW_ver_pat"] is not None: + self.client.publish(f'{topic}/Firmware/Version',\ + f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}', self.qos, self.ret) + + if data["FW_build_dd"] is not None and data["FW_build_mm"] is not None and data["FW_build_yy"] is not None and data["FW_build_HH"] is not None and data["FW_build_MM"] is not None: + self.client.publish(f'{topic}/Firmware/Build_at',\ + f'{data["FW_build_dd"]}/{data["FW_build_mm"]}/{data["FW_build_yy"]}T{data["FW_build_HH"]}:{data["FW_build_MM"]}',\ + self.qos, self.ret) + + if data["FW_HW_ID"] is not None: + self.client.publish(f'{topic}/Firmware/HWPartId',\ + f'{data["FW_HW_ID"]}', self.qos, self.ret) else: raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse')