Browse Source

Merge branch 'development03' into eth

* untested
pull/1093/head
lumapu 2 years ago
parent
commit
e4e7e64861
  1. 2
      Getting_Started.md
  2. 2
      README.md
  3. 4
      User_Manual.md
  4. 5
      src/.vscode/settings.json
  5. 101
      src/CHANGES.md
  6. 303
      src/app.cpp
  7. 85
      src/app.h
  8. 5
      src/appInterface.h
  9. 11
      src/config/config.h
  10. 87
      src/config/settings.h
  11. 8
      src/defines.h
  12. 19
      src/hm/hmDefines.h
  13. 126
      src/hm/hmInverter.h
  14. 55
      src/hm/hmPayload.h
  15. 56
      src/hm/hmRadio.h
  16. 60
      src/hm/hmSystem.h
  17. 99
      src/hm/miPayload.h
  18. 437
      src/hms/cmt2300a.h
  19. 189
      src/hms/esp32_3wSpi.h
  20. 190
      src/hms/hmsDefines.h
  21. 405
      src/hms/hmsPayload.h
  22. 213
      src/hms/hmsRadio.h
  23. 20
      src/main.cpp
  24. 10
      src/platformio.ini
  25. 56
      src/plugins/Display/Display.h
  26. 4
      src/plugins/Display/Display_Mono.h
  27. 2
      src/plugins/Display/Display_Mono_128X32.h
  28. 5
      src/plugins/Display/Display_Mono_128X64.h
  29. 134
      src/plugins/Display/Display_Mono_64X48.h
  30. 11
      src/plugins/Display/Display_Mono_84X48.h
  31. 2
      src/plugins/Display/Display_ePaper.cpp
  32. 48
      src/publisher/pubMqtt.h
  33. 102
      src/publisher/pubMqttIvData.h
  34. 2
      src/publisher/pubSerial.h
  35. 1
      src/utils/dbg.cpp
  36. 23
      src/utils/dbg.h
  37. 18
      src/utils/helper.cpp
  38. 20
      src/utils/helper.h
  39. 221
      src/utils/improv.h
  40. 6
      src/utils/scheduler.h
  41. 66
      src/web/RestApi.h
  42. 10
      src/web/html/api.js
  43. 16
      src/web/html/index.html
  44. 188
      src/web/html/setup.html
  45. 5
      src/web/html/style.css
  46. 3
      src/web/html/visualization.html
  47. 46
      src/web/web.h
  48. 42
      src/wifi/ahoywifi.cpp
  49. 2
      src/wifi/ahoywifi.h
  50. 5
      tools/rpi/Dockerfile
  51. 43
      tools/rpi/hoymiles/__init__.py
  52. 22
      tools/rpi/hoymiles/__main__.py
  53. 27
      tools/rpi/hoymiles/decoders/__init__.py

2
Getting_Started.md

@ -14,6 +14,8 @@ Hoymiles Inverters
| ----- | ----- | ------ | ------- |
| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
| ✔️ | HMT | 1600, 1800, 2250 | |
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
## Table of Contents

2
README.md

@ -22,7 +22,7 @@ Table of approaches:
| Board | MI | HM | HMS/HMT | comment | HowTo start |
| ------ | -- | -- | ------- | ------- | ---------- |
| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | coming soon✨ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | ✔️ ✨ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |

4
User_Manual.md

@ -48,9 +48,11 @@ The AhoyDTU will publish on the following topics
| status code | Remarks |
|---|---|
| 0 | not available and not producing |
| 0 | off: not available and not producing |
| 1 | available but not producing |
| 2 | available and producing |
| 3 | available and was producing |
| 4 | was available |
### `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/ch0/#`

5
src/.vscode/settings.json

@ -8,7 +8,7 @@
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"diffEditor.ignoreTrimWhitespace": true,
"files.autoSave": "afterDelay",
"files.autoSave": "off",
"editor.tabSize": 4,
"editor.insertSpaces": true,
// `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.
@ -79,7 +79,8 @@
"mutex": "cpp",
"ranges": "cpp",
"stop_token": "cpp",
"thread": "cpp"
"thread": "cpp",
"variant": "cpp"
},
"cmake.configureOnOpen": false,
"editor.formatOnSave": false,

101
src/CHANGES.md

@ -1,5 +1,106 @@
# Development Changes
## 0.7.21 - 2023-07-30
* fix MqTT YieldDay Total goes to 0 serveral times #1016
## 0.7.20 - 2023-07-28
* merge PR #1048 version and hash in API, fixes #1045
* fix: no yield day update if yield day reads `0` after inverter reboot (mostly on evening) #848
* try to fix Wifi override #1047
* added information after NTP sync to WebUI #1040
## 0.7.19 - 2023-07-27
* next attempt to fix yield day for multiple inverters #1016
* reduced threshold for inverter state machine from 60min to 15min to go from state `WAS_ON` to `OFF`
## 0.7.18 - 2023-07-26
* next attempt to fix yield day for multiple inverters #1016
## 0.7.17 - 2023-07-25
* next attempt to fix yield day for multiple inverters #1016
* added two more states for the inverter status (also docu)
## 0.7.16 - 2023-07-24
* next attempt to fix yield day for multiple inverters #1016
* fix export settings date #1040
* fix time on WebUI (timezone was not observed) #913 #1016
## 0.7.15 - 2023-07-23
* add NTP sync interval #1019
* adjusted range of contrast / luminance setting #1041
* use only ISO time format in Web-UI #913
## 0.7.14 - 2023-07-23
* fix Contrast for Nokia Display #1041
* attempt to fix #1016 by improving inverter status
* added option to adjust effiency for yield (day/total) #1028
## 0.7.13 - 2023-07-19
* merged display PR #1027
* add date, time and version to export json #1024
## 0.7.12 - 2023-07-09
* added inverter status - state-machine #1016
## 0.7.11 - 2023-07-09
* fix MqTT endless loop #1013
## 0.7.10 - 2023-07-08
* fix MqTT endless loop #1013
## 0.7.9 - 2023-07-08
* added 'improve' functions to set wifi password directly with ESP web tools #1014
* fixed MqTT publish while appling power limit #1013
* slightly improved HMT live view (Voltage & Current)
## 0.7.8 - 2023-07-05
* fix `YieldDay`, `YieldTotal` and `P_AC` in `TotalValues` #929
* fix some serial debug prints
* merge PR #1005 which fixes issue #889
* merge homeassistant PR #963
* merge PR #890 which gives option for scheduled reboot at midnight (default off)
## 0.7.7 - 2023-07-03
* attempt to fix MqTT `YieldDay` in `TotalValues` #927
* attempt to fix MqTT `YieldDay` and `YieldTotal` even if inverters are not completly available #929
* fix wrong message 'NRF not connected' if it is disabled #1007
## 0.7.6 - 2023-06-17
* fix display of hidden SSID checkbox
* changed yield correction data type to `double`, now decimal places are supported
* corrected name of 0.91" display in settings
* attempt to fix MqTT zero values only if setting is there #980, #957
* made AP password configurable #951
* added option to start without time-sync, eg. for AP-only-mode #951
## 0.7.5 - 2023-06-16
* fix yield day reset on midnight #957
* improved tickers in `app.cpp`
## 0.7.4 - 2023-06-15
* fix MqTT `P_AC` send if inverters are available #987
* fix assignments for HMS 1CH and 2CH devices
* fixed uptime overflow #990
## 0.7.3 - 2023-06-09
* fix hidden SSID scan #983
* improved NRF24 missing message on home screen #981
* fix MqTT publishing only updated values #982
## 0.7.2 - 2023-06-08
* fix HMS-800 and HMS-1000 assignments #981
* make nrf enabled all the time for ESP8266
* fix menu item `active` highlight for 'API' and 'Doku'
* fix MqTT totals issue #927, #980
* reduce maximum number of inverters to 4 for ESP8266, increase to 16 for ESP32
## 0.7.1 - 2023-06-05
* enabled power limit control for HMS / HMT devices
* changed NRF24 lib version back to 1.4.5 because of compile problems for EPS8266
## 0.7.0 - 2023-06-04
* HMS / HMT support for ESP32 devices
## 0.6.15 - 2023-05-25
* improved Prometheus Endpoint PR #958
* fix turn off ePaper only if setting was set #956

303
src/app.cpp

@ -23,12 +23,12 @@ void app::setup() {
while (!Serial)
yield();
ah::Scheduler::setup();
resetSystem();
mSettings.setup();
mSettings.getPtr(mConfig);
ah::Scheduler::setup(mConfig->inst.startWithoutTime);
DPRINT(DBG_INFO, F("Settings valid: "));
DSERIAL.flush();
if (mSettings.getValid())
@ -36,8 +36,16 @@ void app::setup() {
else
DBGPRINTLN(F("false"));
mSys.enableDebug();
mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
if(mConfig->nrf.enabled) {
mNrfRadio.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
mNrfRadio.enableDebug();
}
#if defined(ESP32)
if(mConfig->cmt.enabled) {
mCmtRadio.setup(mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false);
mCmtRadio.enableDebug();
}
#endif
#ifdef ETHERNET
delay(1000);
DPRINT(DBG_INFO, F("mEth setup..."));
@ -62,29 +70,34 @@ void app::setup() {
#endif
#endif /* defined(ETHERNET) */
mSys.setup(&mTimestamp);
mSys.addInverters(&mConfig->inst);
mPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp);
if(mConfig->nrf.enabled) {
mPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp);
mPayload.enableSerialDebug(mConfig->serial.debug);
mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1));
mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp);
mMiPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp);
mMiPayload.enableSerialDebug(mConfig->serial.debug);
mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1));
mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
}
// DBGPRINTLN("--- after payload");
// DBGPRINTLN(String(ESP.getFreeHeap()));
// DBGPRINTLN(String(ESP.getHeapFragmentation()));
// DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));
#if defined(ESP32)
mHmsPayload.setup(this, &mSys, &mCmtRadio, &mStat, 5, &mTimestamp);
mHmsPayload.enableSerialDebug(mConfig->serial.debug);
mHmsPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
#endif
if (!mSys.Radio.isChipConnected())
if(mConfig->nrf.enabled) {
if (!mNrfRadio.isChipConnected())
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
// when WiFi is in client mode, then enable mqtt broker
#if !defined(AP_ONLY)
mMqttEnabled = (mConfig->mqtt.broker[0] > 0);
if (mMqttEnabled) {
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp);
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
mMiPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
@ -95,7 +108,7 @@ void app::setup() {
mWeb.setup(this, &mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig);
mApi.setup(this, &mSys, &mNrfRadio, mWeb.getWebSrvPtr(), mConfig);
// Plugins
if (mConfig->plugin.display.type != 0)
@ -103,28 +116,29 @@ void app::setup() {
mPubSerial.setup(mConfig, &mSys, &mTimestamp);
regularTickers();
#if !defined(ETHERNET)
mImprov.setup(this, mConfig->sys.deviceName, mVersion);
#endif
// DBGPRINTLN("--- end setup");
// DBGPRINTLN(String(ESP.getFreeHeap()));
// DBGPRINTLN(String(ESP.getHeapFragmentation()));
// DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));
regularTickers();
}
//-----------------------------------------------------------------------------
void app::loop(void) {
if (mInnerLoopCb)
mInnerLoopCb();
#if !defined(ETHERNET)
mImprov.tickSerial();
#endif
}
//-----------------------------------------------------------------------------
void app::loopStandard(void) {
ah::Scheduler::loop();
if (mSys.Radio.loop()) {
while (!mSys.Radio.mBufCtrl.empty()) {
packet_t *p = &mSys.Radio.mBufCtrl.front();
if (mNrfRadio.loop() && mConfig->nrf.enabled) {
while (!mNrfRadio.mBufCtrl.empty()) {
packet_t *p = &mNrfRadio.mBufCtrl.front();
if (mConfig->serial.debug) {
DPRINT(DBG_INFO, F("RX "));
@ -132,7 +146,7 @@ void app::loopStandard(void) {
DBGPRINT(F("B Ch"));
DBGPRINT(String(p->ch));
DBGPRINT(F(" | "));
mSys.Radio.dumpBuf(p->packet, p->len);
ah::dumpBuf(p->packet, p->len);
}
mStat.frmCnt++;
@ -143,14 +157,42 @@ void app::loopStandard(void) {
else
mMiPayload.add(iv, p);
}
mSys.Radio.mBufCtrl.pop();
mNrfRadio.mBufCtrl.pop();
yield();
}
mPayload.process(true);
mMiPayload.process(true);
}
#if defined(ESP32)
if (mCmtRadio.loop() && mConfig->cmt.enabled) {
while (!mCmtRadio.mBufCtrl.empty()) {
hmsPacket_t *p = &mCmtRadio.mBufCtrl.front();
if (mConfig->serial.debug) {
DPRINT(DBG_INFO, F("RX "));
DBGPRINT(String(p->data[0]));
DBGPRINT(F(" RSSI "));
DBGPRINT(String(p->rssi));
DBGPRINT(F("dBm | "));
ah::dumpBuf(&p->data[1], p->data[0]);
}
mStat.frmCnt++;
Inverter<> *iv = mSys.findInverter(&p->data[2]);
if(NULL != iv) {
if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT))
mHmsPayload.add(iv, p);
}
mCmtRadio.mBufCtrl.pop();
yield();
}
mHmsPayload.process(false); //true
}
#endif
mPayload.loop();
mMiPayload.loop();
#if defined(ESP32)
mHmsPayload.loop();
#endif
if (mMqttEnabled)
mMqtt.loop();
@ -171,6 +213,10 @@ void app::onNetwork(bool gotIp) {
regularTickers(); // reinstall regular tickers
if (gotIp) {
every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend");
#if defined(ESP32)
if(mConfig->cmt.enabled)
everySec(std::bind(&CmtRadioType::tickSecond, &mCmtRadio), "tsCmt");
#endif
mMqttReconnect = true;
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
@ -199,35 +245,10 @@ void app::regularTickers(void) {
if (mConfig->plugin.display.type != 0)
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart");
//everySec(std::bind(&Improv::tickSerial, &mImprov), "impro");
// every([this]() {mPayload.simulation();}, 15, "simul");
}
//-----------------------------------------------------------------------------
void app::tickNtpUpdate(void) {
uint32_t nxtTrig = 5; // default: check again in 5 sec
bool isOK = false;
#if defined(ETHERNET)
if (!(isOK = mEth.updateNtpTime()))
once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
#else /* defined(ETHERNET) */
isOK = mWifi.getNtpTime();
if (isOK || mTimestamp != 0) {
this->updateNtp();
nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min
}
once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
#endif /* defined(ETHERNET) */
// immediately start communicating
// @TODO: leads to reboot loops? not sure #674
if (isOK && mSendFirst) {
mSendFirst = false;
once(std::bind(&app::tickSend, this), 2, "senOn");
}
}
#if defined(ETHERNET)
void app::onNtpUpdate(bool gotTime)
{
@ -242,30 +263,54 @@ void app::onNtpUpdate(bool gotTime)
//-----------------------------------------------------------------------------
void app::updateNtp(void) {
if (mMqttReconnect && mMqttEnabled) {
mMqtt.tickerSecond();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
}
if (mMqttReconnect && mMqttEnabled) {
mMqtt.tickerSecond();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
}
// only install schedulers once even if NTP wasn't successful in first loop
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
if (mConfig->inst.rstValsNotAvail)
everyMin(std::bind(&app::tickMinute, this), "tMin");
if (mConfig->inst.rstYieldMidNight) {
uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
}
// only install schedulers once even if NTP wasn't successful in first loop
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
if (mConfig->inst.rstValsNotAvail)
everyMin(std::bind(&app::tickMinute, this), "tMin");
if (mConfig->inst.rstYieldMidNight) {
uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
}
}
if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) {
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise();
if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) {
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise();
}
mMqttReconnect = false;
}
//-----------------------------------------------------------------------------
void app::tickNtpUpdate(void) {
uint32_t nxtTrig = 5; // default: check again in 5 sec
#if defined(ETHERNET)
bool isOK = mEth.updateNtpTime();
#else
bool isOK = mWifi.getNtpTime();
#endif
if (isOK || mTimestamp != 0) {
this->updateNtp();
nxtTrig = isOK ? (mConfig->ntp.interval * 60) : 60; // depending on NTP update success check again in 12h (depends on setting) or in 1 min
// immediately start communicating
if (isOK && mSendFirst) {
mSendFirst = false;
once(std::bind(&app::tickSend, this), 2, "senOn");
}
mMqttReconnect = false;
}
once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
}
//-----------------------------------------------------------------------------
void app::tickCalcSunrise(void) {
@ -324,43 +369,15 @@ void app::tickComm(void) {
//-----------------------------------------------------------------------------
void app::tickZeroValues(void) {
Inverter<> *iv;
bool changed = false;
// set values to zero, except yields
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
mPayload.zeroInverterValues(iv);
changed = true;
zeroIvValues(!CHECK_AVAIL, SKIP_YIELD_DAY);
}
if(changed)
payloadEventListener(RealTimeRunData_Debug);
}
//-----------------------------------------------------------------------------
void app::tickMinute(void) {
// only triggered if 'reset values on no avail is enabled'
Inverter<> *iv;
bool changed = false;
// set values to zero, except yields
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) {
mPayload.zeroInverterValues(iv);
changed = true;
zeroIvValues(CHECK_AVAIL, SKIP_YIELD_DAY);
}
}
if(changed)
payloadEventListener(RealTimeRunData_Debug);
}
//-----------------------------------------------------------------------------
void app::tickMidnight(void) {
@ -369,20 +386,7 @@ void app::tickMidnight(void) {
uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2");
Inverter<> *iv;
bool changed = false;
// set values to zero, except yield total
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
mPayload.zeroInverterValues(iv, false);
changed = true;
}
if(changed)
payloadEventListener(RealTimeRunData_Debug);
zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY);
if (mMqttEnabled)
mMqtt.tickerMidnight();
@ -390,17 +394,27 @@ void app::tickMidnight(void) {
//-----------------------------------------------------------------------------
void app::tickSend(void) {
if (!mSys.Radio.isChipConnected()) {
if(mConfig->nrf.enabled) {
if(!mNrfRadio.isChipConnected()) {
DPRINTLN(DBG_WARN, F("NRF24 not connected!"));
return;
}
}
if (mIVCommunicationOn) {
if (!mSys.Radio.mBufCtrl.empty()) {
if (!mNrfRadio.mBufCtrl.empty()) {
if (mConfig->serial.debug) {
DPRINT(DBG_DEBUG, F("recbuf not empty! #"));
DBGPRINTLN(String(mSys.Radio.mBufCtrl.size()));
DBGPRINTLN(String(mNrfRadio.mBufCtrl.size()));
}
}
#if defined(ESP32)
if (!mCmtRadio.mBufCtrl.empty()) {
if (mConfig->serial.debug) {
DPRINT(DBG_INFO, F("recbuf not empty! #"));
DBGPRINTLN(String(mCmtRadio.mBufCtrl.size()));
}
}
#endif
int8_t maxLoop = MAX_NUM_INVERTERS;
Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId);
@ -411,11 +425,19 @@ void app::tickSend(void) {
if (NULL != iv) {
if (iv->config->enabled) {
if(mConfig->nrf.enabled) {
if (iv->ivGen == IV_HM)
mPayload.ivSend(iv);
else
else if(iv->ivGen == IV_MI)
mMiPayload.ivSend(iv);
}
#if defined(ESP32)
if(mConfig->cmt.enabled) {
if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT))
mHmsPayload.ivSend(iv);
}
#endif
}
}
} else {
if (mConfig->serial.debug)
@ -426,6 +448,51 @@ void app::tickSend(void) {
updateLed();
}
//-----------------------------------------------------------------------------
void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
Inverter<> *iv;
bool changed = false;
// set values to zero, except yields
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
if (!iv->config->enabled)
continue; // skip to next inverter
if (checkAvail) {
if (!iv->isAvailable())
continue;
}
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t ch = 0; ch <= iv->channels; ch++) {
uint8_t pos = 0;
for(uint8_t fld = 0; fld < FLD_EVT; fld++) {
switch(fld) {
case FLD_YD:
if(skipYieldDay)
continue;
else
break;
case FLD_YT:
continue;
}
pos = iv->getPosByChFld(ch, fld, rec);
iv->setValue(pos, rec, 0.0f);
}
iv->doCalculations();
}
changed = true;
}
if(changed) {
if(mMqttEnabled && !skipYieldDay)
mMqtt.setZeroValuesEnable();
payloadEventListener(RealTimeRunData_Debug, NULL);
}
}
//-----------------------------------------------------------------------------
void app::resetSystem(void) {
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
@ -477,7 +544,7 @@ void app::updateLed(void) {
if (mConfig->led.led0 != 0xff) {
Inverter<> *iv = mSys.getInverterByPos(0);
if (NULL != iv) {
if (iv->isProducing(mTimestamp))
if (iv->isProducing())
digitalWrite(mConfig->led.led0, led_on);
else
digitalWrite(mConfig->led.led0, led_off);

85
src/app.h

@ -9,19 +9,23 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <RF24.h>
#include <RF24_config.h>
#include "appInterface.h"
#include "config/settings.h"
#include "defines.h"
#include "hm/hmPayload.h"
#include "hm/hmSystem.h"
#include "hm/hmRadio.h"
#include "hms/hmsRadio.h"
#include "hms/hmsPayload.h"
#include "hm/hmPayload.h"
#include "hm/miPayload.h"
#include "publisher/pubMqtt.h"
#include "publisher/pubSerial.h"
#include "utils/crc.h"
#include "utils/dbg.h"
#include "utils/scheduler.h"
#include "utils/improv.h"
#include "web/RestApi.h"
#include "web/web.h"
#if defined(ETHERNET)
@ -37,10 +41,14 @@
#define ACOS(x) (degrees(acos(x)))
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
typedef HmPayload<HmSystemType> PayloadType;
typedef MiPayload<HmSystemType> MiPayloadType;
typedef HmPayload<HmSystemType, HmRadio<>> PayloadType;
typedef MiPayload<HmSystemType, HmRadio<>> MiPayloadType;
#ifdef ESP32
typedef CmtRadio<esp32_3wSpi<>> CmtRadioType;
typedef HmsPayload<HmSystemType, CmtRadioType> HmsPayloadType;
#endif
typedef Web<HmSystemType> WebType;
typedef RestApi<HmSystemType> RestApiType;
typedef RestApi<HmSystemType, HmRadio<>> RestApiType;
typedef PubMqtt<HmSystemType> PubMqttType;
typedef PubSerial<HmSystemType> PubSerialType;
@ -63,8 +71,14 @@ class app : public IApp, public ah::Scheduler {
void regularTickers(void);
void handleIntr(void) {
mSys.Radio.handleIntr();
mNrfRadio.handleIntr();
}
#ifdef ESP32
void handleHmsIntr(void) {
mCmtRadio.handleIntr();
}
#endif
uint32_t getUptime() {
return Scheduler::getUptime();
@ -78,6 +92,10 @@ class app : public IApp, public ah::Scheduler {
mShowRebootRequest = true; // only message on index, no reboot
mSavePending = true;
mSaveReboot = reboot;
if(reboot) {
onNetwork(false);
ah::Scheduler::resetTicker();
}
once(std::bind(&app::tickSave, this), 3, "save");
return true;
}
@ -111,8 +129,8 @@ class app : public IApp, public ah::Scheduler {
mWifi.scanAvailNetworks();
}
void getAvailNetworks(JsonObject obj) {
mWifi.getAvailNetworks(obj);
bool getAvailNetworks(JsonObject obj) {
return mWifi.getAvailNetworks(obj);
}
void setOnUpdate() {
@ -177,10 +195,27 @@ class app : public IApp, public ah::Scheduler {
return mWeb.isProtected(request);
}
uint8_t getIrqPin(void) {
void getNrfRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) {
*sendCnt = mNrfRadio.mSendCnt;
*retransmits = mNrfRadio.mRetransmits;
}
bool getNrfEnabled(void) {
return mConfig->nrf.enabled;
}
bool getCmtEnabled(void) {
return mConfig->cmt.enabled;
}
uint8_t getNrfIrqPin(void) {
return mConfig->nrf.pinIrq;
}
uint8_t getCmtIrqPin(void) {
return mConfig->cmt.pinIrq;
}
String getTimeStr(uint32_t offset = 0) {
char str[10];
if(0 == mTimestamp)
@ -217,17 +252,19 @@ class app : public IApp, public ah::Scheduler {
Scheduler::setTimestamp(newTime);
}
HmSystemType mSys;
private:
#define CHECK_AVAIL true
#define SKIP_YIELD_DAY true
typedef std::function<void()> innerLoopCb;
void resetSystem(void);
void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true);
void payloadEventListener(uint8_t cmd) {
void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
#if !defined(AP_ONLY)
if (mMqttEnabled)
mMqtt.payloadEventListener(cmd);
mMqtt.payloadEventListener(cmd, iv);
#endif
if(mConfig->plugin.display.type != 0)
mDisplay.payloadEventListener(cmd);
@ -270,23 +307,12 @@ class app : public IApp, public ah::Scheduler {
void tickMinute(void);
void tickZeroValues(void);
void tickMidnight(void);
/*void tickSerial(void) {
if(Serial.available() == 0)
return;
uint8_t buf[80];
uint8_t len = Serial.readBytes(buf, 80);
DPRINTLN(DBG_INFO, "got serial data, len: " + String(len));
for(uint8_t i = 0; i < len; i++) {
if((0 != i) && (i % 8 == 0))
DBGPRINTLN("");
DBGPRINT(String(buf[i], HEX) + " ");
}
DBGPRINTLN("");
}*/
innerLoopCb mInnerLoopCb;
HmSystemType mSys;
HmRadio<> mNrfRadio;
bool mShowRebootRequest;
bool mIVCommunicationOn;
@ -300,6 +326,13 @@ class app : public IApp, public ah::Scheduler {
PayloadType mPayload;
MiPayloadType mMiPayload;
PubSerialType mPubSerial;
#if !defined(ETHERNET)
Improv mImprov;
#endif
#ifdef ESP32
CmtRadioType mCmtRadio;
HmsPayloadType mHmsPayload;
#endif
char mVersion[12];
settings mSettings;

5
src/appInterface.h

@ -34,7 +34,7 @@ class IApp {
#if !defined(ETHERNET)
virtual void scanAvailNetworks() = 0;
virtual void getAvailNetworks(JsonObject obj) = 0;
virtual bool getAvailNetworks(JsonObject obj) = 0;
#endif /* defined(ETHERNET) */
virtual uint32_t getUptime() = 0;
@ -59,6 +59,9 @@ class IApp {
virtual uint32_t getMqttTxCnt() = 0;
virtual bool getProtection(AsyncWebServerRequest *request) = 0;
virtual void getNrfRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) = 0;
//virtual void getCmtRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) = 0;
};
#endif /*__IAPP_H__*/

11
src/config/config.h

@ -84,7 +84,11 @@
#define PACKET_BUFFER_SIZE 30
// number of configurable inverters
#define MAX_NUM_INVERTERS 10
#if defined(ESP32)
#define MAX_NUM_INVERTERS 16
#else
#define MAX_NUM_INVERTERS 4
#endif
// default serial interval
#define SERIAL_INTERVAL 5
@ -105,7 +109,10 @@
#define DEF_MAX_RETRANS_PER_PYLD 5
// number of seconds since last successful response, before inverter is marked inactive
#define INACT_THRES_SEC 300
#define INVERTER_INACT_THRES_SEC 5*60
// number of seconds since last successful response, before inverter is marked offline
#define INVERTER_OFF_THRES_SEC 15*60
// threshold of minimum power on which the inverter is marked as inactive
#define INACT_PWR_THRESH 3

87
src/config/settings.h

@ -64,6 +64,7 @@ typedef struct {
char adminPwd[PWD_LEN];
uint16_t protectionMask;
bool darkMode;
bool schedReboot;
#if defined(ETHERNET)
// ethernet
@ -72,12 +73,15 @@ typedef struct {
// wifi
char stationSsid[SSID_LEN];
char stationPwd[PWD_LEN];
char apPwd[PWD_LEN];
bool isHidden;
#endif /* defined(ETHERNET) */
cfgIp_t ip;
} cfgSys_t;
typedef struct {
bool enabled;
uint16_t sendInterval;
uint8_t maxRetransPerPyld;
uint8_t pinCs;
@ -89,9 +93,17 @@ typedef struct {
uint8_t amplifierPower;
} cfgNrf24_t;
typedef struct {
bool enabled;
uint8_t pinCsb;
uint8_t pinFcsb;
uint8_t pinIrq;
} cfgCmt_t;
typedef struct {
char addr[NTP_ADDR_LEN];
uint16_t port;
uint16_t interval; // in minutes
} cfgNtp_t;
typedef struct {
@ -126,9 +138,9 @@ typedef struct {
bool enabled;
char name[MAX_NAME_LENGTH];
serial_u serial;
uint16_t chMaxPwr[4];
int32_t yieldCor[4]; // signed YieldTotal correction value
char chName[4][MAX_NAME_LENGTH];
uint16_t chMaxPwr[6];
double yieldCor[6]; // YieldTotal correction value
char chName[6][MAX_NAME_LENGTH];
} cfgIv_t;
typedef struct {
@ -138,6 +150,8 @@ typedef struct {
bool rstYieldMidNight;
bool rstValsNotAvail;
bool rstValsCommStop;
bool startWithoutTime;
float yieldEffiency;
} cfgInst_t;
typedef struct {
@ -163,6 +177,7 @@ typedef struct {
typedef struct {
cfgSys_t sys;
cfgNrf24_t nrf;
cfgCmt_t cmt;
cfgNtp_t ntp;
cfgSun_t sun;
cfgSerial_t serial;
@ -257,6 +272,9 @@ class settings {
mCfg.valid = true;
if(root.containsKey(F("wifi"))) jsonNetwork(root[F("wifi")]);
if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]);
#if defined(ESP32)
if(root.containsKey(F("cmt"))) jsonCmt(root[F("cmt")]);
#endif
if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]);
if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]);
if(root.containsKey(F("serial"))) jsonSerial(root[F("serial")]);
@ -281,6 +299,9 @@ class settings {
JsonObject root = json.to<JsonObject>();
jsonNetwork(root.createNestedObject(F("wifi")), true);
jsonNrf(root.createNestedObject(F("nrf")), true);
#if defined(ESP32)
jsonCmt(root.createNestedObject(F("cmt")), true);
#endif
jsonNtp(root.createNestedObject(F("ntp")), true);
jsonSun(root.createNestedObject(F("sun")), true);
jsonSerial(root.createNestedObject(F("serial")), true);
@ -343,7 +364,7 @@ class settings {
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
mCfg.sys.darkMode = false;
mCfg.sys.schedReboot = false;
// restore temp settings
#if defined(ETHERNET)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
@ -353,6 +374,8 @@ class settings {
else {
snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID);
snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD);
snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD);
mCfg.sys.isHidden = false;
}
#endif /* defined(ETHERNET) */
@ -368,9 +391,16 @@ class settings {
mCfg.nrf.pinSclk = DEF_SCLK_PIN;
mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03;
mCfg.nrf.enabled = true;
mCfg.cmt.pinCsb = DEF_PIN_OFF;
mCfg.cmt.pinFcsb = DEF_PIN_OFF;
mCfg.cmt.pinIrq = DEF_PIN_OFF;
mCfg.cmt.enabled = false;
snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME);
mCfg.ntp.port = DEF_NTP_PORT;
mCfg.ntp.interval = 720;
mCfg.sun.lat = 0.0;
mCfg.sun.lon = 0.0;
@ -391,6 +421,8 @@ class settings {
mCfg.inst.rstYieldMidNight = false;
mCfg.inst.rstValsNotAvail = false;
mCfg.inst.rstValsCommStop = false;
mCfg.inst.startWithoutTime = false;
mCfg.inst.yieldEffiency = 0.955f;
mCfg.led.led0 = DEF_PIN_OFF;
mCfg.led.led1 = DEF_PIN_OFF;
@ -416,11 +448,14 @@ class settings {
#if !defined(ETHERNET)
obj[F("ssid")] = mCfg.sys.stationSsid;
obj[F("pwd")] = mCfg.sys.stationPwd;
obj[F("ap_pwd")] = mCfg.sys.apPwd;
obj[F("hidd")] = (bool) mCfg.sys.isHidden;
#endif /* !defined(ETHERNET) */
obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd;
obj[F("prot_mask")] = mCfg.sys.protectionMask;
obj[F("dark")] = mCfg.sys.darkMode;
obj[F("reb")] = (bool) mCfg.sys.schedReboot;
ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf);
ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf);
ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
@ -430,11 +465,14 @@ class settings {
#if !defined(ETHERNET)
getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN);
getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN);
getChar(obj, F("ap_pwd"), mCfg.sys.apPwd, PWD_LEN);
getVal<bool>(obj, F("hidd"), &mCfg.sys.isHidden);
#endif /* !defined(ETHERNET) */
getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN);
getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN);
getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
getVal<bool>(obj, F("dark"), &mCfg.sys.darkMode);
getVal<bool>(obj, F("reb"), &mCfg.sys.schedReboot);
if(obj.containsKey(F("ip"))) ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
if(obj.containsKey(F("mask"))) ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
@ -458,6 +496,7 @@ class settings {
obj[F("mosi")] = mCfg.nrf.pinMosi;
obj[F("miso")] = mCfg.nrf.pinMiso;
obj[F("pwr")] = mCfg.nrf.amplifierPower;
obj[F("en")] = (bool) mCfg.nrf.enabled;
} else {
getVal<uint16_t>(obj, F("intvl"), &mCfg.nrf.sendInterval);
getVal<uint8_t>(obj, F("maxRetry"), &mCfg.nrf.maxRetransPerPyld);
@ -468,6 +507,11 @@ class settings {
getVal<uint8_t>(obj, F("mosi"), &mCfg.nrf.pinMosi);
getVal<uint8_t>(obj, F("miso"), &mCfg.nrf.pinMiso);
getVal<uint8_t>(obj, F("pwr"), &mCfg.nrf.amplifierPower);
#if !defined(ESP32)
mCfg.nrf.enabled = true; // ESP8266, read always as enabled
#else
mCfg.nrf.enabled = (bool) obj[F("en")];
#endif
if((obj[F("cs")] == obj[F("ce")])) {
mCfg.nrf.pinCs = DEF_CS_PIN;
mCfg.nrf.pinCe = DEF_CE_PIN;
@ -479,13 +523,32 @@ class settings {
}
}
void jsonCmt(JsonObject obj, bool set = false) {
if(set) {
obj[F("csb")] = mCfg.cmt.pinCsb;
obj[F("fcsb")] = mCfg.cmt.pinFcsb;
obj[F("irq")] = mCfg.cmt.pinIrq;
obj[F("en")] = (bool) mCfg.cmt.enabled;
} else {
mCfg.cmt.pinCsb = obj[F("csb")];
mCfg.cmt.pinFcsb = obj[F("fcsb")];
mCfg.cmt.pinIrq = obj[F("irq")];
mCfg.cmt.enabled = (bool) obj[F("en")];
}
}
void jsonNtp(JsonObject obj, bool set = false) {
if(set) {
obj[F("addr")] = mCfg.ntp.addr;
obj[F("port")] = mCfg.ntp.port;
obj[F("intvl")] = mCfg.ntp.interval;
} else {
getChar(obj, F("addr"), mCfg.ntp.addr, NTP_ADDR_LEN);
getVal<uint16_t>(obj, F("port"), &mCfg.ntp.port);
getVal<uint16_t>(obj, F("intvl"), &mCfg.ntp.interval);
if(mCfg.ntp.interval < 5) // minimum 5 minutes
mCfg.ntp.interval = 720; // default -> 12 hours
}
}
@ -586,12 +649,21 @@ class settings {
obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight;
obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop;
obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime;
obj[F("yldEff")] = mCfg.inst.yieldEffiency;
}
else {
getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight);
getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop);
getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime);
getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency);
if(mCfg.inst.yieldEffiency < 0.5)
mCfg.inst.yieldEffiency = 1.0f;
else if(mCfg.inst.yieldEffiency > 1.0f)
mCfg.inst.yieldEffiency = 1.0f;
}
JsonArray ivArr;
@ -611,7 +683,7 @@ class settings {
obj[F("en")] = (bool)cfg->enabled;
obj[F("name")] = cfg->name;
obj[F("sn")] = cfg->serial.u64;
for(uint8_t i = 0; i < 4; i++) {
for(uint8_t i = 0; i < 6; i++) {
obj[F("yield")][i] = cfg->yieldCor[i];
obj[F("pwr")][i] = cfg->chMaxPwr[i];
obj[F("chName")][i] = cfg->chName[i];
@ -620,7 +692,10 @@ class settings {
getVal<bool>(obj, F("en"), &cfg->enabled);
getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH);
getVal<uint64_t>(obj, F("sn"), &cfg->serial.u64);
for(uint8_t i = 0; i < 4; i++) {
uint8_t size = 4;
if(obj.containsKey(F("pwr")))
size = obj[F("pwr")].size();
for(uint8_t i = 0; i < size; i++) {
if(obj.containsKey(F("yield"))) cfg->yieldCor[i] = obj[F("yield")][i];
if(obj.containsKey(F("pwr"))) cfg->chMaxPwr[i] = obj[F("pwr")][i];
if(obj.containsKey(F("chName"))) snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as<const char*>());

8
src/defines.h

@ -12,8 +12,8 @@
// VERSION
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 6
#define VERSION_PATCH 15
#define VERSION_MINOR 7
#define VERSION_PATCH 21
//-------------------------------------
typedef struct {
@ -74,10 +74,6 @@ union serial_u {
#define MIN_MQTT_INTERVAL 60
#define MQTT_STATUS_NOT_AVAIL_NOT_PROD 0
#define MQTT_STATUS_AVAIL_NOT_PROD 1
#define MQTT_STATUS_AVAIL_PROD 2
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
//-------------------------------------

19
src/hm/hmDefines.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
@ -10,7 +10,7 @@
#include <cstdint>
// inverter generations
enum {IV_HM = 0, IV_MI};
enum {IV_HM = 0, IV_MI, IV_HMS, IV_HMT};
// units
enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VAR, UNIT_NONE};
@ -18,19 +18,22 @@ const char* const units[] = {"V", "A", "W", "Wh", "kWh", "Hz", "°C", "%", "var"
// field types
enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT,
FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_T, FLD_PF, FLD_EFF,
FLD_UAC, FLD_UAC_1N, FLD_UAC_2N, FLD_UAC_3N, FLD_UAC_12, FLD_UAC_23, FLD_UAC_31, FLD_IAC,
FLD_IAC_1, FLD_IAC_2, FLD_IAC_3, FLD_PAC, FLD_F, FLD_T, FLD_PF, FLD_EFF,
FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR,
FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_HW_ID,
FLD_ACT_ACTIVE_PWR_LIMIT, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE};
const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal",
"U_AC", "I_AC", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
"U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC",
"IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
"ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId",
"active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"};
const char* const notAvail = "n/a";
const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH,
UNIT_V, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR,
UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A,
UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR,
UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE};
// mqtt discovery device classes
@ -53,7 +56,7 @@ const byteAssign_fieldDeviceClass deviceFieldAssignment[] = {
{FLD_UAC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT},
{FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT},
{FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT},
{FLD_F, DEVICE_CLS_FREQ, STATE_CLS_NONE},
{FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT},
{FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT},
{FLD_PF, DEVICE_CLS_NONE, STATE_CLS_NONE},
{FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE},
@ -67,9 +70,9 @@ enum {CMD_CALC = 0xffff};
// CH0 is default channel (freq, ac, temp)
enum {CH0 = 0, CH1, CH2, CH3, CH4};
enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6};
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH};
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
typedef struct {

126
src/hm/hmInverter.h

@ -12,6 +12,7 @@
#endif
#include "hmDefines.h"
#include "../hms/hmsDefines.h"
#include <memory>
#include <queue>
#include "../config/settings.h"
@ -101,6 +102,13 @@ const calcFunc_t<T> calcFunctions[] = {
{ CALC_IRR_CH, &calcIrradiation }
};
enum class InverterStatus : uint8_t {
OFF,
STARTING,
PRODUCING,
WAS_PRODUCING,
WAS_ON
};
template <class REC_TYP>
class Inverter {
@ -122,6 +130,10 @@ class Inverter {
//String lastAlarmMsg;
bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null)
bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
InverterStatus status; // indicates the current inverter status
static uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup
Inverter() {
ivGen = IV_HM;
@ -134,6 +146,7 @@ class Inverter {
//lastAlarmMsg = "nothing";
alarmMesIndex = 0;
isConnected = false;
status = InverterStatus::OFF;
}
~Inverter() {
@ -265,11 +278,13 @@ class Inverter {
val <<= 8;
val |= buf[ptr];
} while(++ptr != end);
if (FLD_T == rec->assign[pos].fieldId) {
// temperature is a signed value!
rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div);
if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) {
// temperature, Qvar, and power factor are a signed values
rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div);
} else if (FLD_YT == rec->assign[pos].fieldId) {
rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
} else if (FLD_YD == rec->assign[pos].fieldId) {
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency;
} else {
if ((REC_TYP)(div) > 1)
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
@ -318,6 +333,9 @@ class Inverter {
}
else
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
// update status state-machine
isProducing();
}
/*inline REC_TYP getPowerLimit(void) {
@ -371,25 +389,42 @@ class Inverter {
}
}
bool isAvailable(uint32_t timestamp) {
if((timestamp - recordMeas.ts) < INACT_THRES_SEC)
return true;
if((timestamp - recordInfo.ts) < INACT_THRES_SEC)
return true;
if((timestamp - recordConfig.ts) < INACT_THRES_SEC)
return true;
if((timestamp - recordAlarm.ts) < INACT_THRES_SEC)
return true;
return false;
bool isAvailable() {
bool avail = false;
if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordConfig.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordAlarm.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if(avail) {
if(status < InverterStatus::PRODUCING)
status = InverterStatus::STARTING;
} else {
if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC)
status = InverterStatus::OFF;
else
status = InverterStatus::WAS_ON;
}
return avail;
}
bool isProducing(uint32_t timestamp) {
bool isProducing() {
bool producing = false;
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing"));
if(isAvailable(timestamp)) {
uint8_t pos = getPosByChFld(CH0, FLD_PAC, &recordMeas);
return (getValue(pos, &recordMeas) > INACT_PWR_THRESH);
if(isAvailable()) {
producing = (getChannelFieldValue(CH0, FLD_PAC, &recordMeas) > INACT_PWR_THRESH);
if(producing)
status = InverterStatus::PRODUCING;
else if(InverterStatus::PRODUCING == status)
status = InverterStatus::WAS_PRODUCING;
}
return false;
return producing;
}
uint16_t getFwVersion() {
@ -421,22 +456,46 @@ class Inverter {
switch (cmd) {
case RealTimeRunData_Debug:
if (INV_TYPE_1CH == type) {
rec->length = (uint8_t)(HM1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm1chAssignment;
rec->pyldLen = HM1CH_PAYLOAD_LEN;
channels = 1;
if(IV_HM == ivGen) {
rec->length = (uint8_t)(HM1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm1chAssignment;
rec->pyldLen = HM1CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hms1chAssignment;
rec->pyldLen = HMS1CH_PAYLOAD_LEN;
}
channels = 1;
}
else if (INV_TYPE_2CH == type) {
rec->length = (uint8_t)(HM2CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm2chAssignment;
rec->pyldLen = HM2CH_PAYLOAD_LEN;
channels = 2;
if(IV_HM == ivGen) {
rec->length = (uint8_t)(HM2CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm2chAssignment;
rec->pyldLen = HM2CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS2CH_LIST_LEN);
rec->assign = (byteAssign_t *)hms2chAssignment;
rec->pyldLen = HMS2CH_PAYLOAD_LEN;
}
channels = 2;
}
else if (INV_TYPE_4CH == type) {
rec->length = (uint8_t)(HM4CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm4chAssignment;
rec->pyldLen = HM4CH_PAYLOAD_LEN;
channels = 4;
if(IV_HM == ivGen) {
rec->length = (uint8_t)(HM4CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm4chAssignment;
rec->pyldLen = HM4CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS4CH_LIST_LEN);
rec->assign = (byteAssign_t *)hms4chAssignment;
rec->pyldLen = HMS4CH_PAYLOAD_LEN;
}
channels = 4;
}
else if (INV_TYPE_6CH == type) {
rec->length = (uint8_t)(HMT6CH_LIST_LEN);
rec->assign = (byteAssign_t *)hmt6chAssignment;
rec->pyldLen = HMT6CH_PAYLOAD_LEN;
channels = 6;
}
else {
rec->length = 0;
@ -580,6 +639,11 @@ class Inverter {
bool mDevControlRequest; // true if change needed
};
template <class REC_TYP>
uint32_t *Inverter<REC_TYP>::timestamp {0};
template <class REC_TYP>
cfgInst_t *Inverter<REC_TYP>::generalConfig {0};
/**
* To calculate values which are not transmitted by the unit there is a generic

55
src/hm/hmPayload.h

@ -9,6 +9,7 @@
#include "../utils/dbg.h"
#include "../utils/crc.h"
#include "../config/config.h"
#include "hmRadio.h"
#include <Arduino.h>
typedef struct {
@ -27,18 +28,19 @@ typedef struct {
} invPayload_t;
typedef std::function<void(uint8_t)> payloadListenerType;
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(uint16_t alarmCode, uint32_t start, uint32_t end)> alarmListenerType;
template<class HMSYSTEM>
template<class HMSYSTEM, class HMRADIO>
class HmPayload {
public:
HmPayload() {}
void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
mApp = app;
mSys = sys;
mRadio = radio;
mStat = stat;
mMaxRetrans = maxRetransmits;
mTimestamp = timestamp;
@ -93,28 +95,6 @@ class HmPayload {
notify(0x0b);
}*/
void zeroInverterValues(Inverter<> *iv, bool skipYieldDay = true) {
DPRINTLN(DBG_DEBUG, F("zeroInverterValues"));
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t ch = 0; ch <= iv->channels; ch++) {
uint8_t pos = 0;
for(uint8_t fld = 0; fld < FLD_EVT; fld++) {
switch(fld) {
case FLD_YD:
if(skipYieldDay)
continue;
else
break;
case FLD_YT:
continue;
}
pos = iv->getPosByChFld(ch, fld, rec);
iv->setValue(pos, rec, 0.0f);
}
iv->doCalculations();
}
}
void ivSendHighPrio(Inverter<> *iv) {
mHighPrioIv = iv;
}
@ -163,7 +143,7 @@ class HmPayload {
DBGPRINT(F(" power limit "));
DBGPRINTLN(String(iv->powerLimit[0]));
}
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false);
mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false);
mPayload[iv->id].txCmd = iv->devControlCmd;
//iv->clearCmdQueue();
//iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
@ -172,7 +152,7 @@ class HmPayload {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("prepareDevInformCmd 0x"));
DBGHEXLN(cmd);
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false);
mRadio->prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false);
mPayload[iv->id].txCmd = cmd;
}
}
@ -216,7 +196,7 @@ class HmPayload {
ok = false;
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("has "));
DBGPRINT(F(" has "));
if(!ok) DBGPRINT(F("not "));
DBGPRINT(F("accepted power limit set point "));
DBGPRINT(String(iv->powerLimit[0]));
@ -238,7 +218,7 @@ class HmPayload {
if (NULL == iv)
continue; // skip to next inverter
if (IV_MI == iv->ivGen) // only process HM inverters
if (IV_HM != iv->ivGen) // only process HM inverters
continue; // skip to next inverter
if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) {
@ -261,14 +241,14 @@ class HmPayload {
} else if(iv->devControlCmd == ActivePowerContr) {
DPRINT_IVID(DBG_INFO, iv->id);
DPRINTLN(DBG_INFO, F("retransmit power limit"));
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true);
mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true);
} else {
if(false == mPayload[iv->id].gotFragment) {
/*
DPRINTLN(DBG_WARN, F("nothing received: Request Complete Retransmit"));
mPayload[iv->id].txCmd = iv->getQueuedCmd();
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX));
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
*/
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("nothing received"));
@ -280,7 +260,7 @@ class HmPayload {
DBGPRINT(F("Frame "));
DBGPRINT(String(i + 1));
DBGPRINTLN(F(" missing: Request Retransmit"));
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true);
mRadio->sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true);
break; // only request retransmit one frame per loop
}
yield();
@ -297,7 +277,7 @@ class HmPayload {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("prepareDevInformCmd 0x"));
DBGHEXLN(mPayload[iv->id].txCmd);
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
}
} else { // payload complete
DPRINT(DBG_INFO, F("procPyld: cmd: 0x"));
@ -325,7 +305,7 @@ class HmPayload {
DPRINT(DBG_INFO, F("Payload ("));
DBGPRINT(String(payloadLen));
DBGPRINT(F("): "));
mSys->Radio.dumpBuf(payload, payloadLen);
ah::dumpBuf(payload, payloadLen);
}
if (NULL == rec) {
@ -340,7 +320,7 @@ class HmPayload {
yield();
}
iv->doCalculations();
notify(mPayload[iv->id].txCmd);
notify(mPayload[iv->id].txCmd, iv);
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
@ -370,9 +350,9 @@ class HmPayload {
}
private:
void notify(uint8_t val) {
void notify(uint8_t val, Inverter<> *iv) {
if(NULL != mCbPayload)
(mCbPayload)(val);
(mCbPayload)(val, iv);
}
void notify(uint16_t code, uint32_t start, uint32_t endTime) {
@ -425,6 +405,7 @@ class HmPayload {
IApp *mApp;
HMSYSTEM *mSys;
HMRADIO *mRadio;
statistics_t *mStat;
uint8_t mMaxRetrans;
uint32_t *mTimestamp;

56
src/hm/hmRadio.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __RADIO_H__
@ -24,27 +24,6 @@
const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
//-----------------------------------------------------------------------------
// MACROS
//-----------------------------------------------------------------------------
#define CP_U32_LittleEndian(buf, v) ({ \
uint8_t *b = buf; \
b[0] = ((v >> 24) & 0xff); \
b[1] = ((v >> 16) & 0xff); \
b[2] = ((v >> 8) & 0xff); \
b[3] = ((v ) & 0xff); \
})
#define CP_U32_BigEndian(buf, v) ({ \
uint8_t *b = buf; \
b[3] = ((v >> 24) & 0xff); \
b[2] = ((v >> 16) & 0xff); \
b[1] = ((v >> 8) & 0xff); \
b[0] = ((v ) & 0xff); \
})
#define BIT_CNT(x) ((x)<<3)
//-----------------------------------------------------------------------------
// HM Radio class
//-----------------------------------------------------------------------------
@ -53,7 +32,7 @@ class HmRadio {
public:
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) {
if(mSerialDebug) {
DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: "));
DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: "));
DBGPRINT(String(CE_PIN));
DBGPRINT(F(", CS_PIN: "));
DBGPRINT(String(CS_PIN));
@ -105,8 +84,8 @@ class HmRadio {
DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01;
#ifdef ESP32
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
mSpi = new SPIClass(FSPI);
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
mSpi = new SPIClass(HSPI);
#else
mSpi = new SPIClass(VSPI);
#endif
@ -242,7 +221,7 @@ class HmRadio {
mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff;
}
sendPacket(invId, 24, isRetransmit, true);
sendPacket(invId, 24, isRetransmit);
}
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) {
@ -250,15 +229,6 @@ class HmRadio {
sendPacket(invId, 10, isRetransmit, appendCrc16);
}
void dumpBuf(uint8_t buf[], uint8_t len) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf"));
for(uint8_t i = 0; i < len; i++) {
DHEX(buf[i]);
DBGPRINT(" ");
}
DBGPRINTLN("");
}
uint8_t getDataRate(void) {
if(!mNrf24.isChipConnected())
return 3; // unkown
@ -291,17 +261,17 @@ class HmRadio {
p.len = len;
mNrf24.read(p.packet, len);
if (p.packet[0] != 0x00) {
mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received
else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command
else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command
isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received
else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 &&
isLastPackage = true; // response from dev control command
isLastPackage = true; // response from dev control command
}
}
yield();
}
yield();
}
return isLastPackage;
}
@ -311,7 +281,7 @@ class HmRadio {
DHEX(mid);
DBGPRINT(F(" pid: "));
DBGHEXLN(pid);
}
}
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE);
mTxBuf[0] = mid; // message id
CP_U32_BigEndian(&mTxBuf[1], (invId >> 8));
@ -344,7 +314,7 @@ class HmRadio {
DBGPRINT("B Ch");
DBGPRINT(String(mRfChLst[mTxChIdx]));
DBGPRINT(F(" | "));
dumpBuf(mTxBuf, len);
ah::dumpBuf(mTxBuf, len);
}
mNrf24.stopListening();

60
src/hm/hmSystem.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
@ -7,37 +7,34 @@
#define __HM_SYSTEM_H__
#include "hmInverter.h"
#include "hmRadio.h"
template <uint8_t MAX_INVERTER=3, class INVERTERTYPE=Inverter<float>>
class HmSystem {
public:
HmRadio<> Radio;
HmSystem() {}
void setup() {
mNumInv = 0;
Radio.setup();
}
void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin, uint8_t sclkPin, uint8_t mosiPin, uint8_t misoPin) {
void setup(uint32_t *timestamp) {
mInverter[0].timestamp = timestamp;
mNumInv = 0;
Radio.setup(ampPwr, irqPin, cePin, csPin, sclkPin, mosiPin, misoPin);
}
void addInverters(cfgInst_t *config) {
mInverter[0].generalConfig = config;
Inverter<> *iv;
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = addInverter(&config->iv[i]);
if (0ULL != config->iv[i].serial.u64) {
if (NULL != iv) {
DPRINT(DBG_INFO, "added inverter ");
if(iv->config->serial.b[5] == 0x11)
DBGPRINT("HM");
else {
if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04)
DBGPRINT("HMS");
else
DBGPRINT("HM");
} else if(iv->config->serial.b[5] == 0x13)
DBGPRINT("HMT");
else
DBGPRINT(((iv->config->serial.b[4] & 0x03) == 0x01) ? " (2nd Gen) " : " (3rd Gen) ");
}
DBGPRINTLN(String(iv->config->serial.u64, HEX));
@ -61,25 +58,40 @@ class HmSystem {
DPRINTLN(DBG_VERBOSE, " " + String(p->config->serial.b[4], HEX));
if((p->config->serial.b[5] == 0x11) || (p->config->serial.b[5] == 0x10)) {
switch(p->config->serial.b[4]) {
case 0x24: // HMS-500
case 0x22:
case 0x21: p->type = INV_TYPE_1CH; break;
case 0x21: p->type = INV_TYPE_1CH;
break;
case 0x44: // HMS-1000
case 0x42:
case 0x41: p->type = INV_TYPE_2CH; break;
case 0x41: p->type = INV_TYPE_2CH;
break;
case 0x64: // HMS-2000
case 0x62:
case 0x61: p->type = INV_TYPE_4CH; break;
case 0x61: p->type = INV_TYPE_4CH;
break;
default:
DPRINTLN(DBG_ERROR, F("unknown inverter type"));
break;
}
if(p->config->serial.b[5] == 0x11)
p->ivGen = IV_HM;
if(p->config->serial.b[5] == 0x11) {
if((p->config->serial.b[4] & 0x0f) == 0x04)
p->ivGen = IV_HMS;
else
p->ivGen = IV_HM;
}
else if((p->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM
p->ivGen = IV_HM;
else // MI 2nd Gen
p->ivGen = IV_MI;
}
else if(p->config->serial.u64 != 0ULL)
} else if(p->config->serial.b[5] == 0x13) {
p->ivGen = IV_HMT;
p->type = INV_TYPE_6CH;
} else if(p->config->serial.u64 != 0ULL)
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
p->init();
@ -124,10 +136,6 @@ class HmSystem {
return MAX_NUM_INVERTERS;
}
void enableDebug() {
Radio.enableDebug();
}
private:
INVERTERTYPE mInverter[MAX_INVERTER];
uint8_t mNumInv;

99
src/hm/miPayload.h

@ -33,15 +33,15 @@ typedef struct {
} miPayload_t;
typedef std::function<void(uint8_t)> miPayloadListenerType;
typedef std::function<void(uint8_t, Inverter<> *)> miPayloadListenerType;
template<class HMSYSTEM>
template<class HMSYSTEM, class HMRADIO>
class MiPayload {
public:
MiPayload() {}
void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
mApp = app;
mSys = sys;
mStat = stat;
@ -90,21 +90,21 @@ class MiPayload {
DPRINT_IVID(DBG_INFO, iv->id);
if (!mPayload[iv->id].gotFragment) {
mStat->rxFailNoAnser++; // got nothing
if (mSerialDebug)
DBGPRINTLN(F("enqueued cmd failed/timeout"));
if (mSerialDebug)
DBGPRINTLN(F("enqueued cmd failed/timeout"));
} else {
mStat->rxFail++; // got "fragments" (part of the required messages)
// but no complete set of responses
if (mSerialDebug) {
if (mSerialDebug) {
DBGPRINT(F("no complete Payload received! (retransmits: "));
DBGPRINT(String(mPayload[iv->id].retransmits));
DBGPRINTLN(F(")"));
}
DBGPRINT(String(mPayload[iv->id].retransmits));
DBGPRINTLN(F(")"));
}
iv->setQueuedCmdFinished(); // command failed
}
iv->setQueuedCmdFinished(); // command failed
}
}
}
reset(iv->id);
mPayload[iv->id].requested = true;
@ -124,7 +124,7 @@ class MiPayload {
DBGPRINT(F(" power limit "));
DBGPRINTLN(String(iv->powerLimit[0]));
}
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false);
mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false);
mPayload[iv->id].txCmd = iv->devControlCmd;
mPayload[iv->id].limitrequested = true;
@ -148,10 +148,10 @@ class MiPayload {
if (cmd == 0x01 || cmd == SystemConfigPara ) { //0x1 and 0x05 for HM-types
cmd = 0x0f; // for MI, these seem to make part of the Polling the device software and hardware version number command
cmd2 = cmd == SystemConfigPara ? 0x01 : 0x00; //perhaps we can only try to get second frame?
mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false);
mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false);
} else {
//mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd);
mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false);
mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false);
};
mPayload[iv->id].txCmd = cmd;
@ -236,27 +236,27 @@ const byteAssign_t InfoAssignment[] = {
mPayload[iv->id].gotFragment = true;
if(mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DPRINT(DBG_INFO,F("HW_VER is "));
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25]));
DPRINT(DBG_INFO,F("HW_VER is "));
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25]));
}
} else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10
DPRINT_IVID(DBG_INFO, iv->id);
if ( p->packet[9] == 0x01 ) {
DBGPRINTLN(F("got 2nd frame (hw info)"));
DPRINT(DBG_INFO,F("HW_PartNo "));
DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13]));
DPRINT(DBG_INFO,F("HW_PartNo "));
DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13]));
mPayload[iv->id].gotFragment = true;
iv->setValue(iv->getPosByChFld(0, FLD_YT, rec), rec, (float) ((p->packet[20] << 8) + p->packet[21])/1);
if(mSerialDebug) {
DPRINT(DBG_INFO,F("HW_FB_TLmValue "));
DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15]));
DPRINT(DBG_INFO,F("HW_FB_ReSPRT "));
DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17]));
DPRINT(DBG_INFO,F("HW_GridSamp_ResValule "));
DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19]));
DPRINT(DBG_INFO,F("HW_FB_TLmValue "));
DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15]));
DPRINT(DBG_INFO,F("HW_FB_ReSPRT "));
DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17]));
DPRINT(DBG_INFO,F("HW_GridSamp_ResValule "));
DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19]));
DPRINT(DBG_INFO,F("HW_ECapValue "));
DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21]));
}
}
} else {
DBGPRINTLN(F("3rd gen. inverter!")); // see table in OpenDTU code, DevInfoParser.cpp devInfo[]
}
@ -348,7 +348,7 @@ const byteAssign_t InfoAssignment[] = {
DPRINT(DBG_INFO, F("Payload ("));
DBGPRINT(String(payloadLen));
DBGPRINT("): ");
mSys->Radio.dumpBuf(payload, payloadLen);
ah::dumpBuf(payload, payloadLen);
}
if (NULL == rec) {
@ -363,7 +363,7 @@ const byteAssign_t InfoAssignment[] = {
yield();
}
iv->doCalculations();
notify(mPayload[iv->id].txCmd);
notify(mPayload[iv->id].txCmd, iv);
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
@ -431,7 +431,7 @@ const byteAssign_t InfoAssignment[] = {
} else if(iv->devControlCmd == ActivePowerContr) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("retransmit power limit"));
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false);
mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false);
} else {
uint8_t cmd = mPayload[iv->id].txCmd;
if (mPayload[iv->id].retransmits < mMaxRetrans) {
@ -442,7 +442,7 @@ const byteAssign_t InfoAssignment[] = {
mPayload[iv->id].retransmits = mMaxRetrans;
} else if ( cmd == 0x0f ) {
//hard/firmware request
mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true, false);
mRadio->sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true, false);
//iv->setQueuedCmdFinished();
//cmd = iv->getQueuedCmd();
} else {
@ -479,7 +479,7 @@ const byteAssign_t InfoAssignment[] = {
}
DBGPRINT(F(" 0x"));
DBGHEXLN(cmd);
mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true, false);
mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd, true, false);
//mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd);
yield();
}
@ -497,7 +497,7 @@ const byteAssign_t InfoAssignment[] = {
DBGPRINT(F("prepareDevInformCmd 0x"));
DBGHEXLN(mPayload[iv->id].txCmd);
//mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
mSys->Radio.sendCmdPacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].txCmd, false, false);
mRadio->sendCmdPacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].txCmd, false, false);
}
}
/*else { // payload complete
@ -556,9 +556,9 @@ const byteAssign_t InfoAssignment[] = {
}
private:
void notify(uint8_t val) {
void notify(uint8_t val, Inverter<> *iv) {
if(NULL != mCbMiPayload)
(mCbMiPayload)(val);
(mCbMiPayload)(val, iv);
}
void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) {
@ -704,20 +704,20 @@ const byteAssign_t InfoAssignment[] = {
}
/*
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
uint16_t code;
uint32_t start, end;
while(1) {
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end);
if(0 == code)
break;
if (NULL != mCbMiAlarm)
(mCbAlarm)(code, start, end);
yield();
}
}*/
/*
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
uint16_t code;
uint32_t start, end;
while(1) {
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end);
if(0 == code)
break;
if (NULL != mCbMiAlarm)
(mCbAlarm)(code, start, end);
yield();
}
}*/
//if ( mPayload[iv->id].complete || //4ch device
if ( p->packet[0] == (0x39 + ALL_FRAMES) || //4ch device - last message
@ -725,12 +725,12 @@ const byteAssign_t InfoAssignment[] = {
&& mPayload[iv->id].dataAB[CH0]
&& mPayload[iv->id].stsAB[CH0])) {
miComplete(iv);
}
}
}
void miComplete(Inverter<> *iv) {
if ( mPayload[iv->id].complete ) // && iv->type != INV_TYPE_4CH)
return; // if we got second message as well in repreated attempt
return; //if we got second message as well in repreated attempt
mPayload[iv->id].complete = true;
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("got all msgs"));
@ -752,7 +752,7 @@ const byteAssign_t InfoAssignment[] = {
iv->setQueuedCmdFinished();
mStat->rxSuccess++;
yield();
notify(RealTimeRunData_Debug); //iv->type == INV_TYPE_4CH ? 0x36 : 0x09 );
notify(RealTimeRunData_Debug, iv); //iv->type == INV_TYPE_4CH ? 0x36 : 0x09 );
}
bool build(uint8_t id, bool *complete) {
@ -826,6 +826,7 @@ const byteAssign_t InfoAssignment[] = {
IApp *mApp;
HMSYSTEM *mSys;
HMRADIO *mRadio;
statistics_t *mStat;
uint8_t mMaxRetrans;
uint32_t *mTimestamp;

437
src/hms/cmt2300a.h

@ -0,0 +1,437 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __CMT2300A_H__
#define __CMT2300A_H__
#include "esp32_3wSpi.h"
#define WORK_FREQ_KHZ 865000 // disired work frequency between DTU and
// inverter in kHz
#define HOY_BASE_FREQ_KHZ 860000 // in kHz
#define HOY_MAX_FREQ_KHZ 923500 // 0xFE * 250kHz + Base_freq
#define HOY_BOOT_FREQ_KHZ 868000 // Hoymiles boot/init frequency after power up inverter
#define FREQ_STEP_KHZ 250 // channel step size in kHz
#define FREQ_WARN_MIN_KHZ 863000 // for EU 863 - 870 MHz is allowed
#define FREQ_WARN_MAX_KHZ 870000 // for EU 863 - 870 MHz is allowed
// detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf
#define CMT2300A_MASK_CFG_RETAIN 0x10
#define CMT2300A_MASK_RSTN_IN_EN 0x20
#define CMT2300A_MASK_LOCKING_EN 0x20
#define CMT2300A_MASK_CHIP_MODE_STA 0x0F
#define CMT2300A_CUS_CMT10 0x09
#define CMT2300A_CUS_MODE_CTL 0x60 // [7] go_switch
// [6] go_tx
// [5] go_tfs
// [4] go_sleep
// [3] go_rx
// [2] go_rfs
// [1] go_stby
// [0] n/a
#define CMT2300A_CUS_MODE_STA 0x61 // [3:0] 0x00 IDLE
// 0x01 SLEEP
// 0x02 STBY
// 0x03 RFS
// 0x04 TFS
// 0x05 RX
// 0x06 TX
// 0x08 UNLOCKED/LOW_VDD
// 0x09 CAL
#define CMT2300A_CUS_EN_CTL 0x62
#define CMT2300A_CUS_FREQ_CHNL 0x63
#define CMT2300A_CUS_IO_SEL 0x65 // [5:4] GPIO3
// 0x00 CLKO
// 0x01 DOUT / DIN
// 0x02 INT2
// 0x03 DCLK
// [3:2] GPIO2
// 0x00 INT1
// 0x01 INT2
// 0x02 DOUT / DIN
// 0x03 DCLK
// [1:0] GPIO1
// 0x00 DOUT / DIN
// 0x01 INT1
// 0x02 INT2
// 0x03 DCLK
#define CMT2300A_CUS_INT1_CTL 0x66 // [4:0] INT1_SEL
// 0x00 RX active
// 0x01 TX active
// 0x02 RSSI VLD
// 0x03 Pream OK
// 0x04 SYNC OK
// 0x05 NODE OK
// 0x06 CRC OK
// 0x07 PKT OK
// 0x08 SL TMO
// 0x09 RX TMO
// 0x0A TX DONE
// 0x0B RX FIFO NMTY
// 0x0C RX FIFO TH
// 0x0D RX FIFO FULL
// 0x0E RX FIFO WBYTE
// 0x0F RX FIFO OVF
// 0x10 TX FIFO NMTY
// 0x11 TX FIFO TH
// 0x12 TX FIFO FULL
// 0x13 STATE IS STBY
// 0x14 STATE IS FS
// 0x15 STATE IS RX
// 0x16 STATE IS TX
// 0x17 LED
// 0x18 TRX ACTIVE
// 0x19 PKT DONE
#define CMT2300A_CUS_INT2_CTL 0x67 // [4:0] INT2_SEL
#define CMT2300A_CUS_INT_EN 0x68 // [7] SL TMO EN
// [6] RX TMO EN
// [5] TX DONE EN
// [4] PREAM OK EN
// [3] SYNC_OK EN
// [2] NODE OK EN
// [1] CRC OK EN
// [0] PKT DONE EN
#define CMT2300A_CUS_FIFO_CTL 0x69 // [7] TX DIN EN
// [6:5] TX DIN SEL
// 0x00 SEL GPIO1
// 0x01 SEL GPIO2
// 0x02 SEL GPIO3
// [4] FIFO AUTO CLR DIS
// [3] FIFO TX RD EN
// [2] FIFO RX TX SEL
// [1] FIFO MERGE EN
// [0] SPI FIFO RD WR SEL
#define CMT2300A_CUS_INT_CLR1 0x6A // clear interrupts Bank1
#define CMT2300A_CUS_INT_CLR2 0x6B // clear interrupts Bank2
#define CMT2300A_CUS_FIFO_CLR 0x6C
#define CMT2300A_CUS_INT_FLAG 0x6D // [7] LBD FLG
// [6] COL ERR FLG
// [5] PKT ERR FLG
// [4] PREAM OK FLG
// [3] SYNC OK FLG
// [2] NODE OK FLG
// [1] CRC OK FLG
// [0] PKT OK FLG
#define CMT2300A_CUS_RSSI_DBM 0x70
#define CMT2300A_GO_SWITCH 0x80
#define CMT2300A_GO_TX 0x40
#define CMT2300A_GO_TFS 0x20
#define CMT2300A_GO_SLEEP 0x10
#define CMT2300A_GO_RX 0x08
#define CMT2300A_GO_RFS 0x04
#define CMT2300A_GO_STBY 0x02
#define CMT2300A_GO_EEPROM 0x01
#define CMT2300A_STA_IDLE 0x00
#define CMT2300A_STA_SLEEP 0x01
#define CMT2300A_STA_STBY 0x02
#define CMT2300A_STA_RFS 0x03
#define CMT2300A_STA_TFS 0x04
#define CMT2300A_STA_RX 0x05
#define CMT2300A_STA_TX 0x06
#define CMT2300A_STA_EEPROM 0x07
#define CMT2300A_STA_ERROR 0x08
#define CMT2300A_STA_CAL 0x09
#define CMT2300A_INT_SEL_TX_DONE 0x0A
#define CMT2300A_MASK_TX_DONE_FLG 0x08
#define CMT2300A_MASK_PKT_OK_FLG 0x01
// default CMT paramters
static uint8_t cmtConfig[0x60] PROGMEM {
// 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm
0x00, 0x66, 0xEC, 0x1C, 0x70, 0x80, 0x14, 0x08,
0x11, 0x02, 0x02, 0x00, 0xAE, 0xE0, 0x35, 0x00,
// 0x10 - 0x1f
0x00, 0xF4, 0x10, 0xE2, 0x42, 0x20, 0x0C, 0x81,
0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12, // 860MHz as default
// 0x20 - 0x2f
0xA6, 0xC9, 0x20, 0x20, 0xD2, 0x35, 0x0C, 0x0A,
0x9F, 0x4B, 0x29, 0x29, 0xC0, 0x14, 0x05, 0x53,
// 0x30 - 0x3f
0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00,
0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00,
// 0x40 - 0x4f
0x00, 0x48, 0x5A, 0x48, 0x4D, 0x01, 0x1D, 0x00,
0x00, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x60,
// 0x50 - 0x5f
0xFF, 0x00, 0x00, 0x1F, 0x10, 0x70, 0x4D, 0x06,
0x00, 0x07, 0x50, 0x00, 0x42, 0x0C, 0x3F, 0x7F // - TX 13dBm
};
enum {CMT_SUCCESS = 0, CMT_ERR_SWITCH_STATE, CMT_ERR_TX_PENDING, CMT_FIFO_EMPTY, CMT_ERR_RX_IN_FIFO};
template<class SPI>
class Cmt2300a {
typedef SPI SpiType;
public:
Cmt2300a() {}
void setup(uint8_t pinCsb, uint8_t pinFcsb) {
mSpi.setup(pinCsb, pinFcsb);
init();
}
void setup() {
mSpi.setup();
init();
}
// call as often as possible
void loop() {
if(mTxPending) {
if(CMT2300A_MASK_TX_DONE_FLG == mSpi.readReg(CMT2300A_CUS_INT_CLR1)) {
if(cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) {
mTxPending = false;
goRx();
}
}
}
}
uint8_t goRx(void) {
if(mTxPending)
return CMT_ERR_TX_PENDING;
if(mInRxMode)
return CMT_SUCCESS;
mSpi.readReg(CMT2300A_CUS_INT1_CTL);
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE);
uint8_t tmp = mSpi.readReg(CMT2300A_CUS_INT_CLR1);
if(0x08 == tmp) // first time after TX a value of 0x08 is read
mSpi.writeReg(CMT2300A_CUS_INT_CLR1, 0x04);
else
mSpi.writeReg(CMT2300A_CUS_INT_CLR1, 0x00);
if(0x10 == tmp)
mSpi.writeReg(CMT2300A_CUS_INT_CLR2, 0x10);
else
mSpi.writeReg(CMT2300A_CUS_INT_CLR2, 0x00);
mSpi.writeReg(CMT2300A_CUS_FIFO_CTL, 0x02);
mSpi.writeReg(CMT2300A_CUS_FIFO_CLR, 0x02);
mSpi.writeReg(0x16, 0x0C); // [4:3]: RSSI_DET_SEL, [2:0]: RSSI_AVG_MODE
if(!cmtSwitchStatus(CMT2300A_GO_RX, CMT2300A_STA_RX))
return CMT_ERR_SWITCH_STATE;
mInRxMode = true;
return CMT_SUCCESS;
}
uint8_t getRx(uint8_t buf[], uint8_t len, int8_t *rssi) {
if(mTxPending)
return CMT_ERR_TX_PENDING;
if(0x1b != (mSpi.readReg(CMT2300A_CUS_INT_FLAG) & 0x1b))
return CMT_FIFO_EMPTY;
// receive ok (pream, sync, node, crc)
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE;
mSpi.readFifo(buf, len);
*rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128;
if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP))
return CMT_ERR_SWITCH_STATE;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE;
mInRxMode = false;
mCusIntFlag = mSpi.readReg(CMT2300A_CUS_INT_FLAG);
return CMT_SUCCESS;
}
uint8_t tx(uint8_t buf[], uint8_t len) {
if(mTxPending)
return CMT_ERR_TX_PENDING;
if(mInRxMode) {
mInRxMode = false;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE;
}
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE);
// no data received
mSpi.readReg(CMT2300A_CUS_INT_CLR1);
mSpi.writeReg(CMT2300A_CUS_INT_CLR1, 0x00);
mSpi.writeReg(CMT2300A_CUS_INT_CLR2, 0x00);
mSpi.writeReg(CMT2300A_CUS_FIFO_CTL, 0x07);
mSpi.writeReg(CMT2300A_CUS_FIFO_CLR, 0x01);
mSpi.writeReg(0x45, 0x01);
mSpi.writeReg(0x46, len); // payload length
mSpi.writeFifo(buf, len);
if(0xff != mRqstCh) {
mCurCh = mRqstCh;
mRqstCh = 0xff;
mSpi.writeReg(CMT2300A_CUS_FREQ_CHNL, mCurCh);
}
if(!cmtSwitchStatus(CMT2300A_GO_TX, CMT2300A_STA_TX))
return CMT_ERR_SWITCH_STATE;
// wait for tx done
mTxPending = true;
return CMT_SUCCESS;
}
// initialize CMT2300A, returns true on success
bool reset(void) {
mSpi.writeReg(0x7f, 0xff); // soft reset
delay(30);
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return false;
mSpi.writeReg(CMT2300A_CUS_MODE_STA, 0x52);
mSpi.writeReg(0x62, 0x20);
for(uint8_t i = 0; i < 0x60; i++) {
mSpi.writeReg(i, cmtConfig[i]);
}
mSpi.writeReg(CMT2300A_CUS_IO_SEL, 0x20); // -> GPIO3_SEL[1:0] = 0x02
// interrupt 1 control selection to TX DONE
if(CMT2300A_INT_SEL_TX_DONE != mSpi.readReg(CMT2300A_CUS_INT1_CTL))
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE);
// select interrupt 2
if(0x07 != mSpi.readReg(CMT2300A_CUS_INT2_CTL))
mSpi.writeReg(CMT2300A_CUS_INT2_CTL, 0x07);
// interrupt enable (TX_DONE, PREAM_OK, SYNC_OK, CRC_OK, PKT_DONE)
mSpi.writeReg(CMT2300A_CUS_INT_EN, 0x3B);
mSpi.writeReg(0x64, 0x64);
if(0x00 == mSpi.readReg(CMT2300A_CUS_FIFO_CTL))
mSpi.writeReg(CMT2300A_CUS_FIFO_CTL, 0x02); // FIFO_MERGE_EN
if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP))
return false;
delayMicroseconds(95);
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return false;
if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP))
return false;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return false;
//switchDtuFreq(WORK_FREQ_KHZ);
return true;
}
inline uint8_t freq2Chan(const uint32_t freqKhz) {
if((freqKhz % FREQ_STEP_KHZ) != 0) {
DPRINT(DBG_WARN, F("swtich frequency to "));
DBGPRINT(String(freqKhz));
DBGPRINT(F("kHz not possible!"));
return 0xff; // error
// apply the nearest frequency
//freqKhz = (freqKhz + FREQ_STEP_KHZ/2) / FREQ_STEP_KHZ;
//freqKhz *= FREQ_STEP_KHZ;
}
if((freqKhz < HOY_BASE_FREQ_KHZ) || (freqKhz > HOY_MAX_FREQ_KHZ))
return 0xff; // error
if((freqKhz < FREQ_WARN_MIN_KHZ) || (freqKhz > FREQ_WARN_MAX_KHZ))
DPRINTLN(DBG_WARN, F("Disired frequency is out of EU legal range! (863 - 870MHz)"));
return (freqKhz - HOY_BASE_FREQ_KHZ) / FREQ_STEP_KHZ;
}
inline void switchChannel(uint8_t ch) {
mRqstCh = ch;
}
inline uint32_t getFreqKhz(void) {
if(0xff != mRqstCh)
return HOY_BASE_FREQ_KHZ + (mRqstCh * FREQ_STEP_KHZ);
else
return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ);
}
private:
void init() {
mTxPending = false;
mInRxMode = false;
mCusIntFlag = 0x00;
mCnt = 0;
mRqstCh = 0xff;
mCurCh = 0x20;
}
// CMT state machine, wait for next state, true on success
bool cmtSwitchStatus(uint8_t cmd, uint8_t waitFor, uint16_t cycles = 40) {
mSpi.writeReg(CMT2300A_CUS_MODE_CTL, cmd);
while(cycles--) {
yield();
delayMicroseconds(10);
if(waitFor == (getChipStatus() & waitFor))
return true;
}
//Serial.println("status wait for: " + String(waitFor, HEX) + " read: " + String(getChipStatus(), HEX));
return false;
}
inline bool switchDtuFreq(const uint32_t freqKhz) {
uint8_t toCh = freq2Chan(freqKhz);
if(0xff == toCh)
return false;
switchChannel(toCh);
return true;
}
inline uint8_t getChipStatus(void) {
return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA;
}
SpiType mSpi;
uint8_t mCnt;
bool mTxPending;
bool mInRxMode;
uint8_t mCusIntFlag;
uint8_t mRqstCh, mCurCh;
};
#endif /*__CMT2300A_H__*/

189
src/hms/esp32_3wSpi.h

@ -0,0 +1,189 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __ESP32_3WSPI_H__
#define __ESP32_3WSPI_H__
#include "Arduino.h"
#if defined(ESP32)
#include "driver/spi_master.h"
#include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal
#if CONFIG_IDF_TARGET_ESP32S3
#define CLK_PIN 6
#define MOSI_PIN 5
#else
#define CLK_PIN 18
#define MOSI_PIN 23
#endif
#define SPI_CLK 1 * 1000 * 1000 // 1MHz
#define SPI_PARAM_LOCK() \
do { \
} while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS)
#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock)
// for ESP32 this is the so-called HSPI
// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore,
// it is simply the first externally usable hardware SPI master controller
#define SPI_CMT SPI2_HOST
template<uint8_t CSB_PIN=5, uint8_t FCSB_PIN=4> //, uint8_t GPIO3_PIN=15>
class esp32_3wSpi {
public:
esp32_3wSpi() {
mInitialized = false;
}
void setup(uint8_t pinCsb = CSB_PIN, uint8_t pinFcsb = FCSB_PIN) { //, uint8_t pinGpio3 = GPIO3_PIN) {
paramLock = xSemaphoreCreateMutex();
spi_bus_config_t buscfg = {
.mosi_io_num = MOSI_PIN,
.miso_io_num = -1, // single wire MOSI/MISO
.sclk_io_num = CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};
spi_device_interface_config_t devcfg = {
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
.mode = 0,
.cs_ena_pretrans = 1,
.cs_ena_posttrans = 1,
.clock_speed_hz = SPI_CLK,
.spics_io_num = pinCsb,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED));
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg));
// FiFo
spi_device_interface_config_t devcfg2 = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.cs_ena_pretrans = 2,
.cs_ena_posttrans = (uint8_t)(1 / (SPI_CLK * 10e6 * 2) + 2), // >2 us
.clock_speed_hz = SPI_CLK,
.spics_io_num = pinFcsb,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
esp_rom_gpio_connect_out_signal(MOSI_PIN, spi_periph_signal[SPI_CMT].spid_out, true, false);
delay(100);
//pinMode(pinGpio3, INPUT);
mInitialized = true;
}
void writeReg(uint8_t addr, uint8_t reg) {
if(!mInitialized)
return;
uint8_t tx_data;
tx_data = ~reg;
spi_transaction_t t = {
.cmd = 1,
.addr = (uint64_t)(~addr),
.length = 8,
.tx_buffer = &tx_data,
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
}
uint8_t readReg(uint8_t addr) {
if(!mInitialized)
return 0;
uint8_t rx_data;
spi_transaction_t t = {
.cmd = 0,
.addr = (uint64_t)(~addr),
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
return rx_data;
}
void writeFifo(uint8_t buf[], uint8_t len) {
if(!mInitialized)
return;
uint8_t tx_data;
spi_transaction_t t = {
.length = 8,
.tx_buffer = &tx_data, // reference to write data
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
for(uint8_t i = 0; i < len; i++) {
tx_data = ~buf[i]; // negate buffer contents
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
delayMicroseconds(4); // > 4 us
}
SPI_PARAM_UNLOCK();
}
void readFifo(uint8_t buf[], uint8_t len) {
if(!mInitialized)
return;
uint8_t rx_data;
spi_transaction_t t = {
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
for(uint8_t i = 0; i < len; i++) {
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
delayMicroseconds(4); // > 4 us
buf[i] = rx_data;
}
SPI_PARAM_UNLOCK();
}
private:
spi_device_handle_t spi_reg, spi_fifo;
bool mInitialized;
SemaphoreHandle_t paramLock = NULL;
};
#else
template<uint8_t CSB_PIN=5, uint8_t FCSB_PIN=4>
class esp32_3wSpi {
public:
esp32_3wSpi() {}
void setup() {}
void loop() {}
};
#endif
#endif /*__ESP32_3WSPI_H__*/

190
src/hms/hmsDefines.h

@ -0,0 +1,190 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __HMS_DEFINES_H__
#define __HMS_DEFINES_H__
#include "../hm/hmDefines.h"
//-------------------------------------
// HMS-350, HMS-500
//-------------------------------------
const byteAssign_t hms1chAssignment[] = {
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
{ FLD_PDC, UNIT_W, CH1, 6, 2, 10 },
{ FLD_YT, UNIT_KWH, CH1, 8, 4, 1000 },
{ FLD_YD, UNIT_WH, CH1, 12, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC },
{ FLD_UAC, UNIT_V, CH0, 14, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 16, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 18, 2, 10 },
{ FLD_Q, UNIT_VAR, CH0, 20, 2, 10 }, // signed!
{ FLD_IAC, UNIT_A, CH0, 22, 2, 100 },
{ FLD_PF, UNIT_NONE, CH0, 24, 2, 1000 }, // signed!
{ FLD_T, UNIT_C, CH0, 26, 2, 10 }, // signed!
{ FLD_EVT, UNIT_NONE, CH0, 28, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }
};
#define HMS1CH_LIST_LEN (sizeof(hms1chAssignment) / sizeof(byteAssign_t))
#define HMS1CH_PAYLOAD_LEN 30
//-------------------------------------
// HMS-800, HMS-1000
//-------------------------------------
const byteAssign_t hms2chAssignment[] = {
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
{ FLD_IDC, UNIT_A, CH1, 6, 2, 100 },
{ FLD_PDC, UNIT_W, CH1, 10, 2, 10 },
{ FLD_YT, UNIT_KWH, CH1, 14, 4, 1000 },
{ FLD_YD, UNIT_WH, CH1, 22, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC },
{ FLD_UDC, UNIT_V, CH2, 4, 2, 10 },
{ FLD_IDC, UNIT_A, CH2, 8, 2, 100 },
{ FLD_PDC, UNIT_W, CH2, 12, 2, 10 },
{ FLD_YT, UNIT_KWH, CH2, 18, 4, 1000 },
{ FLD_YD, UNIT_WH, CH2, 24, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC },
{ FLD_UAC, UNIT_V, CH0, 26, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 28, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 30, 2, 10 },
{ FLD_Q, UNIT_VAR, CH0, 32, 2, 10 }, // signed!
{ FLD_IAC, UNIT_A, CH0, 34, 2, 100 },
{ FLD_PF, UNIT_NONE, CH0, 36, 2, 1000 }, // signed!
{ FLD_T, UNIT_C, CH0, 38, 2, 10 }, // signed!
{ FLD_EVT, UNIT_NONE, CH0, 40, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }
};
#define HMS2CH_LIST_LEN (sizeof(hms2chAssignment) / sizeof(byteAssign_t))
#define HMS2CH_PAYLOAD_LEN 42
//-------------------------------------
// HMS-1800, HMS-2000
//-------------------------------------
const byteAssign_t hms4chAssignment[] = {
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
{ FLD_IDC, UNIT_A, CH1, 6, 2, 100 },
{ FLD_PDC, UNIT_W, CH1, 10, 2, 10 },
{ FLD_YT, UNIT_KWH, CH1, 14, 4, 1000 },
{ FLD_YD, UNIT_WH, CH1, 22, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC },
{ FLD_UDC, UNIT_V, CH2, 4, 2, 10 },
{ FLD_IDC, UNIT_A, CH2, 8, 2, 100 },
{ FLD_PDC, UNIT_W, CH2, 12, 2, 10 },
{ FLD_YT, UNIT_KWH, CH2, 18, 4, 1000 },
{ FLD_YD, UNIT_WH, CH2, 24, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC },
{ FLD_UDC, UNIT_V, CH3, 26, 2, 10 },
{ FLD_IDC, UNIT_A, CH3, 30, 2, 100 },
{ FLD_PDC, UNIT_W, CH3, 34, 2, 10 },
{ FLD_YT, UNIT_KWH, CH3, 38, 4, 1000 },
{ FLD_YD, UNIT_WH, CH3, 46, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC },
{ FLD_UDC, UNIT_V, CH4, 28, 2, 10 },
{ FLD_IDC, UNIT_A, CH4, 32, 2, 100 },
{ FLD_PDC, UNIT_W, CH4, 36, 2, 10 },
{ FLD_YT, UNIT_KWH, CH4, 42, 4, 1000 },
{ FLD_YD, UNIT_WH, CH4, 48, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC },
{ FLD_UAC, UNIT_V, CH0, 50, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 52, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 54, 2, 10 },
{ FLD_Q, UNIT_VAR, CH0, 56, 2, 10 }, // signed!
{ FLD_IAC, UNIT_A, CH0, 58, 2, 100 },
{ FLD_PF, UNIT_NONE, CH0, 60, 2, 1000 }, // signed!
{ FLD_T, UNIT_C, CH0, 62, 2, 10 }, // signed!
{ FLD_EVT, UNIT_NONE, CH0, 64, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }
};
#define HMS4CH_LIST_LEN (sizeof(hms4chAssignment) / sizeof(byteAssign_t))
#define HMS4CH_PAYLOAD_LEN 66
//-------------------------------------
// HMT-1800, HMT-2250
//-------------------------------------
const byteAssign_t hmt6chAssignment[] = {
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
{ FLD_PDC, UNIT_W, CH1, 8, 2, 10 },
{ FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 },
{ FLD_YD, UNIT_WH, CH1, 20, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC },
{ FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC },
{ FLD_IDC, UNIT_A, CH2, 6, 2, 100 },
{ FLD_PDC, UNIT_W, CH2, 10, 2, 10 },
{ FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 },
{ FLD_YD, UNIT_WH, CH2, 22, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC },
{ FLD_UDC, UNIT_V, CH3, 24, 2, 10 },
{ FLD_IDC, UNIT_A, CH3, 26, 2, 100 },
{ FLD_PDC, UNIT_W, CH3, 30, 2, 10 },
{ FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 },
{ FLD_YD, UNIT_WH, CH3, 42, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC },
{ FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC },
{ FLD_IDC, UNIT_A, CH4, 28, 2, 100 },
{ FLD_PDC, UNIT_W, CH4, 32, 2, 10 },
{ FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 },
{ FLD_YD, UNIT_WH, CH4, 44, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC },
{ FLD_UDC, UNIT_V, CH5, 46, 2, 10 },
{ FLD_IDC, UNIT_A, CH5, 48, 2, 100 },
{ FLD_PDC, UNIT_W, CH5, 52, 2, 10 },
{ FLD_YT, UNIT_KWH, CH5, 56, 4, 1000 },
{ FLD_YD, UNIT_WH, CH5, 64, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH5, CALC_IRR_CH, CH5, CMD_CALC },
{ FLD_UDC, UNIT_V, CH6, CALC_UDC_CH, CH5, CMD_CALC },
{ FLD_IDC, UNIT_A, CH6, 50, 2, 100 },
{ FLD_PDC, UNIT_W, CH6, 54, 2, 10 },
{ FLD_YT, UNIT_KWH, CH6, 60, 4, 1000 },
{ FLD_YD, UNIT_WH, CH6, 66, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH6, CALC_IRR_CH, CH6, CMD_CALC },
{ FLD_UAC_1N, UNIT_V, CH0, 68, 2, 10 },
{ FLD_UAC_2N, UNIT_V, CH0, 70, 2, 10 },
{ FLD_UAC_3N, UNIT_V, CH0, 72, 2, 10 },
{ FLD_UAC_12, UNIT_V, CH0, 74, 2, 10 },
{ FLD_UAC_23, UNIT_V, CH0, 76, 2, 10 },
{ FLD_UAC_31, UNIT_V, CH0, 78, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 80, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 82, 2, 10 },
{ FLD_Q, UNIT_VAR, CH0, 84, 2, 10 },
{ FLD_IAC_1, UNIT_A, CH0, 86, 2, 100 },
{ FLD_IAC_2, UNIT_A, CH0, 88, 2, 100 },
{ FLD_IAC_3, UNIT_A, CH0, 90, 2, 100 },
{ FLD_PF, UNIT_NONE, CH0, 92, 2, 1000 },
{ FLD_T, UNIT_C, CH0, 94, 2, 10 },
{ FLD_EVT, UNIT_NONE, CH0, 96, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }
};
#define HMT6CH_LIST_LEN (sizeof(hmt6chAssignment) / sizeof(byteAssign_t))
#define HMT6CH_PAYLOAD_LEN 98
#endif /*__HMS_DEFINES_H__*/

405
src/hms/hmsPayload.h

@ -0,0 +1,405 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HMS_PAYLOAD_H__
#define __HMS_PAYLOAD_H__
#include "../utils/dbg.h"
#include "../utils/crc.h"
#include "../config/config.h"
#include <Arduino.h>
#define HMS_TIMEOUT_SEC 30 // 30s * 1000
typedef struct {
uint8_t txCmd;
uint8_t txId;
//uint8_t invId;
uint32_t ts;
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE];
uint8_t len[MAX_PAYLOAD_ENTRIES];
bool complete;
uint8_t maxPackId;
bool lastFound;
uint8_t retransmits;
bool requested;
bool gotFragment;
} hmsPayload_t;
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(uint16_t alarmCode, uint32_t start, uint32_t end)> alarmListenerType;
template<class HMSYSTEM, class RADIO>
class HmsPayload {
public:
HmsPayload() {}
void setup(IApp *app, HMSYSTEM *sys, RADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
mApp = app;
mSys = sys;
mRadio = radio;
mStat = stat;
mMaxRetrans = maxRetransmits;
mTimestamp = timestamp;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
reset(i);
mIvCmd56Cnt[i] = 0;
}
mSerialDebug = false;
//mHighPrioIv = NULL;
mCbAlarm = NULL;
mCbPayload = NULL;
//mLastRx = 0;
}
void enableSerialDebug(bool enable) {
mSerialDebug = enable;
}
void addPayloadListener(payloadListenerType cb) {
mCbPayload = cb;
}
void addAlarmListener(alarmListenerType cb) {
mCbAlarm = cb;
}
void loop() {
/*if(NULL != mHighPrioIv) {
ivSend(mHighPrioIv, true);
mHighPrioIv = NULL;
}*/
}
void ivSendHighPrio(Inverter<> *iv) {
//mHighPrioIv = iv;
}
void ivSend(Inverter<> *iv, bool highPrio = false) {
if ((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) // only process HMS inverters
return;
if(!highPrio) {
if (mPayload[iv->id].requested) {
if (!mPayload[iv->id].complete)
process(false); // no retransmit
if (!mPayload[iv->id].complete) {
if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)
mStat->rxFailNoAnser++; // got nothing
else
mStat->rxFail++; // got fragments but not complete response
iv->setQueuedCmdFinished(); // command failed
if (mSerialDebug)
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
/*if (mSerialDebug) {
DPRINT(DBG_INFO, F("(#"));
DBGPRINT(String(iv->id));
DBGPRINT(F(") no Payload received! (retransmits: "));
DBGPRINT(String(mPayload[iv->id].retransmits));
DBGPRINTLN(F(")"));
}*/
}
}
}
reset(iv->id);
mPayload[iv->id].requested = true;
yield();
if (mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Requesting Inv SN "));
DBGPRINTLN(String(iv->config->serial.u64, HEX));
}
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv->getDevControlRequest()) {
if (mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Devcontrol request 0x"));
DBGPRINT(String(iv->devControlCmd, HEX));
DBGPRINT(F(" power limit "));
DBGPRINTLN(String(iv->powerLimit[0]));
}
mRadio->sendControlPacket(&iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false);
mPayload[iv->id].txCmd = iv->devControlCmd;
//iv->clearCmdQueue();
//iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
} else if(((rec->ts + HMS_TIMEOUT_SEC) < *mTimestamp) && (mIvCmd56Cnt[iv->id] < 3)) {
mRadio->switchFrequency(&iv->radioId.u64, HOY_BOOT_FREQ_KHZ, WORK_FREQ_KHZ);
mIvCmd56Cnt[iv->id]++;
} else {
if(++mIvCmd56Cnt[iv->id] == 10)
mIvCmd56Cnt[iv->id] = 0;
uint8_t cmd = iv->getQueuedCmd();
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("prepareDevInformCmd 0x"));
DBGHEXLN(cmd);
mRadio->prepareDevInformCmd(&iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false);
mPayload[iv->id].txCmd = cmd;
}
}
void add(Inverter<> *iv, hmsPacket_t *p) {
if (p->data[1] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
mPayload[iv->id].txId = p->data[1];
DPRINTLN(DBG_DEBUG, F("Response from info request received"));
uint8_t *pid = &p->data[10];
if (*pid == 0x00) {
DPRINT(DBG_DEBUG, F("fragment number zero received and ignored"));
} else {
DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX));
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->data[11], p->data[0] - 11);
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->data[0] -11;
mPayload[iv->id].gotFragment = true;
}
if ((*pid & ALL_FRAMES) == ALL_FRAMES) {
// Last packet
if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) {
mPayload[iv->id].maxPackId = (*pid & 0x7f);
if (*pid > 0x81)
mPayload[iv->id].lastFound = true;
}
}
}
} else if (p->data[1] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received"));
mPayload[iv->id].txId = p->data[1];
iv->clearDevControlRequest();
if ((p->data[13] == ActivePowerContr) && (p->data[14] == 0x00)) {
bool ok = true;
if((p->data[11] == 0x00) && (p->data[12] == 0x00))
mApp->setMqttPowerLimitAck(iv);
else
ok = false;
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F(" has "));
if(!ok) DBGPRINT(F("not "));
DBGPRINT(F("accepted power limit set point "));
DBGPRINT(String(iv->powerLimit[0]));
DBGPRINT(F(" with PowerLimitControl "));
DBGPRINTLN(String(iv->powerLimit[1]));
iv->clearCmdQueue();
iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
}
iv->devControlCmd = Init;
}
}
void process(bool retransmit) {
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
if ((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) // only process HMS inverters
continue; // skip to next inverter
if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) {
// no processing needed if txId is not 0x95
mPayload[iv->id].complete = true;
continue; // skip to next inverter
}
if (!mPayload[iv->id].complete) {
bool crcPass, pyldComplete;
crcPass = build(iv->id, &pyldComplete);
if (!crcPass && !pyldComplete) { // payload not complete
if ((mPayload[iv->id].requested) && (retransmit)) {
if (mPayload[iv->id].retransmits < mMaxRetrans) {
mPayload[iv->id].retransmits++;
if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) {
// This is required to prevent retransmissions without answer.
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm..."));
mPayload[iv->id].retransmits = mMaxRetrans;
} else if(iv->devControlCmd == ActivePowerContr) {
DPRINTLN(DBG_INFO, F("retransmit power limit"));
mRadio->sendControlPacket(&iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true);
} else {
if(false == mPayload[iv->id].gotFragment) {
//DPRINTLN(DBG_WARN, F("nothing received: Request Complete Retransmit"));
//mPayload[iv->id].txCmd = iv->getQueuedCmd();
//DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX));
//mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("nothing received"));
mPayload[iv->id].retransmits = mMaxRetrans;
} else {
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) {
if (mPayload[iv->id].len[i] == 0) {
DPRINT(DBG_WARN, F("Frame "));
DBGPRINT(String(i + 1));
DBGPRINTLN(F(" missing: Request Retransmit"));
//mRadio->sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true);
break; // only request retransmit one frame per loop
}
yield();
}
}
}
}
}
} /*else if(!crcPass && pyldComplete) { // crc error on complete Payload
if (mPayload[iv->id].retransmits < mMaxRetrans) {
mPayload[iv->id].retransmits++;
DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit"));
mPayload[iv->id].txCmd = iv->getQueuedCmd();
DPRINT(DBG_INFO, F("(#"));
DBGPRINT(String(iv->id));
DBGPRINT(F(") prepareDevInformCmd 0x"));
DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX));
mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
}
}*/ else { // payload complete
DPRINT(DBG_INFO, F("procPyld: cmd: 0x"));
DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX));
DPRINT(DBG_INFO, F("procPyld: txid: 0x"));
DBGPRINTLN(String(mPayload[iv->id].txId, HEX));
DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId));
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
mPayload[iv->id].complete = true;
uint8_t payload[100];
uint8_t payloadLen = 0;
memset(payload, 0, 100);
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) {
memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i]));
payloadLen += (mPayload[iv->id].len[i]);
yield();
}
payloadLen -= 2;
if (mSerialDebug) {
DPRINT(DBG_INFO, F("Payload ("));
DBGPRINT(String(payloadLen));
DBGPRINT(F("): "));
ah::dumpBuf(payload, payloadLen);
}
if (NULL == rec) {
DPRINTLN(DBG_ERROR, F("record is NULL!"));
} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) {
if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES))
mStat->rxSuccess++;
rec->ts = mPayload[iv->id].ts;
for (uint8_t i = 0; i < rec->length; i++) {
iv->addValue(i, payload, rec);
yield();
}
iv->doCalculations();
notify(mPayload[iv->id].txCmd, iv);
/*if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
uint16_t code;
uint32_t start, end;
while(1) {
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end);
if(0 == code)
break;
if (NULL != mCbAlarm)
(mCbAlarm)(code, start, end);
yield();
}
}*/
} else {
DPRINT(DBG_ERROR, F("plausibility check failed, expected "));
DBGPRINT(String(rec->pyldLen));
DBGPRINTLN(F(" bytes"));
mStat->rxFail++;
}
iv->setQueuedCmdFinished();
}
}
yield();
}
}
private:
void notify(uint8_t val, Inverter<> *iv) {
if(NULL != mCbPayload)
(mCbPayload)(val, iv);
}
void notify(uint16_t code, uint32_t start, uint32_t endTime) {
if (NULL != mCbAlarm)
(mCbAlarm)(code, start, endTime);
}
bool build(uint8_t id, bool *complete) {
DPRINTLN(DBG_VERBOSE, F("build"));
uint16_t crc = 0xffff, crcRcv = 0x0000;
if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES)
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
// check if all fragments are there
*complete = true;
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) {
if(mPayload[id].len[i] == 0)
*complete = false;
}
if(!*complete)
return false;
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) {
if (mPayload[id].len[i] > 0) {
if (i == (mPayload[id].maxPackId - 1)) {
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 1, crc);
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]);
} else
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
}
yield();
}
return (crc == crcRcv) ? true : false;
}
void reset(uint8_t id) {
DPRINT(DBG_INFO, "resetPayload: id: ");
DBGPRINTLN(String(id));
memset(&mPayload[id], 0, sizeof(hmsPayload_t));
//mPayload[id].txCmd = 0;
mPayload[id].gotFragment = false;
//mPayload[id].retransmits = 0;
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
mPayload[id].lastFound = false;
mPayload[id].complete = false;
mPayload[id].requested = false;
mPayload[id].ts = *mTimestamp;
}
IApp *mApp;
HMSYSTEM *mSys;
RADIO *mRadio;
statistics_t *mStat;
uint8_t mMaxRetrans;
uint32_t *mTimestamp;
//uint32_t mLastRx;
hmsPayload_t mPayload[MAX_NUM_INVERTERS];
uint8_t mIvCmd56Cnt[MAX_NUM_INVERTERS];
bool mSerialDebug;
Inverter<> *mHighPrioIv;
alarmListenerType mCbAlarm;
payloadListenerType mCbPayload;
};
#endif /*__HMS_PAYLOAD_H__*/

213
src/hms/hmsRadio.h

@ -0,0 +1,213 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HMS_RADIO_H__
#define __HMS_RADIO_H__
#include "../utils/dbg.h"
#include "cmt2300a.h"
typedef struct {
int8_t rssi;
uint8_t data[28];
} hmsPacket_t;
#define U32_B3(val) ((uint8_t)((val >> 24) & 0xff))
#define U32_B2(val) ((uint8_t)((val >> 16) & 0xff))
#define U32_B1(val) ((uint8_t)((val >> 8) & 0xff))
#define U32_B0(val) ((uint8_t)((val ) & 0xff))
template<class SPI, uint32_t DTU_SN = 0x81001765>
class CmtRadio {
typedef SPI SpiType;
typedef Cmt2300a<SpiType> CmtType;
public:
CmtRadio() {
mDtuSn = DTU_SN;
}
void setup(uint8_t pinCsb, uint8_t pinFcsb, bool genDtuSn = true) {
mCmt.setup(pinCsb, pinFcsb);
reset(genDtuSn);
}
void setup(bool genDtuSn = true) {
mCmt.setup();
reset(genDtuSn);
}
bool loop() {
mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx))
return false;
getRx();
if(CMT_SUCCESS == mCmt.goRx()) {
mIrqRcvd = false;
mRqstGetRx = false;
return true;
} else
return false;
}
void tickSecond() {
}
void handleIntr(void) {
mIrqRcvd = true;
}
void enableDebug() {
mSerialDebug = true;
}
void sendControlPacket(const uint64_t *ivId, uint8_t cmd, uint16_t *data, bool isRetransmit) {
DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x"));
DBGHEXLN(cmd);
initPacket(ivId, TX_REQ_DEVCONTROL, SINGLE_FRAME);
uint8_t cnt = 10;
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit
mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
}
sendPacket(cnt, isRetransmit);
}
bool switchFrequency(const uint64_t *ivId, uint32_t fromkHz, uint32_t tokHz) {
uint8_t fromCh = mCmt.freq2Chan(fromkHz);
uint8_t toCh = mCmt.freq2Chan(tokHz);
if((0xff == fromCh) || (0xff == toCh))
return false;
mCmt.switchChannel(fromCh);
sendSwitchChCmd(ivId, toCh);
mCmt.switchChannel(toCh);
return true;
}
void prepareDevInformCmd(const uint64_t *ivId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg.
initPacket(ivId, reqfld, ALL_FRAMES);
mTxBuf[10] = cmd;
CP_U32_LittleEndian(&mTxBuf[12], ts);
/*if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) {
mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff;
}*/
sendPacket(24, isRetransmit);
}
void sendPacket(uint8_t len, bool isRetransmit) {
if (len > 14) {
uint16_t crc = ah::crc16(&mTxBuf[10], len - 10);
mTxBuf[len++] = (crc >> 8) & 0xff;
mTxBuf[len++] = (crc ) & 0xff;
}
mTxBuf[len] = ah::crc8(mTxBuf, len);
len++;
if(mSerialDebug) {
DPRINT(DBG_INFO, F("TX "));
DBGPRINT(String(mCmt.getFreqKhz()/1000.0f));
DBGPRINT(F("Mhz | "));
ah::dumpBuf(mTxBuf, len);
}
uint8_t status = mCmt.tx(mTxBuf, len);
if(CMT_SUCCESS != status) {
DPRINT(DBG_WARN, F("CMT TX failed, code: "));
DBGPRINTLN(String(status));
if(CMT_ERR_RX_IN_FIFO == status)
mIrqRcvd = true;
}
if(isRetransmit)
mRetransmits++;
else
mSendCnt++;
}
uint32_t mSendCnt;
uint32_t mRetransmits;
std::queue<hmsPacket_t> mBufCtrl;
private:
inline void reset(bool genDtuSn) {
if(genDtuSn)
generateDtuSn();
if(!mCmt.reset())
DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!"));
else
mCmt.goRx();
mSendCnt = 0;
mRetransmits = 0;
mSerialDebug = false;
mIrqRcvd = false;
mRqstGetRx = false;
}
inline void sendSwitchChCmd(const uint64_t *ivId, uint8_t ch) {
/** ch:
* 0x00: 860.00 MHz
* 0x01: 860.25 MHz
* 0x02: 860.50 MHz
* ...
* 0x14: 865.00 MHz
* ...
* 0x28: 870.00 MHz
* */
initPacket(ivId, 0x56, 0x02);
mTxBuf[10] = 0x15;
mTxBuf[11] = 0x21;
mTxBuf[12] = ch;
mTxBuf[13] = 0x14;
sendPacket(14, false);
mRqstGetRx = true;
}
void initPacket(const uint64_t *ivId, uint8_t mid, uint8_t pid) {
mTxBuf[0] = mid;
CP_U32_BigEndian(&mTxBuf[1], (*ivId) >> 8);
CP_U32_LittleEndian(&mTxBuf[5], mDtuSn);
mTxBuf[9] = pid;
memset(&mTxBuf[10], 0x00, 17);
}
inline void generateDtuSn(void) {
uint32_t chipID = 0;
#ifdef ESP32
uint64_t MAC = ESP.getEfuseMac();
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF);
#endif
mDtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
for(int i = 0; i < 7; i++) {
mDtuSn |= (chipID % 10) << (i * 4);
chipID /= 10;
}
}
inline void getRx(void) {
hmsPacket_t p;
uint8_t status = mCmt.getRx(p.data, 28, &p.rssi);
if(CMT_SUCCESS == status)
mBufCtrl.push(p);
}
CmtType mCmt;
uint32_t mDtuSn;
uint8_t mTxBuf[27];
bool mSerialDebug;
bool mIrqRcvd;
bool mRqstGetRx;
};
#endif /*__HMS_RADIO_H__*/

20
src/main.cpp

@ -5,8 +5,6 @@
#include "utils/dbg.h"
#include "app.h"
#include "config/config.h"
app myApp;
@ -15,13 +13,27 @@ IRAM_ATTR void handleIntr(void) {
myApp.handleIntr();
}
//-----------------------------------------------------------------------------
#ifdef ESP32
IRAM_ATTR void handleHmsIntr(void) {
myApp.handleHmsIntr();
}
#endif
//-----------------------------------------------------------------------------
void setup() {
myApp.setup();
// TODO: move to HmRadio
attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING);
if(myApp.getNrfEnabled()) {
if(DEF_PIN_OFF != myApp.getNrfIrqPin())
attachInterrupt(digitalPinToInterrupt(myApp.getNrfIrqPin()), handleIntr, FALLING);
}
#ifdef ESP32
if(myApp.getCmtEnabled()) {
if(DEF_PIN_OFF != myApp.getCmtIrqPin())
attachInterrupt(digitalPinToInterrupt(myApp.getCmtIrqPin()), handleHmsIntr, RISING);
}
#endif
}

10
src/platformio.ini

@ -24,7 +24,7 @@ extra_scripts =
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24 @ ^1.4.5
nrf24/RF24 @ 1.4.5
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.4.2
bblanchon/ArduinoJson @ ^6.21.2
@ -37,7 +37,7 @@ lib_deps =
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = -D RELEASE
build_flags = -D RELEASE -std=gnu++17
;-Wl,-Map,output.map
monitor_filters =
;default ; Remove typical terminal control codes from input
@ -50,7 +50,7 @@ monitor_filters =
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = -D RELEASE -DENABLE_PROMETHEUS_EP
build_flags = -D RELEASE -std=gnu++17 -DENABLE_PROMETHEUS_EP
monitor_filters =
;default ; Remove typical terminal control codes from input
;time ; Add timestamp with milliseconds for each new line
@ -61,7 +61,7 @@ monitor_filters =
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -std=gnu++17 -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48
build_type = debug
monitor_filters =
;default ; Remove typical terminal control codes from input
@ -73,7 +73,7 @@ platform = espressif8266
board = esp8285
board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L
build_flags = -D RELEASE
build_flags = -D RELEASE -std=gnu++17
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line

56
src/plugins/Display/Display.h

@ -10,12 +10,15 @@
#include "Display_Mono_128X32.h"
#include "Display_Mono_128X64.h"
#include "Display_Mono_84X48.h"
#include "Display_Mono_64X48.h"
#include "Display_ePaper.h"
template <class HMSYSTEM>
class Display {
public:
Display() {}
Display() {
mMono = NULL;
}
void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) {
mCfg = cfg;
@ -25,31 +28,28 @@ class Display {
mLoopCnt = 0;
mVersion = version;
if (mCfg->type == 0)
return;
switch (mCfg->type) {
case 0: mMono = NULL; break;
case 1: // fall-through
case 2: mMono = new DisplayMono128X64(); break;
case 3: mMono = new DisplayMono84X48(); break;
case 4: mMono = new DisplayMono128X32(); break;
case 5: mMono = new DisplayMono64X48(); break;
if ((0 < mCfg->type) && (mCfg->type < 10)) {
switch (mCfg->type) {
case 2:
case 1:
default:
mMono = new DisplayMono128X64();
break;
case 3:
mMono = new DisplayMono84X48();
break;
case 4:
mMono = new DisplayMono128X32();
break;
}
mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast);
mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion);
} else if (mCfg->type >= 10) {
#if defined(ESP32)
mRefreshCycle = 0;
mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline);
mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion);
case 10:
mMono = NULL; // ePaper does not use this
mRefreshCycle = 0;
mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline);
mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion);
break;
#endif
default: mMono = NULL; break;
}
if(mMono) {
mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast);
mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion);
}
}
@ -89,7 +89,7 @@ class Display {
if (iv == NULL)
continue;
if (iv->isProducing(*mUtcTs))
if (iv->isProducing())
isprod++;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
@ -97,14 +97,16 @@ class Display {
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
}
if ((0 < mCfg->type) && (mCfg->type < 10) && (mMono != NULL)) {
if (mMono ) {
mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod);
} else if (mCfg->type >= 10) {
}
#if defined(ESP32)
else if (mCfg->type == 10) {
mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod);
mRefreshCycle++;
#endif
}
#endif
#if defined(ESP32)
if (mRefreshCycle > 480) {

4
src/plugins/Display/Display_Mono.h

@ -35,8 +35,8 @@ class DisplayMono {
uint8_t mLoopCnt;
uint32_t* mUtcTs;
uint8_t mLineXOffsets[5];
uint8_t mLineYOffsets[5];
uint8_t mLineXOffsets[5] = {};
uint8_t mLineYOffsets[5] = {};
uint16_t mDispY;

2
src/plugins/Display/Display_Mono_128X32.h

@ -21,8 +21,6 @@ class DisplayMono128X32 : public DisplayMono {
void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) {
if((0 == type) || (type > 4))
return;
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type;

5
src/plugins/Display/Display_Mono_128X64.h

@ -19,8 +19,6 @@ class DisplayMono128X64 : public DisplayMono {
}
void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) {
if((0 == type) || (type > 4))
return;
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type;
@ -65,8 +63,7 @@ class DisplayMono128X64 : public DisplayMono {
mDisplay->clearBuffer();
// set Contrast of the Display to raise the lifetime
if (3 != mType)
mDisplay->setContrast(mLuminance);
mDisplay->setContrast(mLuminance);
if ((totalPower > 0) && (isprod > 0)) {
mTimeout = DISP_DEFAULT_TIMEOUT;

134
src/plugins/Display/Display_Mono_64X48.h

@ -0,0 +1,134 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#pragma once
#include "Display_Mono.h"
class DisplayMono64X48 : public DisplayMono {
public:
DisplayMono64X48() : DisplayMono() {
mEnPowerSafe = true;
mEnScreenSaver = false;
mLuminance = 20;
mExtra = 0;
mDispY = 0;
mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds)
mUtcTs = NULL;
mType = 0;
}
void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) {
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type;
// Wemos OLed Shield is not defined in u8 lib -> use nearest compatible
mDisplay = new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data);
mUtcTs = utcTs;
mDisplay->begin();
calcLinePositions();
mDisplay->clearBuffer();
mDisplay->setContrast(mLuminance);
printText("AHOY!", 0);
printText("ahoydtu.de", 1);
printText(version, 2);
mDisplay->sendBuffer();
}
void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) {
mEnPowerSafe = enPowerSafe;
mEnScreenSaver = enScreenSaver;
mLuminance = lum;
}
void loop(void) {
if (mEnPowerSafe) {
if (mTimeout != 0)
mTimeout--;
}
}
void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) {
mDisplay->clearBuffer();
// set Contrast of the Display to raise the lifetime
mDisplay->setContrast(mLuminance);
if ((totalPower > 0) && (isprod > 0)) {
mTimeout = DISP_DEFAULT_TIMEOUT;
mDisplay->setPowerSave(false);
if (totalPower > 999)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000));
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower);
printText(mFmtText, 0);
} else {
printText("offline", 0);
// check if it's time to enter power saving mode
if (mTimeout == 0)
mDisplay->setPowerSave(mEnPowerSafe);
}
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", totalYieldDay);
printText(mFmtText, 1);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "T: %4.0f kWh", totalYieldTotal);
printText(mFmtText, 2);
IPAddress ip = WiFi.localIP();
if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", isprod);
printText(mFmtText, 3);
} else if (NULL != mUtcTs)
printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3);
mDisplay->sendBuffer();
mExtra++;
}
private:
void calcLinePositions() {
uint8_t yOff = 0;
for (uint8_t i = 0; i < 4; i++) {
setFont(i);
yOff += (mDisplay->getMaxCharHeight());
mLineYOffsets[i] = yOff;
}
}
inline void setFont(uint8_t line) {
switch (line) {
case 0:
mDisplay->setFont(u8g2_font_fur11_tf);
break;
case 1:
case 2:
mDisplay->setFont(u8g2_font_6x10_tf);
break;
case 3:
mDisplay->setFont(u8g2_font_4x6_tr);
break;
case 4:
mDisplay->setFont(u8g2_font_4x6_tr);
break;
}
}
void printText(const char *text, uint8_t line) {
uint8_t dispX = 0; //small display, use all we have
dispX += (mEnScreenSaver) ? (mExtra % 4) : 0;
setFont(line);
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
}
};

11
src/plugins/Display/Display_Mono_84X48.h

@ -20,8 +20,6 @@ class DisplayMono84X48 : public DisplayMono {
}
void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) {
if((0 == type) || (type > 4))
return;
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type;
@ -33,8 +31,8 @@ class DisplayMono84X48 : public DisplayMono {
calcLinePositions();
mDisplay->clearBuffer();
if (3 != mType)
mDisplay->setContrast(mLuminance);
mDisplay->setContrast(mLuminance);
printText("AHOY!", 0);
printText("ahoydtu.de", 2);
printText(version, 3);
@ -58,8 +56,7 @@ class DisplayMono84X48 : public DisplayMono {
mDisplay->clearBuffer();
// set Contrast of the Display to raise the lifetime
if (3 != mType)
mDisplay->setContrast(mLuminance);
mDisplay->setContrast(mLuminance);
if ((totalPower > 0) && (isprod > 0)) {
mTimeout = DISP_DEFAULT_TIMEOUT;
@ -95,7 +92,7 @@ class DisplayMono84X48 : public DisplayMono {
mDisplay->sendBuffer();
mExtra = 1;
mExtra++;
}
private:

2
src/plugins/Display/Display_ePaper.cpp

@ -26,7 +26,7 @@ DisplayEPaper::DisplayEPaper() {
void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char *version) {
mUtcTs = utcTs;
if (type > 9) {
if (type == 10) {
Serial.begin(115200);
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
hspi.begin(_SCK, _BUSY, _MOSI, _CS);

48
src/publisher/pubMqtt.h

@ -49,19 +49,21 @@ class PubMqtt {
mRxCnt = 0;
mTxCnt = 0;
mSubscriptionCb = NULL;
memset(mLastIvState, MQTT_STATUS_NOT_AVAIL_NOT_PROD, MAX_NUM_INVERTERS);
memset(mLastIvState, (uint8_t)InverterStatus::OFF, MAX_NUM_INVERTERS);
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4);
mLastAnyAvail = false;
mZeroValues = false;
}
~PubMqtt() { }
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) {
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) {
mCfgMqtt = cfg_mqtt;
mDevName = devName;
mVersion = version;
mSys = sys;
mUtcTimestamp = utcTs;
mUptime = uptime;
mIntervalTimeout = 1;
mSendIvData.setup(sys, utcTs, &mSendList);
@ -119,14 +121,14 @@ class PubMqtt {
else { // send mqtt data in a fixed interval
if(mIntervalTimeout == 0) {
mIntervalTimeout = mCfgMqtt->interval;
mSendList.push(RealTimeRunData_Debug);
mSendList.push(sendListCmdIv(RealTimeRunData_Debug, NULL));
sendIvData();
}
}
}
void tickerMinute() {
snprintf(mVal, 40, "%ld", millis() / 1000);
snprintf(mVal, 40, "%d", (*mUptime));
publish(subtopics[MQTT_UPTIME], mVal);
publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str());
publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str());
@ -165,10 +167,10 @@ class PubMqtt {
publish(mSubTopic, mVal, true);
}
void payloadEventListener(uint8_t cmd) {
void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
if(mClient.connected()) { // prevent overflow if MQTT broker is not reachable but set
if((0 == mCfgMqtt->interval) || (RealTimeRunData_Debug != cmd)) // no interval or no live data
mSendList.push(cmd);
mSendList.push(sendListCmdIv(cmd, iv));
}
}
@ -239,6 +241,10 @@ class PubMqtt {
}
}
void setZeroValuesEnable(void) {
mZeroValues = true;
}
private:
void onConnect(bool sessionPreset) {
DPRINTLN(DBG_INFO, F("MQTT connected"));
@ -308,7 +314,7 @@ class PubMqtt {
delete[] pyld;
}
const char *p = topic;
const char *p = topic + strlen(mCfgMqtt->topic);
uint8_t pos = 0;
uint8_t elm = 0;
char tmp[30];
@ -482,24 +488,22 @@ class PubMqtt {
rec = iv->getRecordStruct(RealTimeRunData_Debug);
// inverter status
uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD;
if (iv->isAvailable(*mUtcTimestamp)) {
iv->isProducing(); // recalculate status
if (iv->isAvailable())
anyAvail = true;
status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD;
}
else // inverter is enabled but not available
allAvail = false;
if(mLastIvState[id] != status) {
if(mLastIvState[id] != iv->status) {
// if status changed from producing to not producing send last data immediately
if (MQTT_STATUS_AVAIL_PROD == mLastIvState[id])
if (InverterStatus::WAS_PRODUCING == mLastIvState[id])
sendData(iv, RealTimeRunData_Debug);
mLastIvState[id] = status;
mLastIvState[id] = iv->status;
changed = true;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(mVal, 40, "%d", status);
snprintf(mVal, 40, "%d", (uint8_t)iv->status);
publish(mSubTopic, mVal, true);
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name);
@ -545,7 +549,7 @@ class PubMqtt {
switch (rec->assign[i].fieldId) {
case FLD_YT:
case FLD_YD:
if ((rec->assign[i].ch == CH0) && (!iv->isProducing(*mUtcTimestamp))) // avoids returns to 0 on restart
if ((rec->assign[i].ch == CH0) && (!iv->isProducing())) // avoids returns to 0 on restart
continue;
retained = true;
break;
@ -564,12 +568,13 @@ class PubMqtt {
void sendIvData() {
bool anyAvail = processIvStatus();
if (mLastAnyAvail != anyAvail)
mSendList.push(RealTimeRunData_Debug); // makes sure that total values are calculated
mSendList.push(sendListCmdIv(RealTimeRunData_Debug, NULL)); // makes sure that total values are calculated
if(mSendList.empty())
return;
mSendIvData.start();
mSendIvData.start(mZeroValues);
mZeroValues = false;
mLastAnyAvail = anyAvail;
}
@ -582,13 +587,14 @@ class PubMqtt {
HMSYSTEM *mSys;
PubMqttIvData<HMSYSTEM> mSendIvData;
uint32_t *mUtcTimestamp;
uint32_t *mUtcTimestamp, *mUptime;
uint32_t mRxCnt, mTxCnt;
std::queue<uint8_t> mSendList;
std::queue<sendListCmdIv> mSendList;
std::queue<alarm_t> mAlarmList;
subscriptionCb mSubscriptionCb;
bool mLastAnyAvail;
uint8_t mLastIvState[MAX_NUM_INVERTERS];
bool mZeroValues;
InverterStatus mLastIvState[MAX_NUM_INVERTERS];
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
uint16_t mIntervalTimeout;

102
src/publisher/pubMqttIvData.h

@ -12,14 +12,21 @@
typedef std::function<void(const char *subTopic, const char *payload, bool retained)> pubMqttPublisherType;
struct sendListCmdIv {
uint8_t cmd;
Inverter<> *iv;
sendListCmdIv(uint8_t a, Inverter<> *i) : cmd(a), iv(i) {}
};
template<class HMSYSTEM>
class PubMqttIvData {
public:
void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<uint8_t> *sendList) {
void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) {
mSys = sys;
mUtcTimestamp = utcTs;
mSendList = sendList;
mState = IDLE;
mZeroValues = false;
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4);
mRTRDataHasBeenSent = false;
@ -36,10 +43,11 @@ class PubMqttIvData {
yield();
}
bool start(void) {
bool start(bool zeroValues = false) {
if(IDLE != mState)
return false;
mZeroValues = zeroValues;
mRTRDataHasBeenSent = false;
mState = START;
return true;
@ -59,10 +67,14 @@ class PubMqttIvData {
void stateStart() {
mLastIvId = 0;
mTotalFound = false;
mSendTotalYd = true;
mAllTotalFound = true;
if(!mSendList->empty()) {
mCmd = mSendList->front();
mCmd = mSendList->front().cmd;
mIvSend = mSendList->front().iv;
if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) {
if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) { // send RealTimeRunData only once
mSendTotals = (RealTimeRunData_Debug == mCmd);
memset(mTotal, 0, sizeof(float) * 4);
mState = FIND_NXT_IV;
@ -88,12 +100,14 @@ class PubMqttIvData {
mLastIvId++;
mPos = 0;
if(found)
if(found) {
mIv->isProducing(); // recalculate status
mState = SEND_DATA;
else if(mSendTotals)
} else if(mSendTotals && mTotalFound)
mState = SEND_TOTALS;
else {
mSendList->pop();
mZeroValues = false;
mState = START;
}
}
@ -109,40 +123,43 @@ class PubMqttIvData {
if(mPos < rec->length) {
bool retained = false;
if (mCmd == RealTimeRunData_Debug) {
switch (rec->assign[mPos].fieldId) {
case FLD_YT:
case FLD_YD:
if ((rec->assign[mPos].ch == CH0) && (!mIv->isProducing(*mUtcTimestamp))) { // avoids returns to 0 on restart
mPos++;
return;
}
retained = true;
break;
}
if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId))
retained = true;
// calculate total values for RealTimeRunData_Debug
if (CH0 == rec->assign[mPos].ch) {
switch (rec->assign[mPos].fieldId) {
case FLD_PAC:
mTotal[0] += mIv->getValue(mPos, rec);
break;
case FLD_YT:
mTotal[1] += mIv->getValue(mPos, rec);
break;
case FLD_YD:
mTotal[2] += mIv->getValue(mPos, rec);
break;
case FLD_PDC:
mTotal[3] += mIv->getValue(mPos, rec);
break;
}
if(mIv->status > InverterStatus::STARTING) {
mTotalFound = true;
switch (rec->assign[mPos].fieldId) {
case FLD_PAC:
mTotal[0] += mIv->getValue(mPos, rec);
break;
case FLD_YT:
mTotal[1] += mIv->getValue(mPos, rec);
break;
case FLD_YD: {
float val = mIv->getValue(mPos, rec);
if(0 == val) // inverter restarted during day
mSendTotalYd = false;
else
mTotal[2] += val;
break;
}
case FLD_PDC:
mTotal[3] += mIv->getValue(mPos, rec);
break;
}
} else
mAllTotalFound = false;
}
} else
mIvLastRTRpub[mIv->id] = lastTs;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec)));
mPublish(mSubTopic, mVal, retained);
if((mIvSend == mIv) || (NULL == mIvSend)) { // send only updated values, or all if the inverter is NULL
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec)));
mPublish(mSubTopic, mVal, retained);
}
mPos++;
} else
mState = FIND_NXT_IV;
@ -152,6 +169,7 @@ class PubMqttIvData {
void stateSendTotals() {
uint8_t fieldId;
mRTRDataHasBeenSent = true;
if(mPos < 4) {
bool retained = true;
switch (mPos) {
@ -161,9 +179,17 @@ class PubMqttIvData {
retained = false;
break;
case 1:
if(!mAllTotalFound) {
mPos++;
return;
}
fieldId = FLD_YT;
break;
case 2:
if((!mAllTotalFound) || (!mSendTotalYd)) {
mPos++;
return;
}
fieldId = FLD_YD;
break;
case 3:
@ -177,10 +203,9 @@ class PubMqttIvData {
mPos++;
} else {
mSendList->pop();
mZeroValues = false;
mState = START;
}
mRTRDataHasBeenSent = true;
}
HMSYSTEM *mSys;
@ -191,18 +216,19 @@ class PubMqttIvData {
uint8_t mCmd;
uint8_t mLastIvId;
bool mSendTotals;
bool mSendTotals, mTotalFound, mAllTotalFound, mSendTotalYd;
float mTotal[4];
Inverter<> *mIv;
Inverter<> *mIv, *mIvSend;
uint8_t mPos;
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
bool mRTRDataHasBeenSent;
char mSubTopic[32 + MAX_NAME_LENGTH + 1];
char mVal[40];
bool mZeroValues; // makes sure that yield day is sent even if no inverter is online
std::queue<uint8_t> *mSendList;
std::queue<sendListCmdIv> *mSendList;
};
#endif /*__PUB_MQTT_IV_DATA_H__*/

2
src/publisher/pubSerial.h

@ -28,7 +28,7 @@ class PubSerial {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv->isAvailable(*mUtcTimestamp)) {
if (iv->isAvailable()) {
DPRINTLN(DBG_INFO, "Iv: " + String(id));
for (uint8_t i = 0; i < rec->length; i++) {
if (0.0f != iv->getValue(i, rec)) {

1
src/utils/dbg.cpp

@ -1,3 +1,4 @@
#include "dbg.h"
DBG_CB mCb = NULL;
bool mDebugEn = true;

23
src/utils/dbg.h

@ -39,6 +39,7 @@
#ifdef ARDUINO
#define DBG_CB std::function<void(String)>
extern DBG_CB mCb;
extern bool mDebugEn;
inline void registerDebugCb(DBG_CB cb) {
mCb = cb;
@ -48,22 +49,28 @@
#define DSERIAL Serial
#endif
inline void setDebugEn(bool en) {
mDebugEn = en;
}
//template <class T>
inline void DBGPRINT(String str) { DSERIAL.print(str); if(NULL != mCb) mCb(str); }
inline void DBGPRINT(String str, bool ser = true) { if(ser && mDebugEn) DSERIAL.print(str); if(NULL != mCb) mCb(str); }
//template <class T>
inline void DBGPRINTLN(String str) { DBGPRINT(str); DBGPRINT(F("\r\n")); }
inline void DHEX(uint8_t b) {
if( b<0x10 ) DSERIAL.print(F("0"));
DSERIAL.print(b,HEX);
inline void DBGPRINTLN(String str, bool ser = true) { DBGPRINT(str); DBGPRINT(F("\r\n")); }
inline void DHEX(uint8_t b, bool ser = true) {
if(ser && mDebugEn) {
if( b<0x10 ) DSERIAL.print(F("0"));
DSERIAL.print(b,HEX);
}
if(NULL != mCb) {
if( b<0x10 ) mCb(F("0"));
mCb(String(b, HEX));
}
}
inline void DBGHEXLN(uint8_t b) {
DHEX(b);
DBGPRINT(F("\r\n"));
inline void DBGHEXLN(uint8_t b, bool ser = true) {
DHEX(b, ser);
DBGPRINT(F("\r\n"), ser);
}
/*inline void DHEX(uint16_t b) {
if( b<0x10 ) DSERIAL.print(F("000"));

18
src/utils/helper.cpp

@ -4,6 +4,7 @@
//-----------------------------------------------------------------------------
#include "helper.h"
#include "dbg.h"
namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr) {
@ -40,6 +41,15 @@ namespace ah {
return String(str);
}
String getDateTimeStrFile(time_t t) {
char str[20];
if(0 == t)
sprintf(str, "na");
else
sprintf(str, "%04d-%02d-%02d_%02d-%02d-%02d", year(t), month(t), day(t), hour(t), minute(t), second(t));
return String(str);
}
String getTimeStr(time_t t) {
char str[9];
if(0 == t)
@ -64,4 +74,12 @@ namespace ah {
}
return ret;
}
void dumpBuf(uint8_t buf[], uint8_t len) {
for(uint8_t i = 0; i < len; i++) {
DHEX(buf[i]);
DBGPRINT(" ");
}
DBGPRINTLN("");
}
}

20
src/utils/helper.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de
// 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
@ -20,13 +20,31 @@ static Timezone gTimezone(CEST, CET);
#define CHECK_MASK(a,b) ((a & b) == b)
#define CP_U32_LittleEndian(buf, v) ({ \
uint8_t *b = buf; \
b[0] = ((v >> 24) & 0xff); \
b[1] = ((v >> 16) & 0xff); \
b[2] = ((v >> 8) & 0xff); \
b[3] = ((v ) & 0xff); \
})
#define CP_U32_BigEndian(buf, v) ({ \
uint8_t *b = buf; \
b[3] = ((v >> 24) & 0xff); \
b[2] = ((v >> 16) & 0xff); \
b[1] = ((v >> 8) & 0xff); \
b[0] = ((v ) & 0xff); \
})
namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr);
void ip2Char(uint8_t ip[], char *str);
double round3(double value);
String getDateTimeStr(time_t t);
String getDateTimeStrFile(time_t t);
String getTimeStr(time_t t);
uint64_t Serial2u64(const char *val);
void dumpBuf(uint8_t buf[], uint8_t len);
}
#endif /*__HELPER_H__*/

221
src/utils/improv.h

@ -0,0 +1,221 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __IMPROV_H__
#define __IMPROV_H__
#include <cstring>
#include <functional>
#include "dbg.h"
#include "AsyncJson.h"
// https://www.improv-wifi.com/serial/
// https://github.com/jnthas/improv-wifi-demo/blob/main/src/esp32-wifiimprov/esp32-wifiimprov.ino
// configure ESP through Serial interface
#if !defined(ETHERNET)
class Improv {
public:
void setup(IApp *app, const char *devName, const char *version) {
mApp = app;
mDevName = devName;
mVersion = version;
mScanRunning = false;
}
void tickSerial(void) {
if(mScanRunning)
getNetworks();
if(Serial.available() == 0)
return;
uint8_t buf[40];
uint8_t len = Serial.readBytes(buf, 40);
if(!checkPaket(&buf[0], len, [this](uint8_t type, uint8_t buf[], uint8_t len) {
parsePayload(type, buf, len);
})) {
DBGPRINTLN(F("check paket failed"));
}
dumpBuf(buf, len);
}
private:
enum State : uint8_t {
STATE_STOPPED = 0x00,
STATE_AWAITING_AUTHORIZATION = 0x01,
STATE_AUTHORIZED = 0x02,
STATE_PROVISIONING = 0x03,
STATE_PROVISIONED = 0x04,
};
enum Command : uint8_t {
UNKNOWN = 0x00,
WIFI_SETTINGS = 0x01,
IDENTIFY = 0x02,
GET_CURRENT_STATE = 0x02,
GET_DEVICE_INFO = 0x03,
GET_WIFI_NETWORKS = 0x04,
BAD_CHECKSUM = 0xFF,
};
enum ImprovSerialType : uint8_t {
TYPE_CURRENT_STATE = 0x01,
TYPE_ERROR_STATE = 0x02,
TYPE_RPC = 0x03,
TYPE_RPC_RESPONSE = 0x04
};
void dumpBuf(uint8_t buf[], uint8_t len) {
for(uint8_t i = 0; i < len; i++) {
DHEX(buf[i], false);
DBGPRINT(" ", false);
}
DBGPRINTLN("", false);
}
inline uint8_t buildChecksum(uint8_t buf[], uint8_t len) {
uint8_t calc = 0;
for(uint8_t i = 0; i < len; i++) {
calc += buf[i];
}
return calc;
}
inline bool checkChecksum(uint8_t buf[], uint8_t len) {
/*DHEX(buf[len], false);
DBGPRINT(F(" == "), false);
DBGHEXLN(buildChecksum(buf, len), false);*/
return ((buildChecksum(buf, len)) == buf[len]);
}
bool checkPaket(uint8_t buf[], uint8_t len, std::function<void(uint8_t type, uint8_t b[], uint8_t l)> cb) {
if(len < 11)
return false;
if(0 != strncmp((char*)buf, "IMPROV", 6))
return false;
// verison check (only version 1 is supported!)
if(0x01 != buf[6])
return false;
if(!checkChecksum(buf, (9 + buf[8])))
return false;
cb(buf[7], &buf[9], buf[8]);
return true;
}
uint8_t char2Improv(const char *str, uint8_t buf[]) {
uint8_t len = strlen(str);
buf[0] = len;
for(uint8_t i = 1; i <= len; i++) {
buf[i] = (uint8_t)str[i-1];
}
return len + 1;
}
void sendDevInfo(void) {
uint8_t buf[50];
buf[7] = TYPE_RPC_RESPONSE;
buf[9] = GET_DEVICE_INFO; // repsonse to cmd
uint8_t p = 11;
// firmware name
p += char2Improv("AhoyDTU", &buf[p]);
// firmware version
p += char2Improv(mVersion, &buf[p]);
// chip variant
#if defined(ESP32)
p += char2Improv("ESP32", &buf[p]);
#else
p += char2Improv("ESP8266", &buf[p]);
#endif
// device name
p += char2Improv(mDevName, &buf[p]);
buf[10] = p - 11; // sub length
buf[8] = p - 9; // paket length
sendPaket(buf, p);
}
void getNetworks(void) {
if(!mScanRunning)
mApp->scanAvailNetworks();
JsonObject obj;
if(!mApp->getAvailNetworks(obj))
return;
mScanRunning = false;
uint8_t buf[50];
buf[7] = TYPE_RPC_RESPONSE;
buf[9] = GET_WIFI_NETWORKS; // repsonse to cmd
uint8_t p = 11;
JsonArray arr = obj[F("networks")];
for(uint8_t i = 0; i < arr.size(); i++) {
buf[p++] = strlen(arr[i][F("ssid")]);
// ssid
p += char2Improv(arr[i][F("ssid")], &buf[p]);
buf[p++] = String(arr[i][F("rssi")]).length();
// rssi
p += char2Improv(String(arr[i][F("rssi")]).c_str(), &buf[p]);
buf[10] = p - 11; // sub length
buf[8] = p - 9; // paket length
sendPaket(buf, p);
}
}
void setState(uint8_t state) {
uint8_t buf[20];
buf[7] = TYPE_CURRENT_STATE;
buf[8] = 0x01;
buf[9] = state;
sendPaket(buf, 10);
}
void sendPaket(uint8_t buf[], uint8_t len) {
buf[0] = 'I';
buf[1] = 'M';
buf[2] = 'P';
buf[3] = 'R';
buf[4] = 'O';
buf[5] = 'V';
buf[6] = 1; // protocol version
buf[len] = buildChecksum(buf, len);
len++;
Serial.write(buf, len);
dumpBuf(buf, len);
}
void parsePayload(uint8_t type, uint8_t buf[], uint8_t len) {
if(TYPE_RPC == type) {
if(GET_CURRENT_STATE == buf[0]) {
setDebugEn(false);
setState(STATE_AUTHORIZED);
}
else if(GET_DEVICE_INFO == buf[0])
sendDevInfo();
else if(GET_WIFI_NETWORKS == buf[0])
getNetworks();
}
}
IApp *mApp;
const char *mDevName, *mVersion;
bool mScanRunning;
};
#endif
#endif /*__IMPROV_H__*/

6
src/utils/scheduler.h

@ -31,9 +31,9 @@ namespace ah {
public:
Scheduler() {}
void setup() {
void setup(bool directStart) {
mUptime = 0;
mTimestamp = 0;
mTimestamp = (directStart) ? 1 : 0;
mMax = 0;
mPrevMillis = millis();
resetTicker();
@ -117,6 +117,7 @@ namespace ah {
protected:
uint32_t mTimestamp;
uint32_t mUptime;
private:
inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) {
@ -162,7 +163,6 @@ namespace ah {
sP mTicker[MAX_NUM_TICKER];
bool mTickerInUse[MAX_NUM_TICKER];
uint32_t mMillis, mPrevMillis, mDiff;
uint32_t mUptime;
uint8_t mDiffSeconds;
uint8_t mMax;
};

66
src/web/RestApi.h

@ -28,9 +28,10 @@
#endif
const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q};
const uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q};
const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR};
template <class HMSYSTEM>
template<class HMSYSTEM, class HMRADIO>
class RestApi {
public:
RestApi() {
@ -41,10 +42,11 @@ class RestApi {
nr = 0;
}
void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) {
void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, AsyncWebServer *srv, settings_t *config) {
mApp = app;
mSrv = srv;
mSys = sys;
mRadio = radio;
mConfig = config;
mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody(
@ -188,9 +190,12 @@ class RestApi {
response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp);
}
String filename = ah::getDateTimeStrFile(gTimezone.toLocal(mApp->getTimestamp()));
filename += "_v" + String(mApp->getVersion());
response->addHeader("Content-Type", "application/octet-stream");
response->addHeader("Content-Description", "File Transfer");
response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json");
response->addHeader("Content-Disposition", "attachment; filename=" + filename + "_ahoy_setup.json");
request->send(response);
fp.close();
}
@ -198,6 +203,9 @@ class RestApi {
void getGeneric(AsyncWebServerRequest *request, JsonObject obj) {
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
obj[F("ts_uptime")] = mApp->getUptime();
obj[F("ts_now")] = mApp->getTimestamp();
obj[F("version")] = String(mApp->getVersion());
obj[F("build")] = String(AUTO_GIT_HASH);
obj[F("menu_prot")] = mApp->getProtection(request);
obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask );
obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0);
@ -212,9 +220,12 @@ class RestApi {
void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) {
#if !defined(ETHERNET)
obj[F("ssid")] = mConfig->sys.stationSsid;
obj[F("ap_pwd")] = mConfig->sys.apPwd;
obj[F("hidd")] = mConfig->sys.isHidden;
#endif /* !defined(ETHERNET) */
obj[F("device_name")] = mConfig->sys.deviceName;
obj[F("dark_mode")] = (bool)mConfig->sys.darkMode;
obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot;
obj[F("mac")] = WiFi.macAddress();
obj[F("hostname")] = mConfig->sys.deviceName;
@ -228,7 +239,7 @@ class RestApi {
obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb
getGeneric(request, obj);
getRadio(obj.createNestedObject(F("radio")));
getRadioNrf(obj.createNestedObject(F("radio")));
getStatistics(obj.createNestedObject(F("statistics")));
#if defined(ESP32)
@ -300,8 +311,8 @@ class RestApi {
obj[F("rx_fail")] = stat->rxFail;
obj[F("rx_fail_answer")] = stat->rxFailNoAnser;
obj[F("frame_cnt")] = stat->frmCnt;
obj[F("tx_cnt")] = mSys->Radio.mSendCnt;
obj[F("retransmits")] = mSys->Radio.mRetransmits;
obj[F("tx_cnt")] = mRadio->mSendCnt;
obj[F("retransmits")] = mRadio->mRetransmits;
}
void getInverterList(JsonObject obj) {
@ -320,7 +331,7 @@ class RestApi {
obj2[F("version")] = String(iv->getFwVersion());
for(uint8_t j = 0; j < iv->channels; j ++) {
obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j];
obj2[F("ch_yield_cor")][j] = (double)iv->config->yieldCor[j];
obj2[F("ch_name")][j] = iv->config->chName[j];
obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j];
}
@ -332,6 +343,8 @@ class RestApi {
obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight;
obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop;
obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime;
obj[F("yldEff")] = mConfig->inst.yieldEffiency;
}
void getInverter(JsonObject obj, uint8_t id) {
@ -345,6 +358,7 @@ class RestApi {
obj[F("version")] = String(iv->getFwVersion());
obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit);
obj[F("ts_last_success")] = rec->ts;
obj[F("generation")] = iv->ivGen;
JsonArray ch = obj.createNestedArray("ch");
@ -352,10 +366,17 @@ class RestApi {
uint8_t pos;
obj[F("ch_name")][0] = "AC";
JsonArray ch0 = ch.createNestedArray();
if(IV_HMT == iv->ivGen) {
for (uint8_t fld = 0; fld < sizeof(acListHmt); fld++) {
pos = (iv->getPosByChFld(CH0, acListHmt[fld], rec));
ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
}
} else {
for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
pos = (iv->getPosByChFld(CH0, acList[fld], rec));
ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
}
}
// DC
for(uint8_t j = 0; j < iv->channels; j ++) {
@ -382,6 +403,7 @@ class RestApi {
void getNtp(JsonObject obj) {
obj[F("addr")] = String(mConfig->ntp.addr);
obj[F("port")] = String(mConfig->ntp.port);
obj[F("interval")] = String(mConfig->ntp.interval);
}
void getSun(JsonObject obj) {
@ -403,11 +425,19 @@ class RestApi {
obj[F("led_high_active")] = mConfig->led.led_high_active;
}
void getRadio(JsonObject obj) {
void getRadioCmt(JsonObject obj) {
obj[F("csb")] = mConfig->cmt.pinCsb;
obj[F("fcsb")] = mConfig->cmt.pinFcsb;
obj[F("irq")] = mConfig->cmt.pinIrq;
obj[F("en")] = (bool) mConfig->cmt.enabled;
}
void getRadioNrf(JsonObject obj) {
obj[F("power_level")] = mConfig->nrf.amplifierPower;
obj[F("isconnected")] = mSys->Radio.isChipConnected();
obj[F("DataRate")] = mSys->Radio.getDataRate();
obj[F("isPVariant")] = mSys->Radio.isPVariant();
obj[F("isconnected")] = mRadio->isChipConnected();
obj[F("DataRate")] = mRadio->getDataRate();
obj[F("isPVariant")] = mRadio->isPVariant();
obj[F("en")] = (bool) mConfig->nrf.enabled;
}
void getSerial(JsonObject obj) {
@ -458,16 +488,16 @@ class RestApi {
invObj[F("id")] = i;
invObj[F("name")] = String(iv->config->name);
invObj[F("version")] = String(iv->getFwVersion());
invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp());
invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp());
invObj[F("is_avail")] = iv->isAvailable();
invObj[F("is_producing")] = iv->isProducing();
invObj[F("ts_last_success")] = iv->getLastTs(rec);
}
}
JsonArray warn = obj.createNestedArray(F("warnings"));
if(!mSys->Radio.isChipConnected())
warn.add(F("your NRF24 module can't be reached, check the wiring and pinout"));
else if(!mSys->Radio.isPVariant())
if(!mRadio->isChipConnected() && mConfig->nrf.enabled)
warn.add(F("your NRF24 module can't be reached, check the wiring, pinout and enable"));
else if(!mRadio->isPVariant() && mConfig->nrf.enabled)
warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible"));
if(!mApp->getSettingsValid())
warn.add(F("your settings are invalid"));
@ -496,7 +526,8 @@ class RestApi {
getNtp(obj.createNestedObject(F("ntp")));
getSun(obj.createNestedObject(F("sun")));
getPinout(obj.createNestedObject(F("pinout")));
getRadio(obj.createNestedObject(F("radio")));
getRadioCmt(obj.createNestedObject(F("radioCmt")));
getRadioNrf(obj.createNestedObject(F("radioNrf")));
getSerial(obj.createNestedObject(F("serial")));
getStaticIp(obj.createNestedObject(F("static_ip")));
getDisplay(obj.createNestedObject(F("display")));
@ -620,6 +651,7 @@ class RestApi {
IApp *mApp;
HMSYSTEM *mSys;
HMRADIO *mRadio;
AsyncWebServer *mSrv;
settings_t *mConfig;

10
src/web/html/api.js

@ -78,8 +78,10 @@ function parseNav(obj) {
if(i == 2)
continue;
var l = document.getElementById("nav"+i);
if(window.location.pathname == "/" + l.href.substring(0, l.href.indexOf("?")).split('/').pop())
l.classList.add("active");
if(window.location.pathname == "/" + l.href.substring(0, l.href.indexOf("?")).split('/').pop()) {
if((i != 8 )&& (i != 9))
l.classList.add("active");
}
if(obj["menu_protEn"]) {
if(obj["menu_prot"]) {
@ -117,6 +119,10 @@ function parseRssi(obj) {
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"]));
}
function toIsoDateStr(d) {
return new Date(d.getTime() + (d.getTimezoneOffset() * -60000)).toISOString().substring(0, 19).replace('T', ', ');
}
function setHide(id, hide) {
var elm = document.getElementById(id);
if(hide) {

16
src/web/html/index.html

@ -62,10 +62,6 @@
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
}
function ts2Span(ts) {
return span(new Date(ts * 1000).toLocaleString('de-DE'));
}
function parseGeneric(obj) {
if(exeOnce)
parseESP(obj);
@ -88,8 +84,12 @@
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
var dSpan = document.getElementById("date");
if(0 != obj["ts_now"])
dSpan.innerHTML = date.toLocaleString('de-DE');
if(0 != obj["ts_now"]) {
if(obj["ts_now"] < 1680000000)
setTime();
else
dSpan.innerHTML = toIsoDateStr(date);
}
else {
dSpan.innerHTML = "";
var e = inp("set", "sync from browser", 0, ["btn"], "set", "button");
@ -153,7 +153,7 @@
if(false == i["is_avail"]) {
if(i["ts_last_success"] > 0) {
var date = new Date(i["ts_last_success"] * 1000);
p.append(span("-> last successful transmission: " + date.toLocaleString('de-DE')), br());
p.append(span("-> last successful transmission: " + toIsoDateStr(date)), br());
}
}
}
@ -186,7 +186,7 @@
function tick() {
if(0 != ts)
document.getElementById("date").innerHTML = (new Date((++ts) * 1000)).toLocaleString('de-DE');
document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000)));
if(++tickCnt >= 10) {
tickCnt = 0;
getAjax('/api/index', parse);

188
src/web/html/setup.html

@ -9,6 +9,7 @@
<div id="wrapper">
<div id="content">
<form method="post" action="/save" id="settings">
<fieldset>
<button type="button" class="s_collapsible mt-4">System Config</button>
<div class="s_content">
<fieldset class="mb-2">
@ -17,10 +18,14 @@
<div class="col-12 col-sm-3">Device Name</div>
<div class="col-12 col-sm-9"><input type="text" name="device"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Reboot Ahoy at midnight</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="schedReboot"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Dark Mode</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div>
<div class="col-8 col-sm-3">(empty browser cache or use CTRL + F5 after reboot to apply this setting)</div>
<div class="col-12">(empty browser cache or use CTRL + F5 after reboot to apply this setting)</div>
</div>
</fieldset>
<fieldset class="mb-4">
@ -31,6 +36,9 @@
<p class="des">Radio (NRF24L01+)</p>
<div id="rf24"></div>
<p class="des">Radio (CMT2300A)</p>
<div id="cmt"><div class="col-12">(ESP32 only)</div></div>
<p class="des">Serial Console</p>
<div class="row mb-3">
<div class="col-8 col-sm-3">print inverter data</div>
@ -51,6 +59,12 @@
<div class="s_content">
<fieldset class="mb-2">
<legend class="des">WiFi</legend>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">AP Password (min. length: 8)</div>
<div class="col-12 col-sm-9"><input type="text" name="ap_pwd" minlength="8" /></div>
</div>
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p>
<div class="row mb-3">
@ -70,6 +84,10 @@
<div class="col-12 col-sm-3 my-2">SSID</div>
<div class="col-12 col-sm-9"><input type="text" name="ssid"/></div>
</div>
<div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3">SSID is hidden</div>
<div class="col-12 col-sm-9"><input type="checkbox" name="hidd"/></div>
</div>
<div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3 my-2">Password</div>
<div class="col-12 col-sm-9"><input type="password" name="pwd" value="{PWD}"/></div>
@ -154,6 +172,14 @@
<div class="col-8 col-sm-3">Reset values when inverter status is 'not available'</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstNotAvail"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Start without time sync (useful in AP-Only-Mode)</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="strtWthtTm"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Yield Effiency (should be between 0.95 and 0.96)</div>
<div class="col-4 col-sm-9"><input type="number" name="yldEff" step="any"/></div>
</div>
</fieldset>
</div>
@ -170,13 +196,21 @@
<div class="col-12 col-sm-9"><input type="number" name="ntpPort"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">set system time</div>
<div class="col-12 col-sm-3 my-2">NTP Intervall (in Minutes, min. 5 Minutes)</div>
<div class="col-12 col-sm-9"><input type="number" name="ntpIntvl"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">set System time</div>
<div class="col-12 col-sm-9">
<input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/>
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/>
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/><br/>
<span id="apiResultNtp"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">System Time</div>
<div class="col-12 col-sm-9 my-2"><span id="date"></span></div>
</div>
</fieldset>
</div>
@ -258,7 +292,7 @@
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Luminance</div>
<div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="100"></select></div>
<div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></select></div>
</div>
<p class="des">Pinout</p>
<div id="dispPins"></div>
@ -303,6 +337,7 @@
<script type="text/javascript">
var highestId = 0;
var maxInv = 0;
var ts = 0;
var esp8266pins = [
[255, "off / default"],
@ -406,7 +441,7 @@
[1, "high active"],
];
const re = /11[2,4,6]1.*/;
const re = /1[1,3][2,4,6,8][1,2,4].*/;
window.onload = function() {
for(it of document.getElementsByClassName("s_collapsible")) {
@ -428,7 +463,7 @@
document.getElementById("btnAdd").addEventListener("click", function() {
if(highestId <= (maxInv-1)) {
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_pwr":[0,0,0,0],"ch_name":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId);
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":6,"ch_max_pwr":[0,0,0,0,0,0],"ch_name":["","","","","",""],"ch_yield_cor":[0,0,0,0,0,0]}'), highestId);
}
});
@ -444,11 +479,17 @@
function apiCbNtp(obj) {
var e = document.getElementById("apiResultNtp");
if(obj["success"])
e.innerHTML = "command excuted";
e.innerHTML = "command excuted, set new time ...";
else
e.innerHTML = "Error: " + obj["error"];
}
function apiCbNtp2(obj) {
var e = document.getElementById("apiResultNtp");
var date = new Date(obj["ts_now"] * 1000);
e.innerHTML = "synced at: " + toIsoDateStr(date) + ", difference: " + (ts - obj["ts_now"]) + "ms";
}
function apiCbMqtt(obj) {
var e = document.getElementById("apiResultMqtt");
if(obj["success"])
@ -463,6 +504,7 @@
obj.cmd = "set_time";
obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000);
}
function scan() {
@ -476,6 +518,7 @@
var obj = new Object();
obj.cmd = "sync_ntp";
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000);
}
function sendDiscoveryConfig() {
@ -535,7 +578,7 @@
addr.addEventListener(evt, (e) => {
var serial = addr.value.substring(0,4);
var max = 0;
for(var i=0;i<4;i++) {
for(var i=0;i<6;i++) {
setHide(id+"ModPwr"+i, true);
setHide(id+"ModName"+i, true);
setHide(id+"YieldCor"+i, true);
@ -545,12 +588,13 @@
setHide("row"+id+"YieldCor", true);
if(serial.charAt(0) == 1) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) {
if((serial.charAt(3) == 1) || (serial.charAt(3) == 2)) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1) || (serial.charAt(1) == 3)) {
if((serial.charAt(3) == 1) || (serial.charAt(3) == 2) || (serial.charAt(3) == 4)) {
switch(serial.charAt(2)) {
case "2": max = 1; break;
case "4": max = 2; break;
case "6": max = 4; break;
case "8": max = 6; break;
}
}
}
@ -574,7 +618,7 @@
for(var j of [
["ModPwr", "ch_max_pwr", "Max Module Power (Wp)", 4, "[0-9]+"],
["ModName", "ch_name", "Module Name", 15, null],
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 8, "[0-9-]+"]]) {
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 8, "[0-9-\.]+"]]) {
var cl = (re.test(obj["serial"])) ? "" : " hide";
@ -603,16 +647,19 @@
}
function ivGlob(obj) {
for(var i of [["invInterval", "interval"], ["invRetry", "retries"]])
for(var i of [["invInterval", "interval"], ["invRetry", "retries"], ["yldEff", "yldEff"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
for(var i of [["Mid", "rstMid"], ["ComStop", "rstComStop"], ["NotAvail", "rstNAvail"]])
document.getElementsByName("invRst"+i[0])[0].checked = obj[i[1]];
document.getElementsByName("strtWthtTm")[0].checked = obj["strtWthtTm"];
}
function parseSys(obj) {
for(var i of [["device", "device_name"], ["ssid", "ssid"]])
for(var i of [["device", "device_name"], ["ssid", "ssid"], ["ap_pwd", "ap_pwd"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
document.getElementsByName("hidd")[0].checked = obj["hidd"];
document.getElementsByName("darkMode")[0].checked = obj["dark_mode"];
document.getElementsByName("schedReboot")[0].checked = obj["sched_reboot"];
e = document.getElementsByName("adminpwd")[0];
if(!obj["pwd_set"])
e.value = "";
@ -630,6 +677,9 @@
parseNav(obj);
parseESP(obj);
parseRssi(obj);
ts = obj["ts_now"];
window.setInterval("tick()", 1000);
}
function parseStaticIp(obj) {
@ -651,7 +701,7 @@
}
function parseNtp(obj) {
for(var i of [["ntpAddr", "addr"], ["ntpPort", "port"]])
for(var i of [["ntpAddr", "addr"], ["ntpPort", "port"], ["ntpIntvl", "interval"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
}
@ -667,11 +717,7 @@
function parsePinout(obj, type, system) {
var e = document.getElementById("pinout");
if ("ESP8266" == type) {
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']];
} else {
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso'], ['led0', 'pinLed0'], ['led1', 'pinLed1']];
}
pins = [['led0', 'pinLed0'], ['led1', 'pinLed1']];
for(p of pins) {
e.append(
ml("div", {class: "row mb-3"}, [
@ -689,11 +735,37 @@
sel('pinLedHighActive', led_high_active, obj['led_high_active'])
)
])
);
)
}
function parseRadio(obj) {
var e = document.getElementById("rf24").append(
function parseNrfRadio(obj, objPin, type, system) {
var e = document.getElementById("rf24");
var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox");
en.checked = obj["en"];
e.replaceChildren (
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3 my-2"}, "NRF24 Enable"),
ml("div", {class: "col-4 col-sm-9"}, en)
])
);
if ("ESP8266" == type) {
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
} else {
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']];
}
for(p of pins) {
e.append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, objPin[p[0]])
)
])
);
}
e.append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Power Level"),
ml("div", {class: "col-12 col-sm-9"},
@ -708,6 +780,30 @@
);
}
function parseCmtRadio(obj, type, system) {
var e = document.getElementById("cmt");
var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox");
en.checked = obj["en"];
e.replaceChildren (
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3 my-2"}, "CMT2300A Enable"),
ml("div", {class: "col-4 col-sm-9"}, en)
])
);
pins = [['csb', 'pinCsb'], ['fcsb', 'pinFcsb'], ['irq', 'pinGpio3']];
for(p of pins) {
e.append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[0]])
)
])
);
}
}
function parseSerial(obj) {
for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"]])
document.getElementsByName(i[0])[0].checked = obj[i[1]];
@ -719,6 +815,7 @@
document.getElementsByName(i)[0].checked = obj[i];
var e = document.getElementById("dispPins");
//KEEP this order !!!
var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']];
if("ESP32" == type)
pins.push(['busy', 'disp_bsy']);
@ -733,7 +830,8 @@
);
}
var opts = [[0, "None"], [1, "SSD1306 0.96\" 128X64"], [2, "SH1106 1.3\""], [3, "Nokia5110"], [4, "SSD1306 0.96\" 128X32"]];
// keep display types grouped
var opts = [[0, "None"], [2, "SH1106 1.3\" 128X64"], [5, "SSD1306 0.66\" 64X48 (Wemos OLED Shield)"], [4, "SSD1306 0.91\" 128X32"], [1, "SSD1306 0.96\" 128X64"], [3, "Nokia5110"]];
if("ESP32" == type)
opts.push([10, "ePaper"]);
var dispType = sel("disp_typ", opts, obj["disp_typ"]);
@ -744,7 +842,7 @@
])
);
dispType.addEventListener('change', (e) => {
hideDispPins(pins, e.target.value)
hideDispPins(pins, parseInt(e.target.value))
});
opts = [[0, "0&deg;"], [2, "180&deg;"]];
@ -764,26 +862,33 @@
}
function hideDispPins(pins, dispType) {
// create pin map for each display type.
// It depends on fix pin array (see var pins)
// var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']];
const pinMap = new Map([
[0, [0,0,0,0,0]], //none
[1, [1,1,0,0,0]], //SSD1306_128X64
[2, [1,1,0,0,0]], //SH1106_128X64
[3, [1,1,1,1,0]], //PCD8544_84X48 /nokia5110
[4, [1,1,0,0,0]], //SSD1306_128X32
[5, [1,1,0,0,0]], //SSD1306_64X48
[10, [1,1,1,1,1]] //ePaper
])
for(var i = 0; i < pins.length; i++) {
var cl = document.getElementById("row_" + pins[i][1]).classList;
if(0 == dispType)
cl.add("hide");
else if(dispType <= 2 || dispType == 4) { // OLED
if(i < 2)
cl.remove("hide");
else
cl.add("hide");
} else if(dispType == 3) { // Nokia
if(i < 4)
cl.remove("hide");
else
cl.add("hide");
} else // ePaper
if(pinMap.get(dispType)[i]) {
cl.remove("hide");
}
else {
cl.add("hide");
}
}
}
function tick() {
document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000)));
}
function parse(root) {
if(null != root) {
parseSys(root["system"]);
@ -793,7 +898,9 @@
parseNtp(root["ntp"]);
parseSun(root["sun"]);
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]);
parseRadio(root["radio"]);
parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]);
if(root["generic"]["esp_type"] == "ESP32")
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]);
parseSerial(root["serial"]);
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
getAjax("/api/inverter/list", parseIv);
@ -808,8 +915,7 @@
for(i = 0; i < root["networks"].length; i++) {
s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)"));
}
}
else
} else
s.appendChild(opt("-1", "no network found"));
}

5
src/web/html/style.css

@ -475,6 +475,11 @@ input[type=text], input[type=password], select, input[type=number] {
color: var(--fg);
}
input:invalid {
border: 2px solid #f00 !important;
background-color: #400 !important;
}
input.sh {
max-width: 150px !important;
margin-right: 10px;

3
src/web/html/visualization.html

@ -19,7 +19,6 @@
var units, ivEn;
var mIvHtml = [];
var mNum = 0;
var names = ["Voltage", "Current", "Power", "Yield Day", "Yield Total", "Irradiation"];
var total = Array(5).fill(0);
function parseGeneric(obj) {
@ -159,7 +158,7 @@
var ageInfo = "Last received data requested at: ";
if(ts > 0) {
var date = new Date(ts * 1000);
ageInfo += date.toLocaleString('de-DE');
ageInfo += toIsoDateStr(date);
}
else
ageInfo += "nothing received";

46
src/web/web.h

@ -38,7 +38,7 @@
#define WEB_SERIAL_BUF_SIZE 2048
const char *const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLedHighActive"};
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLedHighActive", "pinCsb", "pinFcsb", "pinGpio3"};
template <class HMSYSTEM>
class Web {
@ -459,10 +459,14 @@ class Web {
request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN);
if (request->arg("pwd") != "{PWD}")
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
if (request->arg("ap_pwd") != "")
request->arg("ap_pwd").toCharArray(mConfig->sys.apPwd, PWD_LEN);
mConfig->sys.isHidden = (request->arg("hidd") == "on");
#endif /* !defined(ETHERNET) */
if (request->arg("device") != "")
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
mConfig->sys.schedReboot = (request->arg("schedReboot") == "on");
// protection
if (request->arg("adminpwd") != "{PWD}") {
@ -499,8 +503,16 @@ class Web {
memset(buf, 0, 20);
iv->config->serial.u64 = ah::Serial2u64(buf);
switch(iv->config->serial.b[4]) {
case 0x24:
case 0x22:
case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break;
case 0x44:
case 0x42:
case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break;
case 0x64:
case 0x62:
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
default: break;
}
@ -509,8 +521,8 @@ class Web {
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
// max channel power / name
for (uint8_t j = 0; j < 4; j++) {
iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toInt();
for (uint8_t j = 0; j < 6; j++) {
iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toDouble();
iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH);
}
@ -524,13 +536,16 @@ class Web {
mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on");
mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on");
mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");
mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on");
mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
// pinout
uint8_t pin;
for (uint8_t i = 0; i < 9; i++) {
for (uint8_t i = 0; i < 12; i++) {
pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) {
default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break;
case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break;
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break;
case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break;
case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_SCLK_PIN); break;
@ -539,16 +554,24 @@ class Web {
case 6: mConfig->led.led0 = pin; break;
case 7: mConfig->led.led1 = pin; break;
case 8: mConfig->led.led_high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense
case 9: mConfig->cmt.pinCsb = pin; break;
case 10: mConfig->cmt.pinFcsb = pin; break;
case 11: mConfig->cmt.pinIrq = pin; break;
}
}
// nrf24 amplifier power
mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03;
mConfig->nrf.enabled = (request->arg("nrfEnable") == "on");
// cmt
mConfig->cmt.enabled = (request->arg("cmtEnable") == "on");
// ntp
if (request->arg("ntpAddr") != "") {
request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN);
mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff;
mConfig->ntp.interval = request->arg("ntpIntvl").toInt() & 0xffff;
}
// sun
@ -585,7 +608,7 @@ class Web {
mConfig->serial.debug = (request->arg("serDbg") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on");
// Needed to log TX buffers to serial console
mSys->Radio.mSerialDebug = mConfig->serial.debug;
// mSys->Radio.mSerialDebug = mConfig->serial.debug;
}
// display
@ -693,11 +716,15 @@ class Web {
// NRF Statistics
stat = mApp->getStatistics();
uint32_t nrfSendCnt;
uint32_t nrfRetransmits;
mApp->getNrfRadioCounters(&nrfSendCnt, &nrfRetransmits);
metrics += radioStatistic(F("rx_success"), stat->rxSuccess);
metrics += radioStatistic(F("rx_fail"), stat->rxFail);
metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser);
metrics += radioStatistic(F("frame_cnt"), stat->frmCnt);
metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt);
metrics += radioStatistic(F("tx_cnt"), nrfSendCnt);
metrics += radioStatistic(F("retrans_cnt"), nrfRetransmits);
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Next is Inverter information
@ -725,7 +752,7 @@ class Web {
case metricsStateInverter3: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable(mApp->getTimestamp());});
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable();});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
metricsStep = metricsStateInverter4;
break;
@ -733,7 +760,7 @@ class Web {
case metricsStateInverter4: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing(mApp->getTimestamp());});
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing();});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Start Realtime Field loop
metricsFieldId = FLD_UDC;
@ -767,7 +794,6 @@ class Web {
// Try inverter channel (channel 0) or any channel with maxPwr > 0
if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) {
if (metricsFieldId == iv->getByteAssign(metricsChannelId, rec)->fieldId) {
// This is the correct field to report
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec));

42
src/wifi/ahoywifi.cpp

@ -17,6 +17,12 @@
ahoywifi::ahoywifi() : mApIp(192, 168, 4, 1) {}
/**
* TODO: ESP32 has native strongest AP support!
* WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
*/
//-----------------------------------------------------------------------------
void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) {
mConfig = config;
@ -49,14 +55,16 @@ void ahoywifi::setupWifi(bool startAP = false) {
}
#endif
#if !defined(AP_ONLY)
if(mConfig->valid) {
#if !defined(FB_WIFI_OVERRIDDEN)
#if defined(FB_WIFI_OVERRIDDEN)
snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", FB_WIFI_SSID);
snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", FB_WIFI_PWD);
setupStation();
#else
if(mConfig->valid) {
if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) != 0)
setupStation();
#else
setupStation();
#endif
}
}
#endif
#endif
}
@ -100,9 +108,17 @@ void ahoywifi::tickWifiLoop() {
mScanCnt = 0;
mScanActive = true;
#if defined(ESP8266)
WiFi.scanNetworks(true, false, 0U, (uint8_t *)mConfig->sys.stationSsid);
WiFi.scanNetworks(true, true, 0U, ([this] () {
if(mConfig->sys.isHidden)
return (uint8_t *)NULL;
return (uint8_t *)(mConfig->sys.stationSsid);
})());
#else
WiFi.scanNetworks(true, false, false, 300U, 0U, mConfig->sys.stationSsid);
WiFi.scanNetworks(true, true, false, 300U, 0U, ([this] () {
if(mConfig->sys.isHidden)
return (char*)NULL;
return (mConfig->sys.stationSsid);
})());
#endif
return;
}
@ -157,7 +173,7 @@ void ahoywifi::setupAp(void) {
DBGPRINT(F("\n---------\nAP MODE\nSSID: "));
DBGPRINTLN(WIFI_AP_SSID);
DBGPRINT(F("PWD: "));
DBGPRINTLN(WIFI_AP_PWD);
DBGPRINTLN(mConfig->sys.apPwd);
DBGPRINT(F("IP Address: http://"));
DBGPRINTLN(mApIp.toString());
DBGPRINTLN(F("---------\n"));
@ -167,7 +183,7 @@ void ahoywifi::setupAp(void) {
WiFi.mode(WIFI_AP_STA);
WiFi.softAPConfig(mApIp, mApIp, IPAddress(255, 255, 255, 0));
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PWD);
WiFi.softAP(WIFI_AP_SSID, mConfig->sys.apPwd);
}
@ -275,12 +291,12 @@ void ahoywifi::scanAvailNetworks(void) {
}
//-----------------------------------------------------------------------------
void ahoywifi::getAvailNetworks(JsonObject obj) {
bool ahoywifi::getAvailNetworks(JsonObject obj) {
JsonArray nets = obj.createNestedArray("networks");
int n = WiFi.scanComplete();
if (n < 0)
return;
return false;
if(n > 0) {
int sort[n];
sortRSSI(&sort[0], n);
@ -293,6 +309,8 @@ void ahoywifi::getAvailNetworks(JsonObject obj) {
WiFi.scanDelete();
if(mStaConn == IN_AP_MODE)
WiFi.mode(WIFI_AP);
return true;
}
//-----------------------------------------------------------------------------

2
src/wifi/ahoywifi.h

@ -27,7 +27,7 @@ class ahoywifi {
void tickWifiLoop(void);
bool getNtpTime(void);
void scanAvailNetworks(void);
void getAvailNetworks(JsonObject obj);
bool getAvailNetworks(JsonObject obj);
private:
typedef enum WiFiStatus {

5
tools/rpi/Dockerfile

@ -2,11 +2,14 @@
# build executable binary
############################
FROM python:slim-bullseye
FROM python:slim-bookworm
COPY . /hoymiles
WORKDIR /hoymiles
# RUN apt-get update \
# && groupadd spi
RUN python3 -m pip install pyrf24 influxdb_client && \
python3 -m pip list #watch for RF24 module - if its there its installed

43
tools/rpi/hoymiles/__init__.py

@ -179,18 +179,45 @@ class ResponseDecoder(ResponseDecoderFactory):
command = self.request_command
if HOYMILES_DEBUG_LOGGING:
if command.upper() == '01':
if command.upper() == '00':
model_desc = "Inverter Dev Inform Simple"
elif command.upper() == '01':
model_desc = "Firmware version / date"
elif command.upper() == '02':
model_desc = "Inverter generic events log"
elif command.upper() == '0B':
elif command.upper() == '03': ## HardWareConfig
model_desc = "Hardware configuration"
elif command.upper() == '04': ## SimpleCalibrationPara
model_desc = "Simple Calibration Parameter"
elif command.upper() == '05': ## SystemConfigPara
model_desc = "Inverter generic SystemConfigPara"
elif command.upper() == '0B': ## 11 - RealTimeRunData_Debug
model_desc = "mirco-inverters status data"
elif command.upper() == '0C':
elif command.upper() == '0C': ## 12 - RealTimeRunData_Reality
model_desc = "mirco-inverters status data"
elif command.upper() == '11':
elif command.upper() == '0D': ## 13 - RealTimeRunData_A_Phase
model_desc = "Real-Time Run Data A Phase "
elif command.upper() == '0E': ## 14 - RealTimeRunData_B_Phase
model_desc = "Real-Time Run Data B Phase "
elif command.upper() == '0F': ## 15 - RealTimeRunData_C_Phase
model_desc = "Real-Time Run Data C Phase "
elif command.upper() == '11': ## 17 - AlarmData
model_desc = "Inverter generic events log"
elif command.upper() == '12':
elif command.upper() == '12': ## 18 - AlarmUpdate
model_desc = "Inverter major events log"
elif command.upper() == '13': ## 19 - RecordData
model_desc = "Record Data"
elif command.upper() == '14': ## 20 - InternalData
model_desc = "Internal Data"
elif command.upper() == '15': ## 21 - GetLossRate
model_desc = "Get Loss Rate"
elif command.upper() == '1E': ## 30 - GetSelfCheckState
model_desc = "Get Self Check State"
elif command.upper() == 'FF': ## 255 - InitDataState
model_desc = "Initi Data State"
else:
model_desc = "event not configured - check ahoy script"
logging.info(f'model_decoder: {model}Decode{command.upper()} - {model_desc}')
model_decoders = __import__('hoymiles.decoders')
@ -290,10 +317,9 @@ class InverterPacketFragment:
:return: log line received frame
:rtype: str
"""
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
size = len(self.frame)
channel = f' channel {self.ch_rx}' if self.ch_rx else ''
return f"{c_datetime} Received {size} bytes{channel}: {hexify_payload(self.frame)}"
return f"Received {size} bytes{channel}: {hexify_payload(self.frame)}"
class HoymilesNRF:
"""Hoymiles NRF24 Interface"""
@ -743,9 +769,8 @@ class InverterTransaction:
:return: log line of payload for transmission
:rtype: str
"""
c_datetime = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
size = len(self.request)
return f'{c_datetime} Transmit | {hexify_payload(self.request)}'
return f'Transmit | {hexify_payload(self.request)}'
def hexify_payload(byte_var):
"""

22
tools/rpi/hoymiles/__main__.py

@ -101,7 +101,7 @@ class SunsetHandler:
logging.info (f'Woke up...')
def sun_status2mqtt(self, dtu_ser, dtu_name):
if not mqtt_client:
if not mqtt_client or not self.suntimes:
return
local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
@ -129,6 +129,11 @@ def main_loop(ahoy_config):
sunset.sun_status2mqtt(dtu_ser, dtu_name)
loop_interval = ahoy_config.get('interval', 1)
transmit_retries = ahoy_config.get('transmit_retries', 5)
if (transmit_retries <= 0):
logging.critical('Parameter "transmit_retries" must be >0 - please check ahoy.yml.')
# print message to console too
print('Parameter "transmit_retries" must be >0 - please check ahoy.yml - STOP(0)x')
sys.exit(0)
try:
do_init = True
@ -175,15 +180,16 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
inv_str = str(inverter_ser)
if do_init:
command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.InverterDevInform_All))
# command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.SystemConfigPara))
#command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.SystemConfigPara))
command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.RealTimeRunData_Debug))
# Put all queued commands for current inverter on air
while len(command_queue[inv_str]) > 0:
payload = command_queue[inv_str].pop(0)
payload = command_queue[inv_str].pop(0) ## Sub.Cmd
# Send payload {ttl}-times until we get at least one reponse
payload_ttl = retries
response = None
while payload_ttl > 0:
payload_ttl = payload_ttl - 1
com = hoymiles.InverterTransaction(
@ -197,7 +203,6 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
src=dtu_ser,
dst=inverter_ser
)))
response = None
while com.rxtx():
try:
response = com.get_payload()
@ -210,8 +215,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
# Handle the response data if any
if response:
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
c_datetime = datetime.now()
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
logging.debug(f'Payload: ' + hoymiles.hexify_payload(response))
# prepare decoder object
decoder = hoymiles.ResponseDecoder(response,
@ -319,9 +323,11 @@ def init_logging(ahoy_config):
max_log_files = log_config.get('max_log_files', max_log_files)
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
lvl = logging.DEBUG
logging.basicConfig(handlers=[RotatingFileHandler(fn, maxBytes=max_log_filesize, backupCount=max_log_files)], format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl)
logging.basicConfig(handlers=[RotatingFileHandler(fn, maxBytes=max_log_filesize, backupCount=max_log_files)],
format='%(asctime)s %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S.%s', level=lvl)
dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu')
logging.info(f'start logging for {dtu_name} with level: {logging.root.level}')
logging.info(f'start logging for {dtu_name} with level: {logging.getLevelName(logging.root.level)}')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles")

27
tools/rpi/hoymiles/decoders/__init__.py

@ -151,18 +151,19 @@ class StatusResponse(Response):
"""
strings = []
s_exists = True
while s_exists:
while s_exists and len(strings) < len(self.inv_strings):
s_exists = False
string_id = len(strings)
string = {}
string['name'] = self.inv_strings[string_id]['s_name']
for key in self.string_keys:
prop = f'dc_{key}_{string_id}'
if hasattr(self, prop):
s_exists = True
string[key] = getattr(self, prop)
if s_exists:
strings.append(string)
if string_id < len(self.inv_strings):
string = {}
string['name'] = self.inv_strings[string_id]['s_name']
for key in self.string_keys:
prop = f'dc_{key}_{string_id}'
if hasattr(self, prop):
s_exists = True
string[key] = getattr(self, prop)
if s_exists:
strings.append(string)
return strings
@ -430,15 +431,15 @@ class DebugDecodeAny(UnknownResponse):
l_payload = len(self.response)
logging.debug(f' payload has {l_payload} bytes')
logging.debug()
logging.debug('')
logging.debug('Field view: int')
print_table_unpack('>B', self.response)
logging.debug()
logging.debug('')
logging.debug('Field view: shorts')
print_table_unpack('>H', self.response)
logging.debug()
logging.debug('')
logging.debug('Field view: longs')
print_table_unpack('>L', self.response)

Loading…
Cancel
Save