From 12a9da55e3b5a5941d73acc0a583c7888a895b74 Mon Sep 17 00:00:00 2001 From: oberfritze <139758614+oberfritze@users.noreply.github.com> Date: Sat, 12 Aug 2023 19:17:06 +0200 Subject: [PATCH 1/4] Add files via upload Updated CHANGES.md about NRF24L01+ radio send channel evaluation --- src/CHANGES.md | 37 +- src/app.cpp | 1044 ++++++++++++++++++++++++------------------------ 2 files changed, 542 insertions(+), 539 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 1ec8d9f1e..dec129870 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,18 +1,19 @@ -Changelog for ahoy-all-in-one compared to 0.6.9 of the main project - -- read SML/OBIS from UART (stream parser with min resources needed); Connections 9600,8,n,1, GND-GND, VCC-3V3, TX-TX, RX-RX -- prepared to show chart of grid power and total solar ac power for current days daylight period (6 a.m. to 8 p.m.) -- show current grid power -- show max solar ac/dc power -- improved radio retransmit (complete retransmit if nothing was received, but only when inverter ought to be active) for the hm series -- shortcut radio traces a little bit - -DRAWBACKS: -- MQTT Source is commented out (except 1 var which is used for other purpose as well) -- only up to 2 Inverters are supported (was: 10) -- RX/TX of UART0 is used for serial interface to IR sensor. Of course you cannot operate a display that uses RX/TX pins of UART0, simultaneously. And unplug serial connection bevor updating via USB (see also below) -- Due to a non-matching licence model of the charting lib certain parts of visualization.html are commented out. See comments there. - -But: Currently there is enough heap available for stable operation on a ESP8266 plattform (WEMOS D1 R1). So adjust the number of inverters and enable MQTT to your needs and see if the AHOY-DTU is still stable in operation with your hw plattform. -To update firmware via USB, unplug serial connection to IR sensor first. Surprisingly during normal operation it seems that one can use a fully connected USB cable (for power supply). But I'm not sure if this works for all hardware plattforms. - +Changelog for ahoy-all-in-one compared to 0.6.9 of the main project + +- configurable read SML/OBIS from UART (stream parser with min resources needed); Connections 9600,8,n,1, GND-GND, VCC-3V3, TX-TX, RX-RX +- prepared to show chart of grid power and total solar ac power for current days daylight period (6 a.m. to 8 p.m.) +- show current grid power +- show max solar ac/dc power +- improved radio retransmit (complete retransmit if nothing was received, but only when inverter ought to be active) for the hm series) +- Heuristic for choosing the best send channel (of 5 possible) helps reducing retransmits +- shortcut radio traces a little bit + +DRAWBACKS: +- MQTT Source is commented out (except 1 var which is used for other purpose as well) +- only up to 2 Inverters are supported (was: 10) +- RX/TX of UART0 is used for serial interface to IR sensor. Of course you cannot operate a display that uses RX/TX pins of UART0, simultaneously. And unplug serial connection bevor updating via USB (see also below) +- Due to a non-matching licence model of the charting lib certain parts of visualization.html are commented out. See comments there. + +But: Currently there is enough heap available for stable operation on a ESP8266 plattform (WEMOS D1 R1). So adjust the number of inverters and enable MQTT to your needs and see if the AHOY-DTU is still stable in operation with your hw plattform. +To update firmware via USB, unplug serial connection to IR sensor first. Surprisingly during normal operation it seems that one can use a fully connected USB cable (for power supply). But I'm not sure if this works for all hardware plattforms. + diff --git a/src/app.cpp b/src/app.cpp index 226e9ecc6..8e0545986 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,521 +1,523 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de -// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed -//----------------------------------------------------------------------------- - -#include "app.h" -#include -#include "utils/sun.h" -#include "plugins/SML_OBIS_Parser.h" - -#ifndef min -#define min(a,b) (((a) < (b)) ? (a) : (b)) -#endif - -//----------------------------------------------------------------------------- -app::app() : ah::Scheduler() {} - -//----------------------------------------------------------------------------- -void app::setup() { -#ifdef AHOY_SML_OBIS_SUPPORT - /* Assumptions made: - Electricity meter sends SML telegrams via IR interface (9600,8,n,1) without being asked (typical behaviour). - An IR sensor is connected to the UART0 of AHOY DTU. Connected pins: GND-GND, 3V3-VCC, RX-RX, TX-TX. - */ - Serial.begin(9600, SERIAL_8N1, SERIAL_RX_ONLY); -#else - Serial.begin(115200); -#endif - while (!Serial) - yield(); - - ah::Scheduler::setup(); - - resetSystem(); - mSettings.setup(); - mSettings.getPtr(mConfig); - DPRINT(DBG_INFO, F("Settings valid: ")); - if (mSettings.getValid()) - DBGPRINTLN(F("true")); - else - DBGPRINTLN(F("false")); - - mSys.enableDebug(); - mSys.setup(&mTimestamp, mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); - -#if defined(AP_ONLY) - mInnerLoopCb = std::bind(&app::loopStandard, this); - #else - mInnerLoopCb = std::bind(&app::loopWifi, this); - #endif - - mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onWifi, this, std::placeholders::_1)); - #if !defined(AP_ONLY) - everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); - #endif - - mSys.addInverters(&mConfig->inst); - - mPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); - mPayload.enableSerialDebug(mConfig->serial.debug); - mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); - - mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); - mMiPayload.enableSerialDebug(mConfig->serial.debug); - mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); - - // DBGPRINTLN("--- after payload"); - // DBGPRINTLN(String(ESP.getFreeHeap())); - // DBGPRINTLN(String(ESP.getHeapFragmentation())); - // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); - - if (!mSys.Radio.isChipConnected()) - DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); - - // when WiFi is in client mode, then enable mqtt broker - #if !defined(AP_ONLY) && defined (AHOY_MQTT_SUPPORT) - mMqttEnabled = (mConfig->mqtt.broker[0] > 0); - if (mMqttEnabled) { - mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp); - mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); - mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - mMiPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - } - #endif - setupLed(); - - mWeb.setup(this, &mSys, mConfig); - mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); - - mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); - - // Plugins - if (mConfig->plugin.display.type != 0) - mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion); - - mPubSerial.setup(mConfig, &mSys, &mTimestamp); - -#ifdef AHOY_SML_OBIS_SUPPORT - sml_setup (this, &mTimestamp); -#endif - - regularTickers(); - - // DBGPRINTLN("--- end setup"); - // DBGPRINTLN(String(ESP.getFreeHeap())); - // DBGPRINTLN(String(ESP.getHeapFragmentation())); - // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); -} - -//----------------------------------------------------------------------------- -void app::loop(void) { - mInnerLoopCb(); -} - -//----------------------------------------------------------------------------- -void app::loopStandard(void) { - ah::Scheduler::loop(); - - if (mSys.Radio.loop()) { - while (!mSys.Radio.mBufCtrl.empty()) { - packet_t *p = &mSys.Radio.mBufCtrl.front(); - - if (mConfig->serial.debug) { - DPRINT(DBG_INFO, F("RX ")); - DBGPRINT(String(p->len)); -#ifdef undef - DBGPRINT(F("B Ch")); - DBGPRINT(String(p->ch)); - DBGPRINT(F(" | ")); - mSys.Radio.dumpBuf(p->packet, p->len); -#else - DBGPRINTLN(" Bytes"); -#endif - } - - mStat.frmCnt++; - - Inverter<> *iv = mSys.findInverter(&p->packet[1]); - if (NULL != iv) { - if (IV_HM == iv->ivGen) - mPayload.add(iv, p); - else - mMiPayload.add(iv, p); - } - mSys.Radio.mBufCtrl.pop(); - yield(); - } - mPayload.process(true); - mMiPayload.process(true); - } - mPayload.loop(); - mMiPayload.loop(); - -#ifdef AHOY_MQTT_SUPPORT - if (mMqttEnabled) { - mMqtt.loop(); - } -#endif - -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - sml_loop (); - } -#endif -} - -//----------------------------------------------------------------------------- -void app::loopWifi(void) { - ah::Scheduler::loop(); - yield(); -} - -//----------------------------------------------------------------------------- -void app::onWifi(bool gotIp) { - DPRINTLN(DBG_DEBUG, F("onWifi")); - ah::Scheduler::resetTicker(); - regularTickers(); // reinstall regular tickers - if (gotIp) { - mInnerLoopCb = std::bind(&app::loopStandard, this); - every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); - - mMqttReconnect = true; - - mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! - once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); - if (WIFI_AP == WiFi.getMode()) { -#ifdef AHOY_MQTT_SUPPORT - mMqttEnabled = false; -#endif - everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); - } - } else { - mInnerLoopCb = std::bind(&app::loopWifi, this); - everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); - } -} - -//----------------------------------------------------------------------------- -void app::regularTickers(void) { - DPRINTLN(DBG_DEBUG, F("regularTickers")); - everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); - // Plugins - if (mConfig->plugin.display.type != 0) - everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); - every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); -} - -//----------------------------------------------------------------------------- -void app::tickNtpUpdate(void) { - uint32_t nxtTrig = 5; // default: check again in 5 sec - bool isOK = mWifi.getNtpTime(); - if (isOK || mTimestamp != 0) { -#ifdef AHOY_MQTT_SUPPORT - if (mMqttReconnect && mMqttEnabled) { - mMqtt.tickerSecond(); - everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); - everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); - } -#endif - - // only install schedulers once even if NTP wasn't successful in first loop - if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed - if (mConfig->inst.rstValsNotAvail) - everyMin(std::bind(&app::tickMinute, this), "tMin"); - - uint32_t localTime = gTimezone.toLocal(mTimestamp); - uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time - onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); - - mSys.cleanup_history(); -#ifdef AHOY_SML_OBIS_SUPPORT - // design: allways try to clean up - sml_cleanup_history (); -#endif - } - - nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min - - if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { - mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; - tickCalcSunrise(); - } - - // immediately start communicating - // @TODO: leads to reboot loops? not sure #674 - if (isOK && mSendFirst) { - mSendFirst = false; - once(std::bind(&app::tickSend, this), 2, "senOn"); - } - - mMqttReconnect = false; - } - once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); -} - -//----------------------------------------------------------------------------- -void app::tickCalcSunrise(void) { - if (mSunrise == 0) // on boot/reboot calc sun values for current time - ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); - - if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day - ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); - - tickIVCommunication(); - - uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop - onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); -#ifdef AHOY_MQTT_SUPPORT - if (mMqttEnabled) - tickSun(); -#endif -} - -//----------------------------------------------------------------------------- -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; - } - } - if (nxtTrig != 0) - onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); - } - tickComm(); -} - -#ifdef AHOY_MQTT_SUPPORT -//----------------------------------------------------------------------------- -void app::tickSun(void) { - // only used and enabled by MQTT (see setup()) - if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) - once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry -} -#endif - -//----------------------------------------------------------------------------- -void app::tickComm(void) { - if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) - once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); - -#ifdef AHOY_MQTT_SUPPORT - if (mMqttEnabled) { - if (!mMqtt.tickerComm(!mIVCommunicationOn)) - once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s - } -#endif -} - -//----------------------------------------------------------------------------- -void app::tickZeroValues(void) { - Inverter<> *iv; - // set values to zero, except yields - for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { - iv = mSys.getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - mPayload.zeroInverterValues(iv); - } -} - -//----------------------------------------------------------------------------- -void app::tickMinute(void) { - // only triggered if 'reset values on no avail is enabled' - - Inverter<> *iv; - // set values to zero, except yields - for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { - iv = mSys.getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) - mPayload.zeroInverterValues(iv); - } -} - -//----------------------------------------------------------------------------- -void app::tickMidnight(void) { - if (mConfig->inst.rstYieldMidNight) { - // only if 'reset values at midnight is enabled' - uint32_t localTime = gTimezone.toLocal(mTimestamp); - uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time - onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); - - Inverter<> *iv; - // set values to zero, except yield total - for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { - iv = mSys.getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - mPayload.zeroInverterValues(iv); - mPayload.zeroYieldDay(iv); - } -#ifdef AHOY_MQTT_SUPPORT - if (mMqttEnabled) - mMqtt.tickerMidnight(); -#endif - } - mSys.cleanup_history (); -#ifdef AHOY_SML_OBIS_SUPPORT - // design: allways try to clean up - sml_cleanup_history(); -#endif -} - -//----------------------------------------------------------------------------- -void app::tickSend(void) { - if (!mSys.Radio.isChipConnected()) { - DPRINTLN(DBG_WARN, F("NRF24 not connected!")); - return; - } - if (mIVCommunicationOn) { - if (!mSys.Radio.mBufCtrl.empty()) { - if (mConfig->serial.debug) { - DPRINT(DBG_DEBUG, F("recbuf not empty! #")); - DBGPRINTLN(String(mSys.Radio.mBufCtrl.size())); - } - } - - int8_t maxLoop = MAX_NUM_INVERTERS; - Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); - 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) { - if (iv->ivGen == IV_HM) - mPayload.ivSend(iv); - else - mMiPayload.ivSend(iv); - } - } - } 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(); -} - -//----------------------------------------------------------------------------- -void app::resetSystem(void) { - snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); - -#ifdef AP_ONLY - mTimestamp = 1; -#endif - - mSendFirst = true; - - mSunrise = 0; - mSunset = 0; - -#ifdef AHOY_MQTT_SUPPORT - mMqttEnabled = false; -#endif - - mSendLastIvId = 0; - mShowRebootRequest = false; - mIVCommunicationOn = true; - mSavePending = false; - mSaveReboot = false; - - memset(&mStat, 0, sizeof(statistics_t)); -} - -#ifdef AHOY_MQTT_SUPPORT -//----------------------------------------------------------------------------- -void app::mqttSubRxCb(JsonObject obj) { - mApi.ctrlRequest(obj); -} -#endif - -//----------------------------------------------------------------------------- -void app::setupLed(void) { - uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; - - if (mConfig->led.led0 != 0xff) { - pinMode(mConfig->led.led0, OUTPUT); - digitalWrite(mConfig->led.led0, led_off); - } - if (mConfig->led.led1 != 0xff) { - pinMode(mConfig->led.led1, OUTPUT); - digitalWrite(mConfig->led.led1, led_off); - } -} - -//----------------------------------------------------------------------------- -void app::updateLed(void) { - uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; - uint8_t led_on = (mConfig->led.led_high_active) ? HIGH : LOW; - - if (mConfig->led.led0 != 0xff) { - Inverter<> *iv = mSys.getInverterByPos(0); - if (NULL != iv) { - if (iv->isProducing(mTimestamp)) - digitalWrite(mConfig->led.led0, led_on); - else - digitalWrite(mConfig->led.led0, led_off); - } - } - -#ifdef AHOY_MQTT_SUPPORT - if (mConfig->led.led1 != 0xff) { - if (getMqttIsConnected()) { - digitalWrite(mConfig->led.led1, led_on); - } else { - digitalWrite(mConfig->led.led1, led_off); - } - } -#endif -} - -//----------------------------------------------------------------------------- -void app::check_hist_file (File file) -{ - if (file) { - uint16_t exp_index = AHOY_MIN_PAC_SUN_HOUR * 60 / AHOY_PAC_INTERVAL, index; - unsigned char data[4]; - - while (file.read (data, sizeof (data)) == sizeof (data)) { - index = data[0] + (data[1] << 8); - if (index != exp_index) { - DPRINTLN (DBG_WARN, "Unexpected " + String (index) + " <-> " + String (exp_index)); - } - exp_index = index + 1; - } - file.close(); - } -} - -//----------------------------------------------------------------------------- -void app::show_history (String path) -{ - Dir dir = LittleFS.openDir (path); - - DPRINTLN (DBG_INFO, "Enter Dir: " + path); - while (dir.next()) { - if (dir.isDirectory ()) { - show_history (path + "/" + dir.fileName()); - } else { - DPRINTLN (DBG_INFO, "file " + dir.fileName() + - ", Size: " + String (dir.fileSize())); - check_hist_file (dir.openFile ("r")); - } - } - DPRINTLN (DBG_INFO, "Leave Dir: " + path); -} +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#include "app.h" +#include +#include "utils/sun.h" +#include "plugins/SML_OBIS_Parser.h" + +#ifndef min +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#endif + +//----------------------------------------------------------------------------- +app::app() : ah::Scheduler() {} + +//----------------------------------------------------------------------------- +void app::setup() { +#ifdef AHOY_SML_OBIS_SUPPORT + /* Assumptions made: + Electricity meter sends SML telegrams via IR interface (9600,8,n,1) without being asked (typical behaviour). + An IR sensor is connected to the UART0 of AHOY DTU. Connected pins: GND-GND, 3V3-VCC, RX-RX, TX-TX. + */ + Serial.begin(9600, SERIAL_8N1, SERIAL_RX_ONLY); +#else + Serial.begin(115200); +#endif + while (!Serial) + yield(); + + ah::Scheduler::setup(); + + resetSystem(); + mSettings.setup(); + mSettings.getPtr(mConfig); + DPRINT(DBG_INFO, F("Settings valid: ")); + if (mSettings.getValid()) + DBGPRINTLN(F("true")); + else + DBGPRINTLN(F("false")); + + mSys.enableDebug(); + mSys.setup(&mTimestamp, mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); + +#if defined(AP_ONLY) + mInnerLoopCb = std::bind(&app::loopStandard, this); + #else + mInnerLoopCb = std::bind(&app::loopWifi, this); + #endif + + mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onWifi, this, std::placeholders::_1)); + #if !defined(AP_ONLY) + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); + #endif + + mSys.addInverters(&mConfig->inst); + + mPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); + mPayload.enableSerialDebug(mConfig->serial.debug); + mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); + + mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); + mMiPayload.enableSerialDebug(mConfig->serial.debug); + mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); + + // DBGPRINTLN("--- after payload"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); + + if (!mSys.Radio.isChipConnected()) + DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); + + // when WiFi is in client mode, then enable mqtt broker + #if !defined(AP_ONLY) && defined (AHOY_MQTT_SUPPORT) + mMqttEnabled = (mConfig->mqtt.broker[0] > 0); + if (mMqttEnabled) { + mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp); + mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); + mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + mMiPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + } + #endif + setupLed(); + + mWeb.setup(this, &mSys, mConfig); + mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); + + mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); + + // Plugins + if (mConfig->plugin.display.type != 0) + mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion); + + mPubSerial.setup(mConfig, &mSys, &mTimestamp); + +#ifdef AHOY_SML_OBIS_SUPPORT + sml_setup (this, &mTimestamp); +#endif + + regularTickers(); + + // DBGPRINTLN("--- end setup"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); +} + +//----------------------------------------------------------------------------- +void app::loop(void) { + mInnerLoopCb(); +} + +//----------------------------------------------------------------------------- +void app::loopStandard(void) { + ah::Scheduler::loop(); + + if (mSys.Radio.loop()) { + while (!mSys.Radio.mBufCtrl.empty()) { + packet_t *p = &mSys.Radio.mBufCtrl.front(); + + if (mConfig->serial.debug) { +#ifdef undef + DPRINT(DBG_INFO, F("RX ")); + DBGPRINT(String(p->len)); + DBGPRINT(F("B Ch")); + DBGPRINT(String(p->ch)); + DBGPRINT(F(" | ")); + mSys.Radio.dumpBuf(p->packet, p->len); +#else + DPRINTLN(DBG_INFO, "RX (Ch " + String (p->ch) + "), " + + String (p->len) + " Bytes"); +#endif + } + + mStat.frmCnt++; + + Inverter<> *iv = mSys.findInverter(&p->packet[1]); + if (NULL != iv) { + if (IV_HM == iv->ivGen) + mPayload.add(iv, p); + else + mMiPayload.add(iv, p); + } + mSys.Radio.mBufCtrl.pop(); + yield(); + } + mPayload.process(true); + mMiPayload.process(true); + } + mPayload.loop(); + mMiPayload.loop(); + +#ifdef AHOY_MQTT_SUPPORT + if (mMqttEnabled) { + mMqtt.loop(); + } +#endif + +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + sml_loop (); + } +#endif +} + +//----------------------------------------------------------------------------- +void app::loopWifi(void) { + ah::Scheduler::loop(); + yield(); +} + +//----------------------------------------------------------------------------- +void app::onWifi(bool gotIp) { + DPRINTLN(DBG_DEBUG, F("onWifi")); + ah::Scheduler::resetTicker(); + regularTickers(); // reinstall regular tickers + if (gotIp) { + mInnerLoopCb = std::bind(&app::loopStandard, this); + every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); + + mMqttReconnect = true; + + mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! + once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); + if (WIFI_AP == WiFi.getMode()) { +#ifdef AHOY_MQTT_SUPPORT + mMqttEnabled = false; +#endif + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); + } + } else { + mInnerLoopCb = std::bind(&app::loopWifi, this); + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); + } +} + +//----------------------------------------------------------------------------- +void app::regularTickers(void) { + DPRINTLN(DBG_DEBUG, F("regularTickers")); + everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); + // Plugins + if (mConfig->plugin.display.type != 0) + everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); + every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); +} + +//----------------------------------------------------------------------------- +void app::tickNtpUpdate(void) { + uint32_t nxtTrig = 5; // default: check again in 5 sec + bool isOK = mWifi.getNtpTime(); + if (isOK || mTimestamp != 0) { +#ifdef AHOY_MQTT_SUPPORT + if (mMqttReconnect && mMqttEnabled) { + mMqtt.tickerSecond(); + everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); + everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); + } +#endif + + // only install schedulers once even if NTP wasn't successful in first loop + if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed + if (mConfig->inst.rstValsNotAvail) + everyMin(std::bind(&app::tickMinute, this), "tMin"); + + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); + + mSys.cleanup_history(); +#ifdef AHOY_SML_OBIS_SUPPORT + // design: allways try to clean up + sml_cleanup_history (); +#endif + } + + nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min + + if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { + mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; + tickCalcSunrise(); + } + + // immediately start communicating + // @TODO: leads to reboot loops? not sure #674 + if (isOK && mSendFirst) { + mSendFirst = false; + once(std::bind(&app::tickSend, this), 2, "senOn"); + } + + mMqttReconnect = false; + } + once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); +} + +//----------------------------------------------------------------------------- +void app::tickCalcSunrise(void) { + if (mSunrise == 0) // on boot/reboot calc sun values for current time + ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); + + if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day + ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); + + tickIVCommunication(); + + uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop + onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); +#ifdef AHOY_MQTT_SUPPORT + if (mMqttEnabled) + tickSun(); +#endif +} + +//----------------------------------------------------------------------------- +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; + } + } + if (nxtTrig != 0) + onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); + } + tickComm(); +} + +#ifdef AHOY_MQTT_SUPPORT +//----------------------------------------------------------------------------- +void app::tickSun(void) { + // only used and enabled by MQTT (see setup()) + if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) + once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry +} +#endif + +//----------------------------------------------------------------------------- +void app::tickComm(void) { + if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) + once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); + +#ifdef AHOY_MQTT_SUPPORT + if (mMqttEnabled) { + if (!mMqtt.tickerComm(!mIVCommunicationOn)) + once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s + } +#endif +} + +//----------------------------------------------------------------------------- +void app::tickZeroValues(void) { + Inverter<> *iv; + // set values to zero, except yields + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + mPayload.zeroInverterValues(iv); + } +} + +//----------------------------------------------------------------------------- +void app::tickMinute(void) { + // only triggered if 'reset values on no avail is enabled' + + Inverter<> *iv; + // set values to zero, except yields + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) + mPayload.zeroInverterValues(iv); + } +} + +//----------------------------------------------------------------------------- +void app::tickMidnight(void) { + if (mConfig->inst.rstYieldMidNight) { + // only if 'reset values at midnight is enabled' + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); + + Inverter<> *iv; + // set values to zero, except yield total + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + mPayload.zeroInverterValues(iv); + mPayload.zeroYieldDay(iv); + } +#ifdef AHOY_MQTT_SUPPORT + if (mMqttEnabled) + mMqtt.tickerMidnight(); +#endif + } + mSys.Radio.resetSendChannelQuality(); + mSys.cleanup_history(); +#ifdef AHOY_SML_OBIS_SUPPORT + // design: allways try to clean up + sml_cleanup_history(); +#endif +} + +//----------------------------------------------------------------------------- +void app::tickSend(void) { + if (!mSys.Radio.isChipConnected()) { + DPRINTLN(DBG_WARN, F("NRF24 not connected!")); + return; + } + if (mIVCommunicationOn) { + if (!mSys.Radio.mBufCtrl.empty()) { + if (mConfig->serial.debug) { + DPRINT(DBG_DEBUG, F("recbuf not empty! #")); + DBGPRINTLN(String(mSys.Radio.mBufCtrl.size())); + } + } + + int8_t maxLoop = MAX_NUM_INVERTERS; + Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); + 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) { + if (iv->ivGen == IV_HM) + mPayload.ivSend(iv); + else + mMiPayload.ivSend(iv); + } + } + } 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(); +} + +//----------------------------------------------------------------------------- +void app::resetSystem(void) { + snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + +#ifdef AP_ONLY + mTimestamp = 1; +#endif + + mSendFirst = true; + + mSunrise = 0; + mSunset = 0; + +#ifdef AHOY_MQTT_SUPPORT + mMqttEnabled = false; +#endif + + mSendLastIvId = 0; + mShowRebootRequest = false; + mIVCommunicationOn = true; + mSavePending = false; + mSaveReboot = false; + + memset(&mStat, 0, sizeof(statistics_t)); +} + +#ifdef AHOY_MQTT_SUPPORT +//----------------------------------------------------------------------------- +void app::mqttSubRxCb(JsonObject obj) { + mApi.ctrlRequest(obj); +} +#endif + +//----------------------------------------------------------------------------- +void app::setupLed(void) { + uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; + + if (mConfig->led.led0 != 0xff) { + pinMode(mConfig->led.led0, OUTPUT); + digitalWrite(mConfig->led.led0, led_off); + } + if (mConfig->led.led1 != 0xff) { + pinMode(mConfig->led.led1, OUTPUT); + digitalWrite(mConfig->led.led1, led_off); + } +} + +//----------------------------------------------------------------------------- +void app::updateLed(void) { + uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; + uint8_t led_on = (mConfig->led.led_high_active) ? HIGH : LOW; + + if (mConfig->led.led0 != 0xff) { + Inverter<> *iv = mSys.getInverterByPos(0); + if (NULL != iv) { + if (iv->isProducing(mTimestamp)) + digitalWrite(mConfig->led.led0, led_on); + else + digitalWrite(mConfig->led.led0, led_off); + } + } + +#ifdef AHOY_MQTT_SUPPORT + if (mConfig->led.led1 != 0xff) { + if (getMqttIsConnected()) { + digitalWrite(mConfig->led.led1, led_on); + } else { + digitalWrite(mConfig->led.led1, led_off); + } + } +#endif +} + +//----------------------------------------------------------------------------- +void app::check_hist_file (File file) +{ + if (file) { + uint16_t exp_index = AHOY_MIN_PAC_SUN_HOUR * 60 / AHOY_PAC_INTERVAL, index; + unsigned char data[4]; + + while (file.read (data, sizeof (data)) == sizeof (data)) { + index = data[0] + (data[1] << 8); + if (index != exp_index) { + DPRINTLN (DBG_WARN, "Unexpected " + String (index) + " <-> " + String (exp_index)); + } + exp_index = index + 1; + } + file.close(); + } +} + +//----------------------------------------------------------------------------- +void app::show_history (String path) +{ + Dir dir = LittleFS.openDir (path); + + DPRINTLN (DBG_INFO, "Enter Dir: " + path); + while (dir.next()) { + if (dir.isDirectory ()) { + show_history (path + "/" + dir.fileName()); + } else { + DPRINTLN (DBG_INFO, "file " + dir.fileName() + + ", Size: " + String (dir.fileSize())); + check_hist_file (dir.openFile ("r")); + } + } + DPRINTLN (DBG_INFO, "Leave Dir: " + path); +} From 3d1a946bff51b838c28028136f3919340ec68d6b Mon Sep 17 00:00:00 2001 From: oberfritze <139758614+oberfritze@users.noreply.github.com> Date: Sat, 12 Aug 2023 19:19:48 +0200 Subject: [PATCH 2/4] Add files via upload NRF24L01+ Radio send channel evaluation and hopping heuristik (helps to reduces TX retransmits in relation to TX count) --- src/hm/hmPayload.h | 929 +++++++++++++++++++++++---------------------- src/hm/hmRadio.h | 849 +++++++++++++++++++++++------------------ 2 files changed, 941 insertions(+), 837 deletions(-) diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h index dfb8464cc..b0d6d8ed5 100644 --- a/src/hm/hmPayload.h +++ b/src/hm/hmPayload.h @@ -1,458 +1,471 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __HM_PAYLOAD_H__ -#define __HM_PAYLOAD_H__ - -#include "../utils/dbg.h" -#include "../utils/crc.h" -#include "../config/config.h" -#include - -typedef struct { - uint8_t txCmd; - uint8_t txId; - uint8_t invId; - uint32_t ts; - uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; - uint8_t len[MAX_PAYLOAD_ENTRIES]; - bool complete; - uint8_t maxPackId; - bool lastFound; - uint8_t retransmits; - bool requested; - bool gotFragment; - bool rxTmo; -} invPayload_t; - - -typedef std::function payloadListenerType; -typedef std::function alarmListenerType; - - -template -class HmPayload { - public: - HmPayload() {} - - void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { - mApp = app; - mSys = sys; - mStat = stat; - mMaxRetrans = maxRetransmits; - mTimestamp = timestamp; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - reset(i); - } - 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); - mHighPrioIv = NULL; - } - } - - void zeroYieldDay(Inverter<> *iv) { - DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - uint8_t pos; - for(uint8_t ch = 0; ch <= iv->channels; ch++) { - pos = iv->getPosByChFld(ch, FLD_YD, rec); - iv->setValue(pos, rec, 0.0f); - pos = iv->getPosByChFld(ch, FLD_MP, rec); - iv->setValue(pos, rec, 0.0f); - } - } - - void zeroInverterValues(Inverter<> *iv) { - DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - for(uint8_t ch = 0; ch <= iv->channels; ch++) { - uint8_t pos = 0; - for(uint8_t fld = 0; fld < FLD_EVT; fld++) { - switch(fld) { - case FLD_YD: - case FLD_YT: - case FLD_MP: - continue; - } - pos = iv->getPosByChFld(ch, fld, rec); - iv->setValue(pos, rec, 0.0f); - } - } - - notify(RealTimeRunData_Debug); - } - - void ivSendHighPrio(Inverter<> *iv) { - mHighPrioIv = iv; - } - - void ivSend(Inverter<> *iv, bool highPrio = false) { - bool save_rxTmo; - - 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) { - mStat->rxFailNoAnser++; // got nothing - if (mSerialDebug) - DBGPRINTLN(F("enqueued cmd failed/timeout")); - } else { - mStat->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 - } - } - } - - save_rxTmo = mPayload[iv->id].rxTmo; - reset(iv->id); - mPayload[iv->id].rxTmo = save_rxTmo; - mPayload[iv->id].requested = true; - - yield(); -#ifdef undef - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("Requesting Inv SN ")); - DBGPRINTLN(String(iv->config->serial.u64, HEX)); - } -#endif - - if (iv->getDevControlRequest()) { - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("Devcontrol request 0x")); - DBGPRINT(String(iv->devControlCmd, HEX)); - DBGPRINT(F(" power limit ")); - DBGPRINTLN(String(iv->powerLimit[0])); - } - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); - mPayload[iv->id].txCmd = iv->devControlCmd; - //iv->clearCmdQueue(); - //iv->enqueCommand(SystemConfigPara); // read back power limit - } else { - uint8_t cmd = iv->getQueuedCmd(); - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(cmd); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); - 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; - } - - 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)) { -#ifdef AHOY_MQTT_SUPPORT - mApp->setMqttPowerLimitAck(iv); -#endif - } else { - ok = false; - } - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("has ")); - if(!ok) DBGPRINT(F("not ")); - DBGPRINT(F("accepted power limit set point ")); - DBGPRINT(String(iv->powerLimit[0])); - DBGPRINT(F(" with PowerLimitControl ")); - DBGPRINTLN(String(iv->powerLimit[1])); - - iv->clearCmdQueue(); - iv->enqueCommand(SystemConfigPara); // read back power limit - 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 (IV_MI == iv->ivGen) // only process HM inverters - continue; // skip to next inverter - - if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { - // no processing needed if txId is not 0x95 - mPayload[iv->id].complete = true; - continue; // skip to next inverter - } - - if (!mPayload[iv->id].complete) { - bool crcPass, pyldComplete; - crcPass = build(iv->id, &pyldComplete); - if (!crcPass && !pyldComplete) { // payload not complete - if ((mPayload[iv->id].requested) && (retransmit)) { - if (mPayload[iv->id].retransmits < mMaxRetrans) { - mPayload[iv->id].retransmits++; - if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { - // This is required to prevent retransmissions without answer. - DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); - mPayload[iv->id].retransmits = mMaxRetrans; - } else if(iv->devControlCmd == ActivePowerContr) { - DPRINT_IVID(DBG_INFO, iv->id); - DPRINTLN(DBG_INFO, F("retransmit power limit")); - mSys->Radio.sendControlPacket(iv->radioId.u64, 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)); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, 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) { - DPRINT_IVID(DBG_WARN, iv->id); - DBGPRINT(F("Frame ")); - DBGPRINT(String(i + 1)); - DBGPRINTLN(F(" missing: Request Retransmit")); - mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); - 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++; - DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); - } 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 { // payload complete -#ifdef undef - DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - DPRINT(DBG_INFO, F("procPyld: txid: 0x")); - DBGHEXLN(mPayload[iv->id].txId); -#endif - 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].rxTmo = false; - - uint8_t payload[128]; - uint8_t payloadLen = 0; - - memset(payload, 0, 128); - - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { - memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); - payloadLen += (mPayload[iv->id].len[i]); - yield(); - } - payloadLen -= 2; - -#ifdef undef - if (mSerialDebug) { - DPRINT(DBG_INFO, F("Payload (")); - DBGPRINT(String(payloadLen)); - DBGPRINT(F("): ")); - mSys->Radio.dumpBuf(payload, payloadLen); - } -#endif - - if (NULL == rec) { - DPRINTLN(DBG_ERROR, F("record is NULL!")); - } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { - if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) - mStat->rxSuccess++; - - rec->ts = mPayload[iv->id].ts; - for (uint8_t i = 0; i < rec->length; i++) { - iv->addValue(i, payload, rec); - yield(); - } - iv->doCalculations(); - uint8_t pos = iv->getPosByChFld(CH0, FLD_PAC, rec); - if (pos != 0xff) { - float ac_power = iv->getValue(pos, rec); - mSys->handle_pac (iv, (uint16_t)(ac_power+0.5f)); - } - - notify(mPayload[iv->id].txCmd); - - if(AlarmData == mPayload[iv->id].txCmd) { - uint8_t i = 0; - uint16_t code; - uint32_t start, end; - while(1) { - code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); - if(0 == code) - break; - if (NULL != mCbAlarm) - (mCbAlarm)(code, start, end); - yield(); - } - } - } else { - DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); - DBGPRINT(String(rec->pyldLen)); - DBGPRINTLN(F(" bytes")); - mStat->rxFail++; - } - - iv->setQueuedCmdFinished(); - } - } - yield(); - } - } - - private: - void notify(uint8_t val) { - if(NULL != mCbPayload) - (mCbPayload)(val); - } - - void notify(uint16_t code, uint32_t start, uint32_t endTime) { - if (NULL != mCbAlarm) - (mCbAlarm)(code, start, endTime); - } - - bool build(uint8_t id, bool *complete) { - DPRINTLN(DBG_VERBOSE, F("build")); - uint16_t crc = 0xffff, crcRcv = 0x0000; - if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) - mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; - - // check if all fragments are there - *complete = true; - for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { - if(mPayload[id].len[i] == 0) - *complete = false; - } - if(!*complete) - return false; - - for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { - if (mPayload[id].len[i] > 0) { - if (i == (mPayload[id].maxPackId - 1)) { - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); - crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); - } else - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); - } - yield(); - } - - return (crc == crcRcv) ? true : false; - } - - void reset(uint8_t id) { -#ifdef undef - DPRINT_IVID(DBG_INFO, id); - DBGPRINTLN(F("resetPayload")); -#endif - 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 = true; // design: dont start with complete retransmit - } - - IApp *mApp; - HMSYSTEM *mSys; - statistics_t *mStat; - uint8_t mMaxRetrans; - uint32_t *mTimestamp; - invPayload_t mPayload[MAX_NUM_INVERTERS]; - bool mSerialDebug; - Inverter<> *mHighPrioIv; - - alarmListenerType mCbAlarm; - payloadListenerType mCbPayload; -}; - -#endif /*__HM_PAYLOAD_H__*/ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __HM_PAYLOAD_H__ +#define __HM_PAYLOAD_H__ + +#include "../utils/dbg.h" +#include "../utils/crc.h" +#include "../config/config.h" +#include + +typedef struct { + uint8_t txCmd; + uint8_t txId; + uint8_t invId; + uint32_t ts; + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + uint8_t maxPackId; + bool lastFound; + uint8_t retransmits; + bool requested; + bool gotFragment; + bool rxTmo; + uint8_t lastFragments; // for send quality measurement +} invPayload_t; + + +typedef std::function payloadListenerType; +typedef std::function alarmListenerType; + + +template +class HmPayload { + public: + HmPayload() {} + + void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + mApp = app; + mSys = sys; + mStat = stat; + mMaxRetrans = maxRetransmits; + mTimestamp = timestamp; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + reset(i); + } + 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); + mHighPrioIv = NULL; + } + } + + void zeroYieldDay(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + uint8_t pos; + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + pos = iv->getPosByChFld(ch, FLD_YD, rec); + iv->setValue(pos, rec, 0.0f); + pos = iv->getPosByChFld(ch, FLD_MP, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + void zeroInverterValues(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + uint8_t pos = 0; + for(uint8_t fld = 0; fld < FLD_EVT; fld++) { + switch(fld) { + case FLD_YD: + case FLD_YT: + case FLD_MP: + continue; + } + pos = iv->getPosByChFld(ch, fld, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + notify(RealTimeRunData_Debug); + } + + void ivSendHighPrio(Inverter<> *iv) { + mHighPrioIv = iv; + } + + void ivSend(Inverter<> *iv, bool highPrio = false) { + bool save_rxTmo; + + 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) { + mStat->rxFailNoAnser++; // got nothing + if (mSerialDebug) + DBGPRINTLN(F("enqueued cmd failed/timeout")); + } else { + mStat->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 + } + } + } + + save_rxTmo = mPayload[iv->id].rxTmo; + reset(iv->id); + mPayload[iv->id].rxTmo = save_rxTmo; + mPayload[iv->id].requested = true; + + yield(); +#ifdef undef + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + } +#endif + + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DBGPRINT(String(iv->devControlCmd, HEX)); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + //iv->clearCmdQueue(); + //iv->enqueCommand(SystemConfigPara); // read back power limit + } else { + uint8_t cmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(cmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); + 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; + } + + 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)) { +#ifdef AHOY_MQTT_SUPPORT + mApp->setMqttPowerLimitAck(iv); +#endif + } else { + ok = false; + } + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("has ")); + if(!ok) DBGPRINT(F("not ")); + DBGPRINT(F("accepted power limit set point ")); + DBGPRINT(String(iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(iv->powerLimit[1])); + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // read back power limit + 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 (IV_MI == iv->ivGen) // only process HM inverters + continue; // skip to next inverter + + if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { + // no processing needed if txId is not 0x95 + mPayload[iv->id].complete = true; + continue; // skip to next inverter + } + + if (!mPayload[iv->id].complete) { + bool crcPass, pyldComplete; + uint8 Fragments; + crcPass = build(iv->id, &pyldComplete, &Fragments); + + // evaluate quality of send channel with rcv params + mSys->Radio.evalSendChannelQuality (crcPass, mPayload[iv->id].retransmits, + Fragments, mPayload[iv->id].lastFragments); + mPayload[iv->id].lastFragments = Fragments; + 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")); + mSys->Radio.sendControlPacket(iv->radioId.u64, 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)); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, 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) { + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINT(F("Frame ")); + DBGPRINT(String(i + 1)); + DBGPRINTLN(F(" missing: Request Retransmit")); + mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); + 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++; + DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } 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 { // payload complete +#ifdef undef + DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + DPRINT(DBG_INFO, F("procPyld: txid: 0x")); + DBGHEXLN(mPayload[iv->id].txId); +#endif + 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].rxTmo = false; + + uint8_t payload[128]; + uint8_t payloadLen = 0; + + memset(payload, 0, 128); + + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + payloadLen += (mPayload[iv->id].len[i]); + yield(); + } + payloadLen -= 2; + +#ifdef undef + if (mSerialDebug) { + DPRINT(DBG_INFO, F("Payload (")); + DBGPRINT(String(payloadLen)); + DBGPRINT(F("): ")); + mSys->Radio.dumpBuf(payload, payloadLen); + } +#endif + + if (NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + mStat->rxSuccess++; + + rec->ts = mPayload[iv->id].ts; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + uint8_t pos = iv->getPosByChFld(CH0, FLD_PAC, rec); + + if (pos != 0xff) { + float ac_power = iv->getValue(pos, rec); + mSys->handle_pac (iv, (uint16_t)(ac_power+0.5f)); + } + + notify(mPayload[iv->id].txCmd); + + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + } + } else { + DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); + DBGPRINT(String(rec->pyldLen)); + DBGPRINTLN(F(" bytes")); + mStat->rxFail++; + } + + iv->setQueuedCmdFinished(); + } + } + yield(); + } + } + + private: + void notify(uint8_t val) { + if(NULL != mCbPayload) + (mCbPayload)(val); + } + + void notify(uint16_t code, uint32_t start, uint32_t endTime) { + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, endTime); + } + + bool build(uint8_t id, bool *complete, uint8_t *fragments) { + DPRINTLN(DBG_VERBOSE, F("build")); + uint16_t crc = 0xffff, crcRcv = 0x0000; + if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + + // check if all fragments are there + *complete = true; + *fragments = 0; + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if(mPayload[id].len[i] == 0) { + *complete = false; + } else { + (*fragments)++; + } + } + if(!*complete) + return false; + + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if (mPayload[id].len[i] > 0) { + if (i == (mPayload[id].maxPackId - 1)) { + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); + crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); + } else + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); + } + yield(); + } + + return (crc == crcRcv) ? true : false; + } + + void reset(uint8_t id) { +#ifdef undef + DPRINT_IVID(DBG_INFO, id); + DBGPRINTLN(F("resetPayload")); +#endif + 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 = true; // design: dont start with complete retransmit + mPayload[id].lastFragments = 0; // for send channel quality measurement + } + + IApp *mApp; + HMSYSTEM *mSys; + statistics_t *mStat; + uint8_t mMaxRetrans; + uint32_t *mTimestamp; + invPayload_t mPayload[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 29d8ae0d6..3ace71ce2 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -1,379 +1,470 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __RADIO_H__ -#define __RADIO_H__ - -#include "../utils/dbg.h" -#include -#include "../utils/crc.h" -#include "../config/config.h" -#include "SPI.h" - -#define SPI_SPEED 1000000 - -#define RF_CHANNELS 5 - -#define TX_REQ_INFO 0x15 -#define TX_REQ_DEVCONTROL 0x51 -#define ALL_FRAMES 0x80 -#define SINGLE_FRAME 0x81 - -const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; - - -//----------------------------------------------------------------------------- -// MACROS -//----------------------------------------------------------------------------- -#define CP_U32_LittleEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[0] = ((v >> 24) & 0xff); \ - b[1] = ((v >> 16) & 0xff); \ - b[2] = ((v >> 8) & 0xff); \ - b[3] = ((v ) & 0xff); \ -}) - -#define CP_U32_BigEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[3] = ((v >> 24) & 0xff); \ - b[2] = ((v >> 16) & 0xff); \ - b[1] = ((v >> 8) & 0xff); \ - b[0] = ((v ) & 0xff); \ -}) - -#define BIT_CNT(x) ((x)<<3) - -//----------------------------------------------------------------------------- -// HM Radio class -//----------------------------------------------------------------------------- -template -class HmRadio { - public: - HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { - if(mSerialDebug) { - DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); - DBGPRINT(String(CE_PIN)); - DBGPRINT(F(", CS_PIN: ")); - DBGPRINT(String(CS_PIN)); - DBGPRINT(F(", SPI_SPEED: ")); - DBGPRINT(String(SPI_SPEED)); - DBGPRINTLN(F(")")); - } - - // 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 - - mSendCnt = 0; - mRetransmits = 0; - - 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) { - DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); - pinMode(irq, INPUT_PULLUP); - - uint32_t dtuSn = 0x87654321; - uint32_t chipID = 0; // will be filled with last 3 bytes of MAC - #ifdef ESP32 - uint64_t MAC = ESP.getEfuseMac(); - chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); - #else - chipID = ESP.getChipId(); - #endif - if(chipID) { - dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal - for(int i = 0; i < 7; i++) { - dtuSn |= (chipID % 10) << (i * 4); - chipID /= 10; - } - } - // change the byte order of the DTU serial number and append the required 0x01 at the end - DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; - - #ifdef ESP32 - #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 - mSpi = new SPIClass(FSPI); - #else - mSpi = new SPIClass(VSPI); - #endif - mSpi->begin(sclk, miso, mosi, cs); - #else - //the old ESP82xx cannot freely place their SPI pins - mSpi = new SPIClass(); - mSpi->begin(); - #endif - mNrf24.begin(mSpi, ce, cs); - mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms - - mNrf24.setChannel(mRfChLst[mRxChIdx]); - mNrf24.startListening(); - mNrf24.setDataRate(RF24_250KBPS); - mNrf24.setAutoAck(true); - mNrf24.enableDynamicPayloads(); - mNrf24.setCRCLength(RF24_CRC_16); - mNrf24.setAddressWidth(5); - mNrf24.openReadingPipe(1, reinterpret_cast(&DTU_RADIO_ID)); - - // 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); - - if(mNrf24.isChipConnected()) { - DPRINTLN(DBG_INFO, F("Radio Config:")); - mNrf24.printPrettyDetails(); - } - else - DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); - } - - bool loop(void) { - if (!mIrqRcvd) - return false; // 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(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 (mIrqRcvd) { - mIrqRcvd = false; - if (getReceived()) { // everything received - return true; - } - } - yield(); - } - // switch to next RX channel - startMicros = micros(); - if(++mRxChIdx >= RF_CHANNELS) - mRxChIdx = 0; - mNrf24.setChannel(mRfChLst[mRxChIdx]); - yield(); - } - // not finished but time is over - return true; - } - - void handleIntr(void) { - mIrqRcvd = true; - } - - bool isChipConnected(void) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); - return mNrf24.isChipConnected(); - } - void enableDebug() { - mSerialDebug = true; - } - - void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { - DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); - DBGHEXLN(cmd); - initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); - uint8_t cnt = 10; - if (isNoMI) { - mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor - mTxBuf[cnt++] = 0x00; - if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet - mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit - mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit - mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings - mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling - } - } else { //MI 2nd gen. specific - switch (cmd) { - case TurnOn: - //mTxBuf[0] = 0x50; - mTxBuf[9] = 0x55; - mTxBuf[10] = 0xaa; - break; - case TurnOff: - mTxBuf[9] = 0xaa; - mTxBuf[10] = 0x55; - break; - case ActivePowerContr: - cnt++; - mTxBuf[9] = 0x5a; - mTxBuf[10] = 0x5a; - mTxBuf[11] = data[0]; // power limit - break; - default: - return; - } - cnt++; - } - sendPacket(invId, cnt, isRetransmit, isNoMI); - } - - void prepareDevInformCmd(uint64_t invId, 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(mSerialDebug) { - DPRINT(DBG_DEBUG, F("prepareDevInformCmd 0x")); - DPRINTLN(DBG_DEBUG,String(cmd, HEX)); - } - initPacket(invId, reqfld, ALL_FRAMES); - mTxBuf[10] = cmd; // cid - mTxBuf[11] = 0x00; - CP_U32_LittleEndian(&mTxBuf[12], ts); - if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { - mTxBuf[18] = (alarmMesId >> 8) & 0xff; - mTxBuf[19] = (alarmMesId ) & 0xff; - } - sendPacket(invId, 24, isRetransmit, true); - } - - void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) { - initPacket(invId, mid, pid); - sendPacket(invId, 10, isRetransmit, appendCrc16); - } - - void dumpBuf(uint8_t buf[], uint8_t len) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); - for(uint8_t i = 0; i < len; i++) { - DHEX(buf[i]); - DBGPRINT(" "); - } - DBGPRINTLN(""); - } - - uint8_t getDataRate(void) { - if(!mNrf24.isChipConnected()) - return 3; // unkown - return mNrf24.getDataRate(); - } - - bool isPVariant(void) { - return mNrf24.isPVariant(); - } - - std::queue mBufCtrl; - - uint32_t mSendCnt; - uint32_t mRetransmits; - - bool mSerialDebug; - - private: - bool getReceived(void) { - bool tx_ok, tx_fail, rx_ready; - mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH - - bool isLastPackage = false; - while(mNrf24.available()) { - uint8_t len; - len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed - if (len > 0) { - packet_t p; - p.ch = mRfChLst[mRxChIdx]; - p.len = len; - mNrf24.read(p.packet, len); - if (p.packet[0] != 0x00) { - mBufCtrl.push(p); - if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command - isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received - else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command - isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received - else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 && - isLastPackage = true; // response from dev control command - } - } - yield(); - } - return isLastPackage; - } - - void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { -#ifdef undef - if(mSerialDebug) { - DPRINT(DBG_VERBOSE, F("initPacket, mid: ")); - DHEX(mid); - DBGPRINT(F(" pid: ")); - DBGHEXLN(pid); - } -#endif - memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); - mTxBuf[0] = mid; // message id - CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); - CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); - mTxBuf[9] = pid; - } - - void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool appendCrc16=true) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); - //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); - - // append crc's - if (appendCrc16 && (len > 10)) { - // crc control data - uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); - mTxBuf[len++] = (crc >> 8) & 0xff; - mTxBuf[len++] = (crc ) & 0xff; - } - // crc over all - mTxBuf[len] = ah::crc8(mTxBuf, len); - len++; - - // set TX and RX channels - mTxChIdx = (mTxChIdx + 1) % RF_CHANNELS; - mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; - - if(mSerialDebug) { - DPRINT(DBG_INFO, F("TX ")); - DBGPRINT(String(len)); -#ifdef undef - DBGPRINT("B Ch"); - DBGPRINT(String(mRfChLst[mTxChIdx])); - DBGPRINT(F(" | ")); - dumpBuf(mTxBuf, len); -#else - DBGPRINTLN (" Bytes"); -#endif - } - - mNrf24.stopListening(); - mNrf24.setChannel(mRfChLst[mTxChIdx]); - mNrf24.openWritingPipe(reinterpret_cast(&invId)); - mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response - - if(isRetransmit) - mRetransmits++; - else - mSendCnt++; - } - - volatile bool mIrqRcvd; - uint64_t DTU_RADIO_ID; - - uint8_t mRfChLst[RF_CHANNELS]; - uint8_t mTxChIdx; - uint8_t mRxChIdx; - - SPIClass* mSpi; - RF24 mNrf24; - uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; -}; - -#endif /*__RADIO_H__*/ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __RADIO_H__ +#define __RADIO_H__ + +#include "../utils/dbg.h" +#include +#include "../utils/crc.h" +#include "../config/config.h" +#include "SPI.h" + +#define SPI_SPEED 1000000 + +#define RF_CHANNELS 5 + +#define TX_REQ_INFO 0x15 +#define TX_REQ_DEVCONTROL 0x51 +#define ALL_FRAMES 0x80 +#define SINGLE_FRAME 0x81 + +#define SEND_CHANNEL_QUALITY_INTEGRATOR_SIZE 4 +#define SEND_CHANNEL_MAX_QUALITY 4 +#define SEND_CHANNEL_MIN_QUALITY -6 +#define SEND_CHANNEL_QUALITY_GOOD 2 +#define SEND_CHANNEL_QUALITY_OK 1 +#define SEND_CHANNEL_QUALITY_NEUTRAL 0 +#define SEND_CHANNEL_QUALITY_LOW -1 +#define SEND_CHANNEL_QUALITY_BAD -2 + +const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; + + +//----------------------------------------------------------------------------- +// MACROS +//----------------------------------------------------------------------------- +#define CP_U32_LittleEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[0] = ((v >> 24) & 0xff); \ + b[1] = ((v >> 16) & 0xff); \ + b[2] = ((v >> 8) & 0xff); \ + b[3] = ((v ) & 0xff); \ +}) + +#define CP_U32_BigEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[3] = ((v >> 24) & 0xff); \ + b[2] = ((v >> 16) & 0xff); \ + b[1] = ((v >> 8) & 0xff); \ + b[0] = ((v ) & 0xff); \ +}) + +#define BIT_CNT(x) ((x)<<3) + +//----------------------------------------------------------------------------- +// HM Radio class +//----------------------------------------------------------------------------- +template +class HmRadio { + public: + HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { + if(mSerialDebug) { + DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); + DBGPRINT(String(CE_PIN)); + DBGPRINT(F(", CS_PIN: ")); + DBGPRINT(String(CS_PIN)); + DBGPRINT(F(", SPI_SPEED: ")); + DBGPRINT(String(SPI_SPEED)); + DBGPRINTLN(F(")")); + } + + // 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 + + mSendCnt = 0; + mRetransmits = 0; + + 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) { + DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); + pinMode(irq, INPUT_PULLUP); + + uint32_t dtuSn = 0x87654321; + uint32_t chipID = 0; // will be filled with last 3 bytes of MAC + #ifdef ESP32 + uint64_t MAC = ESP.getEfuseMac(); + chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); + #else + chipID = ESP.getChipId(); + #endif + if(chipID) { + dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal + for(int i = 0; i < 7; i++) { + dtuSn |= (chipID % 10) << (i * 4); + chipID /= 10; + } + } + // change the byte order of the DTU serial number and append the required 0x01 at the end + DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; + + #ifdef ESP32 + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + mSpi = new SPIClass(FSPI); + #else + mSpi = new SPIClass(VSPI); + #endif + mSpi->begin(sclk, miso, mosi, cs); + #else + //the old ESP82xx cannot freely place their SPI pins + mSpi = new SPIClass(); + mSpi->begin(); + #endif + mNrf24.begin(mSpi, ce, cs); + mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms + + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); + mNrf24.setDataRate(RF24_250KBPS); + mNrf24.setAutoAck(true); + mNrf24.enableDynamicPayloads(); + mNrf24.setCRCLength(RF24_CRC_16); + mNrf24.setAddressWidth(5); + mNrf24.openReadingPipe(1, reinterpret_cast(&DTU_RADIO_ID)); + + // 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); + + if(mNrf24.isChipConnected()) { + DPRINTLN(DBG_INFO, F("Radio Config:")); + mNrf24.printPrettyDetails(); + } + else + DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); + } + + bool loop(void) { + if (!mIrqRcvd) + return false; // 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(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 (mIrqRcvd) { + mIrqRcvd = false; + if (getReceived()) { // everything received + return true; + } + } + yield(); + } + // switch to next RX channel + startMicros = micros(); + if(++mRxChIdx >= RF_CHANNELS) + mRxChIdx = 0; + mNrf24.setChannel(mRfChLst[mRxChIdx]); + yield(); + } + // not finished but time is over + return true; + } + + void handleIntr(void) { + mIrqRcvd = true; + } + + bool isChipConnected(void) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); + return mNrf24.isChipConnected(); + } + void enableDebug() { + mSerialDebug = true; + } + + void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { + DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + DBGHEXLN(cmd); + initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); + uint8_t cnt = 10; + if (isNoMI) { + mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor + mTxBuf[cnt++] = 0x00; + if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet + mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit + mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit + mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + } + } else { //MI 2nd gen. specific + switch (cmd) { + case TurnOn: + //mTxBuf[0] = 0x50; + mTxBuf[9] = 0x55; + mTxBuf[10] = 0xaa; + break; + case TurnOff: + mTxBuf[9] = 0xaa; + mTxBuf[10] = 0x55; + break; + case ActivePowerContr: + cnt++; + mTxBuf[9] = 0x5a; + mTxBuf[10] = 0x5a; + mTxBuf[11] = data[0]; // power limit + break; + default: + return; + } + cnt++; + } + sendPacket(invId, cnt, isRetransmit, isNoMI); + } + + void prepareDevInformCmd(uint64_t invId, 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(mSerialDebug) { + DPRINT(DBG_DEBUG, F("prepareDevInformCmd 0x")); + DPRINTLN(DBG_DEBUG,String(cmd, HEX)); + } + initPacket(invId, reqfld, ALL_FRAMES); + mTxBuf[10] = cmd; // cid + mTxBuf[11] = 0x00; + CP_U32_LittleEndian(&mTxBuf[12], ts); + if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { + mTxBuf[18] = (alarmMesId >> 8) & 0xff; + mTxBuf[19] = (alarmMesId ) & 0xff; + } + sendPacket(invId, 24, isRetransmit, true); + } + + void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) { + initPacket(invId, mid, pid); + sendPacket(invId, 10, isRetransmit, appendCrc16); + } + + void dumpBuf(uint8_t buf[], uint8_t len) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); + for(uint8_t i = 0; i < len; i++) { + DHEX(buf[i]); + DBGPRINT(" "); + } + DBGPRINTLN(""); + } + + uint8_t getDataRate(void) { + if(!mNrf24.isChipConnected()) + return 3; // unkown + return mNrf24.getDataRate(); + } + + bool isPVariant(void) { + return mNrf24.isPVariant(); + } + + bool isNewSendChannel () + { + return mTxChIdx != mTxLastChIdx; + } + + uint8_t getNextSendChannelIndex (void) + { + // start with the next index: round robbin in case of same max bad quality for all channels + uint8_t bestIndex = (mTxChIdx + 1) % RF_CHANNELS; + uint8_t curIndex = (bestIndex + 1) % RF_CHANNELS; + uint16_t i; + + for (i=1; i mChQuality[bestIndex]) { + bestIndex = curIndex; + } + curIndex = (curIndex + 1) % RF_CHANNELS; + } + return bestIndex; + } + + void addSendChannelQuality (int8_t quality) + { + // continous averaging + // assume: mTxChIdx is still the last send channel index used + quality = mChQuality[mTxChIdx] + quality; + if (quality < SEND_CHANNEL_MIN_QUALITY) { + quality = SEND_CHANNEL_MIN_QUALITY; + } else if (quality > SEND_CHANNEL_MAX_QUALITY) { + quality = SEND_CHANNEL_MAX_QUALITY; + } + mChQuality[mTxChIdx] = quality; + } + + void evalSendChannelQuality (bool crcPass, uint8_t Retransmits, uint8_t rxFragments, + uint8_t lastRxFragments) + { + if (lastRxFragments == rxFragments) { + // nothing received: send probably lost + if (!Retransmits || isNewSendChannel()) { + // dont overestimate burst distortion + addSendChannelQuality (SEND_CHANNEL_QUALITY_BAD); + } + } else if (!lastRxFragments && crcPass) { + if (!Retransmits || isNewSendChannel()) { + // every fragment received successfull immediately + addSendChannelQuality (SEND_CHANNEL_QUALITY_GOOD); + } else { + // every fragment received successfully + addSendChannelQuality (SEND_CHANNEL_QUALITY_OK); + } + } else if (crcPass) { + if (isNewSendChannel ()) { + // last Fragment successfully received on new send channel + addSendChannelQuality (SEND_CHANNEL_QUALITY_OK); + } + } else if (!Retransmits || isNewSendChannel()) { + // no complete receive for this send channel + addSendChannelQuality (SEND_CHANNEL_QUALITY_LOW); + } + } + + void resetSendChannelQuality () + { + for(uint8_t i = 0; i < RF_CHANNELS; i++) { + mChQuality[mTxChIdx] = 0; + } + } + + void dumpSendQuality() + { + for(uint8_t i = 0; i < RF_CHANNELS; i++) { + DBGPRINT(" " + String (mChQuality[i])); + } + } + + std::queue mBufCtrl; + + uint32_t mSendCnt; + uint32_t mRetransmits; + + bool mSerialDebug; + + private: + bool getReceived(void) { + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + + bool isLastPackage = false; + while(mNrf24.available()) { + uint8_t len; + len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed + if (len > 0) { + packet_t p; + p.ch = mRfChLst[mRxChIdx]; + p.len = len; + mNrf24.read(p.packet, len); + if (p.packet[0] != 0x00) { + mBufCtrl.push(p); + if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command + isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received + else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command + isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received + else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 && + isLastPackage = true; // response from dev control command + } + } + yield(); + } + return isLastPackage; + } + + void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { +#ifdef undef + if(mSerialDebug) { + DPRINT(DBG_VERBOSE, F("initPacket, mid: ")); + DHEX(mid); + DBGPRINT(F(" pid: ")); + DBGHEXLN(pid); + } +#endif + memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); + mTxBuf[0] = mid; // message id + CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); + CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); + mTxBuf[9] = pid; + } + + void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool appendCrc16=true) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); + //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); + + // append crc's + if (appendCrc16 && (len > 10)) { + // crc control data + uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); + mTxBuf[len++] = (crc >> 8) & 0xff; + mTxBuf[len++] = (crc ) & 0xff; + } + // crc over all + mTxBuf[len] = ah::crc8(mTxBuf, len); + len++; + + // set TX and RX channels + + mTxLastChIdx = mTxChIdx; + mTxChIdx = getNextSendChannelIndex (); + mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; + + if(mSerialDebug) { +#ifdef undef + DPRINT(DBG_INFO, F("TX ")); + DBGPRINT(String(len)); + DBGPRINT("B Ch"); + DBGPRINT(String(mRfChLst[mTxChIdx])); + DBGPRINT(F(" | ")); + dumpBuf(mTxBuf, len); +#else + DPRINT(DBG_INFO, F("TX (Ch ") + String (mRfChLst[mTxChIdx]) + "), " + + String (len) + " Bytes, Quality:"); + dumpSendQuality(); + DBGPRINTLN(""); +#endif + } + mNrf24.stopListening(); + mNrf24.setChannel(mRfChLst[mTxChIdx]); + mNrf24.openWritingPipe(reinterpret_cast(&invId)); + mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response + + if(isRetransmit) + mRetransmits++; + else + mSendCnt++; + } + + volatile bool mIrqRcvd; + uint64_t DTU_RADIO_ID; + + uint8_t mRfChLst[RF_CHANNELS]; + int8_t mChQuality[RF_CHANNELS]; + uint8_t mTxChIdx; + uint8_t mTxLastChIdx; + uint8_t mRxChIdx; + + SPIClass* mSpi; + RF24 mNrf24; + uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; +}; + +#endif /*__RADIO_H__*/ From 9165c975e4dd6b081c69354191c609bd8132766d Mon Sep 17 00:00:00 2001 From: oberfritze <139758614+oberfritze@users.noreply.github.com> Date: Sat, 12 Aug 2023 19:21:30 +0200 Subject: [PATCH 3/4] Add files via upload small bugfix: grid power value -1 W was misinterpreted as no grid value for this interval --- src/plugins/SML_OBIS_Parser.cpp | 2121 ++++++++++++++++--------------- 1 file changed, 1061 insertions(+), 1060 deletions(-) diff --git a/src/plugins/SML_OBIS_Parser.cpp b/src/plugins/SML_OBIS_Parser.cpp index 93d11265a..ab3e1ebfb 100644 --- a/src/plugins/SML_OBIS_Parser.cpp +++ b/src/plugins/SML_OBIS_Parser.cpp @@ -1,1060 +1,1061 @@ -#include -#include -#include -#include -#include "../utils/dbg.h" -#include "../utils/scheduler.h" -#include "../config/settings.h" -#include "SML_OBIS_Parser.h" - -#ifdef AHOY_SML_OBIS_SUPPORT - -// you might use this testwise if you dont have an IR sensor connected to your AHOY-DTU -// #define SML_OBIS_TEST - -// at least the size of the largest entry that is of any interest -#define SML_MAX_SERIAL_BUF 32 - -#ifndef min -#define min(a,b) (((a) < (b)) ? (a) : (b)) -#endif - -#define SML_ESCAPE_CHAR 0x1b -#define SML_VERSION1_CHAR 0x01 -#define SML_MAX_LIST_LAYER 8 - -#define SML_EXT_LENGTH 0x80 -#define SML_OBIS_GRID_POWER_PATH AHOY_HIST_PATH "/grid_power" -#define SML_OBIS_FORMAT_FILE_NAME "%02u_%02u_%04u.bin" - -#define SML_MSG_NONE 0 -#define SML_MSG_GET_LIST_RSP 0x701 - -#define OBIS_SIG_YIELD_IN_ALL "\x01\x08\x00" -#define OBIS_SIG_YIELD_OUT_ALL "\x02\x08\x00" -#define OBIS_SIG_POWER_ALL "\x10\x07\x00" -#define OBIS_SIG_POWER_L1 "\x24\x07\x00" -#define OBIS_SIG_POWER_L2 "\x38\x07\x00" -#define OBIS_SIG_POWER_L3 "\x4c\x07\x00" -/* the folloing OBIS objects may not be transmitted by your electricity meter */ -#define OBIS_SIG_VOLTAGE_L1 "\x20\x07\x00" -#define OBIS_SIG_VOLTAGE_L2 "\x34\x07\x00" -#define OBIS_SIG_VOLTAGE_L3 "\x48\x07\x00" -#define OBIS_SIG_CURRENT_L1 "\x1f\x07\x00" -#define OBIS_SIG_CURRENT_L2 "\x33\x07\x00" -#define OBIS_SIG_CURRENT_L3 "\x47\x07\x00" - -typedef enum _sml_state { - SML_ST_FIND_START_TAG = 0, - SML_ST_FIND_VERSION, - SML_ST_FIND_MSG, - SML_ST_FIND_LIST_ENTRIES, - SML_ST_SKIP_LIST_ENTRY, - SML_ST_FIND_END_TAG, - SML_ST_CHECK_CRC -} sml_state_t; - -typedef enum _sml_list_entry_type { - SML_TYPE_OCTET_STRING = 0x00, - SML_TYPE_BOOL = 0x40, - SML_TYPE_INT = 0x50, - SML_TYPE_UINT = 0x60, - SML_TYPE_LIST = 0x70 -} sml_list_entry_type_t; - -typedef enum _obis_state { - OBIS_ST_NONE = 0, - OBIS_ST_SERIAL_NR, - OBIS_ST_YIELD_IN_ALL, - OBIS_ST_YIELD_OUT_ALL, - OBIS_ST_POWER_ALL, - OBIS_ST_POWER_L1, - OBIS_ST_POWER_L2, - OBIS_ST_POWER_L3, - OBIS_ST_VOLTAGE_L1, - OBIS_ST_VOLTAGE_L2, - OBIS_ST_VOLTAGE_L3, - OBIS_ST_CURRENT_L1, - OBIS_ST_CURRENT_L2, - OBIS_ST_CURRENT_L3, - OBIS_ST_UNKNOWN -} obis_state_t; - -static sml_state_t sml_state = SML_ST_FIND_START_TAG; -static uint16_t cur_sml_list_layer; -static unsigned char sml_list_layer_entries [SML_MAX_LIST_LAYER]; -static unsigned char sml_serial_buf[SML_MAX_SERIAL_BUF]; -static unsigned char *cur_serial_buf = sml_serial_buf; -static uint16 sml_serial_len = 0; -static uint16 sml_skip_len = 0; -static uint32 sml_message = SML_MSG_NONE; -static obis_state_t obis_state = OBIS_ST_NONE; -static int obis_power_all_scale, obis_power_all_value; -/* design: max 16 bit fuer aktuelle Powerwerte */ -static int16_t obis_cur_pac; -static uint16_t sml_telegram_crc; -static uint16_t sml_msg_crc; -static bool sml_msg_failure; -static uint16_t obis_cur_pac_cnt; -static uint16_t obis_cur_pac_index; -static int32_t obis_pac_sum; -static uint32_t *obis_timestamp; -static int obis_yield_in_all_scale, obis_yield_out_all_scale; -static uint64_t obis_yield_in_all_value, obis_yield_out_all_value; -static bool sml_trace_obis = false; -static IApp *mApp; - -const unsigned char version_seq[] = { SML_VERSION1_CHAR, SML_VERSION1_CHAR, SML_VERSION1_CHAR, SML_VERSION1_CHAR }; -const unsigned char esc_seq[] = {SML_ESCAPE_CHAR, SML_ESCAPE_CHAR, SML_ESCAPE_CHAR, SML_ESCAPE_CHAR}; - -#ifdef SML_OBIS_TEST -static size_t sml_test_telegram_offset; -const unsigned char sml_test_telegram[] = { - 0x1b, 0x1b, 0x1b, 0x1b, // Escape sequence - 0x01, 0x01, 0x01, 0x01, // Version 1 - 0x76, // List with 6 enties (1st SML message of this telegram) - 0x05, 0x03, 0x2b, 0x18, 0x20, - 0x62, 0x00, - 0x62, 0x00, - 0x72, - 0x63, 0x01, 0x01, // Message type: OpenResponse - 0x76, - 0x01, - 0x01, - 0x05, 0x01, 0x0e, 0x5d, 0x5b, - 0x0b, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, - 0x01, - 0x63, 0xea, 0xbf, // msg crc (adjust to your needs) - 0x00, - 0x76, // List with 6 entries (2. SML mesaage of this telegram) - 0x05, 0x03, 0x2b, 0x18, 0x21, - 0x62, 0x00, - 0x62, 0x00, - 0x72, - 0x63, 0x07, 0x01, // Message type: GetListResponse - 0x77, - 0x01, - 0x0b, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x07, 0x01, 0x00, 0x62, 0x0a, 0xff, 0xff, - 0x72, - 0x62, 0x01, - 0x65, 0x02, 0x1a, 0x58, 0x7f, - 0x7a, - 0x77, - 0x07, 0x01, 0x00, 0x01, 0x08, 0x00, 0xff, // OBIS: Energy in overall - no tarif - 0x65, 0x00, 0x01, 0x01, 0x80, - 0x01, - 0x62, 0x1e, // "Wh" - 0x52, 0xff, // scaler 0.1 - 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0xc3, 0x05, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x01, 0x08, 0x01, 0xff, // OBIS: Energy in - tarif 1 - 0x01, - 0x01, - 0x62, 0x1e, // "Wh" - 0x52, 0xff, // scaler 0.1 - 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0xc3, 0x05, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x01, 0x08, 0x02, 0xff, // OBIS: Energy in - tarif 2 - 0x01, - 0x01, - 0x62, 0x1e, // "Wh" - 0x52, 0xff, // scaler 0.1 - 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x02, 0x08, 0x00, 0xff, // OBIS: energy out overall - no tarif - 0x01, - 0x01, - 0x62, 0x1e, // "Wh" - 0x52, 0xff, // scaler 0.1 - 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x02, 0x08, 0x01, 0xff, // OBIS: energy out - tarif 1 - 0x01, - 0x01, - 0x62, 0x1e, // "Wh" - 0x52, 0xff, // scaler 0.1 - 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x02, 0x08, 0x02, 0xff, // OBIS: energy out - tarif 2 - 0x01, - 0x01, - 0x62, 0x1e, // "Wh" - 0x52, 0xff, // scaler 0.1 - 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x10, 0x07, 0x00, 0xff, // OBIS: power overall - 0x01, - 0x01, - 0x62, 0x1b, // "W" - 0x52, 0x00, // scaler 1 - 0x55, 0x00, 0x00, 0x00, 0x2a, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x24, 0x07, 0x00, 0xff, // OBIS: power L1 - 0x01, - 0x01, - 0x62, 0x1b, // "W" - 0x52, 0x00, // scaler 1 - 0x55, 0x00, 0x00, 0x00, 0x2a, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x38, 0x07, 0x00, 0xff, // OBIS: power L2 - 0x01, - 0x01, - 0x62, 0x1b, // "W" - 0x52, 0x00, // scaler 1 - 0x55, 0x00, 0x00, 0x00, 0x00, // value - 0x01, - 0x77, - 0x07, 0x01, 0x00, 0x4c, 0x07, 0x00, 0xff, // OBIS: power L3 - 0x01, - 0x01, - 0x62, 0x1b, // "W" - 0x52, 0x00, // scaler 1 - 0x55, 0x00, 0x00, 0x00, 0x00, // value - 0x01, - 0x01, - 0x01, - 0x63, 0x43, 0x92, // msg crc (adjust to your needs) - 0x00, - 0x76, // List with 6 entries (3rd SML message of this telegram) - 0x05, 0x03, 0x2b, 0x18, 0x22, - 0x62, 0x00, - 0x62, 0x00, - 0x72, - 0x63, 0x02, 0x01, // Message type: CloseResponse - 0x71, - 0x01, - 0x63, 0x86, 0x5b, // msg crc (adjust to your needs) - 0x00, - 0x1b, 0x1b, 0x1b, 0x1b, // Escape sequence - 0x1a, 0x00, 0xd9, 0x66 // 1a + number of fill bytes + CRC16 of telegram (change this to your needs) -}; -#endif - -//----------------------------------------------------------------------------- -// DIN EN 62056-46, Polynom 0x1021 -static const uint16_t sml_crctab[256] = { - 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, - 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, - 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, - 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, - 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, - 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, - 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, - 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, - 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, - 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, - 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, - 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, - 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, - 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, - 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, - 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, - 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, - 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, - 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, - 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, - 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, - 0x3de3, 0x2c6a, 0x1ef1, 0x0f78}; - -//----------------------------------------------------------------------------- -uint16_t sml_init_crc () -{ - return 0xffff; -} - -//----------------------------------------------------------------------------- -uint16_t sml_calc_crc (uint16_t crc, unsigned int len, unsigned char *data) -{ - while (len--) { - crc = (crc >> 8) ^ sml_crctab[(crc ^ *data++) & 0xff]; - } - return crc; -} - -//----------------------------------------------------------------------------- -uint16_t sml_finit_crc (uint16_t crc) -{ - crc ^= 0xffff; - crc = (crc << 8) | (crc >> 8); - return crc; -} - -//----------------------------------------------------------------------------- -void sml_set_trace_obis (bool trace_flag) -{ - sml_trace_obis = trace_flag; -} - -//----------------------------------------------------------------------------- -void sml_cleanup_history () -{ - time_t time_today; - - obis_cur_pac = 0; - obis_cur_pac_cnt = 0; - obis_cur_pac_index = 0; - obis_pac_sum = 0; - if ((time_today = *obis_timestamp)) { - Dir grid_power_dir; - char cur_file_name[sizeof (SML_OBIS_FORMAT_FILE_NAME)]; - - time_today = gTimezone.toLocal (time_today); - snprintf (cur_file_name, sizeof (cur_file_name), SML_OBIS_FORMAT_FILE_NAME, - day(time_today), month (time_today), year (time_today)); - grid_power_dir = LittleFS.openDir (SML_OBIS_GRID_POWER_PATH); - /* design: no dataserver, cleanup old history */ - - while (grid_power_dir.next()) { - if (grid_power_dir.fileName() != cur_file_name) { - DPRINTLN (DBG_INFO, "Remove file " + grid_power_dir.fileName() + - ", Size: " + String (grid_power_dir.fileSize())); - LittleFS.remove (SML_OBIS_GRID_POWER_PATH "/" + grid_power_dir.fileName()); - } - } - } else { - DPRINTLN (DBG_WARN, "sml_cleanup_history, no time yet"); - } -} - -//----------------------------------------------------------------------------- -File sml_open_hist () -{ - time_t time_today; - File file = (File) NULL; - - if ((time_today = *obis_timestamp)) { - char file_name[sizeof (SML_OBIS_GRID_POWER_PATH) + sizeof (SML_OBIS_FORMAT_FILE_NAME)]; - - time_today = gTimezone.toLocal(time_today); - snprintf (file_name, sizeof (file_name), SML_OBIS_GRID_POWER_PATH "/" SML_OBIS_FORMAT_FILE_NAME, - day(time_today), month(time_today), year(time_today)); - file = LittleFS.open (file_name, "r"); - if (!file) { - DPRINT (DBG_WARN, "sml_open_hist, failed to open "); - DBGPRINTLN (file_name); - } - } else { - DPRINTLN (DBG_WARN, "sml_open_history, no time yet"); - } - return file; -} - -//----------------------------------------------------------------------------- -void sml_close_hist (File file) -{ - if (file) { - file.close (); - } -} - -//----------------------------------------------------------------------------- -int sml_find_hist_power (File file, uint16_t index) -{ - if (file) { - size_t len; - uint16_t cmp_index = 0; /* init wegen Compilerwarnung */ - unsigned char data[4]; - - while ((len = file.read (data, sizeof (data))) == sizeof (data)) { - cmp_index = data[0] + (data[1] << 8); - if (cmp_index >= index) { - break; - } -// yield(); /* do not do this here: seems to cause hanger */ - } - if (len < sizeof (data)) { - if (index == obis_cur_pac_index) { - return sml_get_obis_pac_average (); - } - DPRINTLN (DBG_DEBUG, "sml_find_hist_power(1), cant find " + String (index)); - return -1; - } - if (cmp_index == index) { - return (int16_t)(data[2] + (data[3] << 8)); - } - DPRINTLN (DBG_DEBUG, "sml_find_hist_power(2), cant find " + String (index) + ", found " + String (cmp_index)); - file.seek (file.position() - sizeof (data)); - } else if ((index == obis_cur_pac_index) && obis_cur_pac_cnt) { - return sml_get_obis_pac_average (); - } - return -1; -} - -//----------------------------------------------------------------------------- -void sml_setup (IApp *app, uint32_t *timestamp) -{ - obis_timestamp = timestamp; - mApp = app; -} - -//----------------------------------------------------------------------------- -int16_t sml_get_obis_pac () -{ - return obis_cur_pac; -} - -//----------------------------------------------------------------------------- -void sml_handle_obis_state (unsigned char *buf) -{ -#ifdef undef - if (sml_trace_obis) { - DPRINTLN(DBG_INFO, "OBIS " + String(buf[0], HEX) + "-" + String(buf[1], HEX) + ":" + String(buf[2], HEX) + - "." + String (buf[3], HEX) + "." + String(buf[4], HEX) + "*" + String(buf[5], HEX)); - } -#endif - if (sml_message == SML_MSG_GET_LIST_RSP) { - if (buf[0] == 1) { - if (!memcmp (&buf[2], OBIS_SIG_YIELD_IN_ALL, 3)) { - obis_state = OBIS_ST_YIELD_IN_ALL; - } else if (!memcmp (&buf[2], OBIS_SIG_YIELD_OUT_ALL, 3)) { - obis_state = OBIS_ST_YIELD_OUT_ALL; - } else if (!memcmp (&buf[2], OBIS_SIG_POWER_ALL, 3)) { - obis_state = OBIS_ST_POWER_ALL; - } else if (!memcmp (&buf[2], OBIS_SIG_POWER_L1, 3)) { - obis_state = OBIS_ST_POWER_L1; - } else if (!memcmp (&buf[2], OBIS_SIG_POWER_L2, 3)) { - obis_state = OBIS_ST_POWER_L2; - } else if (!memcmp (&buf[2], OBIS_SIG_POWER_L3, 3)) { - obis_state = OBIS_ST_POWER_L3; - } else if (!memcmp (&buf[2], OBIS_SIG_CURRENT_L1, 3)) { - obis_state = OBIS_ST_CURRENT_L1; - } else if (!memcmp (&buf[2], OBIS_SIG_CURRENT_L2, 3)) { - obis_state = OBIS_ST_CURRENT_L2; - } else if (!memcmp (&buf[2], OBIS_SIG_CURRENT_L3, 3)) { - obis_state = OBIS_ST_CURRENT_L3; - } else if (!memcmp (&buf[2], OBIS_SIG_VOLTAGE_L1, 3)) { - obis_state = OBIS_ST_VOLTAGE_L1; - } else if (!memcmp (&buf[2], OBIS_SIG_VOLTAGE_L2, 3)) { - obis_state = OBIS_ST_VOLTAGE_L2; - } else if (!memcmp (&buf[2], OBIS_SIG_VOLTAGE_L3, 3)) { - obis_state = OBIS_ST_VOLTAGE_L3; - } else { - obis_state = OBIS_ST_UNKNOWN; - } - } else { - obis_state = OBIS_ST_UNKNOWN; - } - } -} - -//----------------------------------------------------------------------------- -int64_t sml_obis_get_uint (unsigned char *data, unsigned int len) -{ - int64_t value = 0; - - if (len > 8) { - DPRINTLN(DBG_WARN, "Int too big"); - } else { - unsigned int i; - - for (i=0; i 0) { - value = value * (10 * scale); - } else if (scale < 0) { - value = value / (10 * -scale); - } - return (int)value; -} -//----------------------------------------------------------------------------- -int64_t sml_obis_get_int (unsigned char *data, unsigned int len) -{ - int64_t value = 0; - - if (len > 8) { - DPRINTLN(DBG_WARN, "Int too big"); - } else { - unsigned int i; - - if ((len > 0) && (*data & 0x80)) { - value = -1LL; - } - for (i=0; i 0) { - value = value * (10 * scale); - } else if (scale < 0) { - value = value / (10 * -scale); - } - return (int)value; -} - -//----------------------------------------------------------------------------- -int16_t sml_get_obis_pac_average () -{ - int32_t average; - int16_t pac_average = 0; - - if (obis_cur_pac_cnt) { - if (obis_pac_sum >= 0) { - average = (obis_pac_sum + (obis_cur_pac_cnt >> 1)) / obis_cur_pac_cnt; - if (average > INT16_MAX) { - pac_average = INT16_MAX; - } else { - pac_average = average; - } - } else { - average = (obis_pac_sum - (obis_cur_pac_cnt >> 1)) / obis_cur_pac_cnt; - if (average < INT16_MIN) { - pac_average = INT16_MIN; - } else { - pac_average = average; - } - } - } - return pac_average; -} - -//----------------------------------------------------------------------------- -void sml_handle_obis_pac (int16_t pac) -{ - time_t time_today; - obis_cur_pac = pac; - - if ((time_today = *obis_timestamp)) { - uint32_t pac_index; - - time_today = gTimezone.toLocal (time_today); - - pac_index = hour(time_today) * 60 + minute(time_today); - pac_index /= AHOY_PAC_INTERVAL; - - if (pac_index != obis_cur_pac_index) { - /* calc average for last interval */ - if (obis_cur_pac_cnt) { - int16_t pac_average = sml_get_obis_pac_average(); - File file; - char file_name[sizeof (SML_OBIS_GRID_POWER_PATH) + sizeof (SML_OBIS_FORMAT_FILE_NAME)]; - - snprintf (file_name, sizeof (file_name), SML_OBIS_GRID_POWER_PATH "/" SML_OBIS_FORMAT_FILE_NAME, - day(time_today), month(time_today), year(time_today)); - // append last average - if ((file = LittleFS.open (file_name, "a"))) { - unsigned char buf[4]; - buf[0] = obis_cur_pac_index & 0xff; - buf[1] = obis_cur_pac_index >> 8; - buf[2] = pac_average & 0xff; - buf[3] = pac_average >> 8; - if (file.write (buf, sizeof (buf)) != sizeof (buf)) { - DPRINTLN (DBG_WARN, "sml_handle_obis_pac, failed_to_write"); - } else { - DPRINTLN (DBG_DEBUG, "sml_handle_obis_pac, write to " + String(file_name)); - } - file.close (); - } else { - DPRINTLN (DBG_WARN, "sml_handle_obis_pac, failed to open"); - } - obis_cur_pac_cnt = 0; - obis_pac_sum = 0; - } - obis_cur_pac_index = pac_index; - } - if ((pac_index >= AHOY_MIN_PAC_SUN_HOUR * 60 / AHOY_PAC_INTERVAL) && - (pac_index < AHOY_MAX_PAC_SUN_HOUR * 60 / AHOY_PAC_INTERVAL)) { - obis_pac_sum += pac; - obis_cur_pac_cnt++; - } else { - DPRINTLN (DBG_DEBUG, "sml_handle_obis_pac, outside daylight, minutes: " + String (pac_index * AHOY_PAC_INTERVAL)); - } - } else { - DPRINTLN (DBG_INFO, "sml_handle_obis_pac, no time2"); - } -} - -//----------------------------------------------------------------------------- -uint16_t sml_fill_buf (uint16_t len) -{ - len = min (sizeof (sml_serial_buf) - (cur_serial_buf - sml_serial_buf) - sml_serial_len, len); - -#ifdef SML_OBIS_TEST - size_t partlen; - - partlen = min (len, sizeof (sml_test_telegram) - sml_test_telegram_offset); - memcpy (cur_serial_buf + sml_serial_len, &sml_test_telegram[sml_test_telegram_offset], partlen); - sml_serial_len += partlen; - sml_test_telegram_offset += partlen; - if (sml_test_telegram_offset >= sizeof (sml_test_telegram)) { - sml_test_telegram_offset = 0; - } - if (partlen < len) { - memcpy (cur_serial_buf + sml_serial_len, &sml_test_telegram[sml_test_telegram_offset], len - partlen); - sml_serial_len += len - partlen; - sml_test_telegram_offset += len - partlen; - } -#else - if (len) { - len = Serial.readBytes(cur_serial_buf + sml_serial_len, len); - sml_serial_len += len; - } -#endif - return len; -} - -//----------------------------------------------------------------------------- -bool sml_get_list_entries (uint16_t layer) -{ - bool error = false; - - while (!error && sml_serial_len) { - sml_list_entry_type_t type = (sml_list_entry_type_t)(*cur_serial_buf & 0x70); - unsigned char entry_len; - // Acc. to Spec there might be len_info > 2. But does this happen in real life? - // Also: an info_len > 2 could be due to corrupt data. So better break. - uint16 len_info = (*cur_serial_buf & SML_EXT_LENGTH) ? 2 : 1; - -#ifdef undef - DPRINT (DBG_INFO, "get_list_entries"); - DBGPRINT (", layer " + String (layer)); - DBGPRINT (", entries " + String (sml_list_layer_entries[layer])); - DBGPRINT (", type 0x" + String (type, HEX)); - DBGPRINT (", len_info " + String (len_info)); - DBGPRINTLN (", sml_len " + String (sml_serial_len)); -#endif - - if (sml_serial_len < len_info) { - if (cur_serial_buf > sml_serial_buf) { - memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); - cur_serial_buf = sml_serial_buf; - } - error = true; - } else { - if (len_info == 2) { - entry_len = (*cur_serial_buf << 4) | (*(cur_serial_buf+1) & 0xf); - } else { - entry_len = *cur_serial_buf & 0x0f; /* bei Listen andere Bedeutung */ - } - if ((type == SML_TYPE_LIST) || (sml_serial_len >= entry_len)) { - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, len_info, cur_serial_buf); - if (layer || (sml_list_layer_entries[layer] > 2)) { - sml_msg_crc = sml_calc_crc (sml_msg_crc, len_info, cur_serial_buf); - } - - sml_serial_len -= len_info; - if (entry_len && (type != SML_TYPE_LIST)) { - entry_len -= len_info; - } - if (sml_serial_len) { - cur_serial_buf += len_info; - } else { - cur_serial_buf = sml_serial_buf; - } - if (sml_list_layer_entries[layer]) { - switch (type) { - case SML_TYPE_OCTET_STRING: - if ((layer == 4) && (entry_len == 6)) { - sml_handle_obis_state (cur_serial_buf); - } - break; - case SML_TYPE_BOOL: - break; - case SML_TYPE_INT: - if (!layer && (sml_list_layer_entries[layer] == 2)) { - // perhaps there is a creepy smart meter that does send crc as int? - uint16_t rcv_crc = sml_obis_get_int (cur_serial_buf, entry_len); - - sml_msg_crc = sml_finit_crc (sml_msg_crc); - if (rcv_crc != sml_msg_crc) { - DPRINTLN(DBG_WARN, "Wrong CRC for msg 0x" + String (sml_message, HEX) + - ", 0x" + String (sml_msg_crc, HEX) + " <-> 0x" + String (rcv_crc, HEX)); - } else { - sml_msg_failure = 0; - } - } else if (layer == 1) { - if ((sml_message == SML_MSG_NONE) && (sml_list_layer_entries[layer] == 2)) { - sml_message = sml_obis_get_int (cur_serial_buf, entry_len); - } - } else if (layer == 4) { - if (obis_state == OBIS_ST_POWER_ALL) { - if (sml_list_layer_entries[layer] == 3) { - obis_power_all_scale = (int)sml_obis_get_int (cur_serial_buf, entry_len); - } else if (sml_list_layer_entries[layer] == 2) { - obis_power_all_value = (int)sml_obis_get_int (cur_serial_buf, entry_len); - } - } else if (obis_state == OBIS_ST_YIELD_IN_ALL) { - if (sml_list_layer_entries[layer] == 3) { - obis_yield_in_all_scale = (int)sml_obis_get_int (cur_serial_buf, entry_len); - } else if (sml_list_layer_entries[layer] == 2) { - obis_yield_in_all_value = sml_obis_get_int (cur_serial_buf, entry_len); - } - } else if (obis_state == OBIS_ST_YIELD_OUT_ALL) { - if (sml_list_layer_entries[layer] == 3) { - obis_yield_out_all_scale = (int)sml_obis_get_int (cur_serial_buf, entry_len); - } else if (sml_list_layer_entries[layer] == 2) { - obis_yield_out_all_value = sml_obis_get_int (cur_serial_buf, entry_len); - } - } - } - break; - case SML_TYPE_UINT: - if (!layer && (sml_list_layer_entries[layer] == 2)) { - uint16_t rcv_crc = sml_obis_get_uint (cur_serial_buf, entry_len); - - sml_msg_crc = sml_finit_crc (sml_msg_crc); - if (rcv_crc != sml_msg_crc) { - DPRINTLN(DBG_WARN, "Wrong CRC for msg 0x" + String (sml_message, HEX) + - ", 0x" + String (sml_msg_crc, HEX) + " <-> 0x" + String (rcv_crc, HEX)); - } else { - sml_msg_failure = 0; - } - } else if (layer == 1) { - if ((sml_message == SML_MSG_NONE) && (sml_list_layer_entries[layer] == 2)) { - sml_message = sml_obis_get_uint (cur_serial_buf, entry_len); - } - } else if (layer == 4) { - if (obis_state == OBIS_ST_YIELD_IN_ALL) { - if (sml_list_layer_entries[layer] == 2) { - obis_yield_in_all_value = sml_obis_get_uint (cur_serial_buf, entry_len); - } - } else if (obis_state == OBIS_ST_YIELD_OUT_ALL) { - if (sml_list_layer_entries[layer] == 2) { - obis_yield_out_all_value = sml_obis_get_uint (cur_serial_buf, entry_len); - } - } - } - break; - case SML_TYPE_LIST: - if (layer + 1 < SML_MAX_LIST_LAYER) { - sml_list_layer_entries[layer]--; - layer++; - cur_sml_list_layer = layer; - sml_list_layer_entries[layer] = entry_len; -#ifdef undef - DPRINTLN(DBG_INFO, "Open layer " + String(layer) + ", entries " + String(entry_len)); -#endif - if (!sml_serial_len) { - error = true; - } - } else { - sml_state = SML_ST_FIND_START_TAG; - return sml_serial_len ? false : true; - } - break; - default: - DPRINT(DBG_WARN, "Ill Element 0x" + String(type, HEX)); - DBGPRINTLN(", len " + String (entry_len + len_info)); - /* design: aussteigen */ - sml_state = SML_ST_FIND_START_TAG; - return sml_serial_len ? false : true; - } - if (type != SML_TYPE_LIST) { - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, entry_len, cur_serial_buf); - if (layer || (sml_list_layer_entries[layer] > 2)) { - sml_msg_crc = sml_calc_crc (sml_msg_crc, entry_len, cur_serial_buf); - } - sml_serial_len -= entry_len; - if (sml_serial_len) { - cur_serial_buf += entry_len; - } else { - cur_serial_buf = sml_serial_buf; - error = true; - } - sml_list_layer_entries[layer]--; - } - } - while (!sml_list_layer_entries[layer]) { -#ifdef undef - DPRINTLN(DBG_INFO, "COMPLETE, layer " + String (layer)); -#endif - if (layer) { - layer--; - cur_sml_list_layer = layer; - } else { - sml_state = SML_ST_FIND_MSG; - return sml_serial_len ? false : true; - } - } - } else if (entry_len > sizeof (sml_serial_buf)) { - DPRINTLN (DBG_INFO, "skip " + String (entry_len)); - sml_skip_len = entry_len; - sml_state = SML_ST_SKIP_LIST_ENTRY; - return false; - } else { - if (cur_serial_buf > sml_serial_buf) { - memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); - cur_serial_buf = sml_serial_buf; - } - error = true; - } - } - } - return error; -} - -//----------------------------------------------------------------------------- -uint16_t sml_parse_stream (uint16 len) -{ - bool parse_continue; - uint16_t serial_read; - - serial_read = sml_fill_buf (len); - - do { - parse_continue = false; - switch (sml_state) { - case SML_ST_FIND_START_TAG: - if (sml_serial_len >= sizeof (esc_seq)) { - unsigned char *last_serial_buf = cur_serial_buf; - - if ((cur_serial_buf = (unsigned char *)memmem (cur_serial_buf, sml_serial_len, esc_seq, sizeof (esc_seq)))) { - sml_telegram_crc = sml_init_crc (); - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, sizeof (esc_seq), cur_serial_buf); - sml_serial_len -= cur_serial_buf - last_serial_buf; - sml_serial_len -= sizeof (esc_seq); - if (sml_serial_len) { - cur_serial_buf += sizeof (esc_seq); - parse_continue = true; - } else { - cur_serial_buf = sml_serial_buf; - } - sml_state = SML_ST_FIND_VERSION; -#ifdef undef - DPRINTLN(DBG_INFO, "START_TAG, rest " + String(sml_serial_len)); -#endif - } else { - cur_serial_buf = last_serial_buf + sml_serial_len; - last_serial_buf = cur_serial_buf; - - /* handle up to last 3 esc chars */ - while ((*(cur_serial_buf - 1) == SML_ESCAPE_CHAR)) { - cur_serial_buf--; - } - if ((sml_serial_len = last_serial_buf - cur_serial_buf)) { - memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); - } - cur_serial_buf = sml_serial_buf; - } - } else if (cur_serial_buf > sml_serial_buf) { - memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); - cur_serial_buf = sml_serial_buf; - } - break; - case SML_ST_FIND_VERSION: - if (sml_serial_len >=sizeof (version_seq)) { - if (!memcmp (cur_serial_buf, version_seq, sizeof (version_seq))) { - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, sizeof (version_seq), cur_serial_buf); - sml_msg_failure = 0; - sml_state = SML_ST_FIND_MSG; -#ifdef undef - DPRINTLN(DBG_INFO, "VERSION, rest " + String (sml_serial_len - sizeof (version_seq))); -#endif - } else { -#ifdef undef - DPRINTLN(DBG_INFO, "no VERSION, rest " + String (sml_serial_len - sizeof (version_seq))); -#endif - sml_state = SML_ST_FIND_START_TAG; - } - sml_serial_len -= sizeof (version_seq); - if (sml_serial_len) { - cur_serial_buf += sizeof (version_seq); - parse_continue = true; - } else { - cur_serial_buf = sml_serial_buf; - } - } else if (cur_serial_buf > sml_serial_buf) { - memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); - cur_serial_buf = sml_serial_buf; - } - break; - case SML_ST_FIND_MSG: - if (sml_msg_failure) { - sml_state = SML_ST_FIND_START_TAG; - parse_continue = sml_serial_len ? true : false; - } else if (sml_serial_len) { - if (*cur_serial_buf == 0x1b) { - sml_state = SML_ST_FIND_END_TAG; - parse_continue = true; - } else if ((*cur_serial_buf & 0x70) == 0x70) { - /* todo: extended list on 1st level (does this happen in real life?) */ - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, 1, cur_serial_buf); -#ifdef undef - DPRINTLN (DBG_INFO, "TOPLIST 0x" + String(*cur_serial_buf, HEX) + ", rest " + String (sml_serial_len - 1)); -#endif - sml_state = SML_ST_FIND_LIST_ENTRIES; - cur_sml_list_layer = 0; - sml_message = SML_MSG_NONE; - sml_msg_failure = 1; // assume corrupt data (crc of this msg must proof ok) - sml_msg_crc = sml_init_crc (); - sml_msg_crc = sml_calc_crc (sml_msg_crc, 1, cur_serial_buf); - obis_state = OBIS_ST_NONE; - sml_list_layer_entries[0] = *cur_serial_buf & 0xf; - sml_serial_len--; - if (sml_serial_len) { - cur_serial_buf++; - parse_continue = true; - } else { - cur_serial_buf = sml_serial_buf; - } - } else if (*cur_serial_buf == 0x00) { - /* fill byte (depends on the size of the telegram) */ - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, 1, cur_serial_buf); - sml_serial_len--; - if (sml_serial_len) { - cur_serial_buf++; - parse_continue = true; - } else { - cur_serial_buf = sml_serial_buf; - } - } else { - DPRINTLN(DBG_WARN, "Unexpected 0x" + String(*cur_serial_buf, HEX) + ", rest: " + String (sml_serial_len)); - sml_state = SML_ST_FIND_START_TAG; - parse_continue = true; - } - } - break; - case SML_ST_FIND_LIST_ENTRIES: - parse_continue = !sml_get_list_entries (cur_sml_list_layer); - break; - case SML_ST_SKIP_LIST_ENTRY: - if (sml_serial_len) { /* design: keep rcv buf small and skip irrelevant long list entries */ - size_t len = min (sml_serial_len, sml_skip_len); - - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, len, cur_serial_buf); - if (cur_sml_list_layer || (sml_list_layer_entries[cur_sml_list_layer] > 2)) { - sml_msg_crc = sml_calc_crc (sml_msg_crc, len, cur_serial_buf); - } - sml_serial_len -= len; - if (sml_serial_len) { - cur_serial_buf += len; - parse_continue = true; - } else { - cur_serial_buf = sml_serial_buf; - } - sml_skip_len -= len; - if (!sml_skip_len) { - sml_state = SML_ST_FIND_LIST_ENTRIES; - sml_list_layer_entries[cur_sml_list_layer]--; - while (!sml_list_layer_entries[cur_sml_list_layer]) { -#ifdef undef - DPRINTLN(DBG_INFO, "COMPLETE, layer " + String (cur_sml_list_layer)); -#endif - if (cur_sml_list_layer) { - cur_sml_list_layer--; - } else { - sml_state = SML_ST_FIND_MSG; - break; - } - } - } - } - break; - case SML_ST_FIND_END_TAG: - if (sml_serial_len >= sizeof (esc_seq)) { - if (!memcmp (cur_serial_buf, esc_seq, sizeof (esc_seq))) { - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, sizeof (esc_seq), cur_serial_buf); - sml_state = SML_ST_CHECK_CRC; - } else { - DPRINTLN(DBG_WARN, "Missing END_TAG, found 0x" + String (*cur_serial_buf) + - " 0x" + String (*(cur_serial_buf+1)) + - " 0x" + String (*(cur_serial_buf+2)) + - " 0x" + String (*(cur_serial_buf+3))); - sml_state = SML_ST_FIND_START_TAG; - } - sml_serial_len -= sizeof (esc_seq); - if (sml_serial_len) { - cur_serial_buf += sizeof (esc_seq); - parse_continue = true; - } else { - cur_serial_buf = sml_serial_buf; - } - } else if (cur_serial_buf > sml_serial_buf) { - memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); - cur_serial_buf = sml_serial_buf; - } - break; - case SML_ST_CHECK_CRC: - if (sml_serial_len >= 4) { - if (*cur_serial_buf == 0x1a) { - uint16_t calc_crc16, rcv_crc16; - - sml_telegram_crc = sml_calc_crc (sml_telegram_crc, 2, cur_serial_buf); - calc_crc16 = sml_finit_crc (sml_telegram_crc); - rcv_crc16 = (*(cur_serial_buf+2) << 8) + *(cur_serial_buf+3); - if (calc_crc16 == rcv_crc16) { - obis_power_all_value = sml_obis_scale_int (obis_power_all_value, obis_power_all_scale); -#ifdef undef - // a bit more verbose info - obis_yield_in_all_value = sml_obis_scale_uint (obis_yield_in_all_value, obis_yield_in_all_scale); - obis_yield_out_all_value = sml_obis_scale_uint (obis_yield_out_all_value, obis_yield_out_all_scale); - DPRINTLN(DBG_INFO, "Power " + String (obis_power_all_value) + - ", Yield in " + String (obis_yield_in_all_value) + - ", Yield out " + String (obis_yield_out_all_value)); -#else - DPRINTLN(DBG_INFO, "Power " + String (obis_power_all_value)); -#endif - sml_handle_obis_pac (obis_power_all_value); - } else { - DPRINTLN(DBG_WARN, "CRC ERROR 0x" + String (calc_crc16, HEX) + " <-> 0x" + String (rcv_crc16, HEX)); - } - } - sml_state = SML_ST_FIND_START_TAG; - sml_serial_len -= 4; - if (sml_serial_len) { - cur_serial_buf += 4; - parse_continue = true; - } else { - cur_serial_buf = sml_serial_buf; - } - } else if (cur_serial_buf > sml_serial_buf) { - memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); - cur_serial_buf = sml_serial_buf; - } - break; - } - } while (parse_continue); - return serial_read; -} - -//----------------------------------------------------------------------------- -void sml_loop () -{ - uint16_t serial_avail; - uint16_t serial_read = 0; - -#ifdef SML_OBIS_TEST - uint32_t cur_uptime; - static uint32_t last_uptime; - if (((cur_uptime = mApp->getUptime()) > 30) && (cur_uptime != last_uptime)) { - last_uptime = cur_uptime; - serial_avail = sizeof (sml_test_telegram) >> 2; - } else { - serial_avail = 0; - } -#else - serial_avail = Serial.available(); -#endif - if (serial_avail > 0) { - do { - serial_read = sml_parse_stream (serial_avail); - serial_avail -= serial_read; - // yield(); /* unconditionally called this might have a bad effect for TX Retransmit to the Inverter via NRF24L01+ (not quite sure) */ - } while (serial_read && serial_avail); - } -} - -#endif +#include +#include +#include +#include +#include "../utils/dbg.h" +#include "../utils/scheduler.h" +#include "../config/settings.h" +#include "SML_OBIS_Parser.h" + +#ifdef AHOY_SML_OBIS_SUPPORT + +// you might use this testwise if you dont have an IR sensor connected to your AHOY-DTU +// #define SML_OBIS_TEST + +// at least the size of the largest entry that is of any interest +#define SML_MAX_SERIAL_BUF 32 + +#ifndef min +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#endif + + +#define SML_ESCAPE_CHAR 0x1b +#define SML_VERSION1_CHAR 0x01 +#define SML_MAX_LIST_LAYER 8 + +#define SML_EXT_LENGTH 0x80 +#define SML_OBIS_GRID_POWER_PATH AHOY_HIST_PATH "/grid_power" +#define SML_OBIS_FORMAT_FILE_NAME "%02u_%02u_%04u.bin" + +#define SML_MSG_NONE 0 +#define SML_MSG_GET_LIST_RSP 0x701 + +#define OBIS_SIG_YIELD_IN_ALL "\x01\x08\x00" +#define OBIS_SIG_YIELD_OUT_ALL "\x02\x08\x00" +#define OBIS_SIG_POWER_ALL "\x10\x07\x00" +#define OBIS_SIG_POWER_L1 "\x24\x07\x00" +#define OBIS_SIG_POWER_L2 "\x38\x07\x00" +#define OBIS_SIG_POWER_L3 "\x4c\x07\x00" +/* the folloing OBIS objects may not be transmitted by your electricity meter */ +#define OBIS_SIG_VOLTAGE_L1 "\x20\x07\x00" +#define OBIS_SIG_VOLTAGE_L2 "\x34\x07\x00" +#define OBIS_SIG_VOLTAGE_L3 "\x48\x07\x00" +#define OBIS_SIG_CURRENT_L1 "\x1f\x07\x00" +#define OBIS_SIG_CURRENT_L2 "\x33\x07\x00" +#define OBIS_SIG_CURRENT_L3 "\x47\x07\x00" + +typedef enum _sml_state { + SML_ST_FIND_START_TAG = 0, + SML_ST_FIND_VERSION, + SML_ST_FIND_MSG, + SML_ST_FIND_LIST_ENTRIES, + SML_ST_SKIP_LIST_ENTRY, + SML_ST_FIND_END_TAG, + SML_ST_CHECK_CRC +} sml_state_t; + +typedef enum _sml_list_entry_type { + SML_TYPE_OCTET_STRING = 0x00, + SML_TYPE_BOOL = 0x40, + SML_TYPE_INT = 0x50, + SML_TYPE_UINT = 0x60, + SML_TYPE_LIST = 0x70 +} sml_list_entry_type_t; + +typedef enum _obis_state { + OBIS_ST_NONE = 0, + OBIS_ST_SERIAL_NR, + OBIS_ST_YIELD_IN_ALL, + OBIS_ST_YIELD_OUT_ALL, + OBIS_ST_POWER_ALL, + OBIS_ST_POWER_L1, + OBIS_ST_POWER_L2, + OBIS_ST_POWER_L3, + OBIS_ST_VOLTAGE_L1, + OBIS_ST_VOLTAGE_L2, + OBIS_ST_VOLTAGE_L3, + OBIS_ST_CURRENT_L1, + OBIS_ST_CURRENT_L2, + OBIS_ST_CURRENT_L3, + OBIS_ST_UNKNOWN +} obis_state_t; + +static sml_state_t sml_state = SML_ST_FIND_START_TAG; +static uint16_t cur_sml_list_layer; +static unsigned char sml_list_layer_entries [SML_MAX_LIST_LAYER]; +static unsigned char sml_serial_buf[SML_MAX_SERIAL_BUF]; +static unsigned char *cur_serial_buf = sml_serial_buf; +static uint16 sml_serial_len = 0; +static uint16 sml_skip_len = 0; +static uint32 sml_message = SML_MSG_NONE; +static obis_state_t obis_state = OBIS_ST_NONE; +static int obis_power_all_scale, obis_power_all_value; +/* design: max 16 bit fuer aktuelle Powerwerte */ +static int16_t obis_cur_pac; +static uint16_t sml_telegram_crc; +static uint16_t sml_msg_crc; +static bool sml_msg_failure; +static uint16_t obis_cur_pac_cnt; +static uint16_t obis_cur_pac_index; +static int32_t obis_pac_sum; +static uint32_t *obis_timestamp; +static int obis_yield_in_all_scale, obis_yield_out_all_scale; +static uint64_t obis_yield_in_all_value, obis_yield_out_all_value; +static bool sml_trace_obis = false; +static IApp *mApp; + +const unsigned char version_seq[] = { SML_VERSION1_CHAR, SML_VERSION1_CHAR, SML_VERSION1_CHAR, SML_VERSION1_CHAR }; +const unsigned char esc_seq[] = {SML_ESCAPE_CHAR, SML_ESCAPE_CHAR, SML_ESCAPE_CHAR, SML_ESCAPE_CHAR}; + +#ifdef SML_OBIS_TEST +static size_t sml_test_telegram_offset; +const unsigned char sml_test_telegram[] = { + 0x1b, 0x1b, 0x1b, 0x1b, // Escape sequence + 0x01, 0x01, 0x01, 0x01, // Version 1 + 0x76, // List with 6 enties (1st SML message of this telegram) + 0x05, 0x03, 0x2b, 0x18, 0x20, + 0x62, 0x00, + 0x62, 0x00, + 0x72, + 0x63, 0x01, 0x01, // Message type: OpenResponse + 0x76, + 0x01, + 0x01, + 0x05, 0x01, 0x0e, 0x5d, 0x5b, + 0x0b, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, + 0x01, + 0x63, 0xea, 0xbf, // msg crc (adjust to your needs) + 0x00, + 0x76, // List with 6 entries (2. SML mesaage of this telegram) + 0x05, 0x03, 0x2b, 0x18, 0x21, + 0x62, 0x00, + 0x62, 0x00, + 0x72, + 0x63, 0x07, 0x01, // Message type: GetListResponse + 0x77, + 0x01, + 0x0b, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x07, 0x01, 0x00, 0x62, 0x0a, 0xff, 0xff, + 0x72, + 0x62, 0x01, + 0x65, 0x02, 0x1a, 0x58, 0x7f, + 0x7a, + 0x77, + 0x07, 0x01, 0x00, 0x01, 0x08, 0x00, 0xff, // OBIS: Energy in overall - no tarif + 0x65, 0x00, 0x01, 0x01, 0x80, + 0x01, + 0x62, 0x1e, // "Wh" + 0x52, 0xff, // scaler 0.1 + 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0xc3, 0x05, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x01, 0x08, 0x01, 0xff, // OBIS: Energy in - tarif 1 + 0x01, + 0x01, + 0x62, 0x1e, // "Wh" + 0x52, 0xff, // scaler 0.1 + 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0xc3, 0x05, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x01, 0x08, 0x02, 0xff, // OBIS: Energy in - tarif 2 + 0x01, + 0x01, + 0x62, 0x1e, // "Wh" + 0x52, 0xff, // scaler 0.1 + 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x02, 0x08, 0x00, 0xff, // OBIS: energy out overall - no tarif + 0x01, + 0x01, + 0x62, 0x1e, // "Wh" + 0x52, 0xff, // scaler 0.1 + 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x02, 0x08, 0x01, 0xff, // OBIS: energy out - tarif 1 + 0x01, + 0x01, + 0x62, 0x1e, // "Wh" + 0x52, 0xff, // scaler 0.1 + 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x02, 0x08, 0x02, 0xff, // OBIS: energy out - tarif 2 + 0x01, + 0x01, + 0x62, 0x1e, // "Wh" + 0x52, 0xff, // scaler 0.1 + 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x10, 0x07, 0x00, 0xff, // OBIS: power overall + 0x01, + 0x01, + 0x62, 0x1b, // "W" + 0x52, 0x00, // scaler 1 + 0x55, 0x00, 0x00, 0x00, 0x2a, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x24, 0x07, 0x00, 0xff, // OBIS: power L1 + 0x01, + 0x01, + 0x62, 0x1b, // "W" + 0x52, 0x00, // scaler 1 + 0x55, 0x00, 0x00, 0x00, 0x2a, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x38, 0x07, 0x00, 0xff, // OBIS: power L2 + 0x01, + 0x01, + 0x62, 0x1b, // "W" + 0x52, 0x00, // scaler 1 + 0x55, 0x00, 0x00, 0x00, 0x00, // value + 0x01, + 0x77, + 0x07, 0x01, 0x00, 0x4c, 0x07, 0x00, 0xff, // OBIS: power L3 + 0x01, + 0x01, + 0x62, 0x1b, // "W" + 0x52, 0x00, // scaler 1 + 0x55, 0x00, 0x00, 0x00, 0x00, // value + 0x01, + 0x01, + 0x01, + 0x63, 0x43, 0x92, // msg crc (adjust to your needs) + 0x00, + 0x76, // List with 6 entries (3rd SML message of this telegram) + 0x05, 0x03, 0x2b, 0x18, 0x22, + 0x62, 0x00, + 0x62, 0x00, + 0x72, + 0x63, 0x02, 0x01, // Message type: CloseResponse + 0x71, + 0x01, + 0x63, 0x86, 0x5b, // msg crc (adjust to your needs) + 0x00, + 0x1b, 0x1b, 0x1b, 0x1b, // Escape sequence + 0x1a, 0x00, 0xd9, 0x66 // 1a + number of fill bytes + CRC16 of telegram (change this to your needs) +}; +#endif + +//----------------------------------------------------------------------------- +// DIN EN 62056-46, Polynom 0x1021 +static const uint16_t sml_crctab[256] = { + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, + 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, + 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, + 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, + 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, + 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, + 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, + 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, + 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, + 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, + 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, + 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, + 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, + 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, + 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, + 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, + 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78}; + +//----------------------------------------------------------------------------- +uint16_t sml_init_crc () +{ + return 0xffff; +} + +//----------------------------------------------------------------------------- +uint16_t sml_calc_crc (uint16_t crc, unsigned int len, unsigned char *data) +{ + while (len--) { + crc = (crc >> 8) ^ sml_crctab[(crc ^ *data++) & 0xff]; + } + return crc; +} + +//----------------------------------------------------------------------------- +uint16_t sml_finit_crc (uint16_t crc) +{ + crc ^= 0xffff; + crc = (crc << 8) | (crc >> 8); + return crc; +} + +//----------------------------------------------------------------------------- +void sml_set_trace_obis (bool trace_flag) +{ + sml_trace_obis = trace_flag; +} + +//----------------------------------------------------------------------------- +void sml_cleanup_history () +{ + time_t time_today; + + obis_cur_pac = 0; + obis_cur_pac_cnt = 0; + obis_cur_pac_index = 0; + obis_pac_sum = 0; + if ((time_today = *obis_timestamp)) { + Dir grid_power_dir; + char cur_file_name[sizeof (SML_OBIS_FORMAT_FILE_NAME)]; + + time_today = gTimezone.toLocal (time_today); + snprintf (cur_file_name, sizeof (cur_file_name), SML_OBIS_FORMAT_FILE_NAME, + day(time_today), month (time_today), year (time_today)); + grid_power_dir = LittleFS.openDir (SML_OBIS_GRID_POWER_PATH); + /* design: no dataserver, cleanup old history */ + + while (grid_power_dir.next()) { + if (grid_power_dir.fileName() != cur_file_name) { + DPRINTLN (DBG_INFO, "Remove file " + grid_power_dir.fileName() + + ", Size: " + String (grid_power_dir.fileSize())); + LittleFS.remove (SML_OBIS_GRID_POWER_PATH "/" + grid_power_dir.fileName()); + } + } + } else { + DPRINTLN (DBG_WARN, "sml_cleanup_history, no time yet"); + } +} + +//----------------------------------------------------------------------------- +File sml_open_hist () +{ + time_t time_today; + File file = (File) NULL; + + if ((time_today = *obis_timestamp)) { + char file_name[sizeof (SML_OBIS_GRID_POWER_PATH) + sizeof (SML_OBIS_FORMAT_FILE_NAME)]; + + time_today = gTimezone.toLocal(time_today); + snprintf (file_name, sizeof (file_name), SML_OBIS_GRID_POWER_PATH "/" SML_OBIS_FORMAT_FILE_NAME, + day(time_today), month(time_today), year(time_today)); + file = LittleFS.open (file_name, "r"); + if (!file) { + DPRINT (DBG_WARN, "sml_open_hist, failed to open "); + DBGPRINTLN (file_name); + } + } else { + DPRINTLN (DBG_WARN, "sml_open_history, no time yet"); + } + return file; +} + +//----------------------------------------------------------------------------- +void sml_close_hist (File file) +{ + if (file) { + file.close (); + } +} + +//----------------------------------------------------------------------------- +int sml_find_hist_power (File file, uint16_t index) +{ + if (file) { + size_t len; + uint16_t cmp_index = 0; /* init wegen Compilerwarnung */ + unsigned char data[4]; + + while ((len = file.read (data, sizeof (data))) == sizeof (data)) { + cmp_index = data[0] + (data[1] << 8); + if (cmp_index >= index) { + break; + } +// yield(); /* do not do this here: seems to cause hanger */ + } + if (len < sizeof (data)) { + if (index == obis_cur_pac_index) { + return sml_get_obis_pac_average (); + } + DPRINTLN (DBG_DEBUG, "sml_find_hist_power(1), cant find " + String (index)); + return INT32_MIN; + } + if (cmp_index == index) { + return (int16_t)(data[2] + (data[3] << 8)); + } + DPRINTLN (DBG_DEBUG, "sml_find_hist_power(2), cant find " + String (index) + ", found " + String (cmp_index)); + file.seek (file.position() - sizeof (data)); + } else if ((index == obis_cur_pac_index) && obis_cur_pac_cnt) { + return sml_get_obis_pac_average (); + } + return INT32_MIN; +} + +//----------------------------------------------------------------------------- +void sml_setup (IApp *app, uint32_t *timestamp) +{ + obis_timestamp = timestamp; + mApp = app; +} + +//----------------------------------------------------------------------------- +int16_t sml_get_obis_pac () +{ + return obis_cur_pac; +} + +//----------------------------------------------------------------------------- +void sml_handle_obis_state (unsigned char *buf) +{ +#ifdef undef + if (sml_trace_obis) { + DPRINTLN(DBG_INFO, "OBIS " + String(buf[0], HEX) + "-" + String(buf[1], HEX) + ":" + String(buf[2], HEX) + + "." + String (buf[3], HEX) + "." + String(buf[4], HEX) + "*" + String(buf[5], HEX)); + } +#endif + if (sml_message == SML_MSG_GET_LIST_RSP) { + if (buf[0] == 1) { + if (!memcmp (&buf[2], OBIS_SIG_YIELD_IN_ALL, 3)) { + obis_state = OBIS_ST_YIELD_IN_ALL; + } else if (!memcmp (&buf[2], OBIS_SIG_YIELD_OUT_ALL, 3)) { + obis_state = OBIS_ST_YIELD_OUT_ALL; + } else if (!memcmp (&buf[2], OBIS_SIG_POWER_ALL, 3)) { + obis_state = OBIS_ST_POWER_ALL; + } else if (!memcmp (&buf[2], OBIS_SIG_POWER_L1, 3)) { + obis_state = OBIS_ST_POWER_L1; + } else if (!memcmp (&buf[2], OBIS_SIG_POWER_L2, 3)) { + obis_state = OBIS_ST_POWER_L2; + } else if (!memcmp (&buf[2], OBIS_SIG_POWER_L3, 3)) { + obis_state = OBIS_ST_POWER_L3; + } else if (!memcmp (&buf[2], OBIS_SIG_CURRENT_L1, 3)) { + obis_state = OBIS_ST_CURRENT_L1; + } else if (!memcmp (&buf[2], OBIS_SIG_CURRENT_L2, 3)) { + obis_state = OBIS_ST_CURRENT_L2; + } else if (!memcmp (&buf[2], OBIS_SIG_CURRENT_L3, 3)) { + obis_state = OBIS_ST_CURRENT_L3; + } else if (!memcmp (&buf[2], OBIS_SIG_VOLTAGE_L1, 3)) { + obis_state = OBIS_ST_VOLTAGE_L1; + } else if (!memcmp (&buf[2], OBIS_SIG_VOLTAGE_L2, 3)) { + obis_state = OBIS_ST_VOLTAGE_L2; + } else if (!memcmp (&buf[2], OBIS_SIG_VOLTAGE_L3, 3)) { + obis_state = OBIS_ST_VOLTAGE_L3; + } else { + obis_state = OBIS_ST_UNKNOWN; + } + } else { + obis_state = OBIS_ST_UNKNOWN; + } + } +} + +//----------------------------------------------------------------------------- +int64_t sml_obis_get_uint (unsigned char *data, unsigned int len) +{ + int64_t value = 0; + + if (len > 8) { + DPRINTLN(DBG_WARN, "Int too big"); + } else { + unsigned int i; + + for (i=0; i 0) { + value = value * (10 * scale); + } else if (scale < 0) { + value = value / (10 * -scale); + } + return (int)value; +} +//----------------------------------------------------------------------------- +int64_t sml_obis_get_int (unsigned char *data, unsigned int len) +{ + int64_t value = 0; + + if (len > 8) { + DPRINTLN(DBG_WARN, "Int too big"); + } else { + unsigned int i; + + if ((len > 0) && (*data & 0x80)) { + value = -1LL; + } + for (i=0; i 0) { + value = value * (10 * scale); + } else if (scale < 0) { + value = value / (10 * -scale); + } + return (int)value; +} + +//----------------------------------------------------------------------------- +int16_t sml_get_obis_pac_average () +{ + int32_t average; + int16_t pac_average = 0; + + if (obis_cur_pac_cnt) { + if (obis_pac_sum >= 0) { + average = (obis_pac_sum + (obis_cur_pac_cnt >> 1)) / obis_cur_pac_cnt; + if (average > INT16_MAX) { + pac_average = INT16_MAX; + } else { + pac_average = average; + } + } else { + average = (obis_pac_sum - (obis_cur_pac_cnt >> 1)) / obis_cur_pac_cnt; + if (average < INT16_MIN) { + pac_average = INT16_MIN; + } else { + pac_average = average; + } + } + } + return pac_average; +} + +//----------------------------------------------------------------------------- +void sml_handle_obis_pac (int16_t pac) +{ + time_t time_today; + obis_cur_pac = pac; + + if ((time_today = *obis_timestamp)) { + uint32_t pac_index; + + time_today = gTimezone.toLocal (time_today); + + pac_index = hour(time_today) * 60 + minute(time_today); + pac_index /= AHOY_PAC_INTERVAL; + + if (pac_index != obis_cur_pac_index) { + /* calc average for last interval */ + if (obis_cur_pac_cnt) { + int16_t pac_average = sml_get_obis_pac_average(); + File file; + char file_name[sizeof (SML_OBIS_GRID_POWER_PATH) + sizeof (SML_OBIS_FORMAT_FILE_NAME)]; + + snprintf (file_name, sizeof (file_name), SML_OBIS_GRID_POWER_PATH "/" SML_OBIS_FORMAT_FILE_NAME, + day(time_today), month(time_today), year(time_today)); + // append last average + if ((file = LittleFS.open (file_name, "a"))) { + unsigned char buf[4]; + buf[0] = obis_cur_pac_index & 0xff; + buf[1] = obis_cur_pac_index >> 8; + buf[2] = pac_average & 0xff; + buf[3] = pac_average >> 8; + if (file.write (buf, sizeof (buf)) != sizeof (buf)) { + DPRINTLN (DBG_WARN, "sml_handle_obis_pac, failed_to_write"); + } else { + DPRINTLN (DBG_DEBUG, "sml_handle_obis_pac, write to " + String(file_name)); + } + file.close (); + } else { + DPRINTLN (DBG_WARN, "sml_handle_obis_pac, failed to open"); + } + obis_cur_pac_cnt = 0; + obis_pac_sum = 0; + } + obis_cur_pac_index = pac_index; + } + if ((pac_index >= AHOY_MIN_PAC_SUN_HOUR * 60 / AHOY_PAC_INTERVAL) && + (pac_index < AHOY_MAX_PAC_SUN_HOUR * 60 / AHOY_PAC_INTERVAL)) { + obis_pac_sum += pac; + obis_cur_pac_cnt++; + } else { + DPRINTLN (DBG_DEBUG, "sml_handle_obis_pac, outside daylight, minutes: " + String (pac_index * AHOY_PAC_INTERVAL)); + } + } else { + DPRINTLN (DBG_INFO, "sml_handle_obis_pac, no time2"); + } +} + +//----------------------------------------------------------------------------- +uint16_t sml_fill_buf (uint16_t len) +{ + len = min (sizeof (sml_serial_buf) - (cur_serial_buf - sml_serial_buf) - sml_serial_len, len); + +#ifdef SML_OBIS_TEST + size_t partlen; + + partlen = min (len, sizeof (sml_test_telegram) - sml_test_telegram_offset); + memcpy (cur_serial_buf + sml_serial_len, &sml_test_telegram[sml_test_telegram_offset], partlen); + sml_serial_len += partlen; + sml_test_telegram_offset += partlen; + if (sml_test_telegram_offset >= sizeof (sml_test_telegram)) { + sml_test_telegram_offset = 0; + } + if (partlen < len) { + memcpy (cur_serial_buf + sml_serial_len, &sml_test_telegram[sml_test_telegram_offset], len - partlen); + sml_serial_len += len - partlen; + sml_test_telegram_offset += len - partlen; + } +#else + if (len) { + len = Serial.readBytes(cur_serial_buf + sml_serial_len, len); + sml_serial_len += len; + } +#endif + return len; +} + +//----------------------------------------------------------------------------- +bool sml_get_list_entries (uint16_t layer) +{ + bool error = false; + + while (!error && sml_serial_len) { + sml_list_entry_type_t type = (sml_list_entry_type_t)(*cur_serial_buf & 0x70); + unsigned char entry_len; + // Acc. to Spec there might be len_info > 2. But does this happen in real life? + // Also: an info_len > 2 could be due to corrupt data. So better break. + uint16 len_info = (*cur_serial_buf & SML_EXT_LENGTH) ? 2 : 1; + +#ifdef undef + DPRINT (DBG_INFO, "get_list_entries"); + DBGPRINT (", layer " + String (layer)); + DBGPRINT (", entries " + String (sml_list_layer_entries[layer])); + DBGPRINT (", type 0x" + String (type, HEX)); + DBGPRINT (", len_info " + String (len_info)); + DBGPRINTLN (", sml_len " + String (sml_serial_len)); +#endif + + if (sml_serial_len < len_info) { + if (cur_serial_buf > sml_serial_buf) { + memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); + cur_serial_buf = sml_serial_buf; + } + error = true; + } else { + if (len_info == 2) { + entry_len = (*cur_serial_buf << 4) | (*(cur_serial_buf+1) & 0xf); + } else { + entry_len = *cur_serial_buf & 0x0f; /* bei Listen andere Bedeutung */ + } + if ((type == SML_TYPE_LIST) || (sml_serial_len >= entry_len)) { + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, len_info, cur_serial_buf); + if (layer || (sml_list_layer_entries[layer] > 2)) { + sml_msg_crc = sml_calc_crc (sml_msg_crc, len_info, cur_serial_buf); + } + + sml_serial_len -= len_info; + if (entry_len && (type != SML_TYPE_LIST)) { + entry_len -= len_info; + } + if (sml_serial_len) { + cur_serial_buf += len_info; + } else { + cur_serial_buf = sml_serial_buf; + } + if (sml_list_layer_entries[layer]) { + switch (type) { + case SML_TYPE_OCTET_STRING: + if ((layer == 4) && (entry_len == 6)) { + sml_handle_obis_state (cur_serial_buf); + } + break; + case SML_TYPE_BOOL: + break; + case SML_TYPE_INT: + if (!layer && (sml_list_layer_entries[layer] == 2)) { + // perhaps there is a creepy smart meter that does send crc as int? + uint16_t rcv_crc = sml_obis_get_int (cur_serial_buf, entry_len); + + sml_msg_crc = sml_finit_crc (sml_msg_crc); + if (rcv_crc != sml_msg_crc) { + DPRINTLN(DBG_WARN, "Wrong CRC for msg 0x" + String (sml_message, HEX) + + ", 0x" + String (sml_msg_crc, HEX) + " <-> 0x" + String (rcv_crc, HEX)); + } else { + sml_msg_failure = 0; + } + } else if (layer == 1) { + if ((sml_message == SML_MSG_NONE) && (sml_list_layer_entries[layer] == 2)) { + sml_message = sml_obis_get_int (cur_serial_buf, entry_len); + } + } else if (layer == 4) { + if (obis_state == OBIS_ST_POWER_ALL) { + if (sml_list_layer_entries[layer] == 3) { + obis_power_all_scale = (int)sml_obis_get_int (cur_serial_buf, entry_len); + } else if (sml_list_layer_entries[layer] == 2) { + obis_power_all_value = (int)sml_obis_get_int (cur_serial_buf, entry_len); + } + } else if (obis_state == OBIS_ST_YIELD_IN_ALL) { + if (sml_list_layer_entries[layer] == 3) { + obis_yield_in_all_scale = (int)sml_obis_get_int (cur_serial_buf, entry_len); + } else if (sml_list_layer_entries[layer] == 2) { + obis_yield_in_all_value = sml_obis_get_int (cur_serial_buf, entry_len); + } + } else if (obis_state == OBIS_ST_YIELD_OUT_ALL) { + if (sml_list_layer_entries[layer] == 3) { + obis_yield_out_all_scale = (int)sml_obis_get_int (cur_serial_buf, entry_len); + } else if (sml_list_layer_entries[layer] == 2) { + obis_yield_out_all_value = sml_obis_get_int (cur_serial_buf, entry_len); + } + } + } + break; + case SML_TYPE_UINT: + if (!layer && (sml_list_layer_entries[layer] == 2)) { + uint16_t rcv_crc = sml_obis_get_uint (cur_serial_buf, entry_len); + + sml_msg_crc = sml_finit_crc (sml_msg_crc); + if (rcv_crc != sml_msg_crc) { + DPRINTLN(DBG_WARN, "Wrong CRC for msg 0x" + String (sml_message, HEX) + + ", 0x" + String (sml_msg_crc, HEX) + " <-> 0x" + String (rcv_crc, HEX)); + } else { + sml_msg_failure = 0; + } + } else if (layer == 1) { + if ((sml_message == SML_MSG_NONE) && (sml_list_layer_entries[layer] == 2)) { + sml_message = sml_obis_get_uint (cur_serial_buf, entry_len); + } + } else if (layer == 4) { + if (obis_state == OBIS_ST_YIELD_IN_ALL) { + if (sml_list_layer_entries[layer] == 2) { + obis_yield_in_all_value = sml_obis_get_uint (cur_serial_buf, entry_len); + } + } else if (obis_state == OBIS_ST_YIELD_OUT_ALL) { + if (sml_list_layer_entries[layer] == 2) { + obis_yield_out_all_value = sml_obis_get_uint (cur_serial_buf, entry_len); + } + } + } + break; + case SML_TYPE_LIST: + if (layer + 1 < SML_MAX_LIST_LAYER) { + sml_list_layer_entries[layer]--; + layer++; + cur_sml_list_layer = layer; + sml_list_layer_entries[layer] = entry_len; +#ifdef undef + DPRINTLN(DBG_INFO, "Open layer " + String(layer) + ", entries " + String(entry_len)); +#endif + if (!sml_serial_len) { + error = true; + } + } else { + sml_state = SML_ST_FIND_START_TAG; + return sml_serial_len ? false : true; + } + break; + default: + DPRINT(DBG_WARN, "Ill Element 0x" + String(type, HEX)); + DBGPRINTLN(", len " + String (entry_len + len_info)); + /* design: aussteigen */ + sml_state = SML_ST_FIND_START_TAG; + return sml_serial_len ? false : true; + } + if (type != SML_TYPE_LIST) { + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, entry_len, cur_serial_buf); + if (layer || (sml_list_layer_entries[layer] > 2)) { + sml_msg_crc = sml_calc_crc (sml_msg_crc, entry_len, cur_serial_buf); + } + sml_serial_len -= entry_len; + if (sml_serial_len) { + cur_serial_buf += entry_len; + } else { + cur_serial_buf = sml_serial_buf; + error = true; + } + sml_list_layer_entries[layer]--; + } + } + while (!sml_list_layer_entries[layer]) { +#ifdef undef + DPRINTLN(DBG_INFO, "COMPLETE, layer " + String (layer)); +#endif + if (layer) { + layer--; + cur_sml_list_layer = layer; + } else { + sml_state = SML_ST_FIND_MSG; + return sml_serial_len ? false : true; + } + } + } else if (entry_len > sizeof (sml_serial_buf)) { + DPRINTLN (DBG_INFO, "skip " + String (entry_len)); + sml_skip_len = entry_len; + sml_state = SML_ST_SKIP_LIST_ENTRY; + return false; + } else { + if (cur_serial_buf > sml_serial_buf) { + memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); + cur_serial_buf = sml_serial_buf; + } + error = true; + } + } + } + return error; +} + +//----------------------------------------------------------------------------- +uint16_t sml_parse_stream (uint16 len) +{ + bool parse_continue; + uint16_t serial_read; + + serial_read = sml_fill_buf (len); + + do { + parse_continue = false; + switch (sml_state) { + case SML_ST_FIND_START_TAG: + if (sml_serial_len >= sizeof (esc_seq)) { + unsigned char *last_serial_buf = cur_serial_buf; + + if ((cur_serial_buf = (unsigned char *)memmem (cur_serial_buf, sml_serial_len, esc_seq, sizeof (esc_seq)))) { + sml_telegram_crc = sml_init_crc (); + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, sizeof (esc_seq), cur_serial_buf); + sml_serial_len -= cur_serial_buf - last_serial_buf; + sml_serial_len -= sizeof (esc_seq); + if (sml_serial_len) { + cur_serial_buf += sizeof (esc_seq); + parse_continue = true; + } else { + cur_serial_buf = sml_serial_buf; + } + sml_state = SML_ST_FIND_VERSION; +#ifdef undef + DPRINTLN(DBG_INFO, "START_TAG, rest " + String(sml_serial_len)); +#endif + } else { + cur_serial_buf = last_serial_buf + sml_serial_len; + last_serial_buf = cur_serial_buf; + + /* handle up to last 3 esc chars */ + while ((*(cur_serial_buf - 1) == SML_ESCAPE_CHAR)) { + cur_serial_buf--; + } + if ((sml_serial_len = last_serial_buf - cur_serial_buf)) { + memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); + } + cur_serial_buf = sml_serial_buf; + } + } else if (cur_serial_buf > sml_serial_buf) { + memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); + cur_serial_buf = sml_serial_buf; + } + break; + case SML_ST_FIND_VERSION: + if (sml_serial_len >=sizeof (version_seq)) { + if (!memcmp (cur_serial_buf, version_seq, sizeof (version_seq))) { + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, sizeof (version_seq), cur_serial_buf); + sml_msg_failure = 0; + sml_state = SML_ST_FIND_MSG; +#ifdef undef + DPRINTLN(DBG_INFO, "VERSION, rest " + String (sml_serial_len - sizeof (version_seq))); +#endif + } else { +#ifdef undef + DPRINTLN(DBG_INFO, "no VERSION, rest " + String (sml_serial_len - sizeof (version_seq))); +#endif + sml_state = SML_ST_FIND_START_TAG; + } + sml_serial_len -= sizeof (version_seq); + if (sml_serial_len) { + cur_serial_buf += sizeof (version_seq); + parse_continue = true; + } else { + cur_serial_buf = sml_serial_buf; + } + } else if (cur_serial_buf > sml_serial_buf) { + memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); + cur_serial_buf = sml_serial_buf; + } + break; + case SML_ST_FIND_MSG: + if (sml_msg_failure) { + sml_state = SML_ST_FIND_START_TAG; + parse_continue = sml_serial_len ? true : false; + } else if (sml_serial_len) { + if (*cur_serial_buf == 0x1b) { + sml_state = SML_ST_FIND_END_TAG; + parse_continue = true; + } else if ((*cur_serial_buf & 0x70) == 0x70) { + /* todo: extended list on 1st level (does this happen in real life?) */ + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, 1, cur_serial_buf); +#ifdef undef + DPRINTLN (DBG_INFO, "TOPLIST 0x" + String(*cur_serial_buf, HEX) + ", rest " + String (sml_serial_len - 1)); +#endif + sml_state = SML_ST_FIND_LIST_ENTRIES; + cur_sml_list_layer = 0; + sml_message = SML_MSG_NONE; + sml_msg_failure = 1; // assume corrupt data (crc of this msg must proof ok) + sml_msg_crc = sml_init_crc (); + sml_msg_crc = sml_calc_crc (sml_msg_crc, 1, cur_serial_buf); + obis_state = OBIS_ST_NONE; + sml_list_layer_entries[0] = *cur_serial_buf & 0xf; + sml_serial_len--; + if (sml_serial_len) { + cur_serial_buf++; + parse_continue = true; + } else { + cur_serial_buf = sml_serial_buf; + } + } else if (*cur_serial_buf == 0x00) { + /* fill byte (depends on the size of the telegram) */ + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, 1, cur_serial_buf); + sml_serial_len--; + if (sml_serial_len) { + cur_serial_buf++; + parse_continue = true; + } else { + cur_serial_buf = sml_serial_buf; + } + } else { + DPRINTLN(DBG_WARN, "Unexpected 0x" + String(*cur_serial_buf, HEX) + ", rest: " + String (sml_serial_len)); + sml_state = SML_ST_FIND_START_TAG; + parse_continue = true; + } + } + break; + case SML_ST_FIND_LIST_ENTRIES: + parse_continue = !sml_get_list_entries (cur_sml_list_layer); + break; + case SML_ST_SKIP_LIST_ENTRY: + if (sml_serial_len) { /* design: keep rcv buf small and skip irrelevant long list entries */ + size_t len = min (sml_serial_len, sml_skip_len); + + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, len, cur_serial_buf); + if (cur_sml_list_layer || (sml_list_layer_entries[cur_sml_list_layer] > 2)) { + sml_msg_crc = sml_calc_crc (sml_msg_crc, len, cur_serial_buf); + } + sml_serial_len -= len; + if (sml_serial_len) { + cur_serial_buf += len; + parse_continue = true; + } else { + cur_serial_buf = sml_serial_buf; + } + sml_skip_len -= len; + if (!sml_skip_len) { + sml_state = SML_ST_FIND_LIST_ENTRIES; + sml_list_layer_entries[cur_sml_list_layer]--; + while (!sml_list_layer_entries[cur_sml_list_layer]) { +#ifdef undef + DPRINTLN(DBG_INFO, "COMPLETE, layer " + String (cur_sml_list_layer)); +#endif + if (cur_sml_list_layer) { + cur_sml_list_layer--; + } else { + sml_state = SML_ST_FIND_MSG; + break; + } + } + } + } + break; + case SML_ST_FIND_END_TAG: + if (sml_serial_len >= sizeof (esc_seq)) { + if (!memcmp (cur_serial_buf, esc_seq, sizeof (esc_seq))) { + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, sizeof (esc_seq), cur_serial_buf); + sml_state = SML_ST_CHECK_CRC; + } else { + DPRINTLN(DBG_WARN, "Missing END_TAG, found 0x" + String (*cur_serial_buf) + + " 0x" + String (*(cur_serial_buf+1)) + + " 0x" + String (*(cur_serial_buf+2)) + + " 0x" + String (*(cur_serial_buf+3))); + sml_state = SML_ST_FIND_START_TAG; + } + sml_serial_len -= sizeof (esc_seq); + if (sml_serial_len) { + cur_serial_buf += sizeof (esc_seq); + parse_continue = true; + } else { + cur_serial_buf = sml_serial_buf; + } + } else if (cur_serial_buf > sml_serial_buf) { + memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); + cur_serial_buf = sml_serial_buf; + } + break; + case SML_ST_CHECK_CRC: + if (sml_serial_len >= 4) { + if (*cur_serial_buf == 0x1a) { + uint16_t calc_crc16, rcv_crc16; + + sml_telegram_crc = sml_calc_crc (sml_telegram_crc, 2, cur_serial_buf); + calc_crc16 = sml_finit_crc (sml_telegram_crc); + rcv_crc16 = (*(cur_serial_buf+2) << 8) + *(cur_serial_buf+3); + if (calc_crc16 == rcv_crc16) { + obis_power_all_value = sml_obis_scale_int (obis_power_all_value, obis_power_all_scale); +#ifdef undef + // a bit more verbose info + obis_yield_in_all_value = sml_obis_scale_uint (obis_yield_in_all_value, obis_yield_in_all_scale); + obis_yield_out_all_value = sml_obis_scale_uint (obis_yield_out_all_value, obis_yield_out_all_scale); + DPRINTLN(DBG_INFO, "Power " + String (obis_power_all_value) + + ", Yield in " + String (obis_yield_in_all_value) + + ", Yield out " + String (obis_yield_out_all_value)); +#else + DPRINTLN(DBG_INFO, "Power " + String (obis_power_all_value)); +#endif + sml_handle_obis_pac (obis_power_all_value); + } else { + DPRINTLN(DBG_WARN, "CRC ERROR 0x" + String (calc_crc16, HEX) + " <-> 0x" + String (rcv_crc16, HEX)); + } + } + sml_state = SML_ST_FIND_START_TAG; + sml_serial_len -= 4; + if (sml_serial_len) { + cur_serial_buf += 4; + parse_continue = true; + } else { + cur_serial_buf = sml_serial_buf; + } + } else if (cur_serial_buf > sml_serial_buf) { + memmove (sml_serial_buf, cur_serial_buf, sml_serial_len); + cur_serial_buf = sml_serial_buf; + } + break; + } + } while (parse_continue); + return serial_read; +} + +//----------------------------------------------------------------------------- +void sml_loop () +{ + uint16_t serial_avail; + uint16_t serial_read = 0; + +#ifdef SML_OBIS_TEST + uint32_t cur_uptime; + static uint32_t last_uptime; + if (((cur_uptime = mApp->getUptime()) > 30) && (cur_uptime != last_uptime)) { + last_uptime = cur_uptime; + serial_avail = sizeof (sml_test_telegram) >> 2; + } else { + serial_avail = 0; + } +#else + serial_avail = Serial.available(); +#endif + if (serial_avail > 0) { + do { + serial_read = sml_parse_stream (serial_avail); + serial_avail -= serial_read; + // yield(); /* unconditionally called this might have a bad effect for TX Retransmit to the Inverter via NRF24L01+ (not quite sure) */ + } while (serial_read && serial_avail); + } +} + +#endif From ee6f9024e825bf3f2b1b0ef31091a80faff8bcc8 Mon Sep 17 00:00:00 2001 From: oberfritze <139758614+oberfritze@users.noreply.github.com> Date: Sat, 12 Aug 2023 19:52:59 +0200 Subject: [PATCH 4/4] Add files via upload Anpassung an geaenderten Rueckgabewert fuer sml_find_hist_power(). --- src/web/RestApi.h | 1620 ++++++++++++++++++++++----------------------- 1 file changed, 810 insertions(+), 810 deletions(-) diff --git a/src/web/RestApi.h b/src/web/RestApi.h index fb9c33e07..699b13136 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -1,810 +1,810 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __WEB_API_H__ -#define __WEB_API_H__ - -#include "../utils/dbg.h" -#include "../config/config.h" -#ifdef ESP32 -#include "AsyncTCP.h" -#else -#include "ESPAsyncTCP.h" -#endif -#include "../appInterface.h" -#include "../hm/hmSystem.h" -#include "../utils/helper.h" -#include "AsyncJson.h" -#include "ESPAsyncWebServer.h" -#include "../plugins/SML_OBIS_Parser.h" - -#if defined(F) && defined(ESP32) -#undef F -#define F(sl) (sl) -#endif - -const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; -const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; - -template -class RestApi { - public: - RestApi() { - mTimezoneOffset = 0; - mHeapFree = 0; - mHeapFreeBlk = 0; - mHeapFrag = 0; - nr = 0; - } - - void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { - mApp = app; - mSrv = srv; - mSys = sys; - mConfig = config; - mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); - mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( - std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); - mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); - mSrv->on("/get_chartdata", HTTP_GET, std::bind(&RestApi::onGetChartData, this, std::placeholders::_1)); - } - - uint32_t getTimezoneOffset(void) { - return mTimezoneOffset; - } - - void ctrlRequest(JsonObject obj) { - /*char out[128]; - serializeJson(obj, out, 128); - DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ - DynamicJsonDocument json(128); - JsonObject dummy = json.as(); - if(obj[F("path")] == "ctrl") - setCtrl(obj, dummy); - else if(obj[F("path")] == "setup") - setSetup(obj, dummy); - } - - private: - void onApi(AsyncWebServerRequest *request) { - mHeapFree = ESP.getFreeHeap(); - #ifndef ESP32 - mHeapFreeBlk = ESP.getMaxFreeBlockSize(); - mHeapFrag = ESP.getHeapFragmentation(); - #endif - - - String path = request->url().substring(5); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); - JsonObject root = response->getRoot(); - if(path == "html/system") getHtmlSystem(request, root); - else if(path == "html/logout") getHtmlLogout(request, root); - else if(path == "html/reboot") getHtmlReboot(request, root); - else if(path == "html/save") getHtmlSave(request, root); - else if(path == "system") getSysInfo(request, root); - else if(path == "generic") getGeneric(request, root); - else if(path == "reboot") getReboot(request, root); - else if(path == "statistics") getStatistics(root); - else if(path == "inverter/list") getInverterList(root); - else if(path == "index") getIndex(request, root); - else if(path == "setup") getSetup(request, root); - else if(path == "setup/networks") getNetworks(root); - else if(path == "live") getLive(request, root); - else if(path == "record/info") getRecord(root, InverterDevInform_All); - else if(path == "record/alarm") getRecord(root, AlarmData); - else if(path == "record/config") getRecord(root, SystemConfigPara); - else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); - else { - if(path.substring(0, 12) == "inverter/id/") - getInverter(root, request->url().substring(17).toInt()); - else - getNotFound(root, F("http://") + request->host() + F("/api/")); - } - //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); - response->addHeader("Access-Control-Allow-Origin", "*"); - response->addHeader("Access-Control-Allow-Headers", "content-type"); - response->setLength(); - request->send(response); - } - - void onApiPost(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, "onApiPost"); - } - - void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - DPRINTLN(DBG_VERBOSE, "onApiPostBody"); - DynamicJsonDocument json(200); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); - JsonObject root = response->getRoot(); - - DeserializationError err = deserializeJson(json, (const char *)data, len); - JsonObject obj = json.as(); - root[F("success")] = (err) ? false : true; - if(!err) { - String path = request->url().substring(5); - if(path == "ctrl") - root[F("success")] = setCtrl(obj, root); - else if(path == "setup") - root[F("success")] = setSetup(obj, root); - else { - root[F("success")] = false; - root[F("error")] = "Path not found: " + path; - } - } - else { - switch (err.code()) { - case DeserializationError::Ok: break; - case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; - case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; - default: root[F("error")] = F("Deserialization failed"); break; - } - } - - response->setLength(); - request->send(response); - } - - void getNotFound(JsonObject obj, String url) { - JsonObject ep = obj.createNestedObject("avail_endpoints"); - ep[F("system")] = url + F("system"); - ep[F("statistics")] = url + F("statistics"); - ep[F("inverter/list")] = url + F("inverter/list"); - ep[F("index")] = url + F("index"); - ep[F("setup")] = url + F("setup"); - ep[F("live")] = url + F("live"); - ep[F("record/info")] = url + F("record/info"); - ep[F("record/alarm")] = url + F("record/alarm"); - ep[F("record/config")] = url + F("record/config"); - ep[F("record/live")] = url + F("record/live"); - } - - unsigned int get_int_length (int value) - { - unsigned int length = 1; - unsigned int base10 = 10, last_base10; - - if (value < 0) { - length++; - value = -value; - } - while ((unsigned int)value >= base10) { - length++; - last_base10 = base10; - base10 *= 10; - if (base10 <= last_base10) { - break; - } - } - return length; - } - - void onDwnldSetup(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response; - - File fp = LittleFS.open("/settings.json", "r"); - if(!fp) { - DPRINTLN(DBG_ERROR, F("failed to load settings")); - response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); - } - else { - String tmp = fp.readString(); - int i = 0; - // remove all passwords - while (i != -1) { - i = tmp.indexOf("\"pwd\":", i); - if(-1 != i) { - i+=7; - tmp.remove(i, tmp.indexOf("\"", i)-i); - } - } - response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); - } - - response->addHeader("Content-Type", "application/octet-stream"); - response->addHeader("Content-Description", "File Transfer"); - response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); - request->send(response); - fp.close(); - } - - void onGetChartData(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response; - File ac_hist; - unsigned char *ac_hist_buf = NULL, *cur_ac_hist_buf, *end_ac_hist_buf; - uint16_t ac_power, cur_interval, length = 0; - size_t ac_hist_size = 0; - - // phase 1: count mem needed for CSV String - - if ((ac_hist = mSys->open_hist()) && - (ac_hist_size = ac_hist.size()) && - (ac_hist_size <= (AHOY_MAX_PAC_SUN_HOUR - AHOY_MIN_PAC_SUN_HOUR) * 60 / AHOY_PAC_INTERVAL * 4) && - (ac_hist_buf = (unsigned char *)malloc (ac_hist_size))) { - ac_hist.read (ac_hist_buf, ac_hist_size); - mSys->close_hist (ac_hist); - ac_hist.close(); - cur_ac_hist_buf = ac_hist_buf; - end_ac_hist_buf = cur_ac_hist_buf + ac_hist_size; - while (cur_ac_hist_buf < end_ac_hist_buf) { - cur_interval = *cur_ac_hist_buf++; - cur_interval += (*cur_ac_hist_buf++) << 8; - ac_power = *cur_ac_hist_buf++; - ac_power += (*cur_ac_hist_buf++) << 8; - if (cur_interval < 600 / AHOY_PAC_INTERVAL) { - length += 1 + 4 + 1; // +1: comma, +1: lf - } else { - length += 1 + 5 + 1; - } - length += get_int_length (ac_power); -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - length += 1 + 6; // reserve longest power value ,-abcde - } -#endif - } - } - if (mSys->get_cur_value (&cur_interval, &ac_power)) { - if (cur_interval < 600 / AHOY_PAC_INTERVAL) { - length += 1 + 4 + 1; - } else { - length += 1 + 5 + 1; - } - length += get_int_length (ac_power); -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - length += 1 + 6; // reserve longest power value ,-abcde - } -#endif - } - if (length) { - length += mConfig->sml_obis.ir_connected ? sizeof (AHOY_CHARTDATA_WITH_GRID_HDR) : sizeof (AHOY_CHARTDATA_HDR); - } - - // phase 2: concatenate CSV string - - if (length) { - char *content = NULL; - unsigned int index; - - if ((content = (char *)malloc (length))) { - uint16_t minutes; -#ifdef AHOY_SML_OBIS_SUPPORT - int sml_power; - File sml_hist; - - if (mConfig->sml_obis.ir_connected) { - sml_hist = sml_open_hist (); - strcpy (content, AHOY_CHARTDATA_WITH_GRID_HDR); - } else -#endif - { - strcpy (content, AHOY_CHARTDATA_HDR); - } - index = strlen (content); - - cur_ac_hist_buf = ac_hist_buf; - end_ac_hist_buf = cur_ac_hist_buf + ac_hist_size; - while (cur_ac_hist_buf < end_ac_hist_buf) { - cur_interval = *cur_ac_hist_buf++; - cur_interval += (*cur_ac_hist_buf++) << 8; - ac_power = *cur_ac_hist_buf++; - ac_power += (*cur_ac_hist_buf++) << 8; - - minutes = cur_interval * AHOY_PAC_INTERVAL; - -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - if ((sml_power = sml_find_hist_power(sml_hist, cur_interval)) == -1) { - snprintf (&content[index], length - index, "\n%u:%02u,%u,", - minutes / 60, minutes % 60, ac_power); - } else { - snprintf (&content[index], length - index, "\n%u:%02u,%u,%d", - minutes / 60, minutes % 60, ac_power, sml_power); - } - } else -#endif - { - snprintf (&content[index], length - index, "\n%u:%02u,%u", - minutes / 60, minutes % 60, ac_power); - } - index += strlen (&content[index]); - } - if (ac_hist_buf) { - free (ac_hist_buf); - } - if (mSys->get_cur_value (&cur_interval, &ac_power)) { - uint16_t minutes = cur_interval * AHOY_PAC_INTERVAL; - -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - if ((sml_power = sml_find_hist_power(sml_hist, cur_interval)) == -1) { - snprintf (&content[index], length - index, "\n%u:%02u,%u,", - minutes / 60, minutes % 60, ac_power); - } else { - snprintf (&content[index], length - index, "\n%u:%02u,%u,%d", - minutes / 60, minutes % 60, ac_power, sml_power); - } - } else -#endif - { - snprintf (&content[index], length - index, "\n%u:%02u,%u", - minutes / 60, minutes % 60, ac_power); - } - index += strlen (&content[index]); - } -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - sml_close_hist (sml_hist); - } -#endif - response = request->beginResponse(200, F("text/plain"), content); - free (content); - } else if (mConfig->sml_obis.ir_connected) { - response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_WITH_GRID_HDR "\nno memory"); - } else { - response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_HDR "\nno memory"); - } - } else if (mConfig->sml_obis.ir_connected) { - response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_WITH_GRID_HDR "\nno value found"); - } else { - response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_HDR "\nno value found"); - } - if (response) { - response->addHeader("Content-Description", "File Transfer"); - response->addHeader("Content-Disposition", "attachment; filename=chartdata.csv"); - request->send(response); - } else { - request->send(404); - } - } - - void getGeneric(AsyncWebServerRequest *request, JsonObject obj) { - obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); - obj[F("ts_uptime")] = mApp->getUptime(); - obj[F("menu_prot")] = mApp->getProtection(request); - obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); - obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); - - #if defined(ESP32) - obj[F("esp_type")] = F("ESP32"); - #else - obj[F("esp_type")] = F("ESP8266"); - #endif - } - - void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) { - obj[F("ssid")] = mConfig->sys.stationSsid; - obj[F("device_name")] = mConfig->sys.deviceName; - obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; - - obj[F("mac")] = WiFi.macAddress(); - obj[F("hostname")] = mConfig->sys.deviceName; - obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); - obj[F("prot_mask")] = mConfig->sys.protectionMask; - - obj[F("sdk")] = ESP.getSdkVersion(); - obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); - obj[F("heap_free")] = mHeapFree; - obj[F("sketch_total")] = ESP.getFreeSketchSpace(); - obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb - getGeneric(request, obj); - - getRadio(obj.createNestedObject(F("radio"))); - getStatistics(obj.createNestedObject(F("statistics"))); - - #if defined(ESP32) - obj[F("heap_total")] = ESP.getHeapSize(); - obj[F("chip_revision")] = ESP.getChipRevision(); - obj[F("chip_model")] = ESP.getChipModel(); - obj[F("chip_cores")] = ESP.getChipCores(); - //obj[F("core_version")] = F("n/a"); - //obj[F("flash_size")] = F("n/a"); - //obj[F("heap_frag")] = F("n/a"); - //obj[F("max_free_blk")] = F("n/a"); - //obj[F("reboot_reason")] = F("n/a"); - #else - //obj[F("heap_total")] = F("n/a"); - //obj[F("chip_revision")] = F("n/a"); - //obj[F("chip_model")] = F("n/a"); - //obj[F("chip_cores")] = F("n/a"); - obj[F("core_version")] = ESP.getCoreVersion(); - obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb - obj[F("heap_frag")] = mHeapFrag; - obj[F("max_free_blk")] = mHeapFreeBlk; - obj[F("reboot_reason")] = ESP.getResetReason(); - #endif - //obj[F("littlefs_total")] = LittleFS.totalBytes(); - //obj[F("littlefs_used")] = LittleFS.usedBytes(); - - uint8_t max; - mApp->getSchedulerInfo(&max); - obj[F("schMax")] = max; - } - - void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { - getSysInfo(request, obj.createNestedObject(F("system"))); - getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("html")] = F("Factory Reset

Reboot"); - } - - void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) { - getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 3; - obj[F("refresh_url")] = "/"; - obj[F("html")] = F("succesfully logged out"); - } - - void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) { - getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 20; - obj[F("refresh_url")] = "/"; - obj[F("html")] = F("rebooting ..."); - } - - void getHtmlSave(AsyncWebServerRequest *request, JsonObject obj) { - getGeneric(request, obj.createNestedObject(F("generic"))); - obj["pending"] = (bool)mApp->getSavePending(); - obj["success"] = (bool)mApp->getLastSaveSucceed(); - obj["reboot"] = (bool)mApp->getShouldReboot(); - } - - void getReboot(AsyncWebServerRequest *request, JsonObject obj) { - getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 10; - obj[F("refresh_url")] = "/"; - obj[F("html")] = F("reboot. Autoreload after 10 seconds"); - } - - void getStatistics(JsonObject obj) { - statistics_t *stat = mApp->getStatistics(); - obj[F("rx_success")] = stat->rxSuccess; - obj[F("rx_fail")] = stat->rxFail; - obj[F("rx_fail_answer")] = stat->rxFailNoAnser; - obj[F("frame_cnt")] = stat->frmCnt; - obj[F("tx_cnt")] = mSys->Radio.mSendCnt; - obj[F("retransmits")] = mSys->Radio.mRetransmits; - } - - void getInverterList(JsonObject obj) { - JsonArray invArr = obj.createNestedArray(F("inverter")); - - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("enabled")] = (bool)iv->config->enabled; - obj2[F("id")] = i; - obj2[F("name")] = String(iv->config->name); - obj2[F("serial")] = String(iv->config->serial.u64, HEX); - obj2[F("channels")] = iv->channels; - obj2[F("version")] = String(iv->getFwVersion()); - - for(uint8_t j = 0; j < iv->channels; j ++) { - obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j]; - obj2[F("ch_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("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; - obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; - } - - void getInverter(JsonObject obj, uint8_t id) { - Inverter<> *iv = mSys->getInverterByPos(id); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - obj[F("id")] = id; - obj[F("enabled")] = (bool)iv->config->enabled; - obj[F("name")] = String(iv->config->name); - obj[F("serial")] = String(iv->config->serial.u64, HEX); - obj[F("version")] = String(iv->getFwVersion()); - obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); - obj[F("ts_last_success")] = rec->ts; -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - // design: no value of inverter but I want this value to be displayed prominently - obj[F("grid_power")] = sml_get_obis_pac (); - } -#endif - - JsonArray ch = obj.createNestedArray("ch"); - - // AC - uint8_t pos; - obj[F("ch_name")][0] = "AC"; - JsonArray ch0 = ch.createNestedArray(); - for (uint8_t fld = 0; fld < sizeof(acList); fld++) { - pos = (iv->getPosByChFld(CH0, acList[fld], rec)); - ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - } - - // DC - for(uint8_t j = 0; j < iv->channels; j ++) { - obj[F("ch_name")][j+1] = iv->config->chName[j]; - obj[F("ch_max_pwr")][j+1] = iv->config->chMaxPwr[j]; - JsonArray cur = ch.createNestedArray(); - for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { - pos = (iv->getPosByChFld((j+1), dcList[fld], rec)); - cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - } - } - } - } - - void getMqtt(JsonObject obj) { - obj[F("broker")] = String(mConfig->mqtt.broker); - obj[F("port")] = String(mConfig->mqtt.port); - obj[F("user")] = String(mConfig->mqtt.user); - obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); - obj[F("topic")] = String(mConfig->mqtt.topic); - obj[F("interval")] = String(mConfig->mqtt.interval); - } - - void getNtp(JsonObject obj) { - obj[F("addr")] = String(mConfig->ntp.addr); - obj[F("port")] = String(mConfig->ntp.port); - } - - void getSun(JsonObject obj) { - obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; - obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; - obj[F("disnightcom")] = mConfig->sun.disNightCom; - obj[F("offs")] = mConfig->sun.offsetSec; - } - - void getPinout(JsonObject obj) { - obj[F("cs")] = mConfig->nrf.pinCs; - obj[F("ce")] = mConfig->nrf.pinCe; - obj[F("irq")] = mConfig->nrf.pinIrq; - obj[F("sclk")] = mConfig->nrf.pinSclk; - obj[F("mosi")] = mConfig->nrf.pinMosi; - obj[F("miso")] = mConfig->nrf.pinMiso; - obj[F("led0")] = mConfig->led.led0; - obj[F("led1")] = mConfig->led.led1; - obj[F("led_high_active")] = mConfig->led.led_high_active; - } - - void getRadio(JsonObject obj) { - obj[F("power_level")] = mConfig->nrf.amplifierPower; - obj[F("isconnected")] = mSys->Radio.isChipConnected(); - obj[F("DataRate")] = mSys->Radio.getDataRate(); - obj[F("isPVariant")] = mSys->Radio.isPVariant(); - } - - void getSerial(JsonObject obj) { - obj[F("interval")] = (uint16_t)mConfig->serial.interval; - obj[F("show_live_data")] = mConfig->serial.showIv; - obj[F("debug")] = mConfig->serial.debug; - } - - void getStaticIp(JsonObject obj) { - char buf[16]; - ah::ip2Char(mConfig->sys.ip.ip, buf); obj[F("ip")] = String(buf); - ah::ip2Char(mConfig->sys.ip.mask, buf); obj[F("mask")] = String(buf); - ah::ip2Char(mConfig->sys.ip.dns1, buf); obj[F("dns1")] = String(buf); - ah::ip2Char(mConfig->sys.ip.dns2, buf); obj[F("dns2")] = String(buf); - ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf); - } - - void getDisplay(JsonObject obj) { - obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; - obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; - obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift; - obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; - obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; - obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; - obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data; - obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; - obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; - obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; - obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; - } - - void getSML(JsonObject obj) { - obj[F("show_grid_data")] = mConfig->sml_obis.ir_connected; - } - - void getIndex(AsyncWebServerRequest *request, JsonObject obj) { - getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("ts_now")] = mApp->getTimestamp(); - obj[F("ts_sunrise")] = mApp->getSunrise(); - obj[F("ts_sunset")] = mApp->getSunset(); - obj[F("ts_offset")] = mConfig->sun.offsetSec; - obj[F("disNightComm")] = mConfig->sun.disNightCom; - - JsonArray inv = obj.createNestedArray(F("inverter")); - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject invObj = inv.createNestedObject(); - invObj[F("enabled")] = (bool)iv->config->enabled; - invObj[F("id")] = i; - invObj[F("name")] = String(iv->config->name); - invObj[F("version")] = String(iv->getFwVersion()); - invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp()); - invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp()); - invObj[F("ts_last_success")] = iv->getLastTs(rec); - } - } - - JsonArray warn = obj.createNestedArray(F("warnings")); - if(!mSys->Radio.isChipConnected()) - warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); - else if(!mSys->Radio.isPVariant()) - warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); - if(!mApp->getSettingsValid()) - warn.add(F("your settings are invalid")); - if(mApp->getRebootRequestState()) - warn.add(F("reboot your ESP to apply all your configuration changes")); - if(0 == mApp->getTimestamp()) - warn.add(F("time not set. No communication to inverter possible")); - /*if(0 == mSys->getNumInverters()) - warn.add(F("no inverter configured"));*/ -#ifdef AHOY_MQTT_SUPPORT - if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0)) - warn.add(F("MQTT is not connected")); - JsonArray info = obj.createNestedArray(F("infos")); - if(mApp->getMqttIsConnected()) - info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); - if(mConfig->mqtt.interval > 0) - info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds")); -#endif - } - - void getSetup(AsyncWebServerRequest *request, JsonObject obj) { - getGeneric(request, obj.createNestedObject(F("generic"))); - getSysInfo(request, obj.createNestedObject(F("system"))); - //getInverterList(obj.createNestedObject(F("inverter"))); - getMqtt(obj.createNestedObject(F("mqtt"))); - getNtp(obj.createNestedObject(F("ntp"))); - getSun(obj.createNestedObject(F("sun"))); - getPinout(obj.createNestedObject(F("pinout"))); - getRadio(obj.createNestedObject(F("radio"))); - getSerial(obj.createNestedObject(F("serial"))); - getStaticIp(obj.createNestedObject(F("static_ip"))); - getDisplay(obj.createNestedObject(F("display"))); - getSML(obj.createNestedObject(F("sml_obis"))); - } - - void getNetworks(JsonObject obj) { - mApp->getAvailNetworks(obj); - } - - void getLive(AsyncWebServerRequest *request, JsonObject obj) { - getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("refresh")] = mConfig->nrf.sendInterval; -#ifdef AHOY_SML_OBIS_SUPPORT - if (mConfig->sml_obis.ir_connected) { - // additionally here for correct chart titles - obj[F("grid_power")] = sml_get_obis_pac (); - } -#endif - - for (uint8_t fld = 0; fld < sizeof(acList); fld++) { - obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); - obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); - } - for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { - obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); - obj[F("fld_names")][fld] = String(fields[dcList[fld]]); - } - - Inverter<> *iv; - - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - bool parse = false; - if(NULL != iv) - parse = iv->config->enabled; - obj[F("iv")][i] = parse; - } - } - - void getRecord(JsonObject obj, uint8_t recType) { - JsonArray invArr = obj.createNestedArray(F("inverter")); - - Inverter<> *iv; - record_t<> *rec; - uint8_t pos; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - rec = iv->getRecordStruct(recType); - JsonArray obj2 = invArr.createNestedArray(); - for(uint8_t j = 0; j < rec->length; j++) { - byteAssign_t *assign = iv->getByteAssign(j, rec); - pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); - obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; - } - } - } - } - - bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { - Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); - bool accepted = true; - if(NULL == iv) { - jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); - return false; - } - - if(F("power") == jsonIn[F("cmd")]) - accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); - else if(F("restart") == jsonIn[F("restart")]) - accepted = iv->setDevControlRequest(Restart); - else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { - iv->powerLimit[0] = jsonIn["val"]; - if(F("limit_persistent_relative") == jsonIn[F("cmd")]) - iv->powerLimit[1] = RelativPersistent; - else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) - iv->powerLimit[1] = AbsolutPersistent; - else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")]) - iv->powerLimit[1] = RelativNonPersistent; - else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) - iv->powerLimit[1] = AbsolutNonPersistent; - - accepted = iv->setDevControlRequest(ActivePowerContr); - } - else if(F("dev") == jsonIn[F("cmd")]) { - DPRINTLN(DBG_INFO, F("dev cmd")); - iv->enqueCommand(jsonIn[F("val")].as()); - } - else { - jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; - return false; - } - - if(!accepted) { - jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); - return false; - } else - mApp->ivSendHighPrio(iv); - - return true; - } - - bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { - if(F("scan_wifi") == jsonIn[F("cmd")]) - mApp->scanAvailNetworks(); - else if(F("set_time") == jsonIn[F("cmd")]) - mApp->setTimestamp(jsonIn[F("val")]); - else if(F("sync_ntp") == jsonIn[F("cmd")]) - mApp->setTimestamp(0); // 0: update ntp flag - else if(F("serial_utc_offset") == jsonIn[F("cmd")]) - mTimezoneOffset = jsonIn[F("val")]; -#ifdef AHOY_MQTT_SUPPORT - else if(F("discovery_cfg") == jsonIn[F("cmd")]) - mApp->setMqttDiscoveryFlag(); // for homeassistant -#endif - else { - jsonOut[F("error")] = F("unknown cmd"); - return false; - } - - return true; - } - - IApp *mApp; - HMSYSTEM *mSys; - AsyncWebServer *mSrv; - settings_t *mConfig; - - uint32_t mTimezoneOffset; - uint32_t mHeapFree, mHeapFreeBlk; - uint8_t mHeapFrag; - uint16_t nr; -}; - -#endif /*__WEB_API_H__*/ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __WEB_API_H__ +#define __WEB_API_H__ + +#include "../utils/dbg.h" +#include "../config/config.h" +#ifdef ESP32 +#include "AsyncTCP.h" +#else +#include "ESPAsyncTCP.h" +#endif +#include "../appInterface.h" +#include "../hm/hmSystem.h" +#include "../utils/helper.h" +#include "AsyncJson.h" +#include "ESPAsyncWebServer.h" +#include "../plugins/SML_OBIS_Parser.h" + +#if defined(F) && defined(ESP32) +#undef F +#define F(sl) (sl) +#endif + +const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; +const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; + +template +class RestApi { + public: + RestApi() { + mTimezoneOffset = 0; + mHeapFree = 0; + mHeapFreeBlk = 0; + mHeapFrag = 0; + nr = 0; + } + + void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { + mApp = app; + mSrv = srv; + mSys = sys; + mConfig = config; + mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); + mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( + std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); + mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); + mSrv->on("/get_chartdata", HTTP_GET, std::bind(&RestApi::onGetChartData, this, std::placeholders::_1)); + } + + uint32_t getTimezoneOffset(void) { + return mTimezoneOffset; + } + + void ctrlRequest(JsonObject obj) { + /*char out[128]; + serializeJson(obj, out, 128); + DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ + DynamicJsonDocument json(128); + JsonObject dummy = json.as(); + if(obj[F("path")] == "ctrl") + setCtrl(obj, dummy); + else if(obj[F("path")] == "setup") + setSetup(obj, dummy); + } + + private: + void onApi(AsyncWebServerRequest *request) { + mHeapFree = ESP.getFreeHeap(); + #ifndef ESP32 + mHeapFreeBlk = ESP.getMaxFreeBlockSize(); + mHeapFrag = ESP.getHeapFragmentation(); + #endif + + + String path = request->url().substring(5); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); + JsonObject root = response->getRoot(); + if(path == "html/system") getHtmlSystem(request, root); + else if(path == "html/logout") getHtmlLogout(request, root); + else if(path == "html/reboot") getHtmlReboot(request, root); + else if(path == "html/save") getHtmlSave(request, root); + else if(path == "system") getSysInfo(request, root); + else if(path == "generic") getGeneric(request, root); + else if(path == "reboot") getReboot(request, root); + else if(path == "statistics") getStatistics(root); + else if(path == "inverter/list") getInverterList(root); + else if(path == "index") getIndex(request, root); + else if(path == "setup") getSetup(request, root); + else if(path == "setup/networks") getNetworks(root); + else if(path == "live") getLive(request, root); + else if(path == "record/info") getRecord(root, InverterDevInform_All); + else if(path == "record/alarm") getRecord(root, AlarmData); + else if(path == "record/config") getRecord(root, SystemConfigPara); + else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); + else { + if(path.substring(0, 12) == "inverter/id/") + getInverter(root, request->url().substring(17).toInt()); + else + getNotFound(root, F("http://") + request->host() + F("/api/")); + } + //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); + response->addHeader("Access-Control-Allow-Origin", "*"); + response->addHeader("Access-Control-Allow-Headers", "content-type"); + response->setLength(); + request->send(response); + } + + void onApiPost(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, "onApiPost"); + } + + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + DPRINTLN(DBG_VERBOSE, "onApiPostBody"); + DynamicJsonDocument json(200); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); + JsonObject root = response->getRoot(); + + DeserializationError err = deserializeJson(json, (const char *)data, len); + JsonObject obj = json.as(); + root[F("success")] = (err) ? false : true; + if(!err) { + String path = request->url().substring(5); + if(path == "ctrl") + root[F("success")] = setCtrl(obj, root); + else if(path == "setup") + root[F("success")] = setSetup(obj, root); + else { + root[F("success")] = false; + root[F("error")] = "Path not found: " + path; + } + } + else { + switch (err.code()) { + case DeserializationError::Ok: break; + case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; + case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; + default: root[F("error")] = F("Deserialization failed"); break; + } + } + + response->setLength(); + request->send(response); + } + + void getNotFound(JsonObject obj, String url) { + JsonObject ep = obj.createNestedObject("avail_endpoints"); + ep[F("system")] = url + F("system"); + ep[F("statistics")] = url + F("statistics"); + ep[F("inverter/list")] = url + F("inverter/list"); + ep[F("index")] = url + F("index"); + ep[F("setup")] = url + F("setup"); + ep[F("live")] = url + F("live"); + ep[F("record/info")] = url + F("record/info"); + ep[F("record/alarm")] = url + F("record/alarm"); + ep[F("record/config")] = url + F("record/config"); + ep[F("record/live")] = url + F("record/live"); + } + + unsigned int get_int_length (int value) + { + unsigned int length = 1; + unsigned int base10 = 10, last_base10; + + if (value < 0) { + length++; + value = -value; + } + while ((unsigned int)value >= base10) { + length++; + last_base10 = base10; + base10 *= 10; + if (base10 <= last_base10) { + break; + } + } + return length; + } + + void onDwnldSetup(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + + File fp = LittleFS.open("/settings.json", "r"); + if(!fp) { + DPRINTLN(DBG_ERROR, F("failed to load settings")); + response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); + } + else { + String tmp = fp.readString(); + int i = 0; + // remove all passwords + while (i != -1) { + i = tmp.indexOf("\"pwd\":", i); + if(-1 != i) { + i+=7; + tmp.remove(i, tmp.indexOf("\"", i)-i); + } + } + response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); + } + + response->addHeader("Content-Type", "application/octet-stream"); + response->addHeader("Content-Description", "File Transfer"); + response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); + request->send(response); + fp.close(); + } + + void onGetChartData(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + File ac_hist; + unsigned char *ac_hist_buf = NULL, *cur_ac_hist_buf, *end_ac_hist_buf; + uint16_t ac_power, cur_interval, length = 0; + size_t ac_hist_size = 0; + + // phase 1: count mem needed for CSV String + + if ((ac_hist = mSys->open_hist()) && + (ac_hist_size = ac_hist.size()) && + (ac_hist_size <= (AHOY_MAX_PAC_SUN_HOUR - AHOY_MIN_PAC_SUN_HOUR) * 60 / AHOY_PAC_INTERVAL * 4) && + (ac_hist_buf = (unsigned char *)malloc (ac_hist_size))) { + ac_hist.read (ac_hist_buf, ac_hist_size); + mSys->close_hist (ac_hist); + ac_hist.close(); + cur_ac_hist_buf = ac_hist_buf; + end_ac_hist_buf = cur_ac_hist_buf + ac_hist_size; + while (cur_ac_hist_buf < end_ac_hist_buf) { + cur_interval = *cur_ac_hist_buf++; + cur_interval += (*cur_ac_hist_buf++) << 8; + ac_power = *cur_ac_hist_buf++; + ac_power += (*cur_ac_hist_buf++) << 8; + if (cur_interval < 600 / AHOY_PAC_INTERVAL) { + length += 1 + 4 + 1; // +1: comma, +1: lf + } else { + length += 1 + 5 + 1; + } + length += get_int_length (ac_power); +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + length += 1 + 6; // reserve longest power value ,-abcde + } +#endif + } + } + if (mSys->get_cur_value (&cur_interval, &ac_power)) { + if (cur_interval < 600 / AHOY_PAC_INTERVAL) { + length += 1 + 4 + 1; + } else { + length += 1 + 5 + 1; + } + length += get_int_length (ac_power); +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + length += 1 + 6; // reserve longest power value ,-abcde + } +#endif + } + if (length) { + length += mConfig->sml_obis.ir_connected ? sizeof (AHOY_CHARTDATA_WITH_GRID_HDR) : sizeof (AHOY_CHARTDATA_HDR); + } + + // phase 2: concatenate CSV string + + if (length) { + char *content = NULL; + unsigned int index; + + if ((content = (char *)malloc (length))) { + uint16_t minutes; +#ifdef AHOY_SML_OBIS_SUPPORT + int sml_power; + File sml_hist; + + if (mConfig->sml_obis.ir_connected) { + sml_hist = sml_open_hist (); + strcpy (content, AHOY_CHARTDATA_WITH_GRID_HDR); + } else +#endif + { + strcpy (content, AHOY_CHARTDATA_HDR); + } + index = strlen (content); + + cur_ac_hist_buf = ac_hist_buf; + end_ac_hist_buf = cur_ac_hist_buf + ac_hist_size; + while (cur_ac_hist_buf < end_ac_hist_buf) { + cur_interval = *cur_ac_hist_buf++; + cur_interval += (*cur_ac_hist_buf++) << 8; + ac_power = *cur_ac_hist_buf++; + ac_power += (*cur_ac_hist_buf++) << 8; + + minutes = cur_interval * AHOY_PAC_INTERVAL; + +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + if ((sml_power = sml_find_hist_power(sml_hist, cur_interval)) == INT32_MIN) { + snprintf (&content[index], length - index, "\n%u:%02u,%u,", + minutes / 60, minutes % 60, ac_power); + } else { + snprintf (&content[index], length - index, "\n%u:%02u,%u,%d", + minutes / 60, minutes % 60, ac_power, sml_power); + } + } else +#endif + { + snprintf (&content[index], length - index, "\n%u:%02u,%u", + minutes / 60, minutes % 60, ac_power); + } + index += strlen (&content[index]); + } + if (ac_hist_buf) { + free (ac_hist_buf); + } + if (mSys->get_cur_value (&cur_interval, &ac_power)) { + uint16_t minutes = cur_interval * AHOY_PAC_INTERVAL; + +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + if ((sml_power = sml_find_hist_power(sml_hist, cur_interval)) == INT32_MIN) { + snprintf (&content[index], length - index, "\n%u:%02u,%u,", + minutes / 60, minutes % 60, ac_power); + } else { + snprintf (&content[index], length - index, "\n%u:%02u,%u,%d", + minutes / 60, minutes % 60, ac_power, sml_power); + } + } else +#endif + { + snprintf (&content[index], length - index, "\n%u:%02u,%u", + minutes / 60, minutes % 60, ac_power); + } + index += strlen (&content[index]); + } +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + sml_close_hist (sml_hist); + } +#endif + response = request->beginResponse(200, F("text/plain"), content); + free (content); + } else if (mConfig->sml_obis.ir_connected) { + response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_WITH_GRID_HDR "\nno memory"); + } else { + response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_HDR "\nno memory"); + } + } else if (mConfig->sml_obis.ir_connected) { + response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_WITH_GRID_HDR "\nno value found"); + } else { + response = request->beginResponse(200, F("text/plain"), AHOY_CHARTDATA_HDR "\nno value found"); + } + if (response) { + response->addHeader("Content-Description", "File Transfer"); + response->addHeader("Content-Disposition", "attachment; filename=chartdata.csv"); + request->send(response); + } else { + request->send(404); + } + } + + void getGeneric(AsyncWebServerRequest *request, JsonObject obj) { + obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); + obj[F("ts_uptime")] = mApp->getUptime(); + obj[F("menu_prot")] = mApp->getProtection(request); + obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); + obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); + + #if defined(ESP32) + obj[F("esp_type")] = F("ESP32"); + #else + obj[F("esp_type")] = F("ESP8266"); + #endif + } + + void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) { + obj[F("ssid")] = mConfig->sys.stationSsid; + obj[F("device_name")] = mConfig->sys.deviceName; + obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; + + obj[F("mac")] = WiFi.macAddress(); + obj[F("hostname")] = mConfig->sys.deviceName; + obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); + obj[F("prot_mask")] = mConfig->sys.protectionMask; + + obj[F("sdk")] = ESP.getSdkVersion(); + obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); + obj[F("heap_free")] = mHeapFree; + obj[F("sketch_total")] = ESP.getFreeSketchSpace(); + obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb + getGeneric(request, obj); + + getRadio(obj.createNestedObject(F("radio"))); + getStatistics(obj.createNestedObject(F("statistics"))); + + #if defined(ESP32) + obj[F("heap_total")] = ESP.getHeapSize(); + obj[F("chip_revision")] = ESP.getChipRevision(); + obj[F("chip_model")] = ESP.getChipModel(); + obj[F("chip_cores")] = ESP.getChipCores(); + //obj[F("core_version")] = F("n/a"); + //obj[F("flash_size")] = F("n/a"); + //obj[F("heap_frag")] = F("n/a"); + //obj[F("max_free_blk")] = F("n/a"); + //obj[F("reboot_reason")] = F("n/a"); + #else + //obj[F("heap_total")] = F("n/a"); + //obj[F("chip_revision")] = F("n/a"); + //obj[F("chip_model")] = F("n/a"); + //obj[F("chip_cores")] = F("n/a"); + obj[F("core_version")] = ESP.getCoreVersion(); + obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb + obj[F("heap_frag")] = mHeapFrag; + obj[F("max_free_blk")] = mHeapFreeBlk; + obj[F("reboot_reason")] = ESP.getResetReason(); + #endif + //obj[F("littlefs_total")] = LittleFS.totalBytes(); + //obj[F("littlefs_used")] = LittleFS.usedBytes(); + + uint8_t max; + mApp->getSchedulerInfo(&max); + obj[F("schMax")] = max; + } + + void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { + getSysInfo(request, obj.createNestedObject(F("system"))); + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("html")] = F("Factory Reset

Reboot"); + } + + void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 3; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("succesfully logged out"); + } + + void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 20; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("rebooting ..."); + } + + void getHtmlSave(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj["pending"] = (bool)mApp->getSavePending(); + obj["success"] = (bool)mApp->getLastSaveSucceed(); + obj["reboot"] = (bool)mApp->getShouldReboot(); + } + + void getReboot(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 10; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("reboot. Autoreload after 10 seconds"); + } + + void getStatistics(JsonObject obj) { + statistics_t *stat = mApp->getStatistics(); + obj[F("rx_success")] = stat->rxSuccess; + obj[F("rx_fail")] = stat->rxFail; + obj[F("rx_fail_answer")] = stat->rxFailNoAnser; + obj[F("frame_cnt")] = stat->frmCnt; + obj[F("tx_cnt")] = mSys->Radio.mSendCnt; + obj[F("retransmits")] = mSys->Radio.mRetransmits; + } + + void getInverterList(JsonObject obj) { + JsonArray invArr = obj.createNestedArray(F("inverter")); + + Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + if(NULL != iv) { + JsonObject obj2 = invArr.createNestedObject(); + obj2[F("enabled")] = (bool)iv->config->enabled; + obj2[F("id")] = i; + obj2[F("name")] = String(iv->config->name); + obj2[F("serial")] = String(iv->config->serial.u64, HEX); + obj2[F("channels")] = iv->channels; + obj2[F("version")] = String(iv->getFwVersion()); + + for(uint8_t j = 0; j < iv->channels; j ++) { + obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j]; + obj2[F("ch_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("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; + } + + void getInverter(JsonObject obj, uint8_t id) { + Inverter<> *iv = mSys->getInverterByPos(id); + if(NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + obj[F("id")] = id; + obj[F("enabled")] = (bool)iv->config->enabled; + obj[F("name")] = String(iv->config->name); + obj[F("serial")] = String(iv->config->serial.u64, HEX); + obj[F("version")] = String(iv->getFwVersion()); + obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); + obj[F("ts_last_success")] = rec->ts; +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + // design: no value of inverter but I want this value to be displayed prominently + obj[F("grid_power")] = sml_get_obis_pac (); + } +#endif + + JsonArray ch = obj.createNestedArray("ch"); + + // AC + uint8_t pos; + obj[F("ch_name")][0] = "AC"; + JsonArray ch0 = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + pos = (iv->getPosByChFld(CH0, acList[fld], rec)); + ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + + // DC + for(uint8_t j = 0; j < iv->channels; j ++) { + obj[F("ch_name")][j+1] = iv->config->chName[j]; + obj[F("ch_max_pwr")][j+1] = iv->config->chMaxPwr[j]; + JsonArray cur = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + pos = (iv->getPosByChFld((j+1), dcList[fld], rec)); + cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + } + } + } + + void getMqtt(JsonObject obj) { + obj[F("broker")] = String(mConfig->mqtt.broker); + obj[F("port")] = String(mConfig->mqtt.port); + obj[F("user")] = String(mConfig->mqtt.user); + obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); + obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("interval")] = String(mConfig->mqtt.interval); + } + + void getNtp(JsonObject obj) { + obj[F("addr")] = String(mConfig->ntp.addr); + obj[F("port")] = String(mConfig->ntp.port); + } + + void getSun(JsonObject obj) { + obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; + obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; + obj[F("disnightcom")] = mConfig->sun.disNightCom; + obj[F("offs")] = mConfig->sun.offsetSec; + } + + void getPinout(JsonObject obj) { + obj[F("cs")] = mConfig->nrf.pinCs; + obj[F("ce")] = mConfig->nrf.pinCe; + obj[F("irq")] = mConfig->nrf.pinIrq; + obj[F("sclk")] = mConfig->nrf.pinSclk; + obj[F("mosi")] = mConfig->nrf.pinMosi; + obj[F("miso")] = mConfig->nrf.pinMiso; + obj[F("led0")] = mConfig->led.led0; + obj[F("led1")] = mConfig->led.led1; + obj[F("led_high_active")] = mConfig->led.led_high_active; + } + + void getRadio(JsonObject obj) { + obj[F("power_level")] = mConfig->nrf.amplifierPower; + obj[F("isconnected")] = mSys->Radio.isChipConnected(); + obj[F("DataRate")] = mSys->Radio.getDataRate(); + obj[F("isPVariant")] = mSys->Radio.isPVariant(); + } + + void getSerial(JsonObject obj) { + obj[F("interval")] = (uint16_t)mConfig->serial.interval; + obj[F("show_live_data")] = mConfig->serial.showIv; + obj[F("debug")] = mConfig->serial.debug; + } + + void getStaticIp(JsonObject obj) { + char buf[16]; + ah::ip2Char(mConfig->sys.ip.ip, buf); obj[F("ip")] = String(buf); + ah::ip2Char(mConfig->sys.ip.mask, buf); obj[F("mask")] = String(buf); + ah::ip2Char(mConfig->sys.ip.dns1, buf); obj[F("dns1")] = String(buf); + ah::ip2Char(mConfig->sys.ip.dns2, buf); obj[F("dns2")] = String(buf); + ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf); + } + + void getDisplay(JsonObject obj) { + obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; + obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; + obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift; + obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; + obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; + obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; + obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data; + obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; + obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; + obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; + obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; + } + + void getSML(JsonObject obj) { + obj[F("show_grid_data")] = mConfig->sml_obis.ir_connected; + } + + void getIndex(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("ts_now")] = mApp->getTimestamp(); + obj[F("ts_sunrise")] = mApp->getSunrise(); + obj[F("ts_sunset")] = mApp->getSunset(); + obj[F("ts_offset")] = mConfig->sun.offsetSec; + obj[F("disNightComm")] = mConfig->sun.disNightCom; + + JsonArray inv = obj.createNestedArray(F("inverter")); + Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + if(NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + JsonObject invObj = inv.createNestedObject(); + invObj[F("enabled")] = (bool)iv->config->enabled; + invObj[F("id")] = i; + invObj[F("name")] = String(iv->config->name); + invObj[F("version")] = String(iv->getFwVersion()); + invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp()); + invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp()); + invObj[F("ts_last_success")] = iv->getLastTs(rec); + } + } + + JsonArray warn = obj.createNestedArray(F("warnings")); + if(!mSys->Radio.isChipConnected()) + warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); + else if(!mSys->Radio.isPVariant()) + warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); + if(!mApp->getSettingsValid()) + warn.add(F("your settings are invalid")); + if(mApp->getRebootRequestState()) + warn.add(F("reboot your ESP to apply all your configuration changes")); + if(0 == mApp->getTimestamp()) + warn.add(F("time not set. No communication to inverter possible")); + /*if(0 == mSys->getNumInverters()) + warn.add(F("no inverter configured"));*/ +#ifdef AHOY_MQTT_SUPPORT + if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0)) + warn.add(F("MQTT is not connected")); + JsonArray info = obj.createNestedArray(F("infos")); + if(mApp->getMqttIsConnected()) + info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); + if(mConfig->mqtt.interval > 0) + info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds")); +#endif + } + + void getSetup(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + getSysInfo(request, obj.createNestedObject(F("system"))); + //getInverterList(obj.createNestedObject(F("inverter"))); + getMqtt(obj.createNestedObject(F("mqtt"))); + getNtp(obj.createNestedObject(F("ntp"))); + getSun(obj.createNestedObject(F("sun"))); + getPinout(obj.createNestedObject(F("pinout"))); + getRadio(obj.createNestedObject(F("radio"))); + getSerial(obj.createNestedObject(F("serial"))); + getStaticIp(obj.createNestedObject(F("static_ip"))); + getDisplay(obj.createNestedObject(F("display"))); + getSML(obj.createNestedObject(F("sml_obis"))); + } + + void getNetworks(JsonObject obj) { + mApp->getAvailNetworks(obj); + } + + void getLive(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = mConfig->nrf.sendInterval; +#ifdef AHOY_SML_OBIS_SUPPORT + if (mConfig->sml_obis.ir_connected) { + // additionally here for correct chart titles + obj[F("grid_power")] = sml_get_obis_pac (); + } +#endif + + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); + obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); + } + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); + obj[F("fld_names")][fld] = String(fields[dcList[fld]]); + } + + Inverter<> *iv; + + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + bool parse = false; + if(NULL != iv) + parse = iv->config->enabled; + obj[F("iv")][i] = parse; + } + } + + void getRecord(JsonObject obj, uint8_t recType) { + JsonArray invArr = obj.createNestedArray(F("inverter")); + + Inverter<> *iv; + record_t<> *rec; + uint8_t pos; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + if(NULL != iv) { + rec = iv->getRecordStruct(recType); + JsonArray obj2 = invArr.createNestedArray(); + for(uint8_t j = 0; j < rec->length; j++) { + byteAssign_t *assign = iv->getByteAssign(j, rec); + pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); + obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; + obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; + obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; + } + } + } + } + + bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { + Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); + bool accepted = true; + if(NULL == iv) { + jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); + return false; + } + + if(F("power") == jsonIn[F("cmd")]) + accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); + else if(F("restart") == jsonIn[F("restart")]) + accepted = iv->setDevControlRequest(Restart); + else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { + iv->powerLimit[0] = jsonIn["val"]; + if(F("limit_persistent_relative") == jsonIn[F("cmd")]) + iv->powerLimit[1] = RelativPersistent; + else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) + iv->powerLimit[1] = AbsolutPersistent; + else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")]) + iv->powerLimit[1] = RelativNonPersistent; + else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) + iv->powerLimit[1] = AbsolutNonPersistent; + + accepted = iv->setDevControlRequest(ActivePowerContr); + } + else if(F("dev") == jsonIn[F("cmd")]) { + DPRINTLN(DBG_INFO, F("dev cmd")); + iv->enqueCommand(jsonIn[F("val")].as()); + } + else { + jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; + return false; + } + + if(!accepted) { + jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); + return false; + } else + mApp->ivSendHighPrio(iv); + + return true; + } + + bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { + if(F("scan_wifi") == jsonIn[F("cmd")]) + mApp->scanAvailNetworks(); + else if(F("set_time") == jsonIn[F("cmd")]) + mApp->setTimestamp(jsonIn[F("val")]); + else if(F("sync_ntp") == jsonIn[F("cmd")]) + mApp->setTimestamp(0); // 0: update ntp flag + else if(F("serial_utc_offset") == jsonIn[F("cmd")]) + mTimezoneOffset = jsonIn[F("val")]; +#ifdef AHOY_MQTT_SUPPORT + else if(F("discovery_cfg") == jsonIn[F("cmd")]) + mApp->setMqttDiscoveryFlag(); // for homeassistant +#endif + else { + jsonOut[F("error")] = F("unknown cmd"); + return false; + } + + return true; + } + + IApp *mApp; + HMSYSTEM *mSys; + AsyncWebServer *mSrv; + settings_t *mConfig; + + uint32_t mTimezoneOffset; + uint32_t mHeapFree, mHeapFreeBlk; + uint8_t mHeapFrag; + uint16_t nr; +}; + +#endif /*__WEB_API_H__*/