Browse Source

Merge branch 'main' into pypackage

pull/25/head
Jan-Jonas Sämann 3 years ago
parent
commit
0946f1c765
  1. 5
      .gitignore
  2. 10
      README.md
  3. BIN
      doc/AhoyMiles.fzz
  4. BIN
      doc/AhoyMiles_bb.png
  5. BIN
      doc/AhoyMiles_schem.png
  6. BIN
      doc/HM-400 data.xlsx
  7. BIN
      doc/Hoymiles-SerialNumbers.xlsx
  8. 47
      doc/getting-started-ESP8266.md
  9. 1157
      doc/hoymiles-format-description.txt
  10. 158
      tools/HoyDtuSim/CircularBuffer.h
  11. 23
      tools/HoyDtuSim/Debug.h
  12. 38
      tools/HoyDtuSim/HM1200.h
  13. 37
      tools/HoyDtuSim/HM600.h
  14. 605
      tools/HoyDtuSim/HoyDtuSim.ino
  15. 283
      tools/HoyDtuSim/Inverters.h
  16. 151
      tools/HoyDtuSim/ModWebserver.h
  17. 69
      tools/HoyDtuSim/Settings.h
  18. 55
      tools/HoyDtuSim/Sonne.h
  19. 102
      tools/HoyDtuSim/hm_crc.h
  20. 93
      tools/HoyDtuSim/hm_packets.h
  21. 345
      tools/HoyDtuSim/wifi.h
  22. 158
      tools/NRF24_SendRcv/CircularBuffer.h
  23. 23
      tools/NRF24_SendRcv/Debug.h
  24. 129
      tools/NRF24_SendRcv/ModWebserver.h
  25. 597
      tools/NRF24_SendRcv/NRF24_SendRcv.ino
  26. 55
      tools/NRF24_SendRcv/NRF24_sniff_types.h
  27. 82
      tools/NRF24_SendRcv/Settings.h
  28. 55
      tools/NRF24_SendRcv/Sonne.h
  29. 142
      tools/NRF24_SendRcv/hm_crc.cpp
  30. 8
      tools/NRF24_SendRcv/hm_crc.h
  31. 74
      tools/NRF24_SendRcv/hm_packets.cpp
  32. 18
      tools/NRF24_SendRcv/hm_packets.h
  33. 345
      tools/NRF24_SendRcv/wifi.h
  34. 157
      tools/esp8266/CircularBuffer.h
  35. 60
      tools/esp8266/README.md
  36. 611
      tools/esp8266/app.cpp
  37. 94
      tools/esp8266/app.h
  38. 44
      tools/esp8266/config.h
  39. 43
      tools/esp8266/crc.cpp
  40. 16
      tools/esp8266/crc.h
  41. 39
      tools/esp8266/debug.h
  42. 92
      tools/esp8266/defines.h
  43. 133
      tools/esp8266/eep.h
  44. 33
      tools/esp8266/esp8266.ino
  45. 159
      tools/esp8266/hmDefines.h
  46. 213
      tools/esp8266/hmInverter.h
  47. 272
      tools/esp8266/hmRadio.h
  48. 82
      tools/esp8266/hmSystem.h
  49. 28
      tools/esp8266/html/convert.py
  50. 4
      tools/esp8266/html/h/hoymiles_html.h
  51. 4
      tools/esp8266/html/h/index_html.h
  52. 4
      tools/esp8266/html/h/setup_html.h
  53. 4
      tools/esp8266/html/h/style_css.h
  54. 42
      tools/esp8266/html/hoymiles.html
  55. 55
      tools/esp8266/html/index.html
  56. 79
      tools/esp8266/html/setup.html
  57. 190
      tools/esp8266/html/style.css
  58. 432
      tools/esp8266/main.cpp
  59. 125
      tools/esp8266/main.h
  60. 87
      tools/esp8266/mqtt.h
  61. 31
      tools/esp8266/test/hmClassTest/hmClassTest.sln
  62. 147
      tools/esp8266/test/hmClassTest/hmClassTest/hmClassTest.vcxproj
  63. 22
      tools/esp8266/test/hmClassTest/hmClassTest/hmClassTest.vcxproj.filters
  64. 4
      tools/esp8266/test/hmClassTest/hmClassTest/hmClassTest.vcxproj.user
  65. 105
      tools/esp8266/test/hmClassTest/src/main.cpp
  66. 3
      tools/rpi/discover/discover.cpp
  67. 3
      tools/rpi/discover/pretender.cpp

5
.gitignore

@ -10,3 +10,8 @@ compile_commands.json
CTestTestfile.cmake
_deps
build
/**/Debug
/**/v16/*
*.db
*.suo
*.ipch

10
README.md

@ -5,7 +5,13 @@ Various tools, examples, and documentation for communicating with Hoymiles micro
In particular:
* `doc\hoymiles-format-description.txt` is a detailed description of the communications format and the history of this project
* The `tools` folder contains various software tools for RaspberryPi and Arduino
* `doc/hoymiles-format-description.txt` is a detailed description of the communications format and the history of this project
* `doc/getting-started-ESP8266.md` shows the hardware setup for an ESP8266-based system
* The `tools` folder contains various software tools for RaspberryPi, Arduino and ESP8266/ESP32:
* A [version for ESP8266](tools/esp8266) that includes a web interface
* A [version for Arduino Nano](tools/nano/NRF24_SendRcv)
* An [alternative Version of the above](tools/NRF24_SendRcv)
* A [different implementation](tools/HoyDtuSim)
* An [implementation for Raspberry Pi](tools/rpi) that polls an inverter and archives results as log files/stdout as well as posting them to an MQTT broker.
Contributors are always welcome!

BIN
doc/AhoyMiles.fzz

Binary file not shown.

BIN
doc/AhoyMiles_bb.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
doc/AhoyMiles_schem.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
doc/HM-400 data.xlsx

Binary file not shown.

BIN
doc/Hoymiles-SerialNumbers.xlsx

Binary file not shown.

47
doc/getting-started-ESP8266.md

@ -0,0 +1,47 @@
# Getting Started with an ESP8266
Wire Connections
```ditaa
+-----------+ +-----------+
| ESP8266 |--colour--| nRF24L01+ |
| | | |
| 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 |
+-----------+ +-----------+
```
![plot](./AhoyMiles_bb.png)
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
Verify & Compile
* Connect to WiFi Network `ESP AHOY`
* Use password `esp_8266`
* Connect to Network settings
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

1157
doc/hoymiles-format-description.txt

File diff suppressed because it is too large

158
tools/HoyDtuSim/CircularBuffer.h

@ -0,0 +1,158 @@
/*
CircularBuffer - An Arduino circular buffering library for arbitrary types.
Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef CircularBuffer_h
#define CircularBuffer_h
#ifdef ESP8266
#define DISABLE_IRQ noInterrupts()
#define RESTORE_IRQ interrupts()
#else
#define DISABLE_IRQ \
uint8_t sreg = SREG; \
cli();
#define RESTORE_IRQ \
SREG = sreg;
#endif
template <class 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

23
tools/HoyDtuSim/Debug.h

@ -0,0 +1,23 @@
#ifndef __DEBUG_H
#define __DEBUG_H
#ifdef DEBUG
#define DEBUG_OUT Serial
#else
//---
// disable Serial DEBUG output
#define DEBUG_OUT DummySerial
static class {
public:
void begin(...) {}
void print(...) {}
void println(...) {}
void flush() {}
bool available() { return false;}
int readBytes(...) { return 0;}
int printf (...) {return 0;}
} DummySerial;
#endif
#endif

38
tools/HoyDtuSim/HM1200.h

@ -0,0 +1,38 @@
#ifndef __HM1200_H
#define __HM1200_H
#define HM1200
const measureDef_t hm1200_measureDef[] = {
{ IDX_UDC, UNIT_V, CH1, CMD01, 14, BYTES2, DIV10 },
{ IDX_IDC, UNIT_A, CH1, CMD01, 16, BYTES2, DIV100 },
{ IDX_PDC, UNIT_W, CH1, CMD01, 20, BYTES2, DIV10 },
{ IDX_E_TAG, UNIT_WH, CH1, CMD02, 16, BYTES2, DIV1 },
{ IDX_E_TOTAL, UNIT_KWH, CH1, CMD01, 24, BYTES4, DIV1000 },
{ IDX_UDC, UNIT_V, CH2, CMD02, 20, BYTES2, DIV10 },
{ IDX_IDC, UNIT_A, CH2, CMD01, 18, BYTES2, DIV100 },
{ IDX_PDC, UNIT_W, CH2, CMD01, 22, BYTES2, DIV10 },
{ IDX_E_TAG, UNIT_WH, CH2, CMD02, 18, BYTES2, DIV1 },
{ IDX_E_TOTAL, UNIT_KWH, CH2, CMD02, 12, BYTES4, DIV1000 },
{ IDX_IDC, UNIT_A, CH3, CMD02, 22, BYTES2, DIV100 },
{ IDX_PDC, UNIT_W, CH3, CMD02, 26, BYTES2, DIV10 },
{ IDX_E_TAG, UNIT_WH, CH3, CMD03, 22, BYTES2, DIV1 },
{ IDX_E_TOTAL, UNIT_KWH, CH3, CMD03, 14, BYTES4, DIV1000 },
{ IDX_IDC, UNIT_A, CH4, CMD02, 24, BYTES2, DIV100 },
{ IDX_PDC, UNIT_W, CH4, CMD03, 12, BYTES2, DIV10 },
{ IDX_E_TAG, UNIT_WH, CH4, CMD03, 24, BYTES2, DIV1 },
{ IDX_E_TOTAL, UNIT_KWH, CH4, CMD03, 18, BYTES4, DIV1000 },
{ IDX_UAC, UNIT_V, CH0, CMD03, 26, BYTES2, DIV10 },
{ IDX_IPV, UNIT_A, CH0, CMD84, 18, BYTES2, DIV100 },
{ IDX_PAC, UNIT_W, CH0, CMD84, 14, BYTES2, DIV10 },
{ IDX_FREQ, UNIT_HZ, CH0, CMD84, 12, BYTES2, DIV100 },
{ IDX_PERCNT, UNIT_PCT, CH0, CMD84, 20, BYTES2, DIV10 },
{ IDX_WR_TEMP, UNIT_C, CH0, CMD84, 22, BYTES2, DIV10 }
};
measureCalc_t hm1200_measureCalc[] = {};
#define HM1200_MEASURE_LIST_LEN sizeof(hm1200_measureDef)/sizeof(measureDef_t)
#define HM1200_CALCED_LIST_LEN 0
#endif

37
tools/HoyDtuSim/HM600.h

@ -0,0 +1,37 @@
#ifndef __HM600_H
#define __HM600_H
#define HM600
#define HM700
float calcEheute (float *measure) { return measure[8] + measure[9]; }
float calcIpv (float *measure) { return (measure[10] != 0 ? measure[12]/measure[10] : 0); }
const measureDef_t hm600_measureDef[] = {
{ IDX_UDC, CH1, UNIT_V, CMD01, 14, BYTES2, DIV10},
{ IDX_IDC, CH1, UNIT_A, CMD01, 16, BYTES2, DIV100},
{ IDX_PDC, CH1, UNIT_W, CMD01, 18, BYTES2, DIV10},
{ IDX_UDC, CH2, UNIT_V, CMD01, 20, BYTES2, DIV10},
{ IDX_IDC, CH2, UNIT_A, CMD01, 22, BYTES2, DIV100},
{ IDX_PDC, CH2, UNIT_W, CMD01, 24, BYTES2, DIV10},
{ IDX_E_WOCHE,CH0, UNIT_WH, CMD02, 12, BYTES2, DIV1},
{ IDX_E_TOTAL,CH0, UNIT_WH, CMD02, 14, BYTES4, DIV1},
{ IDX_E_TAG, CH1, UNIT_WH, CMD02, 18, BYTES2, DIV1},
{ IDX_E_TAG, CH2, UNIT_WH, CMD02, 20, BYTES2, DIV1},
{ IDX_UAC, CH0, UNIT_V, CMD02, 22, BYTES2, DIV10},
{ IDX_FREQ, CH0, UNIT_HZ, CMD02, 24, BYTES2, DIV100},
{ IDX_PAC, CH0, UNIT_W, CMD02, 26, BYTES2, DIV10},
{ IDX_WR_TEMP,CH0, UNIT_C, CMD83, 18, BYTES2, DIV10}
};
measureCalc_t hm600_measureCalc[] = {
{ IDX_E_HEUTE, UNIT_WH, DIV1, &calcEheute},
{ IDX_IPV, UNIT_A, DIV100, &calcIpv}
};
#define HM600_MEASURE_LIST_LEN sizeof(hm600_measureDef)/sizeof(measureDef_t)
#define HM600_CALCED_LIST_LEN sizeof(hm600_measureCalc)/sizeof(measureCalc_t)
#endif

605
tools/HoyDtuSim/HoyDtuSim.ino

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

283
tools/HoyDtuSim/Inverters.h

@ -0,0 +1,283 @@
#ifndef __INVERTERS_H
#define __INVERTERS_H
// Ausgabe von Debug Infos auf der seriellen Console
#include "Settings.h"
#include "Debug.h"
typedef struct _NRF24_packet_t {
uint32_t timestamp;
uint8_t packetsLost;
uint8_t rcvChannel;
uint8_t packet[MAX_RF_PAYLOAD_SIZE];
} NRF24_packet_t;
typedef struct _Serial_header_t {
unsigned long timestamp;
uint8_t packetsLost;
uint8_t address[RF_MAX_ADDR_WIDTH]; // MSB first, always RF_MAX_ADDR_WIDTH bytes.
} Serial_header_t;
// structs für Inverter und Kanalwerte
// Liste der Einheiten
enum UNITS {UNIT_V = 0, UNIT_HZ, UNIT_A, UNIT_W, UNIT_WH, UNIT_C, UNIT_KWH, UNIT_MA, UNIT_PCT};
const char* const units[] = {"V", "Hz", "A", "W", "Wh", "°C", "KWh", "mA", "%"};
// CH0 is default channel (freq, ac, temp)
enum CHANNELS {CH0 = 0, CH1, CH2, CH3, CH4};
enum CMDS {CMD01 = 0x01, CMD02, CMD03, CMD83 = 0x83, CMD84};
enum DIVS {DIV1 = 0, DIV10, DIV100, DIV1000};
#define BYTES2 2
#define BYTES4 4
const char UDC[] PROGMEM = "Udc";
const char IDC[] PROGMEM = "Idc";
const char PDC[] PROGMEM = "Pdc";
const char E_WOCHE[] PROGMEM = "E-Woche";
const char E_TOTAL[] PROGMEM = "E-Total";
const char E_TAG[] PROGMEM = "E-Tag";
const char UAC[] PROGMEM = "Uac";
const char FREQ[] PROGMEM = "Freq.ac";
const char PAC[] PROGMEM = "Pac";
const char E_HEUTE[] PROGMEM = "E-heute";
const char IPV[] PROGMEM = "Ipv";
const char WR_TEMP[] PROGMEM = "WR-Temp";
const char PERCNT[] PROGMEM = "Pct";
#define IDX_UDC 0
#define IDX_IDC 1
#define IDX_PDC 2
#define IDX_E_WOCHE 3
#define IDX_E_TOTAL 4
#define IDX_E_TAG 5
#define IDX_UAC 6
#define IDX_FREQ 7
#define IDX_PAC 8
#define IDX_E_HEUTE 9
#define IDX_IPV 10
#define IDX_WR_TEMP 11
#define IDX_PERCNT 12
const char* const NAMES[]
= {UDC, IDC, PDC, E_WOCHE, E_TOTAL, E_TAG, UAC, FREQ, PAC, E_HEUTE, IPV, WR_TEMP, PERCNT};
typedef float (*calcValueFunc)(float *);
struct measureDef_t {
uint8_t nameIdx; //const char* name; // Zeiger auf den Messwertnamen
uint8_t channel; // 0..4,
uint8_t unitIdx; // Index in die Liste der Einheiten 'units'
uint8_t teleId; // Telegramm ID, das was hinter der 2. WR Nummer im Telegramm, 02, 03, 83
uint8_t pos; // ab dieser POsition beginnt der Wert (Big Endian)
uint8_t bytes; // Anzahl der Bytes
uint8_t digits;
};
struct measureCalc_t {
uint8_t nameIdx; //const char* name; // Zeiger auf den Messwertnamen
uint8_t unitIdx; // Index in die Liste der Einheiten 'units'
uint8_t digits;
calcValueFunc f; // die Funktion zur Berechnung von Werten, zb Summe von Werten
};
struct inverter_t {
uint8_t ID; // Inverter-ID = Index
char name[20]; // Name des Inverters zb HM-600.1
uint64_t serialNo; // dier Seriennummer wie im Barcode auf dem WR, also 1141.....
uint64_t RadioId; // die gespiegelte (letzte 4 "Bytes") der Seriennummer
const measureDef_t *measureDef; // aus Include HMxxx.h : Liste mit Definitionen der Messwerte, wie Telgramm, offset, länge, ...
uint8_t anzMeasures; // Länge der Liste
measureCalc_t *measureCalculated; // Liste mit Defintion für berechnete Werte
uint8_t anzMeasureCalculated; // Länge der Liste
uint8_t anzTotalMeasures; // Gesamtanzahl Messwerte
float values[MAX_MEASURE_PER_INV]; // DIE Messewerte
};
char _buffer[20];
uint8_t anzInv = 0;
inverter_t inverters[MAX_ANZ_INV];
union longlongasbytes {
uint64_t ull;
uint32_t ul[2];
uint8_t bytes[8];
};
char *uint64toa (uint64_t s) {
//--------------------------------
//0x1141 72607952ULL
sprintf(_buffer, "%lX%08lX", (unsigned long)(s>>32), (unsigned long)(s&0xFFFFFFFFULL));
return _buffer;
}
uint64_t Serial2RadioID (uint64_t sn) {
//----------------------------------
longlongasbytes llsn;
longlongasbytes res;
llsn.ull = sn;
res.ull = 0;
res.bytes[4] = llsn.bytes[0];
res.bytes[3] = llsn.bytes[1];
res.bytes[2] = llsn.bytes[2];
res.bytes[1] = llsn.bytes[3];
res.bytes[0] = 0x01;
return res.ull;
}
void addInverter (uint8_t _ID, const char * _name, uint64_t _serial,
const measureDef_t * liste, int anzMeasure,
measureCalc_t * calcs, int anzMeasureCalculated) {
//-------------------------------------------------------------------------------------
if (anzInv >= MAX_ANZ_INV) {
DEBUG_OUT.println(F("ANZ_INV zu klein!"));
return;
}
inverter_t *p = &(inverters[anzInv]);
p->ID = _ID;
strcpy (p->name, _name);
p->serialNo = _serial;
p->RadioId = Serial2RadioID(_serial);
p->measureDef = liste;
p->anzMeasures = anzMeasure;
p->anzMeasureCalculated = anzMeasureCalculated;
p->measureCalculated = calcs;
p->anzTotalMeasures = anzMeasure + anzMeasureCalculated;
memset (p->values, 0, sizeof(p->values));
DEBUG_OUT.print (F("WR : ")); DEBUG_OUT.println(anzInv);
DEBUG_OUT.print (F("Type : ")); DEBUG_OUT.println(_name);
DEBUG_OUT.print (F("Serial : ")); DEBUG_OUT.println(uint64toa(_serial));
DEBUG_OUT.print (F("Radio-ID : ")); DEBUG_OUT.println(uint64toa(p->RadioId));
anzInv++;
}
static uint8_t toggle = 0; // nur für Test, ob's auch für mehere WR funzt
uint8_t findInverter (uint8_t *fourbytes) {
//---------------------------------------
for (uint8_t i = 0; i < anzInv; i++) {
longlongasbytes llb;
llb.ull = inverters[i].serialNo;
if (llb.bytes[3] == fourbytes[0] &&
llb.bytes[2] == fourbytes[1] &&
llb.bytes[1] == fourbytes[2] &&
llb.bytes[0] == fourbytes[3] )
{
return i;
//if (toggle) toggle = 0; else toggle = 1; return toggle; // Test ob mehr WR auch geht
}
}
return 0xFF; // nicht gefunden
}
char * error = {"error"};
char *getMeasureName (uint8_t wr, uint8_t i){
//------------------------------------------
inverter_t *p = &(inverters[wr]);
if (i >= p->anzTotalMeasures) return error;
uint8_t idx, channel = 0;
if (i < p->anzMeasures) {
idx = p->measureDef[i].nameIdx;
channel = p->measureDef[i].channel;
}
else {
idx = p->measureCalculated[i - p->anzMeasures].nameIdx;
}
char tmp[20];
strcpy_P (_buffer, NAMES[idx]);
if (channel) {
sprintf_P (tmp, PSTR(".CH%d"), channel);
strcat(_buffer,tmp);
}
return _buffer;
}
const char *getUnit (uint8_t wr, uint8_t i) {
//------------------------------------------
inverter_t *p = &(inverters[wr]);
if (i >= p->anzTotalMeasures) return error;
uint8_t idx;
if (i < p->anzMeasures)
idx = p->measureDef[i].unitIdx;
else
idx = p->measureCalculated[i-p->anzMeasures].unitIdx;
//strcpy (_buffer, units[i]);
//return _buffer;
return units[idx];
}
float getMeasureValue (uint8_t wr, uint8_t i) {
//------------------------------------------
if (i >= inverters[wr].anzTotalMeasures) return 0.0;
return inverters[wr].values[i];
}
int getDivisor (uint8_t wr, uint8_t i) {
//------------------------------------
inverter_t *p = &(inverters[wr]);
if (i >= p->anzTotalMeasures) return 1;
if (i < p->anzMeasures) {
uint8_t digits = p->measureDef[i].digits;
if (digits == DIV1) return 1;
if (digits == DIV10) return 10;
if (digits == DIV100) return 100;
if (digits == DIV1000) return 1000;
return 1;
}
else
return p->measureCalculated[i].digits;
}
uint8_t getDigits (uint8_t wr, uint8_t i) {
//---------------------------------------
inverter_t *p = &(inverters[wr]);
if (i >= p->anzTotalMeasures) return 0;
if (i < p->anzMeasures)
return p->measureDef[i].digits;
else
return p->measureCalculated[i-p->anzMeasures].digits;
}
// +++++++++++++++++++++++++++++++++++ Inverter ++++++++++++++++++++++++++++++++++++++++++++++
#include "HM600.h" // für HM-600 und HM-700
#include "HM1200.h"
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
void setupInverts() {
//-----------------
addInverter (0,"HM-600", 0x114172607952ULL,
hm600_measureDef, HM600_MEASURE_LIST_LEN, // Tabelle der Messwerte
hm600_measureCalc, HM600_CALCED_LIST_LEN); // Tabelle berechnete Werte
/*
addInverter (1,"HM-1200", 0x114172607952ULL,
hm1200_measureDef, HM1200_MEASURE_LIST_LEN, // Tabelle der Messwerte
hm1200_measureCalc, HM1200_CALCED_LIST_LEN); // Tabelle berechnete Werte
*/
}
#endif

151
tools/HoyDtuSim/ModWebserver.h

@ -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) + "&deg;" + 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

69
tools/HoyDtuSim/Settings.h

@ -0,0 +1,69 @@
#ifndef __SETTINGS_H
#define __SETTINGS_H
// Ausgabe von Debug Infos auf der seriellen Console
#define DEBUG
#define SER_BAUDRATE (115200)
#include "Debug.h"
// Ausgabe was gesendet wird; 0 oder 1
#define DEBUG_SEND 0
// soll zwichen den Sendekanälen 23, 40, 61, 75 ständig gewechselt werden
#define CHANNEL_HOP
// mit OTA Support, also update der Firmware über WLan mittels IP/update
#define WITH_OTA
// Hardware configuration
#ifdef ESP8266
#define RF1_CE_PIN (D4)
#define RF1_CS_PIN (D8)
#define RF1_IRQ_PIN (D3)
#else
#define RF1_CE_PIN (9)
#define RF1_CS_PIN (10)
#define RF1_IRQ_PIN (2)
#endif
// WR und DTU
#define RF_MAX_ADDR_WIDTH (5)
#define MAX_RF_PAYLOAD_SIZE (32)
#define DEFAULT_RF_DATARATE (RF24_250KBPS) // Datarate
#define USE_POOR_MAN_CHANNEL_HOPPING_RCV 1 // 0 = not use
#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL)
#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL)
#define MAX_ANZ_INV 2 // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#define MAX_MEASURE_PER_INV 25 // hier statisch, könnte auch dynamisch erzeugt werden, aber Overhead für dyn. Speicher?
// Webserver
#define WEBSERVER_PORT 80
// Time Server
//#define TIMESERVER_NAME "pool.ntp.org"
#define TIMESERVER_NAME "fritz.box" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#ifdef WITH_OTA
// OTA Einstellungen
#define UPDATESERVER_PORT WEBSERVER_PORT+1
#define UPDATESERVER_DIR "/update"
#define UPDATESERVER_USER "?????" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#define UPDATESERVER_PW "?????" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#endif
// internes WLan
// PREFIXE dienen dazu, die eigenen WLans (wenn mehrere) von fremden zu unterscheiden
// gehe hier davon aus, dass alle WLans das gleiche Passwort haben. Wenn nicht, dann mehre Passwörter hinterlegen
#define SSID_PREFIX1 "pre1" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#define SSID_PREFIX2 "pre2" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#define SSID_PASSWORD "?????????????????" // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
// zur Berechnung von Sonnenauf- und -untergang
#define geoBreite 49.2866 // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#define geoLaenge 7.3416 // <<<<<<<<<<<<<<<<<<<<<<<< anpassen
#endif

55
tools/HoyDtuSim/Sonne.h

@ -0,0 +1,55 @@
#ifndef __SONNE_H
#define __SONNE_H
#include "Settings.h"
#include "Debug.h"
long SunDown, SunUp;
void calcSunUpDown (time_t date) {
//SunUpDown res = new SunUpDown();
boolean isSummerTime = false; // TODO TimeZone.getDefault().inDaylightTime(new Date(date));
//- Bogenmass
double brad = geoBreite / 180.0 * PI;
// - Höhe Sonne -50 Bogenmin.
double h0 = -50.0 / 60.0 / 180.0 * PI;
//- Deklination dek, Tag des Jahres d0
int tage = 30 * month(date) - 30 + day(date);
double dek = 0.40954 * sin (0.0172 * (tage - 79.35));
double zh1 = sin (h0) - sin (brad) * sin(dek);
double zh2 = cos(brad) * cos(dek);
double zd = 12*acos (zh1/zh2) / PI;
double zgl = -0.1752 * sin (0.03343 * tage + 0.5474) - 0.134 * sin (0.018234 * tage - 0.1939);
//-Sonnenuntergang
double tsu = 12 + zd - zgl;
double su = (tsu + (15.0 - geoLaenge) / 15.0);
int std = (int)su;
int minute = (int) ((su - std)*60);
if (isSummerTime) std++;
SunDown = (100*std + minute) * 100;
//- Sonnenaufgang
double tsa = 12 - zd - zgl;
double sa = (tsa + (15.0 - geoLaenge) /15.0);
std = (int) sa;
minute = (int) ((sa - std)*60);
if (isSummerTime) std++;
SunUp = (100*std + minute) * 100;
DEBUG_OUT.print(F("Sonnenaufgang :")); DEBUG_OUT.println(SunUp);
DEBUG_OUT.print(F("Sonnenuntergang:")); DEBUG_OUT.println(SunDown);
}
boolean isDayTime() {
//-----------------
// 900 = 15 Minuten, vor Sonnenaufgang und nach -untergang
const int offset=60*15;
time_t no = getNow();
long jetztMinuteU = (100 * hour(no+offset) + minute(no+offset)) * 100;
long jetztMinuteO = (100 * hour(no-offset) + minute(no-offset)) * 100;
return ((jetztMinuteU >= SunUp) &&(jetztMinuteO <= SunDown));
}
#endif

102
tools/HoyDtuSim/hm_crc.h

@ -0,0 +1,102 @@
#ifndef __HM_CRC_H
#define __HM_CRC_H
#define BITS_TO_BYTES(x) (((x)+7)>>3)
#define BYTES_TO_BITS(x) ((x)<<3)
extern uint16_t crc16_modbus(uint8_t *puchMsg, uint16_t usDataLen);
extern uint8_t crc8(uint8_t *buf, const uint16_t bufLen);
extern uint16_t crc16(uint8_t* buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits);
//#define OUTPUT_DEBUG_INFO
#define CRC8_INIT 0x00
#define CRC8_POLY 0x01
#define CRC16_MODBUS_POLYNOM 0xA001
uint8_t crc8(uint8_t buf[], uint16_t len) {
uint8_t crc = CRC8_INIT;
for(uint8_t i = 0; i < len; i++) {
crc ^= buf[i];
for(uint8_t b = 0; b < 8; b ++) {
crc = (crc << 1) ^ ((crc & 0x80) ? CRC8_POLY : 0x00);
}
}
return crc;
}
uint16_t crc16_modbus(uint8_t buf[], uint16_t len) {
uint16_t crc = 0xffff;
uint8_t lsb;
for(uint8_t i = 0; i < len; i++) {
crc = crc ^ buf[i];
for(int8_t b = 7; b >= 0; b--) {
lsb = (crc & 0x0001);
if(lsb == 0x01)
crc--;
crc = crc >> 1;
if(lsb == 0x01)
crc = crc ^ CRC16_MODBUS_POLYNOM;
}
}
return crc;
}
// NRF24 CRC16 calculation with poly 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1
uint16_t crc16(uint8_t *buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits)
{
uint16_t crc = startCRC;
if ((len_bits > 0) && (len_bits <= BYTES_TO_BITS(bufLen)))
{
// The length of the data might not be a multiple of full bytes.
// Therefore we proceed over the data bit-by-bit (like the NRF24 does) to
// calculate the CRC.
uint16_t data;
uint8_t byte, shift;
uint16_t bitoffs = startBit;
// Get a new byte for the next 8 bits.
byte = buf[bitoffs >> 3];
#ifdef OUTPUT_DEBUG_INFO
printf_P(PSTR("\nStart CRC %04X, %u bits:"), startCRC, len_bits);
printf_P(PSTR("\nbyte %02X:"), byte);
#endif
while (bitoffs < len_bits + startBit)
{
shift = bitoffs & 7;
// Shift the active bit to the position of bit 15
data = ((uint16_t)byte) << (8 + shift);
#ifdef OUTPUT_DEBUG_INFO
printf_P(PSTR(" bit %u %u,"), shift, data & 0x8000 ? 1 : 0);
#endif
// Assure all other bits are 0
data &= 0x8000;
crc ^= data;
if (crc & 0x8000)
{
crc = (crc << 1) ^ 0x1021; // 0x1021 = (1) 0001 0000 0010 0001 = x^16+x^12+x^5+1
}
else
{
crc = (crc << 1);
}
++bitoffs;
if (0 == (bitoffs & 7))
{
// Get a new byte for the next 8 bits.
byte = buf[bitoffs >> 3];
#ifdef OUTPUT_DEBUG_INFO
printf_P(PSTR("crc %04X:"), crc);
if (bitoffs < len_bits + startBit)
printf_P(PSTR("\nbyte %02X:"), byte);
#endif
}
}
}
return crc;
}
#endif

93
tools/HoyDtuSim/hm_packets.h

@ -0,0 +1,93 @@
#ifndef __HM_PACKETS_H
#define __HM_PACKETS_H
#include "hm_crc.h"
class HM_Packets
{
private:
uint32_t unixTimeStamp;
void prepareBuffer(uint8_t *buf);
void copyToBuffer(uint8_t *buf, uint32_t val);
void copyToBufferBE(uint8_t *buf, uint32_t val);
public:
void SetUnixTimeStamp(uint32_t ts);
void UnixTimeStampTick();
int32_t GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr);
int32_t GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd);
};
void HM_Packets::SetUnixTimeStamp(uint32_t ts)
{
unixTimeStamp = ts;
}
void HM_Packets::UnixTimeStampTick()
{
unixTimeStamp++;
}
void HM_Packets::prepareBuffer(uint8_t *buf)
{
// minimal buffer size of 32 bytes is assumed
memset(buf, 0x00, 32);
}
void HM_Packets::copyToBuffer(uint8_t *buf, uint32_t val)
{
buf[0]= (uint8_t)(val >> 24);
buf[1]= (uint8_t)(val >> 16);
buf[2]= (uint8_t)(val >> 8);
buf[3]= (uint8_t)(val & 0xFF);
}
void HM_Packets::copyToBufferBE(uint8_t *buf, uint32_t val)
{
memcpy(buf, &val, sizeof(uint32_t));
}
static uint8_t cid = 0;
int32_t HM_Packets::GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr)
{
prepareBuffer(buf);
buf[0] = 0x15;
copyToBufferBE(&buf[1], wrAdr);
copyToBufferBE(&buf[5], dtuAdr);
buf[9] = 0x80;
buf[10] = 0x0B; //0x0B; 0x03 0x11
buf[11] = 0x00;
copyToBuffer(&buf[12], unixTimeStamp);
buf[19] = 0x05;
// CRC16
uint16_t crc16 = crc16_modbus(&buf[10], 14);
buf[24] = crc16 >> 8;
buf[25] = crc16 & 0xFF;
// crc8
buf[26] = crc8(&buf[0], 26);
return 27;
}
int32_t HM_Packets::GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd)
{
buf[0] = mid;
copyToBufferBE(&buf[1], wrAdr);
copyToBufferBE(&buf[5], dtuAdr);
buf[9] = cmd;
// crc8
buf[10] = crc8(&buf[0], 10);
return 11;
}
#endif

345
tools/HoyDtuSim/wifi.h

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

158
tools/NRF24_SendRcv/CircularBuffer.h

@ -0,0 +1,158 @@
/*
CircularBuffer - An Arduino circular buffering library for arbitrary types.
Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef CircularBuffer_h
#define CircularBuffer_h
#ifdef ESP8266
#define DISABLE_IRQ noInterrupts()
#define RESTORE_IRQ interrupts()
#else
#define DISABLE_IRQ \
uint8_t sreg = SREG; \
cli();
#define RESTORE_IRQ \
SREG = sreg;
#endif
template <class 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

23
tools/NRF24_SendRcv/Debug.h

@ -0,0 +1,23 @@
#ifndef __DEBUG_H
#define __DEBUG_H
#ifdef DEBUG
#define DEBUG_OUT Serial
#else
//---
// disable Serial DEBUG output
#define DEBUG_OUT DummySerial
static class {
public:
void begin(...) {}
void print(...) {}
void println(...) {}
void flush() {}
bool available() { return false;}
int readBytes(...) { return 0;}
int printf (...) {return 0;}
} DummySerial;
#endif
#endif

129
tools/NRF24_SendRcv/ModWebserver.h

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

597
tools/NRF24_SendRcv/NRF24_SendRcv.ino

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

55
tools/NRF24_SendRcv/NRF24_sniff_types.h

@ -0,0 +1,55 @@
/*
This file is part of NRF24_Sniff.
Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl
NRF24_Sniff is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NRF24_Sniff is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with NRF24_Sniff. If not, see <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

82
tools/NRF24_SendRcv/Settings.h

@ -0,0 +1,82 @@
#ifndef __SETTINGS_H
#define __SETTINGS_H
// Ausgabe von Debug Infos auf der seriellen Console
#define DEBUG
#define SER_BAUDRATE (115200)
// Ausgabe was gesendet wird; 0 oder 1
#define DEBUG_SEND 0
// soll zwichen den Sendekanälen 23, 40, 61, 75 ständig gewechselt werden
#define CHANNEL_HOP
// mit OTA Support, also update der Firmware über WLan mittels IP/update
#define WITH_OTA
// Hardware configuration
#ifdef ESP8266
#define RF1_CE_PIN (D4)
#define RF1_CS_PIN (D8)
#define RF1_IRQ_PIN (D3)
#else
#define RF1_CE_PIN (9)
#define RF1_CS_PIN (10)
#define RF1_IRQ_PIN (2)
#endif
union longlongasbytes {
uint64_t ull;
uint8_t bytes[8];
};
uint64_t Serial2RadioID (uint64_t sn) {
//----------------------------------
longlongasbytes llsn;
longlongasbytes res;
llsn.ull = sn;
res.ull = 0;
res.bytes[4] = llsn.bytes[0];
res.bytes[3] = llsn.bytes[1];
res.bytes[2] = llsn.bytes[2];
res.bytes[1] = llsn.bytes[3];
res.bytes[0] = 0x01;
return res.ull;
}
// WR und DTU
#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL)
#define SerialWR 0x114172607952ULL // <<<<<<<<<<<<<<<<<<<<<<< anpassen
uint64_t WR1_RADIO_ID = Serial2RadioID (SerialWR); // ((uint64_t)0x5279607201ULL);
#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL)
// Webserver
#define WEBSERVER_PORT 80
// Time Server
//#define TIMESERVER_NAME "pool.ntp.org"
#define TIMESERVER_NAME "fritz.box"
#ifdef WITH_OTA
// OTA Einstellungen
#define UPDATESERVER_PORT WEBSERVER_PORT+1
#define UPDATESERVER_DIR "/update" // mittels IP:81/update kommt man dann auf die OTA-Seite
#define UPDATESERVER_USER "username_für_OTA" // <<<<<<<<<<<<<<<<<<<<<<< anpassen
#define UPDATESERVER_PW "passwort_für_OTA" // <<<<<<<<<<<<<<<<<<<<<<< anpassen
#endif
// internes WLan
// PREFIXE dienen dazu, die eigenen WLans (wenn mehrere) vonfremden zu unterscheiden
// gehe hier davon aus, dass alle WLans das gleiche Passwort haben. Wenn nicht, dann mehre Passwörter hinterlegen
#define SSID_PREFIX1 "wlan1-Prefix" // <<<<<<<<<<<<<<<<<<<<<<< anpassen
#define SSID_PREFIX2 "wlan2-Prefix" // <<<<<<<<<<<<<<<<<<<<<<< anpassen
#define SSID_PASSWORD "wlan-passwort" // <<<<<<<<<<<<<<<<<<<<<<< anpassen
// zur Berechnung von Sonnenauf- und -untergang
#define geoBreite 49.2866
#define geoLaenge 7.3416
#endif

55
tools/NRF24_SendRcv/Sonne.h

@ -0,0 +1,55 @@
#ifndef __SONNE_H
#define __SONNE_H
#include "Settings.h"
#include "Debug.h"
long SunDown, SunUp;
void calcSunUpDown (time_t date) {
//SunUpDown res = new SunUpDown();
boolean isSummerTime = false; // TODO TimeZone.getDefault().inDaylightTime(new Date(date));
//- Bogenma�
double brad = geoBreite / 180.0 * PI;
// - H�he Sonne -50 Bogenmin.
double h0 = -50.0 / 60.0 / 180.0 * PI;
//- Deklination dek, Tag des Jahres d0
int tage = 30 * month(date) - 30 + day(date);
double dek = 0.40954 * sin (0.0172 * (tage - 79.35));
double zh1 = sin (h0) - sin (brad) * sin(dek);
double zh2 = cos(brad) * cos(dek);
double zd = 12*acos (zh1/zh2) / PI;
double zgl = -0.1752 * sin (0.03343 * tage + 0.5474) - 0.134 * sin (0.018234 * tage - 0.1939);
//-Sonnenuntergang
double tsu = 12 + zd - zgl;
double su = (tsu + (15.0 - geoLaenge) / 15.0);
int std = (int)su;
int minute = (int) ((su - std)*60);
if (isSummerTime) std++;
SunDown = (100*std + minute) * 100;
//- Sonnenaufgang
double tsa = 12 - zd - zgl;
double sa = (tsa + (15.0 - geoLaenge) /15.0);
std = (int) sa;
minute = (int) ((sa - std)*60);
if (isSummerTime) std++;
SunUp = (100*std + minute) * 100;
DEBUG_OUT.print("Sonnenaufgang :"); DEBUG_OUT.println(SunUp);
DEBUG_OUT.print("Sonnenuntergang:"); DEBUG_OUT.println(SunDown);
}
boolean isDayTime() {
//-----------------
// 900 = 15 Minuten, vor Sonnenaufgang und nach -untergang
const int offset=60*15;
time_t no = getNow();
long jetztMinuteU = (100 * hour(no+offset) + minute(no+offset)) * 100;
long jetztMinuteO = (100 * hour(no-offset) + minute(no-offset)) * 100;
return ((jetztMinuteU >= SunUp) &&(jetztMinuteO <= SunDown));
}
#endif

142
tools/NRF24_SendRcv/hm_crc.cpp

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

8
tools/NRF24_SendRcv/hm_crc.h

@ -0,0 +1,8 @@
#define BITS_TO_BYTES(x) (((x)+7)>>3)
#define BYTES_TO_BITS(x) ((x)<<3)
extern uint16_t crc16_modbus(uint8_t *puchMsg, uint16_t usDataLen);
extern uint8_t crc8(uint8_t *buf, const uint16_t bufLen);
extern uint16_t crc16(uint8_t* buf, const uint16_t bufLen, const uint16_t startCRC, const uint16_t startBit, const uint16_t len_bits);

74
tools/NRF24_SendRcv/hm_packets.cpp

@ -0,0 +1,74 @@
#include "Arduino.h"
#include "hm_crc.h"
#include "hm_packets.h"
void HM_Packets::SetUnixTimeStamp(uint32_t ts)
{
unixTimeStamp = ts;
}
void HM_Packets::UnixTimeStampTick()
{
unixTimeStamp++;
}
void HM_Packets::prepareBuffer(uint8_t *buf)
{
// minimal buffer size of 32 bytes is assumed
memset(buf, 0x00, 32);
}
void HM_Packets::copyToBuffer(uint8_t *buf, uint32_t val)
{
buf[0]= (uint8_t)(val >> 24);
buf[1]= (uint8_t)(val >> 16);
buf[2]= (uint8_t)(val >> 8);
buf[3]= (uint8_t)(val & 0xFF);
}
void HM_Packets::copyToBufferBE(uint8_t *buf, uint32_t val)
{
memcpy(buf, &val, sizeof(uint32_t));
}
int32_t HM_Packets::GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr)
{
prepareBuffer(buf);
buf[0] = 0x15;
copyToBufferBE(&buf[1], wrAdr);
copyToBufferBE(&buf[5], dtuAdr);
buf[9] = 0x80;
buf[10] = 0x0B; // cid
buf[11] = 0x00;
copyToBuffer(&buf[12], unixTimeStamp);
buf[19] = 0x05;
// CRC16
uint16_t crc16 = crc16_modbus(&buf[10], 14);
buf[24] = crc16 >> 8;
buf[25] = crc16 & 0xFF;
// crc8
buf[26] = crc8(&buf[0], 26);
return 27;
}
int32_t HM_Packets::GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd)
{
buf[0] = mid;
copyToBufferBE(&buf[1], wrAdr);
copyToBufferBE(&buf[5], dtuAdr);
buf[9] = cmd;
// crc8
buf[10] = crc8(&buf[0], 10);
return 11;
}

18
tools/NRF24_SendRcv/hm_packets.h

@ -0,0 +1,18 @@
class HM_Packets
{
private:
uint32_t unixTimeStamp;
void prepareBuffer(uint8_t *buf);
void copyToBuffer(uint8_t *buf, uint32_t val);
void copyToBufferBE(uint8_t *buf, uint32_t val);
public:
void SetUnixTimeStamp(uint32_t ts);
void UnixTimeStampTick();
int32_t GetTimePacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr);
int32_t GetCmdPacket(uint8_t *buf, uint32_t wrAdr, uint32_t dtuAdr, uint8_t mid, uint8_t cmd);
};

345
tools/NRF24_SendRcv/wifi.h

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

157
tools/esp8266/CircularBuffer.h

@ -0,0 +1,157 @@
/*
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 BUFFERTYPE, uint8_t BUFFERSIZE>
class CircularBuffer {
typedef BUFFERTYPE BufferType;
BufferType Buffer[BUFFERSIZE];
public:
CircularBuffer() : m_buff(Buffer) {
m_size = BUFFERSIZE;
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.
*/
BUFFERTYPE* getFront(void) const
{
DISABLE_IRQ;
BUFFERTYPE* 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(BUFFERTYPE* record)
{
bool ok = false;
DISABLE_IRQ;
if (!full())
{
BUFFERTYPE* 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.
*/
BUFFERTYPE* getBack(void) const
{
BUFFERTYPE* 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 BUFFERTYPE * 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;
}
uint8_t m_size; // Total number of records that can be stored in the buffer.
BUFFERTYPE* const m_buff;
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

60
tools/esp8266/README.md

@ -0,0 +1,60 @@
## OVERVIEW
This code is intended to run on a Wemos D1mini or similar. The code is based on 'Hubi's code, which can be found here: <https://www.mikrocontroller.net/topic/525778?page=3#7033371>
The NRF24L01+ radio module is connected to the standard SPI pins. Additional there are 3 pins, which can be set individual: CS, CE and IRQ
These pins can be changed from the /setup URL
## Compile
This code can be compiled using Arduino. The settings were:
- Board: Generic ESP8266 Module
- Flash-Size: 1MB (FS: none, OTA: 502kB)
### Optional Configuration before compilation
- number of supported inverters (set to 3 by default) `defines.h`
- enable channel hopping `hmRadio.h`
- DTU radio id `hmRadio.h`
- unformated list in webbrowser `/livedata` `defines.h`, `LIVEDATA_VISUALIZED`
## Flash ESP with firmware
1. flash the ESP with the compiled firmware using the UART pins or any preinstalled firmware with OTA capabilities
2. repower the ESP
3. the ESP will start as access point (AP) if there is no network config stored in its eeprom
4. connect to the AP, you will be forwarded to the setup page
5. configure your WiFi settings, save, repower
6. check your router or serial console for the IP address of the module. You can try ping the configured device name as well.
## Usage
Connect the ESP to power and to your serial console (optional). The webinterface has the following abilities:
- OTA Update (over the air update)
- Configuration (Wifi, inverter(s), Pinout, MQTT)
- visual display of the connected inverters / modules
- some statistics about communication (debug)
The serial console will print the converted values which were read out of the inverter(s)
## Compatiblity
For now the following inverters should work out of the box:
- HM400
- HM600
- HM800
- HM1200
## USED LIBRARIES
- `Time`
- `RF24`
- `PubSubClient`

611
tools/esp8266/app.cpp

@ -0,0 +1,611 @@
#include "app.h"
#include "html/h/index_html.h"
#include "html/h/setup_html.h"
#include "html/h/hoymiles_html.h"
//-----------------------------------------------------------------------------
app::app() : Main() {
mSendTicker = 0xffff;
mSendInterval = 0;
mMqttTicker = 0xffff;
mMqttInterval = 0;
mSerialTicker = 0xffff;
mSerialInterval = 0;
mMqttActive = false;
mTicker = 0;
mShowRebootRequest = false;
mSerialValues = true;
mSerialDebug = false;
memset(mCmds, 0, sizeof(uint32_t)*DBG_CMD_LIST_LEN);
//memset(mChannelStat, 0, sizeof(uint32_t) * 4);
mSys = new HmSystemType();
}
//-----------------------------------------------------------------------------
app::~app(void) {
}
//-----------------------------------------------------------------------------
void app::setup(uint32_t timeout) {
Main::setup(timeout);
mWeb->on("/", std::bind(&app::showIndex, this));
mWeb->on("/setup", std::bind(&app::showSetup, this));
mWeb->on("/save", std::bind(&app::showSave, this));
mWeb->on("/erase", std::bind(&app::showErase, this));
mWeb->on("/cmdstat", std::bind(&app::showStatistics, this));
mWeb->on("/hoymiles", std::bind(&app::showHoymiles, this));
mWeb->on("/livedata", std::bind(&app::showLiveData, this));
if(mSettingsValid) {
uint64_t invSerial;
char invName[MAX_NAME_LENGTH + 1] = {0};
uint8_t invType;
// inverter
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial);
mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), invName, MAX_NAME_LENGTH);
mEep->read(ADDR_INV_TYPE + i, &invType);
if(0ULL != invSerial) {
mSys->addInverter(invName, invSerial, invType);
DPRINTLN("add inverter: " + String(invName) + ", SN: " + String(invSerial, HEX) + ", type: " + String(invType));
}
}
mEep->read(ADDR_INV_INTERVAL, &mSendInterval);
if(mSendInterval < 1)
mSendInterval = 1;
// pinout
mEep->read(ADDR_PINOUT, &mSys->Radio.pinCs);
mEep->read(ADDR_PINOUT+1, &mSys->Radio.pinCe);
mEep->read(ADDR_PINOUT+2, &mSys->Radio.pinIrq);
// nrf24 amplifier power
mEep->read(ADDR_RF24_AMP_PWR, &mSys->Radio.AmplifierPower);
// serial console
uint8_t tmp;
mEep->read(ADDR_SER_INTERVAL, &mSerialInterval);
mEep->read(ADDR_SER_ENABLE, &tmp);
mSerialValues = (tmp == 0x01);
mEep->read(ADDR_SER_DEBUG, &tmp);
mSerialDebug = (tmp == 0x01);
if(mSerialInterval < 1)
mSerialInterval = 1;
// mqtt
uint8_t mqttAddr[MQTT_ADDR_LEN];
uint16_t mqttPort;
char mqttUser[MQTT_USER_LEN];
char mqttPwd[MQTT_PWD_LEN];
char mqttTopic[MQTT_TOPIC_LEN];
mEep->read(ADDR_MQTT_ADDR, mqttAddr, MQTT_ADDR_LEN);
mEep->read(ADDR_MQTT_USER, mqttUser, MQTT_USER_LEN);
mEep->read(ADDR_MQTT_PWD, mqttPwd, MQTT_PWD_LEN);
mEep->read(ADDR_MQTT_TOPIC, mqttTopic, MQTT_TOPIC_LEN);
mEep->read(ADDR_MQTT_INTERVAL, &mMqttInterval);
mEep->read(ADDR_MQTT_PORT, &mqttPort);
char addr[16] = {0};
sprintf(addr, "%d.%d.%d.%d", mqttAddr[0], mqttAddr[1], mqttAddr[2], mqttAddr[3]);
mMqttActive = (mqttAddr[0] > 0);
if(mMqttInterval < 1)
mMqttInterval = 1;
mMqtt.setup(addr, mqttTopic, mqttUser, mqttPwd, mqttPort);
mMqttTicker = 0;
mSerialTicker = 0;
mMqtt.sendMsg("version", mVersion);
}
mSys->setup();
if(!mWifiSettingsValid)
DPRINTLN("Warn: your settings are not valid! check [IP]/setup");
else {
DPRINTLN("\n\n----------------------------------------");
DPRINTLN("Welcome to AHOY!");
DPRINT("\npoint your browser to http://");
DPRINTLN(WiFi.localIP());
DPRINTLN("to configure your device");
DPRINTLN("----------------------------------------\n");
}
}
//-----------------------------------------------------------------------------
void app::loop(void) {
Main::loop();
if(!mSys->BufCtrl.empty()) {
uint8_t len, rptCnt;
packet_t *p = mSys->BufCtrl.getBack();
if(mSerialDebug)
mSys->Radio.dumpBuf("RAW ", p->packet, MAX_RF_PAYLOAD_SIZE);
if(mSys->Radio.checkCrc(p->packet, &len, &rptCnt)) {
// process buffer only on first occurrence
if((0 != len) && (0 == rptCnt)) {
uint8_t *cmd = &p->packet[11];
//DPRINTLN("CMD " + String(*cmd, HEX));
//mSys->Radio.dumpBuf("Payload ", p->packet, len);
Inverter<> *iv = mSys->findInverter(&p->packet[3]);
if(NULL != iv) {
for(uint8_t i = 0; i < iv->listLen; i++) {
if(iv->assign[i].cmdId == *cmd)
iv->addValue(i, &p->packet[11]);
}
iv->doCalculations();
}
if(*cmd == 0x01) mCmds[0]++;
else if(*cmd == 0x02) mCmds[1]++;
else if(*cmd == 0x03) mCmds[2]++;
else if(*cmd == 0x81) mCmds[3]++;
else if(*cmd == 0x82) mCmds[4]++;
else if(*cmd == 0x83) mCmds[5]++;
else if(*cmd == 0x84) mCmds[6]++;
else mCmds[7]++;
/*if(p->sendCh == 23) mChannelStat[0]++;
else if(p->sendCh == 40) mChannelStat[1]++;
else if(p->sendCh == 61) mChannelStat[2]++;
else mChannelStat[3]++;*/
}
}
mSys->BufCtrl.popBack();
}
if(checkTicker(&mTicker, 1000)) {
if(++mSendTicker >= mSendInterval) {
mSendTicker = 0;
Inverter<> *inv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
inv = mSys->getInverterByPos(i);
if(NULL != inv) {
mSys->Radio.sendTimePacket(inv->radioId.u64, mTimestamp);
yield();
}
}
}
if(mMqttActive) {
mMqtt.loop();
if(++mMqttTicker > mMqttInterval) {
mMqttInterval = 0;
mMqtt.isConnected(true);
char topic[30], val[10];
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
for(uint8_t i = 0; i < iv->listLen; i++) {
if(0.0f != iv->getValue(i)) {
snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, fields[iv->assign[i].fieldId]);
snprintf(val, 10, "%.3f", iv->getValue(i));
mMqtt.sendMsg(topic, val);
yield();
}
}
}
}
}
}
if(mSerialValues) {
if(++mSerialTicker > mSerialInterval) {
mSerialInterval = 0;
char topic[30], val[10];
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
for(uint8_t i = 0; i < iv->listLen; i++) {
if(0.0f != iv->getValue(i)) {
snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, iv->getFieldName(i));
snprintf(val, 10, "%.3f %s", iv->getValue(i), iv->getUnit(i));
DPRINTLN(String(topic) + ": " + String(val));
}
yield();
}
}
}
}
}
}
}
//-----------------------------------------------------------------------------
void app::handleIntr(void) {
mSys->Radio.handleIntr();
}
//-----------------------------------------------------------------------------
void app::showIndex(void) {
String html = FPSTR(index_html);
html.replace("{DEVICE}", mDeviceName);
html.replace("{VERSION}", mVersion);
mWeb->send(200, "text/html", html);
}
//-----------------------------------------------------------------------------
void app::showSetup(void) {
// overrides same method in main.cpp
uint16_t interval;
String html = FPSTR(setup_html);
html.replace("{SSID}", mStationSsid);
// PWD will be left at the default value (for protection)
// -> the PWD will only be changed if it does not match the placeholder "{PWD}"
html.replace("{DEVICE}", String(mDeviceName));
html.replace("{VERSION}", String(mVersion));
if(mApActive)
html.replace("{IP}", String("http://192.168.1.1"));
else
html.replace("{IP}", ("http://" + String(WiFi.localIP().toString())));
String inv;
uint64_t invSerial;
char invName[MAX_NAME_LENGTH + 1] = {0};
uint8_t invType;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial);
mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), invName, MAX_NAME_LENGTH);
mEep->read(ADDR_INV_TYPE + i, &invType);
inv += "<p class=\"subdes\">Inverter "+ String(i) + "</p>";
inv += "<label for=\"inv" + String(i) + "Addr\">Address</label>";
inv += "<input type=\"text\" class=\"text\" name=\"inv" + String(i) + "Addr\" value=\"";
if(0ULL != invSerial)
inv += String(invSerial, HEX);
inv += "\"/ maxlength=\"12\">";
inv += "<label for=\"inv" + String(i) + "Name\">Name</label>";
inv += "<input type=\"text\" class=\"text\" name=\"inv" + String(i) + "Name\" value=\"";
inv += String(invName);
inv += "\"/ maxlength=\"" + String(MAX_NAME_LENGTH) + "\">";
inv += "<label for=\"inv" + String(i) + "Type\">Type</label>";
inv += "<select name=\"inv" + String(i) + "Type\">";
for(uint8_t t = 0; t < NUM_INVERTER_TYPES; t++) {
inv += "<option value=\"" + String(t) + "\"";
if(invType == t)
inv += " selected";
inv += ">" + String(invTypes[t]) + "</option>";
}
inv += "</select>";
}
html.replace("{INVERTERS}", String(inv));
// pinout
String pinout;
for(uint8_t i = 0; i < 3; i++) {
pinout += "<label for=\"" + String(pinArgNames[i]) + "\">" + String(pinNames[i]) + "</label>";
pinout += "<select name=\"" + String(pinArgNames[i]) + "\">";
for(uint8_t j = 0; j <= 16; j++) {
pinout += "<option value=\"" + String(j) + "\"";
switch(i) {
default: if(j == mSys->Radio.pinCs) pinout += " selected"; break;
case 1: if(j == mSys->Radio.pinCe) pinout += " selected"; break;
case 2: if(j == mSys->Radio.pinIrq) pinout += " selected"; break;
}
pinout += ">" + String(wemosPins[j]) + "</option>";
}
pinout += "</select>";
}
html.replace("{PINOUT}", String(pinout));
// nrf24l01+
String rf24;
for(uint8_t i = 0; i <= 3; i++) {
rf24 += "<option value=\"" + String(i) + "\"";
if(i == mSys->Radio.AmplifierPower)
rf24 += " selected";
rf24 += ">" + String(rf24AmpPower[i]) + "</option>";
}
html.replace("{RF24}", String(rf24));
if(mSettingsValid) {
mEep->read(ADDR_INV_INTERVAL, &interval);
html.replace("{INV_INTVL}", String(interval));
uint8_t tmp;
mEep->read(ADDR_SER_INTERVAL, &interval);
mEep->read(ADDR_SER_ENABLE, &tmp);
html.replace("{SER_INTVL}", String(interval));
html.replace("{SER_VAL_CB}", (tmp == 0x01) ? "checked" : "");
mEep->read(ADDR_SER_DEBUG, &tmp);
html.replace("{SER_DBG_CB}", (tmp == 0x01) ? "checked" : "");
uint8_t mqttAddr[MQTT_ADDR_LEN] = {0};
uint16_t mqttPort;
mEep->read(ADDR_MQTT_ADDR, mqttAddr, MQTT_ADDR_LEN);
mEep->read(ADDR_MQTT_INTERVAL, &interval);
mEep->read(ADDR_MQTT_PORT, &mqttPort);
char addr[16] = {0};
sprintf(addr, "%d.%d.%d.%d", mqttAddr[0], mqttAddr[1], mqttAddr[2], mqttAddr[3]);
html.replace("{MQTT_ADDR}", String(addr));
html.replace("{MQTT_PORT}", String(mqttPort));
html.replace("{MQTT_USER}", String(mMqtt.getUser()));
html.replace("{MQTT_PWD}", String(mMqtt.getPwd()));
html.replace("{MQTT_TOPIC}", String(mMqtt.getTopic()));
html.replace("{MQTT_INTVL}", String(interval));
}
else {
html.replace("{INV_INTVL}", "5");
html.replace("{SER_VAL_CB}", "checked");
html.replace("{SER_DBG_CB}", "");
html.replace("{SER_INTVL}", "10");
html.replace("{MQTT_ADDR}", "");
html.replace("{MQTT_PORT}", "1883");
html.replace("{MQTT_USER}", "");
html.replace("{MQTT_PWD}", "");
html.replace("{MQTT_TOPIC}", "/inverter");
html.replace("{MQTT_INTVL}", "10");
html.replace("{SER_INTVL}", "10");
}
mWeb->send(200, "text/html", html);
}
//-----------------------------------------------------------------------------
void app::showSave(void) {
saveValues(true);
}
//-----------------------------------------------------------------------------
void app::showErase() {
eraseSettings();
showReboot();
}
//-----------------------------------------------------------------------------
void app::showStatistics(void) {
String content = "CMDs:\n";
for(uint8_t i = 0; i < DBG_CMD_LIST_LEN; i ++) {
content += String("0x") + String(dbgCmds[i], HEX) + String(": ") + String(mCmds[i]) + String("\n");
}
content += String("other: ") + String(mCmds[DBG_CMD_LIST_LEN]) + String("\n\n");
content += "Send Cnt: " + String(mSys->Radio.mSendCnt) + String("\n\n");
/*content += "\nCHANNELs:\n";
content += String("23: ") + String(mChannelStat[0]) + String("\n");
content += String("40: ") + String(mChannelStat[1]) + String("\n");
content += String("61: ") + String(mChannelStat[2]) + String("\n");
content += String("75: ") + String(mChannelStat[3]) + String("\n");*/
if(!mSys->Radio.isChipConnected())
content += "WARNING! your NRF24 module can't be reached, check the wiring and pinout (<a href=\"/setup\">setup</a>)\n";
if(mShowRebootRequest)
content += "INFO: reboot your ESP to apply all your configuration changes!\n";
if(!mSettingsValid)
content += "INFO: your settings are invalid, please switch to <a href=\"/setup\">setup</a> to correct this.\n";
content += "MQTT: ";
if(!mMqtt.isConnected())
content += "not ";
content += "connected\n";
mWeb->send(200, "text/plain", content);
}
//-----------------------------------------------------------------------------
void app::showHoymiles(void) {
String html = FPSTR(hoymiles_html);
html.replace("{DEVICE}", mDeviceName);
html.replace("{VERSION}", mVersion);
mWeb->send(200, "text/html", html);
}
//-----------------------------------------------------------------------------
void app::showLiveData(void) {
String modHtml;
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
#ifdef LIVEDATA_VISUALIZED
uint8_t modNum, pos;
switch(iv->type) {
default: modNum = 1; break;
case INV_TYPE_HM600:
case INV_TYPE_HM800: modNum = 2; break;
case INV_TYPE_HM1200: modNum = 4; break;
}
modHtml += "<div class=\"iv\">";
modHtml += "<div class=\"ch-iv\"><span class=\"head\">" + String(iv->name) + "</span>";
uint8_t list[8] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PCT, FLD_T, FLD_YT, FLD_YD};
for(uint8_t fld = 0; fld < 8; fld++) {
pos = (iv->getPosByChFld(CH0, list[fld]));
if(0xff != pos) {
modHtml += "<div class=\"subgrp\">";
modHtml += "<span class=\"value\">" + String(iv->getValue(pos));
modHtml += "<span class=\"unit\">" + String(iv->getUnit(pos)) + "</span></span>";
modHtml += "<span class=\"info\">" + String(iv->getFieldName(pos)) + "</span>";
modHtml += "</div>";
}
}
modHtml += "</div>";
for(uint8_t ch = 1; ch <= modNum; ch ++) {
modHtml += "<div class=\"ch\"><span class=\"head\">CHANNEL " + String(ch) + "</span>";
for(uint8_t j = 0; j < 5; j++) {
switch(j) {
default: pos = (iv->getPosByChFld(ch, FLD_UDC)); break;
case 1: pos = (iv->getPosByChFld(ch, FLD_IDC)); break;
case 2: pos = (iv->getPosByChFld(ch, FLD_PDC)); break;
case 3: pos = (iv->getPosByChFld(ch, FLD_YD)); break;
case 4: pos = (iv->getPosByChFld(ch, FLD_YT)); break;
}
if(0xff != pos) {
modHtml += "<span class=\"value\">" + String(iv->getValue(pos));
modHtml += "<span class=\"unit\">" + String(iv->getUnit(pos)) + "</span></span>";
modHtml += "<span class=\"info\">" + String(iv->getFieldName(pos)) + "</span>";
}
}
modHtml += "</div>";
}
modHtml += "</div>";
#else
// dump all data to web frontend
modHtml = "<pre>";
char topic[30], val[10];
for(uint8_t i = 0; i < iv->listLen; i++) {
snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, iv->getFieldName(i));
snprintf(val, 10, "%.3f %s", iv->getValue(i), iv->getUnit(i));
modHtml += String(topic) + ": " + String(val) + "\n";
}
modHtml += "</pre>";
#endif
}
}
mWeb->send(200, "text/html", modHtml);
}
//-----------------------------------------------------------------------------
void app::saveValues(bool webSend = true) {
Main::saveValues(false); // general configuration
if(mWeb->args() > 0) {
char *p;
char buf[20] = {0};
uint8_t i = 0;
uint16_t interval;
// inverter
serial_u addr;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
// address
mWeb->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
if(strlen(buf) == 0)
snprintf(buf, 20, "\0");
addr.u64 = Serial2u64(buf);
mEep->write(ADDR_INV_ADDR + (i * 8), addr.u64);
// name
mWeb->arg("inv" + String(i) + "Name").toCharArray(buf, 20);
mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), buf, MAX_NAME_LENGTH);
// type
mWeb->arg("inv" + String(i) + "Type").toCharArray(buf, 20);
uint8_t type = atoi(buf);
mEep->write(ADDR_INV_TYPE + i, type);
}
interval = mWeb->arg("invInterval").toInt();
mEep->write(ADDR_INV_INTERVAL, interval);
// pinout
for(uint8_t i = 0; i < 3; i ++) {
uint8_t pin = mWeb->arg(String(pinArgNames[i])).toInt();
mEep->write(ADDR_PINOUT + i, pin);
}
// nrf24 amplifier power
mSys->Radio.AmplifierPower = mWeb->arg("rf24Power").toInt() & 0x03;
mEep->write(ADDR_RF24_AMP_PWR, mSys->Radio.AmplifierPower);
// mqtt
uint8_t mqttAddr[MQTT_ADDR_LEN] = {0};
uint16_t mqttPort;
char mqttUser[MQTT_USER_LEN];
char mqttPwd[MQTT_PWD_LEN];
char mqttTopic[MQTT_TOPIC_LEN];
mWeb->arg("mqttAddr").toCharArray(buf, 20);
i = 0;
p = strtok(buf, ".");
while(NULL != p) {
mqttAddr[i++] = atoi(p);
p = strtok(NULL, ".");
}
mWeb->arg("mqttUser").toCharArray(mqttUser, MQTT_USER_LEN);
mWeb->arg("mqttPwd").toCharArray(mqttPwd, MQTT_PWD_LEN);
mWeb->arg("mqttTopic").toCharArray(mqttTopic, MQTT_TOPIC_LEN);
interval = mWeb->arg("mqttIntvl").toInt();
mqttPort = mWeb->arg("mqttPort").toInt();
mEep->write(ADDR_MQTT_ADDR, mqttAddr, MQTT_ADDR_LEN);
mEep->write(ADDR_MQTT_PORT, mqttPort);
mEep->write(ADDR_MQTT_USER, mqttUser, MQTT_USER_LEN);
mEep->write(ADDR_MQTT_PWD, mqttPwd, MQTT_PWD_LEN);
mEep->write(ADDR_MQTT_TOPIC, mqttTopic, MQTT_TOPIC_LEN);
mEep->write(ADDR_MQTT_INTERVAL, interval);
// serial console
bool tmp;
interval = mWeb->arg("serIntvl").toInt();
mEep->write(ADDR_SER_INTERVAL, interval);
tmp = (mWeb->arg("serEn") == "on");
mEep->write(ADDR_SER_ENABLE, (uint8_t)((tmp) ? 0x01 : 0x00));
tmp = (mWeb->arg("serDbg") == "on");
mEep->write(ADDR_SER_DEBUG, (uint8_t)((tmp) ? 0x01 : 0x00));
updateCrc();
if((mWeb->arg("reboot") == "on"))
showReboot();
else {
mShowRebootRequest = true;
mWeb->send(200, "text/html", "<!doctype html><html><head><title>Setup saved</title><meta http-equiv=\"refresh\" content=\"3; URL=/setup\"></head><body>"
"<p>saved</p></body></html>");
}
}
else {
mWeb->send(200, "text/html", "<!doctype html><html><head><title>Error</title><meta http-equiv=\"refresh\" content=\"3; URL=/setup\"></head><body>"
"<p>Error while saving</p></body></html>");
}
}
//-----------------------------------------------------------------------------
void app::updateCrc(void) {
Main::updateCrc();
uint16_t crc;
crc = buildEEpCrc(ADDR_START_SETTINGS, (ADDR_NEXT - ADDR_START_SETTINGS));
//DPRINTLN("new CRC: " + String(crc, HEX));
mEep->write(ADDR_SETTINGS_CRC, crc);
}

94
tools/esp8266/app.h

@ -0,0 +1,94 @@
#ifndef __APP_H__
#define __APP_H__
#include <RF24.h>
#include <RF24_config.h>
#include "defines.h"
#include "main.h"
#include "CircularBuffer.h"
#include "hmSystem.h"
#include "mqtt.h"
typedef CircularBuffer<packet_t, PACKET_BUFFER_SIZE> BufferType;
typedef HmRadio<RF24_CE_PIN, RF24_CS_PIN, RF24_IRQ_PIN, BufferType> RadioType;
typedef Inverter<float> InverterType;
typedef HmSystem<RadioType, BufferType, MAX_NUM_INVERTERS, InverterType> HmSystemType;
const char* const wemosPins[] = {"D3 (GPIO0)", "TX (GPIO1)", "D4 (GPIO2)", "RX (GPIO3)",
"D2 (GPIO4)", "D1 (GPIO5)", "GPIO6", "GPIO7", "GPIO8",
"GPIO9", "GPIO10", "GPIO11", "D6 (GPIO12)", "D7 (GPIO13)",
"D5 (GPIO14)", "D8 (GPIO15)", "D0 (GPIO16)"};
const char* const pinNames[] = {"CS", "CE", "IRQ"};
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq"};
const uint8_t dbgCmds[] = {0x01, 0x02, 0x03, 0x81, 0x82, 0x83, 0x84};
#define DBG_CMD_LIST_LEN 7
class app : public Main {
public:
app();
~app();
void setup(uint32_t timeout);
void loop(void);
void handleIntr(void);
uint8_t getIrqPin(void) {
return mSys->Radio.pinIrq;
}
private:
void showIndex(void);
void showSetup(void);
void showSave(void);
void showErase(void);
void showStatistics(void);
void showHoymiles(void);
void showLiveData(void);
void saveValues(bool webSend);
void updateCrc(void);
uint64_t Serial2u64(const char *val) {
char tmp[3] = {0};
uint64_t ret = 0ULL;
uint64_t u64;
for(uint8_t i = 0; i < 6; i++) {
tmp[0] = val[i*2];
tmp[1] = val[i*2 + 1];
if((tmp[0] == '\0') || (tmp[1] == '\0'))
break;
u64 = strtol(tmp, NULL, 16);
ret |= (u64 << ((5-i) << 3));
}
return ret;
}
bool mShowRebootRequest;
HmSystemType *mSys;
uint16_t mSendTicker;
uint16_t mSendInterval;
uint32_t mCmds[DBG_CMD_LIST_LEN+1];
//uint32_t mChannelStat[4];
uint32_t mRecCnt;
// timer
uint32_t mTicker;
bool mSerialValues;
bool mSerialDebug;
// mqtt
mqtt mMqtt;
uint16_t mMqttTicker;
uint16_t mMqttInterval;
bool mMqttActive;
uint16_t mSerialTicker;
uint16_t mSerialInterval;
};
#endif /*__APP_H__*/

44
tools/esp8266/config.h

@ -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 15
// 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 64
// changes the style of "/setup" page, visualized = nicer
#define LIVEDATA_VISUALIZED
#endif /*__CONFIG_H__*/

43
tools/esp8266/crc.cpp

@ -0,0 +1,43 @@
#include "crc.h"
uint8_t crc8(uint8_t buf[], uint8_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(uint8_t buf[], uint8_t len) {
uint16_t crc = 0xffff;
uint8_t shift = 0;
for(uint8_t i = 0; i < len; i ++) {
crc = crc ^ buf[i];
for(uint8_t bit = 0; bit < 8; bit ++) {
shift = (crc & 0x0001);
crc = crc >> 1;
if(shift != 0)
crc = crc ^ 0xA001;
}
}
return crc;
}
uint16_t crc16nrf24(uint8_t buf[], uint16_t lenBits, uint16_t startBit, uint16_t crcIn) {
uint16_t crc = crcIn;
uint8_t idx, val = buf[(startBit >> 3)];
for(uint16_t bit = startBit; bit < lenBits; bit ++) {
idx = bit & 0x07;
if(0 == idx)
val = buf[(bit >> 3)];
crc ^= 0x8000 & (val << (8 + idx));
crc = (crc & 0x8000) ? ((crc << 1) ^ CRC16_NRF24_POLYNOM) : (crc << 1);
}
return crc;
}

16
tools/esp8266/crc.h

@ -0,0 +1,16 @@
#ifndef __CRC_H__
#define __CRC_H__
#include <cstdint>
#define CRC8_INIT 0x00
#define CRC8_POLY 0x01
#define CRC16_MODBUS_POLYNOM 0xA001
#define CRC16_NRF24_POLYNOM 0x1021
uint8_t crc8(uint8_t buf[], uint8_t len);
uint16_t crc16(uint8_t buf[], uint8_t len);
uint16_t crc16nrf24(uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff);
#endif /*__CRC_H__*/

39
tools/esp8266/debug.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__*/

92
tools/esp8266/defines.h

@ -0,0 +1,92 @@
#ifndef __DEFINES_H__
#define __DEFINES_H__
#include "config.h"
//-------------------------------------
// PINOUT (Default, can be changed in setup)
//-------------------------------------
#define RF24_CS_PIN 15
#define RF24_CE_PIN 2
#define RF24_IRQ_PIN 0
//-------------------------------------
// VERSION
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 3
#define VERSION_PATCH 6
//-------------------------------------
typedef struct {
uint8_t sendCh;
uint8_t packet[MAX_RF_PAYLOAD_SIZE];
} packet_t;
//-------------------------------------
// EEPROM
//-------------------------------------
#define SSID_LEN 32
#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 ADDR_START 0
#define ADDR_SSID ADDR_START
#define ADDR_PWD ADDR_SSID + SSID_LEN
#define ADDR_DEVNAME ADDR_PWD + PWD_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_SETTINGS_CRC 400
#if(ADDR_SETTINGS_CRC <= ADDR_NEXT)
#error address overlap!
#endif
#endif /*__DEFINES_H__*/

133
tools/esp8266/eep.h

@ -0,0 +1,133 @@
#ifndef __EEP_H__
#define __EEP_H__
#include "Arduino.h"
#include <EEPROM.h>
class eep {
public:
eep() {
EEPROM.begin(500);
}
~eep() {
EEPROM.end();
}
void read(uint32_t addr, char *str, uint8_t length) {
for(uint8_t i = 0; i < length; i ++) {
*(str++) = (char)EEPROM.read(addr++);
}
}
void 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 read(uint32_t addr, bool *value) {
uint8_t intVal = 0x00;
intVal = EEPROM.read(addr++);
*value = (intVal == 0x01);
}
void read(uint32_t addr, uint8_t *value) {
*value = (EEPROM.read(addr++));
}
void read(uint32_t addr, uint8_t data[], uint16_t length) {
for(uint16_t i = 0; i < length; i ++) {
*(data++) = EEPROM.read(addr++);
}
}
void read(uint32_t addr, uint16_t *value) {
*value = (EEPROM.read(addr++) << 8);
*value |= (EEPROM.read(addr++));
}
void 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 read(uint32_t addr, uint64_t *value) {
read(addr, (uint32_t *)value);
*value <<= 32;
uint32_t tmp;
read(addr+4, &tmp);
*value |= tmp;
/**value = (EEPROM.read(addr++) << 56);
*value |= (EEPROM.read(addr++) << 48);
*value |= (EEPROM.read(addr++) << 40);
*value |= (EEPROM.read(addr++) << 32);
*value |= (EEPROM.read(addr++) << 24);
*value |= (EEPROM.read(addr++) << 16);
*value |= (EEPROM.read(addr++) << 8);
*value |= (EEPROM.read(addr++));*/
}
void 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 write(uint32_t addr, uint8_t data[], uint16_t length) {
for(uint16_t i = 0; i < length; i ++) {
EEPROM.write(addr++, data[i]);
}
EEPROM.commit();
}
void 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 write(uint32_t addr, bool value) {
uint8_t intVal = (value) ? 0x01 : 0x00;
EEPROM.write(addr++, intVal);
EEPROM.commit();
}
void write(uint32_t addr, uint8_t value) {
EEPROM.write(addr++, value);
EEPROM.commit();
}
void write(uint32_t addr, uint16_t value) {
EEPROM.write(addr++, (value >> 8) & 0xff);
EEPROM.write(addr++, (value ) & 0xff);
EEPROM.commit();
}
void 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();
}
void write(uint64_t addr, uint64_t value) {
EEPROM.write(addr++, (value >> 56) & 0xff);
EEPROM.write(addr++, (value >> 48) & 0xff);
EEPROM.write(addr++, (value >> 40) & 0xff);
EEPROM.write(addr++, (value >> 32) & 0xff);
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();
}
};
#endif /*__EEP_H__*/

33
tools/esp8266/esp8266.ino

@ -0,0 +1,33 @@
#include "Arduino.h"
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <Ticker.h>
#include <ESP8266HTTPUpdateServer.h>
#include "app.h"
#include "config.h"
app myApp;
//-----------------------------------------------------------------------------
void setup() {
myApp.setup(WIFI_TRY_CONNECT_TIME);
// TODO: move to HmRadio
attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING);
}
//-----------------------------------------------------------------------------
void loop() {
myApp.loop();
}
//-----------------------------------------------------------------------------
ICACHE_RAM_ATTR void handleIntr(void) {
myApp.handleIntr();
}

159
tools/esp8266/hmDefines.h

@ -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__*/

213
tools/esp8266/hmInverter.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__*/

272
tools/esp8266/hmRadio.h

@ -0,0 +1,272 @@
#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)
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) {
mChanOut[0] = 23;
mChanOut[1] = 40;
mChanOut[2] = 61;
mChanOut[3] = 75;
mChanIdx = 1;
calcDtuCrc();
pinCs = CS_PIN;
pinCe = CE_PIN;
pinIrq = IRQ_PIN;
AmplifierPower = 1;
mSendCnt = 0;
}
~HmRadio() {}
void setup(BUFFER *ctrl) {
//DPRINTLN("HmRadio::setup, pins: " + String(pinCs) + ", " + String(pinCe) + ", " + String(pinIrq));
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();
mSendChannel = 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->sendCh = mSendChannel;
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 mChanOut[2];
}
uint8_t getLastChannel(void) {
return mChanOut[mChanIdx];
}
uint8_t getNxtChannel(void) {
if(++mChanIdx >= 4)
mChanIdx = 0;
return mChanOut[mChanIdx];
}
void sendTimePacket(uint64_t invId, uint32_t ts) {
sendCmdPacket(invId, 0x15, 0x80, false);
mSendBuf[10] = 0x0b; // cid
mSendBuf[11] = 0x00;
CP_U32_LittleEndian(&mSendBuf[12], ts);
mSendBuf[19] = 0x05;
uint16_t crc = crc16(&mSendBuf[10], 14);
mSendBuf[24] = (crc >> 8) & 0xff;
mSendBuf[25] = (crc ) & 0xff;
mSendBuf[26] = crc8(mSendBuf, 26);
sendPacket(invId, mSendBuf, 27);
}
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t cmd, bool calcCrc = true) {
memset(mSendBuf, 0, MAX_RF_PAYLOAD_SIZE);
mSendBuf[0] = mid; // message id
CP_U32_BigEndian(&mSendBuf[1], (invId >> 8));
CP_U32_BigEndian(&mSendBuf[5], (DTU_ID >> 8));
mSendBuf[9] = cmd;
if(calcCrc) {
mSendBuf[10] = crc8(mSendBuf, 10);
sendPacket(invId, mSendBuf, 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) {
DPRINT(String(info));
for(uint8_t i = 0; i < len; i++) {
if(buf[i] < 10)
DPRINT("0");
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) {
//DPRINTLN("sent packet: #" + String(mSendCnt));
//dumpBuf("SEN ", buf, len);
DISABLE_IRQ;
mNrf24.stopListening();
#ifdef CHANNEL_HOP
mSendChannel = getNxtChannel();
#else
mSendChannel = getDefaultChannel();
#endif
mNrf24.setChannel(mSendChannel);
//DPRINTLN("CH: " + String(mSendChannel));
mNrf24.openWritingPipe(invId); // TODO: deprecated
mNrf24.setCRCLength(RF24_CRC_16);
mNrf24.enableDynamicPayloads();
mNrf24.setAutoAck(true);
mNrf24.setRetries(3, 15);
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);
mNrf24.setChannel(DEFAULT_RECV_CHANNEL);
mNrf24.startListening();
RESTORE_IRQ;
mSendCnt++;
}
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 mChanOut[4];
uint8_t mChanIdx;
uint16_t mDtuIdCrc;
uint16_t mLastCrc;
uint8_t mRptCnt;
RF24 mNrf24;
uint8_t mSendChannel;
BUFFER *mBufCtrl;
uint8_t mSendBuf[MAX_RF_PAYLOAD_SIZE];
};
#endif /*__RADIO_H__*/

82
tools/esp8266/hmSystem.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__*/

28
tools/esp8266/html/convert.py

@ -0,0 +1,28 @@
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()
f = open(outName, "w")
f.write("#ifndef __{}_H__\n".format(define))
f.write("#define __{}_H__\n".format(define))
f.write("const char {}[] PROGMEM = \"{}\";\n".format(inFile.replace(".", "_"), data))
f.write("#endif /*__{}_H__*/\n".format(define))
f.close()
convert2Header("index.html")
convert2Header("setup.html")
convert2Header("hoymiles.html")
convert2Header("style.css")

4
tools/esp8266/html/h/hoymiles_html.h

@ -0,0 +1,4 @@
#ifndef __HOYMILES_H__
#define __HOYMILES_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\">&copy 2022</p><p class=\"left\"><a href=\"/\">Home</a></p><p class=\"right\">AHOY :: {VERSION}</p></div></body></html>";
#endif /*__HOYMILES_H__*/

4
tools/esp8266/html/h/index_html.h

@ -0,0 +1,4 @@
#ifndef __INDEX_H__
#define __INDEX_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\">&copy 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_H__*/

4
tools/esp8266/html/h/setup_html.h

@ -0,0 +1,4 @@
#ifndef __SETUP_H__
#define __SETUP_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\">&nbsp;</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_H__*/

4
tools/esp8266/html/h/style_css.h

@ -0,0 +1,4 @@
#ifndef __STYLE_H__
#define __STYLE_H__
const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size: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_H__*/

42
tools/esp8266/html/hoymiles.html

@ -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">&copy 2022</p>
<p class="left"><a href="/">Home</a></p>
<p class="right">AHOY :: {VERSION}</p>
</div>
</body>
</html>

55
tools/esp8266/html/index.html

@ -0,0 +1,55 @@
<!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">&copy 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>

79
tools/esp8266/html/setup.html

@ -0,0 +1,79 @@
<!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">&nbsp;</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>

190
tools/esp8266/html/style.css

@ -0,0 +1,190 @@
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;
}

432
tools/esp8266/main.cpp

@ -0,0 +1,432 @@
#include "main.h"
#include "version.h"
#include "html/h/style_css.h"
#include "html/h/setup_html.h"
//-----------------------------------------------------------------------------
Main::Main(void) {
mDns = new DNSServer();
mWeb = new ESP8266WebServer(80);
mUpdater = new ESP8266HTTPUpdateServer();
mUdp = new WiFiUDP();
mApActive = true;
mWifiSettingsValid = false;
mSettingsValid = false;
mLimit = 10;
mNextTryTs = 0;
mApLastTick = 0;
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
memset(&mDeviceName, 0, DEVNAME_LEN);
mEep = new eep();
Serial.begin(115200);
mUptimeSecs = 0;
mUptimeTicker = 0xffffffff;
mUptimeInterval = 1000;
}
//-----------------------------------------------------------------------------
void Main::setup(uint32_t timeout) {
bool startAp = mApActive;
mLimit = timeout;
mWeb->on("/setup", std::bind(&Main::showSetup, this));
mWeb->on("/save", std::bind(&Main::showSave, this));
mWeb->on("/uptime", std::bind(&Main::showUptime, this));
mWeb->on("/time", std::bind(&Main::showTime, this));
mWeb->on("/style.css", std::bind(&Main::showCss, this));
mWeb->on("/reboot", std::bind(&Main::showReboot, this));
mWeb->on("/factory", std::bind(&Main::showFactoryRst, this));
mWeb->onNotFound ( std::bind(&Main::showNotFound, this));
startAp = getConfig();
#ifndef AP_ONLY
if(false == startAp)
startAp = setupStation(timeout);
#else
setupAp(WIFI_AP_SSID, WIFI_AP_PWD);
#endif
if(!startAp) {
mTimestamp = getNtpTime();
DPRINTLN("[NTP]: " + getDateTimeStr(getNtpTime()));
}
mUpdater->setup(mWeb);
mApActive = startAp;
}
//-----------------------------------------------------------------------------
void Main::loop(void) {
if(mApActive) {
mDns->processNextRequest();
#ifndef AP_ONLY
if(checkTicker(&mNextTryTs, (WIFI_AP_ACTIVE_TIME * 1000))) {
mApLastTick = millis();
mApActive = setupStation(mLimit);
if(mApActive) {
if(strlen(WIFI_AP_PWD) < 8)
DPRINTLN("ERROR: password must be at least 8 characters long");
setupAp(WIFI_AP_SSID, WIFI_AP_PWD);
}
}
else {
if(millis() - mApLastTick > 10000) {
mApLastTick = millis();
DPRINTLN("AP will be closed in " + String((mNextTryTs - mApLastTick) / 1000) + " seconds");
}
}
#endif
}
mWeb->handleClient();
if(checkTicker(&mUptimeTicker, mUptimeInterval)) {
mUptimeSecs++;
mTimestamp++;
}
}
//-----------------------------------------------------------------------------
bool Main::getConfig(void) {
bool mApActive = false;
mWifiSettingsValid = checkEEpCrc(ADDR_START, ADDR_WIFI_CRC, ADDR_WIFI_CRC);
mSettingsValid = checkEEpCrc(ADDR_START_SETTINGS, (ADDR_NEXT-ADDR_START_SETTINGS), ADDR_SETTINGS_CRC);
if(mWifiSettingsValid) {
mEep->read(ADDR_SSID, mStationSsid, SSID_LEN);
mEep->read(ADDR_PWD, mStationPwd, PWD_LEN);
mEep->read(ADDR_DEVNAME, mDeviceName, DEVNAME_LEN);
}
else {
/*mApActive = true;
memset(mStationSsid, 0, SSID_LEN);
memset(mStationPwd, 0, PWD_LEN);
memset(mDeviceName, 0, DEVNAME_LEN);
// erase application settings except wifi settings
eraseSettings();*/
snprintf(mStationSsid, SSID_LEN, "%s", FB_WIFI_SSID);
snprintf(mStationPwd, PWD_LEN, "%s", FB_WIFI_PWD);
snprintf(mDeviceName, DEVNAME_LEN, "%s", DEF_DEVICE_NAME);
}
return mApActive;
}
//-----------------------------------------------------------------------------
void Main::setupAp(const char *ssid, const char *pwd) {
IPAddress apIp(192, 168, 1, 1);
DPRINTLN("\n---------\nAP MODE\nSSDI: "
+ String(ssid) + "\nPWD: "
+ String(pwd) + "\nActive for: "
+ String(WIFI_AP_ACTIVE_TIME) + " seconds"
+ "\n---------\n");
DPRINTLN("DBG: " + String(mNextTryTs));
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(apIp, apIp, IPAddress(255, 255, 255, 0));
WiFi.softAP(ssid, pwd);
mDns->start(mDnsPort, "*", apIp);
mWeb->onNotFound([&]() {
showSetup();
});
mWeb->on("/", std::bind(&Main::showSetup, this));
mWeb->begin();
}
//-----------------------------------------------------------------------------
bool Main::setupStation(uint32_t timeout) {
int32_t cnt;
bool startAp = false;
if(timeout >= 3)
cnt = (timeout - 3) / 2 * 10;
else {
timeout = 1;
cnt = 1;
}
WiFi.mode(WIFI_STA);
WiFi.begin(mStationSsid, mStationPwd);
if(String(mDeviceName) != "")
WiFi.hostname(mDeviceName);
delay(2000);
DPRINTLN("connect to network '" + String(mStationSsid) + "' ...");
while (WiFi.status() != WL_CONNECTED) {
delay(100);
if(cnt % 100 == 0)
Serial.println(".");
else
Serial.print(".");
if(timeout > 0) { // limit == 0 -> no limit
if(--cnt <= 0) {
if(WiFi.status() != WL_CONNECTED) {
startAp = true;
WiFi.disconnect();
}
delay(100);
break;
}
}
}
Serial.println(".");
if(false == startAp) {
mWeb->begin();
}
delay(1000);
return startAp;
}
//-----------------------------------------------------------------------------
void Main::showSetup(void) {
String html = FPSTR(setup_html);
html.replace("{SSID}", mStationSsid);
// PWD will be left at the default value (for protection)
// -> the PWD will only be changed if it does not match the default "{PWD}"
html.replace("{DEVICE}", String(mDeviceName));
html.replace("{VERSION}", String(mVersion));
if(mApActive)
html.replace("{IP}", String("http://192.168.1.1"));
else
html.replace("{IP}", ("http://" + String(WiFi.localIP().toString())));
mWeb->send(200, "text/html", html);
}
//-----------------------------------------------------------------------------
void Main::showCss(void) {
mWeb->send(200, "text/css", FPSTR(style_css));
}
//-----------------------------------------------------------------------------
void Main::showSave(void) {
saveValues(true);
}
//-----------------------------------------------------------------------------
void Main::saveValues(bool webSend = true) {
if(mWeb->args() > 0) {
if(mWeb->arg("ssid") != "") {
memset(mStationSsid, 0, SSID_LEN);
mWeb->arg("ssid").toCharArray(mStationSsid, SSID_LEN);
mEep->write(ADDR_SSID, mStationSsid, SSID_LEN);
if(mWeb->arg("pwd") != "{PWD}") {
memset(mStationPwd, 0, PWD_LEN);
mWeb->arg("pwd").toCharArray(mStationPwd, PWD_LEN);
mEep->write(ADDR_PWD, mStationPwd, PWD_LEN);
}
}
memset(mDeviceName, 0, DEVNAME_LEN);
mWeb->arg("device").toCharArray(mDeviceName, DEVNAME_LEN);
mEep->write(ADDR_DEVNAME, mDeviceName, DEVNAME_LEN);
updateCrc();
if(webSend) {
if(mWeb->arg("reboot") == "on")
showReboot();
else // TODO: add device name as redirect in AP-mode
mWeb->send(200, "text/html", "<!doctype html><html><head><title>Setup saved</title><meta http-equiv=\"refresh\" content=\"0; URL=/setup\"></head><body>"
"<p>saved</p></body></html>");
}
}
}
//-----------------------------------------------------------------------------
void Main::updateCrc(void) {
uint16_t crc;
crc = buildEEpCrc(ADDR_START, ADDR_WIFI_CRC);
//Serial.println("new CRC: " + String(crc, HEX));
mEep->write(ADDR_WIFI_CRC, crc);
}
//-----------------------------------------------------------------------------
void Main::showUptime(void) {
char time[20] = {0};
int upTimeSc = uint32_t((mUptimeSecs) % 60);
int upTimeMn = uint32_t((mUptimeSecs / (60)) % 60);
int upTimeHr = uint32_t((mUptimeSecs / (60 * 60)) % 24);
int upTimeDy = uint32_t((mUptimeSecs / (60 * 60 * 24)) % 365);
snprintf(time, 20, "%d Tage, %02d:%02d:%02d", upTimeDy, upTimeHr, upTimeMn, upTimeSc);
mWeb->send(200, "text/plain", String(time));
}
//-----------------------------------------------------------------------------
void Main::showTime(void) {
mWeb->send(200, "text/plain", getDateTimeStr(mTimestamp));
}
//-----------------------------------------------------------------------------
void Main::showNotFound(void) {
String msg = "File Not Found\n\n";
msg += "URI: ";
msg += mWeb->uri();
msg += "\nMethod: ";
msg += ( mWeb->method() == HTTP_GET ) ? "GET" : "POST";
msg += "\nArguments: ";
msg += mWeb->args();
msg += "\n";
for(uint8_t i = 0; i < mWeb->args(); i++ ) {
msg += " " + mWeb->argName(i) + ": " + mWeb->arg(i) + "\n";
}
mWeb->send(404, "text/plain", msg);
}
//-----------------------------------------------------------------------------
void Main::showReboot(void) {
mWeb->send(200, "text/html", "<!doctype html><html><head><title>Rebooting ...</title><meta http-equiv=\"refresh\" content=\"10; URL=/\"></head><body>rebooting ... auto reload after 10s</body></html>");
delay(1000);
ESP.restart();
}
//-----------------------------------------------------------------------------
void Main::showFactoryRst(void) {
String content = "";
int refresh = 3;
if(mWeb->args() > 0) {
if(mWeb->arg("reset").toInt() == 1) {
eraseSettings(true);
content = "factory reset: success\n\nrebooting ... ";
refresh = 10;
}
else {
content = "factory reset: aborted";
refresh = 3;
}
}
else {
content = "<h1>Factory Reset</h1>";
content += "<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>";
refresh = 120;
}
mWeb->send(200, "text/html", "<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"" + String(refresh) + "; URL=/\"></head><body>" + content + "</body></html>");
if(refresh == 10) {
delay(1000);
ESP.restart();
}
}
//-----------------------------------------------------------------------------
time_t Main::getNtpTime(void) {
time_t date = 0;
IPAddress timeServer;
uint8_t buf[NTP_PACKET_SIZE];
uint8_t retry = 0;
WiFi.hostByName (TIMESERVER_NAME, timeServer);
mUdp->begin(TIME_LOCAL_PORT);
sendNTPpacket(timeServer);
while(retry++ < 5) {
int wait = 150;
while(--wait) {
if(NTP_PACKET_SIZE <= mUdp->parsePacket()) {
uint64_t secsSince1900;
mUdp->read(buf, NTP_PACKET_SIZE);
secsSince1900 = (buf[40] << 24);
secsSince1900 |= (buf[41] << 16);
secsSince1900 |= (buf[42] << 8);
secsSince1900 |= (buf[43] );
date = secsSince1900 - 2208988800UL; // UTC time
date += (TIMEZONE + offsetDayLightSaving(date)) * 3600;
break;
}
else
delay(10);
}
}
return date;
}
//-----------------------------------------------------------------------------
void Main::sendNTPpacket(IPAddress& address) {
uint8_t buf[NTP_PACKET_SIZE] = {0};
buf[0] = B11100011; // LI, Version, Mode
buf[1] = 0; // Stratum
buf[2] = 6; // Max Interval between messages in seconds
buf[3] = 0xEC; // Clock Precision
// bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset
buf[12] = 49; // four-byte reference ID identifying
buf[13] = 0x4E;
buf[14] = 49;
buf[15] = 52;
mUdp->beginPacket(address, 123); // NTP request, port 123
mUdp->write(buf, NTP_PACKET_SIZE);
mUdp->endPacket();
}
//-----------------------------------------------------------------------------
String Main::getDateTimeStr(time_t t) {
char str[20] = {0};
sprintf(str, "%04d-%02d-%02d+%02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t));
return String(str);
}
//-----------------------------------------------------------------------------
// calculates the daylight saving time for middle Europe. Input: Unixtime in UTC
// from: https://forum.arduino.cc/index.php?topic=172044.msg1278536#msg1278536
time_t Main::offsetDayLightSaving (uint32_t local_t) {
int m = month (local_t);
if(m < 3 || m > 10) return 0; // no DSL in Jan, Feb, Nov, Dez
if(m > 3 && m < 10) return 1; // DSL in Apr, May, Jun, Jul, Aug, Sep
int y = year (local_t);
int h = hour (local_t);
int hToday = (h + 24 * day(local_t));
if((m == 3 && hToday >= (1 + TIMEZONE + 24 * (31 - (5 * y /4 + 4) % 7)))
|| (m == 10 && hToday < (1 + TIMEZONE + 24 * (31 - (5 * y /4 + 1) % 7))) )
return 1;
else
return 0;
}

125
tools/esp8266/main.h

@ -0,0 +1,125 @@
#ifndef __MAIN_H__
#define __MAIN_H__
#include "Arduino.h"
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h>
// NTP
#include <WiFiUdp.h>
#include <TimeLib.h>
#include "eep.h"
#include "defines.h"
#include "crc.h"
#include "debug.h"
const byte mDnsPort = 53;
/* TIMESERVER CONFIG */
#define TIMESERVER_NAME "pool.ntp.org"
#define TIME_LOCAL_PORT 8888
#define NTP_PACKET_SIZE 48
#define TIMEZONE 1 // Central European time +1
class Main {
public:
Main(void);
virtual void setup(uint32_t timeout);
virtual void loop();
String getDateTimeStr (time_t t);
protected:
void showReboot(void);
virtual void saveValues(bool webSend);
virtual void updateCrc(void);
inline uint16_t buildEEpCrc(uint32_t start, uint32_t length) {
uint8_t buf[length];
mEep->read(start, buf, length);
return crc16(buf, length);
}
bool checkEEpCrc(uint32_t start, uint32_t length, uint32_t crcPos) {
uint16_t crcRd, crcCheck;
crcCheck = buildEEpCrc(start, length);
mEep->read(crcPos, &crcRd);
return (crcCheck == crcRd);
}
void eraseSettings(bool all = false) {
uint8_t buf[64] = {0};
uint16_t addr = (all) ? ADDR_START : ADDR_START_SETTINGS;
uint16_t end;
do {
end = addr + 64;
if(end > (ADDR_SETTINGS_CRC + 2))
end = (ADDR_SETTINGS_CRC + 2);
DPRINTLN("erase: 0x" + String(addr, HEX) + " - 0x" + String(end, HEX));
mEep->write(addr, buf, (end-addr));
addr = end;
} while(addr < (ADDR_SETTINGS_CRC + 2));
}
inline bool checkTicker(uint32_t *ticker, uint32_t interval) {
uint32_t mil = millis();
if(mil >= *ticker) {
*ticker = mil + interval;
return true;
}
else if(mil < (*ticker - interval)) {
*ticker = mil + interval;
return true;
}
return false;
}
char mStationSsid[SSID_LEN];
char mStationPwd[PWD_LEN];
bool mWifiSettingsValid;
bool mSettingsValid;
bool mApActive;
ESP8266WebServer *mWeb;
char mVersion[9];
char mDeviceName[DEVNAME_LEN];
eep *mEep;
uint32_t mTimestamp;
uint32_t mLimit;
uint32_t mNextTryTs;
uint32_t mApLastTick;
private:
bool getConfig(void);
void setupAp(const char *ssid, const char *pwd);
bool setupStation(uint32_t timeout);
void showNotFound(void);
virtual void showSetup(void);
virtual void showSave(void);
void showUptime(void);
void showTime(void);
void showCss(void);
void showFactoryRst(void);
time_t getNtpTime(void);
void sendNTPpacket(IPAddress& address);
time_t offsetDayLightSaving (uint32_t local_t);
uint32_t mUptimeTicker;
uint16_t mUptimeInterval;
uint32_t mUptimeSecs;
DNSServer *mDns;
ESP8266HTTPUpdateServer *mUpdater;
WiFiUDP *mUdp; // for time server
};
#endif /*__MAIN_H__*/

87
tools/esp8266/mqtt.h

@ -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_*/

31
tools/esp8266/test/hmClassTest/hmClassTest.sln

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

147
tools/esp8266/test/hmClassTest/hmClassTest/hmClassTest.vcxproj

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

22
tools/esp8266/test/hmClassTest/hmClassTest/hmClassTest.vcxproj.filters

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

4
tools/esp8266/test/hmClassTest/hmClassTest/hmClassTest.vcxproj.user

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

105
tools/esp8266/test/hmClassTest/src/main.cpp

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

3
tools/rpi/discover/discover.cpp

@ -109,6 +109,7 @@ int main(int argc, char** argv)
dstaddrs.push_back(string("1Node"));
dstaddrs.push_back(string("2Node"));
dstaddrs.push_back(serno2shockburstaddrbytes(114174608145));
dstaddrs.push_back("\x45\x81\x60\x74\x01");
dstaddrs.push_back(serno2shockburstaddrbytes(114174608177));
// channels that we will scan
@ -127,7 +128,7 @@ int main(int argc, char** argv)
cout << " - ";
}
cout << " " << flush;
delay(10);
//delay(10);
}
cout << endl;
}

3
tools/rpi/discover/pretender.cpp

@ -11,6 +11,7 @@
#include <sstream>
#include <time.h> // CLOCK_MONOTONIC_RAW, timespec, clock_gettime()
#include <RF24/RF24.h> // RF24, RF24_PA_LOW, delay()
#include <unistd.h> // usleep()
using namespace std;
@ -45,7 +46,7 @@ void receiveForever(int ch, string myaddr)
while (true)
{
uint8_t pipe;
delay(500);
usleep(500000);
if (radio.failureDetected) {
cout << "!f! " << flush;
}

Loading…
Cancel
Save