diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 0ba6017..919db46 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -1,4 +1,6 @@ #include "Hoymiles.h" +#include "inverters/HM_1CH.h" +#include "inverters/HM_2CH.h" #include "inverters/HM_4CH.h" #include #include @@ -15,6 +17,33 @@ void HoymilesClass::init() void HoymilesClass::loop() { _radio->loop(); + + if (getNumInverters() > 0) { + EVERY_N_SECONDS(_pollInterval) + { + static uint8_t inverterPos = 0; + + std::shared_ptr iv = getInverterByPos(inverterPos); + if (iv != nullptr && _radio->isIdle()) { + Serial.print("Fetch inverter: "); + Serial.println(iv->serial()); + + iv->clearRxFragmentBuffer(); + + time_t now; + time(&now); + if (now > 0) { + _radio->sendTimePacket(iv, now); + } else { + Serial.println("Cancled. Time not yet synced."); + } + } + + if (++inverterPos >= getNumInverters()) { + inverterPos = 0; + } + } + } } std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) @@ -23,6 +52,12 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, u if (HM_4CH::isValidSerial(serial)) { i = std::make_shared(); } + else if (HM_2CH::isValidSerial(serial)) { + i = std::make_shared(); + } + else if (HM_1CH::isValidSerial(serial)) { + i = std::make_shared(); + } if (i) { i->setSerial(serial); @@ -78,7 +113,9 @@ std::shared_ptr HoymilesClass::getInverterByFragment(fragment_ void HoymilesClass::removeInverterByPos(uint8_t pos) { - _inverters.erase(_inverters.begin() + pos); + if (pos < _inverters.size()) { + _inverters.erase(_inverters.begin() + pos); + } } size_t HoymilesClass::getNumInverters() diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index c01d6ee..a54573a 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -35,9 +35,6 @@ void HoymilesRadio::loop() 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()) { @@ -45,7 +42,6 @@ void HoymilesRadio::loop() fragment_t* f; f = _rxBuffer.getFront(); memset(f->fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); - f->rxCh = _rxChLst[_rxChIdx]; f->len = _radio->getDynamicPayloadSize(); if (f->len > MAX_RF_PAYLOAD_SIZE) f->len = MAX_RF_PAYLOAD_SIZE; @@ -64,23 +60,56 @@ void HoymilesRadio::loop() if (!_rxBuffer.empty()) { fragment_t* f = _rxBuffer.getBack(); if (checkFragmentCrc(f)) { - Serial.println("Frame Ok"); std::shared_ptr inv = Hoymiles.getInverterByFragment(f); if (nullptr != inv) { - Serial.println("Found Inverter"); + // Save packet in inverter rx buffer + dumpBuf("RX ", f->fragment, f->len); + inv->addRxFragment(f->fragment, f->len); } else { - Serial.println("Inverter Not found!"); + Serial.println(F("Inverter Not found!")); } } else { - Serial.println("Frame kaputt"); + Serial.println(F("Frame kaputt")); } // Remove paket from buffer even it was corrupted _rxBuffer.popBack(); } } + + if (_busyFlag && _rxTimeout.occured()) { + Serial.println("Timeout"); + std::shared_ptr inv = Hoymiles.getInverterBySerial(_activeSerial.u64); + + if (nullptr != inv) { + uint8_t verifyResult = inv->verifyAllFragments(); + if (verifyResult == 255) { + Serial.println("Should Retransmit whole thing"); + // todo: irgendwas tun wenn garnichts ankam.... + _busyFlag = false; + + } else if (verifyResult == 254) { + Serial.println("Retransmit timeout"); + _busyFlag = false; + + } else if (verifyResult == 253) { + Serial.println("Packet CRC error"); + _busyFlag = false; + + } else if (verifyResult > 0) { + // Perform Retransmit + Serial.print(F("Request retransmit: ")); + Serial.println(verifyResult); + sendRetransmitPacket(verifyResult); + + } else { + // Successfull received all packages + _busyFlag = false; + } + } + } } void HoymilesRadio::setPALevel(rf24_pa_dbm_e paLevel) @@ -99,6 +128,11 @@ void HoymilesRadio::setDtuSerial(uint64_t serial) openReadingPipe(); } +bool HoymilesRadio::isIdle() +{ + return !_busyFlag; +} + void HoymilesRadio::openReadingPipe() { serial_u s; @@ -106,6 +140,13 @@ void HoymilesRadio::openReadingPipe() _radio->openReadingPipe(1, s.u64); } +void HoymilesRadio::openWritingPipe(serial_u serial) +{ + serial_u s; + s = convertSerialToRadioId(serial); + _radio->openWritingPipe(s.u64); +} + void ARDUINO_ISR_ATTR HoymilesRadio::handleIntr() { _packetReceived = true; @@ -118,6 +159,13 @@ uint8_t HoymilesRadio::getRxNxtChannel() return _rxChLst[_rxChIdx]; } +uint8_t HoymilesRadio::getTxNxtChannel() +{ + if (++_txChIdx >= 1) + _txChIdx = 0; + return _txChLst[_txChIdx]; +} + bool HoymilesRadio::switchRxCh(uint8_t addLoop) { _rxLoopCnt += addLoop; @@ -144,8 +192,99 @@ serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) return radioId; } +void HoymilesRadio::convertSerialToPacketId(uint8_t buffer[], serial_u serial) +{ + buffer[3] = serial.b[0]; + buffer[2] = serial.b[1]; + buffer[1] = serial.b[2]; + buffer[0] = serial.b[3]; +} + bool HoymilesRadio::checkFragmentCrc(fragment_t* fragment) { uint8_t crc = crc8(fragment->fragment, fragment->len - 1); return (crc == fragment->fragment[fragment->len - 1]); +} + +void HoymilesRadio::sendEsbPacket(serial_u target, uint8_t mainCmd, uint8_t subCmd, uint8_t payload[], uint8_t len, uint32_t timeout, bool resend) +{ + static uint8_t txBuffer[MAX_RF_PAYLOAD_SIZE]; + + if (!resend) { + memset(txBuffer, 0, MAX_RF_PAYLOAD_SIZE); + + txBuffer[0] = mainCmd; + convertSerialToPacketId(&txBuffer[1], target); // 4 byte long + convertSerialToPacketId(&txBuffer[5], DtuSerial()); // 4 byte long + txBuffer[9] = subCmd; + + memcpy(&txBuffer[10], payload, len); + txBuffer[10 + len] = crc8(txBuffer, 10 + len); + } + + _radio->stopListening(); + _radio->setChannel(getTxNxtChannel()); + openWritingPipe(target); + _radio->setRetries(3, 15); + + dumpBuf(NULL, txBuffer, 10 + len + 1); + _radio->write(txBuffer, 10 + len + 1); + + _radio->setRetries(0, 0); + openReadingPipe(); + _radio->setChannel(getRxNxtChannel()); + _radio->startListening(); + _busyFlag = true; + _rxTimeout.set(timeout); +} + +void HoymilesRadio::sendTimePacket(std::shared_ptr iv, time_t ts) +{ + uint8_t payload[16] = { 0 }; + + payload[0] = 0x0b; + payload[1] = 0x00; + u32CpyLittleEndian(&payload[2], ts); // sets the 4 following elements {2, 3, 4, 5} + payload[9] = 0x05; + + uint16_t crc = crc16(&payload[0], 14); + payload[14] = (crc >> 8) & 0xff; + payload[15] = (crc)&0xff; + + serial_u s; + s.u64 = iv->serial(); + _activeSerial.u64 = iv->serial(); + + sendEsbPacket(s, 0x15, 0x80, payload, 16, 60); +} + +void HoymilesRadio::sendRetransmitPacket(uint8_t fragment_id) +{ + sendEsbPacket(_activeSerial, 0x15, (uint8_t)(0x80 + fragment_id), 0, 0, 60); +} + +void HoymilesRadio::sendLastPacketAgain() +{ + sendEsbPacket(_activeSerial, 0, 0, 0, 0, 60, true); +} + +void HoymilesRadio::u32CpyLittleEndian(uint8_t dest[], uint32_t src) +{ + dest[0] = ((src >> 24) & 0xff); + dest[1] = ((src >> 16) & 0xff); + dest[2] = ((src >> 8) & 0xff); + dest[3] = ((src)&0xff); +} + +void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len) +{ + + if (NULL != info) + Serial.print(String(info)); + + for (uint8_t i = 0; i < len; i++) { + Serial.print(buf[i], 16); + Serial.print(" "); + } + Serial.println(""); } \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 63caf71..e18e744 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -1,22 +1,17 @@ #pragma once #include "CircularBuffer.h" +#include "TimeoutHelper.h" +#include "inverters/InverterAbstract.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 fragments hold in buffer #define FRAGMENT_BUFFER_SIZE 30 -typedef struct { - uint8_t rxCh; - uint8_t fragment[MAX_RF_PAYLOAD_SIZE]; - uint8_t len; -} fragment_t; + class HoymilesRadio { public: @@ -27,22 +22,40 @@ public: serial_u DtuSerial(); void setDtuSerial(uint64_t serial); + bool isIdle(); + void sendEsbPacket(serial_u target, uint8_t mainCmd, uint8_t subCmd, uint8_t payload[], uint8_t len, uint32_t timeout, bool resend = false); + void sendTimePacket(std::shared_ptr iv, time_t ts); + void sendRetransmitPacket(uint8_t fragment_id); + void sendLastPacketAgain(); + private: void ARDUINO_ISR_ATTR handleIntr(); static serial_u convertSerialToRadioId(serial_u serial); + static void convertSerialToPacketId(uint8_t buffer[], serial_u serial); uint8_t getRxNxtChannel(); + uint8_t getTxNxtChannel(); bool switchRxCh(uint8_t addLoop = 0); void openReadingPipe(); + void openWritingPipe(serial_u serial); bool checkFragmentCrc(fragment_t* fragment); + void dumpBuf(const char* info, uint8_t buf[], uint8_t len); + void u32CpyLittleEndian(uint8_t dest[], uint32_t src); std::unique_ptr _radio; uint8_t _rxChLst[4] = { 3, 23, 61, 75 }; uint8_t _rxChIdx; uint16_t _rxLoopCnt; + uint8_t _txChLst[1] = { 40 }; + uint8_t _txChIdx; + volatile bool _packetReceived; CircularBuffer _rxBuffer; + TimeoutHelper _rxTimeout; serial_u _dtuSerial; + serial_u _activeSerial; + + bool _busyFlag = false; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 98b033c..a67d08b 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -1,4 +1,5 @@ #include "InverterAbstract.h" +#include "crc.h" #include void InverterAbstract::setSerial(uint64_t serial) @@ -14,7 +15,11 @@ uint64_t InverterAbstract::serial() void InverterAbstract::setName(const char* name) { uint8_t len = strlen(name); - strncpy(_name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len); + if (len + 1 > MAX_NAME_LENGTH) { + len = MAX_NAME_LENGTH - 1; + } + strncpy(_name, name, len); + _name[len] = '\0'; } const char* InverterAbstract::name() @@ -24,5 +29,230 @@ const char* InverterAbstract::name() void InverterAbstract::clearRxFragmentBuffer() { + memset(_rxFragmentBuffer, 0, MAX_RF_FRAGMENT_COUNT * MAX_RF_PAYLOAD_SIZE); + _rxFragmentMaxPacketId = 0; + _rxFragmentLastPacketId = 0; + _rxFragmentRetransmitCnt = 0; +} +void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) +{ + uint8_t fragmentCount = fragment[9]; + if ((fragmentCount & 0b01111111) < MAX_RF_FRAGMENT_COUNT) { + // Packets with 0x81 will be seen as 1 + memcpy(_rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].fragment, &fragment[10], len - 11); + _rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].len = len - 11; + + if ((fragmentCount & 0b01111111) > _rxFragmentLastPacketId) { + _rxFragmentLastPacketId = fragmentCount & 0b01111111; + } + } + + // 0b10000000 == 0x80 + if ((fragmentCount & 0b10000000) == 0b10000000) { + _rxFragmentMaxPacketId = fragmentCount & 0b01111111; + } +} + +// Returns Zero on Success or the Fragment ID for retransmit or error code +uint8_t InverterAbstract::verifyAllFragments() +{ + // All missing + if (_rxFragmentLastPacketId == 0) { + Serial.println(F("All missing")); + return 255; + } + + // Last fragment is missing (thte one with 0x80) + if (_rxFragmentMaxPacketId == 0) { + Serial.println(F("Last missing")); + if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) { + return _rxFragmentLastPacketId + 1; + } else { + return 254; + } + } + + // Middle fragment is missing + for (uint8_t i = 0; i < _rxFragmentMaxPacketId - 1; i++) { + if (_rxFragmentBuffer[i].len == 0) { + Serial.println(F("Middle missing")); + if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) { + return i + 1; + } else { + return 254; + } + } + } + + // All fragments are available --> Check CRC + uint16_t crc = 0xffff, crcRcv; + + for (uint8_t i = 0; i < _rxFragmentMaxPacketId; i++) { + if (i == _rxFragmentMaxPacketId - 1) { + // Last packet + crc = crc16(_rxFragmentBuffer[i].fragment, _rxFragmentBuffer[i].len - 2, crc); + crcRcv = (_rxFragmentBuffer[i].fragment[_rxFragmentBuffer[i].len - 2] << 8) + | (_rxFragmentBuffer[i].fragment[_rxFragmentBuffer[i].len - 1]); + } else { + crc = crc16(_rxFragmentBuffer[i].fragment, _rxFragmentBuffer[i].len, crc); + } + } + + if (crc != crcRcv) { + return 253; + } + + // todo: hier muss noch ein check bzgl. packet type usw rein (ist ja nicht alles statistik) + // Move all fragments into target buffer + memset(_payloadStats, 0, MAX_RF_FRAGMENT_COUNT * MAX_RF_PAYLOAD_SIZE); + uint8_t offs = 0; + for (uint8_t i = 0; i < _rxFragmentMaxPacketId; i++) { + memcpy(&_payloadStats[offs], _rxFragmentBuffer[i].fragment, _rxFragmentBuffer[i].len); + offs += (_rxFragmentBuffer[i].len); + } + + return 0; +} + +uint8_t InverterAbstract::getChannelCount() +{ + const byteAssign_t* b = getByteAssignment(); + uint8_t cnt = 0; + for (uint8_t pos = 0; pos < sizeof(b) / sizeof(byteAssign_t); pos++) { + if (b[pos].ch > cnt) { + cnt = b[pos].ch; + } + } + + return cnt; +} + +uint16_t InverterAbstract::getChannelMaxPower(uint8_t channel) +{ + // todo; + return 0; +} + +uint8_t InverterAbstract::getAssignIdxByChannelField(uint8_t channel, uint8_t fieldId) +{ + const byteAssign_t* b = getByteAssignment(); + + uint8_t pos; + for (pos = 0; pos < sizeof(b) / sizeof(byteAssign_t); pos++) { + if (b[pos].ch == channel && b[pos].fieldId == fieldId) { + return pos; + } + } + return 0xff; +} + +float InverterAbstract::getValue(uint8_t channel, uint8_t fieldId) +{ + uint8_t pos = getAssignIdxByChannelField(channel, fieldId); + if (pos = 0xff) { + return 0; + } + + const byteAssign_t* b = getByteAssignment(); + + uint8_t ptr = b[pos].start; + uint8_t end = ptr + b[pos].num; + uint16_t div = b[pos].div; + + if (CMD_CALC != div) { + // Value is a static value + uint32_t val = 0; + do { + val <<= 8; + val |= _payloadStats[ptr]; + } while (++ptr != end); + + return (float)(val) / (float)(div); + } else { + // Value has to be calculated + return calcFunctions[b[pos].start].func(this, b[pos].num); + } + + return 0; +} + +bool InverterAbstract::hasValue(uint8_t channel, uint8_t fieldId) +{ + uint8_t pos = getAssignIdxByChannelField(channel, fieldId); + return pos != 0xff; +} + +const char* InverterAbstract::getUnit(uint8_t channel, uint8_t fieldId) +{ + uint8_t pos = getAssignIdxByChannelField(channel, fieldId); + const byteAssign_t* b = getByteAssignment(); + + return units[b[pos].unitId]; +} + +const char* InverterAbstract::getName(uint8_t channel, uint8_t fieldId) +{ + uint8_t pos = getAssignIdxByChannelField(channel, fieldId); + const byteAssign_t* b = getByteAssignment(); + + return fields[b[pos].fieldId]; +} + +static float calcYieldTotalCh0(InverterAbstract* iv, uint8_t arg0) +{ + float yield = 0; + for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { + yield += iv->getValue(i, FLD_YT); + } + return yield; +} + +static float calcYieldDayCh0(InverterAbstract* iv, uint8_t arg0) +{ + float yield = 0; + for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { + yield += iv->getValue(i, FLD_YD); + } + return yield; +} + +// arg0 = channel of source +static float calcUdcCh(InverterAbstract* iv, uint8_t arg0) +{ + return iv->getValue(arg0, FLD_UDC); +} + +static float calcPowerDcCh0(InverterAbstract* iv, uint8_t arg0) +{ + float dcPower = 0; + for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { + dcPower += iv->getValue(i, FLD_PDC); + } + return dcPower; +} + +// arg0 = channel +static float calcEffiencyCh0(InverterAbstract* iv, uint8_t arg0) +{ + float acPower = iv->getValue(CH0, FLD_PAC); + float dcPower = 0; + for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { + dcPower += iv->getValue(i, FLD_PDC); + } + if (dcPower > 0) { + return acPower / dcPower * 100.0f; + } + + return 0.0; +} + +// arg0 = channel +static float calcIrradiation(InverterAbstract* iv, uint8_t arg0) +{ + if (NULL != iv) { + if (iv->getChannelMaxPower(arg0) > 0) + return iv->getValue(arg0, FLD_PDC) / iv->getChannelMaxPower(arg0) * 100.0f; + } + return 0.0; } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index b45a776..1c8af3a 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -60,6 +60,35 @@ typedef struct { uint16_t div; // divisor / calc command } byteAssign_t; +#define MAX_RF_FRAGMENT_COUNT 5 +#define MAX_RETRANSMIT_COUNT 5 + +class InverterAbstract; + +// prototypes +static float calcYieldTotalCh0(InverterAbstract* iv, uint8_t arg0); +static float calcYieldDayCh0(InverterAbstract* iv, uint8_t arg0); +static float calcUdcCh(InverterAbstract* iv, uint8_t arg0); +static float calcPowerDcCh0(InverterAbstract* iv, uint8_t arg0); +static float calcEffiencyCh0(InverterAbstract* iv, uint8_t arg0); +static float calcIrradiation(InverterAbstract* iv, uint8_t arg0); + +using func_t = float(InverterAbstract*, uint8_t); + +struct calcFunc_t { + uint8_t funcId; // unique id + func_t* func; // function pointer +}; + +const calcFunc_t calcFunctions[] = { + { CALC_YT_CH0, &calcYieldTotalCh0 }, + { CALC_YD_CH0, &calcYieldDayCh0 }, + { CALC_UDC_CH, &calcUdcCh }, + { CALC_PDC_CH0, &calcPowerDcCh0 }, + { CALC_EFF_CH0, &calcEffiencyCh0 }, + { CALC_IRR_CH, &calcIrradiation } +}; + class InverterAbstract { public: void setSerial(uint64_t serial); @@ -68,9 +97,26 @@ public: const char* name(); virtual String typeName() = 0; virtual const byteAssign_t* getByteAssignment() = 0; + uint8_t getChannelCount(); + uint16_t getChannelMaxPower(uint8_t channel); + void clearRxFragmentBuffer(); + void addRxFragment(uint8_t fragment[], uint8_t len); + uint8_t verifyAllFragments(); + + uint8_t getAssignIdxByChannelField(uint8_t channel, uint8_t fieldId); + float getValue(uint8_t channel, uint8_t fieldId); + bool hasValue(uint8_t channel, uint8_t fieldId); + const char* getUnit(uint8_t channel, uint8_t fieldId); + const char* getName(uint8_t channel, uint8_t fieldId); private: serial_u _serial; char _name[MAX_NAME_LENGTH]; + fragment_t _rxFragmentBuffer[MAX_RF_FRAGMENT_COUNT]; + uint8_t _rxFragmentMaxPacketId = 0; + uint8_t _rxFragmentLastPacketId = 0; + uint8_t _rxFragmentRetransmitCnt = 0; + + uint8_t _payloadStats[MAX_RF_FRAGMENT_COUNT * MAX_RF_PAYLOAD_SIZE]; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/types.h b/lib/Hoymiles/src/types.h index ee36a8c..9e317a3 100644 --- a/lib/Hoymiles/src/types.h +++ b/lib/Hoymiles/src/types.h @@ -5,4 +5,12 @@ union serial_u { uint64_t u64; uint8_t b[8]; -}; \ No newline at end of file +}; + +// maximum buffer length of packet received / sent to RF24 module +#define MAX_RF_PAYLOAD_SIZE 32 + +typedef struct { + uint8_t fragment[MAX_RF_PAYLOAD_SIZE]; + uint8_t len; +} fragment_t; \ No newline at end of file