From d195eee498ee199dfdecaf32a6308ea028995269 Mon Sep 17 00:00:00 2001 From: lumapu Date: Mon, 25 Apr 2022 17:48:12 +0200 Subject: [PATCH] * pinout can be saved using the web setup * html / css files are now located inside PROGMEM * conversion of files is done with python script (OS independent, open source) * moved as much as possible for now to the hm* modules - the app should only be the body * successfully tested with HM1200 --- tools/esp8266/README.md | 25 ++-- tools/esp8266/app.cpp | 136 ++------------------ tools/esp8266/app.h | 24 +--- tools/esp8266/defines.h | 13 +- tools/esp8266/eep.h | 4 +- tools/esp8266/esp8266.ino | 7 +- tools/esp8266/hmRadio.h | 179 +++++++++++++++++++++------ tools/esp8266/hmSystem.h | 8 +- tools/esp8266/html/conv.bat | 5 - tools/esp8266/html/convert.py | 28 +++++ tools/esp8266/html/h/hoymiles_html.h | 5 +- tools/esp8266/html/h/index_html.h | 5 +- tools/esp8266/html/h/setup_html.h | 5 +- tools/esp8266/html/h/style_css.h | 5 +- tools/esp8266/main.cpp | 4 +- tools/esp8266/tools/fileConv.exe | Bin 67584 -> 0 bytes 16 files changed, 239 insertions(+), 214 deletions(-) delete mode 100644 tools/esp8266/html/conv.bat create mode 100644 tools/esp8266/html/convert.py delete mode 100644 tools/esp8266/tools/fileConv.exe diff --git a/tools/esp8266/README.md b/tools/esp8266/README.md index f8690dec..66660bcd 100644 --- a/tools/esp8266/README.md +++ b/tools/esp8266/README.md @@ -1,12 +1,9 @@ ## OVERVIEW -This code was tested on a ESP8266 - ESP-07 module. Many parts of the code are based on 'Hubi's code, which can be found here: +This code is intended to run on a Wemos D1mini or similar. The code is based on 'Hubi's code, which can be found here: -The NRF24L01+ radio module is connected to the standard SPI pins. Additional there are 3 pins, which can be set individual: - -- IRQ - Pin 4 -- CE - Pin 5 -- CS - Pin 15 +The NRF24L01+ radio module is connected to the standard SPI pins. Additional there are 3 pins, which can be set individual: CS, CE and IRQ +These pins can be changed from the /setup URL ## Compile @@ -29,14 +26,22 @@ This code can be compiled using Arduino. The settings were: ## Usage -Connect the ESP to power and to your serial console. The webinterface is currently only used for OTA and config. -The serial console will print all information which is send and received. +Connect the ESP to power and to your serial console. The webinterface has the following abilities: + +- OTA Update (over the air update) +- Configuration (Wifi, inverter(s), Pinout, MQTT) +- visual display of the connected inverters / modules +- some statistics about communication (debug) + +The serial console will print the converted values which were read out of the inverter(s) -## Known Issues +## Compatiblity -- only command 0x81 is received +For now the following inverters should work out of the box: +- HM600 +- HM1200 ## USED LIBRARIES diff --git a/tools/esp8266/app.cpp b/tools/esp8266/app.cpp index 2b016014..0c6c499e 100644 --- a/tools/esp8266/app.cpp +++ b/tools/esp8266/app.cpp @@ -1,15 +1,12 @@ #include "app.h" #include "html/h/index_html.h" +#include "html/h/setup_html.h" #include "html/h/hoymiles_html.h" -extern String setup_html; -#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) - //----------------------------------------------------------------------------- app::app() : Main() { - mSendCnt = 0; mSendTicker = new Ticker(); mFlagSend = false; @@ -93,7 +90,7 @@ void app::setup(const char *ssid, const char *pwd, uint32_t timeout) { mMqtt.sendMsg("version", mVersion); } - initRadio(); + mSys->setup(); if(!mSettingsValid) Serial.println("Warn: your settings are not valid! check [IP]/setup"); @@ -107,15 +104,15 @@ void app::loop(void) { if(!mSys->BufCtrl.empty()) { uint8_t len, rptCnt; packet_t *p = mSys->BufCtrl.getBack(); - //dumpBuf("RAW ", p->packet, MAX_RF_PAYLOAD_SIZE); + //mSys->Radio.dumpBuf("RAW ", p->packet, MAX_RF_PAYLOAD_SIZE); if(mSys->Radio.checkCrc(p->packet, &len, &rptCnt)) { // process buffer only on first occurrence if((0 != len) && (0 == rptCnt)) { + uint8_t *cmd = &p->packet[11]; //Serial.println("CMD " + String(*cmd, HEX)); - //dumpBuf("Payload ", p->packet, len); + //mSys->Radio.dumpBuf("Payload ", p->packet, len); - uint8_t *cmd = &p->packet[11]; inverter_t *iv = mSys->findInverter(&p->packet[3]); if(NULL != iv) { for(uint8_t i = 0; i < iv->listLen; i++) { @@ -142,31 +139,11 @@ void app::loop(void) { if(mFlagSend) { mFlagSend = false; - - uint8_t size = 0; inverter_t *inv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { inv = mSys->getInverterByPos(i); if(NULL != inv) { - //if((mSendCnt % 6) == 0) - size = mSys->Radio.getTimePacket(&inv->radioId.u64, mSendBuf, mTimestamp); - /*else if((mSendCnt % 6) == 1) - size = mSys->Radio.getCmdPacket(&inv->radioId.u64, mSendBuf, 0x15, 0x81); - else if((mSendCnt % 6) == 2) - size = mSys->Radio.getCmdPacket(&inv->radioId.u64, mSendBuf, 0x15, 0x80); - else if((mSendCnt % 6) == 3) - size = mSys->Radio.getCmdPacket(&inv->radioId.u64, mSendBuf, 0x15, 0x83); - else if((mSendCnt % 6) == 4) - size = mSys->Radio.getCmdPacket(&inv->radioId.u64, mSendBuf, 0x15, 0x82); - else if((mSendCnt % 6) == 5) - size = mSys->Radio.getCmdPacket(&inv->radioId.u64, mSendBuf, 0x15, 0x84);*/ - - //Serial.println("sent packet: #" + String(mSendCnt)); - //dumpBuf("SEN ", mSendBuf, size); - sendPacket(inv, mSendBuf, size); - mSendCnt++; - + mSys->Radio.sendTimePacket(inv->radioId.u64, mTimestamp); delay(20); } } @@ -213,100 +190,7 @@ void app::loop(void) { //----------------------------------------------------------------------------- void app::handleIntr(void) { - uint8_t pipe, len; - packet_t *p; - - DISABLE_IRQ; - - while(mRadio->available(&pipe)) { - if(!mSys->BufCtrl.full()) { - p = mSys->BufCtrl.getFront(); - memset(p->packet, 0xcc, MAX_RF_PAYLOAD_SIZE); - p->sendCh = mSendChannel; - len = mRadio->getPayloadSize(); - if(len > MAX_RF_PAYLOAD_SIZE) - len = MAX_RF_PAYLOAD_SIZE; - - mRadio->read(p->packet, len); - mSys->BufCtrl.pushFront(p); - } - else { - bool tx_ok, tx_fail, rx_ready; - mRadio->whatHappened(tx_ok, tx_fail, rx_ready); // reset interrupt status - mRadio->flush_rx(); // drop the packet - } - } - - RESTORE_IRQ; -} - - -//----------------------------------------------------------------------------- -void app::initRadio(void) { - mRadio = new RF24(RF24_CE_PIN, RF24_CS_PIN); - - mRadio->begin(); - mRadio->setAutoAck(false); - mRadio->setRetries(0, 0); - - mRadio->setChannel(DEFAULT_RECV_CHANNEL); - mRadio->setDataRate(RF24_250KBPS); - mRadio->disableCRC(); - mRadio->setAutoAck(false); - mRadio->setPayloadSize(MAX_RF_PAYLOAD_SIZE); - mRadio->setAddressWidth(5); - mRadio->openReadingPipe(1, DTU_RADIO_ID); - - // enable only receiving interrupts - mRadio->maskIRQ(true, true, false); - - // Use lo PA level, as a higher level will disturb CH340 serial usb adapter - mRadio->setPALevel(RF24_PA_MAX); - mRadio->startListening(); - - Serial.println("Radio Config:"); - mRadio->printPrettyDetails(); - - mSendChannel = mSys->Radio.getDefaultChannel(); -} - - -//----------------------------------------------------------------------------- -void app::sendPacket(inverter_t *inv, uint8_t buf[], uint8_t len) { - DISABLE_IRQ; - mRadio->stopListening(); - -#ifdef CHANNEL_HOP - //if(mSendCnt % 6 == 0) - mSendChannel = mSys->Radio.getNxtChannel(); - //else - // mSendChannel = mSys->Radio.getLastChannel(); -#else - mSendChannel = mSys->Radio.getDefaultChannel(); -#endif - mRadio->setChannel(mSendChannel); - //Serial.println("CH: " + String(mSendChannel)); - - mRadio->openWritingPipe(inv->radioId.u64); - mRadio->setCRCLength(RF24_CRC_16); - mRadio->enableDynamicPayloads(); - mRadio->setAutoAck(true); - mRadio->setRetries(3, 15); - - mRadio->write(buf, len); - - // Try to avoid zero payload acks (has no effect) - mRadio->openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id? - - mRadio->setAutoAck(false); - mRadio->setRetries(0, 0); - mRadio->disableDynamicPayloads(); - mRadio->setCRCLength(RF24_CRC_DISABLED); - - mRadio->setChannel(DEFAULT_RECV_CHANNEL); - mRadio->startListening(); - - RESTORE_IRQ; + mSys->Radio.handleIntr(); } @@ -324,7 +208,7 @@ void app::mqttTicker(void) { //----------------------------------------------------------------------------- void app::showIndex(void) { - String html = index_html; + String html = FPSTR(index_html); html.replace("{DEVICE}", mDeviceName); html.replace("{VERSION}", mVersion); mWeb->send(200, "text/html", html); @@ -337,7 +221,7 @@ void app::showSetup(void) { uint16_t interval; - String html = setup_html; + String html = FPSTR(setup_html); html.replace("{SSID}", mStationSsid); // PWD will be left at the default value (for protection) // -> the PWD will only be changed if it does not match the placeholder "{PWD}" @@ -455,7 +339,7 @@ void app::showCmdStatistics(void) { //----------------------------------------------------------------------------- void app::showHoymiles(void) { - String html = hoymiles_html; + String html = FPSTR(hoymiles_html); html.replace("{DEVICE}", mDeviceName); html.replace("{VERSION}", mVersion); mWeb->send(200, "text/html", html); diff --git a/tools/esp8266/app.h b/tools/esp8266/app.h index e7ac42ad..c4cabcdd 100644 --- a/tools/esp8266/app.h +++ b/tools/esp8266/app.h @@ -11,8 +11,8 @@ #include "hmSystem.h" #include "mqtt.h" -typedef HmRadio RadioType; typedef CircularBuffer BufferType; +typedef HmRadio RadioType; typedef HmSystem HmSystemType; const char* const wemosPins[] = {"D3 (GPIO0)", "TX (GPIO1)", "D4 (GPIO2)", "RX (GPIO3)", @@ -31,10 +31,11 @@ class app : public Main { void loop(void); void handleIntr(void); - private: - void initRadio(void); - void sendPacket(inverter_t *inv, uint8_t data[], uint8_t length); + uint8_t getIrqPin(void) { + return mSys->Radio.pinIrq; + } + private: void sendTicker(void); void mqttTicker(void); @@ -49,15 +50,6 @@ class app : public Main { void saveValues(bool webSend); void updateCrc(void); - void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { - Serial.print(String(info)); - for(uint8_t i = 0; i < len; i++) { - Serial.print(buf[i], HEX); - Serial.print(" "); - } - Serial.println(); - } - uint64_t Serial2u64(const char *val) { char tmp[3] = {0}; uint64_t ret = 0ULL; @@ -76,16 +68,10 @@ class app : public Main { uint8_t mState; bool mKeyPressed; - RF24 *mRadio; - packet_t mBuffer[PACKET_BUFFER_SIZE]; HmSystemType *mSys; - Ticker *mSendTicker; - uint32_t mSendCnt; - uint8_t mSendBuf[MAX_RF_PAYLOAD_SIZE]; bool mFlagSend; - uint8_t mSendChannel; uint32_t mCmds[6]; uint32_t mChannelStat[4]; diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h index bbc47a11..765ba2b2 100644 --- a/tools/esp8266/defines.h +++ b/tools/esp8266/defines.h @@ -6,9 +6,8 @@ // PINOUT //------------------------------------- #define RF24_CS_PIN 15 -#define RF24_CE_PIN 2 //5 -#define RF24_IRQ_PIN 0 //4 - +#define RF24_CE_PIN 2 +#define RF24_IRQ_PIN 0 //------------------------------------- @@ -17,6 +16,7 @@ #define PACKET_BUFFER_SIZE 30 #define MAX_NUM_INVERTERS 3 #define MAX_NAME_LENGTH 16 +#define MAX_RF_PAYLOAD_SIZE 64 #define LIVEDATA_VISUALIZED // show live data pv-module wise or as dump @@ -28,6 +28,13 @@ #define VERSION_PATCH 4 +//------------------------------------- +typedef struct { + uint8_t sendCh; + uint8_t packet[MAX_RF_PAYLOAD_SIZE]; +} packet_t; + + //------------------------------------- // EEPROM //------------------------------------- diff --git a/tools/esp8266/eep.h b/tools/esp8266/eep.h index e38bc9fd..da1450b0 100644 --- a/tools/esp8266/eep.h +++ b/tools/esp8266/eep.h @@ -36,7 +36,7 @@ class eep { *value = (EEPROM.read(addr++)); } - void read(uint32_t addr, uint8_t data[], uint8_t length) { + void read(uint32_t addr, uint8_t data[], uint16_t length) { for(uint8_t i = 0; i < length; i ++) { *(data++) = EEPROM.read(addr++); } @@ -77,7 +77,7 @@ class eep { EEPROM.commit(); } - void write(uint32_t addr, uint8_t data[], uint8_t length) { + void write(uint32_t addr, uint8_t data[], uint16_t length) { for(uint8_t i = 0; i < length; i ++) { EEPROM.write(addr++, data[i]); } diff --git a/tools/esp8266/esp8266.ino b/tools/esp8266/esp8266.ino index 5bb82995..5f22245e 100644 --- a/tools/esp8266/esp8266.ino +++ b/tools/esp8266/esp8266.ino @@ -13,12 +13,11 @@ app myApp; //----------------------------------------------------------------------------- void setup() { - // TODO: move to HmRadio - pinMode(RF24_IRQ_PIN, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(RF24_IRQ_PIN), handleIntr, FALLING); - // AP name, password, timeout myApp.setup("ESP AHOY", "esp_8266", 15); + + // TODO: move to HmRadio + attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING); } diff --git a/tools/esp8266/hmRadio.h b/tools/esp8266/hmRadio.h index 0ab66484..70e710e0 100644 --- a/tools/esp8266/hmRadio.h +++ b/tools/esp8266/hmRadio.h @@ -8,9 +8,9 @@ //#define CHANNEL_HOP // switch between channels or use static channel to send #define DEFAULT_RECV_CHANNEL 3 -#define MAX_RF_PAYLOAD_SIZE 64 #define DTU_RADIO_ID ((uint64_t)0x1234567801ULL) +#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) //----------------------------------------------------------------------------- @@ -38,17 +38,14 @@ //----------------------------------------------------------------------------- // HM Radio class //----------------------------------------------------------------------------- -template +template class HmRadio { public: - HmRadio() { - //pinMode(IRQ_PIN, INPUT_PULLUP); - //attachInterrupt(digitalPinToInterrupt(IRQ_PIN), handleIntr, FALLING); - - mSendChan[0] = 23; - mSendChan[1] = 40; - mSendChan[2] = 61; - mSendChan[3] = 75; + HmRadio() : mNrf24(CE_PIN, CS_PIN) { + mChanOut[0] = 23; + mChanOut[1] = 40; + mChanOut[2] = 61; + mChanOut[3] = 75; mChanIdx = 1; calcDtuCrc(); @@ -56,47 +53,106 @@ class HmRadio { pinCs = CS_PIN; pinCe = CE_PIN; pinIrq = IRQ_PIN; + + mSendCnt = 0; } ~HmRadio() {} + void setup(BUFFER *ctrl) { + //Serial.println("HmRadio::setup, pins: " + String(pinCs) + ", " + String(pinCe) + ", " + String(pinIrq)); + pinMode(pinIrq, INPUT_PULLUP); + + mBufCtrl = ctrl; + + mNrf24.begin(pinCe, pinCs); + mNrf24.setAutoAck(false); + mNrf24.setRetries(0, 0); + + mNrf24.setChannel(DEFAULT_RECV_CHANNEL); + mNrf24.setDataRate(RF24_250KBPS); + mNrf24.disableCRC(); + mNrf24.setAutoAck(false); + mNrf24.setPayloadSize(MAX_RF_PAYLOAD_SIZE); + mNrf24.setAddressWidth(5); + mNrf24.openReadingPipe(1, DTU_RADIO_ID); + + // enable only receiving interrupts + mNrf24.maskIRQ(true, true, false); + + // Use lo PA level, as a higher level will disturb CH340 serial usb adapter + mNrf24.setPALevel(RF24_PA_MAX); + mNrf24.startListening(); + + Serial.println("Radio Config:"); + mNrf24.printPrettyDetails(); + + mSendChannel = getDefaultChannel(); + } + + void handleIntr(void) { + uint8_t pipe, len; + packet_t *p; + + DISABLE_IRQ; + while(mNrf24.available(&pipe)) { + if(!mBufCtrl->full()) { + p = mBufCtrl->getFront(); + memset(p->packet, 0xcc, MAX_RF_PAYLOAD_SIZE); + p->sendCh = mSendChannel; + len = mNrf24.getPayloadSize(); + if(len > MAX_RF_PAYLOAD_SIZE) + len = MAX_RF_PAYLOAD_SIZE; + + mNrf24.read(p->packet, len); + mBufCtrl->pushFront(p); + } + else { + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // reset interrupt status + mNrf24.flush_rx(); // drop the packet + } + } + RESTORE_IRQ; + } + uint8_t getDefaultChannel(void) { - return mSendChan[2]; + return mChanOut[2]; } uint8_t getLastChannel(void) { - return mSendChan[mChanIdx]; + return mChanOut[mChanIdx]; } uint8_t getNxtChannel(void) { if(++mChanIdx >= 4) mChanIdx = 0; - return mSendChan[mChanIdx]; + return mChanOut[mChanIdx]; } - uint8_t getTimePacket(const uint64_t *invId, uint8_t buf[], uint32_t ts) { - getCmdPacket(invId, buf, 0x15, 0x80, false); - buf[10] = 0x0b; // cid - buf[11] = 0x00; - CP_U32_LittleEndian(&buf[12], ts); - buf[19] = 0x05; + void sendTimePacket(uint64_t invId, uint32_t ts) { + sendCmdPacket(invId, 0x15, 0x80, false); + mSendBuf[10] = 0x0b; // cid + mSendBuf[11] = 0x00; + CP_U32_LittleEndian(&mSendBuf[12], ts); + mSendBuf[19] = 0x05; - uint16_t crc = crc16(&buf[10], 14); - buf[24] = (crc >> 8) & 0xff; - buf[25] = (crc ) & 0xff; - buf[26] = crc8(buf, 26); + uint16_t crc = crc16(&mSendBuf[10], 14); + mSendBuf[24] = (crc >> 8) & 0xff; + mSendBuf[25] = (crc ) & 0xff; + mSendBuf[26] = crc8(mSendBuf, 26); - return 27; + sendPacket(invId, mSendBuf, 27); } - uint8_t getCmdPacket(const uint64_t *invId, uint8_t buf[], uint8_t mid, uint8_t cmd, bool calcCrc = true) { - memset(buf, 0, MAX_RF_PAYLOAD_SIZE); - buf[0] = mid; // message id - CP_U32_BigEndian(&buf[1], ((*invId) >> 8)); - CP_U32_BigEndian(&buf[5], (DTU_ID >> 8)); - buf[9] = cmd; - if(calcCrc) - buf[10] = crc8(buf, 10); - - return 11; + void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t cmd, bool calcCrc = true) { + memset(mSendBuf, 0, MAX_RF_PAYLOAD_SIZE); + mSendBuf[0] = mid; // message id + CP_U32_BigEndian(&mSendBuf[1], (invId >> 8)); + CP_U32_BigEndian(&mSendBuf[5], (DTU_ID >> 8)); + mSendBuf[9] = cmd; + if(calcCrc) { + mSendBuf[10] = crc8(mSendBuf, 10); + sendPacket(invId, mSendBuf, 11); + } } bool checkCrc(uint8_t buf[], uint8_t *len, uint8_t *rptCnt) { @@ -126,6 +182,44 @@ class HmRadio { uint8_t pinIrq; private: + void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len) { + //Serial.println("sent packet: #" + String(mSendCnt)); + //dumpBuf("SEN ", buf, len); + + DISABLE_IRQ; + mNrf24.stopListening(); + + #ifdef CHANNEL_HOP + mSendChannel = getNxtChannel(); + #else + mSendChannel = getDefaultChannel(); + #endif + mNrf24.setChannel(mSendChannel); + //Serial.println("CH: " + String(mSendChannel)); + + mNrf24.openWritingPipe(invId); // TODO: deprecated + mNrf24.setCRCLength(RF24_CRC_16); + mNrf24.enableDynamicPayloads(); + mNrf24.setAutoAck(true); + mNrf24.setRetries(3, 15); + + mNrf24.write(buf, len); + + // Try to avoid zero payload acks (has no effect) + mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated + + mNrf24.setAutoAck(false); + mNrf24.setRetries(0, 0); + mNrf24.disableDynamicPayloads(); + mNrf24.setCRCLength(RF24_CRC_DISABLED); + + mNrf24.setChannel(DEFAULT_RECV_CHANNEL); + mNrf24.startListening(); + + RESTORE_IRQ; + mSendCnt++; + } + void calcDtuCrc(void) { uint64_t addr = DTU_RADIO_ID; uint8_t tmp[5]; @@ -136,11 +230,26 @@ class HmRadio { mDtuIdCrc = crc16nrf24(tmp, BIT_CNT(5)); } - uint8_t mSendChan[4]; + void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { + Serial.print(String(info)); + for(uint8_t i = 0; i < len; i++) { + Serial.print(buf[i], HEX); + Serial.print(" "); + } + Serial.println(); + } + + uint8_t mChanOut[4]; uint8_t mChanIdx; uint16_t mDtuIdCrc; uint16_t mLastCrc; uint8_t mRptCnt; + + RF24 mNrf24; + uint8_t mSendChannel; + BUFFER *mBufCtrl; + uint32_t mSendCnt; + uint8_t mSendBuf[MAX_RF_PAYLOAD_SIZE]; }; #endif /*__RADIO_H__*/ diff --git a/tools/esp8266/hmSystem.h b/tools/esp8266/hmSystem.h index bd573fc6..0f79ca9f 100644 --- a/tools/esp8266/hmSystem.h +++ b/tools/esp8266/hmSystem.h @@ -4,10 +4,6 @@ #include "hmInverters.h" #include "hmRadio.h" -typedef struct { - uint8_t sendCh; - uint8_t packet[MAX_RF_PAYLOAD_SIZE]; -} packet_t; template @@ -25,6 +21,10 @@ class HmSystem { // TODO: cleanup } + void setup() { + Radio.setup(&BufCtrl); + } + inverter_t *addInverter(const char *name, uint64_t serial, uint8_t type) { if(MAX_INVERTER <= mNumInv) { DPRINT("max number of inverters reached!"); diff --git a/tools/esp8266/html/conv.bat b/tools/esp8266/html/conv.bat deleted file mode 100644 index 0627beeb..00000000 --- a/tools/esp8266/html/conv.bat +++ /dev/null @@ -1,5 +0,0 @@ -..\tools\fileConv.exe index.html h\index_html.h index_html -..\tools\fileConv.exe setup.html h\setup_html.h setup_html -..\tools\fileConv.exe hoymiles.html h\hoymiles_html.h hoymiles_html -..\tools\fileConv.exe style.css h\style_css.h style_css -pause diff --git a/tools/esp8266/html/convert.py b/tools/esp8266/html/convert.py new file mode 100644 index 00000000..4b58681f --- /dev/null +++ b/tools/esp8266/html/convert.py @@ -0,0 +1,28 @@ +import re + +def convert2Header(inFile): + outName = "h/" + inFile.replace(".", "_") + ".h" + fileType = inFile.split(".")[1] + + f = open(inFile, "r") + data = f.read().replace('\n', '') + f.close() + if fileType == "html": + data = re.sub(r"\>\s+\<", '><', data) # whitespaces between xml tags + data = re.sub(r"(\;|\}|\>|\{)\s+", r'\1', data) # whitespaces inner javascript + data = re.sub(r"\"", '\\\"', data) # escape quotation marks + else: + data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css + + define = inFile.split(".")[0].upper() + f = open(outName, "w") + f.write("#ifndef __{}_H__\n".format(define)) + f.write("#define __{}_H__\n".format(define)) + f.write("const char {}[] PROGMEM = \"{}\";\n".format(inFile.replace(".", "_"), data)) + f.write("#endif /*__{}_H__*/\n".format(define)) + f.close() + +convert2Header("index.html") +convert2Header("setup.html") +convert2Header("hoymiles.html") +convert2Header("style.css") diff --git a/tools/esp8266/html/h/hoymiles_html.h b/tools/esp8266/html/h/hoymiles_html.h index 4bc89397..594e4b25 100644 --- a/tools/esp8266/html/h/hoymiles_html.h +++ b/tools/esp8266/html/h/hoymiles_html.h @@ -1 +1,4 @@ -String hoymiles_html = "Index - {DEVICE}

AHOY - {DEVICE}

Home

Every 10 seconds the values are updated

© 2022

AHOY :: {VERSION}

"; +#ifndef __HOYMILES_H__ +#define __HOYMILES_H__ +const char hoymiles_html[] PROGMEM = "Index - {DEVICE}

AHOY - {DEVICE}

Home

Every 10 seconds the values are updated

© 2022

AHOY :: {VERSION}

"; +#endif /*__HOYMILES_H__*/ diff --git a/tools/esp8266/html/h/index_html.h b/tools/esp8266/html/h/index_html.h index 8b638b0c..53eeda89 100644 --- a/tools/esp8266/html/h/index_html.h +++ b/tools/esp8266/html/h/index_html.h @@ -1 +1,4 @@ -String index_html = "Index - {DEVICE}

AHOY - {DEVICE}

Hoymiles
Update

Setup
Reboot

Uptime:

Time:

MQTT:

Statistics:

© 2022

AHOY :: {VERSION}

"; +#ifndef __INDEX_H__ +#define __INDEX_H__ +const char index_html[] PROGMEM = "Index - {DEVICE}

AHOY - {DEVICE}

Hoymiles
Update

Setup
Reboot

Uptime:

Time:

MQTT:

Statistics:

© 2022

AHOY :: {VERSION}

"; +#endif /*__INDEX_H__*/ diff --git a/tools/esp8266/html/h/setup_html.h b/tools/esp8266/html/h/setup_html.h index b905b56b..f4e99a1a 100644 --- a/tools/esp8266/html/h/setup_html.h +++ b/tools/esp8266/html/h/setup_html.h @@ -1 +1,4 @@ -String setup_html = "Setup - {DEVICE}

Setup

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

WiFi

Device Host Name

Inverter

{INVERTERS}

General

Pinout

{PINOUT}

MQTT

 

Home

Update Firmware

AHOY - {VERSION}

"; +#ifndef __SETUP_H__ +#define __SETUP_H__ +const char setup_html[] PROGMEM = "Setup - {DEVICE}

Setup

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

WiFi

Device Host Name

Inverter

{INVERTERS}

General

Pinout (Wemos)

{PINOUT}

MQTT

 

Home

Update Firmware

AHOY - {VERSION}

"; +#endif /*__SETUP_H__*/ diff --git a/tools/esp8266/html/h/style_css.h b/tools/esp8266/html/h/style_css.h index 6cc7fc67..cf8ac694 100644 --- a/tools/esp8266/html/h/style_css.h +++ b/tools/esp8266/html/h/style_css.h @@ -1 +1,4 @@ -String style_css = "h1 { margin: 0; padding: 20pt; font-size: 22pt; color: #fff; background-color: #006ec0; display: block; text-transform: uppercase; } html, body { font-family: Arial; margin: 0; padding: 0; } p { text-align: justify; font-size: 13pt; } .des { margin-top: 35px; font-size: 14pt; color: #006ec0; } .subdes { font-size: 13pt; color: #006ec0; margin-left: 7px; } .fw { width: 60px; display: block; float: left; } .color { width: 50px; height: 50px; border: 1px solid #ccc; } .range { width: 300px; } a:link, a:visited { text-decoration: none; font-size: 13pt; color: #006ec0; } a:hover, a:focus { color: #f00; } #content { padding: 15px 15px 60px 15px; } #footer { position: fixed; bottom: 0px; height: 45px; background-color: #006ec0; width: 100%; } #footer p { color: #fff; padding-left: 20px; padding-right: 20px; font-size: 10pt !important; } #footer a { color: #fff; } div.content { background-color: #fff; padding-bottom: 65px; overflow: hidden; } input, select { padding: 7px; font-size: 13pt; } input.text, select { width: 70%; box-sizing: border-box; margin-bottom: 10px; border: 1px solid #ccc; } input.btn { background-color: #006ec0; color: #fff; border: 0px; float: right; text-transform: uppercase; } input.cb { margin-bottom: 20px; } label { width: 20%; display: inline-block; font-size: 12pt; padding-right: 10px; margin-left: 10px; } .left { float: left; } .right { float: right; } div.ch { width: 250px; height: 410px; background-color: #006ec0; display: inline-block; margin-right: 20px; margin-bottom: 20px; } div.ch .value, div.ch .info, div.ch .head { color: #fff; display: block; width: 100%; text-align: center; } div.ch .unit { font-size: 19px; margin-left: 10px; } div.ch .value { margin-top: 20px; font-size: 30px; } div.ch .info { margin-top: 3px; font-size: 10px; } div.ch .head { background-color: #003c80; padding: 10px 0 10px 0; } "; +#ifndef __STYLE_H__ +#define __STYLE_H__ +const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:14pt;color:#006ec0;}.subdes {font-size:13pt;color:#006ec0;margin-left:7px;}.fw {width:60px;display:block;float:left;}.color {width:50px;height:50px;border:1px solid #ccc;}.range {width:300px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;}#footer p {color:#fff;padding-left:20px;padding-right:20px;font-size:10pt !important;}#footer a {color:#fff;}div.content {background-color:#fff;padding-bottom:65px;overflow:hidden;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch {width:250px;height:410px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;}div.ch .value, div.ch .info, div.ch .head {color:#fff;display:block;width:100%;text-align:center;}div.ch .unit {font-size:19px;margin-left:10px;}div.ch .value {margin-top:20px;font-size:30px;}div.ch .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}"; +#endif /*__STYLE_H__*/ diff --git a/tools/esp8266/main.cpp b/tools/esp8266/main.cpp index 2555188d..61b2075c 100644 --- a/tools/esp8266/main.cpp +++ b/tools/esp8266/main.cpp @@ -160,7 +160,7 @@ bool Main::setupStation(uint32_t timeout) { //----------------------------------------------------------------------------- void Main::showSetup(void) { - String html = setup_html; + String html = FPSTR(setup_html); html.replace("{SSID}", mStationSsid); // PWD will be left at the default value (for protection) // -> the PWD will only be changed if it does not match the default "{PWD}" @@ -173,7 +173,7 @@ void Main::showSetup(void) { //----------------------------------------------------------------------------- void Main::showCss(void) { - mWeb->send(200, "text/css", style_css); + mWeb->send(200, "text/css", FPSTR(style_css)); } diff --git a/tools/esp8266/tools/fileConv.exe b/tools/esp8266/tools/fileConv.exe deleted file mode 100644 index 6cd3e212ea1e5d0ccd0a9455a36b66d4d0fbab7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67584 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 frV+JK