Browse Source

Merge branch 'develop03-devil' of https://github.com/beegee3/ahoy into beegee3-develop03-devil

pull/658/head
lumapu 2 years ago
parent
commit
967c7775aa
  1. 2
      .github/workflows/compile_development.yml
  2. 2
      .github/workflows/compile_release.yml
  3. 35
      Getting_Started.md
  4. 2
      User_Manual.md
  5. BIN
      doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png
  6. BIN
      doc/ESP8266_nRF24L01+_Schaltplan.jpg
  7. 47
      doc/prometheus_ep_description.md
  8. 4
      scripts/buildManifest.py
  9. 26
      scripts/getVersion.py
  10. 120
      src/CHANGES.md
  11. 253
      src/app.cpp
  12. 65
      src/app.h
  13. 7
      src/appInterface.h
  14. 2
      src/config/config.h
  15. 119
      src/config/settings.h
  16. 5
      src/defines.h
  17. 161
      src/hm/CircularBuffer.h
  18. 2
      src/hm/hmDefines.h
  19. 169
      src/hm/hmInverter.h
  20. 338
      src/hm/hmRadio.h
  21. 16
      src/hm/hmSystem.h
  22. 288
      src/hm/payload.h
  23. 10
      src/main.cpp
  24. 74
      src/platformio.ini
  25. 418
      src/plugins/MonochromeDisplay/MonochromeDisplay.h
  26. 422
      src/publisher/pubMqtt.h
  27. 4
      src/publisher/pubSerial.h
  28. 27
      src/utils/ahoyTimer.h
  29. 2
      src/utils/dbg.h
  30. 33
      src/utils/handler.h
  31. 9
      src/utils/helper.cpp
  32. 1
      src/utils/helper.h
  33. 110
      src/utils/llist.h
  34. 42
      src/utils/scheduler.h
  35. 104
      src/web/RestApi.h
  36. 2
      src/web/html/index.html
  37. 209
      src/web/html/setup.html
  38. 5
      src/web/html/system.html
  39. 1
      src/web/html/update.html
  40. 256
      src/web/web.h
  41. 125
      src/wifi/ahoywifi.cpp
  42. 15
      src/wifi/ahoywifi.h

2
.github/workflows/compile_development.yml

@ -47,7 +47,7 @@ jobs:
run: python convert.py
- name: Run PlatformIO
run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306
run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp32-wroom32-release
- name: Rename Binary files
id: rename-binary-files

2
.github/workflows/compile_release.yml

@ -51,7 +51,7 @@ jobs:
run: python convert.py
- name: Run PlatformIO
run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306
run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp32-wroom32-release
- name: Rename Binary files
id: rename-binary-files

35
Getting_Started.md

@ -6,9 +6,12 @@
- [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up)
- [ESP8266 wiring example](#esp8266-wiring-example)
- [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- [Schematic](#schematic)
- [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
@ -69,8 +72,9 @@ Make sure the NRF24L01+ module has the "+" in its name as we depend on the 250kb
| **Parts** | **Price** |
| --- | --- |
| D1 ESP8266 Mini WLAN Board Mikrokontroller | 4,40 Euro |
| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 Euro |
| NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro |
| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **10,34 Euro** |
@ -80,6 +84,7 @@ To also run our sister project OpenDTU and be upwards compatible for the future
| --- | --- |
| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro |
| NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro |
| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **14,89 Euro** |
@ -89,6 +94,18 @@ Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebr
An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).<br/>
You are welcome to add more examples of faked chips. We will add that information here.<br/>
Some users reported better connection or longer range through more walls when using the
"E01-ML01DP5" EBYTE 2,4 GHz Wireless Modul nRF24L01 + PA + LNA RF Modul, SMA-K Antenna connector,
which has an eye-catching HF cover. But beware: It comes without the antenna!
In any case you should stabilize the Vcc power by a capacitor and don't exceed the Amplifier Power Level "LOW".
Users reporting good connection over 10m through walls / ceilings with Amplifier Power Level "MIN".
It is not always the bigger the better...
Power levels "HIGH" and "MAX" are meant to wirings where the nRF24 is supplied by an extra 3.3 Volt regulator.
The bultin regulator on ESP boards has only low reserves in case WiFi and nRF are sending simultaneously.
If you operate additional interfaces like a display, the reserve is again reduced.
## Wiring things up
The NRF24L01+ radio module is connected to the standard SPI pins:
@ -107,7 +124,7 @@ Additional, there are 3 pins, which can be set individual:
*These pins can be changed from the /setup URL.*
#### ESP8266 wiring example
#### ESP8266 wiring example on WEMOS D1
This is an example wiring using a Wemos D1 mini.<br>
@ -119,6 +136,18 @@ This is an example wiring using a Wemos D1 mini.<br>
![Symbolic](doc/AhoyWemos_Steckplatine.jpg)
#### ESP8266 wiring example on 30pin Lolin NodeMCU v3
This is an example wiring using a NodeMCU V3.<br>
##### Schematic
![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg)
##### Symbolic view
![Symbolic](doc/ESP8266_nRF24L01+_bb.png)
#### ESP32 wiring example
Example wiring for a 38pin ESP32 module

2
User_Manual.md

@ -29,6 +29,7 @@ The AhoyDTU will publish on the following topics
| `uptime` | 73630 | uptime in seconds | false |
| `version` | 0.5.61 | current installed verison of AhoyDTU | true |
| `wifi_rssi` | -75 | WiFi signal strength | false |
| `ip_addr` | 192.168.178.25 | WiFi Station IP Address | true |
| status code | Remarks |
|---|---|
@ -43,6 +44,7 @@ The AhoyDTU will publish on the following topics
|---|---|---|---|
| `available` | 2 | see table below | true |
| `last_success` | 1672155690 | UTC Timestamp | true |
| `ack_pwr_limit` | true | fast information if inverter has accepted power limit | false |
| status code | Remarks |
|---|---|

BIN
doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

BIN
doc/ESP8266_nRF24L01+_Schaltplan.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

47
doc/prometheus_ep_description.md

@ -0,0 +1,47 @@
# Prometheus Endpoint
Metrics available for AhoyDTU device, inverters and channels.
Prometheus metrics provided at `/metrics`.
## Labels
| Label name | Description |
|:-----------|:--------------------------------------|
| version | current installed version of AhoyDTU |
| image | currently not used |
| devicename | Device name from setup |
| name | Inverter name from setup |
| serial | Serial number of inverter |
| enabled | Communication enable for inverter |
| inverter | Inverter name from setup |
| channel | Channel name from setup |
## Exported Metrics
| Metric name | Type | Description | Labels |
|----------------------------------------|---------|--------------------------------------------------------|--------------|
| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename |
| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial, enabled |
| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter |
| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter |
| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter |
| `ahoy_solar_Q_AC_var` | Gauge | AC reactive power[var] | inverter |
| `ahoy_solar_F_AC_hertz` | Gauge | AC frequency [Hz] | inverter |
| `ahoy_solar_PF_AC` | Gauge | AC Power factor | inverter |
| `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter |
| `ahoy_solar_ALARM_MES_ID` | Gauge | Last alarm message id of inverter | inverter |
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter |
| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter |
| `ahoy_solar_P_DC_watt` | Gauge | DC power of inverter [W] | inverter |
| `ahoy_solar_Efficiency_ratio` | Gauge | ration AC Power over DC Power [%] | inverter |
| `ahoy_solar_U_DC_volt` | Gauge | DC voltage of channel [V] | inverter, channel |
| `ahoy_solar_I_DC_ampere` | Gauge | DC current of channel [A] | inverter, channel |
| `ahoy_solar_P_DC_watt` | Gauge | DC power of channel [P] | inverter, channel |
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel |
| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel |
| `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel |
| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | |
| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | |
| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | |
| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | |
| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | |

4
scripts/buildManifest.py

@ -36,13 +36,13 @@ def buildManifest(path, infile, outfile):
esp32["parts"].append({"path": "bootloader.bin", "offset": 4096})
esp32["parts"].append({"path": "partitions.bin", "offset": 32768})
esp32["parts"].append({"path": "ota.bin", "offset": 57344})
esp32["parts"].append({"path": version[1] + "_esp32_" + sha + ".bin", "offset": 65536})
esp32["parts"].append({"path": version[1] + "_" + sha + "_esp32.bin", "offset": 65536})
data["builds"].append(esp32)
esp8266 = {}
esp8266["chipFamily"] = "ESP8266"
esp8266["parts"] = []
esp8266["parts"].append({"path": version[1] + "_esp8266_" + sha + ".bin", "offset": 0})
esp8266["parts"].append({"path": version[1] + "_" + sha + "_esp8266.bin", "offset": 0})
data["builds"].append(esp8266)
jsonString = json.dumps(data, indent=2)

26
scripts/getVersion.py

@ -52,42 +52,22 @@ def readVersion(path, infile):
os.mkdir(path + "firmware/")
sha = os.getenv("SHA",default="sha")
versionout = version[:-1] + "_esp8266_" + sha + ".bin"
versionout = version[:-1] + "_" + sha + "_esp8266.bin"
src = path + ".pio/build/esp8266-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_esp8266_nokia5110_" + sha + ".bin"
src = path + ".pio/build/esp8266-nokia5110/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_esp8266_ssd1306_" + sha + ".bin"
src = path + ".pio/build/esp8266-ssd1306/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_esp8285_" + sha + ".bin"
versionout = version[:-1] + "_" + sha + "_esp8285.bin"
src = path + ".pio/build/esp8285-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp32_" + sha + ".bin"
versionout = version[:-1] + "_" + sha + "_esp32.bin"
src = path + ".pio/build/esp32-wroom32-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_esp32_nokia5110_" + sha + ".bin"
src = path + ".pio/build/esp32-wroom32-nokia5110/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_esp32_ssd1306_" + sha + ".bin"
src = path + ".pio/build/esp32-wroom32-ssd1306/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
# other ESP32 bin files
src = path + ".pio/build/esp32-wroom32-release/"
dst = path + "firmware/"

120
src/CHANGES.md

@ -1,36 +1,84 @@
# Changelog v0.5.66
**Note:** Version `0.5.42` to `0.5.65` were development versions. Last release version was `0.5.41`
Detailed change log (development changes): https://github.com/lumapu/ahoy/blob/945a671d27d10d0f7c175ebbf2fbb2806f9cd79a/src/CHANGES.md
* updated REST API and MQTT (both of them use the same functionality)
* improved stability
* Regular expressions for input fields which are used for MQTT to be compliant to MQTT
* WiFi optimization (AP Mode and STA in parallel, reconnect if local STA is unavailable)
* improved display of `/system`
* fix Update button protection (prevent double click #527)
* optimized scheduler #515
* fix of duplicates in API `/api/record/live` (#526)
* added update information to `index.html` (check for update with github.com)
* fix web logout (auto logout)
* switched MQTT library
* removed MQTT `available_text` (can be deducted from `available`)
* enhanced MQTT documentation in `User_Manual.md`
* changed MQTT topic `status` to nummeric value, check documentation in `User_Manual.md`
* added immediate (each minute) report of inverter status MQTT #522
* increased MQTT user, pwd and topic length to 64 characters + `\0`. (The string end `\0` reduces the available size by one) #516
* added disable night communication flag to MQTT #505
* added MQTT <TOPIC>/status to show status over all inverters
* added MQTT RX counter to index.html
* added protection mask to select which pages should be protected
* added monochrome display that show values also if nothing changed and in offline mode #498
* added icons to index.html, added WiFi-strength symbol on each page
* refactored communication offset (adjustable in minutes now)
* factory reset formats entire little fs
* renamed sunrise / sunset on index.html to start / stop communication
* fixed static IP save
* fix NTP with static IP
* all values are displayed on /live even if they are 0
* added NRF24 info to Systeminfo
* reordered enqueue commands after boot up to prevent same payload length for successive commands
# Changelog
(starting from release version `0.5.66`)
## 0.5.78
* further improvements regarding wifi #611, fix connection if only one AP with same SSID is there
* fix endless loop in `zerovalues` #564
* fix auto discover again #565
* added total values to autodiscover #630
* improved zero at midnight #625
## 0.5.77
* fix wrong filename for automatically created manifest (online installer) #620
* added rotate display feature #619
* improved Prometheus endpoint #615, thx to @fsck-block
* improved wifi to connect always to strongest RSSI, thx to @beegee3 #611
## 0.5.76
* reduce MQTT retry interval from maximum speed to one second
* fixed homeassistant autodiscovery #565
* implemented `getNTPTime` improvements #609 partially #611
* added alarm messages to MQTT #177, #600, #608
## 0.5.75
* fix wakeup issue, once wifi was lost during night the communication didn't start in the morning
* reenabled FlashStringHelper because of lacking RAM
* complete rewrite of monochrome display class, thx to @dAjaY85 -> displays are now configurable in setup
* fix power limit not possible #607
## 0.5.74
* improved payload handling (retransmit all fragments on CRC error)
* improved `isAvailable`, checkes all record structs, inverter becomes available more early because version is check first
* fix tickers were not set if NTP is not available
* disabled annoying `FlashStringHelper` it gives randomly Expeptions during development, feels more stable since then
* moved erase button to the bottom in settings, not nice but more functional
* split `tx_count` to `tx_cnt` and `retransmits` in `system.html`
* fix mqtt retransmit IP address #602
* added debug infos for `scheduler` (web -> `/debug` as trigger prints list of tickers to serial console)
## 0.5.73
* improved payload handling (request / retransmit) #464
* included alarm ID parse to serial console (in development)
## 0.5.72
* repaired system, scheduler was not called any more #596
## 0.5.71
* improved wifi handling and tickers, many thanks to @beegee3 #571
* fixed YieldTotal correction calculation #589
* fixed serial output of power limit acknowledge #569
* reviewed `sendDiscoveryConfig` #565
* merged PR `Monodisplay`, many thanks to @dAjaY85 #566, Note: (settings are introduced but not able to be modified, will be included in next version)
## 0.5.70
* corrected MQTT `comm_disabled` #529
* fix Prometheus and JSON endpoints (`config_override.h`) #561
* publish MQTT with fixed interval even if inverter is not available #542
* added JSON settings upload. NOTE: settings JSON download changed, so only settings should be uploaded starting from version `0.5.70` #551
* MQTT topic and inverter name have more allowed characters: `[A-Za-z0-9./#$%&=+_-]+`, thx: @Mo Demman
* improved potential issue with `checkTicker`, thx @cbscpe
* MQTT option for reset values on midnight / not avail / communication stop #539
* small fix in `tickIVCommunication` #534
* add `YieldTotal` correction, eg. to have the option to zero at year start #512
## 0.5.69
* merged SH1106 1.3" Display, thx @dAjaY85
* added SH1106 to automatic build
* added IP address to MQTT (version, device and IP are retained and only transmitted once after boot) #556
* added `set_power_limit` acknowledge MQTT publish #553
* changed: version, device name are only published via MQTT once after boot
* added `Login` to menu if admin password is set #554
* added `development` to second changelog link in `index.html` #543
* added interval for MQTT (as option). With this settings MQTT live data is published in a fixed timing (only if inverter is available) #542, #523
* added MQTT `comm_disabled` #529
* changed name of binaries, moved GIT-Sha to the front #538
## 0.5.68
* repaired receive payload
* Powerlimit is transfered immediately to inverter
## 0.5.67
* changed calculation of start / stop communication to 1 min after last comm. stop #515
* moved payload send to `payload.h`, function `ivSend` #515
* payload: if last frame is missing, request all frames again

253
src/app.cpp

@ -3,11 +3,6 @@
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if defined(ESP32) && defined(F)
#undef F
#define F(sl) (sl)
#endif
#include "app.h"
#include <ArduinoJson.h>
#include "utils/sun.h"
@ -33,25 +28,21 @@ void app::setup() {
mSys = new HmSystemType();
mSys->enableDebug();
mSys->setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs);
mPayload.addListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1));
mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1));
#if !defined(AP_ONLY)
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mTimestamp);
#endif
mWifi.setup(mConfig, &mTimestamp);
#if !defined(AP_ONLY)
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi));
#if defined(AP_ONLY)
mInnerLoopCb = std::bind(&app::loopStandard, this);
#else
mInnerLoopCb = std::bind(&app::loopWifi, this);
#endif
mSendTickerId = every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval);
mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onWifi, this, std::placeholders::_1));
#if !defined(AP_ONLY)
once(std::bind(&app::tickNtpUpdate, this), 2);
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
#endif
mSys->addInverters(&mConfig->inst);
mPayload.setup(mSys);
mPayload.setup(this, mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp);
mPayload.enableSerialDebug(mConfig->serial.debug);
if(!mSys->Radio.isChipConnected())
@ -59,93 +50,138 @@ void app::setup() {
// when WiFi is in client mode, then enable mqtt broker
#if !defined(AP_ONLY)
if (mConfig->mqtt.broker[0] > 0) {
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt));
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt));
mMqttEnabled = (mConfig->mqtt.broker[0] > 0);
if (mMqttEnabled) {
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mTimestamp);
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
#endif
setupLed();
mWeb.setup(this, mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
everySec(std::bind(&WebType::tickSecond, &mWeb));
mApi.setup(this, mSys, mWeb.getWebSrvPtr(), mConfig);
// Plugins
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
mMonoDisplay.setup(mSys, &mTimestamp);
everySec(std::bind(&MonoDisplayType::tickerSecond, &mMonoDisplay));
#endif
if(mConfig->plugin.display.type != 0)
mMonoDisplay.setup(&mConfig->plugin.display, mSys, &mTimestamp, 0xff, mVersion);
mPubSerial.setup(mConfig, mSys, &mTimestamp);
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval);
//everySec(std::bind(&app::tickSerial, this));
regularTickers();
}
//-----------------------------------------------------------------------------
void app::loop(void) {
DPRINTLN(DBG_VERBOSE, F("app::loop"));
mInnerLoopCb();
}
//-----------------------------------------------------------------------------
void app::loopStandard(void) {
ah::Scheduler::loop();
mSys->Radio.loop();
yield();
if (mSys->Radio.loop()) {
while (!mSys->Radio.mBufCtrl.empty()) {
packet_t *p = &mSys->Radio.mBufCtrl.front();
if (mConfig->serial.debug) {
DPRINT(DBG_INFO, "RX " + String(p->len) + "B Ch" + String(p->ch) + " | ");
mSys->Radio.dumpBuf(p->packet, p->len);
}
mStat.frmCnt++;
if (ah::checkTicker(&mRxTicker, 5)) {
bool rxRdy = mSys->Radio.switchRxCh();
mPayload.add(p);
mSys->Radio.mBufCtrl.pop();
yield();
}
mPayload.process(true);
}
mPayload.loop();
if (!mSys->BufCtrl.empty()) {
uint8_t len;
packet_t *p = mSys->BufCtrl.getBack();
if(mMqttEnabled)
mMqtt.loop();
}
if (mSys->Radio.checkPaketCrc(p->packet, &len, p->rxCh)) {
if (mConfig->serial.debug) {
DPRINT(DBG_INFO, "RX " + String(len) + "B Ch" + String(p->rxCh) + " | ");
mSys->Radio.dumpBuf(NULL, p->packet, len);
}
mStat.frmCnt++;
//-----------------------------------------------------------------------------
void app::loopWifi(void) {
ah::Scheduler::loop();
yield();
}
if (0 != len)
mPayload.add(p, len);
}
mSys->BufCtrl.popBack();
//-----------------------------------------------------------------------------
void app::onWifi(bool gotIp) {
DPRINTLN(DBG_DEBUG, F("onWifi"));
ah::Scheduler::resetTicker();
regularTickers(); // reinstall regular tickers
if (gotIp) {
mInnerLoopCb = std::bind(&app::loopStandard, this);
every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend");
mMqttReconnect = true;
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
if(WIFI_AP == WiFi.getMode()) {
mMqttEnabled = false;
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
}
yield();
if (rxRdy)
mPayload.process(true, mConfig->nrf.maxRetransPerPyld, &mStat);
}
else {
mInnerLoopCb = std::bind(&app::loopWifi, this);
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
}
}
#if !defined(AP_ONLY)
mMqtt.loop();
#endif
//-----------------------------------------------------------------------------
void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
// Plugins
if(mConfig->plugin.display.type != 0)
everySec(std::bind(&MonoDisplayType::tickerSecond, &mMonoDisplay), "disp");
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart");
}
//-----------------------------------------------------------------------------
void app::tickNtpUpdate(void) {
uint32_t nxtTrig = 5; // default: check again in 5 sec
if (mWifi.getNtpTime()) {
nxtTrig = 43200; // check again in 12 h
bool isOK = mWifi.getNtpTime();
if (isOK || mTimestamp != 0) {
if (mMqttReconnect && mMqttEnabled) {
mMqtt.connect();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
if(mConfig->mqtt.rstYieldMidNight) {
uint32_t midTrig = mTimestamp - ((mTimestamp - 1) % 86400) + 86400; // next midnight
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
}
mMqttReconnect = false;
}
nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min
if((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) {
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise();
}
}
once(std::bind(&app::tickNtpUpdate, this), nxtTrig);
once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
}
//-----------------------------------------------------------------------------
void app::tickCalcSunrise(void) {
ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
if (mSunrise == 0) // on boot/reboot calc sun values for current time
ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day
ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
tickIVCommunication();
uint32_t nxtTrig = mTimestamp - ((mTimestamp + mCalculatedTimezoneOffset - 10) % 86400) + 86400;; // next midnight, -10 for safety that it is certain next day, local timezone
onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig);
if (mConfig->mqtt.broker[0] > 0) {
mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom);
}
uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop
onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri");
if (mMqttEnabled)
tickSun();
}
//-----------------------------------------------------------------------------
@ -156,15 +192,41 @@ void app::tickIVCommunication(void) {
if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start
nxtTrig = mSunrise - mConfig->sun.offsetSec;
} else {
if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
return;
if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
nxtTrig = 0;
} else { // current time lies within communication start/stop time, set next trigger to communication stop
mIVCommunicationOn = true;
nxtTrig = mSunset + mConfig->sun.offsetSec;
}
}
onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig);
if (nxtTrig != 0)
onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom");
}
if (mMqttEnabled)
tickComm();
}
//-----------------------------------------------------------------------------
void app::tickSun(void) {
// only used and enabled by MQTT (see setup())
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom))
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
}
//-----------------------------------------------------------------------------
void app::tickComm(void) {
// only used and enabled by MQTT (see setup())
if (!mMqtt.tickerComm(!mIVCommunicationOn))
once(std::bind(&app::tickComm, this), 1, "mqCom"); // MQTT not connected, retry
}
//-----------------------------------------------------------------------------
void app::tickMidnight(void) {
// only used and enabled by MQTT (see setup())
uint32_t nxtTrig = mTimestamp - ((mTimestamp - 1) % 86400) + 86400; // next midnight
onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2");
mMqtt.tickerMidnight();
}
//-----------------------------------------------------------------------------
@ -174,9 +236,9 @@ void app::tickSend(void) {
return;
}
if (mIVCommunicationOn) {
if (!mSys->BufCtrl.empty()) {
if (!mSys->Radio.mBufCtrl.empty()) {
if (mConfig->serial.debug)
DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->BufCtrl.getFill()));
DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->Radio.mBufCtrl.size()));
}
int8_t maxLoop = MAX_NUM_INVERTERS;
@ -187,49 +249,8 @@ void app::tickSend(void) {
} while ((NULL == iv) && ((maxLoop--) > 0));
if (NULL != iv) {
if(iv->config->enabled) {
if (!mPayload.isComplete(iv))
mPayload.process(false, mConfig->nrf.maxRetransPerPyld, &mStat);
if (!mPayload.isComplete(iv)) {
if (0 == mPayload.getMaxPacketId(iv))
mStat.rxFailNoAnser++;
else
mStat.rxFail++;
iv->setQueuedCmdFinished(); // command failed
if (mConfig->serial.debug)
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
if (mConfig->serial.debug) {
DPRINT(DBG_INFO, F("(#") + String(iv->id) + ") ");
DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload.getRetransmits(iv)) + ")");
}
}
mPayload.reset(iv, mTimestamp);
mPayload.request(iv);
yield();
if (mConfig->serial.debug) {
DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status()));
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Requesting Inv SN ") + String(iv->config->serial.u64, HEX));
}
if (iv->devControlRequest) {
if (mConfig->serial.debug)
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0]));
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit);
mPayload.setTxCmd(iv, iv->devControlCmd);
iv->clearCmdQueue();
iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
} else {
uint8_t cmd = iv->getQueuedCmd();
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket"));
mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload.getTs(iv), iv->alarmMesIndex);
mPayload.setTxCmd(iv, cmd);
mRxTicker = 0;
}
}
if(iv->config->enabled)
mPayload.ivSend(iv);
}
} else {
if (mConfig->serial.debug)
@ -240,27 +261,18 @@ void app::tickSend(void) {
updateLed();
}
//-----------------------------------------------------------------------------
void app::handleIntr(void) {
DPRINTLN(DBG_VERBOSE, F("app::handleIntr"));
mSys->Radio.handleIntr();
}
//-----------------------------------------------------------------------------
void app::resetSystem(void) {
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
#ifdef AP_ONLY
mTimestamp = 1;
#else
mTimestamp = 0;
#endif
mSunrise = 0;
mSunset = 0;
mRxTicker = 0;
mSendTickerId = 0xff; // invalid id
mMqttEnabled = false;
mSendLastIvId = 0;
mShowRebootRequest = false;
@ -296,8 +308,7 @@ void app::updateLed(void) {
if(mConfig->led.led0 != 0xff) {
Inverter<> *iv = mSys->getInverterByPos(0);
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(iv->isProducing(mTimestamp, rec))
if(iv->isProducing(mTimestamp))
digitalWrite(mConfig->led.led0, LOW); // LED on
else
digitalWrite(mConfig->led.led0, HIGH); // LED off

65
src/app.h

@ -18,10 +18,8 @@
#include "config/settings.h"
#include "defines.h"
#include "utils/crc.h"
#include "utils/ahoyTimer.h"
#include "utils/scheduler.h"
#include "hm/CircularBuffer.h"
#include "hm/hmSystem.h"
#include "hm/payload.h"
#include "wifi/ahoywifi.h"
@ -46,10 +44,8 @@ typedef PubMqtt<HmSystemType> PubMqttType;
typedef PubSerial<HmSystemType> PubSerialType;
// PLUGINS
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
#include "plugins/MonochromeDisplay/MonochromeDisplay.h"
typedef MonochromeDisplay<HmSystemType> MonoDisplayType;
#endif
#include "plugins/MonochromeDisplay/MonochromeDisplay.h"
typedef MonochromeDisplay<HmSystemType> MonoDisplayType;
class app : public IApp, public ah::Scheduler {
@ -59,11 +55,10 @@ class app : public IApp, public ah::Scheduler {
void setup(void);
void loop(void);
void handleIntr(void);
void cbMqtt(char* topic, byte* payload, unsigned int length);
void saveValues(void);
void resetPayload(Inverter<>* iv);
bool getWifiApActive(void);
void loopStandard(void);
void loopWifi(void);
void onWifi(bool gotIp);
void regularTickers(void);
uint32_t getUptime() {
return Scheduler::getUptime();
@ -78,6 +73,10 @@ class app : public IApp, public ah::Scheduler {
return mSettings.saveSettings();
}
bool readSettings(const char *path) {
return mSettings.readSettings(path);
}
bool eraseSettings(bool eraseWifi = false) {
return mSettings.eraseSettings(eraseWifi);
}
@ -94,8 +93,12 @@ class app : public IApp, public ah::Scheduler {
mWifi.getAvailNetworks(obj);
}
void setOnUpdate() {
onWifi(false);
}
void setRebootFlag() {
once(std::bind(&app::tickReboot, this), 1);
once(std::bind(&app::tickReboot, this), 3, "rboot");
}
const char *getVersion() {
@ -119,7 +122,15 @@ class app : public IApp, public ah::Scheduler {
}
void setMqttDiscoveryFlag() {
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1);
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf");
}
void setMqttPowerLimitAck(Inverter<> *iv) {
mMqtt.setPowerLimitAck(iv);
}
void ivSendHighPrio(Inverter<> *iv) {
mPayload.ivSendHighPrio(iv);
}
bool getMqttIsConnected() {
@ -159,6 +170,10 @@ class app : public IApp, public ah::Scheduler {
getStat(max);
}
void getSchedulerNames(void) {
printSchedulers();
}
void setTimestamp(uint32_t newTime) {
DPRINTLN(DBG_DEBUG, F("setTimestamp: ") + String(newTime));
if(0 == newTime)
@ -170,15 +185,16 @@ class app : public IApp, public ah::Scheduler {
HmSystemType *mSys;
private:
typedef std::function<void()> innerLoopCb;
void resetSystem(void);
void payloadEventListener(uint8_t cmd) {
#if !defined(AP_ONLY)
mMqtt.payloadEventListener(cmd);
#endif
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
mMonoDisplay.payloadEventListener(cmd);
#endif
if(mConfig->plugin.display.type != 0)
mMonoDisplay.payloadEventListener(cmd);
}
void mqttSubRxCb(JsonObject obj);
@ -188,13 +204,19 @@ class app : public IApp, public ah::Scheduler {
void tickReboot(void) {
DPRINTLN(DBG_INFO, F("Rebooting..."));
onWifi(false);
ah::Scheduler::resetTicker();
WiFi.disconnect();
ESP.restart();
}
void tickNtpUpdate(void);
void tickCalcSunrise(void);
void tickIVCommunication(void);
void tickSun(void);
void tickComm(void);
void tickSend(void);
void tickMidnight(void);
/*void tickSerial(void) {
if(Serial.available() == 0)
return;
@ -210,6 +232,8 @@ class app : public IApp, public ah::Scheduler {
DBGPRINTLN("");
}*/
innerLoopCb mInnerLoopCb;
bool mShowRebootRequest;
bool mIVCommunicationOn;
@ -224,25 +248,20 @@ class app : public IApp, public ah::Scheduler {
settings_t *mConfig;
uint8_t mSendLastIvId;
uint8_t mSendTickerId;
statistics_t mStat;
// timer
uint32_t mRxTicker;
// mqtt
PubMqttType mMqtt;
bool mMqttActive;
bool mMqttReconnect;
bool mMqttEnabled;
// sun
int32_t mCalculatedTimezoneOffset;
uint32_t mSunrise, mSunset;
// plugins
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
MonoDisplayType mMonoDisplay;
#endif
};
#endif /*__APP_H__*/

7
src/appInterface.h

@ -7,6 +7,7 @@
#define __IAPP_H__
#include "defines.h"
#include "hm/hmSystem.h"
// abstract interface to App. Make members of App accessible from child class
// like web or API without forward declaration
@ -14,7 +15,9 @@ class IApp {
public:
virtual ~IApp() {}
virtual bool saveSettings() = 0;
virtual bool readSettings(const char *path) = 0;
virtual bool eraseSettings(bool eraseWifi) = 0;
virtual void setOnUpdate() = 0;
virtual void setRebootFlag() = 0;
virtual const char *getVersion() = 0;
virtual statistics_t *getStatistics() = 0;
@ -29,10 +32,14 @@ class IApp {
virtual String getTimeStr(uint32_t offset) = 0;
virtual uint32_t getTimezoneOffset() = 0;
virtual void getSchedulerInfo(uint8_t *max) = 0;
virtual void getSchedulerNames() = 0;
virtual bool getRebootRequestState() = 0;
virtual bool getSettingsValid() = 0;
virtual void setMqttDiscoveryFlag() = 0;
virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0;
virtual void ivSendHighPrio(Inverter<> *iv) = 0;
virtual bool getMqttIsConnected() = 0;
virtual uint32_t getMqttRxCnt() = 0;

2
src/config/config.h

@ -52,8 +52,6 @@
#define DEF_CE_PIN 2
#define DEF_IRQ_PIN 0
#endif
#define DEF_LED0_PIN 255 // off
#define DEF_LED1_PIN 255 // off
// default NRF24 power, possible values (0 - 3)
#define DEF_AMPLIFIERPOWER 1

119
src/config/settings.h

@ -17,6 +17,7 @@
* More info:
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
* */
#define DEF_PIN_OFF 255
#define PROT_MASK_INDEX 0x0001
@ -96,6 +97,10 @@ typedef struct {
char user[MQTT_USER_LEN];
char pwd[MQTT_PWD_LEN];
char topic[MQTT_TOPIC_LEN];
uint16_t interval;
bool rstYieldMidNight;
bool rstValsNotAvail;
bool rstValsCommStop;
} cfgMqtt_t;
typedef struct {
@ -104,6 +109,7 @@ typedef struct {
serial_u serial;
uint16_t chMaxPwr[4];
char chName[4][MAX_NAME_LENGTH];
uint32_t yieldCor; // YieldTotal correction value
} cfgIv_t;
typedef struct {
@ -111,6 +117,23 @@ typedef struct {
cfgIv_t iv[MAX_NUM_INVERTERS];
} cfgInst_t;
typedef struct {
uint8_t type;
bool pwrSaveAtIvOffline;
bool logoEn;
bool pxShift;
bool rot180;
uint16_t wakeUp;
uint16_t sleepAt;
uint8_t contrast;
uint8_t pin0;
uint8_t pin1;
} display_t;
typedef struct {
display_t display;
} plugins_t;
typedef struct {
cfgSys_t sys;
cfgNrf24_t nrf;
@ -120,6 +143,7 @@ typedef struct {
cfgMqtt_t mqtt;
cfgLed_t led;
cfgInst_t inst;
plugins_t plugin;
bool valid;
} settings_t;
@ -145,16 +169,17 @@ class settings {
if(!LittleFS.begin(LITTLFS_FALSE)) {
DPRINTLN(DBG_INFO, F(".. format .."));
LittleFS.format();
if(LittleFS.begin(LITTLFS_TRUE))
if(LittleFS.begin(LITTLFS_TRUE)) {
DPRINTLN(DBG_INFO, F(".. success"));
else
} else {
DPRINTLN(DBG_INFO, F(".. failed"));
}
}
else
DPRINTLN(DBG_INFO, F(" .. done"));
readSettings();
readSettings("/settings.json");
}
// should be used before OTA
@ -185,9 +210,10 @@ class settings {
#endif
}
void readSettings(void) {
bool readSettings(const char* path) {
bool success = false;
loadDefaults();
File fp = LittleFS.open("/settings.json", "r");
File fp = LittleFS.open(path, "r");
if(!fp)
DPRINTLN(DBG_WARN, F("failed to load json, using default config"));
else {
@ -197,14 +223,16 @@ class settings {
DeserializationError err = deserializeJson(root, fp);
if(!err && (root.size() > 0)) {
mCfg.valid = true;
jsonWifi(root["wifi"]);
jsonNrf(root["nrf"]);
jsonNtp(root["ntp"]);
jsonSun(root["sun"]);
jsonSerial(root["serial"]);
jsonMqtt(root["mqtt"]);
jsonLed(root["led"]);
jsonInst(root["inst"]);
jsonWifi(root[F("wifi")]);
jsonNrf(root[F("nrf")]);
jsonNtp(root[F("ntp")]);
jsonSun(root[F("sun")]);
jsonSerial(root[F("serial")]);
jsonMqtt(root[F("mqtt")]);
jsonLed(root[F("led")]);
jsonPlugin(root[F("plugin")]);
jsonInst(root[F("inst")]);
success = true;
}
else {
Serial.println(F("failed to parse json, using default config"));
@ -212,6 +240,7 @@ class settings {
fp.close();
}
return success;
}
bool saveSettings(void) {
@ -231,6 +260,7 @@ class settings {
jsonSerial(root.createNestedObject(F("serial")), true);
jsonMqtt(root.createNestedObject(F("mqtt")), true);
jsonLed(root.createNestedObject(F("led")), true);
jsonPlugin(root.createNestedObject(F("plugin")), true);
jsonInst(root.createNestedObject(F("inst")), true);
if(0 == serializeJson(root, fp)) {
@ -297,11 +327,23 @@ class settings {
snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER);
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD);
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
mCfg.mqtt.interval = 0; // off
mCfg.mqtt.rstYieldMidNight = false;
mCfg.mqtt.rstValsNotAvail = false;
mCfg.mqtt.rstValsCommStop = false;
mCfg.led.led0 = DEF_LED0_PIN;
mCfg.led.led1 = DEF_LED1_PIN;
mCfg.led.led0 = DEF_PIN_OFF;
mCfg.led.led1 = DEF_PIN_OFF;
memset(&mCfg.inst, 0, sizeof(cfgInst_t));
mCfg.plugin.display.pwrSaveAtIvOffline = false;
mCfg.plugin.display.contrast = 60;
mCfg.plugin.display.logoEn = true;
mCfg.plugin.display.pxShift = true;
mCfg.plugin.display.rot180 = false;
mCfg.plugin.display.pin0 = DEF_PIN_OFF; // SCL
mCfg.plugin.display.pin1 = DEF_PIN_OFF; // SDA
}
void jsonWifi(JsonObject obj, bool set = false) {
@ -396,8 +438,17 @@ class settings {
obj[F("user")] = mCfg.mqtt.user;
obj[F("pwd")] = mCfg.mqtt.pwd;
obj[F("topic")] = mCfg.mqtt.topic;
obj[F("intvl")] = mCfg.mqtt.interval;
obj[F("rstMidNight")] = (bool)mCfg.mqtt.rstYieldMidNight;
obj[F("rstNotAvail")] = (bool)mCfg.mqtt.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mCfg.mqtt.rstValsCommStop;
} else {
mCfg.mqtt.port = obj[F("port")];
mCfg.mqtt.port = obj[F("port")];
mCfg.mqtt.interval = obj[F("intvl")];
mCfg.mqtt.rstYieldMidNight = (bool)obj["rstMidNight"];
mCfg.mqtt.rstValsNotAvail = (bool)obj["rstNotAvail"];
mCfg.mqtt.rstValsCommStop = (bool)obj["rstComStop"];
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as<const char*>());
snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as<const char*>());
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as<const char*>());
@ -415,6 +466,34 @@ class settings {
}
}
void jsonPlugin(JsonObject obj, bool set = false) {
if(set) {
JsonObject disp = obj.createNestedObject("disp");
disp[F("type")] = mCfg.plugin.display.type;
disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline;
disp[F("logo")] = (bool)mCfg.plugin.display.logoEn;
disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift;
disp[F("rot180")] = (bool)mCfg.plugin.display.rot180;
disp[F("wake")] = mCfg.plugin.display.wakeUp;
disp[F("sleep")] = mCfg.plugin.display.sleepAt;
disp[F("contrast")] = mCfg.plugin.display.contrast;
disp[F("pin0")] = mCfg.plugin.display.pin0;
disp[F("pin1")] = mCfg.plugin.display.pin1;
} else {
JsonObject disp = obj["disp"];
mCfg.plugin.display.type = disp[F("type")];
mCfg.plugin.display.pwrSaveAtIvOffline = (bool) disp[F("pwrSafe")];
mCfg.plugin.display.logoEn = (bool) disp[F("logo")];
mCfg.plugin.display.pxShift = (bool) disp[F("pxShift")];
mCfg.plugin.display.rot180 = (bool) disp[F("rot180")];
mCfg.plugin.display.wakeUp = disp[F("wake")];
mCfg.plugin.display.sleepAt = disp[F("sleep")];
mCfg.plugin.display.contrast = disp[F("contrast")];
mCfg.plugin.display.pin0 = disp[F("pin0")];
mCfg.plugin.display.pin1 = disp[F("pin1")];
}
}
void jsonInst(JsonObject obj, bool set = false) {
if(set)
obj[F("en")] = (bool)mCfg.inst.enabled;
@ -434,9 +513,10 @@ class settings {
void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) {
if(set) {
obj[F("en")] = (bool)cfg->enabled;
obj[F("name")] = cfg->name;
obj[F("sn")] = cfg->serial.u64;
obj[F("en")] = (bool)cfg->enabled;
obj[F("name")] = cfg->name;
obj[F("sn")] = cfg->serial.u64;
obj[F("yield")] = cfg->yieldCor;
for(uint8_t i = 0; i < 4; i++) {
obj[F("pwr")][i] = cfg->chMaxPwr[i];
obj[F("chName")][i] = cfg->chName[i];
@ -445,6 +525,7 @@ class settings {
cfg->enabled = (bool)obj[F("en")];
snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as<const char*>());
cfg->serial.u64 = obj[F("sn")];
cfg->yieldCor = obj[F("yield")];
for(uint8_t i = 0; i < 4; i++) {
cfg->chMaxPwr[i] = obj[F("pwr")][i];
snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as<const char*>());

5
src/defines.h

@ -13,11 +13,12 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 66
#define VERSION_PATCH 78
//-------------------------------------
typedef struct {
uint8_t rxCh;
uint8_t ch;
uint8_t len;
uint8_t packet[MAX_RF_PAYLOAD_SIZE];
} packet_t;

161
src/hm/CircularBuffer.h

@ -1,161 +0,0 @@
/*
CircularBuffer - An Arduino circular buffering library for arbitrary types.
Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef CircularBuffer_h
#define CircularBuffer_h
#if defined(ESP8266) || defined(ESP32)
#define DISABLE_IRQ noInterrupts()
#define RESTORE_IRQ interrupts()
#else
#define DISABLE_IRQ \
uint8_t sreg = SREG; \
cli();
#define RESTORE_IRQ \
SREG = sreg;
#endif
template <class BUFFERTYPE, uint8_t BUFFERSIZE>
class CircularBuffer {
typedef BUFFERTYPE BufferType;
BufferType Buffer[BUFFERSIZE];
public:
CircularBuffer() : m_buff(Buffer) {
m_size = BUFFERSIZE;
clear();
}
/** Clear all entries in the circular buffer. */
void clear(void)
{
m_front = 0;
m_fill = 0;
}
/** Test if the circular buffer is empty */
inline bool empty(void) const
{
return !m_fill;
}
/** Return the number of records stored in the buffer */
inline uint8_t available(void) const
{
return m_fill;
}
/** Test if the circular buffer is full */
inline bool full(void) const
{
return m_fill == m_size;
}
inline uint8_t getFill(void) const {
return m_fill;
}
/** Aquire record on front of the buffer, for writing.
* After filling the record, it has to be pushed to actually
* add it to the buffer.
* @return Pointer to record, or NULL when buffer is full.
*/
BUFFERTYPE* getFront(void) const
{
DISABLE_IRQ;
BUFFERTYPE* f = NULL;
if (!full())
f = get(m_front);
RESTORE_IRQ;
return f;
}
/** Push record to front of the buffer
* @param record Record to push. If record was aquired previously (using getFront) its
* data will not be copied as it is already present in the buffer.
* @return True, when record was pushed successfully.
*/
bool pushFront(BUFFERTYPE* record)
{
bool ok = false;
DISABLE_IRQ;
if (!full())
{
BUFFERTYPE* f = get(m_front);
if (f != record)
*f = *record;
m_front = (m_front+1) % m_size;
m_fill++;
ok = true;
}
RESTORE_IRQ;
return ok;
}
/** Aquire record on back of the buffer, for reading.
* After reading the record, it has to be pop'ed to actually
* remove it from the buffer.
* @return Pointer to record, or NULL when buffer is empty.
*/
BUFFERTYPE* getBack(void) const
{
BUFFERTYPE* b = NULL;
DISABLE_IRQ;
if (!empty())
b = get(back());
RESTORE_IRQ;
return b;
}
/** Remove record from back of the buffer.
* @return True, when record was pop'ed successfully.
*/
bool popBack(void)
{
bool ok = false;
DISABLE_IRQ;
if (!empty())
{
m_fill--;
ok = true;
}
RESTORE_IRQ;
return ok;
}
protected:
inline BUFFERTYPE * get(const uint8_t idx) const
{
return &(m_buff[idx]);
}
inline uint8_t back(void) const
{
return (m_front - m_fill + m_size) % m_size;
}
uint8_t m_size; // Total number of records that can be stored in the buffer.
BUFFERTYPE* const m_buff;
volatile uint8_t m_front; // Index of front element (not pushed yet).
volatile uint8_t m_fill; // Amount of records currently pushed.
};
#endif // CircularBuffer_h

2
src/hm/hmDefines.h

@ -106,7 +106,7 @@ const byteAssign_t AlarmDataAssignment[] = {
};
#define HMALARMDATA_LIST_LEN (sizeof(AlarmDataAssignment) / sizeof(byteAssign_t))
#define HMALARMDATA_PAYLOAD_LEN 0 // 0: means check is off
#define ALARM_LOG_ENTRY_SIZE 12
//-------------------------------------

169
src/hm/hmInverter.h

@ -105,32 +105,33 @@ const calcFunc_t<T> calcFunctions[] = {
template <class REC_TYP>
class Inverter {
public:
cfgIv_t *config; // stored settings
uint8_t id; // unique id
uint8_t type; // integer which refers to inverter type
uint16_t alarmMesIndex; // Last recorded Alarm Message Index
uint16_t powerLimit[2]; // limit power output
float actPowerLimit; // actual power limit
uint8_t devControlCmd; // carries the requested cmd
bool devControlRequest; // true if change needed
serial_u radioId; // id converted to modbus
uint8_t channels; // number of PV channels (1-4)
record_t<REC_TYP> recordMeas; // structure for measured values
record_t<REC_TYP> recordInfo; // structure for info values
record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values
cfgIv_t *config; // stored settings
uint8_t id; // unique id
uint8_t type; // integer which refers to inverter type
uint16_t alarmMesIndex; // Last recorded Alarm Message Index
uint16_t powerLimit[2]; // limit power output
float actPowerLimit; // actual power limit
uint8_t devControlCmd; // carries the requested cmd
serial_u radioId; // id converted to modbus
uint8_t channels; // number of PV channels (1-4)
record_t<REC_TYP> recordMeas; // structure for measured values
record_t<REC_TYP> recordInfo; // structure for info values
record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values
String lastAlarmMsg;
bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null)
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)
Inverter() {
powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited
powerLimit[1] = AbsolutNonPersistent; // default power limit setting
actPowerLimit = 0xffff; // init feedback from inverter to -1
devControlRequest = false;
devControlCmd = InitDataState;
initialized = false;
lastAlarmMsg = "nothing";
alarmMesIndex = 0;
powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited
powerLimit[1] = AbsolutNonPersistent; // default power limit setting
actPowerLimit = 0xffff; // init feedback from inverter to -1
mDevControlRequest = false;
devControlCmd = InitDataState;
initialized = false;
lastAlarmMsg = "nothing";
alarmMesIndex = 0;
isConnected = false;
}
~Inverter() {
@ -140,7 +141,7 @@ class Inverter {
template <typename T>
void enqueCommand(uint8_t cmd) {
_commandQueue.push(std::make_shared<T>(cmd));
DPRINTLN(DBG_INFO, F("(#") + String(id) + F(") enqueuedCmd: ") + String(cmd));
DPRINTLN(DBG_INFO, F("(#") + String(id) + F(") enqueuedCmd: 0x") + String(cmd, HEX));
}
void setQueuedCmdFinished() {
@ -161,10 +162,10 @@ class Inverter {
uint8_t getQueuedCmd() {
if (_commandQueue.empty()) {
if (getFwVersion() == 0)
enqueCommand<InfoCommand>(InverterDevInform_All);
enqueCommand<InfoCommand>(RealTimeRunData_Debug);
if (actPowerLimit == 0xffff)
enqueCommand<InfoCommand>(SystemConfigPara);
enqueCommand<InfoCommand>(InverterDevInform_All); // firmware version
enqueCommand<InfoCommand>(RealTimeRunData_Debug); // live data
if ((actPowerLimit == 0xffff) && isConnected)
enqueCommand<InfoCommand>(SystemConfigPara); // power limit info
}
return _commandQueue.front().get()->getCmd();
}
@ -219,6 +220,22 @@ class Inverter {
return 0;
}
bool setDevControlRequest(uint8_t cmd) {
if(isConnected) {
mDevControlRequest = true;
devControlCmd = cmd;
}
return isConnected;
}
void clearDevControlRequest() {
mDevControlRequest = false;
}
inline bool getDevControlRequest() {
return mDevControlRequest;
}
void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue"));
if(NULL != rec) {
@ -233,11 +250,13 @@ class Inverter {
val <<= 8;
val |= buf[ptr];
} while(++ptr != end);
if(FLD_T == rec->assign[pos].fieldId) {
if (FLD_T == rec->assign[pos].fieldId) {
// temperature is a signed value!
rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div);
}
else {
} else if ((FLD_YT == rec->assign[pos].fieldId)
&& (config->yieldCor != 0)) {
rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) - ((REC_TYP)config->yieldCor);
} else {
if ((REC_TYP)(div) > 1)
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
else
@ -254,20 +273,19 @@ class Inverter {
if (alarmMesIndex < rec->record[pos]){
alarmMesIndex = rec->record[pos];
//enqueCommand<InfoCommand>(AlarmUpdate); // What is the function of AlarmUpdate?
DPRINTLN(DBG_INFO, "alarm ID incremented to " + String(alarmMesIndex));
enqueCommand<InfoCommand>(AlarmData);
}
else {
alarmMesIndex = rec->record[pos]; // no change
}
}
}
else if (rec->assign == InfoAssignment) {
DPRINTLN(DBG_DEBUG, "add info");
// eg. fw version ...
isConnected = true;
}
else if (rec->assign == SystemConfigParaAssignment) {
DPRINTLN(DBG_DEBUG, "add config");
// get at least the firmware version and save it to the inverter object
if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
actPowerLimit = rec->record[pos];
DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit, 1));
@ -286,6 +304,32 @@ class Inverter {
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
}
bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue"));
if(NULL == rec)
return false;
if(pos > rec->length)
return false;
rec->record[pos] = val;
return true;
}
REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
uint8_t pos = 0;
if(NULL != rec) {
for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break;
}
if(pos >= rec->length)
return 0;
return rec->record[pos];
}
else
return 0;
}
REC_TYP getValue(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue"));
if(NULL == rec)
@ -306,16 +350,23 @@ class Inverter {
}
}
bool isAvailable(uint32_t timestamp, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isAvailable"));
return ((timestamp - rec->ts) < INACT_THRES_SEC);
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 isProducing(uint32_t timestamp, record_t<> *rec) {
bool isProducing(uint32_t timestamp) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing"));
if(isAvailable(timestamp, rec)) {
uint8_t pos = getPosByChFld(CH0, FLD_PAC, rec);
return (getValue(pos, rec) > INACT_PWR_THRESH);
if(isAvailable(timestamp)) {
uint8_t pos = getPosByChFld(CH0, FLD_PAC, &recordMeas);
return (getValue(pos, &recordMeas) > INACT_PWR_THRESH);
}
return false;
}
@ -333,10 +384,10 @@ class Inverter {
record_t<> *getRecordStruct(uint8_t cmd) {
switch (cmd) {
case RealTimeRunData_Debug: return &recordMeas;
case InverterDevInform_All: return &recordInfo;
case SystemConfigPara: return &recordConfig;
case AlarmData: return &recordAlarm;
case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b
case InverterDevInform_All: return &recordInfo; // 1 = 0x01
case SystemConfigPara: return &recordConfig; // 5 = 0x05
case AlarmData: return &recordAlarm; // 17 = 0x11
default: break;
}
return NULL;
@ -399,7 +450,27 @@ class Inverter {
}
}
String getAlarmStr(u_int16_t alarmCode) {
uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len, uint32_t *start, uint32_t *endTime) {
uint8_t startOff = 2 + id * ALARM_LOG_ENTRY_SIZE;
if((startOff + ALARM_LOG_ENTRY_SIZE) > len)
return 0;
uint16_t wCode = ((uint16_t)pyld[startOff]) << 8 | pyld[startOff+1];
uint32_t startTimeOffset = 0, endTimeOffset = 0;
if (((wCode >> 13) & 0x01) == 1) // check if is AM or PM
startTimeOffset = 12 * 60 * 60;
if (((wCode >> 12) & 0x01) == 1) // check if is AM or PM
endTimeOffset = 12 * 60 * 60;
*start = (((uint16_t)pyld[startOff + 4] << 8) | ((uint16_t)pyld[startOff + 5])) + startTimeOffset;
*endTime = (((uint16_t)pyld[startOff + 6] << 8) | ((uint16_t)pyld[startOff + 7])) + endTimeOffset;
DPRINTLN(DBG_INFO, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(*start) + ", end: " + ah::getTimeStr(*endTime));
return pyld[startOff+1];
}
String getAlarmStr(uint16_t alarmCode) {
switch (alarmCode) { // breaks are intentionally missing!
case 1: return String(F("Inverter start"));
case 2: return String(F("DTU command failed"));
@ -474,7 +545,6 @@ class Inverter {
}
private:
std::queue<std::shared_ptr<CommandAbstract>> _commandQueue;
void toRadioId(void) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:toRadioId"));
radioId.u64 = 0ULL;
@ -484,6 +554,9 @@ class Inverter {
radioId.b[1] = config->serial.b[3];
radioId.b[0] = 0x01;
}
std::queue<std::shared_ptr<CommandAbstract>> _commandQueue;
bool mDevControlRequest; // true if change needed
};

338
src/hm/hmRadio.h

@ -9,28 +9,10 @@
#include "../utils/dbg.h"
#include <RF24.h>
#include "../utils/crc.h"
#ifndef DISABLE_IRQ
#if defined(ESP8266) || defined(ESP32)
#define DISABLE_IRQ noInterrupts()
#define RESTORE_IRQ interrupts()
#else
#define DISABLE_IRQ \
uint8_t sreg = SREG; \
cli();
#define RESTORE_IRQ \
SREG = sreg;
#endif
#endif
//#define CHANNEL_HOP // switch between channels or use static channel to send
#define SPI_SPEED 1000000
#define DEFAULT_RECV_CHANNEL 3
#define SPI_SPEED 1000000
#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL)
#define RF_CHANNELS 5
#define RF_LOOP_CNT 300
#define RF_CHANNELS 5
#define TX_REQ_INFO 0x15
#define TX_REQ_DEVCONTROL 0x51
@ -61,11 +43,12 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
#define BIT_CNT(x) ((x)<<3)
static volatile bool mIrqRcvd;
//-----------------------------------------------------------------------------
// HM Radio class
//-----------------------------------------------------------------------------
template <class BUFFER, uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW>
template <uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW>
class HmRadio {
public:
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) {
@ -84,22 +67,22 @@ class HmRadio {
mRfChLst[3] = 61;
mRfChLst[4] = 75;
// default channels
mTxChIdx = 2; // Start TX with 40
mRxChIdx = 0; // Start RX with 03
mRxLoopCnt = RF_LOOP_CNT;
mSendCnt = 0;
mSendCnt = 0;
mRetransmits = 0;
mSerialDebug = false;
mIrqRcvd = false;
mSerialDebug = false;
mIrqRcvd = false;
}
~HmRadio() {}
void setup(BUFFER *ctrl, uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN) {
void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN) {
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
pinMode(irq, INPUT_PULLUP);
mBufCtrl = ctrl;
attachInterrupt(digitalPinToInterrupt(irq), []()IRAM_ATTR{ mIrqRcvd = true; }, FALLING);
uint32_t dtuSn = 0x87654321;
uint32_t chipID = 0; // will be filled with last 3 bytes of MAC
@ -120,27 +103,22 @@ class HmRadio {
DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01;
mNrf24.begin(ce, cs);
mNrf24.setRetries(0, 0);
mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
mNrf24.setChannel(DEFAULT_RECV_CHANNEL);
mNrf24.setChannel(mRfChLst[mRxChIdx]);
mNrf24.setDataRate(RF24_250KBPS);
mNrf24.setAutoAck(true);
mNrf24.enableDynamicPayloads();
mNrf24.setCRCLength(RF24_CRC_16);
mNrf24.setAutoAck(false);
mNrf24.setPayloadSize(MAX_RF_PAYLOAD_SIZE);
mNrf24.setAddressWidth(5);
mNrf24.openReadingPipe(1, DTU_RADIO_ID);
mNrf24.enableDynamicPayloads();
// enable only receiving interrupts
mNrf24.maskIRQ(true, true, false);
// enable all receiving interrupts
mNrf24.maskIRQ(false, false, false);
DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_"));
DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr]));
mNrf24.setPALevel(ampPwr & 0x03);
mNrf24.startListening();
mTxCh = setDefaultChannels();
if(mNrf24.isChipConnected()) {
DPRINTLN(DBG_INFO, F("Radio Config:"));
@ -150,77 +128,70 @@ class HmRadio {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
void loop(void) {
if(mIrqRcvd) {
DISABLE_IRQ;
mIrqRcvd = false;
bool tx_ok, tx_fail, rx_ready;
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
RESTORE_IRQ;
uint8_t pipe, len;
packet_t *p;
while(mNrf24.available(&pipe)) {
if(!mBufCtrl->full()) {
p = mBufCtrl->getFront();
p->rxCh = mRfChLst[mRxChIdx];
len = mNrf24.getPayloadSize();
if(len > MAX_RF_PAYLOAD_SIZE)
len = MAX_RF_PAYLOAD_SIZE;
mNrf24.read(p->packet, len);
mBufCtrl->pushFront(p);
yield();
bool loop(void) {
if (!mIrqRcvd)
return false; // nothing to do
mIrqRcvd = false;
bool tx_ok, tx_fail, rx_ready;
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
mNrf24.flush_tx(); // empty TX FIFO
//DBGPRINTLN("TX whatHappened Ch" + String(mRfChLst[mTxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready));
// start listening on the default RX channel
mRxChIdx = 0;
mNrf24.setChannel(mRfChLst[mRxChIdx]);
mNrf24.startListening();
//uint32_t debug_ms = millis();
uint16_t cnt = 300; // that is 60 times 5 channels
while (0 < cnt--) {
uint32_t startMillis = millis();
while (millis()-startMillis < 4) { // listen 4ms to each channel
if (mIrqRcvd) {
mIrqRcvd = false;
if (getReceived()) { // everything received
//DBGPRINTLN("RX finished Cnt: " + String(300-cnt) + " time used: " + String(millis()-debug_ms)+ " ms");
mNrf24.stopListening();
return true;
}
}
else
break;
yield();
}
mNrf24.flush_rx(); // drop the packet
RESTORE_IRQ;
switchRxCh(); // switch to next RX channel
yield();
}
// not finished but time is over
//DBGPRINTLN("RX not finished: 300 time used: " + String(millis()-debug_ms)+ " ms");
mNrf24.stopListening();
return true;
}
bool isChipConnected(void) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
return mNrf24.isChipConnected();
}
void enableDebug() {
mSerialDebug = true;
}
void handleIntr(void) {
mIrqRcvd = true;
}
uint8_t setDefaultChannels(void) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setDefaultChannels"));
mTxChIdx = 2; // Start TX with 40
mRxChIdx = 0; // Start RX with 03
return mRfChLst[mTxChIdx];
}
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) {
DPRINTLN(DBG_INFO, F("sendControlPacket cmd: ") + String(cmd));
sendCmdPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME, false);
uint8_t cnt = 0;
mTxBuf[10 + cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[10 + cnt++] = 0x00;
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit) {
DPRINTLN(DBG_INFO, F("sendControlPacket cmd: 0x") + String(cmd, HEX));
initPacket(invId, 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[10 + cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
mTxBuf[10 + cnt++] = ((data[0] * 10) ) & 0xff; // power limit
mTxBuf[10 + cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
mTxBuf[10 + cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
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
}
// crc control data
uint16_t crc = ah::crc16(&mTxBuf[10], cnt);
mTxBuf[10 + cnt++] = (crc >> 8) & 0xff;
mTxBuf[10 + cnt++] = (crc ) & 0xff;
// crc over all
mTxBuf[10 + cnt] = ah::crc8(mTxBuf, 10 + cnt);
sendPacket(invId, mTxBuf, 10 + cnt + 1, true);
sendPacket(invId, cnt, isRetransmit, true);
}
void sendTimePacket(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId) {
DPRINTLN(DBG_INFO, F("sendTimePacket ") + String(cmd, HEX));
sendCmdPacket(invId, TX_REQ_INFO, ALL_FRAMES, false);
void sendTimePacket(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit) {
DPRINTLN(DBG_DEBUG, F("sendTimePacket 0x") + String(cmd, HEX));
initPacket(invId, TX_REQ_INFO, ALL_FRAMES);
mTxBuf[10] = cmd; // cid
mTxBuf[11] = 0x00;
CP_U32_LittleEndian(&mTxBuf[12], ts);
@ -228,61 +199,16 @@ class HmRadio {
mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff;
}
uint16_t crc = ah::crc16(&mTxBuf[10], 14);
mTxBuf[24] = (crc >> 8) & 0xff;
mTxBuf[25] = (crc ) & 0xff;
mTxBuf[26] = ah::crc8(mTxBuf, 26);
sendPacket(invId, mTxBuf, 27, true);
}
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) {
DPRINTLN(DBG_VERBOSE, F("sendCmdPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX));
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE);
mTxBuf[0] = mid; // message id
CP_U32_BigEndian(&mTxBuf[1], (invId >> 8));
CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8));
mTxBuf[9] = pid;
if(calcCrc) {
mTxBuf[10] = ah::crc8(mTxBuf, 10);
sendPacket(invId, mTxBuf, 11, false);
}
}
bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) {
//DPRINTLN(DBG_INFO, F("hmRadio.h:checkPaketCrc"));
*len = (buf[0] >> 2);
if(*len > (MAX_RF_PAYLOAD_SIZE - 2))
*len = MAX_RF_PAYLOAD_SIZE - 2;
for(uint8_t i = 1; i < (*len + 1); i++) {
buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7);
}
uint8_t crc = ah::crc8(buf, *len-1);
bool valid = (crc == buf[*len-1]);
return valid;
sendPacket(invId, 24, isRetransmit, true);
}
bool switchRxCh(uint16_t addLoop = 0) {
if(!mNrf24.isChipConnected())
return true;
mRxLoopCnt += addLoop;
if(mRxLoopCnt != 0) {
mRxLoopCnt--;
DISABLE_IRQ;
mNrf24.stopListening();
mNrf24.setChannel(getRxNxtChannel());
mNrf24.startListening();
RESTORE_IRQ;
}
return (0 == mRxLoopCnt); // receive finished
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit) {
initPacket(invId, mid, pid);
sendPacket(invId, 10, isRetransmit, false);
}
void dumpBuf(const char *info, uint8_t buf[], uint8_t len) {
void dumpBuf(uint8_t buf[], uint8_t len) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf"));
if(NULL != info)
DBGPRINT(String(info));
for(uint8_t i = 0; i < len; i++) {
DHEX(buf[i]);
DBGPRINT(" ");
@ -290,11 +216,6 @@ class HmRadio {
DBGPRINTLN("");
}
bool isChipConnected(void) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
return mNrf24.isChipConnected();
}
uint8_t getDataRate(void) {
if(!mNrf24.isChipConnected())
return 3; // unkown
@ -305,82 +226,99 @@ class HmRadio {
return mNrf24.isPVariant();
}
std::queue<packet_t> mBufCtrl;
uint32_t mSendCnt;
uint32_t mRetransmits;
bool mSerialDebug;
private:
void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len, bool clear=false) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket"));
//DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt));
//dumpBuf("SEN ", buf, len);
if(mSerialDebug) {
DPRINT(DBG_INFO, "TX " + String(len) + "B Ch" + String(mRfChLst[mTxChIdx]) + " | ");
dumpBuf(NULL, buf, len);
bool getReceived(void) {
bool tx_ok, tx_fail, rx_ready;
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
DBGPRINTLN("RX whatHappened Ch" + String(mRfChLst[mRxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready));
bool isLastPackage = false;
while(mNrf24.available()) {
uint8_t len;
len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed
if (len > 0) {
packet_t p;
p.ch = mRfChLst[mRxChIdx];
p.len = len;
mNrf24.read(p.packet, len);
mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
isLastPackage = (p.packet[9] > 0x81); // > 0x81 indicates last packet received
else if (p.packet[0] != 0x00) // ignore fragment number zero
isLastPackage = true; // response from dev control command
yield();
}
}
return isLastPackage;
}
DISABLE_IRQ;
void switchRxCh() {
mNrf24.stopListening();
if(clear)
mRxLoopCnt = RF_LOOP_CNT;
mNrf24.setChannel(mRfChLst[mTxChIdx]);
mTxCh = getTxNxtChannel(); // switch channel for next packet
mNrf24.openWritingPipe(invId); // TODO: deprecated
mNrf24.setCRCLength(RF24_CRC_16);
mNrf24.enableDynamicPayloads();
mNrf24.setAutoAck(true);
mNrf24.setRetries(3, 15); // 3*250us and 15 loops -> 11.25ms
mNrf24.write(buf, len);
// Try to avoid zero payload acks (has no effect)
mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated
mRxChIdx = 0;
// get next channel index
if(++mRxChIdx >= RF_CHANNELS)
mRxChIdx = 0;
mNrf24.setChannel(mRfChLst[mRxChIdx]);
mNrf24.setAutoAck(false);
mNrf24.setRetries(0, 0);
mNrf24.disableDynamicPayloads();
mNrf24.setCRCLength(RF24_CRC_DISABLED);
mNrf24.startListening();
}
RESTORE_IRQ;
mSendCnt++;
void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) {
DPRINTLN(DBG_VERBOSE, F("initPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX));
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE);
mTxBuf[0] = mid; // message id
CP_U32_BigEndian(&mTxBuf[1], (invId >> 8));
CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8));
mTxBuf[9] = pid;
}
uint8_t getTxNxtChannel(void) {
void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool clear=false) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket"));
//DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt));
// append crc's
if (len > 10) {
// crc control data
uint16_t crc = ah::crc16(&mTxBuf[10], len - 10);
mTxBuf[len++] = (crc >> 8) & 0xff;
mTxBuf[len++] = (crc ) & 0xff;
}
// crc over all
mTxBuf[len++] = ah::crc8(mTxBuf, len);
if(mSerialDebug) {
DPRINT(DBG_INFO, "TX " + String(len) + "B Ch" + String(mRfChLst[mTxChIdx]) + " | ");
dumpBuf(mTxBuf, len);
}
mNrf24.setChannel(mRfChLst[mTxChIdx]);
mNrf24.openWritingPipe(reinterpret_cast<uint8_t*>(&invId));
mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response
// switch TX channel for next packet
if(++mTxChIdx >= RF_CHANNELS)
mTxChIdx = 0;
return mRfChLst[mTxChIdx];
}
uint8_t getRxNxtChannel(void) {
if(++mRxChIdx >= RF_CHANNELS)
mRxChIdx = 0;
return mRfChLst[mRxChIdx];
if(isRetransmit)
mRetransmits++;
else
mSendCnt++;
}
uint64_t DTU_RADIO_ID;
uint8_t mTxCh;
uint8_t mTxChIdx;
uint8_t mRfChLst[RF_CHANNELS];
uint8_t mTxChIdx;
uint8_t mRxChIdx;
uint16_t mRxLoopCnt;
RF24 mNrf24;
BUFFER *mBufCtrl;
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
DevControlCmdType DevControlCmd;
volatile bool mIrqRcvd;
};
#endif /*__RADIO_H__*/

16
src/hm/hmSystem.h

@ -8,19 +8,11 @@
#include "hmInverter.h"
#include "hmRadio.h"
#include "CircularBuffer.h"
typedef CircularBuffer<packet_t, PACKET_BUFFER_SIZE> BufferType;
typedef HmRadio<BufferType> RadioType;
template <uint8_t MAX_INVERTER=3, class RADIO = RadioType, class BUFFER = BufferType, class INVERTERTYPE=Inverter<float>>
template <uint8_t MAX_INVERTER=3, class INVERTERTYPE=Inverter<float>>
class HmSystem {
public:
typedef RADIO RadioType;
RadioType Radio;
typedef BUFFER BufferType;
BufferType BufCtrl;
//DevControlCmdType DevControlCmd;
HmRadio<> Radio;
HmSystem() {
mNumInv = 0;
@ -30,11 +22,11 @@ class HmSystem {
}
void setup() {
Radio.setup(&BufCtrl);
Radio.setup();
}
void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin) {
Radio.setup(&BufCtrl, ampPwr, irqPin, cePin, csPin);
Radio.setup(ampPwr, irqPin, cePin, csPin);
}
void addInverters(cfgInst_t *config) {

288
src/hm/payload.h

@ -8,7 +8,6 @@
#include "../utils/dbg.h"
#include "../utils/crc.h"
#include "../utils/handler.h"
#include "../config/config.h"
#include <Arduino.h>
@ -21,63 +20,111 @@ typedef struct {
uint8_t len[MAX_PAYLOAD_ENTRIES];
bool complete;
uint8_t maxPackId;
bool lastFound;
uint8_t retransmits;
bool requested;
bool gotFragment;
} invPayload_t;
typedef std::function<void(uint8_t)> payloadListenerType;
typedef std::function<void(uint16_t alarmCode, uint32_t start, uint32_t end)> alarmListenerType;
template<class HMSYSTEM>
class Payload : public Handler<payloadListenerType> {
class Payload {
public:
Payload() : Handler() {}
void setup(HMSYSTEM *sys) {
mSys = sys;
memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t)));
mLastPacketId = 0x00;
mSerialDebug = false;
Payload() {}
void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
mApp = app;
mSys = sys;
mStat = stat;
mMaxRetrans = maxRetransmits;
mTimestamp = timestamp;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
reset(i);
}
mSerialDebug = false;
mHighPrioIv = NULL;
mCbAlarm = NULL;
}
void enableSerialDebug(bool enable) {
mSerialDebug = enable;
}
bool isComplete(Inverter<> *iv) {
return mPayload[iv->id].complete;
void addPayloadListener(payloadListenerType cb) {
mCbPayload = cb;
}
uint8_t getMaxPacketId(Inverter<> *iv) {
return mPayload[iv->id].maxPackId;
void addAlarmListener(alarmListenerType cb) {
mCbAlarm = cb;
}
uint8_t getRetransmits(Inverter<> *iv) {
return mPayload[iv->id].retransmits;
void loop() {
if(NULL != mHighPrioIv) {
ivSend(mHighPrioIv, true);
mHighPrioIv = NULL;
}
}
uint32_t getTs(Inverter<> *iv) {
return mPayload[iv->id].ts;
void ivSendHighPrio(Inverter<> *iv) {
mHighPrioIv = iv;
}
void request(Inverter<> *iv) {
mPayload[iv->id].requested = true;
}
void ivSend(Inverter<> *iv, bool highPrio = false) {
if(!highPrio) {
if (mPayload[iv->id].requested) {
if (!mPayload[iv->id].complete)
process(false); // no retransmit
if (!mPayload[iv->id].complete) {
if (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("(#") + String(iv->id) + ") ");
DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload[iv->id].retransmits) + ")");
}
}
}
}
void setTxCmd(Inverter<> *iv, uint8_t cmd) {
mPayload[iv->id].txCmd = cmd;
}
reset(iv->id);
mPayload[iv->id].requested = true;
void notify(uint8_t val) {
for(typename std::list<payloadListenerType>::iterator it = mList.begin(); it != mList.end(); ++it) {
(*it)(val);
yield();
if (mSerialDebug)
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Requesting Inv SN ") + String(iv->config->serial.u64, HEX));
if (iv->getDevControlRequest()) {
if (mSerialDebug)
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Devcontrol request 0x") + String(iv->devControlCmd, HEX) + F(" power limit ") + String(iv->powerLimit[0]));
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false);
mPayload[iv->id].txCmd = iv->devControlCmd;
//iv->clearCmdQueue();
//iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
} else {
uint8_t cmd = iv->getQueuedCmd();
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket")); // + String(cmd, HEX));
mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false);
mPayload[iv->id].txCmd = cmd;
}
}
void add(packet_t *p, uint8_t len) {
void add(packet_t *p) {
Inverter<> *iv = mSys->findInverter(&p->packet[1]);
if ((NULL != iv) && (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES))) { // response from get information command
if(NULL == iv)
return;
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
mPayload[iv->id].txId = p->packet[0];
DPRINTLN(DBG_DEBUG, F("Response from info request received"));
uint8_t *pid = &p->packet[9];
@ -85,56 +132,42 @@ class Payload : public Handler<payloadListenerType> {
DPRINT(DBG_DEBUG, F("fragment number zero received and ignored"));
} else {
DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX));
if ((*pid & 0x7F) < 5) {
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len - 11);
mPayload[iv->id].len[(*pid & 0x7F) - 1] = len - 11;
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11);
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11;
mPayload[iv->id].gotFragment = true;
}
if ((*pid & ALL_FRAMES) == ALL_FRAMES) {
// Last packet
if ((*pid & 0x7f) > mPayload[iv->id].maxPackId) {
if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) {
mPayload[iv->id].maxPackId = (*pid & 0x7f);
if (*pid > 0x81)
mLastPacketId = *pid;
mPayload[iv->id].lastFound = true;
}
}
}
}
if ((NULL != iv) && (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES))) { // response from dev control command
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received"));
mPayload[iv->id].txId = p->packet[0];
iv->devControlRequest = false;
iv->clearDevControlRequest();
if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) {
String msg = (p->packet[10] == 0x00 && p->packet[11] == 0x00) ? "" : "NOT ";
String msg = "";
if((p->packet[10] == 0x00) && (p->packet[11] == 0x00))
mApp->setMqttPowerLimitAck(iv);
else
msg = "NOT ";
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has ") + msg + F("accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1]));
iv->clearCmdQueue();
iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
}
iv->devControlCmd = Init;
}
}
bool build(uint8_t id) {
DPRINTLN(DBG_VERBOSE, F("build"));
uint16_t crc = 0xffff, crcRcv = 0x0000;
if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES)
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) {
if (mPayload[id].len[i] > 0) {
if (i == (mPayload[id].maxPackId - 1)) {
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]);
} else
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
}
yield();
}
return (crc == crcRcv) ? true : false;
}
void process(bool retransmit, uint8_t maxRetransmits, statistics_t *stat) {
void process(bool retransmit) {
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv)
@ -142,45 +175,57 @@ class Payload : public Handler<payloadListenerType> {
if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) {
// no processing needed if txId is not 0x95
// DPRINTLN(DBG_INFO, F("processPayload - set complete, txId: ") + String(mPayload[iv->id].txId, HEX));
mPayload[iv->id].complete = true;
continue; // skip to next inverter
}
if (!mPayload[iv->id].complete) {
if (!build(iv->id)) { // payload not complete
bool crcPass, pyldComplete;
crcPass = build(iv->id, &pyldComplete);
if (!crcPass && !pyldComplete) { // payload not complete
if ((mPayload[iv->id].requested) && (retransmit)) {
if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) {
// This is required to prevent retransmissions without answer.
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm..."));
mPayload[iv->id].retransmits = maxRetransmits;
mPayload[iv->id].retransmits = mMaxRetrans;
} else if(iv->devControlCmd == ActivePowerContr) {
DPRINTLN(DBG_INFO, F("retransmit power limit"));
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true);
} else {
if (mPayload[iv->id].retransmits < maxRetransmits) {
if (mPayload[iv->id].retransmits < mMaxRetrans) {
mPayload[iv->id].retransmits++;
if (mPayload[iv->id].maxPackId != 0) {
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(") sendTimePacket 0x") + String(mPayload[iv->id].txCmd, HEX));
mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
*/
DPRINTLN(DBG_WARN, F("(#") + String(iv->id) + 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) {
DPRINTLN(DBG_WARN, F("while retrieving data: Frame ") + String(i + 1) + F(" missing: Request Retransmit"));
DPRINTLN(DBG_WARN, F("Frame ") + String(i + 1) + F(" missing: Request Retransmit"));
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true);
break; // only retransmit one frame per loop
break; // only request retransmit one frame per loop
}
yield();
}
} else {
DPRINTLN(DBG_WARN, F("while retrieving data: last frame missing: Request Retransmit"));
if (0x00 != mLastPacketId)
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, mLastPacketId, true);
else {
mPayload[iv->id].txCmd = iv->getQueuedCmd();
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket"));
mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex);
}
}
mSys->Radio.switchRxCh(100);
}
}
}
} 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();
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket 0x") + String(mPayload[iv->id].txCmd, HEX));
mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
}
} else { // payload complete
DPRINTLN(DBG_INFO, F("procPyld: cmd: ") + String(mPayload[iv->id].txCmd));
DPRINTLN(DBG_INFO, F("procPyld: cmd: 0x") + String(mPayload[iv->id].txCmd, HEX));
DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + 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
@ -200,14 +245,14 @@ class Payload : public Handler<payloadListenerType> {
if (mSerialDebug) {
DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): ");
mSys->Radio.dumpBuf(NULL, payload, payloadLen);
mSys->Radio.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 + 0x80))
stat->rxSuccess++;
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++) {
@ -216,36 +261,95 @@ class Payload : public Handler<payloadListenerType> {
}
iv->doCalculations();
notify(mPayload[iv->id].txCmd);
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
uint16_t code;
uint32_t start, end;
while(1) {
code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end);
if(0 == code)
break;
if (NULL != mCbAlarm)
(mCbAlarm)(code, start, end);
yield();
}
}
} else {
DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes"));
stat->rxFail++;
mStat->rxFail++;
}
iv->setQueuedCmdFinished();
}
}
yield();
}
}
private:
void notify(uint8_t val) {
(mCbPayload)(val);
}
void notify(uint16_t code, uint32_t start, uint32_t endTime) {
if (NULL != mCbAlarm)
(mCbAlarm)(code, start, endTime);
}
bool build(uint8_t id, bool *complete) {
DPRINTLN(DBG_VERBOSE, F("build"));
uint16_t crc = 0xffff, crcRcv = 0x0000;
if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES)
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
// check if all fragments are there
*complete = true;
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) {
if(mPayload[id].len[i] == 0)
*complete = false;
}
if(!*complete)
return false;
for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) {
if (mPayload[id].len[i] > 0) {
if (i == (mPayload[id].maxPackId - 1)) {
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]);
} else
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
}
yield();
}
return (crc == crcRcv) ? true : false;
}
void reset(Inverter<> *iv, uint32_t utcTs) {
DPRINTLN(DBG_INFO, "resetPayload: id: " + String(iv->id));
memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES);
mPayload[iv->id].txCmd = 0;
mPayload[iv->id].retransmits = 0;
mPayload[iv->id].maxPackId = 0;
mPayload[iv->id].complete = false;
mPayload[iv->id].requested = false;
mPayload[iv->id].ts = utcTs;
void reset(uint8_t id) {
DPRINTLN(DBG_INFO, "resetPayload: id: " + String(id));
memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES);
mPayload[id].txCmd = 0;
mPayload[id].gotFragment = false;
mPayload[id].retransmits = 0;
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
mPayload[id].lastFound = false;
mPayload[id].complete = false;
mPayload[id].requested = false;
mPayload[id].ts = *mTimestamp;
}
private:
IApp *mApp;
HMSYSTEM *mSys;
statistics_t *mStat;
uint8_t mMaxRetrans;
uint32_t *mTimestamp;
invPayload_t mPayload[MAX_NUM_INVERTERS];
uint8_t mLastPacketId;
bool mSerialDebug;
Inverter<> *mHighPrioIv;
alarmListenerType mCbAlarm;
payloadListenerType mCbPayload;
};
#endif /*__PAYLOAD_H_*/

10
src/main.cpp

@ -7,21 +7,11 @@
#include "app.h"
#include "config/config.h"
app myApp;
//-----------------------------------------------------------------------------
IRAM_ATTR void handleIntr(void) {
myApp.handleIntr();
}
//-----------------------------------------------------------------------------
void setup() {
myApp.setup();
// TODO: move to HmRadio
attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING);
}

74
src/platformio.ini

@ -38,6 +38,8 @@ lib_deps =
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
https://github.com/JChristensen/Timezone
olikraus/U8g2
;esp8266/DNSServer
;esp8266/EEPROM
;esp8266/ESP8266WiFi
@ -89,42 +91,6 @@ monitor_filters =
time ; Add timestamp with milliseconds for each new line
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
[env:esp8266-nokia5110]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = -D RELEASE -DU8X8_NO_HW_I2C -DENA_NOKIA
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
olikraus/U8g2
https://github.com/JChristensen/Timezone
[env:esp8266-ssd1306]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = -D RELEASE -DENA_SSD1306
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
https://github.com/ThingPulse/esp8266-oled-ssd1306.git
https://github.com/JChristensen/Timezone
[env:esp32-wroom32-release]
platform = espressif32
board = lolin_d32
@ -145,39 +111,3 @@ monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
[env:esp32-wroom32-nokia5110]
platform = espressif32
board = lolin_d32
build_flags = -D RELEASE -std=gnu++14 -DU8X8_NO_HW_I2C -DENA_NOKIA
build_unflags = -std=gnu++11
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
olikraus/U8g2
https://github.com/JChristensen/Timezone
[env:esp32-wroom32-ssd1306]
platform = espressif32
board = lolin_d32
build_flags = -D RELEASE -std=gnu++14 -DENA_SSD1306
build_unflags = -std=gnu++11
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
https://github.com/ThingPulse/esp8266-oled-ssd1306.git
https://github.com/JChristensen/Timezone

418
src/plugins/MonochromeDisplay/MonochromeDisplay.h

@ -1,27 +1,41 @@
#ifndef __MONOCHROME_DISPLAY__
#define __MONOCHROME_DISPLAY__
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
#ifdef ENA_NOKIA
#include <U8g2lib.h>
#define DISP_PROGMEM U8X8_PROGMEM
#else // ENA_SSD1306
/* esp8266 : SCL = 5, SDA = 4 */
/* ewsp32 : SCL = 22, SDA = 21 */
#include <Wire.h>
#include <SSD1306Wire.h>
#define DISP_PROGMEM PROGMEM
#endif
#include <U8g2lib.h>
#include <Timezone.h>
#include "../../utils/helper.h"
#include "../../hm/hmSystem.h"
static uint8_t bmp_arrow[] DISP_PROGMEM = {
#define DISP_DEFAULT_TIMEOUT 60 // in seconds
static uint8_t bmp_logo[] PROGMEM = {
B00000000, B00000000, // ................
B11101100, B00110111, // ..##.######.##..
B11101100, B00110111, // ..##.######.##..
B11100000, B00000111, // .....######.....
B11010000, B00001011, // ....#.####.#....
B10011000, B00011001, // ...##..##..##...
B10000000, B00000001, // .......##.......
B00000000, B00000000, // ................
B01111000, B00011110, // ...####..####...
B11111100, B00111111, // ..############..
B01111100, B00111110, // ..#####..#####..
B00000000, B00000000, // ................
B11111100, B00111111, // ..############..
B11111110, B01111111, // .##############.
B01111110, B01111110, // .######..######.
B00000000, B00000000 // ................
};
static uint8_t bmp_arrow[] PROGMEM = {
B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111,
B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111,
B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000};
B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000
};
static TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Tim
@ -29,47 +43,42 @@ static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central Eu
template<class HMSYSTEM>
class MonochromeDisplay {
public:
#if defined(ENA_NOKIA)
MonochromeDisplay() : mDisplay(U8G2_R0, 5, 4, 16), mCE(CEST, CET) {
mNewPayload = false;
mExtra = 0;
}
#else // ENA_SSD1306
MonochromeDisplay() : mDisplay(0x3c, SDA, SCL), mCE(CEST, CET) {
mNewPayload = false;
mExtra = 0;
mRx = 0;
mUp = 1;
}
#endif
MonochromeDisplay() : mCE(CEST, CET) {}
void setup(HMSYSTEM *sys, uint32_t *utcTs) {
void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, uint8_t disp_reset, const char *version) {
mCfg = cfg;
mSys = sys;
mUtcTs = utcTs;
memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS );
memset( mTotal, 0, sizeof(float)*MAX_NUM_INVERTERS );
mLastHour = 25;
#if defined(ENA_NOKIA)
mDisplay.begin();
ShowInfoText("booting...");
#else
mDisplay.init();
mDisplay.flipScreenVertically();
mDisplay.setContrast(63);
mDisplay.setBrightness(63);
mDisplay.clear();
mDisplay.setFont(ArialMT_Plain_24);
mDisplay.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
mDisplay.drawString(64,22,"Starting...");
mDisplay.display();
mDisplay.setTextAlignment(TEXT_ALIGN_LEFT);
#endif
}
mNewPayload = false;
mLoopCnt = 0;
mTimeout = DISP_DEFAULT_TIMEOUT; // power off timeout (after inverters go offline)
void loop(void) {
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot180) ? U8G2_R2 : U8G2_R0);
if(mCfg->type) {
switch(mCfg->type) {
case 1:
mDisplay = new U8G2_PCD8544_84X48_F_4W_HW_SPI(rot, mCfg->pin0, mCfg->pin1, disp_reset);
break;
case 2:
mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, disp_reset, mCfg->pin0, mCfg->pin1);
break;
case 3:
mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, disp_reset, mCfg->pin0, mCfg->pin1);
break;
}
mDisplay->begin();
mIsLarge = ((mDisplay->getWidth() > 120) && (mDisplay->getHeight() > 60));
calcLineHeights();
mDisplay->clearBuffer();
mDisplay->setContrast(mCfg->contrast);
printText("Ahoy!", 0, 35);
printText("ahoydtu.de", 2, 20);
printText(version, 3, 46);
mDisplay->sendBuffer();
}
}
void payloadEventListener(uint8_t cmd) {
@ -77,231 +86,136 @@ class MonochromeDisplay {
}
void tickerSecond() {
static int cnt=1;
if(mNewPayload || !(cnt % 10)) {
cnt=1;
if(mCfg->pwrSaveAtIvOffline) {
if(mTimeout != 0)
mTimeout--;
}
if(mNewPayload || ((++mLoopCnt % 10) == 0)) {
mNewPayload = false;
mLoopCnt = 0;
DataScreen();
}
else
cnt++;
}
private:
#if defined(ENA_NOKIA)
void ShowInfoText(const char *txt) {
/* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */
mDisplay.clear();
mDisplay.firstPage();
do {
const char *e;
const char *p = txt;
int y=10;
mDisplay.setFont(u8g2_font_5x8_tr);
while(1) {
for(e=p+1; (*e && (*e != '\n')); e++);
size_t len=e-p;
mDisplay.setCursor(2,y);
String res=((String)p).substring(0,len);
mDisplay.print(res);
if ( !*e )
break;
p=e+1;
y+=12;
}
mDisplay.sendBuffer();
} while( mDisplay.nextPage() );
}
#endif
void DataScreen() {
if (mCfg->type == 0)
return;
if(*mUtcTs == 0)
return;
float totalPower = 0;
float totalYieldDay = 0;
float totalYieldTotal = 0;
bool isprod = false;
Inverter<> *iv;
record_t<> *rec;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
iv = mSys->getInverterByPos(i);
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL)
continue;
if (iv->isProducing(*mUtcTs))
isprod = true;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
}
void DataScreen(void) {
String timeStr = ah::getDateTimeStr(mCE.toLocal(*mUtcTs)).substring(2, 22);
int hr = timeStr.substring(9,2).toInt();
IPAddress ip = WiFi.localIP();
float totalYield = 0.0, totalYieldToday = 0.0, totalActual = 0.0;
char fmtText[32];
int ucnt=0, num_inv=0;
unsigned int pow_i[ MAX_NUM_INVERTERS ];
memset( pow_i, 0, sizeof(unsigned int)* MAX_NUM_INVERTERS );
if ( hr < mLastHour ) // next day ? reset today-values
memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS );
mLastHour = hr;
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
uint8_t pos;
uint8_t list[] = {FLD_PAC, FLD_YT, FLD_YD};
for (uint8_t fld = 0; fld < 3; fld++) {
pos = iv->getPosByChFld(CH0, list[fld],rec);
int isprod = iv->isProducing(*mUtcTs,rec);
if(fld == 1)
{
if ( isprod )
mTotal[num_inv] = iv->getValue(pos,rec);
totalYield += mTotal[num_inv];
}
if(fld == 2)
{
if ( isprod )
mToday[num_inv] = iv->getValue(pos,rec);
totalYieldToday += mToday[num_inv];
}
if((fld == 0) && isprod )
{
pow_i[num_inv] = iv->getValue(pos,rec);
totalActual += iv->getValue(pos,rec);
ucnt++;
}
}
num_inv++;
}
mDisplay->clearBuffer();
// Logos
// pxMovement +x (0 - 6 px)
uint8_t ex = (_mExtra % 7);
if (isprod) {
mDisplay->drawXBMP(5 + ex, 1, 8, 17, bmp_arrow);
if (mCfg->logoEn)
mDisplay->drawXBMP(mDisplay->getWidth() - 24 + ex, 2, 16, 16, bmp_logo);
}
/* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */
mDisplay.clear();
#if defined(ENA_NOKIA)
mDisplay.firstPage();
do {
if(ucnt) {
mDisplay.drawXBMP(10,1,8,17,bmp_arrow);
mDisplay.setFont(u8g2_font_logisoso16_tr);
mDisplay.setCursor(25,17);
sprintf(fmtText,"%3.0f",totalActual);
mDisplay.print(String(fmtText)+F(" W"));
}
else
{
mDisplay.setFont(u8g2_font_logisoso16_tr );
mDisplay.setCursor(10,17);
mDisplay.print(String(F("offline")));
}
mDisplay.drawHLine(2,20,78);
mDisplay.setFont(u8g2_font_5x8_tr);
mDisplay.setCursor(5,29);
if (( num_inv < 2 ) || !(mExtra%2))
{
sprintf(fmtText,"%4.0f",totalYieldToday);
mDisplay.print(F("today ")+String(fmtText)+F(" Wh"));
mDisplay.setCursor(5,37);
sprintf(fmtText,"%.1f",totalYield);
mDisplay.print(F("total ")+String(fmtText)+F(" kWh"));
}
else
{
int id1=(mExtra/2)%(num_inv-1);
if( pow_i[id1] )
mDisplay.print(F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W"));
else
mDisplay.print(F("#")+String(id1+1)+F(" -----"));
mDisplay.setCursor(5,37);
if( pow_i[id1+1] )
mDisplay.print(F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W"));
else
mDisplay.print(F("#")+String(id1+2)+F(" -----"));
}
if ( !(mExtra%10) && ip ) {
mDisplay.setCursor(5,47);
mDisplay.print(ip.toString());
}
else {
mDisplay.setCursor(0,47);
mDisplay.print(timeStr);
}
mDisplay.sendBuffer();
} while( mDisplay.nextPage() );
mExtra++;
#else // ENA_SSD1306
if(mUp) {
mRx += 2;
if(mRx >= 20)
mUp = 0;
if ((totalPower > 0) && isprod) {
mTimeout = DISP_DEFAULT_TIMEOUT;
mDisplay->setPowerSave(false);
mDisplay->setContrast(mCfg->contrast);
if (totalPower > 999)
snprintf(_fmtText, sizeof(_fmtText), "%2.1f kW", (totalPower / 1000));
else
snprintf(_fmtText, sizeof(_fmtText), "%3.0f W", totalPower);
printText(_fmtText, 0, 20);
} else {
mRx -= 2;
if(mRx <= 0)
mUp = 1;
}
int ex = 2*( mExtra % 5 );
if(ucnt) {
mDisplay.setBrightness(63);
mDisplay.drawXbm(10+ex,5,8,17,bmp_arrow);
mDisplay.setFont(ArialMT_Plain_24);
sprintf(fmtText,"%3.0f",totalActual);
mDisplay.drawString(25+ex,0,String(fmtText)+F(" W"));
}
else
{
mDisplay.setBrightness(1);
mDisplay.setFont(ArialMT_Plain_24);
mDisplay.drawString(25+ex,0,String(F("offline")));
}
mDisplay.setFont(ArialMT_Plain_16);
if (( num_inv < 2 ) || !(mExtra%2))
{
sprintf(fmtText,"%4.0f",totalYieldToday);
mDisplay.drawString(5,22,F("today ")+String(fmtText)+F(" Wh"));
sprintf(fmtText,"%.1f",totalYield);
mDisplay.drawString(5,35,F("total ")+String(fmtText)+F(" kWh"));
printText("offline", 0, 25);
if(mCfg->pwrSaveAtIvOffline) {
if(mTimeout == 0)
mDisplay->setPowerSave(true);
}
}
else
{
int id1=(mExtra/2)%(num_inv-1);
if( pow_i[id1] )
mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W"));
else
mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" -----"));
if( pow_i[id1+1] )
mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W"));
snprintf(_fmtText, sizeof(_fmtText), "today: %4.0f Wh", totalYieldDay);
printText(_fmtText, 1);
snprintf(_fmtText, sizeof(_fmtText), "total: %.1f kWh", totalYieldTotal);
printText(_fmtText, 2);
IPAddress ip = WiFi.localIP();
if (!(_mExtra % 10) && (ip)) {
printText(ip.toString().c_str(), 3);
} else {
// Get current time
if(mIsLarge)
printText(ah::getDateTimeStr(mCE.toLocal(*mUtcTs)).c_str(), 3);
else
mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" -----"));
printText(ah::getTimeStr(mCE.toLocal(*mUtcTs)).c_str(), 3);
}
mDisplay.drawLine(2,23,123,23);
mDisplay->sendBuffer();
if ( (!(mExtra%10) && ip )|| (timeStr.length()<16))
{
mDisplay.drawString(5,49,ip.toString());
_mExtra++;
}
void calcLineHeights() {
uint8_t yOff = 0;
for(uint8_t i = 0; i < 4; i++) {
setFont(i);
yOff += (mDisplay->getMaxCharHeight() + 1);
mLineOffsets[i] = yOff;
}
else
{
int w=mDisplay.getStringWidth(timeStr.c_str(),timeStr.length(),0);
if ( w>127 )
{
String tt=timeStr.substring(9,17);
w=mDisplay.getStringWidth(tt.c_str(),tt.length(),0);
mDisplay.drawString(127-w-mRx,49,tt);
}
else
mDisplay.drawString(0,49,timeStr);
}
inline void setFont(uint8_t line) {
switch (line) {
case 0: mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_lubBI14_tr); break;
case 3: mDisplay->setFont(u8g2_font_5x8_tr); break;
default: mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); break;
}
}
mDisplay.display();
mExtra++;
#endif
void printText(const char* text, uint8_t line, uint8_t dispX = 5) {
if(!mIsLarge)
dispX = (line == 0) ? 10 : 5;
setFont(line);
if(mCfg->pxShift)
dispX += (_mExtra % 7); // add pixel movement
mDisplay->drawStr(dispX, mLineOffsets[line], text);
}
// private member variables
#if defined(ENA_NOKIA)
U8G2_PCD8544_84X48_1_4W_HW_SPI mDisplay;
#else // ENA_SSD1306
SSD1306Wire mDisplay;
int mRx;
char mUp;
#endif
int mExtra;
U8G2* mDisplay;
uint8_t _mExtra;
uint16_t mTimeout; // interval at which to power save (milliseconds)
char _fmtText[32];
bool mNewPayload;
float mTotal[ MAX_NUM_INVERTERS ];
float mToday[ MAX_NUM_INVERTERS ];
bool mIsLarge;
uint8_t mLoopCnt;
uint32_t *mUtcTs;
int mLastHour;
uint8_t mLineOffsets[5];
display_t *mCfg;
HMSYSTEM *mSys;
Timezone mCE;
};
#endif
#endif /*__MONOCHROME_DISPLAY__*/

422
src/publisher/pubMqtt.h

@ -15,7 +15,6 @@
#endif
#include "../utils/dbg.h"
#include "../utils/ahoyTimer.h"
#include "../config/config.h"
#include <espMqttClient.h>
#include <ArduinoJson.h>
@ -26,36 +25,36 @@
typedef std::function<void(JsonObject)> subscriptionCb;
struct alarm_t {
uint16_t code;
uint32_t start;
uint32_t end;
alarm_t(uint16_t c, uint32_t s, uint32_t e) : code(c), start(s), end(e) {}
};
template<class HMSYSTEM>
class PubMqtt {
public:
PubMqtt() {
mRxCnt = 0;
mTxCnt = 0;
mEnReconnect = false;
mSubscriptionCb = NULL;
mIvAvail = true;
memset(mLastIvState, 0xff, MAX_NUM_INVERTERS);
memset(mLastIvState, MQTT_STATUS_NOT_AVAIL_NOT_PROD, MAX_NUM_INVERTERS);
}
~PubMqtt() { }
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) {
mCfgMqtt = cfg_mqtt;
mDevName = devName;
mVersion = version;
mSys = sys;
mUtcTimestamp = utcTs;
mCfgMqtt = cfg_mqtt;
mDevName = devName;
mVersion = version;
mSys = sys;
mUtcTimestamp = utcTs;
mIntervalTimeout = 1;
mReconnectRequest = false;
snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic);
#if defined(ESP8266)
mHWifiCon = WiFi.onStationModeGotIP(std::bind(&PubMqtt::onWifiConnect, this, std::placeholders::_1));
mHWifiDiscon = WiFi.onStationModeDisconnected(std::bind(&PubMqtt::onWifiDisconnect, this, std::placeholders::_1));
#else
WiFi.onEvent(std::bind(&PubMqtt::onWiFiEvent, this, std::placeholders::_1));
#endif
if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0))
mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd);
mClient.setClientId(mDevName); // TODO: add mac?
@ -69,11 +68,30 @@ class PubMqtt {
void loop() {
#if defined(ESP8266)
mClient.loop();
yield();
#endif
}
void connect() {
mReconnectRequest = false;
if(!mClient.connected())
mClient.connect();
}
void tickerSecond() {
sendIvData();
if(0 == mCfgMqtt->interval) // no fixed interval, publish once new data were received (from inverter)
sendIvData();
else { // send mqtt data in a fixed interval
if(--mIntervalTimeout == 0) {
mIntervalTimeout = mCfgMqtt->interval;
mSendList.push(RealTimeRunData_Debug);
sendIvData();
}
}
if(mReconnectRequest) {
connect();
return;
}
}
void tickerMinute() {
@ -83,36 +101,87 @@ class PubMqtt {
publish("uptime", val);
publish("wifi_rssi", String(WiFi.RSSI()).c_str());
publish("free_heap", String(ESP.getFreeHeap()).c_str());
if(!mClient.connected()) {
if(mEnReconnect)
mClient.connect();
}
}
void tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) {
bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) {
if (!mClient.connected())
return false;
publish("sunrise", String(sunrise).c_str(), true);
publish("sunset", String(sunset).c_str(), true);
publish("comm_start", String(sunrise - offs).c_str(), true);
publish("comm_stop", String(sunset + offs).c_str(), true);
publish("dis_night_comm", ((disNightCom) ? "true" : "false"), true);
return true;
}
bool tickerComm(bool disabled) {
if (!mClient.connected())
return false;
publish("comm_disabled", ((disabled) ? "true" : "false"), true);
publish("comm_dis_ts", String(*mUtcTimestamp).c_str(), true);
if(disabled && (mCfgMqtt->rstValsCommStop))
zeroAllInverters();
return true;
}
void tickerMidnight() {
Inverter<> *iv;
record_t<> *rec;
char topic[7 + MQTT_TOPIC_LEN], val[4];
// set YieldDay to zero
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
rec = iv->getRecordStruct(RealTimeRunData_Debug);
uint8_t pos = iv->getPosByChFld(CH0, FLD_YD, rec);
iv->setValue(pos, rec, 0.0f);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch0/%s", iv->config->name, fields[FLD_YD]);
snprintf(val, 4, "0.0");
publish(topic, val, true);
}
}
void payloadEventListener(uint8_t cmd) {
if(mClient.connected()) // prevent overflow if MQTT broker is not reachable but set
mSendList.push(cmd);
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);
}
}
void alarmEventListener(uint16_t code, uint32_t start, uint32_t endTime) {
if(mClient.connected()) {
mAlarmList.push(alarm_t(code, start, endTime));
}
}
void publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) {
if(!mClient.connected())
return;
char topic[(MQTT_TOPIC_LEN << 1) + 2];
snprintf(topic, ((MQTT_TOPIC_LEN << 1) + 2), "%s/%s", mCfgMqtt->topic, subTopic);
String topic = "";
if(addTopic)
mClient.publish(topic, QOS_0, retained, payload);
else
mClient.publish(subTopic, QOS_0, retained, payload);
topic = String(mCfgMqtt->topic) + "/";
topic += String(subTopic);
do {
if(0 != mClient.publish(topic.c_str(), QOS_0, retained, payload))
break;
if(!mClient.connected())
break;
#if defined(ESP8266)
mClient.loop();
#endif
yield();
} while(1);
mTxCnt++;
}
@ -141,90 +210,91 @@ class PubMqtt {
void sendDiscoveryConfig(void) {
DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig"));
char stateTopic[64], discoveryTopic[64], buffer[512], name[32], uniq_id[32];
char topic[64], name[32], uniq_id[32];
DynamicJsonDocument doc(128);
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
DynamicJsonDocument deviceDoc(128);
deviceDoc[F("name")] = iv->config->name;
deviceDoc[F("ids")] = String(iv->config->serial.u64, HEX);
deviceDoc[F("cu")] = F("http://") + String(WiFi.localIP().toString());
deviceDoc[F("mf")] = F("Hoymiles");
deviceDoc[F("mdl")] = iv->config->name;
JsonObject deviceObj = deviceDoc.as<JsonObject>();
DynamicJsonDocument doc(384);
for (uint8_t i = 0; i < rec->length; i++) {
if (rec->assign[i].ch == CH0) {
if (NULL == iv)
continue;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
doc.clear();
doc[F("name")] = iv->config->name;
doc[F("ids")] = String(iv->config->serial.u64, HEX);
doc[F("cu")] = F("http://") + String(WiFi.localIP().toString());
doc[F("mf")] = F("Hoymiles");
doc[F("mdl")] = iv->config->name;
JsonObject deviceObj = doc.as<JsonObject>(); // deviceObj is only pointer!?
for (uint8_t i = 0; i < (rec->length + 4); i++) {
const char *devCls, *stateCls;
if(i < rec->length) {
if (rec->assign[i].ch == CH0)
snprintf(name, 32, "%s %s", iv->config->name, iv->getFieldName(i, rec));
} else {
else
snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
}
snprintf(stateTopic, 64, "/ch%d/%s", rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(discoveryTopic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(topic, 64, "/ch%d/%s", rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(uniq_id, 32, "ch%d_%s", rec->assign[i].ch, iv->getFieldName(i, rec));
const char *devCls = getFieldDeviceClass(rec->assign[i].fieldId);
const char *stateCls = getFieldStateClass(rec->assign[i].fieldId);
doc[F("name")] = name;
doc[F("stat_t")] = String(mCfgMqtt->topic) + "/" + String(iv->config->name) + String(stateTopic);
doc[F("unit_of_meas")] = iv->getUnit(i, rec);
doc[F("uniq_id")] = String(iv->config->serial.u64, HEX) + "_" + uniq_id;
doc[F("dev")] = deviceObj;
doc[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
if (devCls != NULL)
doc[F("dev_cla")] = devCls;
if (stateCls != NULL)
doc[F("stat_cla")] = stateCls;
serializeJson(doc, buffer);
publish(discoveryTopic, buffer, true, false);
doc.clear();
devCls = getFieldDeviceClass(rec->assign[i].fieldId);
stateCls = getFieldStateClass(rec->assign[i].fieldId);
}
else { // total values
snprintf(name, 32, "Total %s", fields[fldTotal[i-rec->length]]);
snprintf(topic, 64, "/%s", fields[fldTotal[i-rec->length]]);
snprintf(uniq_id, 32, "total_%s", fields[fldTotal[i-rec->length]]);
devCls = getFieldDeviceClass(fldTotal[i-rec->length]);
stateCls = getFieldStateClass(fldTotal[i-rec->length]);
}
yield();
DynamicJsonDocument doc2(512);
doc2[F("name")] = name;
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + String(iv->config->name) + String(topic);
doc2[F("unit_of_meas")] = iv->getUnit(((i < rec->length) ? i : (i - rec->length)), rec);
doc2[F("uniq_id")] = String(iv->config->serial.u64, HEX) + "_" + uniq_id;
doc2[F("dev")] = deviceObj;
doc2[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
if (devCls != NULL)
doc2[F("dev_cla")] = String(devCls);
if (stateCls != NULL)
doc2[F("stat_cla")] = String(stateCls);
if(i < rec->length)
snprintf(topic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
else // total values
snprintf(topic, 64, "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, fields[fldTotal[i-rec->length]]);
size_t size = measureJson(doc2) + 1;
char *buf = new char[size];
memset(buf, 0, size);
serializeJson(doc2, buf, size);
publish(topic, buf, true, false);
delete[] buf;
}
}
}
private:
#if defined(ESP8266)
void onWifiConnect(const WiFiEventStationModeGotIP& event) {
DPRINTLN(DBG_VERBOSE, F("MQTT connecting"));
mClient.connect();
mEnReconnect = true;
yield();
}
}
void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {
mEnReconnect = false;
}
void setPowerLimitAck(Inverter<> *iv) {
if (NULL != iv) {
char topic[7 + MQTT_TOPIC_LEN];
#else
void onWiFiEvent(WiFiEvent_t event) {
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
DPRINTLN(DBG_VERBOSE, F("MQTT connecting"));
mClient.connect();
mEnReconnect = true;
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
mEnReconnect = false;
break;
default:
break;
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ack_pwr_limit", iv->config->name);
publish(topic, "true", true);
}
}
#endif
private:
void onConnect(bool sessionPreset) {
DPRINTLN(DBG_INFO, F("MQTT connected"));
mEnReconnect = true;
publish("version", mVersion, true);
publish("device", mDevName, true);
publish("ip_addr", WiFi.localIP().toString().c_str(), true);
tickerMinute();
publish(mLwtTopic, mLwtOnline, true, false);
@ -238,6 +308,7 @@ class PubMqtt {
switch (reason) {
case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED:
DBGPRINTLN(F("TCP disconnect"));
mReconnectRequest = true;
break;
case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
DBGPRINTLN(F("wrong protocol version"));
@ -336,13 +407,12 @@ class PubMqtt {
bool processIvStatus() {
// returns true if all inverters are available
bool allAvail = true;
bool first = true;
bool allAvail = true; // shows if all enabled inverters are available
bool anyAvail = false; // shows if at least one enabled inverter is available
bool changed = false;
char topic[7 + MQTT_TOPIC_LEN], val[40];
Inverter<> *iv;
record_t<> *rec;
bool totalComplete = true;
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
iv = mSys->getInverterByPos(id);
@ -350,31 +420,23 @@ class PubMqtt {
continue; // skip to next inverter
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(first)
mIvAvail = false;
first = false;
// inverter status
uint8_t status = MQTT_STATUS_AVAIL_PROD;
if ((!iv->isAvailable(*mUtcTimestamp, rec)) || (!iv->config->enabled)) {
status = MQTT_STATUS_NOT_AVAIL_NOT_PROD;
if(iv->config->enabled) { // only change all-avail if inverter is enabled!
totalComplete = false;
uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD;
if (iv->config->enabled) {
if (iv->isAvailable(*mUtcTimestamp))
status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD;
else // inverter is enabled but not available
allAvail = false;
}
}
else if (!iv->isProducing(*mUtcTimestamp, rec)) {
mIvAvail = true;
if (MQTT_STATUS_AVAIL_PROD == status)
status = MQTT_STATUS_AVAIL_NOT_PROD;
}
else
mIvAvail = true;
if(mLastIvState[id] != status) {
mLastIvState[id] = status;
changed = true;
if((MQTT_STATUS_NOT_AVAIL_NOT_PROD == status) && (mCfgMqtt->rstValsNotAvail))
zeroValues(iv);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(val, 40, "%d", status);
publish(topic, val, true);
@ -386,14 +448,28 @@ class PubMqtt {
}
if(changed) {
snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((mIvAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
publish("status", val, true);
sendIvData(false); // false prevents loop of same function
}
return totalComplete;
return allAvail;
}
void sendAlarmData() {
if(mAlarmList.empty())
return;
Inverter<> *iv = mSys->getInverterByPos(0, false);
while(!mAlarmList.empty()) {
alarm_t alarm = mAlarmList.front();
publish("alarm", iv->getAlarmStr(alarm.code).c_str());
publish("alarm_start", String(alarm.start).c_str());
publish("alarm_end", String(alarm.end).c_str());
mAlarmList.pop();
}
}
void sendIvData(void) {
void sendIvData(bool sendTotals = true) {
if(mSendList.empty())
return;
@ -405,55 +481,58 @@ class PubMqtt {
memset(total, 0, sizeof(float) * 4);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv)
if ((NULL == iv) || (MQTT_STATUS_NOT_AVAIL_NOT_PROD == mLastIvState[id]))
continue; // skip to next inverter
record_t<> *rec = iv->getRecordStruct(mSendList.front());
// data
if(iv->isAvailable(*mUtcTimestamp, rec)) {
for (uint8_t i = 0; i < rec->length; i++) {
bool retained = false;
if (mSendList.front() == RealTimeRunData_Debug) {
//if(iv->isAvailable(*mUtcTimestamp, rec) || (0 != mCfgMqtt->interval)) { // is avail or fixed pulish interval was set
for (uint8_t i = 0; i < rec->length; i++) {
bool retained = false;
if (mSendList.front() == RealTimeRunData_Debug) {
switch (rec->assign[i].fieldId) {
case FLD_YT:
case FLD_YD:
retained = true;
break;
}
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(topic, val, retained);
// calculate total values for RealTimeRunData_Debug
if (mSendList.front() == RealTimeRunData_Debug) {
if (CH0 == rec->assign[i].ch) {
switch (rec->assign[i].fieldId) {
case FLD_PAC:
total[0] += iv->getValue(i, rec);
break;
case FLD_YT:
total[1] += iv->getValue(i, rec);
break;
case FLD_YD:
retained = true;
total[2] += iv->getValue(i, rec);
break;
case FLD_PDC:
total[3] += iv->getValue(i, rec);
break;
}
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(topic, val, retained);
// calculate total values for RealTimeRunData_Debug
if (mSendList.front() == RealTimeRunData_Debug) {
if (CH0 == rec->assign[i].ch) {
switch (rec->assign[i].fieldId) {
case FLD_PAC:
total[0] += iv->getValue(i, rec);
break;
case FLD_YT:
total[1] += iv->getValue(i, rec);
break;
case FLD_YD:
total[2] += iv->getValue(i, rec);
break;
case FLD_PDC:
total[3] += iv->getValue(i, rec);
break;
}
}
sendTotal = true;
}
yield();
sendTotal = true;
}
yield();
}
//}
}
mSendList.pop(); // remove from list once all inverters were processed
if(!sendTotals) // skip total value calculation
continue;
if ((true == sendTotal) && processIvStatus()) {
uint8_t fieldId;
for (uint8_t i = 0; i < 4; i++) {
@ -480,6 +559,48 @@ class PubMqtt {
}
}
void zeroAllInverters() {
Inverter<> *iv;
// set values to zero, exept yields
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
zeroValues(iv);
}
sendIvData();
}
void zeroValues(Inverter<> *iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t ch = 0; ch <= iv->channels; ch++) {
uint8_t pos = 0;
uint8_t fld = 0;
while(0xff != pos) {
switch(fld) {
case FLD_YD:
case FLD_YT:
case FLD_FW_VERSION:
case FLD_FW_BUILD_YEAR:
case FLD_FW_BUILD_MONTH_DAY:
case FLD_FW_BUILD_HOUR_MINUTE:
case FLD_HW_ID:
case FLD_ACT_ACTIVE_PWR_LIMIT:
fld++;
continue;
break;
}
pos = iv->getPosByChFld(ch, fld, rec);
iv->setValue(pos, rec, 0.0f);
fld++;
}
}
mSendList.push(RealTimeRunData_Debug);
}
espMqttClient mClient;
cfgMqtt_t *mCfgMqtt;
#if defined(ESP8266)
@ -490,10 +611,11 @@ class PubMqtt {
uint32_t *mUtcTimestamp;
uint32_t mRxCnt, mTxCnt;
std::queue<uint8_t> mSendList;
bool mEnReconnect;
std::queue<alarm_t> mAlarmList;
subscriptionCb mSubscriptionCb;
bool mIvAvail; // shows if at least one inverter is available
bool mReconnectRequest;
uint8_t mLastIvState[MAX_NUM_INVERTERS];
uint16_t mIntervalTimeout;
// last will topic and payload must be available trough lifetime of 'espMqttClient'
char mLwtTopic[MQTT_TOPIC_LEN+5];

4
src/publisher/pubSerial.h

@ -28,8 +28,8 @@ class PubSerial {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv->isAvailable(*mUtcTimestamp, rec)) {
DPRINTLN(DBG_INFO, F("Inverter: ") + String(id));
if (iv->isAvailable(*mUtcTimestamp)) {
DPRINTLN(DBG_INFO, "Iv: " + String(id));
for (uint8_t i = 0; i < rec->length; i++) {
if (0.0f != iv->getValue(i, rec)) {
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));

27
src/utils/ahoyTimer.h

@ -1,27 +0,0 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __AHOY_TIMER_H__
#define __AHOY_TIMER_H__
#include <Arduino.h>
namespace ah {
inline bool checkTicker(uint32_t *ticker, uint32_t interval) {
uint32_t mil = millis();
if(mil >= *ticker) {
*ticker = mil + interval;
return true;
}
else if(mil < (*ticker - interval)) {
*ticker = mil + interval;
return true;
}
return false;
}
}
#endif /*__AHOY_TIMER_H__*/

2
src/utils/dbg.h

@ -5,7 +5,7 @@
#ifndef __DBG_H__
#define __DBG_H__
#if defined(ESP32) && defined(F)
#if defined(F) && defined(ESP32)
#undef F
#define F(sl) (sl)
#endif

33
src/utils/handler.h

@ -1,33 +0,0 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de
// Lukas Pusch, lukas@lpusch.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __HANDLER_H__
#define __HANDLER_H__
#include <memory>
#include <functional>
#include <list>
template<class TYPE>
class Handler {
public:
Handler() {}
void addListener(TYPE f) {
mList.push_back(f);
}
/*virtual void notify(void) {
for(typename std::list<TYPE>::iterator it = mList.begin(); it != mList.end(); ++it) {
(*it)();
}
}*/
protected:
std::list<TYPE> mList;
};
#endif /*__HANDLER_H__*/

9
src/utils/helper.cpp

@ -40,6 +40,15 @@ namespace ah {
return String(str);
}
String getTimeStr(time_t t) {
char str[9];
if(0 == t)
sprintf(str, "n/a");
else
sprintf(str, "%02d:%02d:%02d", hour(t), minute(t), second(t));
return String(str);
}
uint64_t Serial2u64(const char *val) {
char tmp[3];
uint64_t ret = 0ULL;

1
src/utils/helper.h

@ -21,6 +21,7 @@ namespace ah {
void ip2Char(uint8_t ip[], char *str);
double round3(double value);
String getDateTimeStr(time_t t);
String getTimeStr(time_t t);
uint64_t Serial2u64(const char *val);
}

110
src/utils/llist.h

@ -1,110 +0,0 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de
// Lukas Pusch, lukas@lpusch.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __LIST_H__
#define __LIST_H__
template<class T, class... Args>
struct node_s {
typedef T dT;
node_s *pre;
node_s *nxt;
uint8_t id;
dT d;
node_s() : pre(NULL), nxt(NULL), d() {}
node_s(Args... args) : id(0), pre(NULL), nxt(NULL), d(args...) {}
};
template<int MAX_NUM, class T, class... Args>
class llist {
typedef node_s<T, Args...> elmType;
typedef T dataType;
public:
llist() : root(mPool) {
root = NULL;
elmType *p = mPool;
for(uint32_t i = 0; i < MAX_NUM; i++) {
p->id = i;
p++;
}
mFill = mMax = 0;
}
elmType *add(Args... args) {
elmType *p = root, *t;
if(NULL == (t = getFreeNode()))
return NULL;
if(++mFill > mMax)
mMax = mFill;
if(NULL == root) {
p = root = t;
p->pre = p;
p->nxt = p;
}
else {
p = root->pre;
t->pre = p;
p->nxt->pre = t;
t->nxt = p->nxt;
p->nxt = t;
}
t->d = dataType(args...);
return p;
}
elmType *getFront() {
return root;
}
elmType *get(elmType *p) {
p = p->nxt;
return (p == root) ? NULL : p;
}
elmType *rem(elmType *p) {
if(NULL == p)
return NULL;
elmType *t = p->nxt;
p->nxt->pre = p->pre;
p->pre->nxt = p->nxt;
if((root == p) && (p->nxt == p))
root = NULL;
else
root = p->nxt;
p->nxt = NULL;
p->pre = NULL;
p = NULL;
mFill--;
return (NULL == root) ? NULL : ((t == root) ? NULL : t);
}
uint16_t getFill(void) {
return mFill;
}
uint16_t getMaxFill(void) {
return mMax;
}
protected:
elmType *root;
private:
elmType *getFreeNode(void) {
elmType *n = mPool;
for(uint32_t i = 0; i < MAX_NUM; i++) {
if(NULL == n->nxt)
return n;
n++;
}
return NULL;
}
elmType mPool[MAX_NUM];
uint16_t mFill, mMax;
};
#endif /*__LIST_H__*/

42
src/utils/scheduler.h

@ -20,8 +20,9 @@ namespace ah {
uint32_t timeout;
uint32_t reload;
bool isTimestamp;
sP() : c(NULL), timeout(0), reload(0), isTimestamp(false) {}
sP(scdCb a, uint32_t tmt, uint32_t rl, bool its) : c(a), timeout(tmt), reload(rl), isTimestamp(its) {}
char name[6];
sP() : c(NULL), timeout(0), reload(0), isTimestamp(false), name("\n") {}
sP(scdCb a, uint32_t tmt, uint32_t rl, bool its) : c(a), timeout(tmt), reload(rl), isTimestamp(its), name("\n") {}
};
#define MAX_NUM_TICKER 30
@ -35,8 +36,7 @@ namespace ah {
mTimestamp = 0;
mMax = 0;
mPrevMillis = millis();
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++)
mTickerInUse[i] = false;
resetTicker();
}
void loop(void) {
@ -65,15 +65,15 @@ namespace ah {
}
void once(scdCb c, uint32_t timeout) { addTicker(c, timeout, 0, false); }
void onceAt(scdCb c, uint32_t timestamp) { addTicker(c, timestamp, 0, true); }
uint8_t every(scdCb c, uint32_t interval){ return addTicker(c, interval, interval, false); }
void once(scdCb c, uint32_t timeout, const char *name) { addTicker(c, timeout, 0, false, name); }
void onceAt(scdCb c, uint32_t timestamp, const char *name) { addTicker(c, timestamp, 0, true, name); }
uint8_t every(scdCb c, uint32_t interval, const char *name){ return addTicker(c, interval, interval, false, name); }
void everySec(scdCb c) { every(c, SCD_SEC); }
void everyMin(scdCb c) { every(c, SCD_MIN); }
void everyHour(scdCb c) { every(c, SCD_HOUR); }
void every12h(scdCb c) { every(c, SCD_12H); }
void everyDay(scdCb c) { every(c, SCD_DAY); }
void everySec(scdCb c, const char *name) { every(c, SCD_SEC, name); }
void everyMin(scdCb c, const char *name) { every(c, SCD_MIN, name); }
void everyHour(scdCb c, const char *name) { every(c, SCD_HOUR, name); }
void every12h(scdCb c, const char *name) { every(c, SCD_12H, name); }
void everyDay(scdCb c, const char *name) { every(c, SCD_DAY, name); }
virtual void setTimestamp(uint32_t ts) {
mTimestamp = ts;
@ -94,15 +94,28 @@ namespace ah {
return mTimestamp;
}
inline void resetTicker(void) {
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++)
mTickerInUse[i] = false;
}
void getStat(uint8_t *max) {
*max = mMax;
}
void printSchedulers() {
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) {
if (mTickerInUse[i]) {
DPRINTLN(DBG_INFO, String(mTicker[i].name) + ", tmt: " + String(mTicker[i].timeout) + ", rel: " + String(mTicker[i].reload));
}
}
}
protected:
uint32_t mTimestamp;
private:
inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp) {
inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) {
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) {
if (!mTickerInUse[i]) {
mTickerInUse[i] = true;
@ -110,6 +123,8 @@ namespace ah {
mTicker[i].timeout = timeout;
mTicker[i].reload = reload;
mTicker[i].isTimestamp = isTimestamp;
memset(mTicker[i].name, 0, 6);
strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5);
if(mMax == i)
mMax = i + 1;
return i;
@ -129,6 +144,7 @@ namespace ah {
mTickerInUse[i] = false;
else
mTicker[i].timeout = mTicker[i].reload;
//DPRINTLN(DBG_INFO, "checkTick " + String(i) + " reload: " + String(mTicker[i].reload) + ", timeout: " + String(mTicker[i].timeout));
(mTicker[i].c)();
yield();
}

104
src/web/RestApi.h

@ -14,12 +14,18 @@
#include "../appInterface.h"
#if defined(F) && defined(ESP32)
#undef F
#define F(sl) (sl)
#endif
template<class HMSYSTEM>
class RestApi {
public:
RestApi() {
mTimezoneOffset = 0;
mFreeHeap = 0;
nr = 0;
}
void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) {
@ -134,17 +140,34 @@ class RestApi {
ep[F("record/config")] = url + F("record/config");
ep[F("record/live")] = url + F("record/live");
}
void onDwnldSetup(AsyncWebServerRequest *request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
JsonObject root = response->getRoot();
AsyncWebServerResponse *response;
getSetup(root);
File fp = LittleFS.open("/settings.json", "r");
if(!fp) {
DPRINTLN(DBG_ERROR, F("failed to load settings"));
response = request->beginResponse(200, F("application/json"), "{}");
}
else {
String tmp = fp.readString();
int i = 0;
// remove all passwords
while (i != -1) {
i = tmp.indexOf("\"pwd\":", i);
if(-1 != i) {
i+=7;
tmp.remove(i, tmp.indexOf("\"", i)-i);
}
}
response = request->beginResponse(200, F("application/json"), tmp);
}
response->setLength();
response->addHeader("Content-Type", "application/octet-stream");
response->addHeader("Content-Description", "File Transfer");
response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json");
request->send(response);
fp.close();
}
void getGeneric(JsonObject obj) {
@ -165,7 +188,7 @@ class RestApi {
obj[F("device_name")] = mConfig->sys.deviceName;
obj[F("mac")] = WiFi.macAddress();
obj[F("hostname")] = WiFi.getHostname();
obj[F("hostname")] = mConfig->sys.deviceName;
obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0);
obj[F("prot_mask")] = mConfig->sys.protectionMask;
@ -247,6 +270,7 @@ class RestApi {
obj[F("rx_fail_answer")] = stat->rxFailNoAnser;
obj[F("frame_cnt")] = stat->frmCnt;
obj[F("tx_cnt")] = mSys->Radio.mSendCnt;
obj[F("retransmits")] = mSys->Radio.mRetransmits;
}
void getInverterList(JsonObject obj) {
@ -263,6 +287,7 @@ class RestApi {
obj2[F("serial")] = String(iv->config->serial.u64, HEX);
obj2[F("channels")] = iv->channels;
obj2[F("version")] = String(iv->getFwVersion());
obj2[F("yieldCor")] = iv->config->yieldCor;
for(uint8_t j = 0; j < iv->channels; j ++) {
obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j];
@ -276,11 +301,15 @@ class RestApi {
}
void getMqtt(JsonObject obj) {
obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port);
obj[F("user")] = String(mConfig->mqtt.user);
obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String("");
obj[F("topic")] = String(mConfig->mqtt.topic);
obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port);
obj[F("user")] = String(mConfig->mqtt.user);
obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String("");
obj[F("topic")] = String(mConfig->mqtt.topic);
obj[F("interval")] = String(mConfig->mqtt.interval);
obj[F("rstMid")] = (bool)mConfig->mqtt.rstYieldMidNight;
obj[F("rstNAvail")] = (bool)mConfig->mqtt.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mConfig->mqtt.rstValsCommStop;
}
void getNtp(JsonObject obj) {
@ -325,6 +354,17 @@ class RestApi {
ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf);
}
void getDisplay(JsonObject obj) {
obj[F("disp_type")] = (uint8_t)mConfig->plugin.display.type;
obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline;
obj[F("logo_en")] = (bool)mConfig->plugin.display.logoEn;
obj[F("px_shift")] = (bool)mConfig->plugin.display.pxShift;
obj[F("rot180")] = (bool)mConfig->plugin.display.rot180;
obj[F("contrast")] = (uint8_t)mConfig->plugin.display.contrast;
obj[F("pinDisp0")] = mConfig->plugin.display.pin0;
obj[F("pinDisp1")] = mConfig->plugin.display.pin1;
}
void getMenu(JsonObject obj) {
uint8_t i = 0;
uint16_t mask = (mApp->getProtection()) ? mConfig->sys.protectionMask : 0;
@ -357,10 +397,15 @@ class RestApi {
obj[F("name")][i] = "Documentation";
obj[F("link")][i] = "https://ahoydtu.de";
obj[F("trgt")][i++] = "_blank";
if((strlen(mConfig->sys.adminPwd) > 0) && !mApp->getProtection()) {
if(strlen(mConfig->sys.adminPwd) > 0) {
obj[F("name")][i++] = "-";
obj[F("name")][i] = "Logout";
obj[F("link")][i++] = "/logout";
if(mApp->getProtection()) {
obj[F("name")][i] = "Login";
obj[F("link")][i++] = "/login";
} else {
obj[F("name")][i] = "Logout";
obj[F("link")][i++] = "/logout";
}
}
}
@ -385,8 +430,8 @@ 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(), rec);
invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp(), rec);
invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp());
invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp());
invObj[F("ts_last_success")] = iv->getLastTs(rec);
}
}
@ -411,6 +456,8 @@ class RestApi {
JsonArray info = obj.createNestedArray(F("infos"));
if(mApp->getMqttIsConnected())
info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received"));
if(mConfig->mqtt.interval > 0)
info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds"));
}
void getSetup(JsonObject obj) {
@ -425,6 +472,7 @@ class RestApi {
getRadio(obj.createNestedObject(F("radio")));
getSerial(obj.createNestedObject(F("serial")));
getStaticIp(obj.createNestedObject(F("static_ip")));
getDisplay(obj.createNestedObject(F("display")));
}
void getNetworks(JsonObject obj) {
@ -510,18 +558,16 @@ class RestApi {
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]);
bool accepted = true;
if(NULL == iv) {
jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>();
return false;
}
if(F("power") == jsonIn[F("cmd")]) {
iv->devControlCmd = (jsonIn[F("val")] == 1) ? TurnOn : TurnOff;
iv->devControlRequest = true;
} else if(F("restart") == jsonIn[F("restart")]) {
iv->devControlCmd = Restart;
iv->devControlRequest = true;
}
if(F("power") == jsonIn[F("cmd")])
accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff);
else if(F("restart") == jsonIn[F("restart")])
accepted = iv->setDevControlRequest(Restart);
else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) {
iv->powerLimit[0] = jsonIn["val"];
if(F("limit_persistent_relative") == jsonIn[F("cmd")])
@ -532,8 +578,8 @@ class RestApi {
iv->powerLimit[1] = RelativNonPersistent;
else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")])
iv->powerLimit[1] = AbsolutNonPersistent;
iv->devControlCmd = ActivePowerContr;
iv->devControlRequest = true;
accepted = iv->setDevControlRequest(ActivePowerContr);
}
else if(F("dev") == jsonIn[F("cmd")]) {
DPRINTLN(DBG_INFO, F("dev cmd"));
@ -544,13 +590,18 @@ class RestApi {
return false;
}
if(!accepted) {
jsonOut[F("error")] = F("inverter does not accept dev control request at this moment");
return false;
} else
mApp->ivSendHighPrio(iv);
return true;
}
bool setSetup(JsonObject jsonIn, JsonObject jsonOut) {
if(F("scan_wifi") == jsonIn[F("cmd")]) {
if(F("scan_wifi") == jsonIn[F("cmd")])
mApp->scanAvailNetworks();
}
else if(F("set_time") == jsonIn[F("cmd")])
mApp->setTimestamp(jsonIn[F("val")]);
else if(F("sync_ntp") == jsonIn[F("cmd")])
@ -575,6 +626,7 @@ class RestApi {
uint32_t mTimezoneOffset;
uint32_t mFreeHeap;
uint16_t nr;
};
#endif /*__WEB_API_H__*/

2
src/web/html/index.html

@ -51,7 +51,7 @@
<li>Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li>
<li>Contribute to <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">documentation</a></li>
<li><a href="https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip" target="_blank">Download</a> & Test development firmware, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">Changelog</a></li>
<li><a href="https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip" target="_blank">Download</a> & Test development firmware, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">Development Changelog</a></li>
<li>make a <a href="https://paypal.me/lupusch" target="_blank">donation</a></li>
</ul>
<p class="lic">

209
src/web/html/setup.html

@ -30,8 +30,6 @@
</div>
<div id="wrapper">
<div id="content">
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a>
<form method="post" action="/save">
<fieldset>
<legend class="des">Device Host Name</legend>
@ -94,7 +92,7 @@
<input type="button" id="btnAdd" class="btn" value="Add Inverter"/>
<p class="subdes">General</p>
<label for="invInterval">Interval [s]</label>
<input type="text" class="text" name="invInterval"/>
<input type="text" class="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/>
<label for="invRetry">Max retries per Payload</label>
<input type="text" class="text" name="invRetry"/>
</fieldset>
@ -147,7 +145,16 @@
<label for="mqttPwd">Password (optional)</label>
<input type="password" class="text" name="mqttPwd"/>
<label for="mqttTopic">Topic</label>
<input type="text" class="text" name="mqttTopic" pattern="[A-Za-z0-9.\-_\+\/]+" title="Invalid input" />
<input type="text" class="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" />
<label for="mqttRstMid">Reset YieldDay at Midnight</label>
<input type="checkbox" class="cb" name="mqttRstMid"/><br/>
<label for="mqttRstComStop">Reset Values at Communication stop</label>
<input type="checkbox" class="cb" name="mqttRstComStop"/><br/>
<label for="mqttRstNotAvail">Reset Values on 'not available'</label>
<input type="checkbox" class="cb" name="mqttRstNotAvail"/><br/>
<p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p>
<label for="mqttIntvl">Interval [s]</label>
<input type="text" class="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" />
<label for="mqttBtn">Discovery Config (homeassistant)</label>
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/>
<span id="apiResultMqtt"></span>
@ -158,7 +165,7 @@
<div class="s_content">
<fieldset>
<legend class="des">System Config</legend>
<p class="des">Pinout (Wemos)</p>
<p class="des">Pinout</p>
<div id="pinout"></div>
<p class="des">Radio (NRF24L01+)</p>
@ -170,7 +177,28 @@
<label for="serDbg">Serial Debug</label>
<input type="checkbox" class="cb" name="serDbg"/><br/>
<label for="serIntvl">Interval [s]</label>
<input type="text" class="text" name="serIntvl"/>
<input type="text" class="text" name="serIntvl" pattern="[0-9]+" title="Invalid input"/>
</fieldset>
</div>
<button type="button" class="s_collapsible">Display Config</button>
<div class="s_content">
<fieldset>
<legend class="des">Display Config</legend>
<div id="dispType"></div>
<label for="logoEn">Show Logo</label>
<input type="checkbox" class="cb" name="logoEn"/><br/>
<label for="dispPwr">Turn off while inverters are offline</label>
<input type="checkbox" class="cb" name="dispPwr"/><br/>
<label for="dispPxSh">Enable pixel shifting</label>
<input type="checkbox" class="cb" name="dispPxSh"/><br/>
<label for="disp180">Rotate 180 degree</label>
<input type="checkbox" class="cb" name="disp180"/><br/>
<label for="dispCont">Contrast</label>
<select name="dispCont" id="contrast"></select>
<p class="des">Pinout</p>
<div id="dispPins"></div>
</fieldset>
</div>
@ -181,7 +209,15 @@
</div>
<div class="hr mb-3 mt-3"></div>
<div class="mb-4">
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a> (only saved values)
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a>
<fieldset>
<legend class="des">Upload / Store JSON Settings</legend>
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="upload">
<input type="button" class="btn" value="Upload" onclick="hide()">
</form>
</fieldset>
<a class="btn" href="/get_setup" target="_blank">Download settings (JSON file)</a> (only saved values, passwords will be removed!)
</div>
</form>
</div>
@ -206,11 +242,62 @@
var highestId = 0;
var maxInv = 0;
var esp8266pins = [
[255, "off / default"],
[0, "D3 (GPIO0)"],
[1, "TX (GPIO1)"],
[2, "D4 (GPIO2)"],
[3, "RX (GPIO3)"],
[4, "D2 (GPIO4, SDA)"],
[5, "D1 (GPIO5, SCL)"],
[6, "GPIO6"],
[7, "GPIO7"],
[8, "GPIO8"],
[9, "GPIO9"],
[10, "GPIO10"],
[11, "GPIO11"],
[12, "D6 (GPIO12)"],
[13, "D7 (GPIO13)"],
[14, "D5 (GPIO14)"],
[15, "D8 (GPIO15)"],
[16, "D0 (GPIO16 - no IRQ!)"]
];
var esp32pins = [
[255, "off / default"],
[0, "GPIO0"],
[1, "TX (GPIO1)"],
[2, "GPIO2 (LED)"],
[3, "RX (GPIO3)"],
[4, "GPIO4"],
[5, "GPIO5"],
[12, "GPIO12"],
[13, "GPIO13"],
[14, "GPIO14"],
[15, "GPIO15"],
[16, "GPIO16"],
[17, "GPIO17"],
[18, "GPIO18"],
[19, "GPIO19"],
[21, "GPIO21 (SDA)"],
[22, "GPIO22 (SCL)"],
[23, "GPIO23"],
[25, "GPIO25"],
[26, "GPIO26"],
[27, "GPIO27"],
[32, "GPIO32"],
[33, "GPIO33"],
[34, "GPIO34"],
[35, "GPIO35"],
[36, "VP (GPIO36)"],
[39, "VN (GPIO39)"]
];
const re = /11[2,4,6]1.*/;
document.getElementById("btnAdd").addEventListener("click", function() {
if(highestId <= (maxInv-1))
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId + 1);
if(highestId <= (maxInv-1)) {
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId);
}
});
function apiCbWifi(obj) {
@ -265,6 +352,12 @@
getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj));
}
function hide() {
document.getElementById("form").submit();
var e = document.getElementById("content");
e.replaceChildren(span("upload started"));
}
function delIv() {
var id = this.id.substring(0,4);
var e = document.getElementsByName(id + "Addr")[0];
@ -275,8 +368,8 @@
}
function ivHtml(obj, id) {
highestId = id;
if(highestId == (maxInv - 1))
highestId = id + 1;
if(highestId == maxInv)
setHide("btnAdd", true);
iv = document.getElementById("inverter");
iv.appendChild(des("Inverter " + id));
@ -289,7 +382,7 @@
iv.appendChild(br());
iv.appendChild(lbl(id + "Addr", "Serial Number (12 digits)*"));
var addr = inp(id + "Addr", obj["serial"], 12);
var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input");
iv.appendChild(addr);
['keyup', 'change'].forEach(function(evt) {
addr.addEventListener(evt, (e) => {
@ -320,7 +413,7 @@
iv.append(
lbl(id + "Name", "Name*"),
inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9.\\-_\\+\\/]+", "Invalid input")
inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")
);
for(var j of [["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"], ["ModName", "ch_name", "Module Name", 16, null]]) {
@ -336,10 +429,15 @@
iv.appendChild(d);
}
iv.append(
br(),
lbl(id + "YieldCor", "Yield Total Correction (will be subtracted) [kWh]"),
inp(id + "YieldCor", obj["yieldCor"], 32, ["text"], null, "text", "[0-9]+", "Invalid input")
);
var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button");
del.addEventListener("click", delIv);
iv.append(
br(),
lbl(id + "lbldel", "Delete"),
del
);
@ -389,8 +487,11 @@
}
function parseMqtt(obj) {
for(var i of [["Addr", "broker"], ["Port", "port"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"]])
for(var i of [["Addr", "broker"], ["Port", "port"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]])
document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]];
for(var i of [["Mid", "rstMid"], ["ComStop", "rstNAvail"], ["NotAvail", "rstComStop"]])
document.getElementsByName("mqttRst"+i[0])[0].checked = obj[i[1]];
}
function parseNtp(obj) {
@ -413,59 +514,7 @@
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']];
for(p of pins) {
e.appendChild(lbl(p[1], p[0].toUpperCase()));
if("ESP8266" == type) {
e.appendChild(sel(p[1], [
[255, "off / default"],
[0, "D3 (GPIO0)"],
[1, "TX (GPIO1)"],
[2, "D4 (GPIO2)"],
[3, "RX (GPIO3)"],
[4, "D2 (GPIO4)"],
[5, "D1 (GPIO5)"],
[6, "GPIO6"],
[7, "GPIO7"],
[8, "GPIO8"],
[9, "GPIO9"],
[10, "GPIO10"],
[11, "GPIO11"],
[12, "D6 (GPIO12)"],
[13, "D7 (GPIO13)"],
[14, "D5 (GPIO14)"],
[15, "D8 (GPIO15)"],
[16, "D0 (GPIO16 - no IRQ!)"]
], obj[p[0]]));
}
else {
e.appendChild(sel(p[1], [
[255, "off / default"],
[0, "GPIO0"],
[1, "TX (GPIO1)"],
[2, "GPIO2 (LED)"],
[3, "RX (GPIO3)"],
[4, "GPIO4"],
[5, "GPIO5"],
[12, "GPIO12"],
[13, "GPIO13"],
[14, "GPIO14"],
[15, "GPIO15"],
[16, "GPIO16"],
[17, "GPIO17"],
[18, "GPIO18"],
[19, "GPIO19"],
[21, "GPIO21"],
[22, "GPIO22"],
[23, "GPIO23"],
[25, "GPIO25"],
[26, "GPIO26"],
[27, "GPIO27"],
[32, "GPIO32"],
[33, "GPIO33"],
[34, "GPIO34"],
[35, "GPIO35"],
[36, "VP (GPIO36)"],
[39, "VN (GPIO39)"]
], obj[p[0]]));
}
e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]]));
}
}
@ -486,6 +535,29 @@
document.getElementsByName("serIntvl")[0].value = obj["interval"];
}
function parseDisplay(obj, type) {
for(var i of [["logoEn", "logo_en"], ["dispPwr", "disp_pwr"], ["dispPxSh", "px_shift"], ["disp180", "rot180"]])
document.getElementsByName(i[0])[0].checked = obj[i[1]];
var e = document.getElementById("dispPins");
pins = [['SCL / CS', 'pinDisp0'], ['SDA / DC', 'pinDisp1']];
for(p of pins) {
e.appendChild(lbl(p[1], p[0].toUpperCase()));
e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]]));
}
var opts = [[0, "None"], [1, "Nokia5110"], [2, "SSD1306 0.96\""], [3, "SH1106 1.3\""]];
document.getElementById("dispType").append(
lbl("dispType", "Type"),
sel("dispType", opts, obj["disp_type"])
);
e = document.getElementById("contrast");
for(var i = 30; i < 101; i += 2) {
e.appendChild(opt(i, i, (i == obj["contrast"])));
}
}
function parse(root) {
if(null != root) {
parseMenu(root["menu"]);
@ -499,6 +571,7 @@
parsePinout(root["pinout"], root["system"]["esp_type"]);
parseRadio(root["radio"]);
parseSerial(root["serial"]);
parseDisplay(root["display"], root["system"]["esp_type"]);
}
}

5
src/web/html/system.html

@ -94,11 +94,12 @@
}
main.append(
genTabRow("TX count", stat["tx_cnt"]),
genTabRow("RX success", stat["rx_success"]),
genTabRow("RX fail", stat["rx_fail"]),
genTabRow("RX no answer", stat["rx_fail_answer"]),
genTabRow("RX frames received", stat["frame_cnt"]),
genTabRow("TX count", stat["tx_cnt"])
genTabRow("RX fragments", stat["frame_cnt"]),
genTabRow("TX retransmits", stat["retransmits"])
);
}

1
src/web/html/update.html

@ -23,7 +23,6 @@
<input type="file" name="update">
<input type="button" class="btn" value="Update" onclick="hide()">
</form>
</div>
</div>
<div id="footer">

256
src/web/web.h

@ -18,7 +18,6 @@
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/ahoyTimer.h"
#include "../utils/helper.h"
#include "html/h/index_html.h"
@ -71,7 +70,7 @@ class Web {
mWeb.on("/save", HTTP_ANY, std::bind(&Web::showSave, this, std::placeholders::_1));
mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1));
//mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1));
#ifdef ENABLE_JSON_EP
mWeb.on("/json", HTTP_ANY, std::bind(&Web::showJson, this, std::placeholders::_1));
@ -82,8 +81,11 @@ class Web {
mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1));
mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1),
std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1),
std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1));
mWeb.on("/debug", HTTP_GET, std::bind(&Web::onDebug, this, std::placeholders::_1));
mEvts.onConnect(std::bind(&Web::onConnect, this, std::placeholders::_1));
@ -92,6 +94,8 @@ class Web {
mWeb.begin();
registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h
mUploadFail = false;
}
void tickSecond() {
@ -127,6 +131,8 @@ class Web {
}
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
mApp->setOnUpdate();
if(!index) {
Serial.printf("Update Start: %s\n", filename.c_str());
#ifndef ESP32
@ -150,6 +156,34 @@ class Web {
}
}
void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if(!index) {
mUploadFail = false;
mUploadFp = LittleFS.open("/tmp.json", "w");
if(!mUploadFp) {
DPRINTLN(DBG_ERROR, F("can't open file!"));
mUploadFail = true;
mUploadFp.close();
}
}
mUploadFp.write(data, len);
if(final) {
mUploadFp.close();
File fp = LittleFS.open("/tmp.json", "r");
if(!fp)
mUploadFail = true;
else {
if(!mApp->readSettings("tmp.json")) {
mUploadFail = true;
DPRINTLN(DBG_ERROR, F("upload JSON error!"));
}
else
mApp->saveSettings();
}
DPRINTLN(DBG_INFO, F("upload finished!"));
}
}
void serialCb(String msg) {
if(!mSerialClientConnnected)
return;
@ -157,12 +191,15 @@ class Web {
msg.replace("\r\n", "<rn>");
if(mSerialAddTime) {
if((9 + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9);
mSerialBufFill += 9;
if(mApp->getTimestamp() > 0) {
strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9);
mSerialBufFill += 9;
}
}
else {
mSerialBufFill = 0;
mEvts.send("webSerial, buffer overflow!", "serial", millis());
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
return;
}
mSerialAddTime = false;
}
@ -177,7 +214,7 @@ class Web {
}
else {
mSerialBufFill = 0;
mEvts.send("webSerial, buffer overflow!", "serial", millis());
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
}
}
@ -210,7 +247,24 @@ class Web {
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html);
response->addHeader("Connection", "close");
request->send(response);
//if(reboot)
mApp->setRebootFlag();
}
void onUpload(AsyncWebServerRequest *request) {
bool reboot = !mUploadFail;
String html = F("<!doctype html><html><head><title>Upload</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Upload: ");
if(reboot)
html += "success";
else
html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html);
response->addHeader("Connection", "close");
request->send(response);
//if(reboot)
mApp->setRebootFlag();
}
@ -429,6 +483,7 @@ class Web {
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
default: break;
}
iv->config->yieldCor = request->arg("inv" + String(i) + "YieldCor").toInt();
// name
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
@ -494,6 +549,10 @@ class Web {
request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN);
request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
mConfig->mqtt.port = request->arg("mqttPort").toInt();
mConfig->mqtt.interval = request->arg("mqttInterval").toInt();
mConfig->mqtt.rstYieldMidNight = (request->arg("mqttRstMid") == "on");
mConfig->mqtt.rstValsNotAvail = (request->arg("mqttRstNotAvail") == "on");
mConfig->mqtt.rstValsCommStop = (request->arg("mqttRstComStop") == "on");
// serial console
if(request->arg("serIntvl") != "") {
@ -504,6 +563,18 @@ class Web {
// Needed to log TX buffers to serial console
mSys->Radio.mSerialDebug = mConfig->serial.debug;
}
// display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("dispPwr") == "on");
mConfig->plugin.display.logoEn = (request->arg("logoEn") == "on");
mConfig->plugin.display.pxShift = (request->arg("dispPxSh") == "on");
mConfig->plugin.display.rot180 = (request->arg("disp180") == "on");
mConfig->plugin.display.type = request->arg("dispType").toInt();
mConfig->plugin.display.contrast = request->arg("dispCont").toInt();
mConfig->plugin.display.pin0 = request->arg("pinDisp0").toInt();
mConfig->plugin.display.pin1 = request->arg("pinDisp1").toInt();
mApp->saveSettings();
if(request->arg("reboot") == "on")
@ -530,7 +601,7 @@ class Web {
request->send(response);
}
void showWebApi(AsyncWebServerRequest *request) {
/*void showWebApi(AsyncWebServerRequest *request) {
// TODO: remove
DPRINTLN(DBG_VERBOSE, F("web::showWebApi"));
DPRINTLN(DBG_DEBUG, request->arg("plain"));
@ -593,6 +664,12 @@ class Web {
}
}
request->send(200, "text/json", "{success:true}");
}*/
void onDebug(AsyncWebServerRequest *request) {
mApp->getSchedulerNames();
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), "ok");
request->send(response);
}
void onSerial(AsyncWebServerRequest *request) {
@ -626,74 +703,148 @@ class Web {
}
#ifdef ENABLE_JSON_EP
void showJson(void) {
void showJson(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showJson"));
String modJson;
Inverter<> *iv;
record_t<> *rec;
char topic[40], val[25];
modJson = F("{\n");
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
char topic[40], val[25];
snprintf(topic, 30, "\"%s\": {\n", iv->name);
modJson += String(topic);
for(uint8_t i = 0; i < iv->listLen; i++) {
snprintf(topic, 40, "\t\"ch%d/%s\"", iv->assign[i].ch, iv->getFieldName(i));
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i), iv->getUnit(i));
modJson += String(topic) + ": " + String(val) + F(",\n");
}
modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(iv->ts) + F("\"\n\t},\n");
iv = mSys->getInverterByPos(id);
if(NULL == iv)
continue;
rec = iv->getRecordStruct(RealTimeRunData_Debug);
snprintf(topic, 30, "\"%s\": {\n", iv->config->name);
modJson += String(topic);
for(uint8_t i = 0; i < rec->length; i++) {
snprintf(topic, 40, "\t\"ch%d/%s\"", rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i, rec), iv->getUnit(i, rec));
modJson += String(topic) + ": " + String(val) + F(",\n");
}
modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(rec->ts) + F("\"\n\t},\n");
}
modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mMain->mTimestamp)) + F("\"\n}\n");
modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mApp->getTimestamp())) + F("\"\n}\n");
mWeb.send(200, F("application/json"), modJson);
AsyncWebServerResponse *response = request->beginResponse(200, F("application/json"), modJson);
request->send(response);
}
#endif
#ifdef ENABLE_PROMETHEUS_EP
void showMetrics(void) {
enum {
metricsStateStart, metricsStateInverter, metricStateChannel,metricsStateEnd
} metricsStep;
int metricsInverterId,metricsChannelId;
void showMetrics(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
String metrics;
char headline[80];
snprintf(headline, 80, "ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1", mApp->getVersion(), mconfig->sys.deviceName);
metrics += "# TYPE ahoy_solar_info gauge\n" + String(headline) + "\n";
metricsStep = metricsStateStart;
AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"),
[this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t
{
Inverter<> *iv;
record_t<> *rec;
statistics_t *stat;
String metrics;
char type[60], topic[100], val[25];
size_t len = 0;
switch (metricsStep) {
case metricsStateStart: // System Info & NRF Statistics : fit to one packet
snprintf(topic,sizeof(topic),"# TYPE ahoy_solar_info gauge\nahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",
mApp->getVersion(), mConfig->sys.deviceName);
metrics = topic;
// NRF Statistics
stat = mApp->getStatistics();
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);
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Start Inverter loop
metricsInverterId = 0;
metricsStep = metricsStateInverter;
break;
case metricsStateInverter: // Inverter loop
if (metricsInverterId < mSys->getNumInverters()) {
iv = mSys->getInverterByPos(metricsInverterId);
if(NULL != iv) {
// Inverter info
len = snprintf((char *)buffer, maxLen, "ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\",enabled=\"%d\"} 1\n",
iv->config->name, iv->config->serial.u64,iv->config->enabled);
// Start Channel loop for this inverter
metricsChannelId = 0;
metricsStep = metricStateChannel;
}
} else {
metricsStep = metricsStateEnd;
}
break;
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
char type[60], topic[60], val[25];
for(uint8_t i = 0; i < iv->listLen; i++) {
uint8_t channel = iv->assign[i].ch;
if(channel == 0) {
case metricStateChannel: // Channel loop
iv = mSys->getInverterByPos(metricsInverterId);
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (metricsChannelId < rec->length) {
uint8_t channel = rec->assign[metricsChannelId].ch;
String promUnit, promType;
std::tie(promUnit, promType) = convertToPromUnits( iv->getUnit(i) );
snprintf(type, 60, "# TYPE ahoy_solar_%s_%s %s", iv->getFieldName(i), promUnit.c_str(), promType.c_str());
snprintf(topic, 60, "ahoy_solar_%s_%s{inverter=\"%s\"}", iv->getFieldName(i), promUnit.c_str(), iv->name);
snprintf(val, 25, "%.3f", iv->getValue(i));
metrics += String(type) + "\n" + String(topic) + " " + String(val) + "\n";
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec));
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str());
if (0 == channel) {
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name);
} else {
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]);
}
snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec));
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val);
metricsChannelId++;
} else {
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
// All channels processed --> try next inverter
metricsInverterId++;
metricsStep = metricsStateInverter;
}
}
break;
case metricsStateEnd:
default: // end of transmission
len = 0;
break;
}
}
return len;
});
request->send(response);
}
mWeb.send(200, F("text/plain"), metrics);
String radioStatistic(String statistic, uint32_t value) {
char type[60], topic[80], val[25];
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s gauge",statistic.c_str());
snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str());
snprintf(val, sizeof(val), "%d", value);
return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n");
}
std::pair<String, String> convertToPromUnits(String shortUnit) {
if(shortUnit == "A") return {"ampere", "gauge"};
if(shortUnit == "V") return {"volt", "gauge"};
if(shortUnit == "%") return {"ratio", "gauge"};
if(shortUnit == "W") return {"watt", "gauge"};
if(shortUnit == "Wh") return {"watt_daily", "counter"};
if(shortUnit == "kWh") return {"watt_total", "counter"};
if(shortUnit == "°C") return {"celsius", "gauge"};
if(shortUnit == "A") return {"_ampere", "gauge"};
if(shortUnit == "V") return {"_volt", "gauge"};
if(shortUnit == "%") return {"_ratio", "gauge"};
if(shortUnit == "W") return {"_watt", "gauge"};
if(shortUnit == "Wh") return {"_wattHours", "counter"};
if(shortUnit == "kWh") return {"_kilowattHours", "counter"};
if(shortUnit == "°C") return {"_celsius", "gauge"};
if(shortUnit == "var") return {"_var", "gauge"};
if(shortUnit == "Hz") return {"_hertz", "gauge"};
return {"", "gauge"};
}
#endif
AsyncWebServer mWeb;
AsyncEventSource mEvts;
bool mProtected;
@ -707,6 +858,9 @@ class Web {
char mSerialBuf[WEB_SERIAL_BUF_SIZE];
uint16_t mSerialBufFill;
bool mSerialClientConnnected;
File mUploadFp;
bool mUploadFail;
};
#endif /*__WEB_H__*/

125
src/wifi/ahoywifi.cpp

@ -12,15 +12,15 @@
// NTP CONFIG
#define NTP_PACKET_SIZE 48
//-----------------------------------------------------------------------------
ahoywifi::ahoywifi() : mApIp(192, 168, 4, 1) {}
//-----------------------------------------------------------------------------
void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp) {
void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) {
mConfig = config;
mUtcTimestamp = utcTimestamp;
mAppWifiCb = cb;
mStaConn = DISCONNECTED;
mCnt = 0;
@ -64,20 +64,43 @@ void ahoywifi::tickWifiLoop() {
#if !defined(AP_ONLY)
if(mStaConn != GOT_IP) {
if (WiFi.softAPgetStationNum() > 0) { // do not reconnect if any AP connection exists
mDns.processNextRequest();
if((WIFI_AP_STA == WiFi.getMode()) && !mScanActive) {
if(mStaConn != IN_AP_MODE) {
mStaConn = IN_AP_MODE;
// first time switch to AP Mode
if (mScanActive) {
WiFi.scanDelete();
mScanActive = false;
}
DBGPRINTLN(F("AP client connected"));
welcome(mApIp.toString());
WiFi.mode(WIFI_AP);
mDns.start(53, "*", mApIp);
mAppWifiCb(true);
}
mDns.processNextRequest();
return;
}
else if(WIFI_AP == WiFi.getMode()) {
else if(mStaConn == IN_AP_MODE) {
mCnt = 0;
mDns.stop();
WiFi.mode(WIFI_AP_STA);
mStaConn = DISCONNECTED;
}
mCnt++;
if(!mScanActive && mBSSIDList.empty()) { // start scanning APs with the given SSID
DBGPRINT(F("scanning APs with SSID "));
DBGPRINTLN(String(mConfig->sys.stationSsid));
mScanCnt = 0;
mScanActive = true;
#if defined(ESP8266)
WiFi.scanNetworks(true, false, 0U, (uint8_t *)mConfig->sys.stationSsid);
#else
WiFi.scanNetworks(true, false, false, 300U, 0U, mConfig->sys.stationSsid);
#endif
return;
}
uint8_t timeout = 10; // seconds
if (mStaConn == CONNECTED) // connected but no ip
@ -86,10 +109,27 @@ void ahoywifi::tickWifiLoop() {
DBGPRINT(F("reconnect in "));
DBGPRINT(String(timeout-mCnt));
DBGPRINTLN(F(" seconds"));
if(mScanActive) {
getBSSIDs();
if(!mScanActive) // scan completed
if ((mCnt % timeout) < timeout - 2)
mCnt = timeout - 2;
}
if((mCnt % timeout) == 0) { // try to reconnect after x sec without connection
if(mStaConn != CONNECTED)
mStaConn = CONNECTING;
WiFi.reconnect();
if(mBSSIDList.size() > 0) { // get first BSSID in list
DBGPRINT("try to connect to AP with BSSID:");
uint8_t bssid[6];
for (int j = 0; j < 6; j++) {
bssid[j] = mBSSIDList.front();
mBSSIDList.pop_front();
DBGPRINT(" " + String(bssid[j], HEX));
}
DBGPRINTLN("");
WiFi.disconnect();
WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]);
}
mCnt = 0;
}
}
@ -117,8 +157,6 @@ 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);
mDns.start(53, "*", mApIp);
}
@ -134,7 +172,7 @@ void ahoywifi::setupStation(void) {
if(!WiFi.config(ip, gateway, mask, dns1, dns2))
DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
}
mStaConn = (WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd) != WL_CONNECTED) ? DISCONNECTED : CONNECTED;
mBSSIDList.clear();
if(String(mConfig->sys.deviceName) != "")
WiFi.hostname(mConfig->sys.deviceName);
WiFi.mode(WIFI_AP_STA);
@ -205,40 +243,72 @@ void ahoywifi::sendNTPpacket(IPAddress& address) {
mUdp.endPacket();
}
//-----------------------------------------------------------------------------
void ahoywifi::sortRSSI(int *sort, int n) {
for (int i = 0; i < n; i++)
sort[i] = i;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i]))
std::swap(sort[i], sort[j]);
}
//-----------------------------------------------------------------------------
void ahoywifi::scanAvailNetworks(void) {
if(-2 == WiFi.scanComplete()) {
if(!mScanActive) {
mScanActive = true;
if(WIFI_AP == WiFi.getMode())
WiFi.mode(WIFI_AP_STA);
WiFi.mode(WIFI_AP_STA);
WiFi.scanNetworks(true);
}
}
//-----------------------------------------------------------------------------
void ahoywifi::getAvailNetworks(JsonObject obj) {
JsonArray nets = obj.createNestedArray("networks");
int n = WiFi.scanComplete();
if (n < 0)
return;
if(n > 0) {
int sort[n];
for (int i = 0; i < n; i++)
sort[i] = i;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i]))
std::swap(sort[i], sort[j]);
sortRSSI(&sort[0], n);
for (int i = 0; i < n; ++i) {
nets[i]["ssid"] = WiFi.SSID(sort[i]);
nets[i]["rssi"] = WiFi.RSSI(sort[i]);
nets[i]["ssid"] = WiFi.SSID(sort[i]);
nets[i]["rssi"] = WiFi.RSSI(sort[i]);
}
mScanActive = false;
WiFi.scanDelete();
}
mScanActive = false;
WiFi.scanDelete();
if(mStaConn == IN_AP_MODE)
WiFi.mode(WIFI_AP);
}
//-----------------------------------------------------------------------------
void ahoywifi::getBSSIDs() {
int n = WiFi.scanComplete();
if (n < 0) {
mScanCnt++;
if (mScanCnt < 20)
return;
}
if(n > 0) {
mBSSIDList.clear();
int sort[n];
sortRSSI(&sort[0], n);
for (int i = 0; i < n; i++) {
DBGPRINT("BSSID " + String(i) + ":");
uint8_t *bssid = WiFi.BSSID(sort[i]);
for (int j = 0; j < 6; j++){
DBGPRINT(" " + String(bssid[j], HEX));
mBSSIDList.push_back(bssid[j]);
}
DBGPRINTLN("");
}
}
mScanActive = false;
WiFi.scanDelete();
}
//-----------------------------------------------------------------------------
void ahoywifi::connectionEvent(WiFiStatus_t status) {
@ -247,15 +317,19 @@ void ahoywifi::connectionEvent(WiFiStatus_t status) {
if(mStaConn != CONNECTED) {
mStaConn = CONNECTED;
DBGPRINTLN(F("\n[WiFi] Connected"));
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
mDns.stop();
}
break;
case GOT_IP:
mStaConn = GOT_IP;
if (mScanActive) { // maybe another scan has started
WiFi.scanDelete();
mScanActive = false;
}
welcome(WiFi.localIP().toString() + F(" (Station)"));
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
mAppWifiCb(true);
break;
case DISCONNECTED:
@ -263,6 +337,7 @@ void ahoywifi::connectionEvent(WiFiStatus_t status) {
mStaConn = DISCONNECTED;
mCnt = 5; // try to reconnect in 5 sec
setupWifi(); // reconnect with AP / Station setup
mAppWifiCb(false);
DPRINTLN(DBG_INFO, "[WiFi] Connection Lost");
}
break;

15
src/wifi/ahoywifi.h

@ -18,20 +18,23 @@ class app;
class ahoywifi {
public:
typedef std::function<void(bool)> appWifiCb;
ahoywifi();
void setup(settings_t *config, uint32_t *utcTimestamp);
void setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb);
void tickWifiLoop(void);
bool getNtpTime(void);
void scanAvailNetworks(void);
void getAvailNetworks(JsonObject obj);
private:
typedef enum WiFiStatus
{
typedef enum WiFiStatus {
DISCONNECTED = 0,
CONNECTING,
CONNECTED,
IN_AP_MODE,
GOT_IP
} WiFiStatus_t;
@ -39,6 +42,8 @@ class ahoywifi {
void setupAp(void);
void setupStation(void);
void sendNTPpacket(IPAddress& address);
void sortRSSI(int *sort, int n);
void getBSSIDs(void);
void connectionEvent(WiFiStatus_t status);
#if defined(ESP8266)
void onConnect(const WiFiEventStationModeConnected& event);
@ -51,6 +56,7 @@ class ahoywifi {
settings_t *mConfig;
appWifiCb mAppWifiCb;
DNSServer mDns;
IPAddress mApIp;
@ -63,8 +69,9 @@ class ahoywifi {
uint8_t mCnt;
uint32_t *mUtcTimestamp;
uint8_t mLoopCnt;
uint8_t mScanCnt;
bool mScanActive;
std::list<uint8_t> mBSSIDList;
};
#endif /*__AHOYWIFI_H__*/

Loading…
Cancel
Save