diff --git a/Getting_Started.md b/Getting_Started.md index b595da77..ff592f8b 100644 --- a/Getting_Started.md +++ b/Getting_Started.md @@ -53,6 +53,10 @@ Hoymiles Inverters - HM1000? - HM1200 - HM1500 +- MI-300* [For MI inverters see remarks here](User_Manual.md#mi-inverters) +- MI-600* +- MI-700* +- MI-1500* (2nd gen. still untested) TSUN Inverters: diff --git a/README.md b/README.md index 43b31dfe..b7e2aa9e 100644 --- a/README.md +++ b/README.md @@ -48,4 +48,4 @@ Please try to describe your issues as precise as possible and think about if thi - [OpenDTU](https://github.com/tbnobody/OpenDTU) <- Our sister project ✨ for Hoymiles HM-300, HM-600, HM-1200 (for ESP32 only!) - [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles) - <- Go here ✨ for Hoymiles MI-300, MI-600, MI-1200 Software + <- Go here ✨ for Hoymiles MI-300, MI-600, MI-1200 Software (single inverter only) diff --git a/User_Manual.md b/User_Manual.md index 0a0caaed..242a2809 100644 --- a/User_Manual.md +++ b/User_Manual.md @@ -91,9 +91,6 @@ The AhoyDTU will publish on the following topics ## Active Power Limit via Serial / Control Page URL: `/serial` -If you leave the field "Active Power Limit" empty during the setup and reboot the ahoy-dtu will set a value of 65535 in the setup. -That is the value you have to fill in case you want to operate the inverter without a active power limit. -If the value is 65535 or -1 after another reboot the value will be set automatically to "100" and in the drop-down menu "relative in percent persistent" will be set. Of course you can do this also by your self. You can change the setting in the following manner. Decide if you want to set @@ -115,24 +112,17 @@ Also an absolute active power limit below approx. 30 Watt seems to be not meanfu ### Generic Information -The AhoyDTU subscribes on three topics `/ctrl/#`, `/setup` and `/status`. +The AhoyDTU subscribes on following topics: + +- `/ctrl/limit/` +- `/ctrl/restart/` +- `/setup/set_time` 👆 `` can be set on setup page, default is `inverter`. 👆 `` is the number of the specific inverter in the setup page. -### Inverter Power (On / Off) -```mqtt -/ctrl/power/ -``` -with payload `1` = `ON` and `0` = `OFF` - -Example: -```mqtt -inverter/ctrl/power/0 1 -``` - ### Inverter restart ```mqtt /ctrl/restart/ @@ -142,50 +132,35 @@ Example: inverter/ctrl/restart/0 ``` -### Power Limit relative persistent [%] +### Power Limit relative (non persistent) [%] ```mqtt -/ctrl/limit_persistent_relative/ +/ctrl/limit/ ``` with a payload `[2 .. 100]` +**NOTE: optional a `%` can be sent as last character** + Example: ```mqtt -inverter/ctrl/limit_persistent_relative/0 70 +inverter/ctrl/limit/0 70 ``` -### Power Limit absolute persistent [Watts] +### Power Limit absolute (non persistent) [Watts] ```mqtt -/ctrl/limit_persistent_absolute/ +/ctrl/limit/ ``` with a payload `[0 .. 65535]` -Example: -```mqtt -inverter/ctrl/limit_persistent_absolute/0 600 -``` - -### Power Limit relative non persistent [%] -```mqtt -/ctrl/limit_nonpersistent_relative/ -``` -with a payload `[2 .. 100]` +**NOTE: the unit `W` is necessary to determine an absolute limit** Example: ```mqtt -inverter/ctrl/limit_nonpersistent_relative/0 70 -``` - -### Power Limit absolute non persistent [Watts] -```mqtt -/ctrl/limit_nonpersistent_absolute/ +inverter/ctrl/limit/0 600W ``` -with a payload `[0 .. 65535]` -Example: -```mqtt -inverter/ctrl/limit_nonpersistent_absolute/0 600 -``` +### Power Limit persistent +This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter. ## Control via REST API @@ -310,6 +285,8 @@ To get the information open the URL `/api/record/info` on your AhoyDTU. The info | tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | | | rejoe2 | MI-600 | | 236 | 2018 | 11-27 | 17 | | | | rejoe2 | MI-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | | +| dragricola | HM-1200 | | 1.0.16 | 2021 | 10-12 | 100 | | | +| dragricola | MI-300 | | 230 | 2017 | 08-08 | 1 | | | | | | | | | | | | | ## Developer Information about Command Queue @@ -344,3 +321,11 @@ Send Power Limit: - A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy. - You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time. - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. + +## Additional Notes +### MI Inverters +- AhoyDTU supports MI type inverters as well, since dev. version 0.5.70. +- MI inverters are known to be delivered with two different generations of firmwares: inverters with serial numbers 10x2 already use the 3rd generation protocol and behave just like the newer HM models, *the follwoing remarks do not apply to these*. +- Older MI inverters (#sn 10x1) use a different rf protocol and thus do not deliver exactly the same data. E.g. the AC power value will therefore be calculated by AhoyDTU itself, while other values might not be available at all. +- Single and dual channel 2nd gen. devices seem not to accept power limiting commands at all, the lower limit for 4-channel MI is 10% (instead of 2% for newer models) +- 4-channel MI type inverters might work, but code still is untested. diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json index 85de73f4..c0becfd1 100644 --- a/src/.vscode/settings.json +++ b/src/.vscode/settings.json @@ -85,4 +85,5 @@ "stop_token": "cpp", "thread": "cpp" }, + "cmake.configureOnOpen": false, } \ No newline at end of file diff --git a/src/CHANGES.md b/src/CHANGES.md index 97583cec..0a245e80 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,54 @@ (starting from release version `0.5.66`) +## 0.5.104 +* further improved save settings +* removed `#` character from ePaper +* fixed saving pinout for `Nokia-Display` +* removed `Reset` Pin for monochrome displays +* improved wifi connection #652 + +## 0.5.103 +* merged MI improvements, thx @rejoe2 #778 +* changed display inverter online message +* merged heap improvements #772 + +## 0.5.102 +* Warning: old exports are not compatible any more! +* fix JSON import #775 +* fix save settings, at least already stored settings are not lost #771 +* further save settings improvements (only store inverters which are existing) +* improved display of settings save return value +* made save settings asynchronous (more heap memory is free) + +## 0.5.101 +* fix SSD1306 +* update documentation +* Update miPayload.h +* Update README.md +* MI - remarks to user manual +* MI - fix AC calc +* MI - fix status msg. analysis + +## 0.5.100 +* fix add inverter `setup.html` #766 +* fix MQTT retained flag for total values #726 +* renamed buttons for import and export `setup.html` +* added serial message `settings saved` + +## 0.5.99 +* fix limit in [User_Manual.md](../User_Manual.md) +* changed `contrast` to `luminance` in `setup.html` +* try to fix SSD1306 display #759 +* only show necessary display pins depending on setting + +## 0.5.98 +* fix SH1106 rotation and turn off during night #756 +* removed MQTT subscription `sync_ntp`, `set_time` with a value of `0` does the same #696 +* simplified MQTT subscription for `limit`. Check [User_Manual.md](../User_Manual.md) for new syntax #696, #713 +* repaired inverter wise limit control +* fix upload settings #686 + ## 0.5.97 * Attention: re-ordered display types, check your settings! #746 * improved saving settings of display #747, #746 diff --git a/src/LICENSE b/src/LICENSE deleted file mode 100644 index 057d1565..00000000 --- a/src/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -License - -CC-CY-NC-SA 3.0 - -https://creativecommons.org/licenses/by-nc-sa/3.0/de - -This project is for non-commercial use only! diff --git a/src/app.cpp b/src/app.cpp index cf8dfd87..60f4a64a 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #include "app.h" @@ -96,7 +96,7 @@ void app::setup() { // Plugins if (mConfig->plugin.display.type != 0) - mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion); + mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion); mPubSerial.setup(mConfig, &mSys, &mTimestamp); @@ -436,6 +436,8 @@ void app::resetSystem(void) { mSendLastIvId = 0; mShowRebootRequest = false; mIVCommunicationOn = true; + mSavePending = false; + mSaveReboot = false; memset(&mStat, 0, sizeof(statistics_t)); } diff --git a/src/app.h b/src/app.h index 23528e64..95c63225 100644 --- a/src/app.h +++ b/src/app.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __APP_H__ @@ -78,9 +78,12 @@ class app : public IApp, public ah::Scheduler { return Scheduler::getTimestamp(); } - bool saveSettings() { - mShowRebootRequest = true; - return mSettings.saveSettings(); + bool saveSettings(bool reboot) { + mShowRebootRequest = true; // only message on index, no reboot + mSavePending = true; + mSaveReboot = reboot; + once(std::bind(&app::tickSave, this), 3, "save"); + return true; } bool readSettings(const char *path) { @@ -91,6 +94,14 @@ class app : public IApp, public ah::Scheduler { return mSettings.eraseSettings(eraseWifi); } + bool getSavePending() { + return mSavePending; + } + + bool getLastSaveSucceed() { + return mSettings.getLastSaveSucceed(); + } + statistics_t *getStatistics() { return &mStat; } @@ -235,9 +246,19 @@ class app : public IApp, public ah::Scheduler { onWifi(false); ah::Scheduler::resetTicker(); WiFi.disconnect(); + delay(200); ESP.restart(); } + void tickSave(void) { + if(!mSettings.saveSettings()) + mSaveReboot = false; + mSavePending = false; + + if(mSaveReboot) + setRebootFlag(); + } + void tickNtpUpdate(void); void tickCalcSunrise(void); void tickIVCommunication(void); @@ -282,6 +303,8 @@ class app : public IApp, public ah::Scheduler { char mVersion[12]; settings mSettings; settings_t *mConfig; + bool mSavePending; + bool mSaveReboot; uint8_t mSendLastIvId; bool mSendFirst; diff --git a/src/appInterface.h b/src/appInterface.h index 6b969c5a..5e607e9c 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __IAPP_H__ @@ -14,9 +14,11 @@ class IApp { public: virtual ~IApp() {} - virtual bool saveSettings() = 0; + virtual bool saveSettings(bool stopFs) = 0; virtual bool readSettings(const char *path) = 0; virtual bool eraseSettings(bool eraseWifi) = 0; + virtual bool getSavePending() = 0; + virtual bool getLastSaveSucceed() = 0; virtual void setOnUpdate() = 0; virtual void setRebootFlag() = 0; virtual const char *getVersion() = 0; diff --git a/src/config/settings.h b/src/config/settings.h index 2382e0f0..52a24a27 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __SETTINGS_H__ @@ -14,6 +14,12 @@ #include "../utils/dbg.h" #include "../utils/helper.h" +#if defined(ESP32) + #define MAX_ALLOWED_BUF_SIZE ESP.getMaxAllocHeap() - 1024 +#else + #define MAX_ALLOWED_BUF_SIZE ESP.getMaxFreeBlockSize() - 1024 +#endif + /** * More info: * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout @@ -164,7 +170,9 @@ typedef struct { class settings { public: - settings() {} + settings() { + mLastSaveSucceed = false; + } void setup() { DPRINTLN(DBG_INFO, F("Initializing FS ..")); @@ -211,6 +219,10 @@ class settings { return mCfg.valid; } + inline bool getLastSaveSucceed() { + return mLastSaveSucceed; + } + void getInfo(uint32_t *used, uint32_t *size) { #if !defined(ESP32) FSInfo info; @@ -233,8 +245,9 @@ class settings { else { //DPRINTLN(DBG_INFO, fp.readString()); //fp.seek(0, SeekSet); - DynamicJsonDocument root(5500); + DynamicJsonDocument root(MAX_ALLOWED_BUF_SIZE); DeserializationError err = deserializeJson(root, fp); + root.shrinkToFit(); if(!err && (root.size() > 0)) { mCfg.valid = true; jsonWifi(root[F("wifi")]); @@ -259,15 +272,10 @@ class settings { return mCfg.valid; } - bool saveSettings(void) { + bool saveSettings() { DPRINTLN(DBG_DEBUG, F("save settings")); - File fp = LittleFS.open("/settings.json", "w"); - if(!fp) { - DPRINTLN(DBG_ERROR, F("can't open settings file!")); - return false; - } - DynamicJsonDocument json(6500); + DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); JsonObject root = json.to(); jsonWifi(root.createNestedObject(F("wifi")), true); jsonNrf(root.createNestedObject(F("nrf")), true); @@ -282,12 +290,35 @@ class settings { jsonPlugin(root.createNestedObject(F("plugin")), true); jsonInst(root.createNestedObject(F("inst")), true); + DPRINT(DBG_INFO, F("memory usage: ")); + DBGPRINTLN(String(json.memoryUsage())); + DPRINT(DBG_INFO, F("capacity: ")); + DBGPRINTLN(String(json.capacity())); + DPRINT(DBG_INFO, F("max alloc: ")); + DBGPRINTLN(String(MAX_ALLOWED_BUF_SIZE)); + + if(json.overflowed()) { + DPRINTLN(DBG_ERROR, F("buffer too small!")); + mLastSaveSucceed = false; + return false; + } + + File fp = LittleFS.open("/settings.json", "w"); + if(!fp) { + DPRINTLN(DBG_ERROR, F("can't open settings file!")); + mLastSaveSucceed = false; + return false; + } + if(0 == serializeJson(root, fp)) { DPRINTLN(DBG_ERROR, F("can't write settings file!")); + mLastSaveSucceed = false; return false; } fp.close(); + DPRINTLN(DBG_INFO, F("settings saved")); + mLastSaveSucceed = true; return true; } @@ -426,6 +457,11 @@ class settings { mCfg.nrf.pinIrq = obj[F("irq")]; mCfg.nrf.amplifierPower = obj[F("pwr")]; mCfg.nrf.enabled = (bool) obj[F("en")]; + if((obj[F("cs")] == obj[F("ce")])) { + mCfg.nrf.pinCs = DEF_CS_PIN; + mCfg.nrf.pinCe = DEF_CE_PIN; + mCfg.nrf.pinIrq = DEF_IRQ_PIN; + } } } @@ -560,18 +596,22 @@ class settings { if(set) ivArr = obj.createNestedArray(F("iv")); for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - if(set) - jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); - else - jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); + if(set) { + if(mCfg.inst.iv[i].serial.u64 != 0ULL) + jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); + } + else { + if(!obj[F("iv")][i].isNull()) + jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); + } } } 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; for(uint8_t i = 0; i < 4; i++) { obj[F("yield")][i] = cfg->yieldCor[i]; obj[F("pwr")][i] = cfg->chMaxPwr[i]; @@ -590,6 +630,7 @@ class settings { } settings_t mCfg; + bool mLastSaveSucceed; }; #endif /*__SETTINGS_H__*/ diff --git a/src/defines.h b/src/defines.h index 13e525bc..f29f6ea5 100644 --- a/src/defines.h +++ b/src/defines.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __DEFINES_H__ @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 5 -#define VERSION_PATCH 97 +#define VERSION_PATCH 104 //------------------------------------- typedef struct { diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 17421b8a..0063e252 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -223,7 +223,10 @@ class HmRadio { 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 + else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command + isLastPackage = (p.packet[9] > 0x11); // > 0x11 indicates last packet received + else if (p.packet[0] != 0x00 && p.packet[0] != 0x88 && p.packet[0] != 0x92) + // ignore fragment number zero and MI status messages isLastPackage = true; // response from dev control command yield(); } diff --git a/src/hm/miPayload.h b/src/hm/miPayload.h index e311fdbd..530b8051 100644 --- a/src/hm/miPayload.h +++ b/src/hm/miPayload.h @@ -44,7 +44,6 @@ class MiPayload { void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { mApp = app; mSys = sys; - mRadio = radio; mStat = stat; mMaxRetrans = maxRetransmits; mTimestamp = timestamp; @@ -69,50 +68,90 @@ class MiPayload { } void loop() { - /*if(NULL != mHighPrioIv) { - iv->ivSend(mHighPrioIv, true); // should request firmware version etc.? + if(NULL != mHighPrioIv) { + ivSend(mHighPrioIv, true); // for devcontrol commands? mHighPrioIv = NULL; - }*/ + } } void ivSendHighPrio(Inverter<> *iv) { mHighPrioIv = iv; } - void ivSend(Inverter<> *iv) { + void ivSend(Inverter<> *iv, bool highPrio = false) { + if(!highPrio) { + if (mPayload[iv->id].requested) { + if (!mPayload[iv->id].complete) + process(false); // no retransmit + + if (!mPayload[iv->id].complete) { + if (!mPayload[iv->id].gotFragment) + mStat->rxFailNoAnser++; // got nothing + else + mStat->rxFail++; // got fragments but not complete response + + iv->setQueuedCmdFinished(); // command failed + if (mSerialDebug) + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("enqueued cmd failed/timeout")); + if (mSerialDebug) { + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINT(F("no Payload received! (retransmits: ")); + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); + } + } + } + } + reset(iv->id); mPayload[iv->id].requested = true; yield(); - if (mSerialDebug) - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Requesting Inv SN ") + String(iv->config->serial.u64, HEX)); - - uint8_t cmd = iv->getQueuedCmd(); - DPRINT(DBG_INFO, F("(#")); - DBGPRINT(String(iv->id)); - DBGPRINT(F(") prepareDevInformCmd 0x")); - DBGPRINTLN(String(cmd, HEX)); - uint8_t cmd2 = cmd; - if (cmd == 0x1 ) { //0x1 - cmd = 0x0f; - cmd2 = 0x00; - mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false); - } else { - mRadio->prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); - }; - - mRadio->prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); - mPayload[iv->id].txCmd = cmd; - if (iv->type == INV_TYPE_1CH || iv->type == INV_TYPE_2CH) { - mPayload[iv->id].dataAB[CH1] = false; - mPayload[iv->id].stsAB[CH1] = false; - mPayload[iv->id].dataAB[CH0] = false; - mPayload[iv->id].stsAB[CH0] = false; + if (mSerialDebug){ + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); } - if (iv->type == INV_TYPE_2CH) { - mPayload[iv->id].dataAB[CH2] = false; - mPayload[iv->id].stsAB[CH2] = false; + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DBGPRINT(String(iv->devControlCmd, HEX)); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + //iv->clearCmdQueue(); + //iv->enqueCommand(SystemConfigPara); // read back power limit + } else { + uint8_t cmd = iv->getQueuedCmd(); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGPRINTLN(String(cmd, HEX)); + uint8_t cmd2 = cmd; + if (cmd == 0x1 ) { //0x1 + cmd = 0x0f; + cmd2 = 0x00; + mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false); + } else { + mRadio->prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); + }; + + mPayload[iv->id].txCmd = cmd; + if (iv->type == INV_TYPE_1CH || iv->type == INV_TYPE_2CH) { + mPayload[iv->id].dataAB[CH1] = false; + mPayload[iv->id].stsAB[CH1] = false; + mPayload[iv->id].dataAB[CH0] = false; + mPayload[iv->id].stsAB[CH0] = false; + } + + if (iv->type == INV_TYPE_2CH) { + mPayload[iv->id].dataAB[CH2] = false; + mPayload[iv->id].stsAB[CH2] = false; + } } } @@ -179,14 +218,18 @@ const byteAssign_t InfoAssignment[] = { for (uint8_t i = 0; i < 5; i++) { iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); } - iv->setQueuedCmdFinished(); + /*iv->setQueuedCmdFinished(); mStat->rxSuccess++; - mRadio->sendCmdPacket(iv->radioId.u64, 0x0f, 0x01, false); + mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x01, false);*/ } else if ( p->packet[9] == 0x01 ) {//second frame - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got 2nd frame (hw info)")); - mRadio->sendCmdPacket(iv->radioId.u64, 0x0f, 0x12, false); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("got 2nd frame (hw info)")); + //mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x12, false); } else if ( p->packet[9] == 0x12 ) {//3rd frame - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got 3rd frame (hw info)")); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("got 3rd frame (hw info)")); + iv->setQueuedCmdFinished(); + mStat->rxSuccess++; } } else if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command @@ -218,7 +261,8 @@ const byteAssign_t InfoAssignment[] = { } } */ } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command - DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); + DPRINTHEAD(DBG_DEBUG, iv->id); + DBGPRINTLN(F("Response from devcontrol request received")); mPayload[iv->id].txId = p->packet[0]; iv->clearDevControlRequest(); @@ -229,7 +273,10 @@ const byteAssign_t InfoAssignment[] = { 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])); + //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])); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(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 } @@ -300,7 +347,7 @@ const byteAssign_t InfoAssignment[] = { if (NULL == iv) continue; // skip to next inverter - if (IV_MI != iv->ivGen) // only process MI inverters + if (IV_HM == iv->ivGen) // only process MI inverters continue; // skip to next inverter if ( !mPayload[iv->id].complete && @@ -332,21 +379,20 @@ const byteAssign_t InfoAssignment[] = { 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...")); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); mPayload[iv->id].retransmits = mMaxRetrans; } else if(iv->devControlCmd == ActivePowerContr) { - DPRINT(DBG_INFO, F("(#")); - DBGPRINT(String(iv->id)); - DBGPRINTLN(F(") retransmit power limit")); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("retransmit power limit")); mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); } else { uint8_t cmd = mPayload[iv->id].txCmd; if (mPayload[iv->id].retransmits < mMaxRetrans) { mPayload[iv->id].retransmits++; if( !mPayload[iv->id].gotFragment ) { - DPRINT(DBG_INFO, F("(#")); - DBGPRINT(String(iv->id)); - DBGPRINTLN(F(") nothing received")); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("nothing received")); mPayload[iv->id].retransmits = mMaxRetrans; } else if ( cmd == 0x0f ) { //hard/firmware request @@ -360,22 +406,23 @@ const byteAssign_t InfoAssignment[] = { change = true; } else if ( cmd == 0x09 ) {//MI single or dual channel device if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) { - if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) { + if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {} + //first try to get missing sts for first channel a second time + else if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) { cmd = 0x11; change = true; + mPayload[iv->id].retransmits = 0; //reset counter } } } else if ( cmd == 0x11) { - if ( mPayload[iv->id].dataAB[CH2] ) { // data is there, but no status - if (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1] ) { + if ( mPayload[iv->id].dataAB[CH2] ) { // data + status ch2 are there? + if (mPayload[iv->id].stsAB[CH2] && (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1])) { cmd = 0x09; change = true; } } } - DPRINT(DBG_INFO, F("(#")); - DBGPRINT(String(iv->id)); - DBGPRINT(F(") ")); + DPRINTHEAD(DBG_INFO, iv->id); if (change) { DBGPRINT(F("next request is 0x")); } else { @@ -393,9 +440,12 @@ const byteAssign_t InfoAssignment[] = { } 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")); + DPRINTHEAD(DBG_WARN, iv->id); + DBGPRINTLN(F("CRC Error: Request Complete Retransmit")); mPayload[iv->id].txCmd = iv->getQueuedCmd(); - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); + DPRINTHEAD(DBG_INFO, iv->id); + + DBGPRINTLN(F("prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); mRadio->prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); } } @@ -470,15 +520,16 @@ const byteAssign_t InfoAssignment[] = { (mCbMiPayload)(val); } - void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t chan = CH1) { - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") status msg 0x") + String(p->packet[0], HEX)); + void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) { + //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") status msg 0x") + String(p->packet[0], HEX)); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure rec->ts = mPayload[iv->id].ts; mPayload[iv->id].gotFragment = true; mPayload[iv->id].txId = p->packet[0]; - uint8_t status = (p->packet[11] << 8) + p->packet[12]; - uint8_t stschan = p->packet[0] == 0x88 ? CH1 : CH2; + //uint8_t status = (p->packet[11] << 8) + p->packet[12]; + uint8_t status = (p->packet[9] << 8) + p->packet[10]; + //uint8_t stschan = p->packet[0] == 0x88 ? CH1 : CH2; mPayload[iv->id].sts[stschan] = status; mPayload[iv->id].stsAB[stschan] = true; if (mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].stsAB[CH2]) @@ -491,26 +542,13 @@ const byteAssign_t InfoAssignment[] = { if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!? - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") alarm ID incremented to ") + String(iv->alarmMesIndex)); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("alarm ID incremented to ") + String(iv->alarmMesIndex)); iv->enqueCommand(AlarmData); } //mPayload[iv->id].skipfirstrepeat = 1; if (mPayload[iv->id].stsAB[CH0] && mPayload[iv->id].dataAB[CH0] && !mPayload[iv->id].complete) { miComplete(iv); - /*mPayload[iv->id].complete = true; - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got all msgs")); - iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); - //preliminary AC calculation... - uint8_t ac_pow = 0; - //if (mPayload[iv->id].sts[0] == 3) { - ac_pow = calcPowerDcCh0(iv, 0)*9.5; - //} - iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) (ac_pow/10)); - iv->setQueuedCmdFinished(); - iv->doCalculations(); - mPayload[iv->id].skipfirstrepeat = 0; - notify(mPayload[iv->id].txCmd); - yield();*/ } } @@ -523,7 +561,7 @@ const byteAssign_t InfoAssignment[] = { ( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 : p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 : CH4; - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan); + //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan); // count in RF_communication_protocol.xlsx is with offset = -1 iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10); yield(); @@ -581,33 +619,18 @@ const byteAssign_t InfoAssignment[] = { if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; - DPRINTLN(DBG_INFO, F("alarm ID incremented to ") + String(iv->alarmMesIndex)); + DPRINTHEAD(DBG_INFO, iv->id); + DBGPRINTLN(F("alarm ID incremented to ") + String(iv->alarmMesIndex)); //iv->enqueCommand(AlarmData); } } - - if ( mPayload[iv->id].complete || //4ch device (iv->type != INV_TYPE_4CH //other devices && mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].stsAB[CH0])) { miComplete(iv); - /*mPayload[iv->id].complete = true; // For 2 CH devices, this might be too short... - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got all msgs")); - iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); - //preliminary AC calculation... - uint8_t ac_pow = 0; - //if (mPayload[iv->id].sts[0] == 3) { - ac_pow = calcPowerDcCh0(iv, 0)*9.5; - //} - iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) (ac_pow/10)); - iv->doCalculations(); - iv->setQueuedCmdFinished(); - mStat->rxSuccess++; - yield(); - notify(mPayload[iv->id].txCmd);*/ } @@ -633,12 +656,18 @@ const byteAssign_t InfoAssignment[] = { DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got all msgs")); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); + //preliminary AC calculation... - uint8_t ac_pow = 0; - //if (mPayload[iv->id].sts[0] == 3) { - ac_pow = calcPowerDcCh0(iv, 0)*9.5; - //} - iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) (ac_pow/10)); + float ac_pow = 0; + for(uint8_t i = 1; i <= iv->channels; i++) { + if (mPayload[iv->id].sts[i] == 3) { + uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); + ac_pow += iv->getValue(pos, rec); + } + } + ac_pow = (int) (ac_pow*9.5); + iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10); + iv->doCalculations(); iv->setQueuedCmdFinished(); mStat->rxSuccess++; @@ -677,8 +706,16 @@ const byteAssign_t InfoAssignment[] = { return true; } +/* void miDPRINTHead(uint8_t lvl, uint8_t id) { + DPRINT(lvl, F("(#")); + DBGPRINT(String(id)); + DBGPRINT(F(") ")); + }*/ + void reset(uint8_t id) { - DPRINTLN(DBG_INFO, F("resetPayload: id: ") + String(id)); + //DPRINTLN(DBG_INFO, F("resetPayload: id: ") + String(id)); + DPRINTHEAD(DBG_INFO, id); + DBGPRINTLN(F("resetPayload")); memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); mPayload[id].gotFragment = false; /*mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; @@ -695,7 +732,7 @@ const byteAssign_t InfoAssignment[] = { mPayload[id].skipfirstrepeat = 0; mPayload[id].requested = false; mPayload[id].ts = *mTimestamp; - mPayload[id].sts[0] = 0; //disable this in case gotFragment is not working + mPayload[id].sts[0] = 0; mPayload[id].sts[CH1] = 0; mPayload[id].sts[CH2] = 0; mPayload[id].sts[CH3] = 0; diff --git a/src/main.cpp b/src/main.cpp index 966fda8f..f780a002 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #include "utils/dbg.h" diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 5b572219..1a0222b2 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -14,7 +14,7 @@ class Display { public: Display() {} - void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, uint8_t disp_reset, const char *version) { + void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { mCfg = cfg; mSys = sys; mUtcTs = utcTs; @@ -25,9 +25,9 @@ class Display { if (mCfg->type == 0) return; - if ((1 < mCfg->type) && (mCfg->type < 10)) { + if ((0 < mCfg->type) && (mCfg->type < 10)) { mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); - mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); } else if (mCfg->type >= 10) { #if defined(ESP32) mRefreshCycle = 0; @@ -42,7 +42,7 @@ class Display { } void tickerSecond() { - loop(); + mMono.loop(); if (mNewPayload || ((++mLoopCnt % 10) == 0)) { mNewPayload = false; mLoopCnt = 0; @@ -79,7 +79,7 @@ class Display { totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); } - if ((1 < mCfg->type) && (mCfg->type < 10)) { + if ((0 < mCfg->type) && (mCfg->type < 10)) { mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod); } else if (mCfg->type >= 10) { #if defined(ESP32) diff --git a/src/plugins/Display/Display_Mono.cpp b/src/plugins/Display/Display_Mono.cpp index ad27ebcc..d55b6061 100644 --- a/src/plugins/Display/Display_Mono.cpp +++ b/src/plugins/Display/Display_Mono.cpp @@ -27,9 +27,9 @@ DisplayMono::DisplayMono() { -void DisplayMono::init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { +void DisplayMono::init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { if ((0 < type) && (type < 4)) { - u8g2_cb_t *rot = (u8g2_cb_t *)((rot != 0x00) ? U8G2_R2 : U8G2_R0); + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); mType = type; switch(type) { case 1: @@ -108,7 +108,7 @@ void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTo if (!(_mExtra % 10) && (ip)) { printText(ip.toString().c_str(), 3); } else if (!(_mExtra % 5)) { - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "#%d Inverter online", isprod); + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); printText(_fmtText, 3); } else { if(mIsLarge && (NULL != mUtcTs)) diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index fea372cd..a2254b8d 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -168,7 +168,7 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl _display->println("kWh"); _display->setCursor(0, _display->height() - (mHeadFootPadding + 10)); - snprintf(_fmtText, sizeof(_fmtText), "#%d Inverter online", _isprod); + snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); _display->println(_fmtText); } while (_display->nextPage()); diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 4ec4c3f5..73196918 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- // https://bert.emelis.net/espMqttClient/ @@ -59,7 +59,7 @@ class PubMqtt { if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0)) mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd); - snprintf(mClientId, 26, "%s-", mDevName); + snprintf(mClientId, 24, "%s-", mDevName); uint8_t pos = strlen(mClientId); mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0]; mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0]; @@ -111,6 +111,9 @@ class PubMqtt { publish(subtopics[MQTT_UPTIME], val); publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str()); publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str()); + #ifndef ESP32 + publish(subtopics[MQTT_HEAP_FRAG], String(ESP.getHeapFragmentation()).c_str()); + #endif } bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { @@ -162,14 +165,16 @@ class PubMqtt { if(!mClient.connected()) return; - String topic = ""; - if(addTopic) - topic = String(mCfgMqtt->topic) + "/"; - topic += String(subTopic); + memset(mTopic, 0, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1); + if(addTopic){ + snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic); + } else { + snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s", subTopic); + } do { - if(0 != mClient.publish(topic.c_str(), QOS_0, retained, payload)) - break; + if(0 != mClient.publish(mTopic, QOS_0, retained, payload)) + break; if(!mClient.connected()) break; #if defined(ESP8266) @@ -312,13 +317,14 @@ class PubMqtt { tickerMinute(); publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false); - subscribe(subscr[MQTT_SUBS_LMT_PERI_REL]); - subscribe(subscr[MQTT_SUBS_LMT_PERI_ABS]); - subscribe(subscr[MQTT_SUBS_LMT_NONPERI_REL]); - subscribe(subscr[MQTT_SUBS_LMT_NONPERI_ABS]); + char sub[20]; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + snprintf(sub, 20, "ctrl/limit/%d", i); + subscribe(sub); + snprintf(sub, 20, "ctrl/restart/%d", i); + subscribe(sub); + } subscribe(subscr[MQTT_SUBS_SET_TIME]); - subscribe(subscr[MQTT_SUBS_SYNC_NTP]); - //subscribe("status/#"); } void onDisconnect(espMqttClientTypes::DisconnectReason reason) { @@ -358,11 +364,14 @@ class PubMqtt { DynamicJsonDocument json(128); JsonObject root = json.to(); + bool limitAbs = false; if(len > 0) { char *pyld = new char[len + 1]; strncpy(pyld, (const char*)payload, len); pyld[len] = '\0'; - root["val"] = atoi(pyld); + root[F("val")] = atoi(pyld); + if(pyld[len-1] == 'W') + limitAbs = true; delete[] pyld; } @@ -377,8 +386,17 @@ class PubMqtt { tmp[pos] = '\0'; switch(elm++) { case 1: root[F("path")] = String(tmp); break; - case 2: root[F("cmd")] = String(tmp); break; - case 3: root[F("id")] = atoi(tmp); break; + case 2: + if(strncmp("limit", tmp, 5) == 0) { + if(limitAbs) + root[F("cmd")] = F("limit_nonpersistent_absolute"); + else + root[F("cmd")] = F("limit_nonpersistent_relative"); + } + else + root[F("cmd")] = String(tmp); + break; + case 3: root[F("id")] = atoi(tmp); break; default: break; } if('\0' == p[pos]) @@ -569,8 +587,8 @@ class PubMqtt { if (sendTotals) { uint8_t fieldId; - bool retained = true; for (uint8_t i = 0; i < 4; i++) { + bool retained = true; switch (i) { default: case 0: @@ -622,7 +640,9 @@ class PubMqtt { // last will topic and payload must be available trough lifetime of 'espMqttClient' char mLwtTopic[MQTT_TOPIC_LEN+5]; const char *mDevName, *mVersion; - char mClientId[26]; // number of chars is limited to 23 up to v3.1 of MQTT + char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT + // global buffer for mqtt topic. Used when publishing mqtt messages. + char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; }; #endif /*__PUB_MQTT_H__*/ diff --git a/src/publisher/pubMqttDefs.h b/src/publisher/pubMqttDefs.h index 64309b18..088023b7 100644 --- a/src/publisher/pubMqttDefs.h +++ b/src/publisher/pubMqttDefs.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __PUB_MQTT_DEFS_H__ @@ -41,6 +41,7 @@ enum { MQTT_UPTIME = 0, MQTT_RSSI, MQTT_FREE_HEAP, + MQTT_HEAP_FRAG, MQTT_SUNRISE, MQTT_SUNSET, MQTT_COMM_START, @@ -64,6 +65,7 @@ const char* const subtopics[] PROGMEM = { "uptime", "wifi_rssi", "free_heap", + "heap_frag", "sunrise", "sunset", "comm_start", @@ -84,21 +86,11 @@ const char* const subtopics[] PROGMEM = { }; enum { - MQTT_SUBS_LMT_PERI_REL, - MQTT_SUBS_LMT_PERI_ABS, - MQTT_SUBS_LMT_NONPERI_REL, - MQTT_SUBS_LMT_NONPERI_ABS, - MQTT_SUBS_SET_TIME, - MQTT_SUBS_SYNC_NTP + MQTT_SUBS_SET_TIME }; const char* const subscr[] PROGMEM = { - "ctrl/limit_persistent_relative", - "ctrl/limit_persistent_absolute", - "ctrl/limit_nonpersistent_relative", - "ctrl/limit_nonpersistent_absolute", - "setup/set_time", - "setup/sync_ntp" + "setup/set_time" }; #endif /*__PUB_MQTT_DEFS_H__*/ diff --git a/src/utils/dbg.h b/src/utils/dbg.h index bdfa2b15..56623cf8 100644 --- a/src/utils/dbg.h +++ b/src/utils/dbg.h @@ -146,6 +146,10 @@ }\ }) +#define DPRINTHEAD(level, id) ({\ + DPRINT(level, F("(#")); DBGPRINT(String(id)); DBGPRINT(F(") "));\ +}) + #define DPRINTLN(level, str) ({\ switch(level) {\ case DBG_ERROR: PERRLN(str); break; \ @@ -156,6 +160,37 @@ }\ }) +// available text variables +#define TXT_NOPYLD 1 +#define TXT_INVSERNO 2 +#define TXT_GDEVINF 3 +#define TXT_DEVCTRL 4 +#define TXT_INCRALM 5 + + +#define DBGPRINT_TXT(text) ({\ + switch(text) {\ + case TXT_NOPYLD: DBGPRINT(F("no Payload received! (retransmits: ")); break; \ + case TXT_INVSERNO: DBGPRINT(F("Requesting Inv SN ")); break; \ + case TXT_GDEVINF: DBGPRINT(F("prepareDevInformCmd 0x")); break; \ + case TXT_DEVCTRL: DBGPRINT(F("Devcontrol request 0x")); break; \ + case TXT_INCRALM: DBGPRINT(F("alarm ID incremented to ")); break; \ + default: ; break; \ + }\ +}) + +// available text variables w. lf +#define TXT_TIMEOUT 1 +#define TXT_NOPYLD2 2 + +#define DBGPRINTLN_TXT(text) ({\ + switch(text) {\ + case TXT_TIMEOUT: DBGPRINT(F("enqueued cmd failed/timeout\r\n")); break; \ + case TXT_NOPYLD2: DBGPRINT(F("nothing received\r\n")); break; \ + default: ; break; \ + }\ +}) + /*class ahoyLog { public: ahoyLog() {} diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 325fc4a6..79888011 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -267,9 +267,8 @@ class RestApi { void getHtmlSave(JsonObject obj) { getGeneric(obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 2; - obj[F("refresh_url")] = "/setup"; - obj[F("html")] = F("settings succesfully save"); + obj["pending"] = (bool)mApp->getSavePending(); + obj["success"] = (bool)mApp->getLastSaveSucceed(); } void getReboot(JsonObject obj) { @@ -422,10 +421,10 @@ class RestApi { obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data; - obj[F("disp_cs")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; - obj[F("disp_dc")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; - obj[F("disp_rst")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; - obj[F("disp_bsy")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; + obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; + obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; + obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; + obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; } void getIndex(JsonObject obj) { @@ -498,7 +497,6 @@ class RestApi { void getLive(JsonObject obj) { getGeneric(obj.createNestedObject(F("generic"))); - //JsonArray invArr = obj.createNestedArray(F("inverter")); obj[F("refresh")] = mConfig->nrf.sendInterval; for (uint8_t fld = 0; fld < sizeof(acList); fld++) { @@ -518,52 +516,6 @@ class RestApi { parse = iv->config->enabled; obj[F("iv")][i] = parse; } - - /*Inverter<> *iv; - uint8_t pos; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("enabled")] = (bool)iv->config->enabled; - obj2[F("name")] = String(iv->config->name); - obj2[F("channels")] = iv->channels; - obj2[F("power_limit_read")] = ah::round3(iv->actPowerLimit); - //obj2[F("last_alarm")] = String(iv->lastAlarmMsg); - obj2[F("ts_last_success")] = rec->ts; - - JsonArray ch = obj2.createNestedArray("ch"); - JsonArray ch0 = ch.createNestedArray(); - obj2[F("ch_names")][0] = "AC"; - for (uint8_t fld = 0; fld < sizeof(list); fld++) { - pos = (iv->getPosByChFld(CH0, list[fld], rec)); - ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } - - for(uint8_t j = 1; j <= iv->channels; j ++) { - obj2[F("ch_names")][j] = String(iv->config->chName[j-1]); - JsonArray cur = ch.createNestedArray(); - for (uint8_t k = 0; k < 6; k++) { - switch(k) { - default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break; - case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break; - case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break; - case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break; - case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break; - case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; - } - cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - if(1 == j) { - obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } - } - } - } - }*/ } void getRecord(JsonObject obj, uint8_t recType) { diff --git a/src/web/html/includes/footer.html b/src/web/html/includes/footer.html index 5a2b705b..bbc4c6ce 100644 --- a/src/web/html/includes/footer.html +++ b/src/web/html/includes/footer.html @@ -10,7 +10,7 @@ diff --git a/src/web/html/index.html b/src/web/html/index.html index 27a46a9c..72537e5e 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -102,12 +102,12 @@ if(obj["disNightComm"]) { if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"]) && ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) { - commInfo = "Polling inverter(s), will stop at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); } else { commInfo = "Night time, inverter polling disabled, "; if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) { - commInfo += "stopped at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + commInfo += "paused at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); } else { commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')); diff --git a/src/web/html/save.html b/src/web/html/save.html new file mode 100644 index 00000000..54d43d7f --- /dev/null +++ b/src/web/html/save.html @@ -0,0 +1,51 @@ + + + + Save + {#HTML_HEADER} + + + {#HTML_NAV} +
+
+
+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 3b32d109..8d47abc6 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -161,7 +161,7 @@
-
Reset values when inverter polling stops at sunset
+
Reset values when inverter polling pauses at sunset
@@ -213,7 +213,7 @@
-
Stop polling inverters during night
+
Pause polling inverters during night
@@ -269,12 +269,12 @@
-
Enable Screensaver (pixel shifting)
+
Enable Screensaver (pixel shifting, OLED only)
-
Contrast
-
+
Luminance
+

Pinout

@@ -283,25 +283,34 @@
Reboot device after successful save
-
+
- -
-
- ERASE SETTINGS (not WiFi) -
- Upload / Store JSON Settings -
- - -
-
- Download settings (JSON file) (only saved values, passwords will be removed!) -
+
+
+ ERASE SETTINGS (not WiFi) +
+ Import / Export JSON Settings +
+
Import
+
+
+ + +
+
+
+
+
Export
+
+ Export settings (JSON file) (only values, passwords will be removed!) +
+
+
+
{#HTML_FOOTER} @@ -363,7 +372,7 @@ 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":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId); + ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_pwr":[0,0,0,0],"ch_name":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId); } }); @@ -664,12 +673,12 @@ document.getElementsByName(i)[0].checked = obj[i]; var e = document.getElementById("dispPins"); - pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst'], ['busy', 'disp_bsy']]; + var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']]; + if("ESP32" == type) + pins.push(['busy', 'disp_bsy']); for(p of pins) { - if(("ESP8266" == type) && p[0] == "busy") - break; e.append( - ml("div", {class: "row mb-3"}, [ + ml("div", {class: "row mb-3", id: "row_" + p[1]}, [ ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), ml("div", {class: "col-12 col-sm-9"}, sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]]) @@ -681,12 +690,16 @@ var opts = [[0, "None"], [1, "SSD1306 0.96\""], [2, "SH1106 1.3\""], [3, "Nokia5110"]]; if("ESP32" == type) opts.push([10, "ePaper"]); + var dispType = sel("disp_typ", opts, obj["disp_typ"]); document.getElementById("dispType").append( ml("div", {class: "row mb-3"}, [ ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"), - ml("div", {class: "col-12 col-sm-9"}, sel("disp_typ", opts, obj["disp_typ"])) + ml("div", {class: "col-12 col-sm-9"}, dispType) ]) ); + dispType.addEventListener('change', (e) => { + hideDispPins(pins, e.target.value) + }); opts = [[0, "0°"], [2, "180°"]]; if("ESP32" == type) { @@ -701,6 +714,28 @@ ); document.getElementsByName("disp_cont")[0].value = obj["disp_cont"]; + hideDispPins(pins, obj.disp_typ); + } + + function hideDispPins(pins, dispType) { + for(var i = 0; i < pins.length; i++) { + var cl = document.getElementById("row_" + pins[i][1]).classList; + + if(0 == dispType) + cl.add("hide"); + else if(dispType <= 2) { // OLED + if(i < 2) + cl.remove("hide"); + else + cl.add("hide"); + } else if(dispType == 3) { // Nokia + if(i < 4) + cl.remove("hide"); + else + cl.add("hide"); + } else // ePaper + cl.remove("hide"); + } } function parse(root) { diff --git a/src/web/web.h b/src/web/web.h index d0de6abf..30b22901 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __WEB_H__ @@ -27,6 +27,7 @@ #include "html/h/setup_html.h" #include "html/h/style_css.h" #include "html/h/system_html.h" +#include "html/h/save_html.h" #include "html/h/update_html.h" #include "html/h/visualization_html.h" @@ -67,7 +68,7 @@ class Web { mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1)); mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); - mWeb.on("/save", HTTP_ANY, std::bind(&Web::showSave, this, std::placeholders::_1)); + mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1)); mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); //mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1)); @@ -161,22 +162,24 @@ class Web { DPRINTLN(DBG_ERROR, F("can't open file!")); mUploadFail = true; mUploadFp.close(); + return; } } mUploadFp.write(data, len); if (final) { mUploadFp.close(); - File fp = LittleFS.open("/tmp.json", "r"); - if (!fp) + char pwd[PWD_LEN]; + strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD + if (!mApp->readSettings("/tmp.json")) { mUploadFail = true; - else { - if (!mApp->readSettings("tmp.json")) { - mUploadFail = true; - DPRINTLN(DBG_ERROR, F("upload JSON error!")); - } else - mApp->saveSettings(); + DPRINTLN(DBG_ERROR, F("upload JSON error!")); + } else { + LittleFS.remove("/tmp.json"); + strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD + mApp->saveSettings(true); } - DPRINTLN(DBG_INFO, F("upload finished!")); + if (!mUploadFail) + DPRINTLN(DBG_INFO, F("upload finished!")); } } @@ -324,7 +327,7 @@ class Web { mProtected = true; - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), save_html, save_html_len); response->addHeader(F("Content-Encoding"), "gzip"); request->send(response); } @@ -372,7 +375,7 @@ class Web { void onReboot(AsyncWebServerRequest *request) { mApp->setRebootFlag(); - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), save_html, save_html_len); response->addHeader(F("Content-Encoding"), "gzip"); request->send(response); } @@ -414,10 +417,8 @@ class Web { refresh = 120; } request->send(200, F("text/html; charset=UTF-8"), F("Factory Reset") + content + F("")); - if (refresh == 10) { - delay(1000); - ESP.restart(); - } + if (refresh == 10) + onReboot(request); } void onSetup(AsyncWebServerRequest *request) { @@ -597,15 +598,11 @@ class Web { mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt(); mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt(); - mApp->saveSettings(); + mApp->saveSettings((request->arg("reboot") == "on")); - if (request->arg("reboot") == "on") - onReboot(request); - else { - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); - } + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), save_html, save_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); } void onLive(AsyncWebServerRequest *request) { @@ -625,71 +622,6 @@ class Web { request->send(response); } - /*void showWebApi(AsyncWebServerRequest *request) { - // TODO: remove - DPRINTLN(DBG_VERBOSE, F("web::showWebApi")); - DPRINTLN(DBG_DEBUG, request->arg("plain")); - const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity. - DynamicJsonDocument response(capacity); - - // Parse JSON object - deserializeJson(response, request->arg("plain")); - // ToDo: error handling for payload - uint8_t iv_id = response["inverter"]; - uint8_t cmd = response["cmd"]; - Inverter<> *iv = mSys->getInverterByPos(iv_id); - if (NULL != iv) { - if (response["tx_request"] == (uint8_t)TX_REQ_INFO) { - // if the AlarmData is requested set the Alarm Index to the requested one - if (cmd == AlarmData || cmd == AlarmUpdate) { - // set the AlarmMesIndex for the request from user input - iv->alarmMesIndex = response["payload"]; - } - DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"])); - // process payload from web request corresponding to the cmd - iv->enqueCommand(cmd); - } - - - if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) { - if (response["cmd"] == (uint8_t)ActivePowerContr) { - uint16_t webapiPayload = response["payload"]; - uint16_t webapiPayload2 = response["payload2"]; - if (webapiPayload > 0 && webapiPayload < 10000) { - iv->devControlCmd = ActivePowerContr; - iv->powerLimit[0] = webapiPayload; - if (webapiPayload2 > 0) - iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check - else // if not set, set it to 0x0000 default - iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut - if (iv->powerLimit[1] & 0x0001) - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API")); - else - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API")); - iv->devControlRequest = true; // queue it in the request loop - } - } - if (response["cmd"] == (uint8_t)TurnOff) { - iv->devControlCmd = TurnOff; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)TurnOn) { - iv->devControlCmd = TurnOn; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) { - iv->devControlCmd = CleanState_LockAndAlarm; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)Restart) { - iv->devControlCmd = Restart; - iv->devControlRequest = true; // queue it in the request loop - } - } - } - request->send(200, "text/json", "{success:true}"); - }*/ - void onDebug(AsyncWebServerRequest *request) { mApp->getSchedulerNames(); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok"); diff --git a/src/wifi/ahoywifi.cpp b/src/wifi/ahoywifi.cpp index ec48759f..7fbd9590 100644 --- a/src/wifi/ahoywifi.cpp +++ b/src/wifi/ahoywifi.cpp @@ -94,12 +94,11 @@ void ahoywifi::tickWifiLoop() { } mCnt++; - uint8_t timeout = 10; // seconds + uint8_t timeout = (mStaConn == DISCONNECTED) ? 10 : 20; // seconds if (mStaConn == CONNECTED) // connected but no ip timeout = 20; - - if(!mScanActive && mBSSIDList.empty() && ((mCnt % timeout) == 0)) { // start scanning APs with the given SSID + if(!mScanActive && mBSSIDList.empty() && (mStaConn == DISCONNECTED)) { // start scanning APs with the given SSID DBGPRINT(F("scanning APs with SSID ")); DBGPRINTLN(String(mConfig->sys.stationSsid)); mScanCnt = 0; @@ -121,8 +120,9 @@ void ahoywifi::tickWifiLoop() { mCnt = timeout - 2; } if((mCnt % timeout) == 0) { // try to reconnect after x sec without connection - if(mStaConn != CONNECTED) - mStaConn = CONNECTING; + mStaConn = CONNECTING; + WiFi.disconnect(); + if(mBSSIDList.size() > 0) { // get first BSSID in list DBGPRINT(F("try to connect to AP with BSSID:")); uint8_t bssid[6]; @@ -132,9 +132,11 @@ void ahoywifi::tickWifiLoop() { DBGPRINT(" " + String(bssid[j], HEX)); } DBGPRINTLN(""); - WiFi.disconnect(); WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]); } + else + mStaConn = DISCONNECTED; + mCnt = 0; } } diff --git a/tools/rpi/README.md b/tools/rpi/README.md index d1829706..79bc5cbb 100644 --- a/tools/rpi/README.md +++ b/tools/rpi/README.md @@ -21,14 +21,22 @@ Required Hardware Setup `ahoy.py` has been successfully tested with the following setup -- RaspberryPi Model 2B (any model should work) +- RaspberryPi Model 2B, 4B (any model should work) - NRF24L01+ Radio Module connected as described, e.g., in [2] (Instructions at [3] should work identically, but [2] has more pretty pictures.) +- or the [PaHoy board](https://github.com/DM6JM/PaHoy/) - TMRh20's 'Optimized High Speed nRF24L01+ Driver' [3], installed as per the instructions given in [4] - Python Library Wrapper, as per [5] +- or the easy way, using [pyRF24](https://github.com/nRF24/pyRF24)[6] +How to talk to the nRF24L01+ in Python? +--------------------------------------- +Either you make use of the way proposed in the following, using the NRF24 Python Wrapper and the 'Optimized High Speed nRF24L01+ Driver' OR you just use pip and let it install pyRF24. + +- If you go with pyRF24, all that needs to be done is installing pyRF24 as described in [6]. Please be aware that not all examples provided in this repo are prepared to use pyRF24. It might be nescessary to adjust the imports from RF24 to pyRF24 to get them running. Once you installed pyRF24, go on at 'Required python modules' +- If you go with the RF24 wrapper, do the following steps Building the NRF24 Python Wrapper --------------------------------- @@ -220,7 +228,12 @@ Example injects exactly the same as we normally use to poll data This allows for even faster hacking during runtime - +Running it as a service +----------------------- +If you want to run directly from the start, you might want to install it as a service. +Depending on if you want to run it once a user is logged in or as soon as the system is booted, two service examples are included. +ahoy.service allows you to start it as a user service upon login. +ahoy_system.service allows you to start it as a system service already before login without user interaction. Analysing the Logs ------------------ @@ -263,3 +276,4 @@ References - [3] https://nrf24.github.io/RF24/index.html - [4] https://nrf24.github.io/RF24/md_docs_linux_install.html - [5] https://nrf24.github.io/RF24/md_docs_python_wrapper.html +- [6] https://github.com/nRF24/pyRF24 diff --git a/tools/rpi/ahoy_system.service b/tools/rpi/ahoy_system.service new file mode 100644 index 00000000..df8f4b13 --- /dev/null +++ b/tools/rpi/ahoy_system.service @@ -0,0 +1,42 @@ +###################################################################### +# systemd.service configuration for ahoy (lumapu) +# users can modify the lines: +# Description +# ExecStart (example: name of config file) +# WorkingDirectory (absolute path to your private ahoy dir) +# To change other config parameter, please consult systemd documentation +# +# To activate this service, enable and start ahoy.service: +# - Create folder ahoy in /home/ and set owner to the user that the +# service should be executed for (e.g. pi) +# - Copy folder contents to new folder +# - Adjust the user that this service should be executed as, avoid root +# - Execute commands to setup, check and start/stop as wanted +# $ sudo systemctl enable /home/ahoy/tools/rpi/ahoy.service +# $ sudo systemctl status ahoy +# $ sudo systemctl start ahoy +# $ sudo systemctl stop ahoy +# +# 2023.01 +# 2023.03 +###################################################################### + +[Unit] + +Description=ahoy (lumapu) as Service +After=network.target local-fs.target time-sync.target + +[Service] +ExecStart=/usr/bin/env python3 -um hoymiles --log-transactions --verbose --config ahoy.yml +RestartSec=10 +Restart=on-failure +Type=simple +User=pi + +# WorkingDirectory must be an absolute path - not relative path +WorkingDirectory=/home/ahoy/tools/rpi +EnvironmentFile=/etc/environment + +[Install] +WantedBy=default.target +