WIP: Very rough first draft of Hoymiles library

This commit is contained in:
Thomas Basler 2022-05-30 21:50:08 +02:00
parent 0191edcc4b
commit 0d07e3e09f
10 changed files with 452 additions and 0 deletions

View File

@ -0,0 +1,79 @@
#include "Hoymiles.h"
#include "inverters/HM_4CH.h"
#include <Arduino.h>
#include <Every.h>
HoymilesClass Hoymiles;
void HoymilesClass::init()
{
_pollInterval = 0;
_radio.reset(new HoymilesRadio());
_radio->init();
}
void HoymilesClass::loop()
{
_radio->loop();
}
std::shared_ptr<InverterAbstract> HoymilesClass::addInverter(const char* name, uint64_t serial)
{
std::shared_ptr<InverterAbstract> i;
if (HM_4CH::isValidSerial(serial)) {
i = std::make_shared<HM_4CH>();
}
if (i) {
i->setSerial(serial);
i->setName(name);
_inverters.push_back(std::move(i));
return _inverters.back();
}
return nullptr;
}
std::shared_ptr<InverterAbstract> HoymilesClass::getInverterByPos(uint8_t pos)
{
if (pos > _inverters.size()) {
return nullptr;
} else {
return _inverters[pos];
}
}
std::shared_ptr<InverterAbstract> 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;
}

View File

@ -0,0 +1,34 @@
#pragma once
#include "HoymilesRadio.h"
#include "inverters/InverterAbstract.h"
#include "types.h"
#include <SPI.h>
#include <memory>
#include <vector>
class HoymilesClass {
public:
void init();
void loop();
std::shared_ptr<InverterAbstract> addInverter(const char* name, uint64_t serial);
std::shared_ptr<InverterAbstract> getInverterByPos(uint8_t pos);
std::shared_ptr<InverterAbstract> getInverterBySerial(uint64_t serial);
std::shared_ptr<InverterAbstract> 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<std::shared_ptr<InverterAbstract>> _inverters;
std::unique_ptr<HoymilesRadio> _radio;
uint32_t _pollInterval;
};
extern HoymilesClass Hoymiles;

View File

@ -0,0 +1,119 @@
#include "HoymilesRadio.h"
#include <Every.h>
#include <FunctionalInterrupt.h>
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;
}

View File

@ -0,0 +1,45 @@
#pragma once
#include "CircularBuffer.h"
#include "types.h"
#include <RF24.h>
#include <memory>
#include <nRF24L01.h>
// 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<RF24> _radio;
uint8_t _rxChLst[4] = { 3, 23, 61, 75 };
uint8_t _rxChIdx;
uint16_t _rxLoopCnt;
volatile bool _packetReceived;
CircularBuffer<packet_t, PACKET_BUFFER_SIZE> _rxBuffer;
serial_u _dtuSerial;
};

View File

@ -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;
}

View File

@ -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 }
};
};

View File

@ -0,0 +1,23 @@
#include "InverterAbstract.h"
#include <cstring>
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;
}

View File

@ -0,0 +1,75 @@
#pragma once
#include "types.h"
#include <Arduino.h>
#include <cstdint>
#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];
};

8
lib/Hoymiles/src/types.h Normal file
View File

@ -0,0 +1,8 @@
#pragma once
#include <cstdint>
union serial_u {
uint64_t u64;
uint8_t b[8];
};

View File

@ -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