mirror of https://github.com/lumapu/ahoy.git
				
				
			
							committed by
							
								 GitHub
								GitHub
							
						
					
				
				 74 changed files with 8574 additions and 6023 deletions
			
			
		
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					| @ -1,28 +1,47 @@ | |||
| # Getting Started with an ESP8266 | |||
| 
 | |||
| ``` | |||
| Wire Connections | |||
| 
 | |||
| ```ditaa | |||
|     +-----------+          +-----------+ | |||
|     | nRF24L01+ |--colour--| ESP8266   | | |||
|     |  ESP8266  |--colour--| nRF24L01+ | | |||
|     |           |          |           | | |||
|     |       GND |---black--| GND       | | |||
|     |       VCC |----red---| +3.3V     | | |||
|     |        CE |---grey---| D4        | | |||
|     |       CSN |--purple--| D8        | | |||
|     |       SCK |---blue---| D5        | | |||
|     |      MOSI |---green--| D7        | | |||
|     |      MISO |---brown--| D6        | | |||
|     |       IRQ |--yellow--| D3        | | |||
|     |       GND |---black--|[GND]      | | |||
|     |     +3.3V |----red---| VCC       | | |||
|     |        D4 |---grey---| CE        | | |||
|     |        D8 |--purple--| CSN       | | |||
|     |        D5 |---blue---| SCK       | | |||
|     |        D7 |---green--| MOSI      | | |||
|     |        D6 |---brown--| MISO      | | |||
|     |        D3 |--yellow--| IRQ       | | |||
|     +-----------+          +-----------+ | |||
| ``` | |||
| 
 | |||
|  | |||
| 
 | |||
| See | |||
| Fritzing diagrams & schematics | |||
| * [AhoyMiles_bb.png](./AhoyMiles_bb.png) | |||
| * [AhoyMiles_schem.png](./AhoyMiles_schem.png) | |||
| * [AhoyMiles.fzz](./AhoyMiles.fzz) | |||
| 
 | |||
| Libraries to be installed in Arduino IDE: | |||
| * RF24 | |||
| * TimeLib | |||
| 
 | |||
| * AhoyMiles_bb.png | |||
| * AhoyMiles_schem.png | |||
| * AhoyMiles.fzz | |||
| Verify & Compile | |||
| * Connect to WiFi Network `ESP AHOY` | |||
| * Use password `esp_8266` | |||
| * Connect to Network settings  | |||
| 
 | |||
| by "isnoAhoy" | |||
| Setup | |||
| * WiFi  | |||
|   * Enter SSID `mynetwork` | |||
|   * Enter Password `mypassword` | |||
| * Device Host Name | |||
|   * Enter Device Name `esp-ahoy`  | |||
| * General | |||
|   * Hoymiles Address (e.g. 114173123456) | |||
|   * Choose inverter type | |||
|   * Set individual inverter name | |||
|   * [x] Reboot device after successful save | |||
| Save | |||
|  | |||
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -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 T> 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
 | |||
| @ -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  | |||
| @ -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 | |||
| @ -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 | |||
| @ -0,0 +1,605 @@ | |||
| #include <Arduino.h> | |||
| #include <SPI.h> | |||
| #include "CircularBuffer.h" | |||
| #include <RF24.h> | |||
| #include "printf.h" | |||
| #include <RF24_config.h> | |||
| #include "hm_crc.h" | |||
| #include "hm_packets.h" | |||
| 
 | |||
| #include "Settings.h"     // Header für Einstellungen | |||
| #include "Debug.h" | |||
| #include "Inverters.h" | |||
| 
 | |||
| const char VERSION[] PROGMEM = "0.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<NRF24_packet_t> packetBuffer(bufferData, sizeof(bufferData) / sizeof(bufferData[0])); | |||
| 
 | |||
| static Serial_header_t SerialHdr; | |||
| 
 | |||
| #define CHECKCRC  1 | |||
| static uint16_t lastCRC; | |||
| static uint16_t crc; | |||
| 
 | |||
| uint8_t         channels[]            = {3, 23, 40, 61, 75};   //{1, 3, 6, 9, 11, 23, 40, 61, 75}
 | |||
| uint8_t         channelIdx            = 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 | |||
| } | |||
| @ -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 | |||
| @ -0,0 +1,151 @@ | |||
| // #################  WebServer #################
 | |||
| 
 | |||
| #ifndef __MODWEBSERVER_H | |||
| #define __MODWEBSERVER_H | |||
| #define MODWEBSERVER | |||
| 
 | |||
| #include <ESP8266WebServer.h> | |||
| #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 = "<html>"; | |||
|   out += "<body><h2>Hilfe</h2>"; | |||
|   out += "<br><br><table>"; | |||
|   out += "<tr><td>/</td><td>zeigt alle Messwerte in einer Tabelle; refresh alle 10 Sekunden</td></tr>"; | |||
|   out += "<tr><td>/data</td><td>zum Abruf der Messwerte in der Form Name=wert</td></tr>"; | |||
|   out += "<tr><td>:{port+1}/update</td><td>OTA</td></tr>"; | |||
|   out += "<tr><td>/reboot</td><td>startet neu</td></tr>"; | |||
|   out += "</table></body></html>"; | |||
|   server.send (200, "text/html", out); | |||
| } | |||
| 
 | |||
| 
 | |||
| void handleReboot () { | |||
|   //-------------------
 | |||
|   returnOK (); | |||
|   ESP.reset(); | |||
| } | |||
| 
 | |||
| 
 | |||
| void handleRoot() { | |||
|   //----------------
 | |||
|   String out = "<html><head><meta http-equiv=\"refresh\" content=\"10\":URL=\"" + server.uri() + "\"></head>"; | |||
|   out += "<body>"; | |||
|   out += "<h2>Hoymiles Micro-Inverters</h2>"; | |||
|   char floatString[20]; | |||
|   char line[100]; | |||
|   for (uint8_t wr = 0; wr < anzInv; wr++) { | |||
|     out += "<h3>" + String(inverters[wr].name) + "</h3>"; | |||
|     out += "<h3>S/N " + String (getSerialNoTxt(wr)) + "</h3>"; | |||
|     out += "<br><br><table border='1'>"; | |||
|     out += "<tr><th>Kanal</th><th>Wert</th><th>Einheit</th></tr>"; | |||
|     for (uint8_t i = 0; i < inverters[wr].anzTotalMeasures; i++) { | |||
|       dtostrf (getMeasureValue(wr, i),1, getDigits(wr,i), floatString); | |||
|       sprintf(line, "<tr><td>%s</td><td>%s</td><td>%s</td></tr>", getMeasureName(wr, i), floatString, getUnit(wr, i)); | |||
|       //DEBUG_OUT.println(line);
 | |||
|       out += String(line);  | |||
| /*        out += "<tr><td>" + getMeasureName(i) + "</td>";
 | |||
|       out += "<td>" +  String(getMeasureValue(i)) + "</td></tr>"; | |||
|       out += "<td>" +  String(getUnit(i)) + "</td></tr>"; */ | |||
|     } | |||
|     out += "</table>"; | |||
|   } | |||
|   int pos = out.indexOf("°"); | |||
|   do { | |||
|     if (pos>1) { | |||
|       out = out.substring (0, pos) + "°" + out.substring(pos+2); | |||
|     } | |||
|     pos = out.indexOf("°"); | |||
|   } while (pos>1); | |||
|   | |||
|   out += "</body></html>"; | |||
|   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 <ESP8266HTTPUpdateServer.h> | |||
| 
 | |||
| 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 | |||
| @ -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 | |||
| @ -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 | |||
| @ -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 | |||
| @ -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 | |||
| @ -0,0 +1,345 @@ | |||
| #ifndef __WIFI_H | |||
| #define __WIFI_H | |||
| 
 | |||
| #include "Settings.h" | |||
| #include "Debug.h" | |||
| #include <ESP8266WiFi.h> | |||
| #include <Pinger.h>       // 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 <WiFiUdp.h> | |||
| #include <TimeLib.h> | |||
| 
 | |||
| 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 | |||
| @ -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 T> 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
 | |||
| @ -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  | |||
| @ -0,0 +1,129 @@ | |||
| // #################  WebServer #################
 | |||
| 
 | |||
| #ifndef __MODWEBSERVER_H | |||
| #define __MODWEBSERVER_H | |||
| #define MODWEBSERVER | |||
| 
 | |||
| #include <ESP8266WebServer.h> | |||
| #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 = "<html>"; | |||
|   out += "<body><h2>Hilfe</h2>"; | |||
|   out += "<br><br><table>"; | |||
|   out += "<tr><td>/</td><td>zeigt alle Messwerte in einer Tabelle; refresh alle 10 Sekunden</td></tr>"; | |||
|   out += "<tr><td>/data</td><td>zum Abruf der Messwerte in der Form Name=wert</td></tr>"; | |||
|   out += "<tr><td>:{port+1}/update</td><td>OTA</td></tr>"; | |||
|   out += "<tr><td>/reboot</td><td>startet neu</td></tr>"; | |||
|   out += "</table></body></html>"; | |||
|   server.send (200, "text/html", out); | |||
| } | |||
| 
 | |||
| 
 | |||
| void handleReboot () { | |||
|   //-------------------
 | |||
|   returnOK (); | |||
|   ESP.reset(); | |||
| } | |||
| 
 | |||
| 
 | |||
| void handleRoot() { | |||
|   //----------------
 | |||
|   String out = "<html><head><meta http-equiv=\"refresh\" content=\"10\":URL=\"" + server.uri() + "\"></head>"; | |||
|   out += "<body>"; | |||
|   out += "<h2>Hoymiles Micro-Inverter HM-600</h2>"; | |||
|   out += "<br><br><table border='1'>"; | |||
|   out += "<tr><th>Kanal</th><th>Wert</th></tr>"; | |||
|   for (byte i = 0; i < ANZAHL_VALUES; i++) { | |||
|     out += "<tr><td>" +  String(getChannelName(i)) + "</td>"; | |||
|     out += "<td>" +  String(VALUES[i]) + "</td></tr>"; | |||
|   } | |||
|   out += "</table>"; | |||
|   out += "</body></html>"; | |||
|   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 <ESP8266HTTPUpdateServer.h> | |||
| 
 | |||
| 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 | |||
| @ -0,0 +1,597 @@ | |||
| #include <Arduino.h> | |||
| #include <SPI.h> | |||
| #include "CircularBuffer.h" | |||
| #include <RF24.h> | |||
| #include <RF24_config.h> | |||
| #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<NRF24_packet_t> packetBuffer(bufferData, sizeof(bufferData) / sizeof(bufferData[0])); | |||
| 
 | |||
| static Serial_header_t SerialHdr; | |||
| 
 | |||
| #define CHECKCRC  1 | |||
| static uint16_t lastCRC; | |||
| static uint16_t crc; | |||
| 
 | |||
| uint8_t         channels[]            = {/*3,*/ 23, 40, 61, 75};   //{1, 3, 6, 9, 11, 23, 40, 61, 75}
 | |||
| uint8_t         channelIdx            = 1;                         // fange mit 40 an
 | |||
| uint8_t         DEFAULT_SEND_CHANNEL  = channels[channelIdx];      // = 40
 | |||
| 
 | |||
| 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; | |||
| } | |||
| @ -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 <http://www.gnu.org/licenses/>.
 | |||
| */ | |||
| 
 | |||
| #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
 | |||
| @ -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 | |||
| @ -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 | |||
| @ -0,0 +1,142 @@ | |||
| 
 | |||
| #include <stdio.h> | |||
| #include <stdint.h> | |||
| #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; | |||
| } | |||
| @ -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); | |||
| @ -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; | |||
| } | |||
| @ -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); | |||
| }; | |||
| @ -0,0 +1,345 @@ | |||
| #ifndef __WIFI_H | |||
| #define __WIFI_H | |||
| 
 | |||
| #include "Settings.h" | |||
| #include "Debug.h" | |||
| #include <ESP8266WiFi.h> | |||
| #include <Pinger.h>       // 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 <WiFiUdp.h> | |||
| #include <TimeLib.h> | |||
| 
 | |||
| 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 | |||
| @ -0,0 +1,44 @@ | |||
| #ifndef __CONFIG_H__ | |||
| #define __CONFIG_H__ | |||
| 
 | |||
| // fallback WiFi info
 | |||
| #define FB_WIFI_SSID    "YOUR_WIFI_SSID" | |||
| #define FB_WIFI_PWD     "YOUR_WIFI_PWD" | |||
| 
 | |||
| 
 | |||
| // access point info
 | |||
| #define WIFI_AP_SSID    "AHOY DTU" | |||
| #define WIFI_AP_PWD     "esp_8266" | |||
| // stay in access point mode all the time
 | |||
| //#define AP_ONLY
 | |||
| 
 | |||
| 
 | |||
| //-------------------------------------
 | |||
| // CONFIGURATION - COMPILE TIME
 | |||
| //-------------------------------------
 | |||
| // time in seconds how long the station info (ssid + pwd) will be tried
 | |||
| #define WIFI_TRY_CONNECT_TIME   30 | |||
| 
 | |||
| // time during the ESP will act as access point on connection failure (to
 | |||
| // station) in seconds
 | |||
| #define WIFI_AP_ACTIVE_TIME     3*60 | |||
| 
 | |||
| // default device name
 | |||
| #define DEF_DEVICE_NAME         "ESP-DTU" | |||
| 
 | |||
| // number of packets hold in buffer
 | |||
| #define PACKET_BUFFER_SIZE      30 | |||
| 
 | |||
| // number of configurable inverters
 | |||
| #define MAX_NUM_INVERTERS       3 | |||
| 
 | |||
| // maximum human readable inverter name length
 | |||
| #define MAX_NAME_LENGTH         16 | |||
| 
 | |||
| // maximum buffer length of packet received / sent to RF24 module
 | |||
| #define MAX_RF_PAYLOAD_SIZE     32 | |||
| 
 | |||
| // changes the style of "/setup" page, visualized = nicer
 | |||
| #define LIVEDATA_VISUALIZED | |||
| 
 | |||
| #endif /*__CONFIG_H__*/ | |||
| @ -0,0 +1,39 @@ | |||
| #ifndef __DEBUG_H__ | |||
| #define __DEBUG_H__ | |||
| 
 | |||
| #ifdef NDEBUG | |||
|   #define DPRINT(str) | |||
|   #define DPRINTLN(str) | |||
| #else | |||
| 
 | |||
| #ifndef DSERIAL | |||
|   #define DSERIAL Serial | |||
| #endif | |||
| 
 | |||
|   template <class T> | |||
|   inline void DPRINT(T str) { DSERIAL.print(str); } | |||
|   template <class T> | |||
|   inline void DPRINTLN(T str) { DPRINT(str); DPRINT(F("\r\n")); } | |||
|   inline void DHEX(uint8_t b) { | |||
|     if( b<0x10 ) DSERIAL.print('0'); | |||
|     DSERIAL.print(b,HEX); | |||
|   } | |||
|   inline void DHEX(uint16_t b) { | |||
|     if( b<0x10 ) DSERIAL.print(F("000")); | |||
|     else if( b<0x100 ) DSERIAL.print(F("00")); | |||
|     else if( b<0x1000 ) DSERIAL.print(F("0")); | |||
|     DSERIAL.print(b,HEX); | |||
|   } | |||
|   inline void DHEX(uint32_t b) { | |||
|     if( b<0x10 ) DSERIAL.print(F("0000000")); | |||
|     else if( b<0x100 ) DSERIAL.print(F("000000")); | |||
|     else if( b<0x1000 ) DSERIAL.print(F("00000")); | |||
|     else if( b<0x10000 ) DSERIAL.print(F("0000")); | |||
|     else if( b<0x100000 ) DSERIAL.print(F("000")); | |||
|     else if( b<0x1000000 ) DSERIAL.print(F("00")); | |||
|     else if( b<0x10000000 ) DSERIAL.print(F("0")); | |||
|     DSERIAL.print(b,HEX); | |||
|   } | |||
| #endif | |||
| 
 | |||
| #endif /*__DEBUG_H__*/ | |||
| @ -1,41 +1,92 @@ | |||
| #ifndef __DEFINES_H__ | |||
| #define __DEFINES_H__ | |||
| 
 | |||
| #include "config.h" | |||
| 
 | |||
| //-------------------------------------
 | |||
| // PINOUT
 | |||
| // PINOUT (Default, can be changed in setup)
 | |||
| //-------------------------------------
 | |||
| #define RF24_IRQ_PIN        4 | |||
| #define RF24_CE_PIN         5 | |||
| #define RF24_CS_PIN         15 | |||
| #define RF24_CE_PIN         2 | |||
| #define RF24_IRQ_PIN        0 | |||
| 
 | |||
| 
 | |||
| //-------------------------------------
 | |||
| // VERSION
 | |||
| //-------------------------------------
 | |||
| #define VERSION_MAJOR       0 | |||
| #define VERSION_MINOR       1 | |||
| #define VERSION_MINOR       3 | |||
| #define VERSION_PATCH       9 | |||
| 
 | |||
| 
 | |||
| //-------------------------------------
 | |||
| typedef struct { | |||
|     uint8_t rxCh; | |||
|     uint8_t packet[MAX_RF_PAYLOAD_SIZE]; | |||
| } packet_t; | |||
| 
 | |||
| 
 | |||
| //-------------------------------------
 | |||
| // EEPROM
 | |||
| //-------------------------------------
 | |||
| #define SSID_LEN            32 | |||
| #define PWD_LEN             64 | |||
| #define DEVNAME_LEN         32 | |||
| #define CRC_LEN             2 | |||
| #define PWD_LEN             63 | |||
| #define DEVNAME_LEN         16 | |||
| #define CRC_LEN             2 // uint16_t
 | |||
| 
 | |||
| #define INV_ADDR_LEN        MAX_NUM_INVERTERS * 8                // uint64_t
 | |||
| #define INV_NAME_LEN        MAX_NUM_INVERTERS * MAX_NAME_LENGTH  // char[]
 | |||
| #define INV_TYPE_LEN        MAX_NUM_INVERTERS * 1                // uint8_t
 | |||
| #define INV_INTERVAL_LEN    2                                    // uint16_t
 | |||
| 
 | |||
| #define PINOUT_LEN          3 // 3 pins: CS, CE, IRQ
 | |||
| 
 | |||
| #define RF24_AMP_PWR_LEN    1 | |||
| 
 | |||
| #define MQTT_ADDR_LEN       4 // IP
 | |||
| #define MQTT_USER_LEN       16 | |||
| #define MQTT_PWD_LEN        32 | |||
| #define MQTT_TOPIC_LEN      32 | |||
| #define MQTT_INTERVAL_LEN   2 // uint16_t
 | |||
| #define MQTT_PORT_LEN       2 // uint16_t
 | |||
| 
 | |||
| #define SER_ENABLE_LEN      1 // uint8_t
 | |||
| #define SER_DEBUG_LEN       1 // uint8_t
 | |||
| #define SER_INTERVAL_LEN    2 // uint16_t
 | |||
| 
 | |||
| #define HOY_ADDR_LEN        6 | |||
| 
 | |||
| #define ADDR_START          0 | |||
| #define ADDR_SSID           ADDR_START | |||
| #define ADDR_PWD            ADDR_SSID          + SSID_LEN | |||
| #define ADDR_DEVNAME        ADDR_PWD           + PWD_LEN | |||
| #define ADDR_HOY_ADDR       ADDR_DEVNAME       + DEVNAME_LEN | |||
| #define ADDR_WIFI_CRC       ADDR_DEVNAME       + DEVNAME_LEN | |||
| #define ADDR_START_SETTINGS ADDR_WIFI_CRC      + CRC_LEN | |||
| 
 | |||
| #define ADDR_PINOUT         ADDR_START_SETTINGS | |||
| 
 | |||
| #define ADDR_RF24_AMP_PWR   ADDR_PINOUT        + PINOUT_LEN | |||
| 
 | |||
| #define ADDR_INV_ADDR       ADDR_RF24_AMP_PWR  + RF24_AMP_PWR_LEN | |||
| #define ADDR_INV_NAME       ADDR_INV_ADDR      + INV_ADDR_LEN | |||
| #define ADDR_INV_TYPE       ADDR_INV_NAME      + INV_NAME_LEN | |||
| #define ADDR_INV_INTERVAL   ADDR_INV_TYPE      + INV_TYPE_LEN | |||
| 
 | |||
| #define ADDR_MQTT_ADDR      ADDR_INV_INTERVAL  + INV_INTERVAL_LEN | |||
| #define ADDR_MQTT_USER      ADDR_MQTT_ADDR     + MQTT_ADDR_LEN | |||
| #define ADDR_MQTT_PWD       ADDR_MQTT_USER     + MQTT_USER_LEN | |||
| #define ADDR_MQTT_TOPIC     ADDR_MQTT_PWD      + MQTT_PWD_LEN | |||
| #define ADDR_MQTT_INTERVAL  ADDR_MQTT_TOPIC    + MQTT_TOPIC_LEN | |||
| #define ADDR_MQTT_PORT      ADDR_MQTT_INTERVAL + MQTT_INTERVAL_LEN | |||
| 
 | |||
| #define ADDR_SER_ENABLE     ADDR_MQTT_PORT     + MQTT_PORT_LEN | |||
| #define ADDR_SER_DEBUG      ADDR_SER_ENABLE    + SER_ENABLE_LEN | |||
| #define ADDR_SER_INTERVAL   ADDR_SER_DEBUG     + SER_DEBUG_LEN | |||
| #define ADDR_NEXT           ADDR_SER_INTERVAL  + SER_INTERVAL_LEN | |||
| 
 | |||
| #define ADDR_NEXT           ADDR_HOY_ADDR      + HOY_ADDR_LEN | |||
| #define ADDR_SETTINGS_CRC   400 | |||
| 
 | |||
| #define ADDR_SETTINGS_CRC   200 | |||
| #if(ADDR_SETTINGS_CRC <= ADDR_NEXT) | |||
| #error address overlap! | |||
| #endif | |||
| 
 | |||
| #endif /*__DEFINES_H__*/ | |||
|  | |||
| @ -1,130 +0,0 @@ | |||
| #include "eep.h" | |||
| #include <EEPROM.h> | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| eep::eep() { | |||
|     EEPROM.begin(500); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| eep::~eep() { | |||
|     EEPROM.end(); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::read(uint32_t addr, char *str, uint8_t length) { | |||
|     for(uint8_t i = 0; i < length; i ++) { | |||
|         *(str++) = (char)EEPROM.read(addr++); | |||
|     } | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::read(uint32_t addr, float *value) { | |||
|     uint8_t *p = (uint8_t*)value; | |||
|     for(uint8_t i = 0; i < 4; i ++) { | |||
|         *(p++) = (uint8_t)EEPROM.read(addr++); | |||
|     } | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::read(uint32_t addr, bool *value) { | |||
|     uint8_t intVal = 0x00; | |||
|     intVal = EEPROM.read(addr++); | |||
|     *value = (intVal == 0x01); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::read(uint32_t addr, uint8_t *value) { | |||
|     *value = (EEPROM.read(addr++)); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::read(uint32_t addr, uint8_t data[], uint8_t length) { | |||
|     for(uint8_t i = 0; i < length; i ++) { | |||
|         *(data++) = EEPROM.read(addr++); | |||
|     } | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::read(uint32_t addr, uint16_t *value) { | |||
|     *value  = (EEPROM.read(addr++) << 8); | |||
|     *value |= (EEPROM.read(addr++)); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::read(uint32_t addr, uint32_t *value) { | |||
|     *value  = (EEPROM.read(addr++) << 24); | |||
|     *value |= (EEPROM.read(addr++) << 16); | |||
|     *value |= (EEPROM.read(addr++) <<  8); | |||
|     *value |= (EEPROM.read(addr++)); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::write(uint32_t addr, const char *str, uint8_t length) { | |||
|     for(uint8_t i = 0; i < length; i ++) { | |||
|         EEPROM.write(addr++, str[i]); | |||
|     } | |||
|     EEPROM.commit(); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::write(uint32_t addr, uint8_t data[], uint8_t length) { | |||
|     for(uint8_t i = 0; i < length; i ++) { | |||
|         EEPROM.write(addr++, data[i]); | |||
|     } | |||
|     EEPROM.commit(); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::write(uint32_t addr, float value) { | |||
|     uint8_t *p = (uint8_t*)&value; | |||
|     for(uint8_t i = 0; i < 4; i ++) { | |||
|         EEPROM.write(addr++, p[i]); | |||
|     } | |||
|     EEPROM.commit(); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::write(uint32_t addr, bool value) { | |||
|     uint8_t intVal = (value) ? 0x01 : 0x00; | |||
|     EEPROM.write(addr++, intVal); | |||
|     EEPROM.commit(); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::write(uint32_t addr, uint8_t value) { | |||
|     EEPROM.write(addr++, value); | |||
|     EEPROM.commit(); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::write(uint32_t addr, uint16_t value) { | |||
|     EEPROM.write(addr++, (value >> 8) & 0xff); | |||
|     EEPROM.write(addr++, (value     ) & 0xff); | |||
|     EEPROM.commit(); | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void eep::write(uint32_t addr, uint32_t value) { | |||
|     EEPROM.write(addr++, (value >> 24) & 0xff); | |||
|     EEPROM.write(addr++, (value >> 16) & 0xff); | |||
|     EEPROM.write(addr++, (value >>  8) & 0xff); | |||
|     EEPROM.write(addr++, (value      ) & 0xff); | |||
|     EEPROM.commit(); | |||
| } | |||
| @ -0,0 +1,159 @@ | |||
| #ifndef __HM_DEFINES_H__ | |||
| #define __HM_DEFINES_H__ | |||
| 
 | |||
| #include "debug.h" | |||
| #include <cstdint> | |||
| 
 | |||
| 
 | |||
| union serial_u { | |||
|     uint64_t u64; | |||
|     uint8_t  b[8]; | |||
| }; | |||
| 
 | |||
| 
 | |||
| // units
 | |||
| enum {UNIT_V = 0, UNIT_A, UNIT_W,  UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT}; | |||
| const char* const units[] = {"V", "A", "W", "Wh", "kWh", "Hz", "°C", "%"}; | |||
| 
 | |||
| 
 | |||
| // field types
 | |||
| enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT, | |||
|         FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_T, FLD_PCT}; | |||
| const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", | |||
|         "U_AC", "I_AC", "P_AC", "Freq", "Temp", "Pct"}; | |||
| 
 | |||
| 
 | |||
| // indices to calculation functions, defined in hmInverter.h
 | |||
| enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH}; | |||
| 
 | |||
| 
 | |||
| // CH0 is default channel (freq, ac, temp)
 | |||
| 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, INV_TYPE_HM800}; | |||
| const char* const invTypes[] = {"HM600", "HM1200 / HM1500", "HM400", "HM800"}; | |||
| #define NUM_INVERTER_TYPES   4 | |||
| 
 | |||
| 
 | |||
| typedef struct { | |||
|     uint8_t    fieldId; // field id
 | |||
|     uint8_t    unitId;  // uint id
 | |||
|     uint8_t    ch;      // channel 0 - 3
 | |||
|     uint8_t    cmdId;   // received command id
 | |||
|     uint8_t    start;   // pos of first byte in buffer
 | |||
|     uint8_t    num;     // number of bytes in buffer
 | |||
|     uint16_t   div;     // divisor
 | |||
| } byteAssign_t; | |||
| 
 | |||
| 
 | |||
| /**
 | |||
|  *  indices are built for the buffer starting with cmd-id in first byte | |||
|  *  (complete payload in buffer) | |||
|  * */ | |||
| 
 | |||
| //-------------------------------------
 | |||
| // HM400 HM350?, HM300?
 | |||
| //-------------------------------------
 | |||
| const byteAssign_t hm400assignment[] = { | |||
|     { 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_YT,  UNIT_KWH, CH1, CMD01,  9, 4, 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   }, | |||
|     { FLD_IAC, UNIT_A,   CH0, CMD82,  7, 2, 100  }, | |||
|     { FLD_T,   UNIT_C,   CH0, CMD82, 11, 2, 10   } | |||
| }; | |||
| #define HM400_LIST_LEN     (sizeof(hm400assignment) / sizeof(byteAssign_t)) | |||
| 
 | |||
| 
 | |||
| //-------------------------------------
 | |||
| // HM600, HM700
 | |||
| //-------------------------------------
 | |||
| const byteAssign_t hm600assignment[] = { | |||
|     { 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 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
 | |||
| //-------------------------------------
 | |||
| const byteAssign_t hm1200assignment[] = { | |||
|     { FLD_UDC, UNIT_V,   CH1, CMD01,  3, 2, 10   }, | |||
|     { FLD_IDC, UNIT_A,   CH1, CMD01,  5, 2, 100  }, | |||
|     { FLD_PDC, UNIT_W,   CH1, CMD01,  9, 2, 10   }, | |||
|     { FLD_YD,  UNIT_WH,  CH1, CMD02,  5, 2, 1    }, | |||
|     { FLD_YT,  UNIT_KWH, CH1, CMD01, 13, 4, 1000 }, | |||
|     { FLD_UDC, UNIT_V,   CH3, CMD02,  9, 2, 10   }, | |||
|     { FLD_IDC, UNIT_A,   CH2, CMD01,  7, 2, 100  }, | |||
|     { FLD_PDC, UNIT_W,   CH2, CMD01, 11, 2, 10   }, | |||
|     { FLD_YD,  UNIT_WH,  CH2, CMD02,  7, 2, 1    }, | |||
|     { FLD_YT,  UNIT_KWH, CH2, CMD02,  1, 4, 1000 }, | |||
|     { FLD_IDC, UNIT_A,   CH3, CMD02, 11, 2, 100  }, | |||
|     { FLD_PDC, UNIT_W,   CH3, CMD02, 15, 2, 10   }, | |||
|     { FLD_YD,  UNIT_WH,  CH3, CMD03, 11, 2, 1    }, | |||
|     { FLD_YT,  UNIT_KWH, CH3, CMD03,  3, 4, 1000 }, | |||
|     { FLD_IDC, UNIT_A,   CH4, CMD02, 13, 2, 100  }, | |||
|     { FLD_PDC, UNIT_W,   CH4, CMD03,  1, 2, 10   }, | |||
|     { FLD_YD,  UNIT_WH,  CH4, CMD03, 13, 2, 1    }, | |||
|     { FLD_YT,  UNIT_KWH, CH4, CMD03,  7, 4, 1000 }, | |||
|     { FLD_UAC, UNIT_V,   CH0, CMD03, 15, 2, 10   }, | |||
|     { FLD_IAC, UNIT_A,   CH0, CMD84,  7, 2, 100  }, | |||
|     { FLD_PAC, UNIT_W,   CH0, CMD84,  3, 2, 10   }, | |||
|     { FLD_F,   UNIT_HZ,  CH0, CMD84,  1, 2, 100  }, | |||
|     { FLD_PCT, UNIT_PCT, CH0, CMD84,  9, 2, 10   }, | |||
|     { FLD_T,   UNIT_C,   CH0, CMD84, 11, 2, 10   }, | |||
|     { FLD_YD,  UNIT_WH,  CH0, CMDFF, CALC_YD_CH0,   0, 0 }, | |||
|     { FLD_YT,  UNIT_KWH, CH0, CMDFF, CALC_YT_CH0,   0, 0 }, | |||
|     { FLD_UDC, UNIT_V,   CH2, CMDFF, CALC_UDC_CH, CH1, 0 }, | |||
|     { FLD_UDC, UNIT_V,   CH4, CMDFF, CALC_UDC_CH, CH3, 0 } | |||
| }; | |||
| #define HM1200_LIST_LEN     (sizeof(hm1200assignment) / sizeof(byteAssign_t)) | |||
| 
 | |||
| 
 | |||
| 
 | |||
| 
 | |||
| #endif /*__HM_DEFINES_H__*/ | |||
| @ -0,0 +1,213 @@ | |||
| #ifndef __HM_INVERTER_H__ | |||
| #define __HM_INVERTER_H__ | |||
| 
 | |||
| #include "hmDefines.h" | |||
| 
 | |||
| /**
 | |||
|  * For values which are of interest and not transmitted by the inverter can be | |||
|  * calculated automatically. | |||
|  * A list of functions can be linked to the assignment and will be executed | |||
|  * automatically. Their result does not differ from original read values. | |||
|  * The special command 0xff (CMDFF) must be used. | |||
|  */ | |||
| 
 | |||
| // forward declaration of class
 | |||
| template <class RECORDTYPE=float> | |||
| class Inverter; | |||
| 
 | |||
| 
 | |||
| // prototypes
 | |||
| template<class T=float> | |||
| static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0); | |||
| 
 | |||
| template<class T=float> | |||
| static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0); | |||
| 
 | |||
| template<class T=float> | |||
| static T calcUdcCh(Inverter<> *iv, uint8_t arg0); | |||
| 
 | |||
| template<class T=float> | |||
| using func_t = T (Inverter<> *, uint8_t); | |||
| 
 | |||
| template<class T=float> | |||
| struct calcFunc_t { | |||
|     uint8_t funcId; // unique id
 | |||
|     func_t<T>*  func;   // function pointer
 | |||
| } ; | |||
| 
 | |||
| 
 | |||
| // list of all available functions, mapped in hmDefines.h
 | |||
| template<class T=float> | |||
| const calcFunc_t<T> calcFunctions[] = { | |||
|     { CALC_YT_CH0, &calcYieldTotalCh0 }, | |||
|     { CALC_YD_CH0, &calcYieldDayCh0   }, | |||
|     { CALC_UDC_CH, &calcUdcCh         } | |||
| }; | |||
| 
 | |||
| 
 | |||
| template <class RECORDTYPE> | |||
| class Inverter { | |||
|     public: | |||
|         uint8_t       id;       // unique id
 | |||
|         char          name[MAX_NAME_LENGTH]; // human readable name, eg. "HM-600.1"
 | |||
|         uint8_t       type;     // integer which refers to inverter type
 | |||
|         byteAssign_t* assign;   // type of inverter
 | |||
|         uint8_t       listLen;  // length of assignments
 | |||
|         serial_u      serial;   // serial number as on barcode
 | |||
|         serial_u      radioId;  // id converted to modbus
 | |||
|         uint8_t       channels; // number of PV channels (1-4)
 | |||
|         RECORDTYPE    *record;  // pointer for values
 | |||
| 
 | |||
|         Inverter() { | |||
| 
 | |||
|         } | |||
| 
 | |||
|         ~Inverter() { | |||
|             // TODO: cleanup
 | |||
|         } | |||
| 
 | |||
|         void init(void) { | |||
|             getAssignment(); | |||
|             toRadioId(); | |||
|             record = new RECORDTYPE[listLen]; | |||
|             memset(name, 0, MAX_NAME_LENGTH); | |||
|             memset(record, 0, sizeof(RECORDTYPE) * listLen); | |||
|         } | |||
| 
 | |||
|         uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId) { | |||
|             uint8_t pos = 0; | |||
|             for(; pos < listLen; pos++) { | |||
|                 if((assign[pos].ch == channel) && (assign[pos].fieldId == fieldId)) | |||
|                     break; | |||
|             } | |||
|             return (pos >= listLen) ? 0xff : pos; | |||
|         } | |||
| 
 | |||
|         const char *getFieldName(uint8_t pos) { | |||
|             return fields[assign[pos].fieldId]; | |||
|         } | |||
| 
 | |||
|         const char *getUnit(uint8_t pos) { | |||
|             return units[assign[pos].unitId]; | |||
|         } | |||
| 
 | |||
|         uint8_t getChannel(uint8_t pos) { | |||
|             return assign[pos].ch; | |||
|         } | |||
| 
 | |||
|         uint8_t getCmdId(uint8_t pos) { | |||
|             return assign[pos].cmdId; | |||
|         } | |||
| 
 | |||
|         void addValue(uint8_t pos, uint8_t buf[]) { | |||
|             uint8_t ptr  = assign[pos].start; | |||
|             uint8_t end  = ptr + assign[pos].num; | |||
|             uint16_t div = assign[pos].div; | |||
| 
 | |||
|             uint32_t val = 0; | |||
|             do { | |||
|                 val <<= 8; | |||
|                 val |= buf[ptr]; | |||
|             } while(++ptr != end); | |||
| 
 | |||
|             record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div); | |||
|         } | |||
| 
 | |||
|         RECORDTYPE getValue(uint8_t pos) { | |||
|             return record[pos]; | |||
|         } | |||
| 
 | |||
|         void doCalculations(void) { | |||
|             for(uint8_t i = 0; i < listLen; i++) { | |||
|                 if(CMDFF == assign[i].cmdId) { | |||
|                     record[i] = calcFunctions<RECORDTYPE>[assign[i].start].func(this, assign[i].num); | |||
|                 } | |||
|             } | |||
|         } | |||
| 
 | |||
|     private: | |||
|         void toRadioId(void) { | |||
|             radioId.u64  = 0ULL; | |||
|             radioId.b[4] = serial.b[0]; | |||
|             radioId.b[3] = serial.b[1]; | |||
|             radioId.b[2] = serial.b[2]; | |||
|             radioId.b[1] = serial.b[3]; | |||
|             radioId.b[0] = 0x01; | |||
|         } | |||
| 
 | |||
|         void getAssignment(void) { | |||
|             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 == 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 { | |||
|                 listLen  = 0; | |||
|                 channels = 0; | |||
|                 assign   = NULL; | |||
|             } | |||
|         } | |||
| }; | |||
| 
 | |||
| 
 | |||
| /**
 | |||
|  * To calculate values which are not transmitted by the unit there is a generic | |||
|  * list of functions which can be linked to the assignment. | |||
|  * The special command 0xff (CMDFF) must be used. | |||
|  */ | |||
| 
 | |||
| template<class T=float> | |||
| static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) { | |||
|     if(NULL != iv) { | |||
|         T yield = 0; | |||
|         for(uint8_t i = 1; i <= iv->channels; i++) { | |||
|             uint8_t pos = iv->getPosByChFld(i, FLD_YT); | |||
|             yield += iv->getValue(pos); | |||
|         } | |||
|         return yield; | |||
|     } | |||
|     return 0.0; | |||
| } | |||
| 
 | |||
| template<class T=float> | |||
| static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) { | |||
|     if(NULL != iv) { | |||
|         T yield = 0; | |||
|         for(uint8_t i = 1; i <= iv->channels; i++) { | |||
|             uint8_t pos = iv->getPosByChFld(i, FLD_YD); | |||
|             yield += iv->getValue(pos); | |||
|         } | |||
|         return yield; | |||
|     } | |||
|     return 0.0; | |||
| } | |||
| 
 | |||
| template<class T=float> | |||
| static T calcUdcCh(Inverter<> *iv, uint8_t arg0) { | |||
|     // arg0 = channel of source
 | |||
|     for(uint8_t i = 0; i < iv->listLen; i++) { | |||
|         if((FLD_UDC == iv->assign[i].fieldId) && (arg0 == iv->assign[i].ch)) { | |||
|             return iv->getValue(i); | |||
|         } | |||
|     } | |||
| 
 | |||
|     return 0.0; | |||
| } | |||
| 
 | |||
| 
 | |||
| #endif /*__HM_INVERTER_H__*/ | |||
| @ -0,0 +1,328 @@ | |||
| #ifndef __RADIO_H__ | |||
| #define __RADIO_H__ | |||
| 
 | |||
| #include <RF24.h> | |||
| #include <RF24_config.h> | |||
| #include "crc.h" | |||
| 
 | |||
| //#define CHANNEL_HOP // switch between channels or use static channel to send
 | |||
| 
 | |||
| #define DEFAULT_RECV_CHANNEL    3 | |||
| #define SPI_SPEED               1000000 | |||
| 
 | |||
| #define DTU_RADIO_ID            ((uint64_t)0x1234567801ULL) | |||
| #define DUMMY_RADIO_ID          ((uint64_t)0xDEADBEEF01ULL) | |||
| 
 | |||
| #define RX_LOOP_CNT             400 | |||
| 
 | |||
| const char* const rf24AmpPower[] = {"MIN", "LOW", "HIGH", "MAX"}; | |||
| 
 | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| // MACROS
 | |||
| //-----------------------------------------------------------------------------
 | |||
| #define CP_U32_LittleEndian(buf, v) ({ \ | |||
|     uint8_t *b = buf; \ | |||
|     b[0] = ((v >> 24) & 0xff); \ | |||
|     b[1] = ((v >> 16) & 0xff); \ | |||
|     b[2] = ((v >>  8) & 0xff); \ | |||
|     b[3] = ((v      ) & 0xff); \ | |||
| }) | |||
| 
 | |||
| #define CP_U32_BigEndian(buf, v) ({ \ | |||
|     uint8_t *b = buf; \ | |||
|     b[3] = ((v >> 24) & 0xff); \ | |||
|     b[2] = ((v >> 16) & 0xff); \ | |||
|     b[1] = ((v >>  8) & 0xff); \ | |||
|     b[0] = ((v      ) & 0xff); \ | |||
| }) | |||
| 
 | |||
| #define BIT_CNT(x)  ((x)<<3) | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| // HM Radio class
 | |||
| //-----------------------------------------------------------------------------
 | |||
| template <uint8_t CE_PIN, uint8_t CS_PIN, uint8_t IRQ_PIN, class BUFFER, uint64_t DTU_ID=DTU_RADIO_ID> | |||
| class HmRadio { | |||
|     public: | |||
|         HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { | |||
|             mTxChLst[0] = 40; | |||
|             //mTxChIdx = 1;
 | |||
| 
 | |||
|             mRxChLst[0] = 3; | |||
|             mRxChLst[1] = 23; | |||
|             mRxChLst[2] = 61; | |||
|             mRxChLst[3] = 75; | |||
|             mRxChIdx    = 0; | |||
|             mRxLoopCnt  = RX_LOOP_CNT; | |||
| 
 | |||
|             //calcDtuCrc();
 | |||
| 
 | |||
|             pinCs  = CS_PIN; | |||
|             pinCe  = CE_PIN; | |||
|             pinIrq = IRQ_PIN; | |||
| 
 | |||
|             AmplifierPower = 1; | |||
|             mSendCnt       = 0; | |||
|         } | |||
|         ~HmRadio() {} | |||
| 
 | |||
|         void setup(BUFFER *ctrl) { | |||
|             pinMode(pinIrq, INPUT_PULLUP); | |||
| 
 | |||
|             mBufCtrl = ctrl; | |||
| 
 | |||
|             mNrf24.begin(pinCe, pinCs); | |||
|             mNrf24.setRetries(0, 0); | |||
| 
 | |||
|             mNrf24.setChannel(DEFAULT_RECV_CHANNEL); | |||
|             mNrf24.setDataRate(RF24_250KBPS); | |||
|             mNrf24.disableCRC(); | |||
|             mNrf24.setAutoAck(false); | |||
|             mNrf24.setPayloadSize(MAX_RF_PAYLOAD_SIZE); | |||
|             mNrf24.setAddressWidth(5); | |||
|             mNrf24.openReadingPipe(1, DTU_RADIO_ID); | |||
| 
 | |||
|             // enable only receiving interrupts
 | |||
|             mNrf24.maskIRQ(true, true, false); | |||
| 
 | |||
|             DPRINTLN("RF24 Amp Pwr: RF24_PA_" + String(rf24AmpPower[AmplifierPower])); | |||
|             mNrf24.setPALevel(AmplifierPower & 0x03); | |||
|             mNrf24.startListening(); | |||
| 
 | |||
|             DPRINTLN("Radio Config:"); | |||
|             mNrf24.printPrettyDetails(); | |||
| 
 | |||
|             mTxCh = getDefaultChannel(); | |||
| 
 | |||
|             if(!mNrf24.isChipConnected()) { | |||
|                 DPRINTLN("WARNING! your NRF24 module can't be reached, check the wiring"); | |||
|             } | |||
|         } | |||
| 
 | |||
|         void handleIntr(void) { | |||
|             uint8_t pipe, len; | |||
|             packet_t *p; | |||
| 
 | |||
|             DISABLE_IRQ; | |||
|             while(mNrf24.available(&pipe)) { | |||
|                 if(!mBufCtrl->full()) { | |||
|                     p = mBufCtrl->getFront(); | |||
|                     memset(p->packet, 0xcc, MAX_RF_PAYLOAD_SIZE); | |||
|                     p->rxCh = mRxChIdx; | |||
|                     len = mNrf24.getPayloadSize(); | |||
|                     if(len > MAX_RF_PAYLOAD_SIZE) | |||
|                         len = MAX_RF_PAYLOAD_SIZE; | |||
| 
 | |||
|                     mNrf24.read(p->packet, len); | |||
|                     mBufCtrl->pushFront(p); | |||
|                 } | |||
|                 else { | |||
|                     bool tx_ok, tx_fail, rx_ready; | |||
|                     mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // reset interrupt status
 | |||
|                     mNrf24.flush_rx(); // drop the packet
 | |||
|                 } | |||
|             } | |||
|             RESTORE_IRQ; | |||
|         } | |||
| 
 | |||
|         uint8_t getDefaultChannel(void) { | |||
|             return mTxChLst[0]; | |||
|         } | |||
|         /*uint8_t getLastChannel(void) {
 | |||
|             return mTxChLst[mTxChIdx]; | |||
|         } | |||
| 
 | |||
|         uint8_t getNxtChannel(void) { | |||
|             if(++mTxChIdx >= 4) | |||
|                 mTxChIdx = 0; | |||
|             return mTxChLst[mTxChIdx]; | |||
|         }*/ | |||
| 
 | |||
|         void sendTimePacket(uint64_t invId, uint32_t ts) { | |||
|             sendCmdPacket(invId, 0x15, 0x80, false); | |||
|             mTxBuf[10] = 0x0b; // cid
 | |||
|             mTxBuf[11] = 0x00; | |||
|             CP_U32_LittleEndian(&mTxBuf[12], ts); | |||
|             mTxBuf[19] = 0x05; | |||
| 
 | |||
|             uint16_t crc = crc16(&mTxBuf[10], 14); | |||
|             mTxBuf[24] = (crc >> 8) & 0xff; | |||
|             mTxBuf[25] = (crc     ) & 0xff; | |||
|             mTxBuf[26] = crc8(mTxBuf, 26); | |||
| 
 | |||
|             sendPacket(invId, mTxBuf, 27, true); | |||
|         } | |||
| 
 | |||
|         void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t cmd, bool calcCrc = true) { | |||
|             memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); | |||
|             mTxBuf[0] = mid; // message id
 | |||
|             CP_U32_BigEndian(&mTxBuf[1], (invId  >> 8)); | |||
|             CP_U32_BigEndian(&mTxBuf[5], (DTU_ID >> 8)); | |||
|             mTxBuf[9]  = cmd; | |||
|             if(calcCrc) { | |||
|                 mTxBuf[10] = crc8(mTxBuf, 10); | |||
|                 sendPacket(invId, mTxBuf, 11, false); | |||
|             } | |||
|         } | |||
| 
 | |||
|         bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t *rptCnt, uint8_t rxCh) { | |||
|             *len = (buf[0] >> 2); | |||
|             if(*len > (MAX_RF_PAYLOAD_SIZE - 2)) | |||
|                 *len = MAX_RF_PAYLOAD_SIZE - 2; | |||
|             for(uint8_t i = 1; i < (*len + 1); i++) { | |||
|                 buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7); | |||
|             } | |||
| 
 | |||
|             uint8_t crc = crc8(buf, *len-1); | |||
|             bool valid  = (crc == buf[*len-1]); | |||
| 
 | |||
|             if(valid) { | |||
|                 if(mLastCrc == crc) | |||
|                     *rptCnt = (++mRptCnt); | |||
|                 else { | |||
|                     mRptCnt = 0; | |||
|                     *rptCnt = 0; | |||
|                     mLastCrc = crc; | |||
|                 } | |||
|                 mRxStat[(buf[9] & 0x7F)-1]++; | |||
|                 mRxChStat[(buf[9] & 0x7F)-1][rxCh & 0x7]++; | |||
|             } | |||
|             /*else {
 | |||
|                 DPRINT("CRC wrong: "); | |||
|                 DHEX(crc); | |||
|                 DPRINT(" != "); | |||
|                 DHEX(buf[*len-1]); | |||
|                 DPRINTLN(""); | |||
|             }*/ | |||
| 
 | |||
|             return valid; | |||
|         } | |||
| 
 | |||
|         bool switchRxCh(uint8_t addLoop = 0) { | |||
|             mRxLoopCnt += addLoop; | |||
|             if(mRxLoopCnt != 0) { | |||
|                 mRxLoopCnt--; | |||
|                 DISABLE_IRQ; | |||
|                 mNrf24.stopListening(); | |||
|                 mNrf24.setChannel(getRxNxtChannel()); | |||
|                 mNrf24.startListening(); | |||
|                 RESTORE_IRQ; | |||
|             } | |||
|             return (0 == mRxLoopCnt); // receive finished
 | |||
|         } | |||
| 
 | |||
|         void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { | |||
|             DPRINT(String(info)); | |||
|             for(uint8_t i = 0; i < len; i++) { | |||
|                 DHEX(buf[i]); | |||
|                 DPRINT(" "); | |||
|             } | |||
|             DPRINTLN(""); | |||
|         } | |||
| 
 | |||
|         bool isChipConnected(void) { | |||
|             return mNrf24.isChipConnected(); | |||
|         } | |||
| 
 | |||
|         uint8_t pinCs; | |||
|         uint8_t pinCe; | |||
|         uint8_t pinIrq; | |||
| 
 | |||
|         uint8_t AmplifierPower; | |||
|         uint32_t mSendCnt; | |||
| 
 | |||
|     private: | |||
|         void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len, bool clear=false) { | |||
|             //DPRINTLN("sent packet: #" + String(mSendCnt));
 | |||
|             //dumpBuf("SEN ", buf, len);
 | |||
| 
 | |||
|             DISABLE_IRQ; | |||
|             mNrf24.stopListening(); | |||
| 
 | |||
|             if(clear) { | |||
|                 uint8_t cnt = 4; | |||
|                 for(uint8_t i = 0; i < 4; i ++) { | |||
|                     DPRINT(String(mRxStat[i]) + " ("); | |||
|                     for(uint8_t j = 0; j < 4; j++) { | |||
|                         DPRINT(String(mRxChStat[i][j])); | |||
|                     } | |||
|                     DPRINT(") "); | |||
|                     if(0 != mRxStat[i]) | |||
|                         cnt--; | |||
|                 } | |||
|                 if(cnt == 0) | |||
|                     DPRINTLN(" -> all"); | |||
|                 else | |||
|                     DPRINTLN(" -> missing: " + String(cnt)); | |||
|                 memset(mRxStat, 0, 4); | |||
|                 memset(mRxChStat, 0, 4*8); | |||
|                 mRxLoopCnt = RX_LOOP_CNT; | |||
|             } | |||
| 
 | |||
|             mTxCh = getDefaultChannel(); | |||
|             mNrf24.setChannel(mTxCh); | |||
| 
 | |||
|             mNrf24.openWritingPipe(invId); // TODO: deprecated
 | |||
|             mNrf24.setCRCLength(RF24_CRC_16); | |||
|             mNrf24.enableDynamicPayloads(); | |||
|             mNrf24.setAutoAck(true); | |||
|             mNrf24.setRetries(3, 15); // 3*250us and 15 loops -> 11.25ms
 | |||
| 
 | |||
|             mNrf24.write(buf, len); | |||
| 
 | |||
|             // Try to avoid zero payload acks (has no effect)
 | |||
|             mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated
 | |||
| 
 | |||
|             mNrf24.setAutoAck(false); | |||
|             mNrf24.setRetries(0, 0); | |||
|             mNrf24.disableDynamicPayloads(); | |||
|             mNrf24.setCRCLength(RF24_CRC_DISABLED); | |||
| 
 | |||
|             mRxChIdx = 0; | |||
|             mNrf24.setChannel(mRxChLst[mRxChIdx]); | |||
|             mNrf24.startListening(); | |||
| 
 | |||
|             RESTORE_IRQ; | |||
|             mSendCnt++; | |||
|         } | |||
| 
 | |||
|         uint8_t getRxNxtChannel(void) { | |||
|             if(++mRxChIdx >= 4) | |||
|                 mRxChIdx = 0; | |||
|             return mRxChLst[mRxChIdx]; | |||
|         } | |||
| 
 | |||
|         /*void calcDtuCrc(void) {
 | |||
|             uint64_t addr = DTU_RADIO_ID; | |||
|             uint8_t tmp[5]; | |||
|             for(int8_t i = 4; i >= 0; i--) { | |||
|                 tmp[i] = addr; | |||
|                 addr >>= 8; | |||
|             } | |||
|             mDtuIdCrc = crc16nrf24(tmp, BIT_CNT(5)); | |||
|         }*/ | |||
| 
 | |||
|         uint8_t mTxCh; | |||
|         uint8_t mTxChLst[1]; | |||
|         //uint8_t mTxChIdx;
 | |||
| 
 | |||
|         uint8_t mRxChLst[4]; | |||
|         uint8_t mRxChIdx; | |||
|         uint8_t mRxStat[4]; | |||
|         uint8_t mRxChStat[4][8]; | |||
|         uint16_t mRxLoopCnt; | |||
| 
 | |||
|         //uint16_t mDtuIdCrc;
 | |||
|         uint16_t mLastCrc; | |||
|         uint8_t mRptCnt; | |||
| 
 | |||
|         RF24 mNrf24; | |||
|         BUFFER *mBufCtrl; | |||
|         uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; | |||
| 
 | |||
| }; | |||
| 
 | |||
| #endif /*__RADIO_H__*/ | |||
| @ -0,0 +1,82 @@ | |||
| #ifndef __HM_SYSTEM_H__ | |||
| #define __HM_SYSTEM_H__ | |||
| 
 | |||
| #include "hmInverter.h" | |||
| #ifndef NO_RADIO | |||
| #include "hmRadio.h" | |||
| #endif | |||
| 
 | |||
| 
 | |||
| 
 | |||
| template <class RADIO, class BUFFER, uint8_t MAX_INVERTER=3, class INVERTERTYPE=Inverter<float>> | |||
| class HmSystem { | |||
|     public: | |||
|         typedef RADIO RadioType; | |||
|         RadioType Radio; | |||
|         typedef BUFFER BufferType; | |||
|         BufferType BufCtrl; | |||
| 
 | |||
|         HmSystem() { | |||
|             mNumInv = 0; | |||
|         } | |||
|         ~HmSystem() { | |||
|             // TODO: cleanup
 | |||
|         } | |||
| 
 | |||
|         void setup() { | |||
|             Radio.setup(&BufCtrl); | |||
|         } | |||
| 
 | |||
|         INVERTERTYPE *addInverter(const char *name, uint64_t serial, uint8_t type) { | |||
|             if(MAX_INVERTER <= mNumInv) { | |||
|                 DPRINT("max number of inverters reached!"); | |||
|                 return NULL; | |||
|             } | |||
|             INVERTERTYPE *p = &mInverter[mNumInv]; | |||
|             p->id         = mNumInv; | |||
|             p->serial.u64 = serial; | |||
|             p->type       = type; | |||
|             p->init(); | |||
|             uint8_t len   = (uint8_t)strlen(name); | |||
|             strncpy(p->name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len); | |||
| 
 | |||
|             if(NULL == p->assign) { | |||
|                 DPRINT("no assignment for type found!"); | |||
|                 return NULL; | |||
|             } | |||
|             else { | |||
|                 mNumInv ++; | |||
|                 return p; | |||
|             } | |||
|         } | |||
| 
 | |||
|         INVERTERTYPE *findInverter(uint8_t buf[]) { | |||
|             INVERTERTYPE *p; | |||
|             for(uint8_t i = 0; i < mNumInv; i++) { | |||
|                 p = &mInverter[i]; | |||
|                 if((p->serial.b[3] == buf[0]) | |||
|                     && (p->serial.b[2] == buf[1]) | |||
|                     && (p->serial.b[1] == buf[2]) | |||
|                     && (p->serial.b[0] == buf[3])) | |||
|                     return p; | |||
|             } | |||
|             return NULL; | |||
|         } | |||
| 
 | |||
|         INVERTERTYPE *getInverterByPos(uint8_t pos) { | |||
|             if(mInverter[pos].serial.u64 != 0ULL) | |||
|                 return &mInverter[pos]; | |||
|             else | |||
|                 return NULL; | |||
|         } | |||
| 
 | |||
|         uint8_t getNumInverters(void) { | |||
|             return mNumInv; | |||
|         } | |||
| 
 | |||
|     private: | |||
|         INVERTERTYPE mInverter[MAX_INVERTER]; | |||
|         uint8_t mNumInv; | |||
| }; | |||
| 
 | |||
| #endif /*__HM_SYSTEM_H__*/ | |||
| @ -1,178 +0,0 @@ | |||
| #ifndef __HOYMILES_H__ | |||
| #define __HOYMILES_H__ | |||
| 
 | |||
| #include <RF24.h> | |||
| #include <RF24_config.h> | |||
| #include "crc.h" | |||
| 
 | |||
| #define CHANNEL_HOP // switch between channels or use static channel to send
 | |||
| 
 | |||
| #define luint64_t       long long unsigned int | |||
| 
 | |||
| #define DEFAULT_RECV_CHANNEL    3 | |||
| #define MAX_RF_PAYLOAD_SIZE     32 | |||
| #define DTU_RADIO_ID            ((uint64_t)0x1234567801ULL) | |||
| #define DUMMY_RADIO_ID          ((uint64_t)0xDEADBEEF01ULL) | |||
| 
 | |||
| #define PACKET_BUFFER_SIZE      30 | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| // MACROS
 | |||
| #define CP_U32_LittleEndian(buf, v) ({ \ | |||
|     uint8_t *b = buf; \ | |||
|     b[0] = ((v >> 24) & 0xff); \ | |||
|     b[1] = ((v >> 16) & 0xff); \ | |||
|     b[2] = ((v >>  8) & 0xff); \ | |||
|     b[3] = ((v      ) & 0xff); \ | |||
| }) | |||
| 
 | |||
| #define CP_U32_BigEndian(buf, v) ({ \ | |||
|     uint8_t *b = buf; \ | |||
|     b[3] = ((v >> 24) & 0xff); \ | |||
|     b[2] = ((v >> 16) & 0xff); \ | |||
|     b[1] = ((v >>  8) & 0xff); \ | |||
|     b[0] = ((v      ) & 0xff); \ | |||
| }) | |||
| 
 | |||
| #define BIT_CNT(x)  ((x)<<3) | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| union uint64Bytes { | |||
|     uint64_t ull; | |||
|     uint8_t bytes[8]; | |||
| }; | |||
| 
 | |||
| typedef struct { | |||
|     uint8_t sendCh; | |||
|     uint8_t packet[MAX_RF_PAYLOAD_SIZE]; | |||
| } NRF24_packet_t; | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| class hoymiles { | |||
|     public: | |||
|         hoymiles() { | |||
|             serial2RadioId(); | |||
|             calcDtuIdCrc(); | |||
| 
 | |||
|             mChannels[0] = 23; | |||
|             mChannels[1] = 40; | |||
|             mChannels[2] = 61; | |||
|             mChannels[3] = 75; | |||
|             mChanIdx = 1; | |||
| 
 | |||
|             mLastCrc = 0x0000; | |||
|             mRptCnt  = 0; | |||
|         } | |||
| 
 | |||
|         ~hoymiles() {} | |||
| 
 | |||
|         uint8_t getDefaultChannel(void) { | |||
|             return mChannels[2]; | |||
|         } | |||
|         uint8_t getLastChannel(void) { | |||
|             return mChannels[mChanIdx]; | |||
|         } | |||
| 
 | |||
|         uint8_t getNxtChannel(void) { | |||
|             if(++mChanIdx >= 4) | |||
|                 mChanIdx = 0; | |||
|             return mChannels[mChanIdx]; | |||
|         } | |||
| 
 | |||
|         void serial2RadioId(void) { | |||
|             uint64Bytes id; | |||
| 
 | |||
|             id.ull = 0ULL; | |||
|             id.bytes[4] = mAddrBytes[5]; | |||
|             id.bytes[3] = mAddrBytes[4]; | |||
|             id.bytes[2] = mAddrBytes[3]; | |||
|             id.bytes[1] = mAddrBytes[2]; | |||
|             id.bytes[0] = 0x01; | |||
| 
 | |||
|             mRadioId = id.ull; | |||
|         } | |||
| 
 | |||
|         uint8_t getTimePacket(uint8_t buf[], uint32_t ts) { | |||
|             getCmdPacket(buf, 0x15, 0x80, false); | |||
|             buf[10] = 0x0b; // cid
 | |||
|             buf[11] = 0x00; | |||
|             CP_U32_LittleEndian(&buf[12], ts); | |||
|             buf[19] = 0x05; | |||
| 
 | |||
|             uint16_t crc = crc16(&buf[10], 14); | |||
|             buf[24] = (crc >> 8) & 0xff; | |||
|             buf[25] = (crc     ) & 0xff; | |||
|             buf[26] = crc8(buf, 26); | |||
| 
 | |||
|             return 27; | |||
|         } | |||
| 
 | |||
|         uint8_t getCmdPacket(uint8_t buf[], uint8_t mid, uint8_t cmd, bool calcCrc = true) { | |||
|             memset(buf, 0, MAX_RF_PAYLOAD_SIZE); | |||
|             buf[0] = mid; // message id
 | |||
|             CP_U32_BigEndian(&buf[1], (mRadioId     >> 8)); | |||
|             CP_U32_BigEndian(&buf[5], (DTU_RADIO_ID >> 8)); | |||
|             buf[9]  = cmd; | |||
|             if(calcCrc) | |||
|                 buf[10] = crc8(buf, 10); | |||
| 
 | |||
|             return 11; | |||
|         } | |||
| 
 | |||
|         bool checkCrc(uint8_t buf[], uint8_t *len, uint8_t *rptCnt) { | |||
|             *len = (buf[0] >> 2); | |||
|             for (int16_t i = MAX_RF_PAYLOAD_SIZE - 1; i >= 0; i--) { | |||
|                 buf[i] = ((buf[i] >> 7) | ((i > 0) ? (buf[i-1] << 1) : 0x00)); | |||
|             } | |||
|             uint16_t crc = crc16nrf24(buf, BIT_CNT(*len + 2), 7, mDtuIdCrc); | |||
| 
 | |||
|             bool valid = (crc == ((buf[*len+2] << 8) | (buf[*len+3]))); | |||
| 
 | |||
|             if(valid) { | |||
|                 if(mLastCrc == crc) | |||
|                     *rptCnt = (++mRptCnt); | |||
|                 else { | |||
|                     mRptCnt = 0; | |||
|                     *rptCnt = 0; | |||
|                     mLastCrc = crc; | |||
|                 } | |||
|             } | |||
| 
 | |||
|             return valid; | |||
|         } | |||
| 
 | |||
|         void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { | |||
|             Serial.print(String(info)); | |||
|             for(uint8_t i = 0; i < len; i++) { | |||
|                 Serial.print(buf[i], HEX); | |||
|                 Serial.print(" "); | |||
|             } | |||
|             Serial.println(); | |||
|         } | |||
| 
 | |||
|         uint8_t mAddrBytes[6]; | |||
|         luint64_t mRadioId; | |||
| 
 | |||
|     private: | |||
|         void calcDtuIdCrc(void) { | |||
|             uint64_t addr = DTU_RADIO_ID; | |||
|             uint8_t dtuAddr[5]; | |||
|             for(int8_t i = 4; i >= 0; i--) { | |||
|                 dtuAddr[i] = addr; | |||
|                 addr >>= 8; | |||
|             } | |||
|             mDtuIdCrc = crc16nrf24(dtuAddr, BIT_CNT(5)); | |||
|         } | |||
| 
 | |||
| 
 | |||
|         uint8_t mChannels[4]; | |||
|         uint8_t mChanIdx; | |||
|         uint16_t mDtuIdCrc; | |||
|         uint16_t mLastCrc; | |||
|         uint8_t mRptCnt; | |||
| }; | |||
| 
 | |||
| #endif /*__HOYMILES_H__*/ | |||
| @ -1,4 +0,0 @@ | |||
| ..\tools\fileConv.exe index.html h\index_html.h index_html | |||
| ..\tools\fileConv.exe setup.html h\setup_html.h setup_html | |||
| ..\tools\fileConv.exe style.css h\style_css.h style_css | |||
| pause | |||
| @ -0,0 +1,29 @@ | |||
| import re | |||
| 
 | |||
| def convert2Header(inFile): | |||
|     outName  = "h/" + inFile.replace(".", "_") + ".h" | |||
|     fileType = inFile.split(".")[1] | |||
| 
 | |||
|     f = open(inFile, "r") | |||
|     data = f.read().replace('\n', '') | |||
|     f.close() | |||
|     if fileType == "html": | |||
|         data = re.sub(r"\>\s+\<", '><', data)           # whitespaces between xml tags | |||
|         data = re.sub(r"(\;|\}|\>|\{)\s+", r'\1', data) # whitespaces inner javascript | |||
|         data = re.sub(r"\"", '\\\"', data)              # escape quotation marks | |||
|     else: | |||
|         data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css | |||
| 
 | |||
|     define = inFile.split(".")[0].upper() | |||
|     define2 = inFile.split(".")[1].upper() | |||
|     f = open(outName, "w") | |||
|     f.write("#ifndef __{}_{}_H__\n".format(define, define2)) | |||
|     f.write("#define __{}_{}_H__\n".format(define, define2)) | |||
|     f.write("const char {}[] PROGMEM = \"{}\";\n".format(inFile.replace(".", "_"), data)) | |||
|     f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) | |||
|     f.close() | |||
| 
 | |||
| convert2Header("index.html") | |||
| convert2Header("setup.html") | |||
| convert2Header("hoymiles.html") | |||
| convert2Header("style.css") | |||
| @ -0,0 +1,4 @@ | |||
| #ifndef __HOYMILES_HTML_H__ | |||
| #define __HOYMILES_HTML_H__ | |||
| const char hoymiles_html[] PROGMEM = "<!doctype html><html><head><title>Index - {DEVICE}</title><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><script type=\"text/javascript\">getAjax('/livedata', 'livedata');window.setInterval(\"getAjax('/livedata', 'livedata')\", 10000);function getAjax(url, resid) {var http = null;http = new XMLHttpRequest();if(http != null) {http.open(\"GET\", url, true);http.onreadystatechange = print;http.send(null);}function print() {if(http.readyState == 4) {document.getElementById(resid).innerHTML = http.responseText;}}}</script><style type=\"text/css\"></style></head><body><h1>AHOY - {DEVICE}</h1><div id=\"content\" class=\"content\"><div id=\"livedata\"></div><p>Every 10 seconds the values are updated</p></div><div id=\"footer\"><p class=\"left\">© 2022</p><p class=\"left\"><a href=\"/\">Home</a></p><p class=\"right\">AHOY :: {VERSION}</p></div></body></html>"; | |||
| #endif /*__HOYMILES_HTML_H__*/ | |||
| @ -1 +1,4 @@ | |||
| String index_html = "<!doctype html><html><head><title>Index - {DEVICE}</title><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><script type=\"text/javascript\">            window.setInterval(\"getAjax('/uptime', 'uptime')\", 1000);            window.setInterval(\"getAjax('/time', 'time')\", 1000);            window.setInterval(\"getAjax('/cmdstat', 'cmds')\", 2000);            function getAjax(url, resid) {                var http = null;                http = new XMLHttpRequest();                if(http != null) {                   http.open(\"GET\", url, true);                   http.onreadystatechange = print;                   http.send(null);                }                function print() {                    if(http.readyState == 4) {                        document.getElementById(resid).innerHTML = http.responseText;                    }                }            }        </script></head><body><h1>AHOY - {DEVICE}</h1><div id=\"content\" class=\"content\"><p><a href=\"/update\">Update</a><br/><br/><a href=\"/setup\">Setup</a><br/><a href=\"/reboot\">Reboot</a></p><p><span class=\"des\">Uptime: </span><span id=\"uptime\"></span></p><p><span class=\"des\">Time: </span><span id=\"time\"></span></p><p><span class=\"des\">Statistics: </span><pre id=\"cmds\"></pre></p></div><div id=\"footer\"><p class=\"left\">© 2022</p><p class=\"right\">AHOY :: {VERSION}</p></div></body></html>"; | |||
| #ifndef __INDEX_HTML_H__ | |||
| #define __INDEX_HTML_H__ | |||
| const char index_html[] PROGMEM = "<!doctype html><html><head><title>Index - {DEVICE}</title><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><script type=\"text/javascript\">window.setInterval(\"getAjax('/uptime', 'uptime')\", 1000);window.setInterval(\"getAjax('/time', 'time')\", 1000);window.setInterval(\"getAjax('/cmdstat', 'cmds')\", 2000);function getAjax(url, resid) {var http = null;http = new XMLHttpRequest();if(http != null) {http.open(\"GET\", url, true);http.onreadystatechange = print;http.send(null);}function print() {if(http.readyState == 4) {document.getElementById(resid).innerHTML = http.responseText;}}}</script></head><body><h1>AHOY - {DEVICE}</h1><div id=\"content\" class=\"content\"><p><a href=\"/hoymiles\">Visualization</a><br/><br/><a href=\"/setup\">Setup</a><br/></p><p><span class=\"des\">Uptime: </span><span id=\"uptime\"></span></p><p><span class=\"des\">Time: </span><span id=\"time\"></span></p><p><span class=\"des\">Statistics: </span><pre id=\"cmds\"></pre></p><div id=\"note\">This project was started from <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">this discussion. (Mikrocontroller.net)</a><br/>New updates can be found on Github: <a href=\"https://github.com/grindylow/ahoy\" target=\"_blank\">https://github.com/grindylow/ahoy</a><br/><br/>Please report issues using the feature provided by Github.            </div></div><div id=\"footer\"><p class=\"left\">© 2022</p><p class=\"left\"><a href=\"/update\">Update Firmware</a></p><p class=\"right\">AHOY :: {VERSION}</p><p class=\"right\"><a href=\"/reboot\">Reboot</a></p></div></body></html>"; | |||
| #endif /*__INDEX_HTML_H__*/ | |||
|  | |||
| @ -1 +1,4 @@ | |||
| String setup_html = "<!doctype html><html><head><title>Setup - {DEVICE}</title><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"></head><body><h1>Setup</h1><div id=\"setup\" class=\"content\"><div id=\"content\"><p>                    Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.                </p><form method=\"post\" action=\"/save\"><p class=\"des\">WiFi</p><div class=\"inputWrp\"><input type=\"text\" class=\"inputText\" name=\"ssid\" value=\"{SSID}\" required/><span class=\"floating_label\">SSID</span></div><div class=\"inputWrp\"><input type=\"password\" class=\"inputText\" name=\"pwd\" value=\"{PWD}\" required/><span class=\"floating_label\">PASSWORD</span></div><p class=\"des\">Device Host Name</p><div class=\"inputWrp\"><input type=\"text\" class=\"inputText\" name=\"device\" value=\"{DEVICE}\" required/><span class=\"floating_label\">DEVICE NAME</span></div><p class=\"des\">General</p><div class=\"inputWrp\"><input type=\"text\" class=\"inputText\" name=\"hoy_addr\" value=\"{HOY_ADDR}\" required/><span class=\"floating_label\">HOYMILES ADDRESS (eg. 11:22:33:44:55:66)</span></div><input type=\"checkbox\" class=\"cb\" name=\"reboot\"/><label for=\"reboot\">Reboot device after successful save</label><input type=\"submit\" value=\"save\" class=\"button\" /></form></div></div><div id=\"footer\"><p class=\"left\"><a href=\"/\">Home</a></p><p class=\"left\"><a href=\"/update\">Update Firmware</a></p><p class=\"right\">AHOY - {VERSION}</p></div></body></html>"; | |||
| #ifndef __SETUP_HTML_H__ | |||
| #define __SETUP_HTML_H__ | |||
| const char setup_html[] PROGMEM = "<!doctype html><html><head><title>Setup - {DEVICE}</title><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"></head><body><h1>Setup</h1><div id=\"setup\" class=\"content\"><div id=\"content\"><p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.                </p><form method=\"post\" action=\"{IP}/save\"><p class=\"des\">WiFi</p><label for=\"ssid\">SSID</label><input type=\"text\" class=\"text\" name=\"ssid\" value=\"{SSID}\"/><label for=\"pwd\">Password</label><input type=\"password\" class=\"text\" name=\"pwd\" value=\"{PWD}\"/><p class=\"des\">Device Host Name</p><label for=\"device\">Device Name</label><input type=\"text\" class=\"text\" name=\"device\" value=\"{DEVICE}\"/><a class=\"erase\" href=\"/erase\">ERASE SETTINGS (not WiFi)</a><p class=\"des\">Inverter</p>{INVERTERS}<br/><p class=\"subdes\">General</p><label for=\"invInterval\">Interval (s)</label><input type=\"text\" class=\"text\" name=\"invInterval\" value=\"{INV_INTVL}\"/><p class=\"des\">Pinout (Wemos)</p>{PINOUT}<p class=\"des\">Radio (NRF24L01+)</p><label for=\"rf24Power\">Amplifier Power Level</label><select name=\"rf24Power\">{RF24}</select><p class=\"des\">MQTT</p><label for=\"mqttAddr\">Broker / Server IP</label><input type=\"text\" class=\"text\" name=\"mqttAddr\" value=\"{MQTT_ADDR}\"/><label for=\"mqttPort\">Port</label><input type=\"text\" class=\"text\" name=\"mqttPort\" value=\"{MQTT_PORT}\"/><label for=\"mqttUser\">Username (optional)</label><input type=\"text\" class=\"text\" name=\"mqttUser\" value=\"{MQTT_USER}\"/><label for=\"mqttPwd\">Password (optional)</label><input type=\"text\" class=\"text\" name=\"mqttPwd\" value=\"{MQTT_PWD}\"/><label for=\"mqttTopic\">Topic</label><input type=\"text\" class=\"text\" name=\"mqttTopic\" value=\"{MQTT_TOPIC}\"/><label for=\"mqttIntvl\">Interval (s)</label><input type=\"text\" class=\"text\" name=\"mqttIntvl\" value=\"{MQTT_INTVL}\"/><p class=\"des\">Serial Console</p><label for=\"serEn\">print inverter data</label><input type=\"checkbox\" class=\"cb\" name=\"serEn\" {SER_VAL_CB}/><br/><label for=\"serDbg\">print RF24 debug</label><input type=\"checkbox\" class=\"cb\" name=\"serDbg\" {SER_DBG_CB}/><br/><label for=\"serIntvl\">Interval (s)</label><input type=\"text\" class=\"text\" name=\"serIntvl\" value=\"{SER_INTVL}\"/><p class=\"des\"> </p><label for=\"reboot\">Reboot device after successful save</label><input type=\"checkbox\" class=\"cb\" name=\"reboot\"/><input type=\"submit\" value=\"save\" class=\"btn\" /></form></div></div><div id=\"footer\"><p class=\"left\"><a href=\"{IP}/\">Home</a></p><p class=\"left\"><a href=\"{IP}/update\">Update Firmware</a></p><p class=\"right\">AHOY - {VERSION}</p><p class=\"right\"><a href=\"{IP}/factory\">Factory Reset</a></p><p class=\"right\"><a href=\"{IP}/reboot\">Reboot</a></p></div></body></html>"; | |||
| #endif /*__SETUP_HTML_H__*/ | |||
|  | |||
| @ -1 +1,4 @@ | |||
| String style_css = "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 { font-size: 14pt; color: #006ec0; padding-bottom: 0px !important; } .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; } #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; } #footer a:hover { color: #f00; } div.content { background-color: #fff; padding-bottom: 65px; overflow: hidden; } span.warn { display: inline-block; padding-left: 20px; color: #ff9900; font-style: italic; } input { padding: 10px; font-size: 13pt; } input.button { background-color: #006ec0; color: #fff; border: 0px; float: right; text-transform: uppercase; } input.cb { margin-bottom: 20px; } label { font-size: 14pt; } .left { float: left; } .right { float: right; } .inputWrp { position: relative; } .inputWrp .inputText { height: 35px; width: 90%; margin-bottom: 20px; border: 1px solid #ccc; border-top: none; border-right: none; } .inputWrp .floating_label { position: absolute; pointer-events: none; top: 20px; left: 10px; transition: 0.2s ease all; } .inputWrp input:focus ~ .floating_label, .inputWrp input:not(:focus):valid ~ .floating_label { top: 0px; left: 20px; font-size: 10px; color: blue; opacity: 1; } "; | |||
| #ifndef __STYLE_CSS_H__ | |||
| #define __STYLE_CSS_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:13pt;color:#006ec0;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}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%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}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-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:20px;padding-bottom:20px;overflow:auto;}div.ch {width:250px;min-height:420px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv  .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:250px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:30px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:1060px;}div.ch:last-child {margin-right:0px !important;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}"; | |||
| #endif /*__STYLE_CSS_H__*/ | |||
|  | |||
| @ -0,0 +1,42 @@ | |||
| <!doctype html> | |||
| <html> | |||
|     <head> | |||
|         <title>Index - {DEVICE}</title> | |||
|         <link rel="stylesheet" type="text/css" href="style.css"/> | |||
|         <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
|         <script type="text/javascript"> | |||
|             getAjax('/livedata', 'livedata'); | |||
|             window.setInterval("getAjax('/livedata', 'livedata')", 10000); | |||
| 
 | |||
|             function getAjax(url, resid) { | |||
|                 var http = null; | |||
|                 http = new XMLHttpRequest(); | |||
|                 if(http != null) { | |||
|                    http.open("GET", url, true); | |||
|                    http.onreadystatechange = print; | |||
|                    http.send(null); | |||
|                 } | |||
| 
 | |||
|                 function print() { | |||
|                     if(http.readyState == 4) { | |||
|                         document.getElementById(resid).innerHTML = http.responseText; | |||
|                     } | |||
|                 } | |||
|             } | |||
|         </script> | |||
|         <style type="text/css"> | |||
|         </style> | |||
|     </head> | |||
|     <body> | |||
|         <h1>AHOY - {DEVICE}</h1> | |||
|         <div id="content" class="content"> | |||
|             <div id="livedata"></div> | |||
|             <p>Every 10 seconds the values are updated</p> | |||
|         </div> | |||
|         <div id="footer"> | |||
|             <p class="left">© 2022</p> | |||
|             <p class="left"><a href="/">Home</a></p> | |||
|             <p class="right">AHOY :: {VERSION}</p> | |||
|         </div> | |||
|     </body> | |||
| </html> | |||
| @ -0,0 +1,87 @@ | |||
| #ifndef __MQTT_H__ | |||
| #define __MQTT_H__ | |||
| 
 | |||
| #include <ESP8266WiFi.h> | |||
| #include <PubSubClient.h> | |||
| #include "defines.h" | |||
| 
 | |||
| class mqtt { | |||
|     public: | |||
|         mqtt() { | |||
|             mClient     = new PubSubClient(mEspClient); | |||
|             mAddressSet = false; | |||
| 
 | |||
|             memset(mUser,  0, MQTT_USER_LEN); | |||
|             memset(mPwd,   0, MQTT_PWD_LEN); | |||
|             memset(mTopic, 0, MQTT_TOPIC_LEN); | |||
|         } | |||
| 
 | |||
|         ~mqtt() { | |||
|             delete mClient; | |||
|         } | |||
| 
 | |||
|         void setup(const char *broker, const char *topic, const char *user, const char *pwd, uint16_t port) { | |||
|             mAddressSet = true; | |||
|             mClient->setServer(broker, port); | |||
| 
 | |||
|             snprintf(mUser, MQTT_USER_LEN, "%s", user); | |||
|             snprintf(mPwd, MQTT_PWD_LEN, "%s", pwd); | |||
|             snprintf(mTopic, MQTT_TOPIC_LEN, "%s", topic); | |||
|         } | |||
| 
 | |||
|         void sendMsg(const char *topic, const char *msg) { | |||
|             if(mAddressSet) { | |||
|                 char top[64]; | |||
|                 snprintf(top, 64, "%s/%s", mTopic, topic); | |||
| 
 | |||
|                 if(!mClient->connected()) | |||
|                     reconnect(); | |||
|                 mClient->publish(top, msg); | |||
|             } | |||
|         } | |||
| 
 | |||
|         bool isConnected(bool doRecon = false) { | |||
|             if(doRecon) | |||
|                 reconnect(); | |||
|             return mClient->connected(); | |||
|         } | |||
| 
 | |||
|         char *getUser(void) { | |||
|             return mUser; | |||
|         } | |||
| 
 | |||
|         char *getPwd(void) { | |||
|             return mPwd; | |||
|         } | |||
| 
 | |||
|         char *getTopic(void) { | |||
|             return mTopic; | |||
|         } | |||
| 
 | |||
|         void loop() { | |||
|             //if(!mClient->connected())
 | |||
|             //    reconnect();
 | |||
|             mClient->loop(); | |||
|         } | |||
| 
 | |||
|     private: | |||
|         void reconnect(void) { | |||
|             if(!mClient->connected()) { | |||
|                 String mqttId = "ESP-" + String(random(0xffff), HEX); | |||
|                 if((strlen(mUser) > 0) && (strlen(mPwd) > 0)) | |||
|                     mClient->connect(mqttId.c_str(), mUser, mPwd); | |||
|                 else | |||
|                     mClient->connect(mqttId.c_str()); | |||
|             } | |||
|         } | |||
| 
 | |||
|         WiFiClient mEspClient; | |||
|         PubSubClient *mClient; | |||
| 
 | |||
|         bool mAddressSet; | |||
|         char mUser[MQTT_USER_LEN]; | |||
|         char mPwd[MQTT_PWD_LEN]; | |||
|         char mTopic[MQTT_TOPIC_LEN]; | |||
| }; | |||
| 
 | |||
| #endif /*__MQTT_H_*/ | |||
| @ -0,0 +1,31 @@ | |||
|  | |||
| Microsoft Visual Studio Solution File, Format Version 12.00 | |||
| # Visual Studio Version 16 | |||
| VisualStudioVersion = 16.0.32002.261 | |||
| MinimumVisualStudioVersion = 10.0.40219.1 | |||
| Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hmClassTest", "hmClassTest\hmClassTest.vcxproj", "{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}" | |||
| EndProject | |||
| Global | |||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
| 		Debug|x64 = Debug|x64 | |||
| 		Debug|x86 = Debug|x86 | |||
| 		Release|x64 = Release|x64 | |||
| 		Release|x86 = Release|x86 | |||
| 	EndGlobalSection | |||
| 	GlobalSection(ProjectConfigurationPlatforms) = postSolution | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Debug|x64.ActiveCfg = Debug|x64 | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Debug|x64.Build.0 = Debug|x64 | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Debug|x86.ActiveCfg = Debug|Win32 | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Debug|x86.Build.0 = Debug|Win32 | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Release|x64.ActiveCfg = Release|x64 | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Release|x64.Build.0 = Release|x64 | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Release|x86.ActiveCfg = Release|Win32 | |||
| 		{4D899C12-DE0E-4CDB-B48C-FDFEC331F219}.Release|x86.Build.0 = Release|Win32 | |||
| 	EndGlobalSection | |||
| 	GlobalSection(SolutionProperties) = preSolution | |||
| 		HideSolutionNode = FALSE | |||
| 	EndGlobalSection | |||
| 	GlobalSection(ExtensibilityGlobals) = postSolution | |||
| 		SolutionGuid = {7C291F74-09F6-4C84-99E1-6E7294062385} | |||
| 	EndGlobalSection | |||
| EndGlobal | |||
| @ -0,0 +1,147 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
|   <ItemGroup Label="ProjectConfigurations"> | |||
|     <ProjectConfiguration Include="Debug|Win32"> | |||
|       <Configuration>Debug</Configuration> | |||
|       <Platform>Win32</Platform> | |||
|     </ProjectConfiguration> | |||
|     <ProjectConfiguration Include="Release|Win32"> | |||
|       <Configuration>Release</Configuration> | |||
|       <Platform>Win32</Platform> | |||
|     </ProjectConfiguration> | |||
|     <ProjectConfiguration Include="Debug|x64"> | |||
|       <Configuration>Debug</Configuration> | |||
|       <Platform>x64</Platform> | |||
|     </ProjectConfiguration> | |||
|     <ProjectConfiguration Include="Release|x64"> | |||
|       <Configuration>Release</Configuration> | |||
|       <Platform>x64</Platform> | |||
|     </ProjectConfiguration> | |||
|   </ItemGroup> | |||
|   <ItemGroup> | |||
|     <ClCompile Include="..\src\main.cpp" /> | |||
|   </ItemGroup> | |||
|   <PropertyGroup Label="Globals"> | |||
|     <VCProjectVersion>16.0</VCProjectVersion> | |||
|     <Keyword>Win32Proj</Keyword> | |||
|     <ProjectGuid>{4d899c12-de0e-4cdb-b48c-fdfec331f219}</ProjectGuid> | |||
|     <RootNamespace>hmClassTest</RootNamespace> | |||
|     <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion> | |||
|   </PropertyGroup> | |||
|   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> | |||
|     <ConfigurationType>Application</ConfigurationType> | |||
|     <UseDebugLibraries>true</UseDebugLibraries> | |||
|     <PlatformToolset>v142</PlatformToolset> | |||
|     <CharacterSet>Unicode</CharacterSet> | |||
|   </PropertyGroup> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration"> | |||
|     <ConfigurationType>Application</ConfigurationType> | |||
|     <UseDebugLibraries>false</UseDebugLibraries> | |||
|     <PlatformToolset>v142</PlatformToolset> | |||
|     <WholeProgramOptimization>true</WholeProgramOptimization> | |||
|     <CharacterSet>Unicode</CharacterSet> | |||
|   </PropertyGroup> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> | |||
|     <ConfigurationType>Application</ConfigurationType> | |||
|     <UseDebugLibraries>true</UseDebugLibraries> | |||
|     <PlatformToolset>v142</PlatformToolset> | |||
|     <CharacterSet>Unicode</CharacterSet> | |||
|   </PropertyGroup> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> | |||
|     <ConfigurationType>Application</ConfigurationType> | |||
|     <UseDebugLibraries>false</UseDebugLibraries> | |||
|     <PlatformToolset>v142</PlatformToolset> | |||
|     <WholeProgramOptimization>true</WholeProgramOptimization> | |||
|     <CharacterSet>Unicode</CharacterSet> | |||
|   </PropertyGroup> | |||
|   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> | |||
|   <ImportGroup Label="ExtensionSettings"> | |||
|   </ImportGroup> | |||
|   <ImportGroup Label="Shared"> | |||
|   </ImportGroup> | |||
|   <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> | |||
|     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> | |||
|   </ImportGroup> | |||
|   <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> | |||
|     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> | |||
|   </ImportGroup> | |||
|   <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> | |||
|     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> | |||
|   </ImportGroup> | |||
|   <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> | |||
|     <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> | |||
|   </ImportGroup> | |||
|   <PropertyGroup Label="UserMacros" /> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> | |||
|     <LinkIncremental>true</LinkIncremental> | |||
|   </PropertyGroup> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> | |||
|     <LinkIncremental>false</LinkIncremental> | |||
|   </PropertyGroup> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> | |||
|     <LinkIncremental>true</LinkIncremental> | |||
|   </PropertyGroup> | |||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> | |||
|     <LinkIncremental>false</LinkIncremental> | |||
|   </PropertyGroup> | |||
|   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> | |||
|     <ClCompile> | |||
|       <WarningLevel>Level3</WarningLevel> | |||
|       <SDLCheck>true</SDLCheck> | |||
|       <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> | |||
|       <ConformanceMode>true</ConformanceMode> | |||
|     </ClCompile> | |||
|     <Link> | |||
|       <SubSystem>Console</SubSystem> | |||
|       <GenerateDebugInformation>true</GenerateDebugInformation> | |||
|     </Link> | |||
|   </ItemDefinitionGroup> | |||
|   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> | |||
|     <ClCompile> | |||
|       <WarningLevel>Level3</WarningLevel> | |||
|       <FunctionLevelLinking>true</FunctionLevelLinking> | |||
|       <IntrinsicFunctions>true</IntrinsicFunctions> | |||
|       <SDLCheck>true</SDLCheck> | |||
|       <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> | |||
|       <ConformanceMode>true</ConformanceMode> | |||
|     </ClCompile> | |||
|     <Link> | |||
|       <SubSystem>Console</SubSystem> | |||
|       <EnableCOMDATFolding>true</EnableCOMDATFolding> | |||
|       <OptimizeReferences>true</OptimizeReferences> | |||
|       <GenerateDebugInformation>true</GenerateDebugInformation> | |||
|     </Link> | |||
|   </ItemDefinitionGroup> | |||
|   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> | |||
|     <ClCompile> | |||
|       <WarningLevel>Level3</WarningLevel> | |||
|       <SDLCheck>true</SDLCheck> | |||
|       <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> | |||
|       <ConformanceMode>true</ConformanceMode> | |||
|     </ClCompile> | |||
|     <Link> | |||
|       <SubSystem>Console</SubSystem> | |||
|       <GenerateDebugInformation>true</GenerateDebugInformation> | |||
|     </Link> | |||
|   </ItemDefinitionGroup> | |||
|   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> | |||
|     <ClCompile> | |||
|       <WarningLevel>Level3</WarningLevel> | |||
|       <FunctionLevelLinking>true</FunctionLevelLinking> | |||
|       <IntrinsicFunctions>true</IntrinsicFunctions> | |||
|       <SDLCheck>true</SDLCheck> | |||
|       <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> | |||
|       <ConformanceMode>true</ConformanceMode> | |||
|     </ClCompile> | |||
|     <Link> | |||
|       <SubSystem>Console</SubSystem> | |||
|       <EnableCOMDATFolding>true</EnableCOMDATFolding> | |||
|       <OptimizeReferences>true</OptimizeReferences> | |||
|       <GenerateDebugInformation>true</GenerateDebugInformation> | |||
|     </Link> | |||
|   </ItemDefinitionGroup> | |||
|   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> | |||
|   <ImportGroup Label="ExtensionTargets"> | |||
|   </ImportGroup> | |||
| </Project> | |||
| @ -0,0 +1,22 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
|   <ItemGroup> | |||
|     <Filter Include="Quelldateien"> | |||
|       <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier> | |||
|       <Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions> | |||
|     </Filter> | |||
|     <Filter Include="Headerdateien"> | |||
|       <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier> | |||
|       <Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions> | |||
|     </Filter> | |||
|     <Filter Include="Ressourcendateien"> | |||
|       <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier> | |||
|       <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions> | |||
|     </Filter> | |||
|   </ItemGroup> | |||
|   <ItemGroup> | |||
|     <ClCompile Include="..\src\main.cpp"> | |||
|       <Filter>Quelldateien</Filter> | |||
|     </ClCompile> | |||
|   </ItemGroup> | |||
| </Project> | |||
| @ -0,0 +1,4 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
|   <PropertyGroup /> | |||
| </Project> | |||
| @ -0,0 +1,105 @@ | |||
| #ifdef _MSC_VER | |||
| #define _CRT_SECURE_NO_WARNINGS | |||
| #endif | |||
| 
 | |||
| #include <cstdint> | |||
| #include <cstdio> | |||
| #include <cstdlib> | |||
| #include <cstring> | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| #define MAX_NUM_INVERTERS       3 | |||
| #define MAX_NAME_LENGTH         16 | |||
| #define NDEBUG | |||
| #define NO_RADIO | |||
| 
 | |||
| #include "../../../hmDefines.h" | |||
| #include "../../../hmInverter.h" | |||
| #include "../../../hmSystem.h" | |||
| 
 | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| typedef int RadioType; | |||
| typedef int BufferType; | |||
| typedef Inverter<float> InverterType; | |||
| typedef HmSystem<RadioType, BufferType, MAX_NUM_INVERTERS, InverterType> HmSystemType; | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void valToBuf(InverterType *iv, uint8_t fld, uint8_t ch, float val, uint8_t bufPos); | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| int main(int argc, char* argv[]) { | |||
|     HmSystemType sys; | |||
|     InverterType *iv0, *iv1; | |||
|     uint8_t buf[30] = { 0xcc }; | |||
| 
 | |||
|     iv0 = sys.addInverter("HM1200", 0x1122334455ULL, INV_TYPE_HM1200); | |||
|     iv1 = sys.addInverter("HM600",  0x1234567891ULL, INV_TYPE_HM600); | |||
| 
 | |||
|     valToBuf(iv0, FLD_UDC, CH1, 29.5,  3); | |||
|     valToBuf(iv0, FLD_UDC, CH3, 30.6,  9); | |||
|     valToBuf(iv0, FLD_YD,  CH1, 1234,  5); | |||
|     valToBuf(iv0, FLD_YD,  CH2, 1199,  7); | |||
|     valToBuf(iv0, FLD_YD,  CH3,  899, 11); | |||
|     valToBuf(iv0, FLD_YD,  CH4,  932, 13); | |||
|     valToBuf(iv0, FLD_YT,  CH1, 40.123, 13); | |||
|     valToBuf(iv0, FLD_YT,  CH2, 57.231,  1); | |||
|     valToBuf(iv0, FLD_YT,  CH3, 59.372,  3); | |||
|     valToBuf(iv0, FLD_YT,  CH4, 43.966,  7); | |||
| 
 | |||
|     iv0->doCalculations(); | |||
|     for(uint8_t i = 0; i < iv0->listLen; i ++) { | |||
|         float val = iv0->getValue(i); | |||
|         if(0.0 != val) { | |||
|             printf("%10s [CH%d] = %.3f %s\n", iv0->getFieldName(i), iv0->getChannel(i), val, iv0->getUnit(i)); | |||
|         } | |||
|     } | |||
| 
 | |||
|     return 0; | |||
| } | |||
| 
 | |||
| 
 | |||
| //-----------------------------------------------------------------------------
 | |||
| void valToBuf(InverterType *iv, uint8_t fld, uint8_t ch, float val, uint8_t bufPos) { | |||
|     uint8_t buf[30] = { 0xcc }; | |||
|     uint8_t len; | |||
|     uint16_t factor; | |||
| 
 | |||
|     switch(fld) { | |||
|         default:     len = 2; break; | |||
|         case FLD_YT: len = 4; break; | |||
|     } | |||
| 
 | |||
|     switch(fld) { | |||
|         case FLD_YD:  factor = 1;    break; | |||
|         case FLD_UDC: | |||
|         case FLD_PDC: | |||
|         case FLD_UAC: | |||
|         case FLD_PAC: | |||
|         case FLD_PCT: | |||
|         case FLD_T:   factor = 10;   break; | |||
|         case FLD_IDC: | |||
|         case FLD_IAC: | |||
|         case FLD_F:   factor = 100;  break; | |||
|         default:      factor = 1000; break; | |||
|     } | |||
| 
 | |||
|     uint8_t *p = &buf[bufPos]; | |||
| 
 | |||
|     uint32_t intval = (uint32_t)(val * factor); | |||
|     if(2 == len) { | |||
|         p[0] = (intval >> 8) & 0xff; | |||
|         p[1] = (intval     ) & 0xff; | |||
|     } | |||
|     else { | |||
|         p[0] = (intval >> 24) & 0xff; | |||
|         p[1] = (intval >> 16) & 0xff; | |||
|         p[2] = (intval >>  8) & 0xff; | |||
|         p[3] = (intval      ) & 0xff; | |||
|     } | |||
|     iv->addValue(iv->getPosByChFld(ch, fld), buf); | |||
| } | |||
								
									Binary file not shown.
								
							
						
					| @ -0,0 +1,11 @@ | |||
| # Python | |||
| # Byte-compiled / optimized / DLL files | |||
| __pycache__/ | |||
| *.py[cod] | |||
| *$py.class | |||
| 
 | |||
| # Virtual Environment | |||
| venv/ | |||
| 
 | |||
| # vim leftovers | |||
| **.swp | |||
| @ -1,11 +0,0 @@ | |||
| [mqtt] | |||
| host = 192.168.84.2 | |||
| port = 1883 | |||
| user = bla | |||
| password = blub | |||
| 
 | |||
| [dtu] | |||
| serial = 99978563412 | |||
| 
 | |||
| [inverter] | |||
| serial = 444473104619 | |||
| @ -1,345 +0,0 @@ | |||
| """ | |||
| First attempt at providing basic 'master' ('DTU') functionality | |||
| for Hoymiles micro inverters. | |||
| Based in particular on demostrated first contact by 'of22'. | |||
| """ | |||
| import sys | |||
| import argparse | |||
| import time | |||
| import struct | |||
| import crcmod | |||
| import json | |||
| from datetime import datetime | |||
| from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS | |||
| import paho.mqtt.client | |||
| from configparser import ConfigParser | |||
| 
 | |||
| cfg = ConfigParser() | |||
| cfg.read('ahoy.conf') | |||
| mqtt_host = cfg.get('mqtt', 'host', fallback='192.168.1.1') | |||
| mqtt_port = cfg.getint('mqtt', 'port', fallback=1883) | |||
| mqtt_user = cfg.get('mqtt', 'user', fallback='') | |||
| mqtt_password = cfg.get('mqtt', 'password', fallback='') | |||
| 
 | |||
| radio = RF24(22, 0, 1000000) | |||
| mqtt_client = paho.mqtt.client.Client() | |||
| mqtt_client.username_pw_set(mqtt_user, mqtt_password) | |||
| mqtt_client.connect(mqtt_host, mqtt_port) | |||
| mqtt_client.loop_start() | |||
| 
 | |||
| # Master Address ('DTU') | |||
| dtu_ser = cfg.get('dtu', 'serial', fallback='99978563412')  # identical to fc22's | |||
| 
 | |||
| # inverter serial numbers | |||
| inv_ser = cfg.get('inverter', 'serial', fallback='444473104619')  # my inverter | |||
| 
 | |||
| # all inverters | |||
| #... | |||
| 
 | |||
| f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') | |||
| f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) | |||
| 
 | |||
| 
 | |||
| def ser_to_hm_addr(s): | |||
|     """ | |||
|     Calculate the 4 bytes that the HM devices use in their internal messages to  | |||
|     address each other. | |||
|     """ | |||
|     bcd = int(str(s)[-8:], base=16) | |||
|     return struct.pack('>L', bcd) | |||
| 
 | |||
| 
 | |||
| def ser_to_esb_addr(s): | |||
|     """ | |||
|     Convert a Hoymiles inverter/DTU serial number into its | |||
|     corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes). | |||
| 
 | |||
|     The NRF library expects these in LSB to MSB order, even though the transceiver | |||
|     itself will then output them in MSB-to-LSB order over the air. | |||
|      | |||
|     The inverters use a BCD representation of the last 8 | |||
|     digits of their serial number, in reverse byte order,  | |||
|     followed by \x01. | |||
|     """ | |||
|     air_order = ser_to_hm_addr(s)[::-1] + b'\x01' | |||
|     return air_order[::-1] | |||
| 
 | |||
| 
 | |||
| def compose_0x80_msg(dst_ser_no=72220200, src_ser_no=72220200, ts=None): | |||
|     """ | |||
|     Create a valid 0x80 request with the given parameters, and containing the  | |||
|     current system time. | |||
|     """ | |||
| 
 | |||
|     if not ts: | |||
|         ts = 0x623C8ECF  # identical to fc22's for testing  # doc: 1644758171 | |||
| 
 | |||
|     # "framing" | |||
|     p = b'' | |||
|     p = p + b'\x15' | |||
|     p = p + ser_to_hm_addr(dst_ser_no) | |||
|     p = p + ser_to_hm_addr(src_ser_no) | |||
|     p = p + b'\x80' | |||
| 
 | |||
|     # encapsulated payload | |||
|     pp = b'\x0b\x00' | |||
|     pp = pp + struct.pack('>L', ts)  # big-endian: msb at low address | |||
|     #pp = pp + b'\x00' * 8    # of22 adds a \x05 at position 19 | |||
| 
 | |||
|     pp = pp + b'\x00\x00\x00\x05\x00\x00\x00\x00' | |||
| 
 | |||
|     # CRC_M | |||
|     crc_m = f_crc_m(pp) | |||
| 
 | |||
|     p = p + pp | |||
|     p = p + struct.pack('>H', crc_m) | |||
| 
 | |||
|     crc8 = f_crc8(p) | |||
|     p = p + struct.pack('B', crc8)    | |||
|     return p | |||
| 
 | |||
| 
 | |||
| def print_addr(a): | |||
|     print(f"ser# {a} ", end='') | |||
|     print(f" -> HM  {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='') | |||
|     print(f" -> ESB {' '.join([f'{x:02x}' for x in ser_to_esb_addr(a)])}") | |||
| 
 | |||
| # time of last transmission - to calculcate response time | |||
| t_last_tx = 0 | |||
| 
 | |||
| def on_receive(p, ch_rx=None, ch_tx=None): | |||
|     """ | |||
|     Callback: get's invoked whenever a packet has been received. | |||
|     :param p: Payload of the received packet. | |||
|     """ | |||
| 
 | |||
|     d = {} | |||
| 
 | |||
|     t_now_ns = time.monotonic_ns() | |||
|     ts = datetime.utcnow() | |||
|     ts_unixtime = ts.timestamp() | |||
|     d['ts_unixtime'] = ts_unixtime | |||
|     d['isodate'] = ts.isoformat() | |||
|     d['rawdata'] = " ".join([f"{b:02x}" for b in p]) | |||
|     print(ts.isoformat(), end='Z ') | |||
| 
 | |||
|     # check crc8 | |||
|     crc8 = f_crc8(p[:-1]) | |||
|     d['crc8_valid'] = True if crc8==p[-1] else False | |||
| 
 | |||
|     # interpret content | |||
|     mid = p[0] | |||
|     d['mid'] = mid | |||
|     d['response_time_ns'] = t_now_ns-t_last_tx | |||
|     d['ch_rx'] = ch_rx | |||
|     d['ch_tx'] = ch_tx | |||
|     d['src'] = 'src_unkn' | |||
|     d['name'] = 'name_unkn' | |||
|   | |||
|     if mid == 0x95: | |||
|         src, dst, cmd = struct.unpack('>LLB', p[1:10]) | |||
|         d['src'] = f'{src:08x}' | |||
|         d['dst'] = f'{dst:08x}' | |||
|         d['cmd'] = cmd | |||
|         print(f'MSG src={d["src"]}, dst={d["dst"]}, cmd={d["cmd"]}:') | |||
| 
 | |||
|         if cmd==1: | |||
|             d['name'] = 'dcdata' | |||
|             unknown1, u1, i1, p1, u2, i2, p2, unknown2 = struct.unpack( | |||
|                 '>HHHHHHHH', p[10:26]) | |||
|             d['u1_V'] = u1/10 | |||
|             d['i1_A'] = i1/100 | |||
|             d['p1_W'] = p1/10 | |||
|             d['u2_V'] = u2/10 | |||
|             d['i2_A'] = i2/100 | |||
|             d['p2_W'] = p2/10 | |||
|             d['p_W'] = d['p1_W']+d['p2_W'] | |||
|             d['unknown1'] = unknown1 | |||
|             d['unknown2'] = unknown2 | |||
| 
 | |||
|         elif cmd==2: | |||
|             d['name'] = 'acdata' | |||
|             uk1, uk2, uk3, uk4, uk5, u, f, p = struct.unpack( | |||
|                 '>HHHHHHHH', p[10:26]) | |||
|             d['u_V'] = u/10 | |||
|             d['f_Hz'] = f/100 | |||
|             d['p_W'] = p/10 | |||
|             d['wtot1_Wh'] = uk1 | |||
|             d['wtot2_Wh'] = uk3 | |||
|             d['wday1_Wh'] = uk4 | |||
|             d['wday2_Wh'] = uk5 | |||
|             d['uk2'] = uk2 | |||
| 
 | |||
|         elif cmd==129: | |||
|             d['name'] = 'error' | |||
| 
 | |||
|         elif cmd==131:  # 0x83 | |||
|             d['name'] = 'statedata' | |||
|             uk1, l, uk3, t, uk5, uk6 = struct.unpack('>HHHHHH', p[10:22]) | |||
|             d['l_Pct'] = l | |||
|             d['t_C'] = t/10 | |||
|             d['uk1'] = uk1 | |||
|             d['uk3'] = uk3 | |||
|             d['uk5'] = uk5 | |||
|             d['uk6'] = uk6 | |||
| 
 | |||
|         elif cmd==132:  # 0x84 | |||
|             d['name'] = 'unknown0x84' | |||
|             uk1, uk2, uk3, uk4, uk5, uk6, uk7, uk8 = struct.unpack( | |||
|                 '>HHHHHHHH', p[10:26]) | |||
|             d['uk1'] = uk1 | |||
|             d['uk2'] = uk2 | |||
|             d['uk3'] = uk3 | |||
|             d['uk4'] = uk4 | |||
|             d['uk5'] = uk5 | |||
|             d['uk6'] = uk6 | |||
|             d['uk7'] = uk7 | |||
|             d['uk8'] = uk8 | |||
| 
 | |||
|         else: | |||
|             print(f'unknown cmd {cmd}') | |||
|     else: | |||
|         print(f'unknown frame id {p[0]}') | |||
| 
 | |||
|     # output to stdout | |||
|     if d: | |||
|         print(json.dumps(d)) | |||
| 
 | |||
|     # output to MQTT | |||
|     if d: | |||
|         j = json.dumps(d) | |||
|         mqtt_client.publish(f"ahoy/{d['src']}/{d['name']}", j) | |||
|         if d['cmd']==2: | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter/0/voltage', d['u_V']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter/0/power', d['p_W']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter/0/total', d['wtot1_Wh']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/frequency', d['f_Hz']) | |||
|         if d['cmd']==1: | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter-dc/0/power', d['p1_W']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter-dc/0/voltage', d['u1_V']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter-dc/0/current', d['i1_A']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter-dc/1/power', d['p2_W']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter-dc/1/voltage', d['u2_V']) | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/emeter-dc/1/current', d['i2_A']) | |||
|         if d['cmd']==131: | |||
|             mqtt_client.publish(f'ahoy/{d["src"]}/temperature', d['t_C']) | |||
| 
 | |||
| 
 | |||
| 
 | |||
| def main_loop(): | |||
|     """ | |||
|     Keep receiving on channel 3. Every once in a while, transmit a request | |||
|     to one of our inverters on channel 40. | |||
|     """ | |||
| 
 | |||
|     global t_last_tx | |||
| 
 | |||
|     print_addr(inv_ser) | |||
|     print_addr(dtu_ser) | |||
| 
 | |||
|     ctr = 1 | |||
|     last_tx_message = '' | |||
| 
 | |||
|     ts = int(time.time())  # see what happens if we always send one and the same (constant) time! | |||
|      | |||
|     rx_channels = [3,23,61,75] | |||
|     rx_channel_id = 0 | |||
|     rx_channel = rx_channels[rx_channel_id] | |||
| 
 | |||
|     tx_channels = [40] | |||
|     tx_channel_id = 0 | |||
|     tx_channel = tx_channels[tx_channel_id] | |||
| 
 | |||
|     while True: | |||
|         # Sweep receive start channel | |||
|         rx_channel_id = ctr % len(rx_channels) | |||
|         rx_channel = rx_channels[rx_channel_id] | |||
| 
 | |||
|         radio.setChannel(rx_channel) | |||
|         radio.enableDynamicPayloads() | |||
|         radio.setAutoAck(False) | |||
|         radio.setPALevel(RF24_PA_MAX) | |||
|         radio.setDataRate(RF24_250KBPS) | |||
|         radio.openWritingPipe(ser_to_esb_addr(inv_ser)) | |||
|         radio.flush_rx() | |||
|         radio.flush_tx() | |||
|         radio.openReadingPipe(1,ser_to_esb_addr(dtu_ser)) | |||
|         radio.startListening() | |||
| 
 | |||
|         tx_channel_id = tx_channel_id + 1 | |||
|         if tx_channel_id >= len(tx_channels): | |||
|             tx_channel_id = 0 | |||
|         tx_channel = tx_channels[tx_channel_id] | |||
| 
 | |||
|         # | |||
|         # TX | |||
|         # | |||
|         radio.stopListening()  # put radio in TX mode | |||
|         radio.setChannel(tx_channel) | |||
|         radio.openWritingPipe(ser_to_esb_addr(inv_ser)) | |||
| 
 | |||
|         ts = int(time.time()) | |||
|         payload = compose_0x80_msg(src_ser_no=dtu_ser, dst_ser_no=inv_ser, ts=ts) | |||
|         dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") | |||
|         last_tx_message = f"{dt} Transmit {ctr:5d}: channel={tx_channel} len={len(payload)} | " + \ | |||
|             " ".join([f"{b:02x}" for b in payload]) + f" rx_ch: {rx_channel}" | |||
|         print(last_tx_message) | |||
| 
 | |||
|         # for i in range(0,3): | |||
|         result = radio.write(payload)  # will always yield 'True' because auto-ack is disabled | |||
|         #    time.sleep(.05) | |||
| 
 | |||
|         t_last_tx = time.monotonic_ns() | |||
|         ctr = ctr + 1 | |||
| 
 | |||
|         t_end = time.monotonic_ns()+5e9 | |||
|         tslots = [1000]  #, 40, 50, 60, 70]  # switch channel at these ms times since transmission | |||
| 
 | |||
|         for tslot in tslots: | |||
|             t_end = t_last_tx + tslot*1e6  # ms to ns | |||
| 
 | |||
|             radio.stopListening() | |||
|             radio.setChannel(rx_channel) | |||
|             radio.startListening() | |||
|             while time.monotonic_ns() < t_end: | |||
|                 has_payload, pipe_number = radio.available_pipe() | |||
|                 if has_payload: | |||
|                     size = radio.getDynamicPayloadSize() | |||
|                     payload = radio.read(size) | |||
|                     # print(last_tx_message, end='') | |||
|                     last_tx_message = '' | |||
|                     dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") | |||
|                     print(f"{dt} Received {size} bytes on channel {rx_channel} pipe {pipe_number}: " + | |||
|                           " ".join([f"{b:02x}" for b in payload])) | |||
|                     on_receive(payload, ch_rx=rx_channel, ch_tx=tx_channel) | |||
|                 else: | |||
|                     pass | |||
|                     # time.sleep(0.001) | |||
| 
 | |||
|             rx_channel_id = rx_channel_id + 1 | |||
|             if rx_channel_id >= len(rx_channels): | |||
|                 rx_channel_id = 0 | |||
|             rx_channel = rx_channels[rx_channel_id] | |||
| 
 | |||
|         print(flush=True, end='') | |||
|         # time.sleep(2) | |||
| 
 | |||
| 
 | |||
| 
 | |||
| if __name__ == "__main__": | |||
| 
 | |||
|     if not radio.begin(): | |||
|         raise RuntimeError("radio hardware is not responding") | |||
| 
 | |||
|     radio.setPALevel(RF24_PA_LOW)  # RF24_PA_MAX is default | |||
| 
 | |||
|     # radio.printDetails();  # (smaller) function that prints raw register values | |||
|     # radio.printPrettyDetails();  # (larger) function that prints human readable data | |||
| 
 | |||
|     try: | |||
|         main_loop() | |||
| 
 | |||
|     except KeyboardInterrupt: | |||
|         print(" Keyboard Interrupt detected. Exiting...") | |||
|         radio.powerDown() | |||
|         sys.exit() | |||
| @ -0,0 +1,27 @@ | |||
| --- | |||
| 
 | |||
| ahoy: | |||
|   interval: 0 | |||
|   sunset: true | |||
| 
 | |||
|   # List of available NRF24 transceivers | |||
|   nrf: | |||
|     - ce_pin: 22 | |||
|       cs_pin: 0 | |||
| 
 | |||
|   mqtt: | |||
|     disabled: false | |||
|     host: example-broker.local | |||
|     port: 1883 | |||
|     user: 'username' | |||
|     password: 'password' | |||
| 
 | |||
|   dtu: | |||
|     serial: 99978563001 | |||
| 
 | |||
|   inverters: | |||
|     - name: 'balkon' | |||
|       serial: 114172220003 | |||
|       mqtt: | |||
|         send_raw_enabled: false        # allow inject debug data via mqtt | |||
|         topic: 'hoymiles/114172221234' # defaults to 'hoymiles/{serial}' | |||
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -0,0 +1,661 @@ | |||
| import struct | |||
| import crcmod | |||
| import json | |||
| import time | |||
| import re | |||
| from datetime import datetime | |||
| from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 | |||
| from .decoders import * | |||
| 
 | |||
| f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') | |||
| f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) | |||
| 
 | |||
| 
 | |||
| HOYMILES_TRANSACTION_LOGGING=False | |||
| HOYMILES_DEBUG_LOGGING=False | |||
| 
 | |||
| def ser_to_hm_addr(s): | |||
|     """ | |||
|     Calculate the 4 bytes that the HM devices use in their internal messages to  | |||
|     address each other. | |||
| 
 | |||
|     :param str s: inverter serial | |||
|     :return: inverter address | |||
|     :rtype: bytes | |||
|     """ | |||
|     bcd = int(str(s)[-8:], base=16) | |||
|     return struct.pack('>L', bcd) | |||
| 
 | |||
| def ser_to_esb_addr(s): | |||
|     """ | |||
|     Convert a Hoymiles inverter/DTU serial number into its | |||
|     corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes). | |||
| 
 | |||
|     The NRF library expects these in LSB to MSB order, even though the transceiver | |||
|     itself will then output them in MSB-to-LSB order over the air. | |||
|      | |||
|     The inverters use a BCD representation of the last 8 | |||
|     digits of their serial number, in reverse byte order,  | |||
|     followed by \x01. | |||
| 
 | |||
|     :param str s: inverter serial | |||
|     :return: ESB inverter address | |||
|     :rtype: bytes | |||
|     """ | |||
|     air_order = ser_to_hm_addr(s)[::-1] + b'\x01' | |||
|     return air_order[::-1] | |||
| 
 | |||
| def print_addr(a): | |||
|     """ | |||
|     Debug print addresses | |||
| 
 | |||
|     :param str a: inverter serial | |||
|     """ | |||
|     print(f"ser# {a} ", end='') | |||
|     print(f" -> HM  {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='') | |||
|     print(f" -> ESB {' '.join([f'{x:02x}' for x in ser_to_esb_addr(a)])}") | |||
| 
 | |||
| # time of last transmission - to calculcate response time | |||
| t_last_tx = 0 | |||
| 
 | |||
| class ResponseDecoderFactory: | |||
|     """ | |||
|     Prepare payload decoder | |||
| 
 | |||
|     :param bytes response: ESB response frame to decode | |||
|     :param request: ESB request frame | |||
|     :type request: bytes | |||
|     :param inverter_ser: inverter serial | |||
|     :type inverter_ser: str | |||
|     """ | |||
|     model = None | |||
|     request = None | |||
|     response = None | |||
| 
 | |||
|     def __init__(self, response, **params): | |||
|         self.response = response | |||
| 
 | |||
|         if 'request' in params: | |||
|             self.request = params['request'] | |||
|         elif hasattr(response, 'request'): | |||
|             self.request = response.request | |||
| 
 | |||
|         if 'inverter_ser' in params: | |||
|             self.inverter_ser = params['inverter_ser'] | |||
|             self.model = self.inverter_model | |||
| 
 | |||
|     def unpack(self, fmt, base): | |||
|         """ | |||
|         Data unpack helper | |||
| 
 | |||
|         :param str fmt: struct format string | |||
|         :param int base: unpack base position from self.response bytes | |||
|         :return: unpacked values | |||
|         :rtype: tuple | |||
|         """ | |||
|         size = struct.calcsize(fmt) | |||
|         return struct.unpack(fmt, self.response[base:base+size]) | |||
| 
 | |||
|     @property | |||
|     def inverter_model(self): | |||
|         """ | |||
|         Find decoder for inverter model | |||
| 
 | |||
|         :return: suitable decoder model string | |||
|         :rtype: str | |||
|         :raises ValueError: on invalid inverter serial | |||
|         :raises NotImplementedError: if inverter model can not be determined | |||
|         """ | |||
|         if not self.inverter_ser: | |||
|             raise ValueError('Inverter serial while decoding response') | |||
| 
 | |||
|         ser_db = [ | |||
|                 ('HM300', r'^1121........'), | |||
|                 ('HM600', r'^1141........'), | |||
|                 ('HM1200', r'^1161........'), | |||
|                 ] | |||
|         ser_str = str(self.inverter_ser) | |||
| 
 | |||
|         model = None | |||
|         for m, r in ser_db: | |||
|             if re.match(r, ser_str): | |||
|                 model = m | |||
|                 break | |||
| 
 | |||
|         if len(model): | |||
|             return model | |||
|         raise NotImplementedError('Model lookup failed for serial {ser_str}') | |||
| 
 | |||
|     @property | |||
|     def request_command(self): | |||
|         """ | |||
|         Return requested command identifier byte | |||
| 
 | |||
|         :return: hexlified command byte string | |||
|         :rtype: str | |||
|         """ | |||
|         r_code = self.request[10] | |||
|         return f'{r_code:02x}' | |||
| 
 | |||
| class ResponseDecoder(ResponseDecoderFactory): | |||
|     """ | |||
|     Base response | |||
| 
 | |||
|     :param bytes response: ESB frame response | |||
|     """ | |||
|     def __init__(self, response, **params): | |||
|         """Initialize ResponseDecoder""" | |||
|         ResponseDecoderFactory.__init__(self, response, **params) | |||
| 
 | |||
|     def decode(self): | |||
|         """ | |||
|         Decode Payload | |||
| 
 | |||
|         :return: payload decoder instance | |||
|         :rtype: object | |||
|         """ | |||
|         model = self.inverter_model | |||
|         command = self.request_command | |||
| 
 | |||
|         model_decoders = __import__(f'hoymiles.decoders') | |||
|         if hasattr(model_decoders, f'{model}_Decode{command.upper()}'): | |||
|             device = getattr(model_decoders, f'{model}_Decode{command.upper()}') | |||
|         else: | |||
|             if HOYMILES_DEBUG_LOGGING: | |||
|                 device = getattr(model_decoders, f'DEBUG_DecodeAny') | |||
| 
 | |||
|         return device(self.response) | |||
| 
 | |||
| class InverterPacketFragment: | |||
|     """ESB Frame""" | |||
|     def __init__(self, time_rx=None, payload=None, ch_rx=None, ch_tx=None, **params): | |||
|         """ | |||
|         Callback: get's invoked whenever a Nordic ESB packet has been received. | |||
| 
 | |||
|         :param time_rx: datetime when frame was received | |||
|         :type time_rx: datetime | |||
|         :param payload: payload bytes | |||
|         :type payload: bytes | |||
|         :param ch_rx: channel where packet was received | |||
|         :type ch_rx: int | |||
|         :param ch_tx: channel where request was sent | |||
|         :type ch_tx: int | |||
|         """ | |||
| 
 | |||
|         if not time_rx: | |||
|             time_rx = datetime.now() | |||
|         self.time_rx = time_rx | |||
| 
 | |||
|         self.frame = payload | |||
| 
 | |||
|         # check crc8 | |||
|         if f_crc8(payload[:-1]) != payload[-1]: | |||
|             raise BufferError('Frame kaputt') | |||
| 
 | |||
|         self.ch_rx = ch_rx | |||
|         self.ch_tx = ch_tx | |||
| 
 | |||
|     @property | |||
|     def mid(self): | |||
|         """Transaction counter""" | |||
|         return self.frame[0] | |||
| 
 | |||
|     @property | |||
|     def src(self): | |||
|         """ | |||
|         Sender adddress | |||
| 
 | |||
|         :return: sender address | |||
|         :rtype: int | |||
|         """ | |||
|         src = struct.unpack('>L', self.frame[1:5]) | |||
|         return src[0] | |||
|     @property | |||
|     def dst(self): | |||
|         """ | |||
|         Receiver adddress | |||
| 
 | |||
|         :return: receiver address | |||
|         :rtype: int | |||
|         """ | |||
|         dst = struct.unpack('>L', self.frame[5:8]) | |||
|         return dst[0] | |||
|     @property | |||
|     def seq(self): | |||
|         """ | |||
|         Framne sequence number | |||
| 
 | |||
|         :return: sequence number | |||
|         :rtype: int | |||
|         """ | |||
|         result = struct.unpack('>B', self.frame[9:10]) | |||
|         return result[0] | |||
|     @property | |||
|     def data(self): | |||
|         """ | |||
|         Data without protocol framing | |||
| 
 | |||
|         :return: payload chunk | |||
|         :rtype: bytes | |||
|         """ | |||
|         return self.frame[10:-1] | |||
| 
 | |||
|     def __str__(self): | |||
|         """ | |||
|         Represent received ESB frame | |||
| 
 | |||
|         :return: log line received frame | |||
|         :rtype: str | |||
|         """ | |||
|         dt = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") | |||
|         size = len(self.frame) | |||
|         channel = f' channel {self.ch_rx}' if self.ch_rx else '' | |||
|         raw = " ".join([f"{b:02x}" for b in self.frame]) | |||
|         return f"{dt} Received {size} bytes{channel}: {raw}" | |||
| 
 | |||
| class HoymilesNRF: | |||
|     """Hoymiles NRF24 Interface""" | |||
|     tx_channel_id = 0 | |||
|     tx_channel_list = [40] | |||
|     rx_channel_id = 0 | |||
|     rx_channel_list = [3,23,40,61,75] | |||
|     rx_channel_ack = False | |||
|     rx_error = 0 | |||
| 
 | |||
|     def __init__(self, device): | |||
|         """ | |||
|         Claim radio device | |||
| 
 | |||
|         :param NRF24 device: instance of NRF24 | |||
|         """ | |||
|         self.radio = device | |||
| 
 | |||
|     def transmit(self, packet): | |||
|         """ | |||
|         Transmit Packet | |||
| 
 | |||
|         :param bytes packet: buffer to send | |||
|         :return: if ACK received of ACK disabled | |||
|         :rtype: bool | |||
|         """ | |||
| 
 | |||
|         inv_esb_addr = b'\01' + packet[1:5] | |||
|         dtu_esb_addr = b'\01' + packet[5:9] | |||
| 
 | |||
|         self.radio.stopListening()  # put radio in TX mode | |||
|         self.radio.setDataRate(RF24_250KBPS) | |||
|         #self.radio.setPALevel(RF24_PA_LOW) | |||
|         self.radio.openReadingPipe(1,dtu_esb_addr) | |||
|         self.radio.openWritingPipe(inv_esb_addr) | |||
|         self.radio.setChannel(self.tx_channel) | |||
|         self.radio.setAutoAck(True) | |||
|         self.radio.setRetries(3, 15) | |||
|         self.radio.setCRCLength(RF24_CRC_16) | |||
|         self.radio.enableDynamicPayloads() | |||
| 
 | |||
|         return self.radio.write(packet) | |||
| 
 | |||
|     def receive(self, timeout=None): | |||
|         """ | |||
|         Receive Packets | |||
| 
 | |||
|         :param timeout: receive timeout in nanoseconds (default: 12e8) | |||
|         :type timeout: int | |||
|         :yields: fragment | |||
|         """ | |||
| 
 | |||
|         if not timeout: | |||
|             timeout=12e8 | |||
| 
 | |||
|         self.radio.setChannel(self.rx_channel) | |||
|         self.radio.setAutoAck(False) | |||
|         self.radio.setRetries(0, 0) | |||
|         self.radio.enableDynamicPayloads() | |||
|         self.radio.setCRCLength(RF24_CRC_16) | |||
|         self.radio.startListening() | |||
| 
 | |||
|         fragments = [] | |||
| 
 | |||
|         # Receive: Loop | |||
|         t_end = time.monotonic_ns()+timeout | |||
|         while time.monotonic_ns() < t_end: | |||
| 
 | |||
|             has_payload, pipe_number = self.radio.available_pipe() | |||
|             if has_payload: | |||
|                 # Data in nRF24 buffer, read it | |||
|                 self.rx_error = 0 | |||
|                 self.rx_channel_ack = True | |||
|                 t_end = time.monotonic_ns()+5e8 | |||
| 
 | |||
|                 size = self.radio.getDynamicPayloadSize() | |||
|                 payload = self.radio.read(size) | |||
|                 fragment = InverterPacketFragment( | |||
|                         payload=payload, | |||
|                         ch_rx=self.rx_channel, ch_tx=self.tx_channel, | |||
|                         time_rx=datetime.now() | |||
|                         ) | |||
|                 yield(fragment) | |||
| 
 | |||
|             else: | |||
|                 # No data in nRF rx buffer, search and wait | |||
|                 # Channel lock in (not currently used) | |||
|                 self.rx_error = self.rx_error + 1 | |||
|                 if self.rx_error > 1: | |||
|                     self.rx_channel_ack = False | |||
|                 # Channel hopping | |||
|                 if self.next_rx_channel(): | |||
|                     self.radio.stopListening() | |||
|                     self.radio.setChannel(self.rx_channel) | |||
|                     self.radio.startListening() | |||
| 
 | |||
|             time.sleep(0.005) | |||
| 
 | |||
|     def next_rx_channel(self): | |||
|         """ | |||
|         Select next channel from hop list | |||
|         - if hopping enabled | |||
|         - if channel has no ack | |||
| 
 | |||
|         :return: if new channel selected | |||
|         :rtype: bool | |||
|         """ | |||
|         if not self.rx_channel_ack: | |||
|             self.rx_channel_id = self.rx_channel_id + 1 | |||
|             if self.rx_channel_id >= len(self.rx_channel_list): | |||
|                 self.rx_channel_id = 0 | |||
|             return True | |||
|         return False | |||
| 
 | |||
|     @property | |||
|     def tx_channel(self): | |||
|         """ | |||
|         Get current tx channel | |||
| 
 | |||
|         :return: tx_channel | |||
|         :rtype: int | |||
|         """ | |||
|         return self.tx_channel_list[self.tx_channel_id] | |||
| 
 | |||
|     @property | |||
|     def rx_channel(self): | |||
|         """ | |||
|         Get current rx channel | |||
| 
 | |||
|         :return: rx_channel | |||
|         :rtype: int | |||
|         """ | |||
|         return self.rx_channel_list[self.rx_channel_id] | |||
| 
 | |||
| def frame_payload(payload): | |||
|     """ | |||
|     Prepare payload for transmission, append Modbus CRC16 | |||
| 
 | |||
|     :param bytes payload: payload to be prepared | |||
|     :return: payload + crc | |||
|     :rtype: bytes | |||
|     """ | |||
|     payload_crc = f_crc_m(payload) | |||
|     payload = payload + struct.pack('>H', payload_crc) | |||
| 
 | |||
|     return payload | |||
| 
 | |||
| def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params): | |||
|     """ | |||
|     Build standart ESB request fragment | |||
| 
 | |||
|     :param bytes fragment: up to 16 bytes payload chunk | |||
|     :param seq: frame sequence byte | |||
|     :type seq: bytes | |||
|     :param src: dtu address | |||
|     :type src: int | |||
|     :param dst: inverter address | |||
|     :type dst: int | |||
|     :return: esb frame fragment | |||
|     :rtype: bytes | |||
|     :raises ValueError: if fragment size larger 16 byte | |||
|     """ | |||
|     if len(fragment) > 17: | |||
|         raise ValueError(f'ESB fragment exeeds mtu ({mtu}): Fragment size {len(fragment)} bytes') | |||
| 
 | |||
|     p = b'' | |||
|     p = p + b'\x15' | |||
|     p = p + ser_to_hm_addr(dst) | |||
|     p = p + ser_to_hm_addr(src) | |||
|     p = p + seq | |||
| 
 | |||
|     p = p + fragment | |||
| 
 | |||
|     crc8 = f_crc8(p) | |||
|     p = p + struct.pack('B', crc8) | |||
| 
 | |||
|     return p | |||
| 
 | |||
| def compose_esb_packet(packet, mtu=17, **params): | |||
|     """ | |||
|     Build ESB packet, chunk packet | |||
| 
 | |||
|     :param bytes packet: payload data | |||
|     :param mtu: maximum transmission unit per frame (default: 17) | |||
|     :type mtu: int | |||
|     :yields: fragment | |||
|     """ | |||
|     for i in range(0, len(packet), mtu): | |||
|         fragment = compose_esb_fragment(packet[i:i+mtu], **params) | |||
|         yield(fragment) | |||
| 
 | |||
| def compose_set_time_payload(timestamp=None): | |||
|     """ | |||
|     Build set time request packet | |||
| 
 | |||
|     :param timestamp: time to set (default: int(time.time()) ) | |||
|     :type timestamp: int | |||
|     :return: payload | |||
|     :rtype: bytes | |||
|     """ | |||
|     if not timestamp: | |||
|         timestamp = int(time.time()) | |||
| 
 | |||
|     payload = b'\x0b\x00' | |||
|     payload = payload + struct.pack('>L', timestamp)  # big-endian: msb at low address | |||
|     payload = payload + b'\x00\x00\x00\x05\x00\x00\x00\x00' | |||
| 
 | |||
|     return frame_payload(payload) | |||
| 
 | |||
| class InverterTransaction: | |||
|     """ | |||
|     Inverter transaction buffer, implements transport-layer functions while | |||
|     communicating with Hoymiles inverters | |||
|     """ | |||
|     tx_queue = [] | |||
|     scratch = [] | |||
|     inverter_ser = None | |||
|     inverter_addr = None | |||
|     dtu_ser = None | |||
|     req_type = None | |||
| 
 | |||
|     radio = None | |||
| 
 | |||
|     def __init__(self, | |||
|             request_time=None, | |||
|             inverter_ser=None, | |||
|             dtu_ser=None, | |||
|             radio=None, | |||
|             **params): | |||
|         """ | |||
|         :param request: Transmit ESB packet | |||
|         :type request: bytes | |||
|         :param request_time: datetime of transmission | |||
|         :type request_time: datetime | |||
|         :param inverter_ser: inverter serial | |||
|         :type inverter_ser: str | |||
|         :param dtu_ser: DTU serial | |||
|         :type dtu_ser: str | |||
|         :param radio: HoymilesNRF instance to use | |||
|         :type radio: HoymilesNRF or None | |||
|         """ | |||
| 
 | |||
|         if radio: | |||
|             self.radio = radio | |||
| 
 | |||
|         if not request_time: | |||
|             request_time=datetime.now() | |||
| 
 | |||
|         self.scratch = [] | |||
|         if 'scratch' in params: | |||
|             self.scratch = params['scratch'] | |||
| 
 | |||
|         self.inverter_ser = inverter_ser | |||
|         if inverter_ser: | |||
|             self.inverter_addr = ser_to_hm_addr(inverter_ser) | |||
| 
 | |||
|         self.dtu_ser = dtu_ser | |||
|         if dtu_ser: | |||
|             self.dtu_addr = ser_to_hm_addr(dtu_ser) | |||
| 
 | |||
|         self.request = None | |||
|         if 'request' in params: | |||
|             self.request = params['request'] | |||
|             self.queue_tx(self.request) | |||
|             self.inverter_addr, self.dtu_addr, seq, self.req_type = struct.unpack('>LLBB', params['request'][1:11]) | |||
|         self.request_time = request_time | |||
| 
 | |||
|     def rxtx(self): | |||
|         """ | |||
|         Transmit next packet from tx_queue if available | |||
|         and wait for responses | |||
| 
 | |||
|         :return: if we got contact | |||
|         :rtype: bool | |||
|         """ | |||
|         if not self.radio: | |||
|             return False | |||
| 
 | |||
|         if not len(self.tx_queue): | |||
|             return False | |||
| 
 | |||
|         packet = self.tx_queue.pop(0) | |||
| 
 | |||
|         if HOYMILES_TRANSACTION_LOGGING: | |||
|             dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") | |||
|             print(f'{dt} Transmit {len(packet)} | {hexify_payload(packet)}') | |||
|          | |||
|         self.radio.transmit(packet) | |||
| 
 | |||
|         wait = False | |||
|         try: | |||
|             for response in self.radio.receive(): | |||
|                 if HOYMILES_TRANSACTION_LOGGING: | |||
|                     print(response) | |||
|          | |||
|                 self.frame_append(response) | |||
|                 wait = True | |||
|         except TimeoutError: | |||
|             pass | |||
| 
 | |||
|         return wait | |||
| 
 | |||
|     def frame_append(self, frame): | |||
|         """ | |||
|         Append received raw frame to local scratch buffer | |||
| 
 | |||
|         :param bytes frame: Received ESB frame | |||
|         :return None | |||
|         """ | |||
|         self.scratch.append(frame) | |||
| 
 | |||
|     def queue_tx(self, frame): | |||
|         """ | |||
|         Enqueue packet for transmission if radio is available | |||
| 
 | |||
|         :param bytes frame: ESB frame for transmit | |||
|         :return: if radio is available and frame scheduled | |||
|         :rtype: bool | |||
|         """ | |||
|         if not self.radio: | |||
|             return False | |||
| 
 | |||
|         self.tx_queue.append(frame) | |||
| 
 | |||
|         return True | |||
| 
 | |||
|     def get_payload(self, src=None): | |||
|         """ | |||
|         Reconstruct Hoymiles payload from scratch buffer | |||
| 
 | |||
|         :param src: filter frames by inverter hm_address (default self.inverter_address) | |||
|         :type src: bytes | |||
|         :return: payload | |||
|         :rtype: bytes | |||
|         :raises BufferError: if one or more frames are missing | |||
|         :raises ValueError: if assambled payload fails CRC check | |||
|         """ | |||
| 
 | |||
|         if not src: | |||
|             src = self.inverter_addr | |||
| 
 | |||
|         # Collect all frames from source_address src | |||
|         frames = [frame for frame in self.scratch if frame.src == src] | |||
| 
 | |||
|         tr_len = 0 | |||
|         # Find end frame and extract message frame count | |||
|         try: | |||
|             end_frame = next(frame for frame in frames if frame.seq > 0x80) | |||
|             self.time_rx = end_frame.time_rx | |||
|             tr_len = end_frame.seq - 0x80 | |||
|         except StopIteration: | |||
|             raise BufferError(f'Missing packet: Last packet {len(self.scratch)}') | |||
| 
 | |||
|         # Rebuild payload from unordered frames | |||
|         payload = b'' | |||
|         for frame_id in range(1, tr_len): | |||
|             try: | |||
|                 data_frame = next(item for item in frames if item.seq == frame_id) | |||
|                 payload = payload + data_frame.data | |||
|             except StopIteration: | |||
|                 self.__retransmit_frame(frame_id) | |||
|                 raise BufferError(f'Frame {frame_id} missing: Request Retransmit') | |||
| 
 | |||
|         payload = payload + end_frame.data | |||
| 
 | |||
|         # check crc | |||
|         pcrc = struct.unpack('>H', payload[-2:])[0] | |||
|         if f_crc_m(payload[:-2]) != pcrc: | |||
|             raise ValueError('Payload failed CRC check.') | |||
| 
 | |||
|         return payload | |||
| 
 | |||
|     def __retransmit_frame(self, frame_id): | |||
|         """ | |||
|         Build and queue retransmit request | |||
| 
 | |||
|         :param int frame_id: frame id to re-schedule | |||
|         :return: if successful scheduled | |||
|         :rtype: bool | |||
|         """ | |||
|         packet = compose_esb_fragment(b'', | |||
|                 seq=int(0x80 + frame_id).to_bytes(1, 'big'), | |||
|                 src=self.dtu_ser, | |||
|                 dst=self.inverter_ser) | |||
| 
 | |||
|         return self.queue_tx(packet) | |||
| 
 | |||
|     def __str__(self): | |||
|         """ | |||
|         Represent transmit payload | |||
| 
 | |||
|         :return: log line of payload for transmission | |||
|         :rtype: str | |||
|         """ | |||
|         dt = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f") | |||
|         size = len(self.request) | |||
|         return f'{dt} Transmit | {hexify_payload(self.request)}' | |||
| 
 | |||
| def hexify_payload(byte_var): | |||
|     """ | |||
|     Represent bytes | |||
| 
 | |||
|     :param bytes byte_var: bytes to be hexlified | |||
|     :return: two-byte while-space padded byte representation | |||
|     :rtype: str | |||
|     """ | |||
|     return ' '.join([f"{b:02x}" for b in byte_var]) | |||
| @ -0,0 +1,261 @@ | |||
| #!/usr/bin/env python3 | |||
| # -*- coding: utf-8 -*- | |||
| 
 | |||
| import sys | |||
| import struct | |||
| import re | |||
| import time | |||
| from datetime import datetime | |||
| import argparse | |||
| import hoymiles | |||
| from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 | |||
| import paho.mqtt.client | |||
| import yaml | |||
| from yaml.loader import SafeLoader | |||
| 
 | |||
| def main_loop(): | |||
|     """Main loop""" | |||
|     inverters = [ | |||
|             inverter for inverter in ahoy_config.get('inverters', []) | |||
|             if not inverter.get('disabled', False)] | |||
| 
 | |||
|     for inverter in inverters: | |||
|         if hoymiles.HOYMILES_DEBUG_LOGGING: | |||
|             print(f'Poll inverter {inverter["serial"]}') | |||
|         poll_inverter(inverter) | |||
| 
 | |||
| def poll_inverter(inverter, retries=4): | |||
|     """ | |||
|     Send/Receive command_queue, initiate status poll on inverter | |||
| 
 | |||
|     :param str inverter: inverter serial | |||
|     :param retries: tx retry count if no inverter contact | |||
|     :type retries: int | |||
|     """ | |||
|     inverter_ser = inverter.get('serial') | |||
|     dtu_ser = ahoy_config.get('dtu', {}).get('serial') | |||
| 
 | |||
|     # Queue at least status data request | |||
|     command_queue[str(inverter_ser)].append(hoymiles.compose_set_time_payload()) | |||
| 
 | |||
|     # Putt all queued commands for current inverter on air | |||
|     while len(command_queue[str(inverter_ser)]) > 0: | |||
|         payload = command_queue[str(inverter_ser)].pop(0) | |||
| 
 | |||
|         # Send payload {ttl}-times until we get at least one reponse | |||
|         payload_ttl = retries | |||
|         while payload_ttl > 0: | |||
|             payload_ttl = payload_ttl - 1 | |||
|             com = hoymiles.InverterTransaction( | |||
|                     radio=hmradio, | |||
|                     dtu_ser=dtu_ser, | |||
|                     inverter_ser=inverter_ser, | |||
|                     request=next(hoymiles.compose_esb_packet( | |||
|                         payload, | |||
|                         seq=b'\x80', | |||
|                         src=dtu_ser, | |||
|                         dst=inverter_ser | |||
|                         ))) | |||
|             response = None | |||
|             while com.rxtx(): | |||
|                 try: | |||
|                     response = com.get_payload() | |||
|                     payload_ttl = 0 | |||
|                 except Exception as e: | |||
|                     print(f'Error while retrieving data: {e}') | |||
|                     pass | |||
| 
 | |||
|         # Handle the response data if any | |||
|         if response: | |||
|             dt = datetime.now() | |||
|             print(f'{dt} Payload: ' + hoymiles.hexify_payload(response)) | |||
|             decoder = hoymiles.ResponseDecoder(response, | |||
|                     request=com.request, | |||
|                     inverter_ser=inverter_ser | |||
|                     ) | |||
|             result = decoder.decode() | |||
|             if isinstance(result, hoymiles.decoders.StatusResponse): | |||
|                 data = result.__dict__() | |||
|                 if hoymiles.HOYMILES_DEBUG_LOGGING: | |||
|                     print(f'{dt} Decoded: {data["temperature"]}', end='') | |||
|                     phase_id = 0 | |||
|                     for phase in data['phases']: | |||
|                         print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='') | |||
|                         phase_id = phase_id + 1 | |||
|                     string_id = 0 | |||
|                     for string in data['strings']: | |||
|                         print(f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}', end='') | |||
|                         string_id = string_id + 1 | |||
|                     print() | |||
| 
 | |||
|                 if mqtt_client: | |||
|                     mqtt_send_status(mqtt_client, inverter_ser, data, | |||
|                             topic=inverter.get('mqtt', {}).get('topic', None)) | |||
| 
 | |||
| def mqtt_send_status(broker, inverter_ser, data, topic=None): | |||
|     """ | |||
|     Publish StatusResponse object | |||
| 
 | |||
|     :param paho.mqtt.client.Client broker: mqtt-client instance | |||
|     :param str inverter_ser: inverter serial | |||
|     :param hoymiles.StatusResponse data: decoded inverter StatusResponse | |||
|     :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) | |||
|     :type topic: str | |||
|     """ | |||
| 
 | |||
|     if not topic: | |||
|         topic = f'hoymiles/{inverter_ser}' | |||
| 
 | |||
|     # AC Data | |||
|     phase_id = 0 | |||
|     for phase in data['phases']: | |||
|         broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) | |||
|         broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) | |||
|         broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) | |||
|         phase_id = phase_id + 1 | |||
| 
 | |||
|     # DC Data | |||
|     string_id = 0 | |||
|     for string in data['strings']: | |||
|         broker.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) | |||
|         broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) | |||
|         broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) | |||
|         broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) | |||
|         string_id = string_id + 1 | |||
|     # Global | |||
|     broker.publish(f'{topic}/frequency', data['frequency']) | |||
|     broker.publish(f'{topic}/temperature', data['temperature']) | |||
| 
 | |||
| def mqtt_on_command(client, userdata, message): | |||
|     """ | |||
|     Handle commands to topic | |||
|         hoymiles/{inverter_ser}/command | |||
|     frame a payload and put onto command_queue | |||
| 
 | |||
|     Inverters must have mqtt.send_raw_enabled: true configured | |||
| 
 | |||
|     This can be used to inject debug payloads | |||
|     The message must be in hexlified format | |||
| 
 | |||
|     Use of variables: | |||
|     tttttttt gets expanded to a current int(time) | |||
| 
 | |||
|     Example injects exactly the same as we normally use to poll data: | |||
|       mosquitto -h broker -t inverter_topic/command -m 800b00tttttttt0000000500000000 | |||
| 
 | |||
|     This allows for even faster hacking during runtime | |||
| 
 | |||
|     :param paho.mqtt.client.Client client: mqtt-client instance | |||
|     :param dict userdata: Userdata | |||
|     :param dict message: mqtt-client message object | |||
|     """ | |||
|     try: | |||
|         inverter_ser = next( | |||
|                 item[0] for item in mqtt_command_topic_subs if item[1] == message.topic) | |||
|     except StopIteration: | |||
|         print('Unexpedtedly received mqtt message for {message.topic}') | |||
| 
 | |||
|     if inverter_ser: | |||
|         p_message = message.payload.decode('utf-8').lower() | |||
| 
 | |||
|         # Expand tttttttt to current time for use in hexlified payload | |||
|         expand_time = ''.join(f'{b:02x}' for b in struct.pack('>L', int(time.time()))) | |||
|         p_message = p_message.replace('tttttttt', expand_time) | |||
| 
 | |||
|         if (len(p_message) < 2048 \ | |||
|             and len(p_message) % 2 == 0 \ | |||
|             and re.match(r'^[a-f0-9]+$', p_message)): | |||
|             payload = bytes.fromhex(p_message) | |||
|             # commands must start with \x80 | |||
|             if payload[0] == 0x80: | |||
|                 command_queue[str(inverter_ser)].append( | |||
|                     hoymiles.frame_payload(payload[1:])) | |||
| 
 | |||
| if __name__ == '__main__': | |||
|     parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles") | |||
|     parser.add_argument("-c", "--config-file", nargs="?", required=True, | |||
|         help="configuration file") | |||
|     parser.add_argument("--log-transactions", action="store_true", default=False, | |||
|         help="Enable transaction logging output") | |||
|     parser.add_argument("--verbose", action="store_true", default=False, | |||
|         help="Enable debug output") | |||
|     global_config = parser.parse_args() | |||
| 
 | |||
|     # Load ahoy.yml config file | |||
|     try: | |||
|         if isinstance(global_config.config_file, str) == True: | |||
|             with open(global_config.config_file, 'r') as yf: | |||
|                 cfg = yaml.load(yf, Loader=SafeLoader) | |||
|         else: | |||
|             with open('ahoy.yml', 'r') as yf: | |||
|                 cfg = yaml.load(yf, Loader=SafeLoader) | |||
|     except FileNotFoundError: | |||
|         print("Could not load config file. Try --help") | |||
|         sys.exit(2) | |||
|     except yaml.YAMLError as ye: | |||
|         print('Failed to load config frile {global_config.config_file}: {ye}') | |||
|         sys.exit(1) | |||
| 
 | |||
|     ahoy_config = dict(cfg.get('ahoy', {})) | |||
| 
 | |||
|     # Prepare for multiple transceivers, makes them configurable (currently | |||
|     # only one supported) | |||
|     for radio_config in ahoy_config.get('nrf', [{}]): | |||
|         radio = RF24( | |||
|                 radio_config.get('ce_pin', 22), | |||
|                 radio_config.get('cs_pin', 0), | |||
|                 radio_config.get('spispeed', 1000000)) | |||
|         hmradio = hoymiles.HoymilesNRF(device=radio) | |||
| 
 | |||
|     mqtt_client = None | |||
| 
 | |||
|     command_queue = {} | |||
|     mqtt_command_topic_subs = [] | |||
| 
 | |||
|     if global_config.log_transactions: | |||
|         hoymiles.HOYMILES_TRANSACTION_LOGGING=True | |||
|     if global_config.verbose: | |||
|         hoymiles.HOYMILES_DEBUG_LOGGING=True | |||
| 
 | |||
|     mqtt_config = ahoy_config.get('mqtt', []) | |||
|     if not mqtt_config.get('disabled', False): | |||
|         mqtt_client = paho.mqtt.client.Client() | |||
|         mqtt_client.username_pw_set(mqtt_config.get('user', None), mqtt_config.get('password', None)) | |||
|         mqtt_client.connect(mqtt_config.get('host', '127.0.0.1'), mqtt_config.get('port', 1883)) | |||
|         mqtt_client.loop_start() | |||
|         mqtt_client.on_message = mqtt_on_command | |||
| 
 | |||
|     if not radio.begin(): | |||
|         raise RuntimeError('Can\'t open radio') | |||
| 
 | |||
|     inverters = [inverter.get('serial') for inverter in ahoy_config.get('inverters', [])] | |||
|     for inverter in ahoy_config.get('inverters', []): | |||
|         inverter_ser = inverter.get('serial') | |||
|         command_queue[str(inverter_ser)] = [] | |||
| 
 | |||
|         # | |||
|         # Enables and subscribe inverter to mqtt /command-Topic | |||
|         # | |||
|         if mqtt_client and inverter.get('mqtt', {}).get('send_raw_enabled', False): | |||
|             topic_item = ( | |||
|                     str(inverter_ser), | |||
|                     inverter.get('mqtt', {}).get('topic', f'hoymiles/{inverter_ser}') + '/command' | |||
|                     ) | |||
|             mqtt_client.subscribe(topic_item[1]) | |||
|             mqtt_command_topic_subs.append(topic_item) | |||
| 
 | |||
|     loop_interval = ahoy_config.get('interval', 1) | |||
|     try: | |||
|         while True: | |||
|             t_loop_start = time.time() | |||
| 
 | |||
|             main_loop() | |||
| 
 | |||
|             print('', end='', flush=True) | |||
| 
 | |||
|             if loop_interval > 0 and (time.time() - t_loop_start) < loop_interval: | |||
|                 time.sleep(time.time() % loop_interval) | |||
| 
 | |||
|     except KeyboardInterrupt: | |||
|         radio.powerDown() | |||
|         sys.exit() | |||
| @ -0,0 +1,587 @@ | |||
| #!/usr/bin/python3 | |||
| # -*- coding: utf-8 -*- | |||
| import struct | |||
| import crcmod | |||
| from datetime import timedelta | |||
| 
 | |||
| f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') | |||
| 
 | |||
| class StatusResponse: | |||
|     """Inverter StatusResponse object""" | |||
|     e_keys  = ['voltage','current','power','energy_total','energy_daily'] | |||
| 
 | |||
|     def unpack(self, fmt, base): | |||
|         """ | |||
|         Data unpack helper | |||
| 
 | |||
|         :param str fmt: struct format string | |||
|         :param int base: unpack base position from self.response bytes | |||
|         :return: unpacked values | |||
|         :rtype: tuple | |||
|         """ | |||
|         size = struct.calcsize(fmt) | |||
|         return struct.unpack(fmt, self.response[base:base+size]) | |||
| 
 | |||
|     @property | |||
|     def phases(self): | |||
|         """ | |||
|         AC power data | |||
| 
 | |||
|         :retrun: list of dict's | |||
|         :rtype: list | |||
|         """ | |||
|         phases = [] | |||
|         p_exists = True | |||
|         while p_exists: | |||
|             p_exists = False | |||
|             phase_id = len(phases) | |||
|             phase = {} | |||
|             for key in self.e_keys: | |||
|                 prop = f'ac_{key}_{phase_id}' | |||
|                 if hasattr(self, prop): | |||
|                     p_exists = True | |||
|                     phase[key] = getattr(self, prop) | |||
|             if p_exists: | |||
|                 phases.append(phase) | |||
| 
 | |||
|         return phases | |||
| 
 | |||
|     @property | |||
|     def strings(self): | |||
|         """ | |||
|         DC PV-string data | |||
| 
 | |||
|         :retrun: list of dict's | |||
|         :rtype: list | |||
|         """ | |||
|         strings = [] | |||
|         s_exists = True | |||
|         while s_exists: | |||
|             s_exists = False | |||
|             string_id = len(strings) | |||
|             string = {} | |||
|             for key in self.e_keys: | |||
|                 prop = f'dc_{key}_{string_id}' | |||
|                 if hasattr(self, prop): | |||
|                     s_exists = True | |||
|                     string[key] = getattr(self, prop) | |||
|             if s_exists: | |||
|                 strings.append(string) | |||
| 
 | |||
|         return strings | |||
| 
 | |||
|     def __dict__(self): | |||
|         """ | |||
|         Get all known data | |||
| 
 | |||
|         :return: dict of properties | |||
|         :rtype: dict | |||
|         """ | |||
|         data = {} | |||
|         data['phases'] = self.phases | |||
|         data['strings'] = self.strings | |||
|         data['temperature'] = self.temperature | |||
|         data['frequency'] = self.frequency | |||
|         return data | |||
| 
 | |||
| class UnknownResponse: | |||
|     """ | |||
|     Debugging helper for unknown payload format | |||
|     """ | |||
|     @property | |||
|     def hex_ascii(self): | |||
|         """ | |||
|         Generate white-space separated byte representation | |||
| 
 | |||
|         :return: hexlifierd byte string | |||
|         :rtype: str | |||
|         """ | |||
|         return ' '.join([f'{b:02x}' for b in self.response]) | |||
| 
 | |||
|     @property | |||
|     def valid_crc(self): | |||
|         """ | |||
|         Checks if self.response has valid Modbus CRC | |||
| 
 | |||
|         :return: if crc is available and correct | |||
|         :rtype: bool | |||
|         """ | |||
|         # check crc | |||
|         pcrc = struct.unpack('>H', self.response[-2:])[0] | |||
|         return f_crc_m(self.response[:-2]) == pcrc | |||
| 
 | |||
|     @property | |||
|     def dump_longs(self): | |||
|         """Get all data, interpreted as long""" | |||
|         if len(self.response) < 5: | |||
|             return None | |||
| 
 | |||
|         res = self.response | |||
| 
 | |||
|         r = len(res) % 16 | |||
|         res = res[:r*-1] | |||
| 
 | |||
|         vals = None | |||
|         if len(res) % 16 == 0: | |||
|             n = len(res)/4 | |||
|             vals = struct.unpack(f'>{int(n)}L', res) | |||
| 
 | |||
|         return vals | |||
| 
 | |||
|     @property | |||
|     def dump_longs_pad1(self): | |||
|         """Get all data, interpreted as long""" | |||
|         if len(self.response) < 7: | |||
|             return None | |||
| 
 | |||
|         res = self.response[2:] | |||
| 
 | |||
|         r = len(res) % 16 | |||
|         res = res[:r*-1] | |||
| 
 | |||
|         vals = None | |||
|         if len(res) % 16 == 0: | |||
|             n = len(res)/4 | |||
|             vals = struct.unpack(f'>{int(n)}L', res) | |||
| 
 | |||
|         return vals | |||
| 
 | |||
|     @property | |||
|     def dump_longs_pad2(self): | |||
|         """Get all data, interpreted as long""" | |||
|         if len(self.response) < 9: | |||
|             return None | |||
| 
 | |||
|         res = self.response[4:] | |||
| 
 | |||
|         r = len(res) % 16 | |||
|         res = res[:r*-1] | |||
| 
 | |||
|         vals = None | |||
|         if len(res) % 16 == 0: | |||
|             n = len(res)/4 | |||
|             vals = struct.unpack(f'>{int(n)}L', res) | |||
| 
 | |||
|         return vals | |||
| 
 | |||
|     @property | |||
|     def dump_longs_pad3(self): | |||
|         """Get all data, interpreted as long""" | |||
|         if len(self.response) < 11: | |||
|             return None | |||
| 
 | |||
|         res = self.response[6:] | |||
| 
 | |||
|         r = len(res) % 16 | |||
|         res = res[:r*-1] | |||
| 
 | |||
|         vals = None | |||
|         if len(res) % 16 == 0: | |||
|             n = len(res)/4 | |||
|             vals = struct.unpack(f'>{int(n)}L', res) | |||
| 
 | |||
|         return vals | |||
| 
 | |||
|     @property | |||
|     def dump_shorts(self): | |||
|         """Get all data, interpreted as short""" | |||
|         if len(self.response) < 5: | |||
|             return None | |||
| 
 | |||
|         res = self.response | |||
| 
 | |||
|         r = len(res) % 4 | |||
|         res = res[:r*-1] | |||
| 
 | |||
|         vals = None | |||
|         if len(res) % 4 == 0: | |||
|             n = len(res)/2 | |||
|             vals = struct.unpack(f'>{int(n)}H', res) | |||
| 
 | |||
|         return vals | |||
| 
 | |||
|     @property | |||
|     def dump_shorts_pad1(self): | |||
|         """Get all data, interpreted as short""" | |||
|         if len(self.response) < 6: | |||
|             return None | |||
| 
 | |||
|         res = self.response[1:] | |||
| 
 | |||
|         r = len(res) % 4 | |||
|         res = res[:r*-1] | |||
| 
 | |||
|         vals = None | |||
|         if len(res) % 4 == 0: | |||
|             n = len(res)/2 | |||
|             vals = struct.unpack(f'>{int(n)}H', res) | |||
| 
 | |||
|         return vals | |||
| 
 | |||
| class EventsResponse(UnknownResponse): | |||
| 
 | |||
|     alarm_codes = { | |||
|             1: 'Inverter start', | |||
|             2: 'Producing power', | |||
|             121: 'Over temperature protection', | |||
|             125: 'Grid configuration parameter error', | |||
|             126: 'Software error code 126', | |||
|             127: 'Firmware error', | |||
|             128: 'Software error code 128', | |||
|             129: 'Software error code 129', | |||
|             130: 'Offline', | |||
|             141: 'Grid overvoltage', | |||
|             142: 'Average grid overvoltage', | |||
|             143: 'Grid undervoltage', | |||
|             144: 'Grid overfrequency', | |||
|             145: 'Grid underfrequency', | |||
|             146: 'Rapid grid frequency change', | |||
|             147: 'Power grid outage', | |||
|             148: 'Grid disconnection', | |||
|             149: 'Island detected', | |||
|             205: 'Input port 1 & 2 overvoltage', | |||
|             206: 'Input port 3 & 4 overvoltage', | |||
|             207: 'Input port 1 & 2 undervoltage', | |||
|             208: 'Input port 3 & 4 undervoltage', | |||
|             209: 'Port 1 no input', | |||
|             210: 'Port 2 no input', | |||
|             211: 'Port 3 no input', | |||
|             212: 'Port 4 no input', | |||
|             213: 'PV-1 & PV-2 abnormal wiring', | |||
|             214: 'PV-3 & PV-4 abnormal wiring', | |||
|             215: 'PV-1 Input overvoltage', | |||
|             216: 'PV-1 Input undervoltage', | |||
|             217: 'PV-2 Input overvoltage', | |||
|             218: 'PV-2 Input undervoltage', | |||
|             219: 'PV-3 Input overvoltage', | |||
|             220: 'PV-3 Input undervoltage', | |||
|             221: 'PV-4 Input overvoltage', | |||
|             222: 'PV-4 Input undervoltage', | |||
|             301: 'Hardware error code 301', | |||
|             302: 'Hardware error code 302', | |||
|             303: 'Hardware error code 303', | |||
|             304: 'Hardware error code 304', | |||
|             305: 'Hardware error code 305', | |||
|             306: 'Hardware error code 306', | |||
|             307: 'Hardware error code 307', | |||
|             308: 'Hardware error code 308', | |||
|             309: 'Hardware error code 309', | |||
|             310: 'Hardware error code 310', | |||
|             311: 'Hardware error code 311', | |||
|             312: 'Hardware error code 312', | |||
|             313: 'Hardware error code 313', | |||
|             314: 'Hardware error code 314', | |||
|             5041: 'Error code-04 Port 1', | |||
|             5042: 'Error code-04 Port 2', | |||
|             5043: 'Error code-04 Port 3', | |||
|             5044: 'Error code-04 Port 4', | |||
|             5051: 'PV Input 1 Overvoltage/Undervoltage', | |||
|             5052: 'PV Input 2 Overvoltage/Undervoltage', | |||
|             5053: 'PV Input 3 Overvoltage/Undervoltage', | |||
|             5054: 'PV Input 4 Overvoltage/Undervoltage', | |||
|             5060: 'Abnormal bias', | |||
|             5070: 'Over temperature protection', | |||
|             5080: 'Grid Overvoltage/Undervoltage', | |||
|             5090: 'Grid Overfrequency/Underfrequency', | |||
|             5100: 'Island detected', | |||
|             5120: 'EEPROM reading and writing error', | |||
|             5150: '10 min value grid overvoltage', | |||
|             5200: 'Firmware error', | |||
|             8310: 'Shut down', | |||
|             9000: 'Microinverter is suspected of being stolen' | |||
|             } | |||
| 
 | |||
|     def __init__(self, response): | |||
|         self.response = response | |||
| 
 | |||
|         crc_valid = self.valid_crc | |||
|         if crc_valid: | |||
|             print(' payload has valid modbus crc') | |||
|             self.response = response[:-2] | |||
| 
 | |||
|         status = self.response[:2] | |||
| 
 | |||
|         chunk_size = 12 | |||
|         for c in range(2, len(self.response), chunk_size): | |||
|             chunk = self.response[c:c+chunk_size] | |||
| 
 | |||
|             print(' '.join([f'{b:02x}' for b in chunk]) + ': ') | |||
| 
 | |||
|             opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6]) | |||
|             a_text = self.alarm_codes.get(a_code, 'N/A') | |||
| 
 | |||
|             print(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}') | |||
| 
 | |||
|             for fmt in ['BBHHHHH']: | |||
|                 print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))) | |||
|             print(end='', flush=True) | |||
| 
 | |||
| class DEBUG_DecodeAny(UnknownResponse): | |||
|     """Default decoder""" | |||
|     def __init__(self, response): | |||
|         """ | |||
|         Try interpret and print unknown response data | |||
| 
 | |||
|         :param bytes response: response payload bytes | |||
|         """ | |||
|         self.response = response | |||
| 
 | |||
|         crc_valid = self.valid_crc | |||
|         if crc_valid: | |||
|             print(' payload has valid modbus crc') | |||
|             self.response = response[:-2] | |||
| 
 | |||
|         l_payload = len(self.response) | |||
|         print(f' payload has {l_payload} bytes') | |||
| 
 | |||
|         longs = self.dump_longs | |||
|         if not longs: | |||
|             print(' type long      : unable to decode (len or not mod 4)') | |||
|         else: | |||
|             print(' type long      : ' + str(longs)) | |||
| 
 | |||
|         longs = self.dump_longs_pad1 | |||
|         if not longs: | |||
|             print(' type long pad1 : unable to decode (len or not mod 4)') | |||
|         else: | |||
|             print(' type long pad1 : ' + str(longs)) | |||
| 
 | |||
|         longs = self.dump_longs_pad2 | |||
|         if not longs: | |||
|             print(' type long pad2 : unable to decode (len or not mod 4)') | |||
|         else: | |||
|             print(' type long pad2 : ' + str(longs)) | |||
| 
 | |||
|         longs = self.dump_longs_pad3 | |||
|         if not longs: | |||
|             print(' type long pad3 : unable to decode (len or not mod 4)') | |||
|         else: | |||
|             print(' type long pad3 : ' + str(longs)) | |||
| 
 | |||
|         shorts = self.dump_shorts | |||
|         if not shorts: | |||
|             print(' type short     : unable to decode (len or not mod 2)') | |||
|         else: | |||
|             print(' type short     : ' + str(shorts)) | |||
| 
 | |||
|         shorts = self.dump_shorts_pad1 | |||
|         if not shorts: | |||
|             print(' type short pad1: unable to decode (len or not mod 2)') | |||
|         else: | |||
|             print(' type short pad1: ' + str(shorts)) | |||
| 
 | |||
|         try: | |||
|             if len(self.response) > 2: | |||
|                 print(' type utf-8  : ' + self.response.decode('utf-8')) | |||
|         except UnicodeDecodeError: | |||
|             print(' type utf-8  : utf-8 decode error') | |||
| 
 | |||
|         try: | |||
|             if len(self.response) > 2: | |||
|                 print(' type ascii  : ' + self.response.decode('ascii')) | |||
|         except UnicodeDecodeError: | |||
|             print(' type ascii  : ascii decode error') | |||
| 
 | |||
| 
 | |||
| # 1121-Series Intervers, 1 MPPT, 1 Phase | |||
| class HM300_Decode0B(StatusResponse): | |||
|     def __init__(self, response): | |||
|         self.response = response | |||
| 
 | |||
|     @property | |||
|     def dc_voltage_0(self): | |||
|         return self.unpack('>H', 2)[0]/10 | |||
|     @property | |||
|     def dc_current_0(self): | |||
|         return self.unpack('>H', 4)[0]/100 | |||
|     @property | |||
|     def dc_power_0(self): | |||
|         return self.unpack('>H', 6)[0]/10 | |||
|     @property | |||
|     def dc_energy_total_0(self): | |||
|         return self.unpack('>L', 8)[0] | |||
|     @property | |||
|     def dc_energy_daily_0(self): | |||
|         return self.unpack('>H', 12)[0] | |||
| 
 | |||
| 
 | |||
|     @property | |||
|     def ac_voltage_0(self): | |||
|         return self.unpack('>H', 14)[0]/10 | |||
|     @property | |||
|     def ac_current_0(self): | |||
|         return self.unpack('>H', 22)[0]/100 | |||
|     @property | |||
|     def ac_power_0(self): | |||
|         return self.unpack('>H', 18)[0]/10 | |||
|     @property | |||
|     def frequency(self): | |||
|         return self.unpack('>H', 16)[0]/100 | |||
|     @property | |||
|     def temperature(self): | |||
|         return self.unpack('>H', 26)[0]/10 | |||
| 
 | |||
| class HM300_Decode11(EventsResponse): | |||
|     def __init__(self, response): | |||
|         super().__init__(response) | |||
| 
 | |||
| class HM300_Decode12(EventsResponse): | |||
|     def __init__(self, response): | |||
|         super().__init__(response) | |||
| 
 | |||
| 
 | |||
| 
 | |||
| # 1141-Series Inverters, 2 MPPT, 1 Phase | |||
| class HM600_Decode0B(StatusResponse): | |||
|     def __init__(self, response): | |||
|         self.response = response | |||
| 
 | |||
|     @property | |||
|     def dc_voltage_0(self): | |||
|         return self.unpack('>H', 2)[0]/10 | |||
|     @property | |||
|     def dc_current_0(self): | |||
|         return self.unpack('>H', 4)[0]/100 | |||
|     @property | |||
|     def dc_power_0(self): | |||
|         return self.unpack('>H', 6)[0]/10 | |||
|     @property | |||
|     def dc_energy_total_0(self): | |||
|         return self.unpack('>L', 14)[0] | |||
|     @property | |||
|     def dc_energy_daily_0(self): | |||
|         return self.unpack('>H', 22)[0] | |||
| 
 | |||
|     @property | |||
|     def dc_voltage_1(self): | |||
|         return self.unpack('>H', 8)[0]/10 | |||
|     @property | |||
|     def dc_current_1(self): | |||
|         return self.unpack('>H', 10)[0]/100 | |||
|     @property | |||
|     def dc_power_1(self): | |||
|         return self.unpack('>H', 12)[0]/10 | |||
|     @property | |||
|     def dc_energy_total_1(self): | |||
|         return self.unpack('>L', 18)[0] | |||
|     @property | |||
|     def dc_energy_daily_1(self): | |||
|         return self.unpack('>H', 24)[0] | |||
| 
 | |||
|     @property | |||
|     def ac_voltage_0(self): | |||
|         return self.unpack('>H', 26)[0]/10 | |||
|     @property | |||
|     def ac_current_0(self): | |||
|         return self.unpack('>H', 34)[0]/10 | |||
|     @property | |||
|     def ac_power_0(self): | |||
|         return self.unpack('>H', 30)[0]/10 | |||
|     @property | |||
|     def frequency(self): | |||
|         return self.unpack('>H', 28)[0]/100 | |||
|     @property | |||
|     def temperature(self): | |||
|         return self.unpack('>H', 38)[0]/10 | |||
| 
 | |||
| class HM600_Decode11(EventsResponse): | |||
|     def __init__(self, response): | |||
|         super().__init__(response) | |||
| 
 | |||
| class HM600_Decode12(EventsResponse): | |||
|     def __init__(self, response): | |||
|         super().__init__(response) | |||
| 
 | |||
| 
 | |||
| # 1161-Series Inverters, 4 MPPT, 1 Phase | |||
| class HM1200_Decode0B(StatusResponse): | |||
|     def __init__(self, response): | |||
|         self.response = response | |||
| 
 | |||
|     @property | |||
|     def dc_voltage_0(self): | |||
|         return self.unpack('>H', 2)[0]/10 | |||
|     @property | |||
|     def dc_current_0(self): | |||
|         return self.unpack('>H', 4)[0]/100 | |||
|     @property | |||
|     def dc_power_0(self): | |||
|         return self.unpack('>H', 8)[0]/10 | |||
|     @property | |||
|     def dc_energy_total_0(self): | |||
|         return self.unpack('>L', 12)[0] | |||
|     @property | |||
|     def dc_energy_daily_0(self): | |||
|         return self.unpack('>H', 20)[0] | |||
| 
 | |||
|     @property | |||
|     def dc_voltage_1(self): | |||
|         return self.unpack('>H', 2)[0]/10 | |||
|     @property | |||
|     def dc_current_1(self): | |||
|         return self.unpack('>H', 4)[0]/100 | |||
|     @property | |||
|     def dc_power_1(self): | |||
|         return self.unpack('>H', 10)[0]/10 | |||
|     @property | |||
|     def dc_energy_total_1(self): | |||
|         return self.unpack('>L', 16)[0] | |||
|     @property | |||
|     def dc_energy_daily_1(self): | |||
|         return self.unpack('>H', 22)[0] | |||
| 
 | |||
|     @property | |||
|     def dc_voltage_2(self): | |||
|         return self.unpack('>H', 24)[0]/10 | |||
|     @property | |||
|     def dc_current_2(self): | |||
|         return self.unpack('>H', 26)[0]/100 | |||
|     @property | |||
|     def dc_power_2(self): | |||
|         return self.unpack('>H', 30)[0]/10 | |||
|     @property | |||
|     def dc_energy_total_2(self): | |||
|         return self.unpack('>L', 34)[0] | |||
|     @property | |||
|     def dc_energy_daily_2(self): | |||
|         return self.unpack('>H', 42)[0] | |||
| 
 | |||
|     @property | |||
|     def dc_voltage_3(self): | |||
|         return self.unpack('>H', 24)[0]/10 | |||
|     @property | |||
|     def dc_current_3(self): | |||
|         return self.unpack('>H', 28)[0]/100 | |||
|     @property | |||
|     def dc_power_3(self): | |||
|         return self.unpack('>H', 32)[0]/10 | |||
|     @property | |||
|     def dc_energy_total_3(self): | |||
|         return self.unpack('>L', 38)[0] | |||
|     @property | |||
|     def dc_energy_daily_3(self): | |||
|         return self.unpack('>H', 44)[0] | |||
| 
 | |||
|     @property | |||
|     def ac_voltage_0(self): | |||
|         return self.unpack('>H', 46)[0]/10 | |||
|     @property | |||
|     def ac_current_0(self): | |||
|         return self.unpack('>H', 54)[0]/100 | |||
|     @property | |||
|     def ac_power_0(self): | |||
|         return self.unpack('>H', 50)[0]/10 | |||
|     @property | |||
|     def frequency(self): | |||
|         return self.unpack('>H', 48)[0]/100 | |||
|     @property | |||
|     def temperature(self): | |||
|         return self.unpack('>H', 58)[0]/10 | |||
| 
 | |||
| class HM1200_Decode11(EventsResponse): | |||
|     def __init__(self, response): | |||
|         super().__init__(response) | |||
| 
 | |||
| class HM1200_Decode12(EventsResponse): | |||
|     def __init__(self, response): | |||
|         super().__init__(response) | |||
| @ -1,2 +1,3 @@ | |||
| paho-mqtt | |||
| crcmod | |||
| paho-mqtt>=1.5 | |||
| crcmod>=1.7 | |||
| PyYAML>=5.0 | |||
|  | |||
					Loading…
					
					
				
		Reference in new issue