(Hoymiles) Datenlogger DTU

Aus TippvomTibb
Zur Navigation springen Zur Suche springen

Allgemeines

Hoymiles bietet fuer seine (Mikro-)verschiedene Datenlogger (DTU) an. Die Bezeichnungen der verschiedenen Varianten sind aber recht verwirrend, so dass ich hier versuche ein wenig Ordnung in die Auswahl zu bekommen.

Datenlogger Varianten

Das nachgestellte S ist die Unterscheidung zwischen den Sub-1G (Texas Instruments C1310-C1312) und den 2,4 GHz Nordic Varianten. Die 2,4 GHz Nordic sind fuer die HM und MI Wechselrichterserie und die Sub-1G fuer die HMS und HMT Serie. Die MI-Serie hat eine innen liegende Antenne, die HM-Serie hat einen kleinen Stummel und beide habe einen "Bluetooth"-Chip der Firma Nordic verbaut. Die HMT sind die (neuen) dreiphasigen Wechselrichter mit einem neuen 3-pahsen Stecker und sind nicht mehr daisyChained, genau wie die HMS-Serie zwar einphasig ist aber auch mit nur einem Anschluss. Beide haben das neue Kommunikationssytem von Texas Instruments Sub-1g verbaut und meint eigentlich nur Frequenz unter (sub) 1 GHz , also 433 MHz, 868 MHz und 915 MHz. Welche Frequenz Hoymiles nutzt habe ich noch nicht getestet.

Fuer die Nordic-Serie gibt es schon ein paar Informationen auf GitHub.

DTU-MI

DTU-MI total front.jpg
DTU-MI total back.jpg
DTU-MI PCB bottom.jpg
DTU-MI PCB top.jpg

Der Prozessor ist ein ARM STM32F207IGT6. Die Komminkation ermöglichen ein AMSC 8720A (Ethernet), ein Shinwa (Nordic) 2,4 GHz Modul und ein Sipex 3232EE (RS232).

DTU-Pro

DTU-W100

DTU-Lite 4G

DTU-Lite

DTU-WLite

DTU-Lite-S

DTU-Lite-S SE

DTU-Pro-S

DTU-WLite-S

Protokolle

TODO

Cloudfree

TODO

https://pypi.org/project/hoymilesdtumi/

Github/HoyDtuSim

Mit Hilfe dieses kleinen Codebrockens kann man scheinbar die Hoymiles-DTUs ersetzen. Die Software wird auf einen ES32, ESP8266 oder ... aufgespielt, ein nRF24L01 drangesteckt (SPI) und los geht's. Die Infobasis des/der Kommunikationsprotokoll(e) liegt hier. Für andere "Plattformen" gibt's hier noch mehr Material. Habe mal einen kurzen Blick in den Code geworfen.

HoyDtuSim.ino
#include <Arduino.h>
#include <SPI.h>
#include "CircularBuffer.h"
#include <RF24.h>
#include "printf.h"
#include "hm_crc.h"
#include "hm_packets.h"

#include "Settings.h"     // Header für Einstellungen
#include "Debug.h"
#include "Inverters.h"

const char VERSION[] PROGMEM = "0.4.1";


#ifdef ESP8266
  #define DISABLE_EINT noInterrupts()
  #define ENABLE_EINT  interrupts()
#else     // für AVR z.B. ProMini oder Nano
  #define DISABLE_EINT EIMSK = 0x00
  #define ENABLE_EINT EIMSK = 0x01
#endif


#ifdef ESP8266
#define PACKET_BUFFER_SIZE      (30) 
#else
#define PACKET_BUFFER_SIZE      (10) 
#endif

// Startup defaults until user reconfigures it
//#define DEFAULT_RECV_CHANNEL    (3)             // 3 = Default channel for Hoymiles
//#define DEFAULT_SEND_CHANNEL  (75)            // 40 = Default channel for Hoymiles, 61

static HM_Packets     hmPackets;
static uint32_t       tickMillis;

// Set up nRF24L01 radio on SPI bus plus CE/CS pins
// If more than one RF24 unit is used the another CS pin than 10 must be used
// This pin is used hard coded in SPI library
static RF24 Radio (RF1_CE_PIN, RF1_CS_PIN);

static NRF24_packet_t bufferData[PACKET_BUFFER_SIZE];

static CircularBuffer<NRF24_packet_t> packetBuffer(bufferData, sizeof(bufferData) / sizeof(bufferData[0]));

static Serial_header_t SerialHdr;

#define CHECKCRC  1
static uint16_t lastCRC;
static uint16_t crc;

uint8_t         channels[]            = {3, 23, 40, 61, 75};   //{1, 3, 6, 9, 11, 23, 40, 61, 75}
uint8_t         channelIdx            = 1;                         // fange mit 40 an
uint8_t         DEFAULT_SEND_CHANNEL  = channels[channelIdx];      // = 40

#if USE_POOR_MAN_CHANNEL_HOPPING_RCV
uint8_t         rcvChannelIdx         = 0; 
//uint8_t         rcvChannels[]         = {3, 23, 40, 61, 75};   //{1, 3, 6, 9, 11, 23, 40, 61, 75}
#define rcvChannels channels
uint8_t         DEFAULT_RECV_CHANNEL  = rcvChannels[rcvChannelIdx];      //3;
uint8_t         intvl = 4;          // Zeit für poor man hopping
int             hophop;
#else
uint8_t         DEFAULT_RECV_CHANNEL  = 3;
#endif

boolean         valueChanged          = false;

uint8_t         aktWR                 = 0x00;                    


#define RESET_VALUES_AFTER_TIME_NO_PAKET 1000UL*60*10
// wenn 10 Minuten keine Antwort mehr von WR, dann Werte auf 0 setzen

static unsigned long timeLastPacket = millis();
static unsigned long timeLastIstTagCheck =  timeLastPacket;
static unsigned long timeLastRcvChannelSwitch = timeLastPacket;
static unsigned long timeLastHoyOnCheck = timeLastPacket;

static const char BLANK = ' ';
static boolean istTag = true;


// Function forward declaration
static void SendPacket(uint64_t dest, uint8_t *buf, uint8_t len);
void shiftPayload (NRF24_packet_t *p);
void outputPacket(NRF24_packet_t *p, uint8_t payloadLen);


#ifdef ESP8266
  #include "wifi.h"
  #include "ModWebserver.h"
  #include "Sonne.h"
#endif


inline static void dumpData(uint8_t *p, int len) {
//-----------------------------------------------
  while (len > 0){
    if (*p < 16)
      DEBUG_OUT.print(F("0"));
    DEBUG_OUT.print(*p++, HEX);
    len--;
  }
  DEBUG_OUT.print(BLANK);
}

uint32_t extractInt (uint8_t *p, uint8_t bytes) {
  uint32_t val = 0;
  do {
      val <<= 8;
      val |= *p++;
  } while(--bytes);
  return val;  
}

float extractValue (uint8_t *p, uint8_t bytes, uint16_t divisor) {
//--------------------------------------------------------------  
  volatile uint32_t val = 0;
  /*
  do {
      val <<= 8;
      val |= *p++;
  } while(--bytes);
  */
  val = extractInt (p, bytes);
  return (float)val / (float) divisor;
}

void analyseWords (uint8_t *p) {    // p zeigt auf 01 hinter 2. WR-Adr
//----------------------------------
  //uint16_t val;
  DEBUG_OUT.print (F("analyse words:"));
  p++;
  for (int i = 0; i <12;i++) {
    DEBUG_OUT.print(extractValue(p,2,1));
    DEBUG_OUT.print(BLANK);
    p++;
  }
  DEBUG_OUT.println();
}


void outChannel (uint8_t wr, uint8_t i) {
//------------------------------------
  DEBUG_OUT.print(getMeasureName(wr, i)); 
  DEBUG_OUT.print(F("\t:")); 
  DEBUG_OUT.print(getMeasureValue(wr,i)); 
  DEBUG_OUT.println(BLANK);  
}


void analyse (NRF24_packet_t *p) {
//------------------------------
  uint8_t wrIdx = findInverter (&p->packet[3]);
  //DEBUG_OUT.print ("wrIdx="); DEBUG_OUT.println (wrIdx);
  if (wrIdx == 0xFF || wrIdx != aktWR) {
    DEBUG_OUT.print(F("unbek. wrId=")); DEBUG_OUT.println(wrIdx);
    return;
  }
    
  uint8_t subcmd = p->packet[11];
  uint8_t response = p->packet[2];
  float val = 0;

  if (response == HOY_ANSWER_DATA) {
    if (subcmd < inverters[wrIdx].fragmentCount || subcmd == (0x80 + inverters[wrIdx].fragmentCount)) {
      const measureDef_t *defs = inverters[wrIdx].measureDef;
      for (uint8_t i = 0; i < inverters[wrIdx].anzMeasures; i++) {
        if (defs[i].teleId == subcmd) {
          uint8_t pos     = defs[i].pos;
          uint8_t bytes   = defs[i].bytes;
          uint8_t frlIdx  = (subcmd & 0x7F)-1;               //(cmd > 0x80 ? cmd - 0x80 : cmd) -  1;
          if (pos + bytes <= 12 + inverters[wrIdx].fragmentLen[frlIdx])
            val = extractValue (&p->packet[pos], bytes, getDivisor(wrIdx, i) );
          else {
            // gesplitteter Wert
            val = inverters[wrIdx].values[i];   // damit Wert bleibt, wenn nicht berechnet werden kann
            NRF24_packet_t *x;
            // suche Daten von cmd+1 
            uint8_t fragmentCount = inverters[wrIdx].fragmentCount;
            uint8_t suchCmd = (subcmd == fragmentCount-1 ? 0x80 + fragmentCount : subcmd + 1);
            for (uint8_t b = 0; b < PACKET_BUFFER_SIZE; b++) {
              x = &bufferData[b]; 
              if (x->timestamp) {
                if (x->packet[11] == suchCmd) {
                  uint32_t val1 = extractInt (&p->packet[pos], 2);    // (p->packet[pos] << 8) | p->packet[pos+1];
                  uint32_t val2 = extractInt (&x->packet[12], 2);     // (x->packet[12] << 8) | x->packet[13];
                  val1 = (val1 <<16) + val2;
                  val = (float)(val1) / (float)getDivisor(wrIdx, i);
                } // if
              } // if
            } // for
          }
          valueChanged = valueChanged || (val != inverters[wrIdx].values[i]);
          inverters[wrIdx].values[i] = val;
        }      
      }
      // calculated funstions
      for (uint8_t i = 0; i < inverters[wrIdx].anzMeasureCalculated; i++) {
        val = inverters[wrIdx].measureCalculated[i].f (wrIdx);   //(inverters[wrIdx].values);
        int idx = inverters[wrIdx].anzMeasures + i;
        valueChanged = valueChanged ||(val != inverters[wrIdx].values[idx]);
        inverters[wrIdx].values[idx] = val;
      }
    }
  } else if (response == HOY_ANSWER_BROADCAST) {
    DEBUG_OUT.println("+++++++++++++++");
  }
  if (p->packetsLost > 0) {
    DEBUG_OUT.print   (F(" Lost: "));
    DEBUG_OUT.println (p->packetsLost);
  }
}

#ifdef ESP8266
IRAM_ATTR
#endif
void handleNrf1Irq() {
//-------------------------
  static uint8_t lostPacketCount = 0;
  
  DISABLE_EINT;
  
  // Loop until RX buffer(s) contain no more packets.
  while (Radio.available()) {
    if (!packetBuffer.full()) {
      NRF24_packet_t *p = packetBuffer.getFront();
      p->timestamp   = micros(); // Micros does not increase in interrupt, but it can be used.
      p->packetsLost = lostPacketCount;
      p->rcvChannel  = DEFAULT_RECV_CHANNEL;
      uint8_t packetLen = Radio.getPayloadSize();
      if (packetLen > MAX_RF_PAYLOAD_SIZE)
        packetLen = MAX_RF_PAYLOAD_SIZE;

      Radio.read(p->packet, packetLen);
      shiftPayload(p);
      uint8_t wrIdx = findInverter (&p->packet[3]);
      if (wrIdx == aktWR)
        packetBuffer.pushFront(p);
      lostPacketCount = 0;
    }
    else {
      // Buffer full. Increase lost packet counter.
      bool tx_ok, tx_fail, rx_ready;
      if (lostPacketCount < 255)
        lostPacketCount++;
      // Call 'whatHappened' to reset interrupt status.
      Radio.whatHappened(tx_ok, tx_fail, rx_ready);
      // Flush buffer to drop the packet.
      Radio.flush_rx();
    }
  }
  ENABLE_EINT;
}


static void activateConf(void) {
//-----------------------------
  Radio.begin();
  // Disable shockburst for receiving and decode payload manually
  Radio.setAutoAck(false);
  Radio.setRetries(0, 0);
  Radio.setChannel(DEFAULT_RECV_CHANNEL);
  Radio.setDataRate(DEFAULT_RF_DATARATE);
  Radio.disableCRC();
  Radio.setAutoAck(0x00);
  Radio.setPayloadSize(MAX_RF_PAYLOAD_SIZE);
  Radio.setAddressWidth(5);
  Radio.openReadingPipe(1, DTU_RADIO_ID);

  // We want only RX irqs
  Radio.maskIRQ(true, true, false);

  // Use lo PA level, as a higher level will disturb CH340 DEBUG_OUT usb adapter
  Radio.setPALevel (PA_LEVEL);
  Radio.startListening();

  // Attach interrupt handler to NRF IRQ output. Overwrites any earlier handler.
  attachInterrupt(digitalPinToInterrupt(RF1_IRQ_PIN), handleNrf1Irq, FALLING); // NRF24 Irq pin is active low.

  // Initialize SerialHdr header's address member to promiscuous address.
  uint64_t addr = DTU_RADIO_ID;
  for (int8_t i = sizeof(SerialHdr.address) - 1; i >= 0; --i) {
    SerialHdr.address[i] = addr;
    addr >>= 8;
  }

  //Radio.printDetails();
  //DEBUG_OUT.println();
  tickMillis = millis() + 200;
}

#define resetRF24() activateConf()


void setup(void) {
//--------------
  #ifndef DEBUG
  #ifndef ESP8266
  Serial.begin(SER_BAUDRATE);
  #endif
  #endif
  printf_begin();
  DEBUG_OUT.begin(SER_BAUDRATE);
  DEBUG_OUT.flush();

  DEBUG_OUT.println(F("-- Hoymiles DTU Simulation --"));

  // Configure nRF IRQ input
  pinMode(RF1_IRQ_PIN, INPUT);

  activateConf();

#ifdef ESP8266
  setupWifi();
  setupClock();
  setupWebServer();
  setupUpdateByOTA();
  calcSunUpDown (getNow());
  istTag = isDayTime();
  DEBUG_OUT.print (F("Es ist ")); DEBUG_OUT.println (istTag?F("Tag"):F("Nacht"));
  hmPackets.SetUnixTimeStamp (getNow());
#else
  hmPackets.SetUnixTimeStamp(0x62456430);
#endif

  setupInverters();
}

uint8_t sendBuf[MAX_RF_PAYLOAD_SIZE];
uint8_t lastRequest = 0;

void sendRequest (uint8_t wr, uint8_t MID, uint8_t subcmd) {
//--------------------------------------------------------
  int32_t size = 0;
  lastRequest = MID;
  uint64_t dest = inverters[wr].RadioId;
  if (MID == HOY_REQUEST_DATA && subcmd == 0) {
    #ifdef ESP8266
    hmPackets.SetUnixTimeStamp (getNow());
    #endif
    size = hmPackets.GetTimePacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8);
    //DEBUG_OUT.print ("Timepacket mit cid="); DEBUG_OUT.println(sendBuf[10], HEX);
  }
  else
    size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, MID,  subcmd);
  SendPacket (dest, (uint8_t *)&sendBuf, size);
}

static uint8_t requestSend = 0;

void isTime2Send () {
//-----------------
  // Second timer
  static const uint8_t warteZeit = WAIT_TILL_NEXT_SEND;   // 10
  static uint8_t tickSec = 0;
  if (millis() >= tickMillis) {
    static uint8_t tel = 0;
    tickMillis += warteZeit*1000;    //200;
    tickSec++; 
   
    if (++tickSec >= 1) {   // 5
      for (uint8_t c=0; c < warteZeit; c++) hmPackets.UnixTimeStampTick();
      tickSec = 0;
    } 

#if MAX_ANZ_INV > 1  
    aktWR++;
    if (aktWR >= anzInv) 
      aktWR = 0;
    #if TEST_MULTI
    for (uint8_t i = 0; i < anzInv; i++) {
      setSerialNo (i, 0x114172600000ULL );
    }
    if (aktWR == 0) setSerialNo (0, WR1_SERIAL); 
    if (aktWR == 1) setSerialNo (1, WR2_SERIAL); 
    #if MAX_ANZ_INV > 2
    if (aktWR == 2) setSerialNo (3, WR3_SERIAL); 
    #endif
    #endif
#endif
    
    DEBUG_OUT.print(aktWR); DEBUG_OUT.print(BLANK);
    sendRequest (aktWR, HOY_REQUEST_DATA, 0);
    requestSend++;  
    
    packetBuffer.clear();
    memset (bufferData, 0, sizeof(bufferData));
    //tel++;
  }
}


void outputPacket(NRF24_packet_t *p, uint8_t payloadLen) {
//-----------------------------------------------------
    // Write timestamp, packets lost, address and payload length
    //printf(" %09lu ", SerialHdr.timestamp);
    char _buf[20];
    sprintf_P(_buf, PSTR("rcv CH:%d "), p->rcvChannel);
    DEBUG_OUT.print (_buf);
    dumpData((uint8_t *)&SerialHdr.packetsLost, sizeof(SerialHdr.packetsLost));
    dumpData((uint8_t *)&SerialHdr.address, sizeof(SerialHdr.address));

    // Trailing bit?!?
    dumpData(&p->packet[0], 2);

    // Payload length from PCF
    dumpData(&payloadLen, sizeof(payloadLen));

    // Packet control field - PID Packet identification
    uint8_t val = (p->packet[1] >> 1) & 0x03;
    DEBUG_OUT.print(val);
    DEBUG_OUT.print(F("  "));

    if (payloadLen > 9) {
      dumpData(&p->packet[2], 1);
      dumpData(&p->packet[3], 4);
      dumpData(&p->packet[7], 4);
      
      uint16_t remain = payloadLen - 2 - 1 - 4 - 4 + 4;

      if (remain < 32) {
        dumpData(&p->packet[11], remain);
        printf_P(PSTR("%04X "), crc);

        if (((crc >> 8) != p->packet[payloadLen + 2]) || ((crc & 0xFF) != p->packet[payloadLen + 3]))
          DEBUG_OUT.print(0);
        else
          DEBUG_OUT.print(1);
      }
      else {
        DEBUG_OUT.print(F("Ill remain "));
        DEBUG_OUT.print(remain);
      }
    }
    else {
      dumpData(&p->packet[2], payloadLen + 2);
      printf_P(PSTR("%04X "), crc);
    }
    DEBUG_OUT.println(); 
    DEBUG_OUT.flush();
}


void writeArduinoInterface() {
//--------------------------
  if (valueChanged) {
    //for (uint8_t wr = 0; wr < anzInv; wr++) {
    {  uint8_t wr = aktWR;
      for (uint8_t i = 0; i < inverters[wr].anzTotalMeasures; i++) {
        if (anzInv > 1) {
          Serial.print(wr); Serial.print('.');
        }
        Serial.print(getMeasureName(wr,i));    // Schnittstelle bei Arduino
        Serial.print('='); 
        Serial.print(getMeasureValue(wr,i), getDigits(wr,i));   // Schnittstelle bei Arduino
        Serial.print (BLANK);
        Serial.println (getUnit(wr, i));
      }  // for i
      
    }  // for wr
    Serial.println(F("-----------------------"));
    valueChanged = false;
  }
}


boolean doCheckCrc (NRF24_packet_t *p, uint8_t payloadLen) {
//--------------------------------------------------------
  crc = 0xFFFF;
  crc = crc16((uint8_t *)&SerialHdr.address, sizeof(SerialHdr.address), crc, 0, BYTES_TO_BITS(sizeof(SerialHdr.address)));
  // Payload length
  // Add one byte and one bit for 9-bit packet control field
  crc = crc16((uint8_t *)&p->packet[0], sizeof(p->packet), crc, 7, BYTES_TO_BITS(payloadLen + 1) + 1);
  
  if (CHECKCRC) {
    // If CRC is invalid only show lost packets
    if (((crc >> 8) != p->packet[payloadLen + 2]) || ((crc & 0xFF) != p->packet[payloadLen + 3])) {
      if (p->packetsLost > 0) {
        DEBUG_OUT.print(F(" Lost: "));
        DEBUG_OUT.println(p->packetsLost);
      }
      packetBuffer.popBack();
      return false;
    }
  
    // Dump a decoded packet only once
    if (lastCRC == crc) {
      packetBuffer.popBack();
      return false;
    }
    lastCRC = crc;
  }
  
  // Don't dump mysterious ack packages
  if (payloadLen == 0) {
      packetBuffer.popBack();
      return false;
  }
  return true;
}


#if USE_POOR_MAN_CHANNEL_HOPPING_RCV
inline void poorManChannelHopping() {
//--------------------------
  if (hophop <= 0) return;
  if (millis() >= timeLastRcvChannelSwitch + intvl) {
    rcvChannelIdx--;                      // abwärts
    if (rcvChannelIdx >= sizeof(rcvChannels))
      rcvChannelIdx = sizeof(rcvChannels)-1;        // 0;
    DEFAULT_RECV_CHANNEL  = rcvChannels[rcvChannelIdx]; 
    DISABLE_EINT;
    Radio.stopListening();
    Radio.setChannel (DEFAULT_RECV_CHANNEL);
    Radio.startListening();
    ENABLE_EINT;      
    timeLastRcvChannelSwitch = millis();
    hophop--;
  }
}
#endif

inline void checkRF24isWorking() {
//------------------------------
  if (millis()  > timeLastPacket + 5*60000UL) { // 5 Minuten nichts mehr empfangen
    DEBUG_OUT.println (F("Reset RF24"));
    resetRF24();
    timeLastPacket = millis(); 
  }
}


inline void checkHoymilesIsOn() {
//-----------------------------
  if ( packetBuffer.empty() && millis() > timeLastHoyOnCheck + RESET_VALUES_AFTER_TIME_NO_PAKET) {
    for (uint8_t wr = 0; wr < anzInv; wr++) {
      memset (inverters[wr].values, 0, sizeof(inverters[wr].values));   
    }
    timeLastHoyOnCheck = millis();
  }
}


void shiftPayload (NRF24_packet_t *p) {
//----------------------------------- Shift payload data due to 9-bit packet control field
  for (int8_t j = sizeof(p->packet) - 1; j >= 0; j--) {
    if (j > 0)
      p->packet[j] = (byte)(p->packet[j] >> 7) | (byte)(p->packet[j - 1] << 1);
    else
      p->packet[j] = (byte)(p->packet[j] >> 7);
  }
}

static boolean retryMode = false;

boolean packetsComplete() {
//------------------------
// checks that all cmd are recvd; if not sends request for missing cmd
  if (retryMode && hophop <= 0) retryMode = false;
  if (retryMode) return false;
  boolean is[10] = {};    // 0 nicht besetzt
  uint8_t cmd, i;
  uint8_t wr = aktWR;     // TODO
  NRF24_packet_t *x;
  uint8_t fc = inverters[wr].fragmentCount;
  for (uint8_t b = 0; b < PACKET_BUFFER_SIZE; b++) {
    x = &bufferData[b]; 
    if (x->timestamp) {
       cmd = x->packet[11];
       i = (cmd & 0x7F);
       is[i] = true;
    }
  }
  for (i = 1; i <= fc; i++) {
    if (!is[i]) {
      DEBUG_OUT.print(F("Request cmd=")); DEBUG_OUT.println(i);
      sendRequest (aktWR, HOY_REQUEST_DATA, 0x80 + i); 
      retryMode = true;
      return false;
    }
  }
  return true;
}

void loop(void) {
//=============
  // poor man channel hopping on receive
#if USE_POOR_MAN_CHANNEL_HOPPING_RCV
  poorManChannelHopping();
#endif

  checkRF24isWorking();
  
  checkHoymilesIsOn();

  if ((packetBuffer.available() >= inverters[aktWR].fragmentCount && packetsComplete())
      // || (packetBuffer.available() && lastRequest == HOY_BROADCAST )
     ) {
    while (!packetBuffer.empty()) {
      timeLastPacket = millis();
      // One or more records present
      NRF24_packet_t *p = packetBuffer.getBack();
      // Shift payload data due to 9-bit packet control field
      //shiftPayload (p);
      
      SerialHdr.timestamp   = p->timestamp;
      SerialHdr.packetsLost = p->packetsLost;
  
      uint8_t payloadLen = ((p->packet[0] & 0x01) << 5) | (p->packet[1] >> 3);
      // Check CRC
      if (! doCheckCrc(p, payloadLen) )
        continue;
  
      #ifdef DEBUG
      //uint8_t cmd = p->packet[11];
      //if (cmd != 0x01 && cmd != 0x02 && cmd != 0x83 && cmd != 0x81)
        outputPacket (p, payloadLen);
      #endif
  
      analyse (p);
  
      // Remove record as we're done with it.
      packetBuffer.popBack();
    }
    memset (bufferData, 0, sizeof(bufferData));
  }
  #ifndef ESP8266
  writeArduinoInterface();
  #endif

  if (istTag) 
    isTime2Send();

  #ifdef ESP8266
  checkWifi();
#if USE_POOR_MAN_CHANNEL_HOPPING_RCV
  poorManChannelHopping();
#endif
  webserverHandle();       
#if USE_POOR_MAN_CHANNEL_HOPPING_RCV
  poorManChannelHopping();
#endif
  checkUpdateByOTA();
#if USE_POOR_MAN_CHANNEL_HOPPING_RCV
  poorManChannelHopping();
#endif
  if (hour() == 0 && minute() == 0) {
    calcSunUpDown(getNow());  
    delay (60*1000);
  }

  if (millis() > timeLastIstTagCheck + 15UL * 60UL * 1000UL) {   // alle 15 Minuten neu berechnen ob noch hell
    istTag = isDayTime();
    DEBUG_OUT.print (F("Es ist ")); DEBUG_OUT.println (istTag?F("Tag"):F("Nacht"));
    timeLastIstTagCheck = millis();
  }
  #endif
/*
  if (millis() > timeLastPacket + 60UL*SECOND) {  // 60 Sekunden
    channelIdx++;
    if (channelIdx >= sizeof(channels)) channelIdx = 0;
    DEFAULT_SEND_CHANNEL = channels[channelIdx];
    DEBUG_OUT.print (F("\nneuer DEFAULT_SEND_CHANNEL: ")); DEBUG_OUT.println(DEFAULT_SEND_CHANNEL);
    timeLastPacket = millis();
  }
*/
}


static void SendPacket(uint64_t dest, uint8_t *buf, uint8_t len) {
//--------------------------------------------------------------
  //DEBUG_OUT.print (F("Sende: ")); DEBUG_OUT.println (buf[9],  HEX);
  //dumpData (buf, len); DEBUG_OUT.println();
  DISABLE_EINT;
  Radio.stopListening();

#ifdef CHANNEL_HOP
  static uint8_t hop = 0;
  DEFAULT_SEND_CHANNEL = channels[hop++];
  Radio.setChannel(DEFAULT_SEND_CHANNEL);
  if (hop >= sizeof(channels) / sizeof(channels[0]))
    hop = 0;
#else
  Radio.setChannel(DEFAULT_SEND_CHANNEL);
#endif
#if DEBUG_SEND    
  DEBUG_OUT.print(F("Send... CH"));
  DEBUG_OUT.println(DEFAULT_SEND_CHANNEL);
#endif  

  Radio.openWritingPipe(dest);
  Radio.setCRCLength(RF24_CRC_16);
  Radio.enableDynamicPayloads();
  Radio.setAutoAck(true);
  Radio.setRetries(3, 15);

  bool res = Radio.write(buf, len);
  // Try to avoid zero payload acks (has no effect)
  Radio.openWritingPipe(DUMMY_RADIO_ID);

  Radio.setAutoAck(false);
  Radio.setRetries(0, 0);
  Radio.disableDynamicPayloads();
  Radio.setCRCLength(RF24_CRC_DISABLED);

  Radio.setChannel(DEFAULT_RECV_CHANNEL);
  Radio.startListening();
  ENABLE_EINT;
#if USE_POOR_MAN_CHANNEL_HOPPING_RCV
  hophop = 50 * sizeof(rcvChannels);
#endif
  yield();
}

Zum Testen uebernehme ich den Code in PlatformIO. Das Importieren des Arduino-Projektes hat geklappt. Das anschliessende Kommando "platformuio run" brachte allerdings noch ein(en) (paar) Fehler.

**************************************************************
* Looking for RF24.h dependency? Check our library registry!
*
* CLI  > platformio lib search "header:RF24.h"
* Web  > https://registry.platformio.org/search?q=header:RF24.h
*
**************************************************************

Die Suche ergab 6 Treffer, ueber die Library-Suche in der IDE sogar 19. Ich vermute mal, der erste Treffer ist die richtige Library.

nrf24/RF24
Library • 1.4.5 • Published on Tue Jul 19 12:13:10 2022
Radio driver, OSI layer 2 library for nRF24L01(+) transceiver modules.


****************************************************************
* Looking for Pinger.h dependency? Check our library registry!
*
* CLI  > platformio lib search "header:Pinger.h"
* Web  > https://registry.platformio.org/search?q=header:Pinger.h
*
****************************************************************

Ich gehe mal von der Richtigkeit des einzigen Treffers aus;-)

ESP8266-ping by Alessio Leoncini
*****************************************************************
* Looking for TimeLib.h dependency? Check our library registry!
*
* CLI  > platformio lib search "header:TimeLib.h"
* Web  > https://registry.platformio.org/search?q=header:TimeLib.h
*
*****************************************************************

Ich gehe mal hier von aus:

Time by Michael Margolis

In der Zusammenfassung fehlten 3 Libraries:

[env:nodemcuv2]
platform = espressif8266
board = nodemcuv2
framework = arduino
lib_deps = 

nrf24/RF24@^1.4.5 bluemurder/ESP8266-ping@^2.0.1 paulstoffregen/Time@^1.6.1

Das Kompilieren verlief dann mit Erfolg, allerdings noch mit etlichen Warnungen.

Zeile 26 und 28 #define nicht eingerueckt.

 src/Inverters.h:214:17: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
  214 | char * error = {"error"};
      |                 ^~~~~~~
/home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino: In function 'void isTime2Send()':
/home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino:360:20: warning: unused variable 'tel' [-Wunused-variable]
  360 |     static uint8_t tel = 0;
      |                    ^~~
/home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino: In function 'void SendPacket(uint64_t, uint8_t*, uint8_t)':
/home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino:700:8: warning: unused variable 'res' [-Wunused-variable]
  700 |   bool res = Radio.write(buf, len);
      |        ^~~
/home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino: At global scope:
/home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino:360:20: warning: 'tel' defined but not used [-Wunused-variable]
  360 |     static uint8_t tel = 0;
      |                    ^~~
In file included from src/Settings.h:43,
                 from /home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino:9:
src/Inverters.h:195:16: warning: 'toggle' defined but not used [-Wunused-variable]
  195 | static uint8_t toggle = 0;           // nur für Test, ob's auch für mehere WR funzt
      |                ^~~~~~
In file included from /home/chris/Dokumente/PlatformIO/Projects/220904-090158-nodemcuv2/src/HoyDtuSim.ino:7:
src/hm_packets.h:57:16: warning: 'cid' defined but not used [-Wunused-variable]
   57 | static uint8_t cid = 0;
      |                ^~~

Bevor ich hier weitermache, schaue ich mir mal im Vergleich OpenDTU an.

OpenDTU

https://github.com/tbnobody/OpenDTU#opendtu

Das hier sieht schon etwas besser/einfacher aus. Die Daten sind direkt in PlatformIO nutzbar.

Protokollanalyse

Testschnipsel

Chronologischer Mitschnitt der Essentials der Protokollanalyse.

Begonnen wurde am 10.10.2021.

 Meine Tests von heute morgen brachten folgende Resultate:
- es scheint kein alive-Paket zu geben, das von den Hoymiles verschickt 
wird
- Test auf Carrier-Signal auf den Kanälen, 13, 40 und 75 , dass nur auf 
Kanal 40 ein Carrier-Signal gefunden wurde, innerhalb einer Minute 
mehrere hundert.Auf den beiden anderen Kanälen war ab und zu ein 
Carrier, kann auch Rauschen sein.
Ich denke, ohne dass man da die Kommunikation mitschneidet wird es sehr 
aufwendig. Ich versuch mal, ähnlich wie bei dem o.g. Projekt mit 
APsystems zu pollen.

 Meine Tests haben zu keinem Ergebnis geführt. Das große Problem dabei 
ist, dass zuviele Parameter zueinander passen müssen, nämlich Kanal, 
(5-byte) Adressen der NRFs, Protokoll (Shockburst oder enhanced SB), 
Aufbau Datenpaket, "Magic" Byte, Füllbytes, CRC etc.

 
Hallo zusammen.
Die Teile sind da und ich habe bereits erste Versuche durchgeführt, 
wovon ich euch einen Zwischenstand mitteilen möchte.

Zum Funkprotokoll:
Die Kanäle 3,23,40,75 scheinen zu passen, hier sieht man im Spektrum die 
Datenpakete, die per NRF gesendet werden. Die DTU scheint auch 
regelmäßig alle Kanäle zu bedienen - ob der Inverter immer auf dem 
selben Antwortet oder ob die Pakete redundant sind kann ich noch nicht 
sagen.

Im Zeitverlauf kann man die Datenpakete auch schön sehen, allerdings bin 
ich mir nicht sicher ob der Universal Radio Hacker sie richtig 
dekodieren kann - mir fehlt der Vergleich, welche Daten es denn sein 
sollen.

 Dann hab ich mir die DTU mal genauer angeschaut. Alles in allem keine 
Raketentechnik.
-Ein GD32F303 (Gigadevice, ein STM-Klon) mit SPI Flash 25Q128 .
-Ein ESP8266 für die WLAN Kommunikation
-Ein NRF24LE1E mit 2401C (RF Front End)

Nachdem ich die beschriftete SPI-Schnittstelle gesehen habe, dachte ich 
"Da kann ich ja direkt die NRF-Kommunikation abfangen und mit den 
Funkpaketen vergleichen" - war aber leider nix, die Schnittstelle wird 
nicht benutzt. Ich muss jetzt also herausfinden, wo der NRF mit dem 
anderen uC kommuniziert. Ich vermute mal, dass irgendwo noch eine UART 
lauert.

Den "USB"-Anschluss werde ich mir auch noch genauer ansehen, der scheint 
nämlich der Beschriftung nach ebenfalls eine UART zu sein. Mal schauen, 
was die ausgibt. Gibt also noch bisschen was zu tun...

Hallo Martin
hast du dir schon das Shockburst bzw enhanced Shockburst Protokoll von 
Nordic angeschaut? Dein Abtast sieht danach aus, denn da findet man die 
Startsequenz 0x55. Danach sollte eine 3 bzw 5 byte lange Adresse folgen, 
und im Fall von enhanced SB ein 9-bittiges (richtig neun!) Kontrollbyte.

Hallo Hubi,
ja, das Enhanced Shockburst hab ich mir angeschaut - dürfte passen.
Ich habe hier mehrere Re-Transmits eines Pakets untereinandergelegt, die 
sich (halbwegs) sauber demodulieren ließen (bisschen Jitter ist in den 
einzelnen Bitfolgen).
Es tauchen vertraute Zahlenfolgen darin auf, nämlich jeweils die letzten 
8 Ziffern (=Hex) der Seriennummern von Inverter und DTU. Siehe Beispiel 
- möchte meine Seriennummern nicht preisgeben ;-)

Die eigentliche RF-Payload ist 27 Byte. Das passt auch zu dem, was der 
NRF seriell an den Controller schickt - das sind 29 Byte, wobei das 
erste und letzte Byte immer gleich sind (0x7E..0x7F). Ich vermute mal, 
die werden als Start- und Endmarker benutzt oder als Commands für den 
uC.

Die Datenrate müsste 250 kbps sein, das habe ich mal mit einem eigenen 
NRF und einem Sendetest quergeprüft.

Ich konnte bisher noch nicht herausfinden, welche CRC verwendet wird und 
ob die Payload selbst auch nochmal mit einer CRC versehen ist.
Als nächstes will ich deshalb untersuchen, welche Daten in der Payload 
versteckt sind. Da müssten sich ja theoretisch die Leistungs- und 
Stromwerte finden lassen, die die App anzeigt. Das wird aber nochmal ein 
größerer Brocken Arbeit...

Hallo Martin,

hast du in der Spec vom Nordic den Abschnitt von der CRC gesehen:

CRC (Cyclic Redundancy Check)
The CRC is the error detection mechanism in the packet. It may either be 
1 or 2 bytes and is calculated over the address, Packet Control Field 
and Payload.
The polynomial for 1 byte CRC is X8 + X2 + X + 1. Initial value 0xFF.
The polynomial for 2 byte CRC is X16+ X12 + X5 + 1. Initial value 
0xFFFF. No packet is accepted by Enhanced ShockBurstTM if the CRC fails.

Das könnte helfen, die verwendete crc herauszufinden.

Hallo zusammen,
hier mal wieder ein Update zur Protokoll-Analyse.

Ich habe nun etwas mehr Durchblick bei der Kommunikation erreicht. Also 
es ist definitiv das Enhanced Shockburst Protokoll. Das direkt per 
RadioHacker zu analysieren ist schwierig, da es nicht immer exakt 
demoduliert wird - hinzu kommen die 9 Bit Status-"Byte", die die 
erkannte Bitfolge um 1 verschieben ggü. der eigentlichen Payload. 
Ziemlich unschön. Aber wenn man damit die Randbedingungen geklärt hat, 
gibt es eine bessere Methode: Sniffen mit dem nrf24sniffer von yveaux.

Er hat ein kleines Arduino-Programm für einen Nano mit nrf24, sowie ein 
Windows-Tool, das mit Wireshark gekoppelt wird - Anleitung auf seiner 
Homepage, siehe GitHub-Link.

https://github.com/Yveaux/NRF24_Sniffer

Dazu muss man noch folgende Startparameter wissen:
-Die Empfängeradresse des Inverters entspricht den letzten 8 Stellen der 
Seriennummer, aber umgedreht (Little Endian) - 00 anhängen nicht 
vergessen, da die gesamte Adresse des NRF 5 Byte lang ist.
-Die Datenrate ist 250 kbps, max. Payload 32.

Man kann das Tool dann hiermit starten (siehe Screenshot) und bekommt 
die Datenpakete in Wireshark serviert. Der oben bereits beschriebene 
Protokoll-Aufbau (innerhalb der Payload) scheint zu passen, eine CRC 
konnte ich darin nicht ausfindig machen - die des NRF wird mit diesem 
Tool automatisch geprüft.

So konnte ich das was die DTU sendet direkt auf dem Rechner empfangen.
Im nächsten Schritt hänge ich einen 2. NRF mit der Adresse der DTU dran, 
um das was der Inverter zurück schickt abzufangen. Und dann geht es 
darum, den Inhalt zu identifizieren - einen offensichtlichen 
Zusammenhang zu den physikalischen Größen habe ich leider noch nicht 
entdeckt. Scheint aber eine Menge Overhead drin zu sein...

Martins Radio-Mitschnitt hat bewiesen, dass der "nRF24L01+"-Teil nach 
dem Nordic "enhanced Shockburst" Protokoll funkt.

Dieses beinhaltet insbesondere immer eine Zieladresse. Siehe [1], 
Kapitel 3.4.3:
- Preamble 1 byte
- Address 3-5 byte
- Packet Control Field 9 bit
- Payload 0-32 byte
- CRC 1-2 byte

Für Shockburst gibt es leider keinen einfachen (d.h. 
NRF24LE1E-basierten) "Sniffer", um alle Nachrichten mitzulesen. Daher 
das weiter oben erwähnte Projekt in [2].

du hast recht. Ich hatte verdrängt das auf den NRF24 selbst auch noch 
Logik vorhanden sein kann.
Ich würde jetzt mal unterstellen, dass der WR immer auf seine 
Seriennummer (= Adresse) lauscht und der DTU ebenfalls auf seiner 
Seriennummer.

In Init 1 (siehe deine Text File) würde die DTU zwar an die Adresse des 
WR ein Paket senden aber der WR würde nicht wissen an welche Adresse er 
antworten soll. Dies ginge erst ab Init 2 weil dort die DTU seine eigene 
Adresse an den WR schickt.

Bei der Durchsicht des Datenblattes [1] ist mir noch etwas im Kapitel 
3.4.3.3 aufgefallen. Dort wird u.a. das No Acknowledgment flag 
beschrieben.
Im Post Beitrag "Re: Wechselrichter Hoymiles HM-xxxx 2,4 GhZ Nordic Protokoll?" wird 
beschrieben das das PCF 0D8 (== b011011000) ist. Was ja bedeutet das 
NO_ACK 0 ist.
"Setting the flag high, tells the receiver that the packet is not to be 
auto acknowledged."

Es ist low, also muss AutoAck true sein. Das wäre zumindest etwas was 
mir im Code von Hubi aufgefallen ist.
Ebenso wird im oben genannten Post beschrieben das Channel 3 verwendet 
wird.

radio.setAutoAck(true);

radio.setChannel(3);


#See RFC

Request for Comments

Loading comments...