(Hoymiles) Datenlogger DTU
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
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.
#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
- https://github.com/atc1441/NETSGPClient (Polldemo)
- https://github.com/Yogui79/IntexPureSpa
- https://fcc.report/FCC-ID/2ARNB-DTUW100
- https://fccid.io/2ARNB-HM2401
- https://github.com/Koenkk/zigbee2mqtt/issues/4221
- https://github.com/Eule01x/NETSGPClient-for-Hoymiles
- https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v11.0.0%2Fesb_users_guide.html&cp=5_0_0_5_2
- https://infocenter.nordicsemi.com/pdf/nRF24LE1_PS_v1.6.pdf
- https://github.com/Yveaux/NRF24_Sniffer
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);