diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp new file mode 100644 index 00000000..e372222e --- /dev/null +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -0,0 +1,79 @@ +#include "Hoymiles.h" +#include "inverters/HM_4CH.h" +#include +#include + +HoymilesClass Hoymiles; + +void HoymilesClass::init() +{ + _pollInterval = 0; + _radio.reset(new HoymilesRadio()); + _radio->init(); +} + +void HoymilesClass::loop() +{ + _radio->loop(); +} + +std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) +{ + std::shared_ptr i; + if (HM_4CH::isValidSerial(serial)) { + i = std::make_shared(); + } + + if (i) { + i->setSerial(serial); + i->setName(name); + _inverters.push_back(std::move(i)); + return _inverters.back(); + } + + return nullptr; +} + +std::shared_ptr HoymilesClass::getInverterByPos(uint8_t pos) +{ + if (pos > _inverters.size()) { + return nullptr; + } else { + return _inverters[pos]; + } +} + +std::shared_ptr HoymilesClass::getInverterBySerial(uint64_t serial) +{ + for (uint8_t i = 0; i < _inverters.size(); i++) { + if (_inverters[i]->serial() == serial) { + return _inverters[i]; + } + } + return nullptr; +} + +void HoymilesClass::removeInverterByPos(uint8_t pos) +{ + _inverters.erase(_inverters.begin() + pos); +} + +size_t HoymilesClass::getNumInverters() +{ + return _inverters.size(); +} + +HoymilesRadio* HoymilesClass::getRadio() +{ + return _radio.get(); +} + +uint32_t HoymilesClass::PollInterval() +{ + return _pollInterval; +} + +void HoymilesClass::setPollInterval(uint32_t interval) +{ + _pollInterval = interval; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h new file mode 100644 index 00000000..cba4ddd2 --- /dev/null +++ b/lib/Hoymiles/src/Hoymiles.h @@ -0,0 +1,34 @@ +#pragma once + +#include "HoymilesRadio.h" +#include "inverters/InverterAbstract.h" +#include "types.h" +#include +#include +#include + +class HoymilesClass { +public: + void init(); + void loop(); + + std::shared_ptr addInverter(const char* name, uint64_t serial); + std::shared_ptr getInverterByPos(uint8_t pos); + std::shared_ptr getInverterBySerial(uint64_t serial); + std::shared_ptr getInverterByTelegram(uint8_t buf[]); + void removeInverterByPos(uint8_t pos); + size_t getNumInverters(); + + HoymilesRadio* getRadio(); + + uint32_t PollInterval(); + void setPollInterval(uint32_t interval); + +private: + std::vector> _inverters; + std::unique_ptr _radio; + + uint32_t _pollInterval; +}; + +extern HoymilesClass Hoymiles; \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp new file mode 100644 index 00000000..e1357753 --- /dev/null +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -0,0 +1,119 @@ +#include "HoymilesRadio.h" +#include +#include + +void HoymilesRadio::init() +{ + _dtuSerial.u64 = 0; + + _radio.reset(new RF24(4, 5)); + _radio->begin(); + _radio->setDataRate(RF24_250KBPS); + _radio->enableDynamicPayloads(); + _radio->setCRCLength(RF24_CRC_16); + _radio->setAddressWidth(5); + _radio->setAutoAck(false); + _radio->setRetries(0, 0); + _radio->setPALevel(RF_PWR_LOW); + _radio->maskIRQ(true, true, false); // enable only receiving interrupts + if (_radio->isChipConnected()) { + Serial.println(F("Connection successfull")); + } else { + Serial.println(F("Connection error!!")); + } + + setDtuSerial(_dtuSerial.u64); + + attachInterrupt(digitalPinToInterrupt(16), std::bind(&HoymilesRadio::handleIntr, this), FALLING); + _radio->startListening(); +} + +void HoymilesRadio::loop() +{ + EVERY_N_MILLIS(4) + { + switchRxCh(1); + } + + // Irgendwie muss man hier die paket crc prüfen und ggf. einen retransmit anfordern + // ggf aber immer nur ein paket analysieren damit die loop schnell bleibt + + if (_packetReceived) { + Serial.println(F("Interrupt received")); + while (_radio->available()) { + if (!_rxBuffer.full()) { + packet_t* p; + uint8_t len; + p = _rxBuffer.getFront(); + memset(p->packet, 0xcc, MAX_RF_PAYLOAD_SIZE); + p->rxCh = _rxChLst[_rxChIdx]; + len = _radio->getPayloadSize(); + if (len > MAX_RF_PAYLOAD_SIZE) + len = MAX_RF_PAYLOAD_SIZE; + + _radio->read(p->packet, len); + _rxBuffer.pushFront(p); + } else { + Serial.println(F("Buffer full")); + _radio->flush_rx(); + } + } + + _packetReceived = false; + } +} + +void HoymilesRadio::setPALevel(rf24_pa_dbm_e paLevel) +{ + _radio->setPALevel(paLevel); +} + +serial_u HoymilesRadio::DtuSerial() +{ + return _dtuSerial; +} + +void HoymilesRadio::setDtuSerial(uint64_t serial) +{ + serial_u s; + _dtuSerial.u64 = serial; + s = convertSerialToRadioId(_dtuSerial); + _radio->openReadingPipe(1, s.u64); +} + +void ARDUINO_ISR_ATTR HoymilesRadio::handleIntr() +{ + _packetReceived = true; +} + +uint8_t HoymilesRadio::getRxNxtChannel() +{ + if (++_rxChIdx >= 4) + _rxChIdx = 0; + return _rxChLst[_rxChIdx]; +} + +bool HoymilesRadio::switchRxCh(uint8_t addLoop) +{ + _rxLoopCnt += addLoop; + if (_rxLoopCnt != 0) { + _rxLoopCnt--; + // portDISABLE_INTERRUPTS(); + _radio->stopListening(); + _radio->setChannel(getRxNxtChannel()); + _radio->startListening(); + // portENABLE_INTERRUPTS(); + } + return (0 == _rxLoopCnt); // receive finished +} + +serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) +{ + serial_u radioId; + radioId.b[4] = serial.b[0]; + radioId.b[3] = serial.b[1]; + radioId.b[2] = serial.b[2]; + radioId.b[1] = serial.b[3]; + radioId.b[0] = 0x01; + return radioId; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h new file mode 100644 index 00000000..9c646945 --- /dev/null +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -0,0 +1,45 @@ +#pragma once + +#include "CircularBuffer.h" +#include "types.h" +#include +#include +#include + +// maximum buffer length of packet received / sent to RF24 module +#define MAX_RF_PAYLOAD_SIZE 32 + +// number of packets hold in buffer +#define PACKET_BUFFER_SIZE 30 + +typedef struct { + uint8_t rxCh; + uint8_t packet[MAX_RF_PAYLOAD_SIZE]; +} packet_t; + +class HoymilesRadio { +public: + void init(); + void loop(); + void setPALevel(rf24_pa_dbm_e paLevel); + + serial_u DtuSerial(); + void setDtuSerial(uint64_t serial); + +private: + void ARDUINO_ISR_ATTR handleIntr(); + static serial_u convertSerialToRadioId(serial_u serial); + uint8_t getRxNxtChannel(); + bool switchRxCh(uint8_t addLoop = 0); + + std::unique_ptr _radio; + uint8_t _rxChLst[4] = { 3, 23, 61, 75 }; + uint8_t _rxChIdx; + uint16_t _rxLoopCnt; + + volatile bool _packetReceived; + + CircularBuffer _rxBuffer; + + serial_u _dtuSerial; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp new file mode 100644 index 00000000..4dd395d8 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -0,0 +1,16 @@ +#include "HM_4CH.h" + +bool HM_4CH::isValidSerial(uint64_t serial) +{ + return serial >= 116100000000 && serial <= 116199999999; +} + +String HM_4CH::typeName() +{ + return String(F("HM-1500")); +} + +const byteAssign_t* HM_4CH::getByteAssignment() +{ + return byteAssignment; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_4CH.h b/lib/Hoymiles/src/inverters/HM_4CH.h new file mode 100644 index 00000000..89de7116 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HM_4CH.h @@ -0,0 +1,52 @@ +#pragma once + +#include "InverterAbstract.h" + +class HM_4CH : public InverterAbstract { +public: + static bool isValidSerial(uint64_t serial); + String typeName(); + const byteAssign_t* getByteAssignment(); + +private: + const byteAssign_t byteAssignment[34] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 8, 2, 10 }, + { FLD_YD, UNIT_WH, CH1, 20, 2, 1 }, + { FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 }, + { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC }, + { FLD_IDC, UNIT_A, CH2, 6, 2, 100 }, + { FLD_PDC, UNIT_W, CH2, 10, 2, 10 }, + { FLD_YD, UNIT_WH, CH2, 22, 2, 1 }, + { FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 }, + { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH3, 24, 2, 10 }, + { FLD_IDC, UNIT_A, CH3, 26, 2, 100 }, + { FLD_PDC, UNIT_W, CH3, 30, 2, 10 }, + { FLD_YD, UNIT_WH, CH3, 42, 2, 1 }, + { FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 }, + { FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC }, + { FLD_IDC, UNIT_A, CH4, 28, 2, 100 }, + { FLD_PDC, UNIT_W, CH4, 32, 2, 10 }, + { FLD_YD, UNIT_WH, CH4, 44, 2, 1 }, + { FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 }, + { FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC }, + + { FLD_UAC, UNIT_V, CH0, 46, 2, 10 }, + { FLD_IAC, UNIT_A, CH0, 54, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 50, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 48, 2, 100 }, + { FLD_PCT, UNIT_PCT, CH0, 56, 2, 10 }, + { FLD_T, UNIT_C, CH0, 58, 2, 10 }, + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, + { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, + { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } + }; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp new file mode 100644 index 00000000..1278afc1 --- /dev/null +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -0,0 +1,23 @@ +#include "InverterAbstract.h" +#include + +void InverterAbstract::setSerial(uint64_t serial) +{ + _serial.u64 = serial; +} + +uint64_t InverterAbstract::serial() +{ + return _serial.u64; +} + +void InverterAbstract::setName(const char* name) +{ + uint8_t len = strlen(name); + strncpy(_name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len); +} + +const char* InverterAbstract::name() +{ + return _name; +} diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h new file mode 100644 index 00000000..e57f3d66 --- /dev/null +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -0,0 +1,75 @@ +#pragma once + +#include "types.h" +#include +#include + +#define MAX_NAME_LENGTH 16 + +// units +enum { UNIT_V = 0, + UNIT_A, + UNIT_W, + UNIT_WH, + UNIT_KWH, + UNIT_HZ, + UNIT_C, + UNIT_PCT }; +const char* const units[] = { "V", "A", "W", "Wh", "kWh", "Hz", "°C", "%" }; + +// field types +enum { FLD_UDC = 0, + FLD_IDC, + FLD_PDC, + FLD_YD, + FLD_YW, + FLD_YT, + FLD_UAC, + FLD_IAC, + FLD_PAC, + FLD_F, + FLD_T, + FLD_PCT, + FLD_EFF, + FLD_IRR }; +const char* const fields[] = { "U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", + "U_AC", "I_AC", "P_AC", "Freq", "Temp", "Pct", "Effiency", "Irradiation" }; + +// indices to calculation functions, defined in hmInverter.h +enum { CALC_YT_CH0 = 0, + CALC_YD_CH0, + CALC_UDC_CH, + CALC_PDC_CH0, + CALC_EFF_CH0, + CALC_IRR_CH }; +enum { CMD_CALC = 0xffff }; + +// CH0 is default channel (freq, ac, temp) +enum { CH0 = 0, + CH1, + CH2, + CH3, + CH4 }; + +typedef struct { + uint8_t fieldId; // field id + uint8_t unitId; // uint id + uint8_t ch; // channel 0 - 4 + uint8_t start; // pos of first byte in buffer + uint8_t num; // number of bytes in buffer + uint16_t div; // divisor / calc command +} byteAssign_t; + +class InverterAbstract { +public: + void setSerial(uint64_t serial); + uint64_t serial(); + void setName(const char* name); + const char* name(); + virtual String typeName() = 0; + virtual const byteAssign_t* getByteAssignment() = 0; + +private: + serial_u _serial; + char _name[MAX_NAME_LENGTH]; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/types.h b/lib/Hoymiles/src/types.h new file mode 100644 index 00000000..ee36a8c1 --- /dev/null +++ b/lib/Hoymiles/src/types.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +union serial_u { + uint64_t u64; + uint8_t b[8]; +}; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index d0442c8b..db2d053e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,6 +26,7 @@ lib_deps = https://github.com/me-no-dev/ESPAsyncWebServer.git bblanchon/ArduinoJson @ ^6.19.4 https://github.com/marvinroger/async-mqtt-client.git + nrf24/RF24 @ ^1.4.2 board = esp32dev board_build.partitions = partitions_custom.csv