diff --git a/README.md b/README.md index fda91fe4..6c781bbf 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ In particular: * `doc/hoymiles-format-description.txt` is a detailed description of the communications format and the history of this project * `doc/getting-started-ESP8266.md` shows the hardware setup for an ESP8266-based system -* The `tools` folder contains various software tools for RaspberryPi, Arduino and ESP8266/ESP32 +* The `tools` folder contains various software tools for RaspberryPi, Arduino and ESP8266/ESP32: + * A [version for ESP8266](tools/esp8266) that includes a web interface + * A [version for Arduino Nano](tools/nano/NRF24_SendRcv) + * An [alternative Version of the above](tools/NRF24_SendRcv) + * A [different implementation](tools/HoyDtuSim) + * An [implementation for Raspberry Pi](tools/rpi) that polls an inverter and archives results as log files/stdout as well as posting them to an MQTT broker. Contributors are always welcome! diff --git a/doc/HM-400 data.xlsx b/doc/HM-400 data.xlsx index d6295022..2c1bf5e5 100755 Binary files a/doc/HM-400 data.xlsx and b/doc/HM-400 data.xlsx differ diff --git a/tools/HoyDtuSim/CircularBuffer.h b/tools/HoyDtuSim/CircularBuffer.h new file mode 100644 index 00000000..a7fafdb7 --- /dev/null +++ b/tools/HoyDtuSim/CircularBuffer.h @@ -0,0 +1,158 @@ +/* + CircularBuffer - An Arduino circular buffering library for arbitrary types. + + Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef CircularBuffer_h +#define CircularBuffer_h + +#ifdef ESP8266 +#define DISABLE_IRQ noInterrupts() +#define RESTORE_IRQ interrupts() +#else +#define DISABLE_IRQ \ + uint8_t sreg = SREG; \ + cli(); + +#define RESTORE_IRQ \ + SREG = sreg; +#endif + +template class CircularBuffer +{ + public: + /** Constructor + * @param buffer Preallocated buffer of at least size records. + * @param size Number of records available in the buffer. + */ + CircularBuffer(T* buffer, const uint8_t size ) + : m_size(size), m_buff(buffer) + { + clear(); + } + + /** Clear all entries in the circular buffer. */ + void clear(void) + { + m_front = 0; + m_fill = 0; + } + + /** Test if the circular buffer is empty */ + inline bool empty(void) const + { + return !m_fill; + } + + /** Return the number of records stored in the buffer */ + inline uint8_t available(void) const + { + return m_fill; + } + + /** Test if the circular buffer is full */ + inline bool full(void) const + { + return m_fill == m_size; + } + + /** Aquire record on front of the buffer, for writing. + * After filling the record, it has to be pushed to actually + * add it to the buffer. + * @return Pointer to record, or NULL when buffer is full. + */ + T* getFront(void) const + { + DISABLE_IRQ; + T* f = NULL; + if (!full()) + f = get(m_front); + RESTORE_IRQ; + return f; + } + + /** Push record to front of the buffer + * @param record Record to push. If record was aquired previously (using getFront) its + * data will not be copied as it is already present in the buffer. + * @return True, when record was pushed successfully. + */ + bool pushFront(T* record) + { + bool ok = false; + DISABLE_IRQ; + if (!full()) + { + T* f = get(m_front); + if (f != record) + *f = *record; + m_front = (m_front+1) % m_size; + m_fill++; + ok = true; + } + RESTORE_IRQ; + return ok; + } + + /** Aquire record on back of the buffer, for reading. + * After reading the record, it has to be pop'ed to actually + * remove it from the buffer. + * @return Pointer to record, or NULL when buffer is empty. + */ + T* getBack(void) const + { + T* b = NULL; + DISABLE_IRQ; + if (!empty()) + b = get(back()); + RESTORE_IRQ; + return b; + } + + /** Remove record from back of the buffer. + * @return True, when record was pop'ed successfully. + */ + bool popBack(void) + { + bool ok = false; + DISABLE_IRQ; + if (!empty()) + { + m_fill--; + ok = true; + } + RESTORE_IRQ; + return ok; + } + + protected: + inline T * get(const uint8_t idx) const + { + return &(m_buff[idx]); + } + inline uint8_t back(void) const + { + return (m_front - m_fill + m_size) % m_size; + } + + const uint8_t m_size; // Total number of records that can be stored in the buffer. + T* const m_buff; // Ptr to buffer holding all records. + volatile uint8_t m_front; // Index of front element (not pushed yet). + volatile uint8_t m_fill; // Amount of records currently pushed. +}; + +#endif // CircularBuffer_h diff --git a/tools/HoyDtuSim/Debug.h b/tools/HoyDtuSim/Debug.h new file mode 100644 index 00000000..3b2807d6 --- /dev/null +++ b/tools/HoyDtuSim/Debug.h @@ -0,0 +1,23 @@ +#ifndef __DEBUG_H + +#define __DEBUG_H + +#ifdef DEBUG + #define DEBUG_OUT Serial +#else +//--- +// disable Serial DEBUG output + #define DEBUG_OUT DummySerial + static class { + public: + void begin(...) {} + void print(...) {} + void println(...) {} + void flush() {} + bool available() { return false;} + int readBytes(...) { return 0;} + int printf (...) {return 0;} + } DummySerial; +#endif + +#endif diff --git a/tools/HoyDtuSim/HM1200.h b/tools/HoyDtuSim/HM1200.h new file mode 100644 index 00000000..c8c27d87 --- /dev/null +++ b/tools/HoyDtuSim/HM1200.h @@ -0,0 +1,38 @@ +#ifndef __HM1200_H +#define __HM1200_H + +#define HM1200 + +const measureDef_t hm1200_measureDef[] = { + { IDX_UDC, UNIT_V, CH1, CMD01, 14, BYTES2, DIV10 }, + { IDX_IDC, UNIT_A, CH1, CMD01, 16, BYTES2, DIV100 }, + { IDX_PDC, UNIT_W, CH1, CMD01, 20, BYTES2, DIV10 }, + { IDX_E_TAG, UNIT_WH, CH1, CMD02, 16, BYTES2, DIV1 }, + { IDX_E_TOTAL, UNIT_KWH, CH1, CMD01, 24, BYTES4, DIV1000 }, + { IDX_UDC, UNIT_V, CH2, CMD02, 20, BYTES2, DIV10 }, + { IDX_IDC, UNIT_A, CH2, CMD01, 18, BYTES2, DIV100 }, + { IDX_PDC, UNIT_W, CH2, CMD01, 22, BYTES2, DIV10 }, + { IDX_E_TAG, UNIT_WH, CH2, CMD02, 18, BYTES2, DIV1 }, + { IDX_E_TOTAL, UNIT_KWH, CH2, CMD02, 12, BYTES4, DIV1000 }, + { IDX_IDC, UNIT_A, CH3, CMD02, 22, BYTES2, DIV100 }, + { IDX_PDC, UNIT_W, CH3, CMD02, 26, BYTES2, DIV10 }, + { IDX_E_TAG, UNIT_WH, CH3, CMD03, 22, BYTES2, DIV1 }, + { IDX_E_TOTAL, UNIT_KWH, CH3, CMD03, 14, BYTES4, DIV1000 }, + { IDX_IDC, UNIT_A, CH4, CMD02, 24, BYTES2, DIV100 }, + { IDX_PDC, UNIT_W, CH4, CMD03, 12, BYTES2, DIV10 }, + { IDX_E_TAG, UNIT_WH, CH4, CMD03, 24, BYTES2, DIV1 }, + { IDX_E_TOTAL, UNIT_KWH, CH4, CMD03, 18, BYTES4, DIV1000 }, + { IDX_UAC, UNIT_V, CH0, CMD03, 26, BYTES2, DIV10 }, + { IDX_IPV, UNIT_A, CH0, CMD84, 18, BYTES2, DIV100 }, + { IDX_PAC, UNIT_W, CH0, CMD84, 14, BYTES2, DIV10 }, + { IDX_FREQ, UNIT_HZ, CH0, CMD84, 12, BYTES2, DIV100 }, + { IDX_PERCNT, UNIT_PCT, CH0, CMD84, 20, BYTES2, DIV10 }, + { IDX_WR_TEMP, UNIT_C, CH0, CMD84, 22, BYTES2, DIV10 } +}; + +measureCalc_t hm1200_measureCalc[] = {}; + +#define HM1200_MEASURE_LIST_LEN sizeof(hm1200_measureDef)/sizeof(measureDef_t) +#define HM1200_CALCED_LIST_LEN 0 + +#endif diff --git a/tools/HoyDtuSim/HM600.h b/tools/HoyDtuSim/HM600.h new file mode 100644 index 00000000..9ba5c1a8 --- /dev/null +++ b/tools/HoyDtuSim/HM600.h @@ -0,0 +1,37 @@ +#ifndef __HM600_H +#define __HM600_H + +#define HM600 +#define HM700 + + +float calcEheute (float *measure) { return measure[8] + measure[9]; } +float calcIpv (float *measure) { return (measure[10] != 0 ? measure[12]/measure[10] : 0); } + +const measureDef_t hm600_measureDef[] = { + { IDX_UDC, CH1, UNIT_V, CMD01, 14, BYTES2, DIV10}, + { IDX_IDC, CH1, UNIT_A, CMD01, 16, BYTES2, DIV100}, + { IDX_PDC, CH1, UNIT_W, CMD01, 18, BYTES2, DIV10}, + { IDX_UDC, CH2, UNIT_V, CMD01, 20, BYTES2, DIV10}, + { IDX_IDC, CH2, UNIT_A, CMD01, 22, BYTES2, DIV100}, + { IDX_PDC, CH2, UNIT_W, CMD01, 24, BYTES2, DIV10}, + { IDX_E_WOCHE,CH0, UNIT_WH, CMD02, 12, BYTES2, DIV1}, + { IDX_E_TOTAL,CH0, UNIT_WH, CMD02, 14, BYTES4, DIV1}, + { IDX_E_TAG, CH1, UNIT_WH, CMD02, 18, BYTES2, DIV1}, + { IDX_E_TAG, CH2, UNIT_WH, CMD02, 20, BYTES2, DIV1}, + { IDX_UAC, CH0, UNIT_V, CMD02, 22, BYTES2, DIV10}, + { IDX_FREQ, CH0, UNIT_HZ, CMD02, 24, BYTES2, DIV100}, + { IDX_PAC, CH0, UNIT_W, CMD02, 26, BYTES2, DIV10}, + { IDX_WR_TEMP,CH0, UNIT_C, CMD83, 18, BYTES2, DIV10} +}; + + +measureCalc_t hm600_measureCalc[] = { + { IDX_E_HEUTE, UNIT_WH, DIV1, &calcEheute}, + { IDX_IPV, UNIT_A, DIV100, &calcIpv} +}; + +#define HM600_MEASURE_LIST_LEN sizeof(hm600_measureDef)/sizeof(measureDef_t) +#define HM600_CALCED_LIST_LEN sizeof(hm600_measureCalc)/sizeof(measureCalc_t) + +#endif diff --git a/tools/HoyDtuSim/HoyDtuSim.ino b/tools/HoyDtuSim/HoyDtuSim.ino new file mode 100644 index 00000000..87423e49 --- /dev/null +++ b/tools/HoyDtuSim/HoyDtuSim.ino @@ -0,0 +1,605 @@ +#include +#include +#include "CircularBuffer.h" +#include +#include "printf.h" +#include +#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.1.6"; + + +#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 (20) +#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 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 = 2; // 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} +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; + +static unsigned long timeLastPacket = millis(); +static unsigned long timeLastIstTagCheck = millis(); +static unsigned long timeLastRcvChannelSwitch = millis(); + +// Function forward declaration +static void SendPacket(uint64_t dest, uint8_t *buf, uint8_t len); + + +static const char BLANK = ' '; + +static boolean istTag = true; + +char CHANNELNAME_BUFFER[15]; + +#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); +} + + +float extractValue2 (uint8_t *p, int divisor) { +//------------------------------------------- + uint16_t b1 = *p++; + return ((float) (b1 << 8) + *p) / (float) divisor; +} + + +float extractValue4 (uint8_t *p, int divisor) { +//------------------------------------------- + uint32_t ret = *p++; + for (uint8_t i = 1; i <= 3; i++) + ret = (ret << 8) + *p++; + return (ret / divisor); +} + +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 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(extractValue2(p,1)); + DEBUG_OUT.print(BLANK); + p++; + } + DEBUG_OUT.println(); +} + +void analyseLongs (uint8_t *p) { // p zeigt auf 01 hinter 2. WR-Adr +//---------------------------------- + //uint16_t val; + DEBUG_OUT.print (F("analyse longs:")); + p++; + for (int i = 0; i <12;i++) { + DEBUG_OUT.print(extractValue4(p,1)); + DEBUG_OUT.print(BLANK); + p++; + } + DEBUG_OUT.println(); +} + + +void analyse (NRF24_packet_t *p) { +//------------------------------ + uint8_t wrIdx = findInverter (&p->packet[3]); + //DEBUG_OUT.print ("wrIdx="); DEBUG_OUT.println (wrIdx); + if (wrIdx == 0xFF) return; + uint8_t cmd = p->packet[11]; + float val = 0; + if (cmd == 0x01 || cmd == 0x02 || cmd == 0x83) { + const measureDef_t *defs = inverters[wrIdx].measureDef; + + for (uint8_t i = 0; i < inverters[wrIdx].anzMeasures; i++) { + if (defs[i].teleId == cmd) { + uint8_t pos = defs[i].pos; + if (defs[i].bytes == 2) + val = extractValue2 (&p->packet[pos], getDivisor(wrIdx, i) ); + else if (defs[i].bytes == 4) + val = extractValue4 (&p->packet[pos], getDivisor(wrIdx, i) ); + 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 (inverters[wrIdx].values); + int idx = inverters[wrIdx].anzMeasures + i; + valueChanged = valueChanged ||(val != inverters[wrIdx].values[idx]); + inverters[wrIdx].values[idx] = val; + } + } + else if (cmd == 0x81) { + ; + } + else { + DEBUG_OUT.print (F("---- neues cmd=")); DEBUG_OUT.println(cmd, HEX); + analyseWords (&p->packet[11]); + analyseLongs (&p->packet[11]); + 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; + uint8_t pipe; + + DISABLE_EINT; + + // Loop until RX buffer(s) contain no more packets. + while (Radio.available(&pipe)) { + 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); + 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(RF24_PA_MAX); + 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 + + setupInverts(); +} + + uint8_t sendBuf[MAX_RF_PAYLOAD_SIZE]; + +void isTime2Send () { +//----------------- + // Second timer + static const uint8_t warteZeit = 1; + 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; + } + + int32_t size = 0; + uint64_t dest = 0; + for (uint8_t wr = 0; wr < anzInv; wr++) { + dest = inverters[wr].RadioId; + + if (tel > 1) + tel = 0; + + if (tel == 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 if (tel <= 1) + size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x80 + tel - 1); + + SendPacket (dest, (uint8_t *)&sendBuf, size); + } // for wr + + tel++; + +/* for (uint8_t warte = 0; warte < 2; warte++) { + delay(1000); + hmPackets.UnixTimeStampTick(); + }*/ + } +} + + +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++) { + if (anzInv > 1) { + Serial.print(wr); Serial.print('.'); + } + for (uint8_t i = 0; i < inverters[wr].anzTotalMeasures; i++) { + 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; +} + +void poorManChannelHopping() { +//-------------------------- + if (hophop <= 0) return; + if (millis() >= timeLastRcvChannelSwitch + intvl) { + rcvChannelIdx++; + if (rcvChannelIdx >= sizeof(rcvChannels)) + rcvChannelIdx = 0; + DEFAULT_RECV_CHANNEL = rcvChannels[rcvChannelIdx]; + DISABLE_EINT; + Radio.stopListening(); + Radio.setChannel (DEFAULT_RECV_CHANNEL); + Radio.startListening(); + ENABLE_EINT; + timeLastRcvChannelSwitch = millis(); + hophop--; + } + +} +void loop(void) { +//============= + // poor man channel hopping on receive +#if USE_POOR_MAN_CHANNEL_HOPPING_RCV + poorManChannelHopping(); +#endif + + if (millis() > timeLastPacket + 50000UL) { + DEBUG_OUT.println (F("Reset RF24")); + resetRF24(); + timeLastPacket = millis(); + } + + 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 + for (int16_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); + } + + 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); + + #ifndef ESP8266 + writeArduinoInterface(); + #endif + + // Remove record as we're done with it. + packetBuffer.popBack(); + } + + if (istTag) + isTime2Send(); + + #ifdef ESP8266 + checkWifi(); + webserverHandle(); + checkUpdateByOTA(); + 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; + #if DEBUG_SEND + DEBUG_OUT.print(F("Send... CH")); + DEBUG_OUT.println(channels[hop]); + #endif + Radio.setChannel(channels[hop++]); + if (hop >= sizeof(channels) / sizeof(channels[0])) + hop = 0; +#else + Radio.setChannel(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 = 5 * sizeof(rcvChannels); +#endif +} diff --git a/tools/HoyDtuSim/Inverters.h b/tools/HoyDtuSim/Inverters.h new file mode 100644 index 00000000..af929af2 --- /dev/null +++ b/tools/HoyDtuSim/Inverters.h @@ -0,0 +1,283 @@ +#ifndef __INVERTERS_H +#define __INVERTERS_H + +// Ausgabe von Debug Infos auf der seriellen Console + +#include "Settings.h" +#include "Debug.h" + + +typedef struct _NRF24_packet_t { + uint32_t timestamp; + uint8_t packetsLost; + uint8_t rcvChannel; + uint8_t packet[MAX_RF_PAYLOAD_SIZE]; +} NRF24_packet_t; + + +typedef struct _Serial_header_t { + unsigned long timestamp; + uint8_t packetsLost; + uint8_t address[RF_MAX_ADDR_WIDTH]; // MSB first, always RF_MAX_ADDR_WIDTH bytes. +} Serial_header_t; + + +// structs für Inverter und Kanalwerte + +// Liste der Einheiten +enum UNITS {UNIT_V = 0, UNIT_HZ, UNIT_A, UNIT_W, UNIT_WH, UNIT_C, UNIT_KWH, UNIT_MA, UNIT_PCT}; +const char* const units[] = {"V", "Hz", "A", "W", "Wh", "°C", "KWh", "mA", "%"}; + +// CH0 is default channel (freq, ac, temp) +enum CHANNELS {CH0 = 0, CH1, CH2, CH3, CH4}; +enum CMDS {CMD01 = 0x01, CMD02, CMD03, CMD83 = 0x83, CMD84}; +enum DIVS {DIV1 = 0, DIV10, DIV100, DIV1000}; + +#define BYTES2 2 +#define BYTES4 4 + +const char UDC[] PROGMEM = "Udc"; +const char IDC[] PROGMEM = "Idc"; +const char PDC[] PROGMEM = "Pdc"; +const char E_WOCHE[] PROGMEM = "E-Woche"; +const char E_TOTAL[] PROGMEM = "E-Total"; +const char E_TAG[] PROGMEM = "E-Tag"; +const char UAC[] PROGMEM = "Uac"; +const char FREQ[] PROGMEM = "Freq.ac"; +const char PAC[] PROGMEM = "Pac"; +const char E_HEUTE[] PROGMEM = "E-heute"; +const char IPV[] PROGMEM = "Ipv"; +const char WR_TEMP[] PROGMEM = "WR-Temp"; +const char PERCNT[] PROGMEM = "Pct"; + +#define IDX_UDC 0 +#define IDX_IDC 1 +#define IDX_PDC 2 +#define IDX_E_WOCHE 3 +#define IDX_E_TOTAL 4 +#define IDX_E_TAG 5 +#define IDX_UAC 6 +#define IDX_FREQ 7 +#define IDX_PAC 8 +#define IDX_E_HEUTE 9 +#define IDX_IPV 10 +#define IDX_WR_TEMP 11 +#define IDX_PERCNT 12 + +const char* const NAMES[] + = {UDC, IDC, PDC, E_WOCHE, E_TOTAL, E_TAG, UAC, FREQ, PAC, E_HEUTE, IPV, WR_TEMP, PERCNT}; + +typedef float (*calcValueFunc)(float *); + +struct measureDef_t { + uint8_t nameIdx; //const char* name; // Zeiger auf den Messwertnamen + uint8_t channel; // 0..4, + uint8_t unitIdx; // Index in die Liste der Einheiten 'units' + uint8_t teleId; // Telegramm ID, das was hinter der 2. WR Nummer im Telegramm, 02, 03, 83 + uint8_t pos; // ab dieser POsition beginnt der Wert (Big Endian) + uint8_t bytes; // Anzahl der Bytes + uint8_t digits; +}; + +struct measureCalc_t { + uint8_t nameIdx; //const char* name; // Zeiger auf den Messwertnamen + uint8_t unitIdx; // Index in die Liste der Einheiten 'units' + uint8_t digits; + calcValueFunc f; // die Funktion zur Berechnung von Werten, zb Summe von Werten +}; + + +struct inverter_t { + uint8_t ID; // Inverter-ID = Index + char name[20]; // Name des Inverters zb HM-600.1 + uint64_t serialNo; // dier Seriennummer wie im Barcode auf dem WR, also 1141..... + uint64_t RadioId; // die gespiegelte (letzte 4 "Bytes") der Seriennummer + const measureDef_t *measureDef; // aus Include HMxxx.h : Liste mit Definitionen der Messwerte, wie Telgramm, offset, länge, ... + uint8_t anzMeasures; // Länge der Liste + measureCalc_t *measureCalculated; // Liste mit Defintion für berechnete Werte + uint8_t anzMeasureCalculated; // Länge der Liste + uint8_t anzTotalMeasures; // Gesamtanzahl Messwerte + float values[MAX_MEASURE_PER_INV]; // DIE Messewerte +}; + + +char _buffer[20]; + +uint8_t anzInv = 0; +inverter_t inverters[MAX_ANZ_INV]; + +union longlongasbytes { + uint64_t ull; + uint32_t ul[2]; + uint8_t bytes[8]; +}; + +char *uint64toa (uint64_t s) { +//-------------------------------- +//0x1141 72607952ULL + sprintf(_buffer, "%lX%08lX", (unsigned long)(s>>32), (unsigned long)(s&0xFFFFFFFFULL)); + return _buffer; +} + + +uint64_t Serial2RadioID (uint64_t sn) { +//---------------------------------- + longlongasbytes llsn; + longlongasbytes res; + llsn.ull = sn; + res.ull = 0; + res.bytes[4] = llsn.bytes[0]; + res.bytes[3] = llsn.bytes[1]; + res.bytes[2] = llsn.bytes[2]; + res.bytes[1] = llsn.bytes[3]; + res.bytes[0] = 0x01; + return res.ull; +} + + +void addInverter (uint8_t _ID, const char * _name, uint64_t _serial, + const measureDef_t * liste, int anzMeasure, + measureCalc_t * calcs, int anzMeasureCalculated) { +//------------------------------------------------------------------------------------- + if (anzInv >= MAX_ANZ_INV) { + DEBUG_OUT.println(F("ANZ_INV zu klein!")); + return; + } + inverter_t *p = &(inverters[anzInv]); + p->ID = _ID; + strcpy (p->name, _name); + p->serialNo = _serial; + p->RadioId = Serial2RadioID(_serial); + p->measureDef = liste; + p->anzMeasures = anzMeasure; + p->anzMeasureCalculated = anzMeasureCalculated; + p->measureCalculated = calcs; + p->anzTotalMeasures = anzMeasure + anzMeasureCalculated; + memset (p->values, 0, sizeof(p->values)); + + DEBUG_OUT.print (F("WR : ")); DEBUG_OUT.println(anzInv); + DEBUG_OUT.print (F("Type : ")); DEBUG_OUT.println(_name); + DEBUG_OUT.print (F("Serial : ")); DEBUG_OUT.println(uint64toa(_serial)); + DEBUG_OUT.print (F("Radio-ID : ")); DEBUG_OUT.println(uint64toa(p->RadioId)); + + anzInv++; +} + + +static uint8_t toggle = 0; // nur für Test, ob's auch für mehere WR funzt +uint8_t findInverter (uint8_t *fourbytes) { +//--------------------------------------- + for (uint8_t i = 0; i < anzInv; i++) { + longlongasbytes llb; + llb.ull = inverters[i].serialNo; + if (llb.bytes[3] == fourbytes[0] && + llb.bytes[2] == fourbytes[1] && + llb.bytes[1] == fourbytes[2] && + llb.bytes[0] == fourbytes[3] ) + { + return i; + //if (toggle) toggle = 0; else toggle = 1; return toggle; // Test ob mehr WR auch geht + } + } + return 0xFF; // nicht gefunden +} + + +char * error = {"error"}; + +char *getMeasureName (uint8_t wr, uint8_t i){ +//------------------------------------------ + inverter_t *p = &(inverters[wr]); + if (i >= p->anzTotalMeasures) return error; + uint8_t idx, channel = 0; + if (i < p->anzMeasures) { + idx = p->measureDef[i].nameIdx; + channel = p->measureDef[i].channel; + } + else { + idx = p->measureCalculated[i - p->anzMeasures].nameIdx; + } + char tmp[20]; + strcpy_P (_buffer, NAMES[idx]); + if (channel) { + sprintf_P (tmp, PSTR(".CH%d"), channel); + strcat(_buffer,tmp); + } + return _buffer; +} + +const char *getUnit (uint8_t wr, uint8_t i) { +//------------------------------------------ + inverter_t *p = &(inverters[wr]); + if (i >= p->anzTotalMeasures) return error; + uint8_t idx; + if (i < p->anzMeasures) + idx = p->measureDef[i].unitIdx; + else + idx = p->measureCalculated[i-p->anzMeasures].unitIdx; + + //strcpy (_buffer, units[i]); + //return _buffer; + return units[idx]; +} + + +float getMeasureValue (uint8_t wr, uint8_t i) { +//------------------------------------------ + if (i >= inverters[wr].anzTotalMeasures) return 0.0; + return inverters[wr].values[i]; +} + + +int getDivisor (uint8_t wr, uint8_t i) { +//------------------------------------ + inverter_t *p = &(inverters[wr]); + if (i >= p->anzTotalMeasures) return 1; + if (i < p->anzMeasures) { + uint8_t digits = p->measureDef[i].digits; + if (digits == DIV1) return 1; + if (digits == DIV10) return 10; + if (digits == DIV100) return 100; + if (digits == DIV1000) return 1000; + return 1; + } + else + return p->measureCalculated[i].digits; +} + + +uint8_t getDigits (uint8_t wr, uint8_t i) { +//--------------------------------------- + inverter_t *p = &(inverters[wr]); + if (i >= p->anzTotalMeasures) return 0; + if (i < p->anzMeasures) + return p->measureDef[i].digits; + else + return p->measureCalculated[i-p->anzMeasures].digits; +} + +// +++++++++++++++++++++++++++++++++++ Inverter ++++++++++++++++++++++++++++++++++++++++++++++ + +#include "HM600.h" // für HM-600 und HM-700 + +#include "HM1200.h" + +// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + +void setupInverts() { +//----------------- + + addInverter (0,"HM-600", 0x114172607952ULL, + hm600_measureDef, HM600_MEASURE_LIST_LEN, // Tabelle der Messwerte + hm600_measureCalc, HM600_CALCED_LIST_LEN); // Tabelle berechnete Werte + +/* + addInverter (1,"HM-1200", 0x114172607952ULL, + hm1200_measureDef, HM1200_MEASURE_LIST_LEN, // Tabelle der Messwerte + hm1200_measureCalc, HM1200_CALCED_LIST_LEN); // Tabelle berechnete Werte +*/ +} + + +#endif diff --git a/tools/HoyDtuSim/ModWebserver.h b/tools/HoyDtuSim/ModWebserver.h new file mode 100644 index 00000000..13c88df2 --- /dev/null +++ b/tools/HoyDtuSim/ModWebserver.h @@ -0,0 +1,151 @@ +// ################# WebServer ################# + +#ifndef __MODWEBSERVER_H +#define __MODWEBSERVER_H +#define MODWEBSERVER + +#include +#include "Debug.h" +#include "Settings.h" + +ESP8266WebServer server (WEBSERVER_PORT); + + +void returnOK () { + //-------------- + server.send(200, F("text/plain"), ""); +} + + +void returnFail(String msg) { + //------------------------- + server.send(500, F("text/plain"), msg + "\r\n"); +} + +void handleHelp () { +//----------------- + String out = ""; + out += "

Hilfe

"; + out += "

"; + out += ""; + out += ""; + out += ""; + out += ""; + out += "
/zeigt alle Messwerte in einer Tabelle; refresh alle 10 Sekunden
/datazum Abruf der Messwerte in der Form Name=wert
:{port+1}/updateOTA
/rebootstartet neu
"; + server.send (200, "text/html", out); +} + + +void handleReboot () { + //------------------- + returnOK (); + ESP.reset(); +} + + +void handleRoot() { + //---------------- + String out = ""; + out += ""; + out += "

Hoymiles Micro-Inverters

"; + char floatString[20]; + char line[100]; + for (uint8_t wr = 0; wr < anzInv; wr++) { + out += "

" + String(inverters[wr].name) + "

"; + out += "

S/N " + String (getSerialNoTxt(wr)) + "

"; + out += "

"; + out += ""; + for (uint8_t i = 0; i < inverters[wr].anzTotalMeasures; i++) { + dtostrf (getMeasureValue(wr, i),1, getDigits(wr,i), floatString); + sprintf(line, "", getMeasureName(wr, i), floatString, getUnit(wr, i)); + //DEBUG_OUT.println(line); + out += String(line); +/* out += ""; + out += ""; + out += ""; */ + } + out += "
KanalWertEinheit
%s%s%s
" + getMeasureName(i) + "" + String(getMeasureValue(i)) + "
" + String(getUnit(i)) + "
"; + } + int pos = out.indexOf("°"); + do { + if (pos>1) { + out = out.substring (0, pos) + "°" + out.substring(pos+2); + } + pos = out.indexOf("°"); + } while (pos>1); + + out += ""; + server.send (200, "text/html", out); + //DEBUG_OUT.println (out); +} + + +void handleData () { +//----------------- + String out = ""; + for (uint8_t wr = 0; wr < anzInv; wr++) { + for (int i = 0; i < inverters[wr].anzTotalMeasures; i++) { + out += (anzInv <= 1 ? "" : String (wr) + ".") + String(getMeasureName(wr,i)) + '=' + + String (getMeasureValue(wr,i)) /*+ ' ' + String(getUnit(wr,i))*/ + '\n'; + } + } + server.send(200, "text/plain", out); +} + + +void handleNotFound() { +//-------------------- + String message = "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + for (uint8_t i = 0; i < server.args(); i++) { + message += " NAME:" + server.argName(i) + "\n VALUE:" + server.arg(i) + "\n"; + } + server.send(404, "text/plain", message); +} + + +void setupWebServer (void) { + //------------------------- + server.begin(); + server.on("/", handleRoot); + server.on("/reboot", handleReboot); + server.on("/data", handleData); + server.on("/help", handleHelp); + //server.onNotFound(handleNotFound); wegen Spiffs-Dateimanager + + DEBUG_OUT.println ("[HTTP] installed"); +} + +void webserverHandle() { +//==================== + server.handleClient(); +} + + +// ################# OTA ################# + +#ifdef WITH_OTA +#include + +ESP8266WebServer httpUpdateServer (UPDATESERVER_PORT); +ESP8266HTTPUpdateServer httpUpdater; + +void setupUpdateByOTA () { + //------------------------ + httpUpdater.setup (&httpUpdateServer, UPDATESERVER_DIR, UPDATESERVER_USER, UPDATESERVER_PW); + httpUpdateServer.begin(); + DEBUG_OUT.println ("[OTA] installed"); +} + +void checkUpdateByOTA() { +//--------------------- + httpUpdateServer.handleClient(); +} +#endif + +#endif diff --git a/tools/HoyDtuSim/Settings.h b/tools/HoyDtuSim/Settings.h new file mode 100644 index 00000000..3e263fd5 --- /dev/null +++ b/tools/HoyDtuSim/Settings.h @@ -0,0 +1,69 @@ +#ifndef __SETTINGS_H +#define __SETTINGS_H + +// Ausgabe von Debug Infos auf der seriellen Console +#define DEBUG +#define SER_BAUDRATE (115200) + +#include "Debug.h" + +// Ausgabe was gesendet wird; 0 oder 1 +#define DEBUG_SEND 0 + +// soll zwichen den Sendekanälen 23, 40, 61, 75 ständig gewechselt werden +#define CHANNEL_HOP + +// mit OTA Support, also update der Firmware über WLan mittels IP/update +#define WITH_OTA + +// Hardware configuration +#ifdef ESP8266 +#define RF1_CE_PIN (D4) +#define RF1_CS_PIN (D8) +#define RF1_IRQ_PIN (D3) +#else +#define RF1_CE_PIN (9) +#define RF1_CS_PIN (10) +#define RF1_IRQ_PIN (2) +#endif + +// WR und DTU +#define RF_MAX_ADDR_WIDTH (5) +#define MAX_RF_PAYLOAD_SIZE (32) +#define DEFAULT_RF_DATARATE (RF24_250KBPS) // Datarate + +#define USE_POOR_MAN_CHANNEL_HOPPING_RCV 1 // 0 = not use + +#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) +#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL) +#define MAX_ANZ_INV 2 // <<<<<<<<<<<<<<<<<<<<<<<< anpassen +#define MAX_MEASURE_PER_INV 25 // hier statisch, könnte auch dynamisch erzeugt werden, aber Overhead für dyn. Speicher? + +// Webserver +#define WEBSERVER_PORT 80 + +// Time Server +//#define TIMESERVER_NAME "pool.ntp.org" +#define TIMESERVER_NAME "fritz.box" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen + +#ifdef WITH_OTA +// OTA Einstellungen +#define UPDATESERVER_PORT WEBSERVER_PORT+1 +#define UPDATESERVER_DIR "/update" +#define UPDATESERVER_USER "?????" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen +#define UPDATESERVER_PW "?????" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen +#endif + +// internes WLan +// PREFIXE dienen dazu, die eigenen WLans (wenn mehrere) von fremden zu unterscheiden +// gehe hier davon aus, dass alle WLans das gleiche Passwort haben. Wenn nicht, dann mehre Passwörter hinterlegen +#define SSID_PREFIX1 "pre1" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen +#define SSID_PREFIX2 "pre2" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen +#define SSID_PASSWORD "?????????????????" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen + +// zur Berechnung von Sonnenauf- und -untergang +#define geoBreite 49.2866 // <<<<<<<<<<<<<<<<<<<<<<<< anpassen +#define geoLaenge 7.3416 // <<<<<<<<<<<<<<<<<<<<<<<< anpassen + + +#endif diff --git a/tools/HoyDtuSim/Sonne.h b/tools/HoyDtuSim/Sonne.h new file mode 100644 index 00000000..e80a8356 --- /dev/null +++ b/tools/HoyDtuSim/Sonne.h @@ -0,0 +1,55 @@ +#ifndef __SONNE_H +#define __SONNE_H + +#include "Settings.h" +#include "Debug.h" + + +long SunDown, SunUp; + +void calcSunUpDown (time_t date) { + //SunUpDown res = new SunUpDown(); + boolean isSummerTime = false; // TODO TimeZone.getDefault().inDaylightTime(new Date(date)); + + //- Bogenmass + double brad = geoBreite / 180.0 * PI; + // - Höhe Sonne -50 Bogenmin. + double h0 = -50.0 / 60.0 / 180.0 * PI; + //- Deklination dek, Tag des Jahres d0 + int tage = 30 * month(date) - 30 + day(date); + double dek = 0.40954 * sin (0.0172 * (tage - 79.35)); + double zh1 = sin (h0) - sin (brad) * sin(dek); + double zh2 = cos(brad) * cos(dek); + double zd = 12*acos (zh1/zh2) / PI; + double zgl = -0.1752 * sin (0.03343 * tage + 0.5474) - 0.134 * sin (0.018234 * tage - 0.1939); + //-Sonnenuntergang + double tsu = 12 + zd - zgl; + double su = (tsu + (15.0 - geoLaenge) / 15.0); + int std = (int)su; + int minute = (int) ((su - std)*60); + if (isSummerTime) std++; + SunDown = (100*std + minute) * 100; + + //- Sonnenaufgang + double tsa = 12 - zd - zgl; + double sa = (tsa + (15.0 - geoLaenge) /15.0); + std = (int) sa; + minute = (int) ((sa - std)*60); + if (isSummerTime) std++; + SunUp = (100*std + minute) * 100; + DEBUG_OUT.print(F("Sonnenaufgang :")); DEBUG_OUT.println(SunUp); + DEBUG_OUT.print(F("Sonnenuntergang:")); DEBUG_OUT.println(SunDown); +} + +boolean isDayTime() { +//----------------- +// 900 = 15 Minuten, vor Sonnenaufgang und nach -untergang + const int offset=60*15; + time_t no = getNow(); + long jetztMinuteU = (100 * hour(no+offset) + minute(no+offset)) * 100; + long jetztMinuteO = (100 * hour(no-offset) + minute(no-offset)) * 100; + + return ((jetztMinuteU >= SunUp) &&(jetztMinuteO <= SunDown)); +} + +#endif diff --git a/tools/HoyDtuSim/hm_crc.h b/tools/HoyDtuSim/hm_crc.h new file mode 100644 index 00000000..8a029619 --- /dev/null +++ b/tools/HoyDtuSim/hm_crc.h @@ -0,0 +1,102 @@ +#ifndef __HM_CRC_H +#define __HM_CRC_H + +#define BITS_TO_BYTES(x) (((x)+7)>>3) +#define BYTES_TO_BITS(x) ((x)<<3) + +extern uint16_t crc16_modbus(uint8_t *puchMsg, uint16_t usDataLen); +extern uint8_t crc8(uint8_t *buf, const uint16_t bufLen); +extern uint16_t crc16(uint8_t* buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits); + +//#define OUTPUT_DEBUG_INFO + +#define CRC8_INIT 0x00 +#define CRC8_POLY 0x01 + +#define CRC16_MODBUS_POLYNOM 0xA001 + +uint8_t crc8(uint8_t buf[], uint16_t len) { + uint8_t crc = CRC8_INIT; + for(uint8_t i = 0; i < len; i++) { + crc ^= buf[i]; + for(uint8_t b = 0; b < 8; b ++) { + crc = (crc << 1) ^ ((crc & 0x80) ? CRC8_POLY : 0x00); + } + } + return crc; +} + +uint16_t crc16_modbus(uint8_t buf[], uint16_t len) { + uint16_t crc = 0xffff; + uint8_t lsb; + + for(uint8_t i = 0; i < len; i++) { + crc = crc ^ buf[i]; + for(int8_t b = 7; b >= 0; b--) { + lsb = (crc & 0x0001); + if(lsb == 0x01) + crc--; + crc = crc >> 1; + if(lsb == 0x01) + crc = crc ^ CRC16_MODBUS_POLYNOM; + } + } + + return crc; +} + +// NRF24 CRC16 calculation with poly 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1 +uint16_t crc16(uint8_t *buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits) +{ + uint16_t crc = startCRC; + if ((len_bits > 0) && (len_bits <= BYTES_TO_BITS(bufLen))) + { + // The length of the data might not be a multiple of full bytes. + // Therefore we proceed over the data bit-by-bit (like the NRF24 does) to + // calculate the CRC. + uint16_t data; + uint8_t byte, shift; + uint16_t bitoffs = startBit; + + // Get a new byte for the next 8 bits. + byte = buf[bitoffs >> 3]; +#ifdef OUTPUT_DEBUG_INFO + printf_P(PSTR("\nStart CRC %04X, %u bits:"), startCRC, len_bits); + printf_P(PSTR("\nbyte %02X:"), byte); +#endif + while (bitoffs < len_bits + startBit) + { + shift = bitoffs & 7; + // Shift the active bit to the position of bit 15 + data = ((uint16_t)byte) << (8 + shift); +#ifdef OUTPUT_DEBUG_INFO + printf_P(PSTR(" bit %u %u,"), shift, data & 0x8000 ? 1 : 0); +#endif + // Assure all other bits are 0 + data &= 0x8000; + crc ^= data; + if (crc & 0x8000) + { + crc = (crc << 1) ^ 0x1021; // 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1 + } + else + { + crc = (crc << 1); + } + ++bitoffs; + if (0 == (bitoffs & 7)) + { + // Get a new byte for the next 8 bits. + byte = buf[bitoffs >> 3]; +#ifdef OUTPUT_DEBUG_INFO + printf_P(PSTR("crc %04X:"), crc); + if (bitoffs < len_bits + startBit) + printf_P(PSTR("\nbyte %02X:"), byte); +#endif + } + } + } + return crc; +} + +#endif diff --git a/tools/HoyDtuSim/hm_packets.h b/tools/HoyDtuSim/hm_packets.h new file mode 100644 index 00000000..c6959cd9 --- /dev/null +++ b/tools/HoyDtuSim/hm_packets.h @@ -0,0 +1,93 @@ +#ifndef __HM_PACKETS_H +#define __HM_PACKETS_H + +#include "hm_crc.h" + +class HM_Packets +{ +private: + uint32_t unixTimeStamp; + + void prepareBuffer(uint8_t *buf); + void copyToBuffer(uint8_t *buf, uint32_t val); + void copyToBufferBE(uint8_t *buf, uint32_t val); + +public: + void SetUnixTimeStamp(uint32_t ts); + void UnixTimeStampTick(); + + int32_t GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr); + int32_t GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd); +}; + +void HM_Packets::SetUnixTimeStamp(uint32_t ts) +{ + unixTimeStamp = ts; +} + +void HM_Packets::UnixTimeStampTick() +{ + unixTimeStamp++; +} + +void HM_Packets::prepareBuffer(uint8_t *buf) +{ + // minimal buffer size of 32 bytes is assumed + memset(buf, 0x00, 32); +} + +void HM_Packets::copyToBuffer(uint8_t *buf, uint32_t val) +{ + buf[0]= (uint8_t)(val >> 24); + buf[1]= (uint8_t)(val >> 16); + buf[2]= (uint8_t)(val >> 8); + buf[3]= (uint8_t)(val & 0xFF); +} + +void HM_Packets::copyToBufferBE(uint8_t *buf, uint32_t val) +{ + memcpy(buf, &val, sizeof(uint32_t)); +} + +static uint8_t cid = 0; + +int32_t HM_Packets::GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr) +{ + prepareBuffer(buf); + + buf[0] = 0x15; + copyToBufferBE(&buf[1], wrAdr); + copyToBufferBE(&buf[5], dtuAdr); + buf[9] = 0x80; + buf[10] = 0x0B; //0x0B; 0x03 0x11 + buf[11] = 0x00; + + copyToBuffer(&buf[12], unixTimeStamp); + + buf[19] = 0x05; + + // CRC16 + uint16_t crc16 = crc16_modbus(&buf[10], 14); + buf[24] = crc16 >> 8; + buf[25] = crc16 & 0xFF; + + // crc8 + buf[26] = crc8(&buf[0], 26); + + return 27; +} + +int32_t HM_Packets::GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd) +{ + buf[0] = mid; + copyToBufferBE(&buf[1], wrAdr); + copyToBufferBE(&buf[5], dtuAdr); + buf[9] = cmd; + + // crc8 + buf[10] = crc8(&buf[0], 10); + + return 11; +} + +#endif diff --git a/tools/HoyDtuSim/wifi.h b/tools/HoyDtuSim/wifi.h new file mode 100644 index 00000000..d98cc40f --- /dev/null +++ b/tools/HoyDtuSim/wifi.h @@ -0,0 +1,345 @@ +#ifndef __WIFI_H +#define __WIFI_H + +#include "Settings.h" +#include "Debug.h" +#include +#include // von url=https://www.technologytourist.com + +String SSID = ""; // bestes WLan + +// Prototypes +time_t getNow (); +boolean setupWifi (); +boolean checkWifi(); + + +String findWifi () { +//---------------- + String ssid; + int32_t rssi; + uint8_t encryptionType; + uint8_t* bssid; + int32_t channel; + bool hidden; + int scanResult; + + String best_ssid = ""; + int32_t best_rssi = -100; + + DEBUG_OUT.println(F("Starting WiFi scan...")); + + scanResult = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true); + + if (scanResult == 0) { + DEBUG_OUT.println(F("keine WLans")); + } else if (scanResult > 0) { + DEBUG_OUT.printf(PSTR("%d WLans gefunden:\n"), scanResult); + + // Print unsorted scan results + for (int8_t i = 0; i < scanResult; i++) { + WiFi.getNetworkInfo(i, ssid, encryptionType, rssi, bssid, channel, hidden); + + DEBUG_OUT.printf(PSTR(" %02d: [CH %02d] [%02X:%02X:%02X:%02X:%02X:%02X] %ddBm %c %c %s\n"), + i, + channel, + bssid[0], bssid[1], bssid[2], + bssid[3], bssid[4], bssid[5], + rssi, + (encryptionType == ENC_TYPE_NONE) ? ' ' : '*', + hidden ? 'H' : 'V', + ssid.c_str()); + delay(1); + boolean check; + #ifdef SSID_PREFIX1 + check = ssid.substring(0,strlen(SSID_PREFIX1)).equals(SSID_PREFIX1); + #else + check = true; + #endif + #ifdef SSID_PREFIX2 + check = check || ssid.substring(0,strlen(SSID_PREFIX2)).equals(SSID_PREFIX2); + #endif + if (check) { + if (rssi > best_rssi) { + best_rssi = rssi; + best_ssid = ssid; + } + } + } + } else { + DEBUG_OUT.printf(PSTR("WiFi scan error %d"), scanResult); + } + + if (! best_ssid.equals("")) { + SSID = best_ssid; + DEBUG_OUT.printf ("Bestes Wifi unter: %s\n", SSID.c_str()); + return SSID; + } + else + return ""; +} + +void IP2string (IPAddress IP, char * buf) { + sprintf (buf, "%d.%d.%d.%d", IP[0], IP[1], IP[2], IP[3]); +} + +void connectWifi() { +//------------------ +// if (SSID.equals("")) + String s = findWifi(); + + if (!SSID.equals("")) { + DEBUG_OUT.print("versuche zu verbinden mit "); DEBUG_OUT.println(SSID); + //while (WiFi.status() != WL_CONNECTED) { + WiFi.begin (SSID, SSID_PASSWORD); + int versuche = 20; + while (WiFi.status() != WL_CONNECTED && versuche > 0) { + delay(1000); + versuche--; + DEBUG_OUT.print(versuche); DEBUG_OUT.print(' '); + } + //} + if (WiFi.status() == WL_CONNECTED) { + char buffer[30]; + IP2string (WiFi.localIP(), buffer); + String out = "\n[WiFi]Verbunden; meine IP:" + String (buffer); + DEBUG_OUT.println (out); + } + else + DEBUG_OUT.print("\nkeine Verbindung mit SSID "); DEBUG_OUT.println(SSID); + } +} + + +boolean setupWifi () { +//------------------ + int count=5; + while (count-- && WiFi.status() != WL_CONNECTED) + connectWifi(); + return (WiFi.status() == WL_CONNECTED); +} + + +Pinger pinger; +IPAddress ROUTER = IPAddress(192,168,1,1); + +boolean checkWifi() { +//--------------- + boolean NotConnected = (WiFi.status() != WL_CONNECTED) || !pinger.Ping(ROUTER); + if (NotConnected) { + setupWifi(); + if (WiFi.status() == WL_CONNECTED) + getNow(); + } + return (WiFi.status() == WL_CONNECTED); +} + + + +// ################ Clock ################# + +#include +#include + +IPAddress timeServer; +unsigned int localPort = 8888; +const int NTP_PACKET_SIZE= 48; // NTP time stamp is in the first 48 bytes of the message +byte packetBuf[NTP_PACKET_SIZE]; // Buffer to hold incoming and outgoing packets +const int timeZone = 1; // Central European Time = +1 +long SYNCINTERVALL = 0; +WiFiUDP Udp; // A UDP instance to let us send and receive packets over UDP + +// prototypes +time_t getNtpTime (); +void sendNTPpacket (IPAddress &address); +time_t getNow (); +char* getDateTimeStr (time_t no = getNow()); +time_t offsetDayLightSaving (uint32_t local_t); +bool isDayofDaylightChange (time_t local_t); + + +void _setSyncInterval (long intervall) { +//---------------------------------------- + SYNCINTERVALL = intervall; + setSyncInterval (intervall); +} + +void setupClock() { +//----------------- + WiFi.hostByName (TIMESERVER_NAME,timeServer); // at this point the function works + + Udp.begin(localPort); + + getNtpTime(); + + setSyncProvider (getNtpTime); + while(timeStatus()== timeNotSet) + delay(1); // + + _setSyncInterval (SECS_PER_DAY / 2); // Set seconds between re-sync + + //lastClock = now(); + //Serial.print("[NTP] get time from NTP server "); + getNow(); + //char buf[20]; + DEBUG_OUT.print ("[NTP] get time from NTP server "); + DEBUG_OUT.print (timeServer); + //sprintf (buf, ": %02d:%02d:%02d", hour(no), minute(no), second(no)); + DEBUG_OUT.print (": got "); + DEBUG_OUT.println (getDateTimeStr()); +} + +//*-------- NTP code ----------*/ + + +time_t getNtpTime() { +//------------------- + sendNTPpacket(timeServer); // send an NTP packet to a time server + //uint32_t beginWait = millis(); + //while (millis() - beginWait < 1500) { + int versuch = 0; + while (versuch < 5) { + int wait = 150; // results in max 1500 ms waitTime + while (wait--) { + int size = Udp.parsePacket(); + if (size >= NTP_PACKET_SIZE) { + //Serial.println("Receive NTP Response"); + Udp.read(packetBuf, NTP_PACKET_SIZE); // read packet into the buffer + unsigned long secsSince1900; + // convert four bytes starting at location 40 to a long integer + secsSince1900 = (unsigned long)packetBuf[40] << 24; + secsSince1900 |= (unsigned long)packetBuf[41] << 16; + secsSince1900 |= (unsigned long)packetBuf[42] << 8; + secsSince1900 |= (unsigned long)packetBuf[43]; + // time_t now = secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR; + + time_t utc = secsSince1900 - 2208988800UL; + time_t now = utc + (timeZone +offsetDayLightSaving(utc)) * SECS_PER_HOUR; + + if (isDayofDaylightChange (utc) && hour(utc) <= 4) + _setSyncInterval (SECS_PER_HOUR); + else + _setSyncInterval (SECS_PER_DAY / 2); + + return now; + } + else + delay(10); + } + versuch++; + } + return 0; +} + +// send an NTP request to the time server at the given address +void sendNTPpacket(IPAddress& address) { +//------------------------------------ + memset(packetBuf, 0, NTP_PACKET_SIZE); // set all bytes in the buffer to 0 + // Initialize values needed to form NTP request + packetBuf[0] = B11100011; // LI, Version, Mode + packetBuf[1] = 0; // Stratum + packetBuf[2] = 6; // Max Interval between messages in seconds + packetBuf[3] = 0xEC; // Clock Precision + // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset + packetBuf[12] = 49; // four-byte reference ID identifying + packetBuf[13] = 0x4E; + packetBuf[14] = 49; + packetBuf[15] = 52; + // send the packet requesting a timestamp: + Udp.beginPacket(address, 123); //NTP requests are to port 123 + Udp.write(packetBuf,NTP_PACKET_SIZE); + Udp.endPacket(); + +} + +int getTimeTrials = 0; + +bool isValidDateTime (time_t no) { + return (year(no) > 2020 && year(no) < 2038); +} + +bool isDayofDaylightChange (time_t local_t) { +//----------------------------------------- + int jahr = year (local_t); + int monat = month (local_t); + int tag = day (local_t); + bool ret = ( (monat ==3 && tag == (31 - (5 * jahr /4 + 4) % 7)) || + (monat==10 && tag == (31 - (5 * jahr /4 + 1) % 7))); + DEBUG_OUT.print ("isDayofDaylightChange="); DEBUG_OUT.println (ret); + return ret; +} + +// calculates the daylight saving time for middle Europe. Input: Unixtime in UTC (!) +// übernommen von Jurs, see : https://forum.arduino.cc/index.php?topic=172044.msg1278536#msg1278536 +time_t offsetDayLightSaving (uint32_t local_t) { +//-------------------------------------------- + int monat = month (local_t); + if (monat < 3 || monat > 10) return 0; // no DSL in Jan, Feb, Nov, Dez + if (monat > 3 && monat < 10) return 1; // DSL in Apr, May, Jun, Jul, Aug, Sep + int jahr = year (local_t); + int std = hour (local_t); + //int tag = day (local_t); + int stundenBisHeute = (std + 24 * day(local_t)); + if ( (monat == 3 && stundenBisHeute >= (1 + timeZone + 24 * (31 - (5 * jahr /4 + 4) % 7))) || + (monat == 10 && stundenBisHeute < (1 + timeZone + 24 * (31 - (5 * jahr /4 + 1) % 7))) ) + return 1; + else + return 0; + /* + int stundenBisWechsel = (1 + 24 * (31 - (5 * year(local_t) / 4 + 4) % 7)); + if (monat == 3 && stundenBisHeute >= stundenBisWechsel || monat == 10 && stundenBisHeute < stundenBisWechsel) + return 1; + else + return 0; + */ +} + + +time_t getNow () { +//--------------- + time_t jetzt = now(); + while (!isValidDateTime(jetzt) && getTimeTrials < 10) { // ungültig, max 10x probieren + if (getTimeTrials) { + //Serial.print (getTimeTrials); + //Serial.println(". Versuch für getNtpTime"); + } + jetzt = getNtpTime (); + if (isValidDateTime(jetzt)) { + setTime (jetzt); + getTimeTrials = 0; + } + else + getTimeTrials++; + } + //return jetzt + offsetDayLightSaving(jetzt)*SECS_PER_HOUR; + return jetzt; +} + + +char _timestr[24]; + +char* getNowStr (time_t no = getNow()) { +//------------------------------------ + sprintf (_timestr, "%02d:%02d:%02d", hour(no), minute(no), second(no)); + return _timestr; +} + +char* getTimeStr (time_t no = getNow()) { +//------------------------------------ + return getNowStr (no); +} + +char* getDateTimeStr (time_t no) { +//------------------------------ + sprintf (_timestr, "%04d-%02d-%02d+%02d:%02d:%02d", year(no), month(no), day(no), hour(no), minute(no), second(no)); + return _timestr; +} + +char* getDateStr (time_t no) { +//------------------------------ + sprintf (_timestr, "%04d-%02d-%02d", year(no), month(no), day(no)); + return _timestr; +} + + +#endif diff --git a/tools/NRF24_SendRcv/CircularBuffer.h b/tools/NRF24_SendRcv/CircularBuffer.h new file mode 100644 index 00000000..a7fafdb7 --- /dev/null +++ b/tools/NRF24_SendRcv/CircularBuffer.h @@ -0,0 +1,158 @@ +/* + CircularBuffer - An Arduino circular buffering library for arbitrary types. + + Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef CircularBuffer_h +#define CircularBuffer_h + +#ifdef ESP8266 +#define DISABLE_IRQ noInterrupts() +#define RESTORE_IRQ interrupts() +#else +#define DISABLE_IRQ \ + uint8_t sreg = SREG; \ + cli(); + +#define RESTORE_IRQ \ + SREG = sreg; +#endif + +template class CircularBuffer +{ + public: + /** Constructor + * @param buffer Preallocated buffer of at least size records. + * @param size Number of records available in the buffer. + */ + CircularBuffer(T* buffer, const uint8_t size ) + : m_size(size), m_buff(buffer) + { + clear(); + } + + /** Clear all entries in the circular buffer. */ + void clear(void) + { + m_front = 0; + m_fill = 0; + } + + /** Test if the circular buffer is empty */ + inline bool empty(void) const + { + return !m_fill; + } + + /** Return the number of records stored in the buffer */ + inline uint8_t available(void) const + { + return m_fill; + } + + /** Test if the circular buffer is full */ + inline bool full(void) const + { + return m_fill == m_size; + } + + /** Aquire record on front of the buffer, for writing. + * After filling the record, it has to be pushed to actually + * add it to the buffer. + * @return Pointer to record, or NULL when buffer is full. + */ + T* getFront(void) const + { + DISABLE_IRQ; + T* f = NULL; + if (!full()) + f = get(m_front); + RESTORE_IRQ; + return f; + } + + /** Push record to front of the buffer + * @param record Record to push. If record was aquired previously (using getFront) its + * data will not be copied as it is already present in the buffer. + * @return True, when record was pushed successfully. + */ + bool pushFront(T* record) + { + bool ok = false; + DISABLE_IRQ; + if (!full()) + { + T* f = get(m_front); + if (f != record) + *f = *record; + m_front = (m_front+1) % m_size; + m_fill++; + ok = true; + } + RESTORE_IRQ; + return ok; + } + + /** Aquire record on back of the buffer, for reading. + * After reading the record, it has to be pop'ed to actually + * remove it from the buffer. + * @return Pointer to record, or NULL when buffer is empty. + */ + T* getBack(void) const + { + T* b = NULL; + DISABLE_IRQ; + if (!empty()) + b = get(back()); + RESTORE_IRQ; + return b; + } + + /** Remove record from back of the buffer. + * @return True, when record was pop'ed successfully. + */ + bool popBack(void) + { + bool ok = false; + DISABLE_IRQ; + if (!empty()) + { + m_fill--; + ok = true; + } + RESTORE_IRQ; + return ok; + } + + protected: + inline T * get(const uint8_t idx) const + { + return &(m_buff[idx]); + } + inline uint8_t back(void) const + { + return (m_front - m_fill + m_size) % m_size; + } + + const uint8_t m_size; // Total number of records that can be stored in the buffer. + T* const m_buff; // Ptr to buffer holding all records. + volatile uint8_t m_front; // Index of front element (not pushed yet). + volatile uint8_t m_fill; // Amount of records currently pushed. +}; + +#endif // CircularBuffer_h diff --git a/tools/NRF24_SendRcv/Debug.h b/tools/NRF24_SendRcv/Debug.h new file mode 100644 index 00000000..3b2807d6 --- /dev/null +++ b/tools/NRF24_SendRcv/Debug.h @@ -0,0 +1,23 @@ +#ifndef __DEBUG_H + +#define __DEBUG_H + +#ifdef DEBUG + #define DEBUG_OUT Serial +#else +//--- +// disable Serial DEBUG output + #define DEBUG_OUT DummySerial + static class { + public: + void begin(...) {} + void print(...) {} + void println(...) {} + void flush() {} + bool available() { return false;} + int readBytes(...) { return 0;} + int printf (...) {return 0;} + } DummySerial; +#endif + +#endif diff --git a/tools/NRF24_SendRcv/ModWebserver.h b/tools/NRF24_SendRcv/ModWebserver.h new file mode 100644 index 00000000..38bb77f5 --- /dev/null +++ b/tools/NRF24_SendRcv/ModWebserver.h @@ -0,0 +1,129 @@ +// ################# WebServer ################# + +#ifndef __MODWEBSERVER_H +#define __MODWEBSERVER_H +#define MODWEBSERVER + +#include +#include "Debug.h" +#include "Settings.h" + +ESP8266WebServer server (WEBSERVER_PORT); + + +void returnOK () { + //-------------- + server.send(200, F("text/plain"), ""); +} + + +void returnFail(String msg) { + //------------------------- + server.send(500, F("text/plain"), msg + "\r\n"); +} + +void handleHelp () { +//----------------- + String out = ""; + out += "

Hilfe

"; + out += "

"; + out += ""; + out += ""; + out += ""; + out += ""; + out += "
/zeigt alle Messwerte in einer Tabelle; refresh alle 10 Sekunden
/datazum Abruf der Messwerte in der Form Name=wert
:{port+1}/updateOTA
/rebootstartet neu
"; + server.send (200, "text/html", out); +} + + +void handleReboot () { + //------------------- + returnOK (); + ESP.reset(); +} + + +void handleRoot() { + //---------------- + String out = ""; + out += ""; + out += "

Hoymiles Micro-Inverter HM-600

"; + out += "

"; + out += ""; + for (byte i = 0; i < ANZAHL_VALUES; i++) { + out += ""; + out += ""; + } + out += "
KanalWert
" + String(getChannelName(i)) + "" + String(VALUES[i]) + "
"; + out += ""; + server.send (200, "text/html", out); + //DEBUG_OUT.println (out); +} + + +void handleData () { +//----------------- + String out = ""; + for (int i = 0; i < ANZAHL_VALUES; i++) { + out += String(getChannelName(i)) + '=' + String (VALUES[i]) + '\n'; + } + server.send(200, "text/plain", out); +} + + +void handleNotFound() { +//-------------------- + String message = "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + for (uint8_t i = 0; i < server.args(); i++) { + message += " NAME:" + server.argName(i) + "\n VALUE:" + server.arg(i) + "\n"; + } + server.send(404, "text/plain", message); +} + + +void setupWebServer (void) { + //------------------------- + server.on("/", handleRoot); + server.on("/reboot", handleReboot); + server.on("/data", handleData); + server.on("/help", handleHelp); + //server.onNotFound(handleNotFound); wegen Spiffs-Dateimanager + + server.begin(); + DEBUG_OUT.println ("[HTTP] installed"); +} + +void webserverHandle() { +//==================== + server.handleClient(); +} + + +// ################# OTA ################# + +#ifdef WITH_OTA +#include + +ESP8266WebServer httpUpdateServer (UPDATESERVER_PORT); +ESP8266HTTPUpdateServer httpUpdater; + +void setupUpdateByOTA () { + //------------------------ + httpUpdater.setup (&httpUpdateServer, UPDATESERVER_DIR, UPDATESERVER_USER, UPDATESERVER_PW); + httpUpdateServer.begin(); + DEBUG_OUT.println (F("[OTA] installed")); +} + +void checkUpdateByOTA() { +//--------------------- + httpUpdateServer.handleClient(); +} +#endif + +#endif diff --git a/tools/NRF24_SendRcv/NRF24_SendRcv.ino b/tools/NRF24_SendRcv/NRF24_SendRcv.ino new file mode 100644 index 00000000..9eed320d --- /dev/null +++ b/tools/NRF24_SendRcv/NRF24_SendRcv.ino @@ -0,0 +1,597 @@ +#include +#include +#include "CircularBuffer.h" +#include +#include +#include "hm_crc.h" +#include "hm_packets.h" + +#include "Settings.h" // Header für Einstellungen + +#include "Debug.h" + +#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 + + +#define RF_MAX_ADDR_WIDTH (5) +#define MAX_RF_PAYLOAD_SIZE (32) + +#ifdef ESP8266 +#define PACKET_BUFFER_SIZE (30) +#else +#define PACKET_BUFFER_SIZE (20) +#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 +#define DEFAULT_RF_DATARATE (RF24_250KBPS) // Datarate + +#include "NRF24_sniff_types.h" + +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 radio1 (RF1_CE_PIN, RF1_CS_PIN); + +static NRF24_packet_t bufferData[PACKET_BUFFER_SIZE]; + +static CircularBuffer 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 + +static unsigned long timeLastPacket = millis(); + +// Function forward declaration +static void SendPacket(uint64_t dest, uint8_t *buf, uint8_t len); +char * getChannelName (uint8_t i); + +static const int ANZAHL_VALUES = 16; +static float VALUES[ANZAHL_VALUES] = {}; +static const char *CHANNEL_NAMES[ANZAHL_VALUES] + = {"P1.Udc", "P1.Idc", "P1.Pdc", "P2.Udc", "P2.Idc", "P2.Pdc", + "E-Woche", "E-Total", "E1-Tag", "E2-Tag", "Uac", "Freq.ac", "Pac", "E-heute", "Ipv", "WR-Temp"}; +static const uint8_t DIVISOR[ANZAHL_VALUES] = {10,100,10,10,100,10,1,1,1,1,10,100,10,0,0,10}; + +static const char BLANK = ' '; + +static boolean istTag = true; + +char CHANNELNAME_BUFFER[15]; + +#ifdef ESP8266 + #include "wifi.h" + #include "ModWebserver.h" + #include "Sonne.h" +#endif + +char * getChannelName (uint8_t i) { +//------------------------------- + memset (CHANNELNAME_BUFFER, 0, sizeof(CHANNELNAME_BUFFER)); + strcpy (CHANNELNAME_BUFFER, CHANNEL_NAMES[i]); + //itoa (i, CHANNELNAME_BUFFER, 10); + return CHANNELNAME_BUFFER; +} + +inline static void dumpData(uint8_t *p, int len) { +//----------------------------------------------- + while (len--){ + if (*p < 16) + DEBUG_OUT.print(F("0")); + DEBUG_OUT.print(*p++, HEX); + } + DEBUG_OUT.print(BLANK); +} + + +float extractValue2 (uint8_t *p, int divisor) { +//------------------------------------------- + uint16_t b1 = *p++; + return ((float) (b1 << 8) + *p) / (float) divisor; +} + + +float extractValue4 (uint8_t *p, int divisor) { +//------------------------------------------- + uint32_t ret = *p++; + for (uint8_t i = 1; i <= 3; i++) + ret = (ret << 8) + *p++; + return (ret / divisor); +} + +void outChannel (uint8_t i) { +//------------------------- + DEBUG_OUT.print(getChannelName(i)); DEBUG_OUT.print(F("\t:")); DEBUG_OUT.print(VALUES[i]); DEBUG_OUT.println(BLANK); +} + + +void analyse01 (uint8_t *p) { // p zeigt auf 01 hinter 2. WR-Adr +//---------------------------------- + //uint16_t val; + //DEBUG_OUT.print (F("analyse 01: ")); + p += 3; + // PV1.U PV1.I PV1.P PV2.U PV2.I PV2.P + // [0.1V] [0.01A] [.1W] [0.1V] [0.01A] [.1W] + for (int i = 0; i < 6; i++) { + VALUES[i] = extractValue2 (p,DIVISOR[i]); p += 2; + outChannel(i); + } +/* + DEBUG_OUT.print(F("PV1.U:")); DEBUG_OUT.print(extractValue2(p,10)); + p += 2; + DEBUG_OUT.print(F(" PV1.I:")); DEBUG_OUT.print(extractValue2(p,100)); + p += 2; + DEBUG_OUT.print(F(" PV1.Pac:")); DEBUG_OUT.print(extractValue2(p,10)); + p += 2; + DEBUG_OUT.print(F(" PV2.U:")); DEBUG_OUT.print(extractValue2(p,10)); + p += 2; + DEBUG_OUT.print(F(" PV2.I:")); DEBUG_OUT.print(extractValue2(p,100)); + p += 2; + DEBUG_OUT.print(F(" PV2.Pac:")); DEBUG_OUT.print(extractValue2(p,10)); +*/ + DEBUG_OUT.println(); +} + + +void analyse02 (uint8_t *p) { // p zeigt auf 02 hinter 2. WR-Adr +//---------------------------------- + //uint16_t val; + //DEBUG_OUT.print (F("analyse 02: ")); + // +11 = Spannung, +13 = Frequenz, +15 = Leistung + //p += 11; + p++; + for (int i = 6; i < 13; i++) { + if (i == 7) { + VALUES[i] = extractValue4 (p,DIVISOR[i]); + p += 4; + } + else { + VALUES[i] = extractValue2 (p,DIVISOR[i]); + p += 2; + } + outChannel(i); + } + VALUES[13] = VALUES[8] + VALUES[9]; // E-heute = P1+P2 + if (VALUES[10] > 0) + VALUES[14] = VALUES[12] / VALUES[10]; // Ipv = Pac / Spannung +/* + DEBUG_OUT.print(F("P Woche:")); DEBUG_OUT.print(extractValue2(p,1)); + p += 2; + DEBUG_OUT.print(F(" P Total:")); DEBUG_OUT.print(extractValue4(p,1)); + p += 4; + DEBUG_OUT.print(F(" P1 Tag:")); DEBUG_OUT.print(extractValue2(p,1)); + p += 2; + DEBUG_OUT.print(F(" P2 Tag:")); DEBUG_OUT.print(extractValue2(p,1)); + p += 2; + + DEBUG_OUT.print(F(" Spannung:")); DEBUG_OUT.print(extractValue2(p,10)); + p += 2; + DEBUG_OUT.print(F(" Freq.:")); DEBUG_OUT.print(extractValue2(p,100)); + p += 2; + DEBUG_OUT.print(F(" Leist.:")); DEBUG_OUT.print(extractValue2(p,10)); +*/ + DEBUG_OUT.println(); +} + + +void analyse83 (uint8_t *p) { // p zeigt auf 83 hinter 2. WR-Adr +//---------------------------------- + //uint16_t val; + //DEBUG_OUT.print (F("++++++analyse 83:")); + p += 7; + VALUES[15] = extractValue2 (p,DIVISOR[15]); + outChannel(15); + DEBUG_OUT.println(); +} + +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(extractValue2(p,1)); + DEBUG_OUT.print(BLANK); + p++; + } + DEBUG_OUT.println(); +} + +void analyseLongs (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(extractValue4(p,1)); + DEBUG_OUT.print(BLANK); + p++; + } + DEBUG_OUT.println(); +} + + +#ifdef ESP8266 +IRAM_ATTR +#endif +void handleNrf1Irq() { +//------------------------- + static uint8_t lostPacketCount = 0; + uint8_t pipe; + + DISABLE_EINT; + + // Loop until RX buffer(s) contain no more packets. + while (radio1.available(&pipe)) { + 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; + uint8_t packetLen = radio1.getPayloadSize(); + if (packetLen > MAX_RF_PAYLOAD_SIZE) + packetLen = MAX_RF_PAYLOAD_SIZE; + + radio1.read(p->packet, packetLen); + 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. + radio1.whatHappened(tx_ok, tx_fail, rx_ready); + // Flush buffer to drop the packet. + radio1.flush_rx(); + } + } + ENABLE_EINT; +} + + +static void activateConf(void) { +//----------------------------- + radio1.setChannel(DEFAULT_RECV_CHANNEL); + radio1.setDataRate(DEFAULT_RF_DATARATE); + radio1.disableCRC(); + radio1.setAutoAck(0x00); + radio1.setPayloadSize(MAX_RF_PAYLOAD_SIZE); + radio1.setAddressWidth(5); + radio1.openReadingPipe(1, DTU_RADIO_ID); + + // We want only RX irqs + radio1.maskIRQ(true, true, false); + + // Use lo PA level, as a higher level will disturb CH340 DEBUG_OUT usb adapter + radio1.setPALevel(RF24_PA_MAX); + radio1.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; + } + +#ifndef ESP8266 + DEBUG_OUT.println(F("\nRadio Config:")); + radio1.printPrettyDetails(); + DEBUG_OUT.println(); +#endif + tickMillis = millis() + 200; +} + + +void setup(void) { +//-------------- + //Serial.begin(SER_BAUDRATE); + DEBUG_OUT.begin(SER_BAUDRATE); + DEBUG_OUT.flush(); + + DEBUG_OUT.println(F("-- Hoymiles DTU Simulation --")); + + radio1.begin(); + + // Disable shockburst for receiving and decode payload manually + radio1.setAutoAck(false); + radio1.setRetries(0, 0); + + // Configure nRF IRQ input + pinMode(RF1_IRQ_PIN, INPUT); + + activateConf(); + +#ifdef ESP8266 + setupWifi(); + setupClock(); + setupWebServer(); + setupUpdateByOTA(); + calcSunUpDown (getNow()); + istTag = isDayTime(); + DEBUG_OUT.print ("Es ist "); DEBUG_OUT.println (istTag?"Tag":"Nacht"); + hmPackets.SetUnixTimeStamp (getNow()); +#else + hmPackets.SetUnixTimeStamp(0x62456430); +#endif +} + + uint8_t sendBuf[MAX_RF_PAYLOAD_SIZE]; + +void isTime2Send () { +//----------------- + // Second timer + + if (millis() >= tickMillis) { + static uint8_t tel = 0; + tickMillis += 1000; //200; + //tickSec++; + hmPackets.UnixTimeStampTick(); +/* if (++tickSec >= 5) { // 5 + hmPackets.UnixTimeStampTick(); + tickSec = 0; + } */ + + int32_t size = 0; + uint64_t dest = WR1_RADIO_ID; + + if (tel > 5) + tel = 0; + + if (tel == 0) { + #ifdef ESP8266 + hmPackets.SetUnixTimeStamp (getNow()); + #endif + size = hmPackets.GetTimePacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8); + } + else if (tel == 1) + size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x81); + else if (tel == 2) + size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x80); + else if (tel == 3) { + size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x83); + //tel = 0; + } + else if (tel == 4) + size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x82); + else if (tel == 5) + size = hmPackets.GetCmdPacket((uint8_t *)&sendBuf, dest >> 8, DTU_RADIO_ID >> 8, 0x15, 0x84); + + SendPacket(dest, (uint8_t *)&sendBuf, size); + + tel++; + +/* for (uint8_t warte = 0; warte < 2; warte++) { + delay(1000); + hmPackets.UnixTimeStampTick(); + }*/ + } +} + + +void outputPacket(NRF24_packet_t *p, uint8_t payloadLen) { +//----------------------------------------------------- + + // Write timestamp, packets lost, address and payload length + //printf(" %09lu ", SerialHdr.timestamp); + 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(); +} + + +void loop(void) { +//============= + 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 + for (int16_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); + } + + SerialHdr.timestamp = p->timestamp; + SerialHdr.packetsLost = p->packetsLost; + + // Check CRC + crc = 0xFFFF; + crc = crc16((uint8_t *)&SerialHdr.address, sizeof(SerialHdr.address), crc, 0, BYTES_TO_BITS(sizeof(SerialHdr.address))); + // Payload length + uint8_t payloadLen = ((p->packet[0] & 0x01) << 5) | (p->packet[1] >> 3); + // 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(); + continue; + } + + // Dump a decoded packet only once + if (lastCRC == crc) { + packetBuffer.popBack(); + continue; + } + lastCRC = crc; + } + + // Don't dump mysterious ack packages + if (payloadLen == 0) { + packetBuffer.popBack(); + continue; + } + + #ifdef DEBUG + outputPacket (p, payloadLen); + #endif + + uint8_t cmd = p->packet[11]; + if (cmd == 0x02) + analyse02 (&p->packet[11]); + else if (cmd == 0x01) + analyse01 (&p->packet[11]); + //if (p->packet[11] == 0x83 || p->packet[11] == 0x82) analyse83 (&p->packet[11], payloadLen); + else if (cmd == 0x03) { + analyseWords (&p->packet[11]); + analyseLongs (&p->packet[11]); + } + else if (cmd == 0x81) // ??? + ; + else if (cmd == 0x83) + analyse83 (&p->packet[11]); + else { + DEBUG_OUT.print (F("---- neues cmd=")); DEBUG_OUT.println(cmd, HEX); + analyseWords (&p->packet[11]); + analyseLongs (&p->packet[11]); + } + if (p->packetsLost > 0) { + DEBUG_OUT.print(F(" Lost: ")); + DEBUG_OUT.print(p->packetsLost); + } + DEBUG_OUT.println(); + + #ifndef ESP8266 + for (uint8_t i = 0; i < ANZAHL_VALUES; i++) { + //outChannel(i); + Serial.print(getChannelName(i)); Serial.print(':'); Serial.print(VALUES[i]); Serial.println(BLANK); // Schnittstelle bei Arduino + } + DEBUG_OUT.println(); + #endif + + // Remove record as we're done with it. + packetBuffer.popBack(); + } + + if (istTag) + isTime2Send(); + + #ifdef ESP8266 + checkWifi(); + webserverHandle(); + checkUpdateByOTA(); + if (hour() == 0 && minute() == 0) { + calcSunUpDown(getNow()); + } + if (minute() % 15 == 0 && second () == 0) { // alle 15 Minuten neu berechnen ob noch hell + istTag = isDayTime(); + DEBUG_OUT.print ("Es ist "); DEBUG_OUT.println (istTag?"Tag":"Nacht"); + } + #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) { +//-------------------------------------------------------------- + DISABLE_EINT; + radio1.stopListening(); + +#ifdef CHANNEL_HOP + static uint8_t hop = 0; + #if DEBUG_SEND + DEBUG_OUT.print(F("Send... CH")); + DEBUG_OUT.println(channels[hop]); + #endif + radio1.setChannel(channels[hop++]); + if (hop >= sizeof(channels) / sizeof(channels[0])) + hop = 0; +#else + radio1.setChannel(DEFAULT_SEND_CHANNEL); +#endif + + radio1.openWritingPipe(dest); + radio1.setCRCLength(RF24_CRC_16); + radio1.enableDynamicPayloads(); + radio1.setAutoAck(true); + radio1.setRetries(3, 15); + + radio1.write(buf, len); + + // Try to avoid zero payload acks (has no effect) + radio1.openWritingPipe(DUMMY_RADIO_ID); + + radio1.setAutoAck(false); + radio1.setRetries(0, 0); + radio1.disableDynamicPayloads(); + radio1.setCRCLength(RF24_CRC_DISABLED); + + radio1.setChannel(DEFAULT_RECV_CHANNEL); + radio1.startListening(); + ENABLE_EINT; +} diff --git a/tools/NRF24_SendRcv/NRF24_sniff_types.h b/tools/NRF24_SendRcv/NRF24_sniff_types.h new file mode 100644 index 00000000..7a032df5 --- /dev/null +++ b/tools/NRF24_SendRcv/NRF24_sniff_types.h @@ -0,0 +1,55 @@ +/* + This file is part of NRF24_Sniff. + + Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl + + NRF24_Sniff is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NRF24_Sniff is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NRF24_Sniff. If not, see . +*/ + +#ifndef NRF24_sniff_types_h +#define NRF24_sniff_types_h + +typedef struct _NRF24_packet_t +{ + uint32_t timestamp; + uint8_t packetsLost; + uint8_t packet[MAX_RF_PAYLOAD_SIZE]; +} NRF24_packet_t; + +typedef struct _Serial_header_t +{ + unsigned long timestamp; + uint8_t packetsLost; + uint8_t address[RF_MAX_ADDR_WIDTH]; // MSB first, always RF_MAX_ADDR_WIDTH bytes. +} Serial_header_t; + +typedef struct _Serial_config_t +{ + uint8_t channel; + uint8_t rate; // rf24_datarate_e: 0 = 1Mb/s, 1 = 2Mb/s, 2 = 250Kb/s + uint8_t addressLen; // Number of bytes used in address, range [2..5] + uint8_t addressPromiscLen; // Number of bytes used in promiscuous address, range [2..5]. E.g. addressLen=5, addressPromiscLen=4 => 1 byte unique identifier. + uint64_t address; // Base address, LSB first. + uint8_t crcLength; // Length of active CRC, range [0..2] + uint8_t maxPayloadSize; // Maximum size of payload for nRF (including nRF header), range[4?..32] +} Serial_config_t; + +#define MSG_TYPE_PACKET (0) +#define MSG_TYPE_CONFIG (1) + +#define SET_MSG_TYPE(var,type) (((var) & 0x3F) | ((type) << 6)) +#define GET_MSG_TYPE(var) ((var) >> 6) +#define GET_MSG_LEN(var) ((var) & 0x3F) + +#endif // NRF24_sniff_types_h diff --git a/tools/NRF24_SendRcv/Settings.h b/tools/NRF24_SendRcv/Settings.h new file mode 100644 index 00000000..27e2453d --- /dev/null +++ b/tools/NRF24_SendRcv/Settings.h @@ -0,0 +1,82 @@ +#ifndef __SETTINGS_H +#define __SETTINGS_H + +// Ausgabe von Debug Infos auf der seriellen Console +#define DEBUG +#define SER_BAUDRATE (115200) + +// Ausgabe was gesendet wird; 0 oder 1 +#define DEBUG_SEND 0 + +// soll zwichen den Sendekanälen 23, 40, 61, 75 ständig gewechselt werden +#define CHANNEL_HOP + +// mit OTA Support, also update der Firmware über WLan mittels IP/update +#define WITH_OTA + +// Hardware configuration +#ifdef ESP8266 +#define RF1_CE_PIN (D4) +#define RF1_CS_PIN (D8) +#define RF1_IRQ_PIN (D3) +#else +#define RF1_CE_PIN (9) +#define RF1_CS_PIN (10) +#define RF1_IRQ_PIN (2) +#endif + +union longlongasbytes { + uint64_t ull; + uint8_t bytes[8]; +}; + + +uint64_t Serial2RadioID (uint64_t sn) { +//---------------------------------- + longlongasbytes llsn; + longlongasbytes res; + llsn.ull = sn; + res.ull = 0; + res.bytes[4] = llsn.bytes[0]; + res.bytes[3] = llsn.bytes[1]; + res.bytes[2] = llsn.bytes[2]; + res.bytes[1] = llsn.bytes[3]; + res.bytes[0] = 0x01; + return res.ull; +} + +// WR und DTU +#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) +#define SerialWR 0x114172607952ULL // <<<<<<<<<<<<<<<<<<<<<<< anpassen +uint64_t WR1_RADIO_ID = Serial2RadioID (SerialWR); // ((uint64_t)0x5279607201ULL); +#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL) + + +// Webserver +#define WEBSERVER_PORT 80 + +// Time Server +//#define TIMESERVER_NAME "pool.ntp.org" +#define TIMESERVER_NAME "fritz.box" + +#ifdef WITH_OTA +// OTA Einstellungen +#define UPDATESERVER_PORT WEBSERVER_PORT+1 +#define UPDATESERVER_DIR "/update" // mittels IP:81/update kommt man dann auf die OTA-Seite +#define UPDATESERVER_USER "username_für_OTA" // <<<<<<<<<<<<<<<<<<<<<<< anpassen +#define UPDATESERVER_PW "passwort_für_OTA" // <<<<<<<<<<<<<<<<<<<<<<< anpassen +#endif + +// internes WLan +// PREFIXE dienen dazu, die eigenen WLans (wenn mehrere) vonfremden zu unterscheiden +// gehe hier davon aus, dass alle WLans das gleiche Passwort haben. Wenn nicht, dann mehre Passwörter hinterlegen +#define SSID_PREFIX1 "wlan1-Prefix" // <<<<<<<<<<<<<<<<<<<<<<< anpassen +#define SSID_PREFIX2 "wlan2-Prefix" // <<<<<<<<<<<<<<<<<<<<<<< anpassen +#define SSID_PASSWORD "wlan-passwort" // <<<<<<<<<<<<<<<<<<<<<<< anpassen + +// zur Berechnung von Sonnenauf- und -untergang +#define geoBreite 49.2866 +#define geoLaenge 7.3416 + + +#endif diff --git a/tools/NRF24_SendRcv/Sonne.h b/tools/NRF24_SendRcv/Sonne.h new file mode 100644 index 00000000..ce70a150 --- /dev/null +++ b/tools/NRF24_SendRcv/Sonne.h @@ -0,0 +1,55 @@ +#ifndef __SONNE_H +#define __SONNE_H + +#include "Settings.h" +#include "Debug.h" + + +long SunDown, SunUp; + +void calcSunUpDown (time_t date) { + //SunUpDown res = new SunUpDown(); + boolean isSummerTime = false; // TODO TimeZone.getDefault().inDaylightTime(new Date(date)); + + //- Bogenma� + double brad = geoBreite / 180.0 * PI; + // - H�he Sonne -50 Bogenmin. + double h0 = -50.0 / 60.0 / 180.0 * PI; + //- Deklination dek, Tag des Jahres d0 + int tage = 30 * month(date) - 30 + day(date); + double dek = 0.40954 * sin (0.0172 * (tage - 79.35)); + double zh1 = sin (h0) - sin (brad) * sin(dek); + double zh2 = cos(brad) * cos(dek); + double zd = 12*acos (zh1/zh2) / PI; + double zgl = -0.1752 * sin (0.03343 * tage + 0.5474) - 0.134 * sin (0.018234 * tage - 0.1939); + //-Sonnenuntergang + double tsu = 12 + zd - zgl; + double su = (tsu + (15.0 - geoLaenge) / 15.0); + int std = (int)su; + int minute = (int) ((su - std)*60); + if (isSummerTime) std++; + SunDown = (100*std + minute) * 100; + + //- Sonnenaufgang + double tsa = 12 - zd - zgl; + double sa = (tsa + (15.0 - geoLaenge) /15.0); + std = (int) sa; + minute = (int) ((sa - std)*60); + if (isSummerTime) std++; + SunUp = (100*std + minute) * 100; + DEBUG_OUT.print("Sonnenaufgang :"); DEBUG_OUT.println(SunUp); + DEBUG_OUT.print("Sonnenuntergang:"); DEBUG_OUT.println(SunDown); +} + +boolean isDayTime() { +//----------------- +// 900 = 15 Minuten, vor Sonnenaufgang und nach -untergang + const int offset=60*15; + time_t no = getNow(); + long jetztMinuteU = (100 * hour(no+offset) + minute(no+offset)) * 100; + long jetztMinuteO = (100 * hour(no-offset) + minute(no-offset)) * 100; + + return ((jetztMinuteU >= SunUp) &&(jetztMinuteO <= SunDown)); +} + +#endif diff --git a/tools/NRF24_SendRcv/hm_crc.cpp b/tools/NRF24_SendRcv/hm_crc.cpp new file mode 100644 index 00000000..74d41ce3 --- /dev/null +++ b/tools/NRF24_SendRcv/hm_crc.cpp @@ -0,0 +1,142 @@ + +#include +#include +#include "hm_crc.h" +//#define OUTPUT_DEBUG_INFO + +/* Table of CRC values for high-order byte */ +static const uint8_t auchCRCHi[] = { + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, + 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, + 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40}; + +/* Table of CRC values for low-order byte */ +static const uint8_t auchCRCLo[] = { + 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, + 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, + 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, + 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, + 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, + 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, + 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, + 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, + 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, + 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, + 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, + 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, + 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, + 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, + 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, + 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, + 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, + 0x40}; + +uint16_t crc16_modbus(uint8_t *puchMsg, uint16_t usDataLen) +{ + uint8_t uchCRCHi = 0xFF; /* high byte of CRC initialized */ + uint8_t uchCRCLo = 0xFF; /* low byte of CRC initialized */ + uint16_t uIndex; /* will index into CRC lookup table */ + while (usDataLen--) /* pass through message buffer */ + { + uIndex = uchCRCLo ^ *puchMsg++; /* calculate the CRC */ + uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex]; + uchCRCHi = auchCRCLo[uIndex]; + } + return (uchCRCHi << 8 | uchCRCLo); +} + +// Hoymiles CRC8 calculation with poly 0x01, Initial value 0x00 and final XOR 0x00 +uint8_t crc8(uint8_t *buf, const uint16_t bufLen) +{ + uint32_t crc; + uint16_t i, bit; + + crc = 0x00; + for (i = 0; i < bufLen; i++) + { + crc ^= buf[i]; + for (bit = 0; bit < 8; bit++) + { + if ((crc & 0x80) != 0) + { + crc <<= 1; + crc ^= 0x01; + } + else + { + crc <<= 1; + } + } + } + + return (crc & 0xFF); +} + +// NRF24 CRC16 calculation with poly 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1 +uint16_t crc16(uint8_t *buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits) +{ + uint16_t crc = startCRC; + if ((len_bits > 0) && (len_bits <= BYTES_TO_BITS(bufLen))) + { + // The length of the data might not be a multiple of full bytes. + // Therefore we proceed over the data bit-by-bit (like the NRF24 does) to + // calculate the CRC. + uint16_t data; + uint8_t byte, shift; + uint16_t bitoffs = startBit; + + // Get a new byte for the next 8 bits. + byte = buf[bitoffs >> 3]; +#ifdef OUTPUT_DEBUG_INFO + printf("\nStart CRC %04X, %u bits:", startCRC, len_bits); + printf("\nbyte %02X:", byte); +#endif + while (bitoffs < len_bits + startBit) + { + shift = bitoffs & 7; + // Shift the active bit to the position of bit 15 + data = ((uint16_t)byte) << (8 + shift); +#ifdef OUTPUT_DEBUG_INFO + printf(" bit %u %u,", shift, data & 0x8000 ? 1 : 0); +#endif + // Assure all other bits are 0 + data &= 0x8000; + crc ^= data; + if (crc & 0x8000) + { + crc = (crc << 1) ^ 0x1021; // 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1 + } + else + { + crc = (crc << 1); + } + ++bitoffs; + if (0 == (bitoffs & 7)) + { + // Get a new byte for the next 8 bits. + byte = buf[bitoffs >> 3]; +#ifdef OUTPUT_DEBUG_INFO + printf("crc %04X:", crc); + if (bitoffs < len_bits + startBit) + printf("\nbyte %02X:", byte); +#endif + } + } + } + return crc; +} \ No newline at end of file diff --git a/tools/NRF24_SendRcv/hm_crc.h b/tools/NRF24_SendRcv/hm_crc.h new file mode 100644 index 00000000..7f3c32e3 --- /dev/null +++ b/tools/NRF24_SendRcv/hm_crc.h @@ -0,0 +1,8 @@ + + +#define BITS_TO_BYTES(x) (((x)+7)>>3) +#define BYTES_TO_BITS(x) ((x)<<3) + +extern uint16_t crc16_modbus(uint8_t *puchMsg, uint16_t usDataLen); +extern uint8_t crc8(uint8_t *buf, const uint16_t bufLen); +extern uint16_t crc16(uint8_t* buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits); \ No newline at end of file diff --git a/tools/NRF24_SendRcv/hm_packets.cpp b/tools/NRF24_SendRcv/hm_packets.cpp new file mode 100644 index 00000000..7b09df61 --- /dev/null +++ b/tools/NRF24_SendRcv/hm_packets.cpp @@ -0,0 +1,74 @@ +#include "Arduino.h" + +#include "hm_crc.h" +#include "hm_packets.h" + +void HM_Packets::SetUnixTimeStamp(uint32_t ts) +{ + unixTimeStamp = ts; +} + +void HM_Packets::UnixTimeStampTick() +{ + unixTimeStamp++; +} + +void HM_Packets::prepareBuffer(uint8_t *buf) +{ + // minimal buffer size of 32 bytes is assumed + memset(buf, 0x00, 32); +} + +void HM_Packets::copyToBuffer(uint8_t *buf, uint32_t val) +{ + buf[0]= (uint8_t)(val >> 24); + buf[1]= (uint8_t)(val >> 16); + buf[2]= (uint8_t)(val >> 8); + buf[3]= (uint8_t)(val & 0xFF); +} + +void HM_Packets::copyToBufferBE(uint8_t *buf, uint32_t val) +{ + memcpy(buf, &val, sizeof(uint32_t)); +} + + +int32_t HM_Packets::GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr) +{ + prepareBuffer(buf); + + buf[0] = 0x15; + copyToBufferBE(&buf[1], wrAdr); + copyToBufferBE(&buf[5], dtuAdr); + buf[9] = 0x80; + + buf[10] = 0x0B; // cid + buf[11] = 0x00; + + copyToBuffer(&buf[12], unixTimeStamp); + + buf[19] = 0x05; + + // CRC16 + uint16_t crc16 = crc16_modbus(&buf[10], 14); + buf[24] = crc16 >> 8; + buf[25] = crc16 & 0xFF; + + // crc8 + buf[26] = crc8(&buf[0], 26); + + return 27; +} + +int32_t HM_Packets::GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd) +{ + buf[0] = mid; + copyToBufferBE(&buf[1], wrAdr); + copyToBufferBE(&buf[5], dtuAdr); + buf[9] = cmd; + + // crc8 + buf[10] = crc8(&buf[0], 10); + + return 11; +} diff --git a/tools/NRF24_SendRcv/hm_packets.h b/tools/NRF24_SendRcv/hm_packets.h new file mode 100644 index 00000000..1c8aca45 --- /dev/null +++ b/tools/NRF24_SendRcv/hm_packets.h @@ -0,0 +1,18 @@ + + +class HM_Packets +{ +private: + uint32_t unixTimeStamp; + + void prepareBuffer(uint8_t *buf); + void copyToBuffer(uint8_t *buf, uint32_t val); + void copyToBufferBE(uint8_t *buf, uint32_t val); + +public: + void SetUnixTimeStamp(uint32_t ts); + void UnixTimeStampTick(); + + int32_t GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr); + int32_t GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd); +}; diff --git a/tools/NRF24_SendRcv/wifi.h b/tools/NRF24_SendRcv/wifi.h new file mode 100644 index 00000000..d98cc40f --- /dev/null +++ b/tools/NRF24_SendRcv/wifi.h @@ -0,0 +1,345 @@ +#ifndef __WIFI_H +#define __WIFI_H + +#include "Settings.h" +#include "Debug.h" +#include +#include // von url=https://www.technologytourist.com + +String SSID = ""; // bestes WLan + +// Prototypes +time_t getNow (); +boolean setupWifi (); +boolean checkWifi(); + + +String findWifi () { +//---------------- + String ssid; + int32_t rssi; + uint8_t encryptionType; + uint8_t* bssid; + int32_t channel; + bool hidden; + int scanResult; + + String best_ssid = ""; + int32_t best_rssi = -100; + + DEBUG_OUT.println(F("Starting WiFi scan...")); + + scanResult = WiFi.scanNetworks(/*async=*/false, /*hidden=*/true); + + if (scanResult == 0) { + DEBUG_OUT.println(F("keine WLans")); + } else if (scanResult > 0) { + DEBUG_OUT.printf(PSTR("%d WLans gefunden:\n"), scanResult); + + // Print unsorted scan results + for (int8_t i = 0; i < scanResult; i++) { + WiFi.getNetworkInfo(i, ssid, encryptionType, rssi, bssid, channel, hidden); + + DEBUG_OUT.printf(PSTR(" %02d: [CH %02d] [%02X:%02X:%02X:%02X:%02X:%02X] %ddBm %c %c %s\n"), + i, + channel, + bssid[0], bssid[1], bssid[2], + bssid[3], bssid[4], bssid[5], + rssi, + (encryptionType == ENC_TYPE_NONE) ? ' ' : '*', + hidden ? 'H' : 'V', + ssid.c_str()); + delay(1); + boolean check; + #ifdef SSID_PREFIX1 + check = ssid.substring(0,strlen(SSID_PREFIX1)).equals(SSID_PREFIX1); + #else + check = true; + #endif + #ifdef SSID_PREFIX2 + check = check || ssid.substring(0,strlen(SSID_PREFIX2)).equals(SSID_PREFIX2); + #endif + if (check) { + if (rssi > best_rssi) { + best_rssi = rssi; + best_ssid = ssid; + } + } + } + } else { + DEBUG_OUT.printf(PSTR("WiFi scan error %d"), scanResult); + } + + if (! best_ssid.equals("")) { + SSID = best_ssid; + DEBUG_OUT.printf ("Bestes Wifi unter: %s\n", SSID.c_str()); + return SSID; + } + else + return ""; +} + +void IP2string (IPAddress IP, char * buf) { + sprintf (buf, "%d.%d.%d.%d", IP[0], IP[1], IP[2], IP[3]); +} + +void connectWifi() { +//------------------ +// if (SSID.equals("")) + String s = findWifi(); + + if (!SSID.equals("")) { + DEBUG_OUT.print("versuche zu verbinden mit "); DEBUG_OUT.println(SSID); + //while (WiFi.status() != WL_CONNECTED) { + WiFi.begin (SSID, SSID_PASSWORD); + int versuche = 20; + while (WiFi.status() != WL_CONNECTED && versuche > 0) { + delay(1000); + versuche--; + DEBUG_OUT.print(versuche); DEBUG_OUT.print(' '); + } + //} + if (WiFi.status() == WL_CONNECTED) { + char buffer[30]; + IP2string (WiFi.localIP(), buffer); + String out = "\n[WiFi]Verbunden; meine IP:" + String (buffer); + DEBUG_OUT.println (out); + } + else + DEBUG_OUT.print("\nkeine Verbindung mit SSID "); DEBUG_OUT.println(SSID); + } +} + + +boolean setupWifi () { +//------------------ + int count=5; + while (count-- && WiFi.status() != WL_CONNECTED) + connectWifi(); + return (WiFi.status() == WL_CONNECTED); +} + + +Pinger pinger; +IPAddress ROUTER = IPAddress(192,168,1,1); + +boolean checkWifi() { +//--------------- + boolean NotConnected = (WiFi.status() != WL_CONNECTED) || !pinger.Ping(ROUTER); + if (NotConnected) { + setupWifi(); + if (WiFi.status() == WL_CONNECTED) + getNow(); + } + return (WiFi.status() == WL_CONNECTED); +} + + + +// ################ Clock ################# + +#include +#include + +IPAddress timeServer; +unsigned int localPort = 8888; +const int NTP_PACKET_SIZE= 48; // NTP time stamp is in the first 48 bytes of the message +byte packetBuf[NTP_PACKET_SIZE]; // Buffer to hold incoming and outgoing packets +const int timeZone = 1; // Central European Time = +1 +long SYNCINTERVALL = 0; +WiFiUDP Udp; // A UDP instance to let us send and receive packets over UDP + +// prototypes +time_t getNtpTime (); +void sendNTPpacket (IPAddress &address); +time_t getNow (); +char* getDateTimeStr (time_t no = getNow()); +time_t offsetDayLightSaving (uint32_t local_t); +bool isDayofDaylightChange (time_t local_t); + + +void _setSyncInterval (long intervall) { +//---------------------------------------- + SYNCINTERVALL = intervall; + setSyncInterval (intervall); +} + +void setupClock() { +//----------------- + WiFi.hostByName (TIMESERVER_NAME,timeServer); // at this point the function works + + Udp.begin(localPort); + + getNtpTime(); + + setSyncProvider (getNtpTime); + while(timeStatus()== timeNotSet) + delay(1); // + + _setSyncInterval (SECS_PER_DAY / 2); // Set seconds between re-sync + + //lastClock = now(); + //Serial.print("[NTP] get time from NTP server "); + getNow(); + //char buf[20]; + DEBUG_OUT.print ("[NTP] get time from NTP server "); + DEBUG_OUT.print (timeServer); + //sprintf (buf, ": %02d:%02d:%02d", hour(no), minute(no), second(no)); + DEBUG_OUT.print (": got "); + DEBUG_OUT.println (getDateTimeStr()); +} + +//*-------- NTP code ----------*/ + + +time_t getNtpTime() { +//------------------- + sendNTPpacket(timeServer); // send an NTP packet to a time server + //uint32_t beginWait = millis(); + //while (millis() - beginWait < 1500) { + int versuch = 0; + while (versuch < 5) { + int wait = 150; // results in max 1500 ms waitTime + while (wait--) { + int size = Udp.parsePacket(); + if (size >= NTP_PACKET_SIZE) { + //Serial.println("Receive NTP Response"); + Udp.read(packetBuf, NTP_PACKET_SIZE); // read packet into the buffer + unsigned long secsSince1900; + // convert four bytes starting at location 40 to a long integer + secsSince1900 = (unsigned long)packetBuf[40] << 24; + secsSince1900 |= (unsigned long)packetBuf[41] << 16; + secsSince1900 |= (unsigned long)packetBuf[42] << 8; + secsSince1900 |= (unsigned long)packetBuf[43]; + // time_t now = secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR; + + time_t utc = secsSince1900 - 2208988800UL; + time_t now = utc + (timeZone +offsetDayLightSaving(utc)) * SECS_PER_HOUR; + + if (isDayofDaylightChange (utc) && hour(utc) <= 4) + _setSyncInterval (SECS_PER_HOUR); + else + _setSyncInterval (SECS_PER_DAY / 2); + + return now; + } + else + delay(10); + } + versuch++; + } + return 0; +} + +// send an NTP request to the time server at the given address +void sendNTPpacket(IPAddress& address) { +//------------------------------------ + memset(packetBuf, 0, NTP_PACKET_SIZE); // set all bytes in the buffer to 0 + // Initialize values needed to form NTP request + packetBuf[0] = B11100011; // LI, Version, Mode + packetBuf[1] = 0; // Stratum + packetBuf[2] = 6; // Max Interval between messages in seconds + packetBuf[3] = 0xEC; // Clock Precision + // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset + packetBuf[12] = 49; // four-byte reference ID identifying + packetBuf[13] = 0x4E; + packetBuf[14] = 49; + packetBuf[15] = 52; + // send the packet requesting a timestamp: + Udp.beginPacket(address, 123); //NTP requests are to port 123 + Udp.write(packetBuf,NTP_PACKET_SIZE); + Udp.endPacket(); + +} + +int getTimeTrials = 0; + +bool isValidDateTime (time_t no) { + return (year(no) > 2020 && year(no) < 2038); +} + +bool isDayofDaylightChange (time_t local_t) { +//----------------------------------------- + int jahr = year (local_t); + int monat = month (local_t); + int tag = day (local_t); + bool ret = ( (monat ==3 && tag == (31 - (5 * jahr /4 + 4) % 7)) || + (monat==10 && tag == (31 - (5 * jahr /4 + 1) % 7))); + DEBUG_OUT.print ("isDayofDaylightChange="); DEBUG_OUT.println (ret); + return ret; +} + +// calculates the daylight saving time for middle Europe. Input: Unixtime in UTC (!) +// übernommen von Jurs, see : https://forum.arduino.cc/index.php?topic=172044.msg1278536#msg1278536 +time_t offsetDayLightSaving (uint32_t local_t) { +//-------------------------------------------- + int monat = month (local_t); + if (monat < 3 || monat > 10) return 0; // no DSL in Jan, Feb, Nov, Dez + if (monat > 3 && monat < 10) return 1; // DSL in Apr, May, Jun, Jul, Aug, Sep + int jahr = year (local_t); + int std = hour (local_t); + //int tag = day (local_t); + int stundenBisHeute = (std + 24 * day(local_t)); + if ( (monat == 3 && stundenBisHeute >= (1 + timeZone + 24 * (31 - (5 * jahr /4 + 4) % 7))) || + (monat == 10 && stundenBisHeute < (1 + timeZone + 24 * (31 - (5 * jahr /4 + 1) % 7))) ) + return 1; + else + return 0; + /* + int stundenBisWechsel = (1 + 24 * (31 - (5 * year(local_t) / 4 + 4) % 7)); + if (monat == 3 && stundenBisHeute >= stundenBisWechsel || monat == 10 && stundenBisHeute < stundenBisWechsel) + return 1; + else + return 0; + */ +} + + +time_t getNow () { +//--------------- + time_t jetzt = now(); + while (!isValidDateTime(jetzt) && getTimeTrials < 10) { // ungültig, max 10x probieren + if (getTimeTrials) { + //Serial.print (getTimeTrials); + //Serial.println(". Versuch für getNtpTime"); + } + jetzt = getNtpTime (); + if (isValidDateTime(jetzt)) { + setTime (jetzt); + getTimeTrials = 0; + } + else + getTimeTrials++; + } + //return jetzt + offsetDayLightSaving(jetzt)*SECS_PER_HOUR; + return jetzt; +} + + +char _timestr[24]; + +char* getNowStr (time_t no = getNow()) { +//------------------------------------ + sprintf (_timestr, "%02d:%02d:%02d", hour(no), minute(no), second(no)); + return _timestr; +} + +char* getTimeStr (time_t no = getNow()) { +//------------------------------------ + return getNowStr (no); +} + +char* getDateTimeStr (time_t no) { +//------------------------------ + sprintf (_timestr, "%04d-%02d-%02d+%02d:%02d:%02d", year(no), month(no), day(no), hour(no), minute(no), second(no)); + return _timestr; +} + +char* getDateStr (time_t no) { +//------------------------------ + sprintf (_timestr, "%04d-%02d-%02d", year(no), month(no), day(no)); + return _timestr; +} + + +#endif diff --git a/tools/esp8266/README.md b/tools/esp8266/README.md index 66660bcd..cdb120cd 100644 --- a/tools/esp8266/README.md +++ b/tools/esp8266/README.md @@ -13,6 +13,13 @@ This code can be compiled using Arduino. The settings were: - Board: Generic ESP8266 Module - Flash-Size: 1MB (FS: none, OTA: 502kB) +### Optional Configuration before compilation + +- number of supported inverters (set to 3 by default) `defines.h` +- enable channel hopping `hmRadio.h` +- DTU radio id `hmRadio.h` +- unformated list in webbrowser `/livedata` `defines.h`, `LIVEDATA_VISUALIZED` + ## Flash ESP with firmware @@ -21,12 +28,12 @@ This code can be compiled using Arduino. The settings were: 3. the ESP will start as access point (AP) if there is no network config stored in its eeprom 4. connect to the AP, you will be forwarded to the setup page 5. configure your WiFi settings, save, repower -6. check your router for the IP address of the module +6. check your router or serial console for the IP address of the module. You can try ping the configured device name as well. ## Usage -Connect the ESP to power and to your serial console. The webinterface has the following abilities: +Connect the ESP to power and to your serial console (optional). The webinterface has the following abilities: - OTA Update (over the air update) - Configuration (Wifi, inverter(s), Pinout, MQTT) @@ -40,11 +47,14 @@ The serial console will print the converted values which were read out of the in For now the following inverters should work out of the box: +- HM400 - HM600 +- HM800 - HM1200 ## USED LIBRARIES - `Time` - `RF24` +- `PubSubClient` diff --git a/tools/esp8266/app.cpp b/tools/esp8266/app.cpp index c54335c8..f92e5432 100644 --- a/tools/esp8266/app.cpp +++ b/tools/esp8266/app.cpp @@ -164,7 +164,7 @@ void app::loop(void) { if(NULL != inv) { mSys->Radio.sendTimePacket(inv->radioId.u64, mTimestamp); yield(); - delay(100); + //delay(100); } } } @@ -184,7 +184,7 @@ void app::loop(void) { snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, fields[iv->assign[i].fieldId]); snprintf(val, 10, "%.3f", iv->getValue(i)); mMqtt.sendMsg(topic, val); - delay(20); + //delay(20); yield(); } } @@ -409,6 +409,8 @@ void app::showLiveData(void) { case INV_TYPE_HM1200: modNum = 4; break; } + modHtml += "

" + String(iv->name) + "

"; + for(uint8_t ch = 1; ch <= modNum; ch ++) { modHtml += "
CHANNEL " + String(ch) + ""; for(uint8_t j = 0; j < 5; j++) { @@ -427,6 +429,8 @@ void app::showLiveData(void) { } modHtml += "
"; } + + modHtml += "
"; #else // dump all data to web frontend modHtml = "
";
diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h
index 1aa22cee..f5c57530 100644
--- a/tools/esp8266/defines.h
+++ b/tools/esp8266/defines.h
@@ -25,7 +25,7 @@
 //-------------------------------------
 #define VERSION_MAJOR       0
 #define VERSION_MINOR       2
-#define VERSION_PATCH       11
+#define VERSION_PATCH       12
 
 
 //-------------------------------------
diff --git a/tools/esp8266/hmDefines.h b/tools/esp8266/hmDefines.h
index b36eac03..ac53b876 100644
--- a/tools/esp8266/hmDefines.h
+++ b/tools/esp8266/hmDefines.h
@@ -26,9 +26,9 @@ enum {CH0 = 0, CH1, CH2, CH3, CH4};
 // received command ids, special command CMDFF for calculations
 enum {CMD01 = 0x01, CMD02, CMD03, CMD82 = 0x82, CMD83, CMD84, CMDFF=0xff};
 
-enum {INV_TYPE_HM600 = 0, INV_TYPE_HM1200, INV_TYPE_HM400};
-const char* const invTypes[] = {"HM600", "HM1200 / HM1500", "HM400"};
-#define NUM_INVERTER_TYPES   3
+enum {INV_TYPE_HM600 = 0, INV_TYPE_HM1200, INV_TYPE_HM400, INV_TYPE_HM800};
+const char* const invTypes[] = {"HM600", "HM1200 / HM1500", "HM400", "HM800"};
+#define NUM_INVERTER_TYPES   4
 
 typedef struct {
     uint8_t    fieldId; // field id
@@ -54,7 +54,7 @@ const byteAssign_t hm400assignment[] = {
     { FLD_IDC, UNIT_A,   CH1, CMD01,  5, 2, 100  },
     { FLD_PDC, UNIT_W,   CH1, CMD01,  7, 2, 10   },
     { FLD_YT,  UNIT_KWH, CH1, CMD01,  9, 4, 1000 },
-    { FLD_YD,  UNIT_WH,  CH1, CMD01, 13, 2, 1000 },
+    { FLD_YD,  UNIT_WH,  CH1, CMD01, 13, 2, 1    },
     { FLD_UAC, UNIT_V,   CH0, CMD01, 15, 2, 10   },
     { FLD_F,   UNIT_HZ,  CH0, CMD82,  1, 2, 100  },
     { FLD_PAC, UNIT_W,   CH0, CMD82,  3, 2, 10   },
@@ -86,6 +86,30 @@ const byteAssign_t hm600assignment[] = {
 #define HM600_LIST_LEN     (sizeof(hm600assignment) / sizeof(byteAssign_t))
 
 
+//-------------------------------------
+// HM800
+//-------------------------------------
+const byteAssign_t hm800assignment[] = {
+
+    { FLD_UDC, UNIT_V,   CH1, CMD01,  3, 2, 10   },
+    { FLD_IDC, UNIT_A,   CH1, CMD01,  5, 2, 100  },
+    { FLD_PDC, UNIT_W,   CH1, CMD01,  7, 2, 10   },
+    { FLD_UDC, UNIT_V,   CH2, CMD01,  9, 2, 10   },
+    { FLD_IDC, UNIT_A,   CH2, CMD01, 11, 2, 100  },
+    { FLD_PDC, UNIT_W,   CH2, CMD01, 13, 2, 10   },
+    { FLD_YW,  UNIT_WH,  CH0, CMD02,  1, 2, 1    },
+    { FLD_YT,  UNIT_KWH, CH0, CMD02,  3, 4, 1000 },
+    { FLD_YD,  UNIT_WH,  CH1, CMD02,  7, 2, 1    },
+    { FLD_YD,  UNIT_WH,  CH2, CMD02,  9, 2, 1    },
+    { FLD_UAC, UNIT_V,   CH0, CMD02, 11, 2, 10   },
+    { FLD_F,   UNIT_HZ,  CH0, CMD02, 13, 2, 100  },
+    { FLD_PAC, UNIT_W,   CH0, CMD02, 15, 2, 10   },
+    { FLD_IAC, UNIT_A,   CH0, CMD83,  3, 2, 100  },
+    { FLD_T,   UNIT_C,   CH0, CMD83,  7, 2, 10   }
+};
+#define HM800_LIST_LEN     (sizeof(hm800assignment) / sizeof(byteAssign_t))
+
+
 //-------------------------------------
 // HM1200, HM1500
 //-------------------------------------
diff --git a/tools/esp8266/hmInverter.h b/tools/esp8266/hmInverter.h
index 9be3a981..999545bf 100644
--- a/tools/esp8266/hmInverter.h
+++ b/tools/esp8266/hmInverter.h
@@ -81,21 +81,26 @@ class Inverter {
         }
 
         void getAssignment(void) {
-            if(INV_TYPE_HM600 == type) {
+            if(INV_TYPE_HM400 == type) {
+                listLen  = (uint8_t)(HM400_LIST_LEN);
+                assign   = (byteAssign_t*)hm400assignment;
+                channels = 1;
+            }
+            else if(INV_TYPE_HM600 == type) {
                 listLen  = (uint8_t)(HM600_LIST_LEN);
                 assign   = (byteAssign_t*)hm600assignment;
                 channels = 2;
             }
+            else if(INV_TYPE_HM800 == p->type) {
+                listLen  = (uint8_t)(HM800_LIST_LEN);
+                assign   = (byteAssign_t*)hm800assignment;
+                channels = 2;
+            }
             else if(INV_TYPE_HM1200 == type) {
                 listLen  = (uint8_t)(HM1200_LIST_LEN);
                 assign   = (byteAssign_t*)hm1200assignment;
                 channels = 4;
             }
-            else if(INV_TYPE_HM400 == type) {
-                listLen  = (uint8_t)(HM400_LIST_LEN);
-                assign   = (byteAssign_t*)hm400assignment;
-                channels = 1;
-            }
             else {
                 listLen  = 0;
                 channels = 0;
diff --git a/tools/esp8266/html/h/style_css.h b/tools/esp8266/html/h/style_css.h
index dc3639c4..491afc1e 100644
--- a/tools/esp8266/html/h/style_css.h
+++ b/tools/esp8266/html/h/style_css.h
@@ -1,4 +1,4 @@
 #ifndef __STYLE_H__
 #define __STYLE_H__
-const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:14pt;color:#006ec0;}.subdes {font-size:13pt;color:#006ec0;margin-left:7px;}.fw {width:60px;display:block;float:left;}.color {width:50px;height:50px;border:1px solid #ccc;}.range {width:300px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;}#footer p {color:#fff;padding-left:20px;padding-right:20px;font-size:10pt !important;}#footer a {color:#fff;}div.content {background-color:#fff;padding-bottom:65px;overflow:hidden;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch {width:250px;height:410px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;}div.ch .value, div.ch .info, div.ch .head {color:#fff;display:block;width:100%;text-align:center;}div.ch .unit {font-size:19px;margin-left:10px;}div.ch .value {margin-top:20px;font-size:30px;}div.ch .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}";
+const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:14pt;color:#006ec0;}.subdes {font-size:13pt;color:#006ec0;margin-left:7px;}.fw {width:60px;display:block;float:left;}.color {width:50px;height:50px;border:1px solid #ccc;}.range {width:300px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;}#footer p {color:#fff;padding-left:20px;padding-right:20px;font-size:10pt !important;}#footer a {color:#fff;}div.content {background-color:#fff;padding-bottom:65px;overflow:hidden;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch-group {display:inline-block;}div.ch {width:250px;height:410px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;}div.ch .value, div.ch .info, div.ch .head {color:#fff;display:block;width:100%;text-align:center;}div.ch .unit {font-size:19px;margin-left:10px;}div.ch .value {margin-top:20px;font-size:30px;}div.ch .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}";
 #endif /*__STYLE_H__*/
diff --git a/tools/esp8266/html/style.css b/tools/esp8266/html/style.css
index 21d5340f..88c1b57c 100644
--- a/tools/esp8266/html/style.css
+++ b/tools/esp8266/html/style.css
@@ -136,6 +136,10 @@ label {
     float: right;
 }
 
+div.ch-group {
+    display: inline-block;
+}
+
 div.ch {
     width: 250px;
     height: 410px;