diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 8d8ab45e..ec435880 100644 --- a/.github/workflows/compile_development.yml +++ b/.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 diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml index 20fcef87..91ae35bf 100644 --- a/.github/workflows/compile_release.yml +++ b/.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 diff --git a/Getting_Started.md b/Getting_Started.md index fd4de83e..af8789c3 100644 --- a/Getting_Started.md +++ b/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). You are welcome to add more examples of faked chips. We will add that information here. +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. @@ -119,6 +136,18 @@ This is an example wiring using a Wemos D1 mini. ![Symbolic](doc/AhoyWemos_Steckplatine.jpg) +#### ESP8266 wiring example on 30pin Lolin NodeMCU v3 + +This is an example wiring using a NodeMCU V3. + +##### 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 diff --git a/User_Manual.md b/User_Manual.md index 529125a0..be7519ec 100644 --- a/User_Manual.md +++ b/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 | |---|---| diff --git a/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png b/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png new file mode 100644 index 00000000..2b37a3b0 Binary files /dev/null and b/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png differ diff --git a/doc/ESP8266_nRF24L01+_Schaltplan.jpg b/doc/ESP8266_nRF24L01+_Schaltplan.jpg new file mode 100644 index 00000000..749e3fa3 Binary files /dev/null and b/doc/ESP8266_nRF24L01+_Schaltplan.jpg differ diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md new file mode 100644 index 00000000..bce0525f --- /dev/null +++ b/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 | | + diff --git a/scripts/buildManifest.py b/scripts/buildManifest.py index 9a5411d5..db29b352 100644 --- a/scripts/buildManifest.py +++ b/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) diff --git a/scripts/getVersion.py b/scripts/getVersion.py index f7c825ce..4ecb714e 100644 --- a/scripts/getVersion.py +++ b/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/" diff --git a/src/CHANGES.md b/src/CHANGES.md index f8a2c5ca..9e31ada2 100644 --- a/src/CHANGES.md +++ b/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 /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 diff --git a/src/app.cpp b/src/app.cpp index 29df1e64..f5478f0f 100644 --- a/src/app.cpp +++ b/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 #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(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 diff --git a/src/app.h b/src/app.h index 54dca314..d3709b89 100644 --- a/src/app.h +++ b/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 PubMqttType; typedef PubSerial PubSerialType; // PLUGINS -#if defined(ENA_NOKIA) || defined(ENA_SSD1306) - #include "plugins/MonochromeDisplay/MonochromeDisplay.h" - typedef MonochromeDisplay MonoDisplayType; -#endif +#include "plugins/MonochromeDisplay/MonochromeDisplay.h" +typedef MonochromeDisplay 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 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__*/ diff --git a/src/appInterface.h b/src/appInterface.h index 94f75399..83a4f769 100644 --- a/src/appInterface.h +++ b/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; diff --git a/src/config/config.h b/src/config/config.h index 2b5a0688..69e7193b 100644 --- a/src/config/config.h +++ b/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 diff --git a/src/config/settings.h b/src/config/settings.h index 47c59a8c..4a922962 100644 --- a/src/config/settings.h +++ b/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()); snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as()); snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as()); @@ -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()); 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()); diff --git a/src/defines.h b/src/defines.h index ddfda35e..d5f0fcb3 100644 --- a/src/defines.h +++ b/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; diff --git a/src/hm/CircularBuffer.h b/src/hm/CircularBuffer.h deleted file mode 100644 index 65c9e768..00000000 --- a/src/hm/CircularBuffer.h +++ /dev/null @@ -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 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 diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 21af98f0..fa7e08b2 100644 --- a/src/hm/hmDefines.h +++ b/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 //------------------------------------- diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 8c1fccc8..640aeae3 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -105,32 +105,33 @@ const calcFunc_t calcFunctions[] = { template 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 recordMeas; // structure for measured values - record_t recordInfo; // structure for info values - record_t recordConfig; // structure for system config values - record_t 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 recordMeas; // structure for measured values + record_t recordInfo; // structure for info values + record_t recordConfig; // structure for system config values + record_t 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 void enqueCommand(uint8_t cmd) { _commandQueue.push(std::make_shared(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(InverterDevInform_All); - enqueCommand(RealTimeRunData_Debug); - if (actPowerLimit == 0xffff) - enqueCommand(SystemConfigPara); + enqueCommand(InverterDevInform_All); // firmware version + enqueCommand(RealTimeRunData_Debug); // live data + if ((actPowerLimit == 0xffff) && isConnected) + enqueCommand(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(AlarmUpdate); // What is the function of AlarmUpdate? + + DPRINTLN(DBG_INFO, "alarm ID incremented to " + String(alarmMesIndex)); enqueCommand(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> _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> _commandQueue; + bool mDevControlRequest; // true if change needed }; diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 13508a47..ea440b8c 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -9,28 +9,10 @@ #include "../utils/dbg.h" #include #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 +template 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 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(&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__*/ diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index 85976cbd..58099d40 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -8,19 +8,11 @@ #include "hmInverter.h" #include "hmRadio.h" -#include "CircularBuffer.h" -typedef CircularBuffer BufferType; -typedef HmRadio RadioType; - -template > +template > 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) { diff --git a/src/hm/payload.h b/src/hm/payload.h index ef69c4fe..72166390 100644 --- a/src/hm/payload.h +++ b/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 @@ -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 payloadListenerType; +typedef std::function alarmListenerType; template -class Payload : public Handler { +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::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(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 { 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(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 { 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 { 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 { } 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_*/ diff --git a/src/main.cpp b/src/main.cpp index c585d0f2..42ed55f4 100644 --- a/src/main.cpp +++ b/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); } diff --git a/src/platformio.ini b/src/platformio.ini index ba6c8be0..60245f69 100644 --- a/src/platformio.ini +++ b/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 diff --git a/src/plugins/MonochromeDisplay/MonochromeDisplay.h b/src/plugins/MonochromeDisplay/MonochromeDisplay.h index 83236992..5cfd85f0 100644 --- a/src/plugins/MonochromeDisplay/MonochromeDisplay.h +++ b/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 - #define DISP_PROGMEM U8X8_PROGMEM -#else // ENA_SSD1306 - /* esp8266 : SCL = 5, SDA = 4 */ - /* ewsp32 : SCL = 22, SDA = 21 */ - #include - #include - #define DISP_PROGMEM PROGMEM -#endif - +#include #include #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 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__*/ diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index bd7a709f..938499ea 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -15,7 +15,6 @@ #endif #include "../utils/dbg.h" -#include "../utils/ahoyTimer.h" #include "../config/config.h" #include #include @@ -26,36 +25,36 @@ typedef std::function 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 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(); - 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(); // 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 mSendList; - bool mEnReconnect; + std::queue 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]; diff --git a/src/publisher/pubSerial.h b/src/publisher/pubSerial.h index d1cc55aa..522a227d 100644 --- a/src/publisher/pubSerial.h +++ b/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)); diff --git a/src/utils/ahoyTimer.h b/src/utils/ahoyTimer.h deleted file mode 100644 index 5c960a34..00000000 --- a/src/utils/ahoyTimer.h +++ /dev/null @@ -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 - -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__*/ diff --git a/src/utils/dbg.h b/src/utils/dbg.h index 17b334c9..4f765ca6 100644 --- a/src/utils/dbg.h +++ b/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 diff --git a/src/utils/handler.h b/src/utils/handler.h deleted file mode 100644 index 51d64c0d..00000000 --- a/src/utils/handler.h +++ /dev/null @@ -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 -#include -#include - -template -class Handler { - public: - Handler() {} - - void addListener(TYPE f) { - mList.push_back(f); - } - - /*virtual void notify(void) { - for(typename std::list::iterator it = mList.begin(); it != mList.end(); ++it) { - (*it)(); - } - }*/ - - protected: - std::list mList; -}; - -#endif /*__HANDLER_H__*/ diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp index 6854a7e8..7d54e2f4 100644 --- a/src/utils/helper.cpp +++ b/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; diff --git a/src/utils/helper.h b/src/utils/helper.h index 7e908624..179e6078 100644 --- a/src/utils/helper.h +++ b/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); } diff --git a/src/utils/llist.h b/src/utils/llist.h deleted file mode 100644 index 69750f19..00000000 --- a/src/utils/llist.h +++ /dev/null @@ -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 -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 -class llist { - typedef node_s 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__*/ diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h index 36dcdaae..280f4a66 100644 --- a/src/utils/scheduler.h +++ b/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(); } diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 8ad55e26..43d53500 100644 --- a/src/web/RestApi.h +++ b/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 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(); 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(), 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__*/ diff --git a/src/web/html/index.html b/src/web/html/index.html index 0cd7f430..6634197e 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -51,7 +51,7 @@ Discuss with us on Discord Report issues Contribute to documentation - Download & Test development firmware, Changelog + Download & Test development firmware, Development Changelog make a donation diff --git a/src/web/html/setup.html b/src/web/html/setup.html index cf57f12b..fe514449 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -30,8 +30,6 @@ - ERASE SETTINGS (not WiFi) - Device Host Name @@ -94,7 +92,7 @@ General Interval [s] - + Max retries per Payload @@ -147,7 +145,16 @@ Password (optional) Topic - + + Reset YieldDay at Midnight + + Reset Values at Communication stop + + Reset Values on 'not available' + + 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) + Interval [s] + Discovery Config (homeassistant) @@ -158,7 +165,7 @@ System Config - Pinout (Wemos) + Pinout Radio (NRF24L01+) @@ -170,7 +177,28 @@ Serial Debug Interval [s] - + + + + + Display Config + + + Display Config + + Show Logo + + Turn off while inverters are offline + + Enable pixel shifting + + Rotate 180 degree + + + Contrast + + Pinout + @@ -181,7 +209,15 @@ - Download your settings (JSON file) (only saved values) + ERASE SETTINGS (not WiFi) + + Upload / Store JSON Settings + + + + + + Download settings (JSON file) (only saved values, passwords will be removed!) @@ -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"]); } } diff --git a/src/web/html/system.html b/src/web/html/system.html index 5419d7ba..75e05765 100644 --- a/src/web/html/system.html +++ b/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"]) ); } diff --git a/src/web/html/update.html b/src/web/html/update.html index 215188db..e9bcde87 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -23,7 +23,6 @@ -
diff --git a/src/web/html/setup.html b/src/web/html/setup.html index cf57f12b..fe514449 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -30,8 +30,6 @@
General
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)
Pinout (Wemos)
Pinout
Radio (NRF24L01+)