mirror of https://github.com/lumapu/ahoy.git
16 changed files with 2698 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
.pio |
|||
.vscode/.browse.c_cpp.db* |
|||
.vscode/c_cpp_properties.json |
|||
.vscode/launch.json |
|||
.vscode/ipch |
|||
.log |
|||
config_override.h |
|||
|
@ -0,0 +1,39 @@ |
|||
|
|||
This directory is intended for project header files. |
|||
|
|||
A header file is a file containing C declarations and macro definitions |
|||
to be shared between several project source files. You request the use of a |
|||
header file in your project source file (C, C++, etc) located in `src` folder |
|||
by including it, with the C preprocessing directive `#include'. |
|||
|
|||
```src/main.c |
|||
|
|||
#include "header.h" |
|||
|
|||
int main (void) |
|||
{ |
|||
... |
|||
} |
|||
``` |
|||
|
|||
Including a header file produces the same results as copying the header file |
|||
into each source file that needs it. Such copying would be time-consuming |
|||
and error-prone. With a header file, the related declarations appear |
|||
in only one place. If they need to be changed, they can be changed in one |
|||
place, and programs that include the header file will automatically use the |
|||
new version when next recompiled. The header file eliminates the labor of |
|||
finding and changing all the copies as well as the risk that a failure to |
|||
find one copy will result in inconsistencies within a program. |
|||
|
|||
In C, the usual convention is to give header files names that end with `.h'. |
|||
It is most portable to use only letters, digits, dashes, and underscores in |
|||
header file names, and at most one dot. |
|||
|
|||
Read more about using header files in official GCC documentation: |
|||
|
|||
* Include Syntax |
|||
* Include Operation |
|||
* Once-Only Headers |
|||
* Computed Includes |
|||
|
|||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html |
@ -0,0 +1,46 @@ |
|||
|
|||
This directory is intended for project specific (private) libraries. |
|||
PlatformIO will compile them to static libraries and link into executable file. |
|||
|
|||
The source code of each library should be placed in a an own separate directory |
|||
("lib/your_library_name/[here are source files]"). |
|||
|
|||
For example, see a structure of the following two libraries `Foo` and `Bar`: |
|||
|
|||
|--lib |
|||
| | |
|||
| |--Bar |
|||
| | |--docs |
|||
| | |--examples |
|||
| | |--src |
|||
| | |- Bar.c |
|||
| | |- Bar.h |
|||
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html |
|||
| | |
|||
| |--Foo |
|||
| | |- Foo.c |
|||
| | |- Foo.h |
|||
| | |
|||
| |- README --> THIS FILE |
|||
| |
|||
|- platformio.ini |
|||
|--src |
|||
|- main.c |
|||
|
|||
and a contents of `src/main.c`: |
|||
``` |
|||
#include <Foo.h> |
|||
#include <Bar.h> |
|||
|
|||
int main (void) |
|||
{ |
|||
... |
|||
} |
|||
|
|||
``` |
|||
|
|||
PlatformIO Library Dependency Finder will find automatically dependent |
|||
libraries scanning project source files. |
|||
|
|||
More information about PlatformIO Library Dependency Finder |
|||
- https://docs.platformio.org/page/librarymanager/ldf.html |
@ -0,0 +1,51 @@ |
|||
; PlatformIO Project Configuration File |
|||
; |
|||
; Build options: build flags, source filter |
|||
; Upload options: custom upload port, speed and extra flags |
|||
; Library options: dependencies, extra library storages |
|||
; Advanced options: extra scripting |
|||
; |
|||
; Please visit documentation for the other options and examples |
|||
; https://docs.platformio.org/page/projectconf.html |
|||
|
|||
|
|||
|
|||
[env:nanoatmega328] |
|||
platform = atmelavr |
|||
board = nanoatmega328new |
|||
framework = arduino |
|||
upload_speed = 115200 |
|||
;monitor_speed = 57600 |
|||
; change microcontroller |
|||
board_build.mcu = atmega328p |
|||
; change MCU frequency |
|||
;board_build.f_cpu = 16000000L |
|||
monitor_speed = 57600 |
|||
lib_deps = |
|||
nrf24/RF24@^1.4.5 |
|||
monitor_filters = |
|||
send_on_enter |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “./*-monitor.log” located in the current working directory |
|||
|
|||
|
|||
[env:esp8266-release] |
|||
platform = espressif8266 |
|||
board = esp12e |
|||
framework = arduino |
|||
board_build.f_cpu = 80000000L |
|||
build_flags = -D RELEASE |
|||
monitor_speed = 57600 |
|||
lib_deps = |
|||
nrf24/RF24 |
|||
monitor_filters = |
|||
send_on_enter |
|||
;default ; Remove typical terminal control codes from input |
|||
time ; Add timestamp with milliseconds for each new line |
|||
log2file ; Log data to a file “./*-monitor.log” located in the current working directory |
|||
|
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,9 @@ |
|||
{ |
|||
"folders": [ |
|||
{ |
|||
"name": "AUL", |
|||
"path": ".." |
|||
} |
|||
], |
|||
"settings": {} |
|||
} |
@ -0,0 +1,164 @@ |
|||
/*
|
|||
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() |
|||
#elif defined(ESP32) |
|||
#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; |
|||
} |
|||
|
|||
inline uint8_t getFill(void) const { |
|||
return m_fill; |
|||
} |
|||
|
|||
/** 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
|
@ -0,0 +1,76 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __CONFIG_H__ |
|||
#define __CONFIG_H__ |
|||
|
|||
#include <stdint.h> |
|||
|
|||
//-------------------------------------
|
|||
// CONFIGURATION - COMPILE TIME
|
|||
//-------------------------------------
|
|||
|
|||
#if defined(ESP8266) |
|||
// for esp8266 environment
|
|||
#define DEF_DEVICE_NAME "\n\n +++ AHOY-UL ESP8266 +++" |
|||
#define DEF_RF24_CS_PIN 15 |
|||
#define DEF_RF24_CE_PIN 2 |
|||
#define DEF_RF24_IRQ_PIN 0 |
|||
#else |
|||
// default pinout (GPIO Number) for Arduino Nano 328p
|
|||
#define DEF_DEVICE_NAME "\n\n +++ AHOY-UL A-NANO +++" |
|||
#define DEF_RF24_CE_PIN (4) |
|||
#define DEF_RF24_CS_PIN (10) |
|||
#define DEF_RF24_IRQ_PIN (3) |
|||
#endif |
|||
|
|||
|
|||
|
|||
// default radio ID
|
|||
#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL) |
|||
|
|||
// real inverter ID is taken from config_override.h at the bottom of this file
|
|||
#define IV1_RADIO_ID ((uint64_t) 0x114144332211ULL) // 0x1141 is for HM800, lowerb4bytes must be filled with real ID from INV-plate
|
|||
|
|||
// default NRF24 power, possible values (0 - 3)
|
|||
#define DEF_AMPLIFIERPOWER 2 |
|||
|
|||
// number of packets hold in buffer
|
|||
#define PACKET_BUFFER_SIZE 7 |
|||
|
|||
// number of configurable inverters
|
|||
#define MAX_NUM_INVERTERS 1 |
|||
|
|||
// default serial interval
|
|||
#define SERIAL_INTERVAL 5 |
|||
|
|||
// default send interval
|
|||
#define SEND_INTERVAL 60 //send interval if Rx OK
|
|||
#define SEND_INTERVAL_MIN 10 //send interval if no RX (used initial sync or signal loss), todo: shall be disabled over night
|
|||
|
|||
// maximum human readable inverter name length
|
|||
#define MAX_NAME_LENGTH 16 |
|||
|
|||
// maximum buffer length of packet received / sent to RF24 module
|
|||
#define MAX_RF_PAYLOAD_SIZE 32 |
|||
|
|||
// maximum total payload buffers (must be greater than the number of received frame fragments)
|
|||
#define MAX_PAYLOAD_ENTRIES 8 |
|||
|
|||
// maximum requests for retransmits per payload (per inverter)
|
|||
#define DEF_MAX_RETRANS_PER_PYLD 10 |
|||
|
|||
// number of seconds since last successful response, before inverter is marked inactive
|
|||
#define INACT_THRES_SEC 300 |
|||
|
|||
// threshold of minimum power on which the inverter is marked as inactive
|
|||
#define INACT_PWR_THRESH 3 |
|||
|
|||
|
|||
#if __has_include("config_override.h") |
|||
#include "config_override.h" |
|||
#endif |
|||
|
|||
#endif /*__CONFIG_H__*/ |
@ -0,0 +1,38 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#include "crc.h" |
|||
|
|||
namespace Hoymiles { |
|||
|
|||
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); |
|||
} |
|||
yield(); |
|||
} |
|||
return crc; |
|||
} |
|||
|
|||
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start) { |
|||
uint16_t crc = start; |
|||
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 ^ CRC16_MODBUS_POLYNOM; |
|||
} |
|||
yield(); |
|||
} |
|||
return crc; |
|||
} |
|||
} // namespace Hoymiles
|
@ -0,0 +1,23 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
#ifndef __CRC_H__ |
|||
#define __CRC_H__ |
|||
|
|||
#include <stdint.h> |
|||
#include "Arduino.h" |
|||
|
|||
#define CRC8_INIT 0x00 |
|||
#define CRC8_POLY 0x01 |
|||
|
|||
#define CRC16_MODBUS_POLYNOM 0xA001 |
|||
|
|||
namespace Hoymiles { |
|||
|
|||
uint8_t crc8(uint8_t buf[], uint8_t len); |
|||
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff); |
|||
|
|||
} |
|||
#endif /*__CRC_H__*/ |
@ -0,0 +1,198 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
|
|||
#ifndef __DBG_H__ |
|||
#define __DBG_H__ |
|||
#if defined(ESP32) && defined(F) |
|||
#undef F |
|||
#define F(sl) (sl) |
|||
#endif |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
// available levels
|
|||
#define DBG_ERROR 1 |
|||
#define DBG_WARN 2 |
|||
#define DBG_INFO 3 |
|||
#define DBG_DEBUG 4 |
|||
#define DBG_VERBOSE 5 |
|||
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
// globally used level
|
|||
#define DEBUG_LEVEL DBG_INFO //adapt DEBUG_LEVEL to your needs
|
|||
|
|||
#ifdef ARDUINO |
|||
#include "Arduino.h" |
|||
#endif |
|||
|
|||
#ifdef NDEBUG |
|||
#define DBGPRINT(str) |
|||
#define DBGPRINTLN(str) |
|||
#else |
|||
#ifdef ARDUINO |
|||
#ifndef DSERIAL |
|||
#define DSERIAL Serial |
|||
#endif |
|||
|
|||
template <class T> |
|||
inline void DBGPRINT(T str) { DSERIAL.print(str); } |
|||
template <class T> |
|||
inline void DBGPRINTLN(T str) { DBGPRINT(str); DBGPRINT(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 |
|||
|
|||
|
|||
#if DEBUG_LEVEL >= DBG_ERROR |
|||
#define PERR(str) DBGPRINT(F("\nE: ")); DBGPRINT(str); |
|||
#define PERRLN(str) DBGPRINT(F("\nE: ")); DBGPRINTLN(str); |
|||
#define _PERR(str) DBGPRINT(str); |
|||
#define _PERRLN(str) DBGPRINTLN(str); |
|||
#define _PERRHEX(str) DHEX(str); |
|||
#else |
|||
//no output
|
|||
#define PERR(str) |
|||
#define PERRLN(str) |
|||
#define _PERR(str) |
|||
#define _PERRLN(str) |
|||
#define _PERRHEX(str) |
|||
#endif |
|||
|
|||
#if DEBUG_LEVEL >= DBG_WARN |
|||
#define PWARN(str) DBGPRINT(F("\nW: ")); DBGPRINT(str); |
|||
#define PWARNLN(str) DBGPRINT(F("\nW: ")); DBGPRINTLN(str); |
|||
#define _PWARN(str) DBGPRINT(str); |
|||
#define _PWARNLN(str) DBGPRINTLN(str); |
|||
#define _PWARNHEX(str) DHEX(str); |
|||
#else |
|||
//no output
|
|||
#define PWARN(str) |
|||
#define PWARNLN(str) |
|||
#define _PWARN(str) |
|||
#define _PWARNLN(str) |
|||
#define _PWARNHEX(str) |
|||
#endif |
|||
|
|||
#if DEBUG_LEVEL >= DBG_INFO |
|||
#define PINFO(str) DBGPRINT(F("\nI: ")); DBGPRINT(str); |
|||
#define PINFOLN(str) DBGPRINT(F("\nI: ")); DBGPRINTLN(str); |
|||
#define _PINFO(str) DBGPRINT(str); |
|||
#define _PINFOLN(str) DBGPRINTLN(str); |
|||
#define _PINFOHEX(str) DHEX(str); |
|||
#else |
|||
//no output
|
|||
#define PINFO(str) |
|||
#define PINFOLN(str) |
|||
#define _PINFO(str) |
|||
#define _PINFOLN(str) |
|||
#define _PINFOHEX(str) |
|||
#endif |
|||
|
|||
#if DEBUG_LEVEL >= DBG_DEBUG |
|||
#define PDBG(str) DBGPRINT(F("\nD: ")); DBGPRINT(str); |
|||
#define PDBGLN(str) DBGPRINT(F("D: ")); DBGPRINTLN(str); |
|||
#define _PDBG(str) DBGPRINT(str); |
|||
#define _PDBGLN(str) DBGPRINTLN(str); |
|||
#define _PDBGHEX(str) DHEX(str); |
|||
#else |
|||
//no output
|
|||
#define PDBG(str) |
|||
#define PDBGLN(str) |
|||
#define _PDBG(str) |
|||
#define _PDBGLN(str) |
|||
#define _PDBGHEX(str) |
|||
#endif |
|||
|
|||
#if DEBUG_LEVEL >= DBG_VERBOSE |
|||
#define PVERB(str) DBGPRINT(F("\nV: ")); DBGPRINT(str); |
|||
#define PVERBLN(str) DBGPRINT(F("\nV: ")); DBGPRINTLN(str); |
|||
#define _PVERB(str) DBGPRINT(str); |
|||
#define _PVERBLN(str) DBGPRINTLN(str); |
|||
#define _PVERBHEX(str) DHEX(str); |
|||
#else |
|||
#define PVERB(str) |
|||
#define PVERBLN(str) |
|||
#define _PVERB(str) |
|||
#define _PVERBLN(str) |
|||
#define _PVERBHEX(str) |
|||
#endif |
|||
|
|||
|
|||
|
|||
//external methods
|
|||
|
|||
#define DPRINT(level, str) ({\ |
|||
switch(level) {\ |
|||
case DBG_ERROR: PERR(str); break; \ |
|||
case DBG_WARN: PWARN(str); break; \ |
|||
case DBG_INFO: PINFO(str); break; \ |
|||
case DBG_DEBUG: PDBG(str); break; \ |
|||
default: PVERB(str); break; \ |
|||
}\ |
|||
}) |
|||
|
|||
#define DPRINTLN(level, str) ({\ |
|||
switch(level) {\ |
|||
case DBG_ERROR: PERRLN(str); break; \ |
|||
case DBG_WARN: PWARNLN(str); break; \ |
|||
case DBG_INFO: PINFOLN(str); break; \ |
|||
case DBG_DEBUG: PDBGLN(str); break; \ |
|||
default: PVERBLN(str); break; \ |
|||
}\ |
|||
}) |
|||
|
|||
#define _DPRINT(level, str) ({\ |
|||
switch(level) {\ |
|||
case DBG_ERROR: _PERR(str); break; \ |
|||
case DBG_WARN: _PWARN(str); break; \ |
|||
case DBG_INFO: _PINFO(str); break; \ |
|||
case DBG_DEBUG: _PDBG(str); break; \ |
|||
default: _PVERB(str); break; \ |
|||
}\ |
|||
}) |
|||
|
|||
#define _DPRINTLN(level, str) ({\ |
|||
switch(level) {\ |
|||
case DBG_ERROR: _PERRLN(str); break; \ |
|||
case DBG_WARN: _PWARNLN(str); break; \ |
|||
case DBG_INFO: _PINFOLN(str); break; \ |
|||
case DBG_DEBUG: _PDBGLN(str); break; \ |
|||
default: _PVERBLN(str); break; \ |
|||
}\ |
|||
}) |
|||
|
|||
//2022-10-30: added hex output
|
|||
#define _DPRINTHEX(level, str) ({\ |
|||
switch(level) {\ |
|||
case DBG_ERROR: _PERRHEX(str); break; \ |
|||
case DBG_WARN: _PWARNHEX(str); break; \ |
|||
case DBG_INFO: _PINFOHEX(str); break; \ |
|||
case DBG_DEBUG: _PDBGHEX(str); break; \ |
|||
default: _PVERBHEX(str); break; \ |
|||
}\ |
|||
}) |
|||
|
|||
#endif /*__DBG_H__*/ |
@ -0,0 +1,154 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
//2022: mb some adaptation for Arduinio Nano version
|
|||
|
|||
#ifndef __DEFINES_H__ |
|||
#define __DEFINES_H__ |
|||
|
|||
#include "config.h" |
|||
|
|||
//-------------------------------------
|
|||
// VERSION
|
|||
//-------------------------------------
|
|||
#define VERSION_MAJOR 0 |
|||
#define VERSION_MINOR 5 |
|||
#define VERSION_PATCH 17+ |
|||
|
|||
|
|||
//-------------------------------------
|
|||
typedef struct { |
|||
uint8_t rfch; |
|||
uint8_t plen; |
|||
uint8_t data[MAX_RF_PAYLOAD_SIZE]; |
|||
} packet_t; |
|||
|
|||
|
|||
typedef struct { |
|||
uint8_t txId; |
|||
uint8_t invId[5]; // 5byte inverter address field is used
|
|||
uint16_t invType; // 2bytes inverter type header e.g. 0x1141 for hoymiles 2 channel type
|
|||
volatile uint32_t ts; //timestamp in millisec
|
|||
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; |
|||
uint8_t len[MAX_PAYLOAD_ENTRIES]; |
|||
bool receive; //indicates that at least one packet was received and stored --> trigger retransmission of missing packets
|
|||
uint8_t maxPackId; //indicates the last fragment id was received and defines the end of payload data packet
|
|||
uint8_t retransmits; |
|||
bool requested; //indicates the sending requst
|
|||
uint8_t rxChIdx; //mb 2022-10-30: added to keep the last rx channel index for the evaluated payload. I think its needed to handle different inverter simultaniously, which can work on different frequencies.
|
|||
bool isMACPacket; |
|||
} invPayload_t; |
|||
|
|||
|
|||
typedef enum { |
|||
InverterDevInform_Simple = 0, // 0x00
|
|||
InverterDevInform_All = 1, // 0x01
|
|||
GridOnProFilePara = 2, // 0x02
|
|||
HardWareConfig = 3, // 0x03
|
|||
SimpleCalibrationPara = 4, // 0x04
|
|||
SystemConfigPara = 5, // 0x05
|
|||
RealTimeRunData_Debug = 11, // 0x0b
|
|||
RealTimeRunData_Reality = 12, // 0x0c
|
|||
RealTimeRunData_A_Phase = 13, // 0x0d
|
|||
RealTimeRunData_B_Phase = 14, // 0x0e
|
|||
RealTimeRunData_C_Phase = 15, // 0x0f
|
|||
AlarmData = 17, // 0x11, Alarm data - all unsent alarms
|
|||
AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms
|
|||
RecordData = 19, // 0x13
|
|||
InternalData = 20, // 0x14
|
|||
GetLossRate = 21, // 0x15
|
|||
GetSelfCheckState = 30, // 0x1e
|
|||
InitDataState = 0xff |
|||
} InfoCmdType; |
|||
|
|||
typedef enum { |
|||
TurnOn = 0, // 0x00
|
|||
TurnOff = 1, // 0x01
|
|||
Restart = 2, // 0x02
|
|||
Lock = 3, // 0x03
|
|||
Unlock = 4, // 0x04
|
|||
ActivePowerContr = 11, // 0x0b
|
|||
ReactivePowerContr = 12, // 0x0c
|
|||
PFSet = 13, // 0x0d
|
|||
CleanState_LockAndAlarm = 20, // 0x14
|
|||
SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files
|
|||
Init = 0xff |
|||
} DevControlCmdType; |
|||
|
|||
typedef enum { // ToDo: to be verified by field tests
|
|||
NoPowerLimit = 0xffff, // ahoy internal value, no hoymiles value!
|
|||
AbsolutNonPersistent = 0UL, // 0x0000
|
|||
RelativNonPersistent = 1UL, // 0x0001
|
|||
AbsolutPersistent = 256UL, // 0x0100
|
|||
RelativPersistent = 257UL // 0x0101
|
|||
} PowerLimitControlType; |
|||
|
|||
|
|||
// minimum serial interval
|
|||
#define MIN_SERIAL_INTERVAL 5 |
|||
|
|||
// minimum send interval
|
|||
#define MIN_SEND_INTERVAL 15 |
|||
|
|||
#define INV_ADDR_LEN MAX_NUM_INVERTERS * 8 // uint64_t
|
|||
#define INV_NAME_LEN MAX_NUM_INVERTERS * MAX_NAME_LENGTH // char[]
|
|||
#define INV_CH_CH_PWR_LEN MAX_NUM_INVERTERS * 2 * 4 // uint16_t (4 channels)
|
|||
#define INV_CH_CH_NAME_LEN MAX_NUM_INVERTERS * MAX_NAME_LENGTH * 4 // (4 channels)
|
|||
#define INV_INTERVAL_LEN 2 // uint16_t
|
|||
#define INV_MAX_RTRY_LEN 1 // uint8_t
|
|||
#define INV_PWR_LIM_LEN MAX_NUM_INVERTERS * 2 // uint16_t
|
|||
|
|||
#define PINOUT_LEN 3 // 3 pins: CS, CE, IRQ
|
|||
#define RF24_AMP_PWR_LEN 1 |
|||
|
|||
#define SER_ENABLE_LEN 1 // uint8_t
|
|||
#define SER_DEBUG_LEN 1 // uint8_t
|
|||
#define SER_INTERVAL_LEN 2 // uint16_t
|
|||
|
|||
typedef struct { |
|||
// nrf24
|
|||
uint16_t sendInterval; |
|||
uint8_t maxRetransPerPyld; |
|||
uint8_t pinCs; |
|||
uint8_t pinCe; |
|||
uint8_t pinIrq; |
|||
uint8_t amplifierPower; |
|||
|
|||
// serial
|
|||
uint16_t serialInterval; |
|||
bool serialShowIv; |
|||
bool serialDebug; |
|||
} /*__attribute__((__packed__))*/ config_t; |
|||
|
|||
|
|||
#define ADDR_CFG ADDR_START_SETTINGS |
|||
#define ADDR_CFG_INVERTER ADDR_CFG + CFG_LEN |
|||
|
|||
#define ADDR_INV_ADDR ADDR_CFG_INVERTER |
|||
#define ADDR_INV_NAME ADDR_INV_ADDR + INV_ADDR_LEN |
|||
#define ADDR_INV_CH_PWR ADDR_INV_NAME + INV_NAME_LEN |
|||
#define ADDR_INV_CH_NAME ADDR_INV_CH_PWR + INV_CH_CH_PWR_LEN |
|||
#define ADDR_INV_INTERVAL ADDR_INV_CH_NAME + INV_CH_CH_NAME_LEN |
|||
#define ADDR_INV_MAX_RTRY ADDR_INV_INTERVAL + INV_INTERVAL_LEN |
|||
#define ADDR_INV_PWR_LIM ADDR_INV_MAX_RTRY + INV_MAX_RTRY_LEN |
|||
#define ADDR_INV_PWR_LIM_CON ADDR_INV_PWR_LIM + INV_PWR_LIM_LEN |
|||
|
|||
#define ADDR_NEXT ADDR_INV_PWR_LIM_CON + INV_PWR_LIM_LEN |
|||
|
|||
|
|||
#define ADDR_SETTINGS_CRC ADDR_NEXT + 2 |
|||
|
|||
#if(ADDR_SETTINGS_CRC <= ADDR_NEXT) |
|||
#pragma error "address overlap! (ADDR_SETTINGS_CRC="+ ADDR_SETTINGS_CRC +", ADDR_NEXT="+ ADDR_NEXT +")" |
|||
#endif |
|||
|
|||
#if(ADDR_SETTINGS_CRC >= 4096 - CRC_LEN) |
|||
#pragma error "EEPROM size exceeded! (ADDR_SETTINGS_CRC="+ ADDR_SETTINGS_CRC +", CRC_LEN="+ CRC_LEN +")" |
|||
#pragma error "Configure less inverters? (MAX_NUM_INVERTERS=" + MAX_NUM_INVERTERS +")" |
|||
#endif |
|||
|
|||
|
|||
|
|||
#endif /*__DEFINES_H__*/ |
@ -0,0 +1,265 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://github.com/lumpapu/ahoy
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
//2022 mb using PROGMEM for lage char* arrays, used and tested for HM800 decoding (2channel), no calculation yet
|
|||
|
|||
#ifndef __HM_DEFINES_H__ |
|||
#define __HM_DEFINES_H__ |
|||
|
|||
#include "dbg.h" |
|||
|
|||
|
|||
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, UNIT_VAR, UNIT_NONE}; |
|||
//const char* const units[] = {" V", " A", " W", " Wh", " kWh", " Hz", " °C", " %", " var", ""};
|
|||
const char PGM_units[][5] PROGMEM {" V", " A", " W", " Wh", " kWh", " Hz", " °C", " %", " var", ""}; |
|||
|
|||
// 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_PF, FLD_EFF, |
|||
FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR, |
|||
FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_HW_ID, |
|||
FLD_ACT_ACTIVE_PWR_LIMIT, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE}; |
|||
|
|||
//PGM Flash memory usage instead of RAM for ARDUINO NANO, idea given by Nick Gammon great webpage http://gammon.com.au/progmem
|
|||
const char PGM_fields[/*NUM ELEMENTS*/][25] PROGMEM { |
|||
{"U_DC"}, |
|||
{"I_DC"}, |
|||
{"P_DC"}, |
|||
{"YieldDay"}, |
|||
{"YieldWeek"}, |
|||
{"YieldTotal"}, |
|||
{"U_AC"}, |
|||
{"I_AC"}, |
|||
{"P_AC"}, |
|||
{"F_AC"}, |
|||
{"Temp"}, |
|||
{"PF_AC"}, |
|||
{"Efficiency"}, |
|||
{"Irradiation"}, |
|||
{"Q_AC"}, |
|||
{"ALARM_MES_ID"}, |
|||
{"FWVersion"}, |
|||
{"FWBuildYear"}, |
|||
{"FWBuildMonthDay"}, |
|||
{"FWBuildHourMinute"}, |
|||
{"HWPartId"}, |
|||
{"active PowerLimit"}, |
|||
/* {"reactive PowerLimit"},
|
|||
{"Powerfactor"}*/ |
|||
{"LastAlarmCode"}, |
|||
}; |
|||
|
|||
|
|||
// const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal",
|
|||
// "U_AC", "I_AC", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
|
|||
// "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId",
|
|||
// "active PowerLimit", /*"reactive PowerLimit","Powerfactor",*/ "LastAlarmCode"};
|
|||
|
|||
const char* const notAvail = "n/a"; |
|||
|
|||
// mqtt discovery device classes
|
|||
enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; |
|||
const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"}; |
|||
enum {STATE_CLS_NONE = 0, STATE_CLS_MEASUREMENT, STATE_CLS_TOTAL_INCREASING}; |
|||
const char* const stateClasses[] = {0, "measurement", "total_increasing"}; |
|||
typedef struct { |
|||
uint8_t fieldId; // field id
|
|||
uint8_t deviceClsId; // device class
|
|||
uint8_t stateClsId; // state class
|
|||
} byteAssign_fieldDeviceClass; |
|||
const byteAssign_fieldDeviceClass deviceFieldAssignment[] = { |
|||
{FLD_UDC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT}, |
|||
{FLD_IDC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT}, |
|||
{FLD_PDC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT}, |
|||
{FLD_YD, DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING}, |
|||
{FLD_YW, DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING}, |
|||
{FLD_YT, DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING}, |
|||
{FLD_UAC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT}, |
|||
{FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT}, |
|||
{FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT}, |
|||
{FLD_F, DEVICE_CLS_FREQ, STATE_CLS_NONE}, |
|||
{FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT}, |
|||
{FLD_PF, DEVICE_CLS_NONE, STATE_CLS_NONE}, |
|||
{FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE}, |
|||
{FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE} |
|||
}; |
|||
#define DEVICE_CLS_ASSIGN_LIST_LEN (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass)) |
|||
|
|||
// indices to calculation functions, defined in hmInverter.h
|
|||
enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH, CALC_PDC_CH0, CALC_EFF_CH0, CALC_IRR_CH}; |
|||
enum {CMD_CALC = 0xffff}; |
|||
|
|||
|
|||
// CH0 is default channel (freq, ac, temp)
|
|||
enum {CH0 = 0, CH1, CH2, CH3, CH4}; |
|||
|
|||
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH}; |
|||
|
|||
|
|||
typedef struct { |
|||
uint8_t fieldId; // field id
|
|||
uint8_t unitId; // uint id
|
|||
uint8_t ch; // channel 0 - 4
|
|||
uint8_t start; // pos of first byte in buffer
|
|||
uint8_t num; // number of bytes in buffer
|
|||
uint16_t div; // divisor / calc command
|
|||
} byteAssign_t; |
|||
|
|||
|
|||
/**
|
|||
* indices are built for the buffer starting with cmd-id in first byte |
|||
* (complete payload in buffer) |
|||
* */ |
|||
|
|||
//-------------------------------------
|
|||
// HM-Series
|
|||
//-------------------------------------
|
|||
const byteAssign_t InfoAssignment[] = { |
|||
{ FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, |
|||
{ FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, |
|||
{ FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, |
|||
{ FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 }, |
|||
{ FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } |
|||
}; |
|||
#define HMINFO_LIST_LEN (sizeof(InfoAssignment) / sizeof(byteAssign_t)) |
|||
#define HMINFO_PAYLOAD_LEN 14 |
|||
|
|||
const byteAssign_t SystemConfigParaAssignment[] = { |
|||
{ FLD_ACT_ACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 2, 2, 10 }/*,
|
|||
{ FLD_ACT_REACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 4, 2, 10 }, |
|||
{ FLD_ACT_PF, UNIT_NONE, CH0, 6, 2, 1000 }*/ |
|||
}; |
|||
#define HMSYSTEM_LIST_LEN (sizeof(SystemConfigParaAssignment) / sizeof(byteAssign_t)) |
|||
#define HMSYSTEM_PAYLOAD_LEN 14 |
|||
|
|||
const byteAssign_t AlarmDataAssignment[] = { |
|||
{ FLD_LAST_ALARM_CODE, UNIT_NONE, CH0, 0, 2, 1 } |
|||
}; |
|||
#define HMALARMDATA_LIST_LEN (sizeof(AlarmDataAssignment) / sizeof(byteAssign_t)) |
|||
#define HMALARMDATA_PAYLOAD_LEN 0 // 0: means check is off
|
|||
|
|||
|
|||
|
|||
//-------------------------------------
|
|||
// HM300, HM350, HM400
|
|||
//-------------------------------------
|
|||
const byteAssign_t hm1chAssignment[] = { |
|||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, |
|||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, |
|||
{ FLD_PDC, UNIT_W, CH1, 6, 2, 10 }, |
|||
{ FLD_YD, UNIT_WH, CH1, 12, 2, 1 }, |
|||
{ FLD_YT, UNIT_KWH, CH1, 8, 4, 1000 }, |
|||
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, |
|||
|
|||
{ FLD_UAC, UNIT_V, CH0, 14, 2, 10 }, |
|||
{ FLD_IAC, UNIT_A, CH0, 22, 2, 100 }, |
|||
{ FLD_PAC, UNIT_W, CH0, 18, 2, 10 }, |
|||
{ FLD_Q, UNIT_VAR, CH0, 20, 2, 10 }, |
|||
{ FLD_F, UNIT_HZ, CH0, 16, 2, 100 }, |
|||
{ FLD_PF, UNIT_NONE, CH0, 24, 2, 1000 }, |
|||
{ FLD_T, UNIT_C, CH0, 26, 2, 10 }, |
|||
{ FLD_EVT, UNIT_NONE, CH0, 28, 2, 1 }, |
|||
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, |
|||
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, |
|||
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, |
|||
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } |
|||
}; |
|||
#define HM1CH_LIST_LEN (sizeof(hm1chAssignment) / sizeof(byteAssign_t)) |
|||
#define HM1CH_PAYLOAD_LEN 30 |
|||
|
|||
|
|||
//-------------------------------------
|
|||
// HM600, HM700, HM800
|
|||
//-------------------------------------
|
|||
const byteAssign_t hm2chAssignment[] = { |
|||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, |
|||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, |
|||
{ FLD_PDC, UNIT_W, CH1, 6, 2, 10 }, |
|||
{ FLD_YD, UNIT_WH, CH1, 22, 2, 1 }, |
|||
{ FLD_YT, UNIT_KWH, CH1, 14, 4, 1000 }, |
|||
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, |
|||
|
|||
{ FLD_UDC, UNIT_V, CH2, 8, 2, 10 }, |
|||
{ FLD_IDC, UNIT_A, CH2, 10, 2, 100 }, |
|||
{ FLD_PDC, UNIT_W, CH2, 12, 2, 10 }, |
|||
{ FLD_YD, UNIT_WH, CH2, 24, 2, 1 }, |
|||
{ FLD_YT, UNIT_KWH, CH2, 18, 4, 1000 }, |
|||
{ FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC }, |
|||
|
|||
{ FLD_UAC, UNIT_V, CH0, 26, 2, 10 }, |
|||
{ FLD_IAC, UNIT_A, CH0, 34, 2, 100 }, |
|||
{ FLD_PAC, UNIT_W, CH0, 30, 2, 10 }, |
|||
{ FLD_Q, UNIT_VAR, CH0, 32, 2, 10 }, |
|||
{ FLD_F, UNIT_HZ, CH0, 28, 2, 100 }, |
|||
{ FLD_PF, UNIT_NONE, CH0, 36, 2, 1000 }, |
|||
{ FLD_T, UNIT_C, CH0, 38, 2, 10 }, |
|||
{ FLD_EVT, UNIT_NONE, CH0, 40, 2, 1 }, |
|||
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, |
|||
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, |
|||
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, |
|||
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } |
|||
|
|||
}; |
|||
#define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t)) |
|||
#define HM2CH_PAYLOAD_LEN 42 |
|||
|
|||
|
|||
//-------------------------------------
|
|||
// HM1200, HM1500
|
|||
//-------------------------------------
|
|||
const byteAssign_t hm4chAssignment[] = { |
|||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, |
|||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, |
|||
{ FLD_PDC, UNIT_W, CH1, 8, 2, 10 }, |
|||
{ FLD_YD, UNIT_WH, CH1, 20, 2, 1 }, |
|||
{ FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 }, |
|||
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, |
|||
|
|||
{ FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC }, |
|||
{ FLD_IDC, UNIT_A, CH2, 6, 2, 100 }, |
|||
{ FLD_PDC, UNIT_W, CH2, 10, 2, 10 }, |
|||
{ FLD_YD, UNIT_WH, CH2, 22, 2, 1 }, |
|||
{ FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 }, |
|||
{ FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC }, |
|||
|
|||
{ FLD_UDC, UNIT_V, CH3, 24, 2, 10 }, |
|||
{ FLD_IDC, UNIT_A, CH3, 26, 2, 100 }, |
|||
{ FLD_PDC, UNIT_W, CH3, 30, 2, 10 }, |
|||
{ FLD_YD, UNIT_WH, CH3, 42, 2, 1 }, |
|||
{ FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 }, |
|||
{ FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC }, |
|||
|
|||
{ FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC }, |
|||
{ FLD_IDC, UNIT_A, CH4, 28, 2, 100 }, |
|||
{ FLD_PDC, UNIT_W, CH4, 32, 2, 10 }, |
|||
{ FLD_YD, UNIT_WH, CH4, 44, 2, 1 }, |
|||
{ FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 }, |
|||
{ FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC }, |
|||
|
|||
{ FLD_UAC, UNIT_V, CH0, 46, 2, 10 }, |
|||
{ FLD_IAC, UNIT_A, CH0, 54, 2, 100 }, |
|||
{ FLD_PAC, UNIT_W, CH0, 50, 2, 10 }, |
|||
{ FLD_Q, UNIT_VAR, CH0, 52, 2, 10 }, |
|||
{ FLD_F, UNIT_HZ, CH0, 48, 2, 100 }, |
|||
{ FLD_PF, UNIT_NONE, CH0, 56, 2, 1000 }, |
|||
{ FLD_T, UNIT_C, CH0, 58, 2, 10 }, |
|||
{ FLD_EVT, UNIT_NONE, CH0, 60, 2, 1 }, |
|||
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, |
|||
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, |
|||
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, |
|||
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC } |
|||
}; |
|||
#define HM4CH_LIST_LEN (sizeof(hm4chAssignment) / sizeof(byteAssign_t)) |
|||
#define HM4CH_PAYLOAD_LEN 62 |
|||
|
|||
|
|||
#endif /*__HM_DEFINES_H__*/ |
@ -0,0 +1,549 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
// 2022 mb modified for AHOY-UL (Hoymiles Arduino Nano, USB light IF)
|
|||
// - RF handling and function sendPacket_raw() without automatic channel increment
|
|||
// - loop() saves new packet fragments only to save space
|
|||
|
|||
#ifndef __RADIO_H__ |
|||
#define __RADIO_H__ |
|||
|
|||
#include <RF24.h> |
|||
#include <RF24_config.h> |
|||
#include <SPI.h> |
|||
|
|||
#include "CircularBuffer.h" |
|||
#include "config.h" |
|||
#include "crc.h" |
|||
#include "dbg.h" |
|||
|
|||
#define DEFAULT_RECV_CHANNEL 3 |
|||
#define SPI_SPEED 1000000 |
|||
#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) |
|||
#define RF_CHANNELS 5 |
|||
#define RF_LOOP_CNT 300 |
|||
|
|||
// used in main.cpp for response catching and data requests
|
|||
#define TX_REQ_INFO 0X15 |
|||
#define TX_REQ_DEVCONTROL 0x51 |
|||
#define ALL_FRAMES 0x80 |
|||
#define SINGLE_FRAME 0x81 |
|||
|
|||
const char *const rf24AmpPowerNames[] = {"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, class BUFFER, uint64_t DTU_ID = DTU_RADIO_ID> |
|||
class HmRadio { |
|||
uint8_t mTxCh; |
|||
uint8_t mRxCh; // only dummy use
|
|||
uint8_t mTxChIdx; |
|||
uint8_t mRfChLst[RF_CHANNELS]; |
|||
uint8_t mRxChIdx; |
|||
uint16_t mRxLoopCnt; |
|||
|
|||
RF24 mNrf24; |
|||
// RF24 mNrf24(CE_PIN, CS_PIN, SPI_SPEED);
|
|||
|
|||
BUFFER *mBufCtrl; |
|||
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; |
|||
DevControlCmdType DevControlCmd; |
|||
volatile bool mIrqRcvd; |
|||
|
|||
uint8_t last_rxdata[2]; |
|||
|
|||
public: |
|||
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { |
|||
// HmRadio() : mNrf24(CE_PIN, CS_PIN) {
|
|||
|
|||
// Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz.
|
|||
// Channel List 2403, 2423, 2440, 2461, 2475MHz
|
|||
mRfChLst[0] = 03; |
|||
mRfChLst[1] = 23; |
|||
mRfChLst[2] = 40; |
|||
mRfChLst[3] = 61; |
|||
mRfChLst[4] = 75; |
|||
|
|||
mTxChIdx = 2; |
|||
mRxChIdx = 4; |
|||
|
|||
mRxLoopCnt = RF_LOOP_CNT; |
|||
mSendCnt = 0; |
|||
mSerialDebug = true; |
|||
mIrqRcvd = true; |
|||
} |
|||
|
|||
~HmRadio() {} |
|||
|
|||
void setup(config_t *config, BUFFER *ctrl) { |
|||
DPRINT(DBG_VERBOSE, F("hmRadio.h: setup(CE_PIN: ")); |
|||
_DPRINT(DBG_VERBOSE, config->pinCe); |
|||
_DPRINT(DBG_VERBOSE, F(", CS_PIN: ")); |
|||
_DPRINT(DBG_VERBOSE, config->pinCs); |
|||
_DPRINT(DBG_VERBOSE, F(", SPI_SPEED: ")); |
|||
_DPRINT(DBG_VERBOSE, SPI_SPEED); |
|||
_DPRINTLN(DBG_VERBOSE, F(")")); |
|||
delay(100); |
|||
|
|||
pinMode(config->pinIrq, INPUT_PULLUP); |
|||
mBufCtrl = ctrl; |
|||
|
|||
// mNrf24.begin(config->pinCe, config->pinCs);
|
|||
mNrf24.begin(); |
|||
mNrf24.setRetries(0, 0); |
|||
mNrf24.setChannel(DEFAULT_RECV_CHANNEL); |
|||
mNrf24.setDataRate(RF24_250KBPS); |
|||
mNrf24.setCRCLength(RF24_CRC_16); |
|||
mNrf24.setAutoAck(false); |
|||
mNrf24.setPayloadSize(MAX_RF_PAYLOAD_SIZE); |
|||
mNrf24.setAddressWidth(5); |
|||
mNrf24.openReadingPipe(1, DTU_RADIO_ID); |
|||
mNrf24.enableDynamicPayloads(); |
|||
|
|||
// enable only receiving interrupts
|
|||
mNrf24.maskIRQ(true, true, false); |
|||
|
|||
DPRINT(DBG_VERBOSE, F("RF24 Amp Pwr: RF24_PA_")); |
|||
_DPRINT(DBG_VERBOSE, rf24AmpPowerNames[config->amplifierPower]); |
|||
mNrf24.setPALevel(config->amplifierPower & 0x03); |
|||
mNrf24.startListening(); |
|||
|
|||
if (DEBUG_LEVEL >= DBG_VERBOSE) { |
|||
DPRINTLN(DBG_VERBOSE, F("Radio Config: ")); |
|||
mNrf24.printPrettyDetails(); |
|||
} |
|||
|
|||
mTxCh = setDefaultChannels(); |
|||
mRxCh = 0; // only dummy
|
|||
|
|||
if (!mNrf24.isChipConnected()) { |
|||
DPRINT(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* writes to circular packet_t buffer on Rx interrupt data |
|||
*/ |
|||
bool loop(void) { |
|||
volatile bool _status; |
|||
volatile uint8_t dlen; |
|||
_status = false; |
|||
DISABLE_IRQ; |
|||
if (mIrqRcvd) { |
|||
mIrqRcvd = false; |
|||
bool tx_ok, tx_fail, rx_ready; |
|||
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
|
|||
RESTORE_IRQ; |
|||
uint8_t pipe; //, len;
|
|||
packet_t *p; |
|||
while (mNrf24.available(&pipe)) { |
|||
DPRINT(DBG_DEBUG, F("RxAvail ")); |
|||
_DPRINT(DBG_DEBUG, pipe); |
|||
if (!mBufCtrl->full()) { |
|||
_status = true; |
|||
p = mBufCtrl->getFront(); // init pointer address with circular buffer space
|
|||
p->rfch = mRxCh; |
|||
p->plen = mNrf24.getPayloadSize(); |
|||
// p->plen = mNrf24.getDynamicPayloadSize(); //is not the real RF payload fragment (packet) length, calculate later
|
|||
if (p->plen < 0) { |
|||
DPRINT(DBG_ERROR, F("Rx plen false")); |
|||
mNrf24.flush_rx(); // drop the packet and leave
|
|||
return false; |
|||
} else if (p->plen > MAX_RF_PAYLOAD_SIZE) { |
|||
p->plen = MAX_RF_PAYLOAD_SIZE; |
|||
} // end if-else
|
|||
mNrf24.read(p->data, p->plen); |
|||
|
|||
// only store new packets, try to get the CRC part only for matching
|
|||
// PACKET_BUFFER_SIZE can be reduced to max number of expected packets, e.g. 6...8 for 4 channel inverter (payload of 62 bytes)
|
|||
dlen = p->data[0]; // get decoded data len
|
|||
dlen = dlen >> 2; |
|||
if (dlen < MAX_RF_PAYLOAD_SIZE) { |
|||
if (p->data[dlen - 2] == last_rxdata[0] && p->data[dlen - 1] == last_rxdata[1]) { |
|||
_DPRINT(DBG_DEBUG, F("-> skip")); |
|||
continue; |
|||
} // if()
|
|||
_DPRINT(DBG_DEBUG, F("-> take")); |
|||
last_rxdata[0] = p->data[dlen - 2]; |
|||
last_rxdata[1] = p->data[dlen - 1]; |
|||
} // end if()
|
|||
mBufCtrl->pushFront(p); |
|||
|
|||
} else |
|||
break; |
|||
} // end while()
|
|||
DPRINT(DBG_DEBUG, F("Rx buf ")); |
|||
_DPRINT(DBG_DEBUG, mBufCtrl->available()); |
|||
mNrf24.flush_rx(); // drop the packet
|
|||
} |
|||
RESTORE_IRQ; |
|||
return _status; |
|||
} // end loop()
|
|||
|
|||
void handleIntr(void) { |
|||
DPRINT(DBG_DEBUG, F(" i")); |
|||
mIrqRcvd = true; |
|||
} |
|||
|
|||
uint8_t setDefaultChannels(void) { |
|||
// DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setDefaultChannels"));
|
|||
mTxChIdx = 2; // Start TX with 40
|
|||
mRxChIdx = 4; // Start RX with 75
|
|||
return mRfChLst[mTxChIdx]; |
|||
} |
|||
|
|||
void sendControlPacket(uint8_t *_radio_id, uint8_t cmd, uint16_t *data) { |
|||
DPRINTLN(DBG_VERBOSE, F("hmRadio:sendControlPacket")); |
|||
sendCmdPacket(_radio_id, TX_REQ_DEVCONTROL, ALL_FRAMES, false); // 0x80 implementation as original DTU code
|
|||
int cnt = 0; |
|||
mTxBuf[10] = cmd; // cmd --> 0x0b => Type_ActivePowerContr, 0 on, 1 off, 2 restart, 12 reactive power, 13 power factor
|
|||
mTxBuf[10 + (++cnt)] = 0x00; |
|||
if (cmd >= ActivePowerContr && cmd <= PFSet) { |
|||
mTxBuf[10 + (++cnt)] = ((data[0] * 10) >> 8) & 0xff; // power limit
|
|||
mTxBuf[10 + (++cnt)] = ((data[0] * 10)) & 0xff; // power limit
|
|||
mTxBuf[10 + (++cnt)] = ((data[1]) >> 8) & 0xff; // setting for persistens handlings
|
|||
mTxBuf[10 + (++cnt)] = ((data[1])) & 0xff; // setting for persistens handling
|
|||
} |
|||
// crc control data
|
|||
uint16_t crc = Hoymiles::crc16(&mTxBuf[10], cnt + 1); |
|||
mTxBuf[10 + (++cnt)] = (crc >> 8) & 0xff; |
|||
mTxBuf[10 + (++cnt)] = (crc)&0xff; |
|||
// crc over all
|
|||
cnt += 1; |
|||
mTxBuf[10 + cnt] = Hoymiles::crc8(mTxBuf, 10 + cnt); |
|||
|
|||
sendPacket(_radio_id, mTxBuf, 10 + (++cnt), true); |
|||
} |
|||
|
|||
void sendTimePacket(uint8_t *_radio_id, uint8_t cmd, uint32_t ts, uint16_t alarmMesId) { |
|||
// DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendTimePacket"));
|
|||
sendCmdPacket(_radio_id, TX_REQ_INFO, ALL_FRAMES, false); |
|||
mTxBuf[10] = cmd; // cid
|
|||
mTxBuf[11] = 0x00; |
|||
CP_U32_LittleEndian(&mTxBuf[12], ts); //?? adapt for atmega/nano
|
|||
// dumpBuf(" ", &mTxBuf[12], 4);
|
|||
|
|||
if (cmd == RealTimeRunData_Debug || cmd == AlarmData) { |
|||
mTxBuf[18] = (alarmMesId >> 8) & 0xff; |
|||
mTxBuf[19] = (alarmMesId)&0xff; |
|||
} else { |
|||
mTxBuf[18] = 0x00; |
|||
mTxBuf[19] = 0x00; |
|||
} |
|||
uint16_t crc = Hoymiles::crc16(&mTxBuf[10], 14); |
|||
mTxBuf[24] = (crc >> 8) & 0xff; |
|||
mTxBuf[25] = (crc)&0xff; |
|||
mTxBuf[26] = Hoymiles::crc8(mTxBuf, 26); |
|||
|
|||
sendPacket(_radio_id, mTxBuf, 27, true); |
|||
} |
|||
|
|||
void sendCmdPacket(uint8_t *_radio_id, uint8_t mid, uint8_t pid, bool calcCrc = true) { |
|||
// DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendCmdPacket"));
|
|||
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); |
|||
mTxBuf[0] = mid; // message id
|
|||
// CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); // this must match with the given invID format, big endian is needed because the id from the plate is entered big endian
|
|||
memcpy(&mTxBuf[1], &_radio_id[1], 4); // copy 4bytes of radio_id to right possition at txbuffer
|
|||
CP_U32_BigEndian(&mTxBuf[5], (DTU_ID >> 8)); // needed on Arduino nano
|
|||
mTxBuf[9] = pid; |
|||
if (calcCrc) { |
|||
mTxBuf[10] = Hoymiles::crc8(mTxBuf, 10); |
|||
sendPacket(_radio_id, mTxBuf, 11, false); |
|||
} |
|||
} |
|||
|
|||
bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) { |
|||
// DPRINTLN(DBG_VERBOSE, F("hmRadio.h:checkPaketCrc"));
|
|||
|
|||
// will be returned by pointer value
|
|||
*len = (buf[0] >> 2); |
|||
|
|||
// DPRINT(DBG_INFO, String(F("RX crc_in ")) + String(*len) + String(F("B Ch")) + String(rxCh) + String(F(" | ")));
|
|||
// dumpBuf(NULL, buf, *len, DBG_DEBUG);
|
|||
|
|||
if (*len > (MAX_RF_PAYLOAD_SIZE - 2)) |
|||
*len = MAX_RF_PAYLOAD_SIZE - 2; |
|||
|
|||
for (uint8_t i = 1; i < (*len + 1); i++) { |
|||
buf[i - 1] = (buf[i] << 1) | (buf[i + 1] >> 7); |
|||
} |
|||
|
|||
// DPRINT(DBG_INFO, String(F("RX crc_out ")) + String(*len) + String(F("B Ch")) + String(rxCh) + String(F(" | ")));
|
|||
// dumpBuf(NULL, buf, *len, DBG_DEBUG);
|
|||
|
|||
volatile uint8_t crc = Hoymiles::crc8(buf, *len - 1); |
|||
volatile bool valid = (crc == buf[*len - 1]); |
|||
|
|||
DPRINT(DBG_DEBUG, F("RX crc-valid ")); |
|||
_DPRINT(DBG_DEBUG, valid); |
|||
return valid; |
|||
} |
|||
|
|||
bool switchRxCh(uint16_t addLoop = 0) { |
|||
DPRINT(DBG_DEBUG, F("hmRadio.h:switchRxCh: try")); |
|||
// DPRINTLN(DBG_VERBOSE, F("R"));
|
|||
|
|||
mRxLoopCnt += addLoop; |
|||
if (mRxLoopCnt != 0) { |
|||
mRxLoopCnt--; |
|||
DISABLE_IRQ; |
|||
mNrf24.stopListening(); |
|||
mNrf24.setChannel(getRxNxtChannel()); |
|||
mNrf24.startListening(); |
|||
RESTORE_IRQ; |
|||
} |
|||
return (0 == mRxLoopCnt); // receive finished
|
|||
} |
|||
|
|||
/**
|
|||
* Hex string output of uint8_t array |
|||
*/ |
|||
void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { |
|||
if (NULL != info) DBGPRINT(info); |
|||
for (uint8_t i = 0; i < len; i++) { |
|||
DHEX(buf[i]); |
|||
if (i % 10 == 0) DBGPRINT(" "); |
|||
} // end for()
|
|||
} |
|||
|
|||
/**
|
|||
* Hex output with debug-flag dependancy |
|||
*/ |
|||
void dumpBuf(int _thisdebug, const char *info, uint8_t buf[], uint8_t len) { |
|||
// DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf"));
|
|||
if (_thisdebug <= DEBUG_LEVEL) { |
|||
dumpBuf(info, buf, len); |
|||
} // end if(debug)
|
|||
// DBGPRINTLN("");
|
|||
} |
|||
|
|||
bool isChipConnected(void) { |
|||
// DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
|
|||
return mNrf24.isChipConnected(); |
|||
} |
|||
|
|||
uint32_t mSendCnt; |
|||
bool mSerialDebug; |
|||
|
|||
/**
|
|||
* send the buf content to the tx-channel, the rx-channel is switched to two higher index |
|||
*/ |
|||
uint8_t sendPacket_raw(uint8_t *_radio_id, packet_t *_p, uint8_t _rxch) { |
|||
// void sendPacket_raw(uint64_t _radio_id64, packet_t *_p, uint8_t _rxch) {
|
|||
// DPRINT(DBG_DEBUG, F("sendPacket_raw"));
|
|||
uint8_t _arc = 0; |
|||
|
|||
DPRINT(DBG_INFO, F("TXraw Ch")); |
|||
if (_p->rfch) _DPRINT(DBG_INFO, F("0")); |
|||
_DPRINT(DBG_INFO, _p->rfch); |
|||
_DPRINT(DBG_INFO, F(" ")); |
|||
_DPRINT(DBG_INFO, _p->plen); |
|||
_DPRINT(DBG_INFO, F("B | ")); |
|||
dumpBuf(DBG_INFO, NULL, _p->data, _p->plen); |
|||
|
|||
DISABLE_IRQ; |
|||
mNrf24.stopListening(); |
|||
mNrf24.setChannel(_p->rfch); |
|||
// mTxCh = getTxNxtChannel(); // prepare Tx channel for next packet
|
|||
DPRINT(DBG_DEBUG, F("RF24 addr ")); |
|||
dumpBuf(DBG_DEBUG, NULL, _radio_id, 5); |
|||
delay(25); // wait serial data sending before RF transmission, otherwise missing first RF packet
|
|||
/*
|
|||
if(mSerialDebug) { |
|||
DPRINT(DBG_INFO, "TX iv-ID 0x"); |
|||
DHEX((uint32_t)(_radio_id64>>32)); Serial.print(F(" ")); DHEX((uint32_t)(_radio_id64)); |
|||
} |
|||
*/ |
|||
// mNrf24.openWritingPipe(_radio_id64); // 2022-10-17: mb, addr taken from buf (must be transformed to big endian)
|
|||
mNrf24.openWritingPipe(_radio_id); // 2022-10-17: mb, addr taken from _p->data must be transformed to big endian,
|
|||
mNrf24.setCRCLength(RF24_CRC_16); |
|||
mNrf24.enableDynamicPayloads(); |
|||
mNrf24.setAutoAck(true); |
|||
mNrf24.setRetries(3, 15); // 3*250us and 15 loops -> 11.25ms
|
|||
mNrf24.write(_p->data, _p->plen); |
|||
//_arc = mNrf24.getARC();
|
|||
|
|||
// Try to avoid zero payload acks (has no effect)
|
|||
mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated
|
|||
// mNrf24.setChannel(getRxChannel(_p->rfch)); // switch to Rx channel that matches to Tx channel
|
|||
setRxChanIdx(getChanIdx(_rxch)); |
|||
mNrf24.setChannel(_rxch); // switch to Rx channel
|
|||
mNrf24.setAutoAck(false); |
|||
mNrf24.setRetries(0, 0); |
|||
mNrf24.disableDynamicPayloads(); |
|||
mNrf24.setCRCLength(RF24_CRC_DISABLED); |
|||
mNrf24.startListening(); |
|||
RESTORE_IRQ; |
|||
|
|||
_DPRINT(DBG_INFO, F(" ->ARC ")); _DPRINT(DBG_INFO, _arc); |
|||
DPRINT(DBG_INFO, "RX Ch"); |
|||
if (_rxch < 10) _DPRINT(DBG_INFO, F("0")); |
|||
_DPRINT(DBG_INFO, _rxch); |
|||
_DPRINT(DBG_INFO, " wait"); |
|||
return _arc; |
|||
} |
|||
|
|||
uint8_t getRxChannel(uint8_t _txch) { |
|||
uint8_t ix; |
|||
ix = getChanIdx(_txch); |
|||
ix = (ix + 2) % RF_CHANNELS; |
|||
return mRfChLst[mRxChIdx]; |
|||
} |
|||
|
|||
uint8_t getRxChan() { |
|||
return mRfChLst[mRxChIdx]; |
|||
} |
|||
|
|||
uint8_t getChanIdx(uint8_t _channel) { |
|||
// uses global channel list
|
|||
static uint8_t ix; |
|||
for (ix = 0; ix < RF_CHANNELS; ix++) { |
|||
if (_channel == mRfChLst[ix]) break; |
|||
} |
|||
return ix; |
|||
} |
|||
|
|||
void setRxChanIdx(uint8_t ix) { |
|||
mRxChIdx = ix; |
|||
} |
|||
|
|||
void print_radio_details() { |
|||
mNrf24.printPrettyDetails(); |
|||
} |
|||
|
|||
// todo: scan all channel for 1bit rdp value (1 == >=-64dBm, 0 == <-64dBm)
|
|||
void scanRF(void) { |
|||
bool _rdp = 0; |
|||
DISABLE_IRQ; |
|||
DPRINT(DBG_INFO, F("RDP[0...124] ")); |
|||
for (uint8_t _ch = 0; _ch < 125; _ch++) { |
|||
if (_ch % 10 == 0) _DPRINT(DBG_INFO, " "); |
|||
mNrf24.setChannel(_ch); |
|||
mNrf24.startListening(); |
|||
_DPRINT(DBG_INFO, mNrf24.testRPD()); |
|||
} |
|||
RESTORE_IRQ; |
|||
} |
|||
|
|||
private: |
|||
/**
|
|||
* That is the actual send function |
|||
*/ |
|||
bool sendPacket(uint8_t *_radio_id, uint8_t buf[], uint8_t len, bool clear = false, bool doSend = true) { |
|||
// DPRINT(DBG_DEBUG, F("hmRadio.h:sendPacket"));
|
|||
//bool _tx_ok = 0; // could also query the num of tx retries as uplink quality indication, no lib interface seen yet
|
|||
uint8_t _arc = 0; |
|||
|
|||
|
|||
// DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt));
|
|||
// dumpBuf("SEN ", buf, len);
|
|||
if (mSerialDebug) { |
|||
DPRINT(DBG_INFO, F("TX Ch")); |
|||
if (mRfChLst[mTxChIdx] < 10) _DPRINT(DBG_INFO, F("0")); |
|||
_DPRINT(DBG_INFO, mRfChLst[mTxChIdx]); |
|||
_DPRINT(DBG_INFO, F(" ")); |
|||
_DPRINT(DBG_INFO, len); |
|||
_DPRINT(DBG_INFO, F("B | ")); |
|||
dumpBuf(DBG_INFO, NULL, buf, len); |
|||
|
|||
DPRINT(DBG_VERBOSE, F("TX iv-ID 0x")); |
|||
dumpBuf(DBG_VERBOSE, NULL, _radio_id, 5); |
|||
// DPRINT(DBG_INFO, "TX iv-ID 0x");
|
|||
// DHEX((uint32_t)(invId>>32)); Serial.print(F(" ")); DHEX((uint32_t)(invId));
|
|||
delay(20); // wait serial data sending before RF transmission, otherwise missing first RF packet
|
|||
} |
|||
|
|||
DISABLE_IRQ; |
|||
mNrf24.stopListening(); |
|||
if (clear) |
|||
mRxLoopCnt = RF_LOOP_CNT; |
|||
mNrf24.setChannel(mRfChLst[mTxChIdx]); |
|||
mTxCh = getTxNxtChannel(); // prepare Tx channel for next packet
|
|||
|
|||
// mNrf24.openWritingPipe(invId); // TODO: deprecated, !!!! invID must be given in big endian uint8_t*
|
|||
mNrf24.openWritingPipe(_radio_id); |
|||
mNrf24.setCRCLength(RF24_CRC_16); |
|||
mNrf24.enableDynamicPayloads(); |
|||
mNrf24.setAutoAck(true); |
|||
mNrf24.setRetries(3, 15); // 3*250us and 15 loops -> 11.25ms, I guess Hoymiles has disabled autoack, thus always max loops send
|
|||
if (doSend) mNrf24.write(buf, len); // only send in case of send-flag true, _tx_ok seems to be always false
|
|||
//_arc = mNrf24.getARC(); // is always 15, hoymiles receiver might have autoack=false
|
|||
|
|||
// Try to avoid zero payload acks (has no effect)
|
|||
mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated
|
|||
// mRxChIdx = 0;
|
|||
mNrf24.setChannel(mRfChLst[mRxChIdx]); // switch to Rx channel that matches to Tx channel
|
|||
mRxCh = mRfChLst[mRxChIdx]; |
|||
mNrf24.setAutoAck(false); |
|||
mNrf24.setRetries(0, 0); |
|||
mNrf24.disableDynamicPayloads(); |
|||
mNrf24.setCRCLength(RF24_CRC_DISABLED); |
|||
mNrf24.startListening(); |
|||
RESTORE_IRQ; |
|||
|
|||
if (mSerialDebug) { |
|||
//_DPRINT(DBG_INFO, F(" ->ARC ")); _DPRINT(DBG_INFO, _arc);
|
|||
|
|||
DPRINT(DBG_VERBOSE, "RX Ch"); |
|||
if (mRfChLst[mRxChIdx] < 10) _DPRINT(DBG_VERBOSE, F("0")); |
|||
_DPRINT(DBG_VERBOSE, mRfChLst[mRxChIdx]); |
|||
_DPRINT(DBG_VERBOSE, F(" wait")); |
|||
getRxNxtChannel(); // prepare Rx channel for next packet
|
|||
} |
|||
|
|||
mSendCnt++; |
|||
return true; |
|||
} |
|||
|
|||
uint8_t getTxNxtChannel(void) { |
|||
if (++mTxChIdx >= RF_CHANNELS) |
|||
mTxChIdx = 0; |
|||
// DPRINT(DBG_DEBUG, F("next TX Ch"));
|
|||
//_DPRINT(DBG_DEBUG, mRfChLst[mTxChIdx]);
|
|||
return mRfChLst[mTxChIdx]; |
|||
} |
|||
|
|||
uint8_t getRxNxtChannel(void) { |
|||
if (++mRxChIdx >= RF_CHANNELS) |
|||
mRxChIdx = 0; |
|||
// PRINT(DBG_DEBUG, F("next RX Ch"));
|
|||
//_DPRINT(DBG_DEBUG, mRfChLst[mRxChIdx]);
|
|||
return mRfChLst[mRxChIdx]; |
|||
} |
|||
|
|||
void setChanIdx(uint8_t _txch) { |
|||
mTxChIdx = getChanIdx(_txch); |
|||
mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; |
|||
} |
|||
|
|||
uint8_t getRxChannel(void) { |
|||
return mRxCh; |
|||
} |
|||
|
|||
}; // end class
|
|||
|
|||
#endif /*__RADIO_H__*/ |
@ -0,0 +1,834 @@ |
|||
//-----------------------------------------------------------------------------
|
|||
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
|
|||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
//
|
|||
// 2022: mb, AHOY-src modified for AHOY-UL (Hoymiles, USB light IF) for Arduino Nano (also works on ESP8266 without WIFI/BT)
|
|||
// The HW is same as described in AHOY project.
|
|||
// The Hoymiles inverter (e.g. HM800) is accessed via NRF24L01+.
|
|||
// The interface is USB2serial for input and output.
|
|||
// There are two modes of operation:
|
|||
// - automode: one REQUEST message is polled periodically and decoded payload is given by serial-IF (@57600baud), some comfig inputs possible
|
|||
// - smac-mode: -> The hoymiles specific REQUEST messages must be given as input via serial-IF (@57600baud)
|
|||
// <- The full sorted RESPONSE is given to the serial-IF with as smac-packet (to be used with python, fhem, etc.)
|
|||
//
|
|||
|
|||
#include <Arduino.h> |
|||
#include <printf.h> |
|||
#include <stdint.h> |
|||
#include <stdio.h> |
|||
|
|||
#include <RF24.h> |
|||
#include <RF24_config.h> |
|||
#include <SPI.h> |
|||
|
|||
#include "CircularBuffer.h" |
|||
#include "config.h" |
|||
#include "dbg.h" |
|||
#include "defines.h" |
|||
#include "hmDefines.h" |
|||
#include "hmRadio.h" |
|||
#include "utils_serial.h" |
|||
|
|||
typedef CircularBuffer<packet_t, PACKET_BUFFER_SIZE> BufferType; |
|||
typedef HmRadio<DEF_RF24_CE_PIN, DEF_RF24_CS_PIN, BufferType> RadioType; |
|||
// typedef Inverter<float> InverterType;
|
|||
// typedef HmSystem<RadioType, BufferType, MAX_NUM_INVERTERS, InverterType> HmSystemType;
|
|||
|
|||
// declaration of functions
|
|||
static void resetSystem(void); |
|||
static void loadDefaultConfig(config_t *_mconfig); |
|||
// utility function
|
|||
static int availableMemory(void); |
|||
static void swap_bytes(uint8_t *, uint32_t); |
|||
static uint32_t swap_bytes(uint32_t); |
|||
static bool check_array(uint8_t *, uint8_t *, uint8_t); |
|||
// low level packet payload and inverter handling
|
|||
static uint8_t getInvID(invPayload_t *, uint8_t, uint8_t *); |
|||
static uint8_t getNumInv(invPayload_t *, uint8_t); |
|||
static bool copyPacket2Payload(invPayload_t *, uint8_t, packet_t *, bool); |
|||
static bool savePayloadfragment(invPayload_t *, packet_t *, uint8_t, uint8_t); |
|||
static bool processPayload(invPayload_t *, config_t *, bool); |
|||
static uint8_t checkPayload(invPayload_t *); |
|||
static bool resetPayload(invPayload_t *); |
|||
// output and decoding
|
|||
static bool returnMACPackets(invPayload_t *); |
|||
static uint8_t returnPayload(invPayload_t *, uint8_t *, uint8_t); |
|||
static void decodePayload(uint8_t, uint8_t *, uint8_t, uint32_t, char*, uint8_t, uint16_t); |
|||
//interrupt handler
|
|||
static void handleIntr(void); |
|||
|
|||
// sysConfig_t mSysConfig;
|
|||
static config_t mConfig; |
|||
static BufferType packet_Buffer; |
|||
// static char mVersion[12];
|
|||
|
|||
static volatile uint32_t mTimestamp; |
|||
// static uint16_t mSendTicker;
|
|||
// static uint8_t mSendLastIvId;
|
|||
|
|||
static invPayload_t mPayload[MAX_NUM_INVERTERS]; |
|||
// static uint32_t mRxFailed;
|
|||
// static uint32_t mRxSuccess;
|
|||
// static uint32_t mFrameCnt;
|
|||
// static uint8_t mLastPacketId;
|
|||
|
|||
// serial
|
|||
static volatile uint16_t mSerialTicker; |
|||
|
|||
// timer
|
|||
// static uint32_t mTicker;
|
|||
// static uint32_t mRxTicker;
|
|||
|
|||
// static HmSystemType *mSys;
|
|||
static RadioType hmRadio; |
|||
// static uint8_t radio_id[5]; //todo: use the mPayload[].id field ,this defines the radio-id (domain) of the rf24 transmission, will be derived from inverter id
|
|||
static uint64_t radio_id64 = 0ULL; |
|||
|
|||
#define P(x) (__FlashStringHelper *)(x) // PROGMEM-Makro for variables
|
|||
static const char COMPILE_DATE[] PROGMEM = {__DATE__}; |
|||
static const char COMPILE_TIME[] PROGMEM = {__TIME__}; |
|||
static const char NAME[] PROGMEM = {DEF_DEVICE_NAME}; |
|||
|
|||
#define USER_PAYLOAD_MAXLEN 128 |
|||
static uint8_t user_payload[USER_PAYLOAD_MAXLEN]; // used for simple decoding and output only
|
|||
static uint32_t user_pl_ts = 0; |
|||
|
|||
#define MAX_STRING_LEN 51 |
|||
static char strout[MAX_STRING_LEN]; //global string buffer for sprintf, snprintf, snprintf_P outputs
|
|||
|
|||
///////////////////////////////////////////////////////////////////
|
|||
void setup() { |
|||
// Serial.begin(115200);
|
|||
Serial.begin(57600); |
|||
printf_begin(); |
|||
Serial.flush(); |
|||
Serial.print(P(NAME)); |
|||
Serial.print(F("\ncompiled ")); |
|||
Serial.print(P(COMPILE_DATE)); |
|||
Serial.print(F(" ")); |
|||
Serial.print(P(COMPILE_TIME)); |
|||
|
|||
mSerialTicker = 0xffff; |
|||
resetSystem(); // reset allocated mPayload buffer
|
|||
loadDefaultConfig(&mConfig); // fills the mConfig parameter with values
|
|||
strout[MAX_STRING_LEN-1] = '\0'; //string termination
|
|||
|
|||
|
|||
// todo: loadEEconfig() from Flash, like Inverter-ID, power setting
|
|||
// mSys = new HmSystemType();
|
|||
// mSys->setup(&mConfig);
|
|||
|
|||
DPRINT(DBG_INFO, F("freeRAM ")); _DPRINT(DBG_INFO,availableMemory()); |
|||
delay(2000); |
|||
hmRadio.setup(&mConfig, &packet_Buffer); |
|||
attachInterrupt(digitalPinToInterrupt(DEF_RF24_IRQ_PIN), handleIntr, FALLING); |
|||
|
|||
// prepare radio domain ID
|
|||
// radio_id[0] = (uint8_t) 0x01;
|
|||
// swap_bytes( &radio_id[1], (uint32_t)IV1_RADIO_ID );
|
|||
|
|||
// assign inverter ID to the payload structure
|
|||
mPayload[0].invId[0] = (uint8_t)0x01; |
|||
swap_bytes(&mPayload[0].invId[1], (uint32_t)IV1_RADIO_ID); |
|||
mPayload[0].invType = (uint16_t)(IV1_RADIO_ID >> 32); //keep just upper 6 and 5th byte (e.g.0x1141) of interter plate id
|
|||
|
|||
// hmRadio.dumpBuf("\nsetup InvID ", &mPayload[0].invId[0], 5, DBG_DEBUG);
|
|||
|
|||
// alternativly radio-id
|
|||
//radio_id64 = (uint64_t)(swap_bytes((uint32_t)IV1_RADIO_ID)) << 8 | 0x01;
|
|||
|
|||
// todo: load Inverter decoder depending on InvID
|
|||
|
|||
} // end setup()
|
|||
|
|||
// volatile static uint32_t current_millis = 0;
|
|||
static volatile uint32_t timer1_millis = 0L; // general loop timer
|
|||
static volatile uint32_t timer2_millis = 0L; // send Request timer
|
|||
static volatile uint32_t lastRx_millis = 0L; |
|||
#define ONE_SECOND (1000L) |
|||
#define ONE_MINUTE (60L * ONE_SECOND) |
|||
#define QUARTER_HOUR (15L * ONE_MINUTE) |
|||
#define SEND_INTERVAL_ms (ONE_SECOND * SEND_INTERVAL) |
|||
#define MIN_SEND_INTERVAL_ms (ONE_SECOND * MIN_SEND_INTERVAL) |
|||
static uint8_t c_loop = 0; |
|||
static volatile int sread_len = 0; // UART read length
|
|||
// static volatile bool rxRdy = false; //will be set true on first receive packet during sending interval, reset to false before sending
|
|||
static uint8_t inv_ix; |
|||
static bool payload_used[MAX_NUM_INVERTERS]; |
|||
// static bool saveMACPacket = false; // when true the the whole MAC packet with address is kept, remove the MAC for CRC calc
|
|||
static bool automode = true; |
|||
static bool doDecode = false; |
|||
static bool showMAC = true; |
|||
static volatile uint32_t polling_inv_msec = SEND_INTERVAL_ms; |
|||
static volatile uint16_t tmp16 = 0; |
|||
static uint8_t tmp8 = 0; |
|||
static uint32_t min_SEND_SYNC_msec = MIN_SEND_INTERVAL_ms; |
|||
|
|||
/////////////////////////////////////////////////////////////////////////
|
|||
///////////////////////////////////////////////////////////////////////
|
|||
void loop() { |
|||
// the inverter id/ix I shall be handled in a loop if more than one inverter is registered for periodic polling
|
|||
inv_ix = 0; |
|||
|
|||
// counting the time reference of seconds
|
|||
if (millis() - timer1_millis >= ONE_SECOND) { |
|||
timer1_millis = millis(); |
|||
mTimestamp += 1; |
|||
c_loop++; |
|||
if (c_loop > 15) { |
|||
c_loop = 0; |
|||
// DPRINT(DBG_VERBOSE, F("loop().."));
|
|||
DPRINT(DBG_DEBUG, millis()); |
|||
_DPRINT(DBG_DEBUG, F(" loop:freeRAM ")); |
|||
_DPRINT(DBG_DEBUG, availableMemory()); |
|||
} // end if
|
|||
} |
|||
|
|||
// query serial-IF for some control and data to be send via RF (cmd: sMAC:chXX:... see eval_uart_smac_cmd() format)
|
|||
if (Serial.available()) { |
|||
// wait char
|
|||
inSer = Serial.read(); |
|||
delay(10); |
|||
switch (inSer) { |
|||
case (char)'s': { |
|||
// sending smac commands which are send to the inverter, same format as from inverter, see details in funct. eval_uart_smac_cmd(...)
|
|||
//e.g. "smac:ch03:958180....:rc40:" -- ch03 for Tx channel, <raw data with crc8>, rc40 for rx channel 40, if rx channel is left then default tx_ix+2
|
|||
DPRINT(DBG_INFO, F("s OK")); // OK needed only for my simple python at-terminal
|
|||
static packet_t rfTX_packet; |
|||
static uint8_t rxch; |
|||
ser_buffer[0] = '\0'; |
|||
automode = false; |
|||
sread_len = serReadBytes_ms(ser_buffer, MAX_SERBYTES, 2000); |
|||
if (eval_uart_smac_cmd(ser_buffer, sread_len, &rfTX_packet, &rxch)) { |
|||
// send on Tx channel and receive on Rx channel
|
|||
if (rxch == 0) { |
|||
// if rxchannel not given, then set automatically
|
|||
rxch = hmRadio.getRxChannel(rfTX_packet.rfch); |
|||
} |
|||
hmRadio.setRxChanIdx(hmRadio.getChanIdx(rxch)); |
|||
|
|||
// compare inv-id from packet data with all registerd inv-id of the payload_t struct array
|
|||
inv_ix = getInvID(mPayload, MAX_NUM_INVERTERS, &rfTX_packet.data[0]); |
|||
if (inv_ix != 0xFF) { |
|||
DPRINT(DBG_DEBUG, F("match, inv_ix ")); |
|||
_DPRINT(DBG_DEBUG, inv_ix); |
|||
payload_used[inv_ix] = !resetPayload(&mPayload[inv_ix]); |
|||
mPayload[inv_ix].isMACPacket = true; //MAC must be enabled to show the full MAC packet, no need for user_payload only
|
|||
mPayload[inv_ix].receive = false; |
|||
hmRadio.sendPacket_raw(&mPayload[0].invId[0], &rfTX_packet, rxch); // 2022-10-30: byte array transfer working
|
|||
mPayload[inv_ix].requested = true; |
|||
} else { |
|||
// no matching inverter, do nothing
|
|||
inv_ix = 0; |
|||
} |
|||
} |
|||
break; |
|||
} // end case s
|
|||
|
|||
case (char)'c': { |
|||
// todo: scan all channels for 1bit RSSI value and print result
|
|||
Serial.print(F("\nc OK ")); |
|||
hmRadio.scanRF(); |
|||
break; |
|||
} |
|||
|
|||
case (char)'z': { |
|||
// todo: register new Inverter ID in payload_t array via "z:<5bytes>:<crc>:", save to eeprom
|
|||
// todo: query via z1? for first inverter
|
|||
break; |
|||
} |
|||
|
|||
case (char)'a': { |
|||
//enable automode with REQ polling interval via a10 => 10sec, a100 => 100sec or other range 5....3600sec
|
|||
automode = true; |
|||
sread_len = serReadBytes_ms(ser_buffer, MAX_SERBYTES, 2000); |
|||
polling_inv_msec = eval_uart_decimal_val("auto polling msec ", ser_buffer, sread_len, 5, 3600, ONE_SECOND); |
|||
break; |
|||
} // end case a
|
|||
|
|||
case (char)'d': { |
|||
// trigger decoding, enable periodic decoding via "d1" and disable via "d0"
|
|||
Serial.print(F("\nd OK ")); |
|||
// simple decoding
|
|||
decodePayload(TX_REQ_INFO + 0x80, &user_payload[0], 42, user_pl_ts, &strout[0], MAX_STRING_LEN, mPayload[inv_ix].invType); |
|||
sread_len = serReadBytes_ms(ser_buffer, MAX_SERBYTES, 2000); |
|||
doDecode = (bool)eval_uart_decimal_val("decoding ", ser_buffer, sread_len, 0, 255, 1); |
|||
break; |
|||
} // end case d
|
|||
|
|||
case (char)'m': { |
|||
// enable/disable show MACmessages via "m0" or "d1"
|
|||
Serial.print(F("\nm OK ")); |
|||
sread_len = serReadBytes_ms(ser_buffer, MAX_SERBYTES, 2000); |
|||
showMAC = (bool)eval_uart_decimal_val("showMAC ", ser_buffer, sread_len, 0, 255, 1); |
|||
break; |
|||
} // end case m
|
|||
|
|||
case (char)'i': { |
|||
// query current radio information
|
|||
DPRINT(DBG_INFO, F("i OK")); |
|||
hmRadio.print_radio_details(); |
|||
break; |
|||
} |
|||
|
|||
case (char)'t': { |
|||
// set the time sec since Jan-01 1970 (UNIX epoch time) as decimal String value e.g. "t1612345678:" for Feb-03 2021 9:47:58
|
|||
// timestamp is only used for sending packet timer, but not for the timing of Tx/Rx scheduling etc...
|
|||
sread_len = serReadBytes_ms(ser_buffer, MAX_SERBYTES, 2000); |
|||
mTimestamp = eval_uart_decimal_val("time set ", ser_buffer, sread_len, 12 * 3600, 0xFFFFFFFF, 1); |
|||
|
|||
|
|||
break; |
|||
} // end case t
|
|||
} // end switch-case
|
|||
} // end if serial...
|
|||
|
|||
// automode RF-Tx-trigger
|
|||
if (automode) { |
|||
//slow down the sending if no Rx for long time
|
|||
if (millis() - lastRx_millis > QUARTER_HOUR) { |
|||
min_SEND_SYNC_msec = QUARTER_HOUR; |
|||
} else { |
|||
min_SEND_SYNC_msec = MIN_SEND_INTERVAL_ms; |
|||
} |
|||
|
|||
// normal sending request interval
|
|||
// todo: add queue of cmds or schedule simple device control request for power limit values
|
|||
if ( millis() - timer2_millis >= min_SEND_SYNC_msec && ((millis() - lastRx_millis > polling_inv_msec) || millis() < 60000) ) { |
|||
timer2_millis = millis(); |
|||
// DISABLE_IRQ;
|
|||
// todo: handle different inverter via inv_id
|
|||
payload_used[inv_ix] = !resetPayload(&mPayload[inv_ix]); |
|||
mPayload[inv_ix].isMACPacket = true; |
|||
mPayload[inv_ix].receive = false; |
|||
hmRadio.sendTimePacket(&mPayload[inv_ix].invId[0], 0x0B, mTimestamp, 0x0000); |
|||
mPayload[inv_ix].requested = true; |
|||
// RESTORE_IRQ;
|
|||
} |
|||
} |
|||
|
|||
// RF-Rx-loop raw reading
|
|||
// receives rf data and writes data into circular buffer (packet_Buffer)
|
|||
hmRadio.loop(); |
|||
|
|||
// eval RF-Rx raw data (one single entry per loop to keep receiving new messages from one inverter (inverter domain is set via invID)
|
|||
if (!packet_Buffer.empty()) { |
|||
packet_t *p; |
|||
p = packet_Buffer.getBack(); |
|||
if (hmRadio.checkPaketCrc(&p->data[0], &p->plen, p->rfch)) { |
|||
// process buffer only on first occurrence
|
|||
DPRINT(DBG_INFO, F("RX Ch")); |
|||
if (p->rfch < 10) _DPRINT(DBG_INFO, F("0")); |
|||
_DPRINT(DBG_INFO, p->rfch); |
|||
_DPRINT(DBG_INFO, F(" ")); |
|||
_DPRINT(DBG_INFO, p->plen); |
|||
_DPRINT(DBG_INFO, F("B | ")); |
|||
hmRadio.dumpBuf(DBG_INFO, NULL, p->data, p->plen); |
|||
// mFrameCnt++;
|
|||
|
|||
if (p->plen) { |
|||
// no need to get the inv_ix, because only desired inverter will answer, payload buffer is CRC protected
|
|||
// inv_ix = getInvID(mPayload, MAX_NUM_INVERTERS, &p->data[0]);
|
|||
if (inv_ix >= 0 && inv_ix < MAX_NUM_INVERTERS) { |
|||
if (copyPacket2Payload(&mPayload[inv_ix], inv_ix, p, mPayload[inv_ix].isMACPacket)) { |
|||
lastRx_millis = millis(); |
|||
} |
|||
} |
|||
} // end if(plen)
|
|||
} // end if(checkPacketCRC)
|
|||
packet_Buffer.popBack(); // remove last entry after eval and print
|
|||
DPRINT(DBG_DEBUG, F("Records in p_B ")); |
|||
_DPRINT(DBG_DEBUG, packet_Buffer.available()); |
|||
} // end if()
|
|||
|
|||
// handle output of data if ready
|
|||
// after min 500msec and all packets shall be copied successfully to mPayload structure, run only if requested and some data received after REQ-trigger
|
|||
if ((millis() - timer2_millis >= 500) && packet_Buffer.empty() && mPayload[inv_ix].requested && mPayload[inv_ix].receive) { |
|||
// process payload some sec after last sending
|
|||
if (false == payload_used[inv_ix]) { |
|||
if (processPayload(&mPayload[inv_ix], &mConfig, true)) { |
|||
// data valid and complete
|
|||
|
|||
if (mPayload[inv_ix].isMACPacket && showMAC) { |
|||
returnMACPackets(&mPayload[inv_ix]); |
|||
} |
|||
payload_used[inv_ix] = true; |
|||
tmp8 = returnPayload(&mPayload[inv_ix], &user_payload[0], USER_PAYLOAD_MAXLEN); |
|||
user_pl_ts = mPayload[inv_ix].ts; |
|||
|
|||
if (tmp8 == 42 && doDecode) { |
|||
decodePayload(mPayload[inv_ix].txId, &user_payload[0], tmp8, user_pl_ts, &strout[0], MAX_STRING_LEN, mPayload[inv_ix].invType); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} // end loop()
|
|||
//-----------------------------------------------------------------------------
|
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
static void resetSystem(void) { |
|||
mTimestamp = 1665740000; // sec since 1970 Jan-01, is Oct-14 2022 ~09:33
|
|||
// mTicker = 0;
|
|||
// mRxTicker = 0;
|
|||
// mSendLastIvId = 0;
|
|||
memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t))); |
|||
// mRxFailed = 0;
|
|||
// mRxSuccess = 0;
|
|||
// mFrameCnt = 0;
|
|||
// mLastPacketId = 0x00;
|
|||
// mSerialTicker = 0xffff;
|
|||
} |
|||
|
|||
//-----------------------------------------------------------------------------
|
|||
static void loadDefaultConfig(config_t *_mconfig) { |
|||
DPRINT(DBG_VERBOSE, F("loadDefaultCfg ..")); |
|||
// memset(&mSysConfig, 0, sizeof(sysConfig_t));
|
|||
memset(_mconfig, 0, sizeof(config_t)); |
|||
// snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
|
|||
// snprintf(mSysConfig.deviceName, DEVNAME_LEN, "%s", DEF_DEVICE_NAME);
|
|||
|
|||
// nrf24
|
|||
_mconfig->sendInterval = SEND_INTERVAL; |
|||
_mconfig->maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; |
|||
_mconfig->pinCs = DEF_RF24_CS_PIN; |
|||
_mconfig->pinCe = DEF_RF24_CE_PIN; |
|||
_mconfig->pinIrq = DEF_RF24_IRQ_PIN; |
|||
_mconfig->amplifierPower = DEF_AMPLIFIERPOWER & 0x03; |
|||
|
|||
// serial
|
|||
_mconfig->serialInterval = SERIAL_INTERVAL; |
|||
_mconfig->serialShowIv = true; |
|||
_mconfig->serialDebug = true; |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
// free RAM check for debugging. SRAM for ATmega328p = 2048Kb.
|
|||
static int availableMemory() { |
|||
// Use 1024 with ATmega168
|
|||
volatile int size = 2048; |
|||
byte *buf; |
|||
while ((buf = (byte *)malloc(--size)) == NULL) |
|||
; |
|||
free(buf); |
|||
return size; |
|||
} |
|||
|
|||
static void swap_bytes(uint8_t *_buf, uint32_t _v) { |
|||
_buf[0] = ((_v >> 24) & 0xff); |
|||
_buf[1] = ((_v >> 16) & 0xff); |
|||
_buf[2] = ((_v >> 8) & 0xff); |
|||
_buf[3] = ((_v)&0xff); |
|||
return; |
|||
} |
|||
|
|||
static uint32_t swap_bytes(uint32_t _v) { |
|||
volatile uint32_t _res; |
|||
_res = ((_v >> 24) & 0x000000ff) | ((_v >> 8) & 0x0000ff00) | ((_v << 8) & 0x00ff0000) | ((_v << 24) & 0xff000000); |
|||
return _res; |
|||
} |
|||
|
|||
|
|||
////////////////// handle payload function, maybe later as own class //////////////////
|
|||
/**
|
|||
* compares the two arrays and returns true when equal |
|||
*/ |
|||
static bool check_array(uint8_t *invID, uint8_t *rcvID, uint8_t _len) { |
|||
uint8_t i; |
|||
for (i = 0; i < _len; i++) { |
|||
if (invID[i] != rcvID[i]) return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
/**
|
|||
* gets the payload index that matches to the received ID, in case of no match 0xFF is returned |
|||
*/ |
|||
static uint8_t getInvID(invPayload_t *_payload, uint8_t _pMAX, uint8_t *rcv_data) { |
|||
uint8_t i; |
|||
for (i = 0; i < _pMAX; i++) { |
|||
// comparison starts at index 1 of invID, because index zero contains the pipe, only 4 bytes are compared
|
|||
if (check_array(&_payload->invId[1], &rcv_data[1], 4)) return i; |
|||
_payload++; |
|||
} |
|||
return (uint8_t)0xFF; |
|||
} |
|||
|
|||
static uint8_t getNumInv(invPayload_t *_payload, uint8_t _pMAX) { |
|||
uint8_t i; |
|||
for (i = 0; i < _pMAX; i++) { |
|||
// check if invID is not set on first two entries, shall be sufficient for now
|
|||
if (_payload->invId[1] == 0x00 && _payload->invId[2] == 0x00) break; |
|||
_payload++; |
|||
} |
|||
return i; |
|||
} |
|||
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
//
|
|||
// low level payload packet handling
|
|||
//
|
|||
//
|
|||
|
|||
/**
|
|||
* clearing of the current inverter data structure, except the ID and typedef |
|||
*/ |
|||
static bool resetPayload(invPayload_t *_payload) { |
|||
// static uint8_t inv_ix;
|
|||
uint8_t backup_inv_id[5]; |
|||
uint16_t backup_inv_type; |
|||
DPRINT(DBG_VERBOSE, F("resetPayload invid ")); |
|||
hmRadio.dumpBuf(DBG_VERBOSE, NULL, &_payload->invId[0], 5); |
|||
memcpy(backup_inv_id, &_payload->invId[0], 5); |
|||
backup_inv_type = _payload->invType; |
|||
// nv_ix = getInvID(_payload, _pMAX, *invID);
|
|||
memset(_payload, 0, sizeof(invPayload_t)); |
|||
memcpy(&_payload->invId[0], backup_inv_id, 5); |
|||
_payload->invType = backup_inv_type; |
|||
return true; |
|||
} |
|||
|
|||
/**
|
|||
* copies data (response of REQ_INFO message) from Rx packet buffer into payload structure per inverter |
|||
*/ |
|||
static bool copyPacket2Payload(invPayload_t *_payload, uint8_t _invx, packet_t *_p, bool _keepMAC_packet) { |
|||
// check whether inverter ID is registered and save data per registered inverter, but for TX_REQ_INFO only
|
|||
bool _res; |
|||
uint8_t ix; |
|||
uint8_t pid; |
|||
|
|||
_res = false; |
|||
DPRINT(DBG_DEBUG, F("copyPacket2Payload invx ")); |
|||
_DPRINT(DBG_DEBUG, _invx); |
|||
|
|||
pid = _p->data[9]; |
|||
// check first if all RF data shall be kept, e.g. for transfer via uart-if as MAC packets
|
|||
ix = 10; // start of real payload without address overhead
|
|||
_payload->isMACPacket = _keepMAC_packet; |
|||
if (_keepMAC_packet) { |
|||
ix = 0; |
|||
} |
|||
|
|||
if ((pid & 0x7F) > MAX_PAYLOAD_ENTRIES) { |
|||
DPRINT(DBG_ERROR, F(" high index ")); |
|||
_DPRINT(DBG_ERROR, pid & 0x7F); |
|||
return false; |
|||
} // end
|
|||
|
|||
// parsing of payload responses, only keeping the real payload data
|
|||
if (_p->data[0] == (TX_REQ_INFO + 0x80)) { |
|||
// eval response from get information command 0x15
|
|||
DPRINT(DBG_DEBUG, F(" resp getinfo ")); |
|||
_DPRINT(DBG_DEBUG, F(" pid ")); |
|||
_DPRINT(DBG_DEBUG, pid & 0x7F); |
|||
if (pid == 0x00) { |
|||
_DPRINT(DBG_DEBUG, F(" ignore")); |
|||
} else { |
|||
// store regular fragments only
|
|||
_res = savePayloadfragment(_payload, _p, pid, ix); |
|||
} // end if-else(*pid == 0x00)
|
|||
} // end if(TX_REQ_INFO +0x80)
|
|||
|
|||
// check and save dev control
|
|||
else if (_p->data[0] == (TX_REQ_DEVCONTROL + 0x80)) { |
|||
// response from dev control request
|
|||
DPRINT(DBG_DEBUG, F(" resp devcontrol")); |
|||
|
|||
switch (_p->data[12]) { |
|||
case ActivePowerContr: |
|||
_DPRINTLN(DBG_INFO, F(" ActivePowerContr")); |
|||
break; |
|||
default: |
|||
_DPRINTLN(DBG_INFO, F(" default data[12] handling")); |
|||
_res = savePayloadfragment(_payload, _p, pid, ix); |
|||
break; |
|||
} // end switch-case
|
|||
|
|||
} else { |
|||
// handle all unknown Request response fragments
|
|||
_res = savePayloadfragment(_payload, _p, pid, ix); |
|||
} // end else-if
|
|||
|
|||
return _res; |
|||
} // end savePayload()
|
|||
|
|||
|
|||
/**
|
|||
* saves copies one received payload-packet into inverter memory structure |
|||
*/ |
|||
static bool savePayloadfragment(invPayload_t *_payload, packet_t *_p, uint8_t _pid, uint8_t _ix) { |
|||
volatile bool _res = false; |
|||
if ((_pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { |
|||
_res = true; |
|||
memcpy(&_payload->data[(_pid & 0x7F) - 1][0], &_p->data[_ix], _p->plen - _ix - 1); |
|||
_payload->len[(_pid & 0x7F) - 1] = _p->plen - _ix - 1; |
|||
_payload->rxChIdx = _p->rfch; |
|||
_payload->ts = millis(); |
|||
_payload->txId = _p->data[0]; |
|||
_payload->receive = _res; // indicates that a packet was received at least once per Request iteration
|
|||
|
|||
// handle last packet additionally
|
|||
if ((_pid & 0x80) == 0x80) { |
|||
_DPRINT(DBG_DEBUG, F(" fragm.end")); |
|||
if ((_pid & 0x7f) > _payload->maxPackId) { |
|||
_payload->maxPackId = (_pid & 0x7f); // set maxPackId > 0 as indication that last fragment was detected
|
|||
_DPRINT(DBG_DEBUG, F(" maxPID ")); |
|||
_DPRINT(DBG_DEBUG, _payload->maxPackId); |
|||
} |
|||
} // end if((*pid & 0x80)...)
|
|||
} else { |
|||
_DPRINT(DBG_ERROR, F(" pid out of range")); |
|||
} // end if(MAX_PAYLOAD_ENTRIES)
|
|||
return _res; |
|||
} // end savePayload fragment
|
|||
|
|||
|
|||
/**
|
|||
* process the received payload and handles retransmission request for one inverter |
|||
*/ |
|||
static bool processPayload(invPayload_t *_payload, config_t *_mconfig, bool retransmit) { |
|||
// uses gloabal send timer2_millis
|
|||
// for one inverter only at a given time only, but payload structure array can hold more inverter
|
|||
DPRINT(DBG_VERBOSE, F("processPayload")); |
|||
bool _res; |
|||
uint8_t _reqfragment; |
|||
|
|||
_res = 0x00; |
|||
//_payload->complete = false;
|
|||
|
|||
_reqfragment = checkPayload(_payload); |
|||
if (_reqfragment == 0xff) { |
|||
return false; |
|||
} |
|||
|
|||
if (_reqfragment > 0x80) { |
|||
if (retransmit) { |
|||
if (_payload->retransmits < _mconfig->maxRetransPerPyld) { |
|||
_payload->retransmits++; |
|||
DPRINT(DBG_DEBUG, F(" req fragment ")); |
|||
_DPRINTHEX(DBG_DEBUG, (uint8_t)(_reqfragment & 0x7f)); |
|||
hmRadio.sendCmdPacket(_payload->invId, TX_REQ_INFO, _reqfragment, true); |
|||
timer2_millis = millis(); // set sending timer2
|
|||
// leaves with false to quickly receive again
|
|||
// so far the Tx and Rx frequency is handled globally, it shall be individually per inverter ID?
|
|||
} |
|||
} |
|||
} else { |
|||
// all data valid
|
|||
_res = true; |
|||
//_payload->complete = true;
|
|||
} // end if-else()
|
|||
return _res; |
|||
|
|||
} // end processPayload()
|
|||
|
|||
|
|||
/**
|
|||
* checks the paypload of all received packet-fragments via CRC16 and length |
|||
* returns: |
|||
* - 0x00 == OK if all fragments received and CRC OK, |
|||
* - PID + 0x81 can be used directly to request the first next missing packet |
|||
* - 0xFF in case of CRC16 failure and next missing packet would be bigger than payload->maxPacketId |
|||
*/ |
|||
static uint8_t checkPayload(invPayload_t *_payload) { |
|||
// DPRINT(DBG_VERBOSE, F("checkPayload "));
|
|||
volatile uint16_t crc = 0xffff, crcRcv = 0x0000; |
|||
uint8_t i; |
|||
uint8_t ixpl; // index were payload starts in each MAC packet
|
|||
|
|||
if (_payload->maxPackId > MAX_PAYLOAD_ENTRIES) { |
|||
_payload->maxPackId = MAX_PAYLOAD_ENTRIES; |
|||
} |
|||
|
|||
ixpl = 0; |
|||
if (_payload->isMACPacket) { |
|||
ixpl = 10; |
|||
} |
|||
|
|||
// try to get the next missing packet via len detection
|
|||
if (_payload->maxPackId == 0) { |
|||
for (i = 0; i < MAX_PAYLOAD_ENTRIES; i++) { |
|||
if (_payload->len[i] == 0) break; |
|||
} |
|||
} else { |
|||
// check CRC over all entries
|
|||
for (i = 0; i < _payload->maxPackId; i++) { |
|||
if (_payload->len[i] > 0) { |
|||
DPRINT(DBG_VERBOSE, F(" checkPL ")); |
|||
_DPRINT(DBG_VERBOSE, _payload->len[i] - ixpl); |
|||
_DPRINT(DBG_VERBOSE, F("B ")); |
|||
hmRadio.dumpBuf(DBG_VERBOSE, NULL, &_payload->data[i][ixpl], _payload->len[i] - ixpl); |
|||
|
|||
if (i == (_payload->maxPackId - 1)) { |
|||
crc = Hoymiles::crc16(&_payload->data[i][ixpl], _payload->len[i] - ixpl - 2, crc); |
|||
crcRcv = (_payload->data[i][_payload->len[i] - 2] << 8) | (_payload->data[i][_payload->len[i] - 1]); |
|||
} else |
|||
crc = Hoymiles::crc16(&_payload->data[i][ixpl], _payload->len[i] - ixpl, crc); |
|||
} else { |
|||
// entry in range with len == zero
|
|||
//--> request retransmit with this index
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (crc == crcRcv) { |
|||
// success --> leave here
|
|||
DPRINT(DBG_DEBUG, F(" checkPL -> CRC16 OK")); |
|||
return (uint8_t)0x00; |
|||
|
|||
|
|||
} else { |
|||
if ((_payload->maxPackId > 0) && (i >= _payload->maxPackId)) { |
|||
DPRINT(DBG_ERROR, F(" crc ")); |
|||
_DPRINTHEX(DBG_ERROR, crc); |
|||
DPRINT(DBG_ERROR, F(" crcRcv ")); |
|||
_DPRINTHEX(DBG_ERROR, crcRcv); |
|||
// wrong CRC16 over all packets, must actually never happen for correct packets, must be programming bug
|
|||
DPRINT(DBG_ERROR, F(" cPL wrong req. ")); |
|||
_DPRINTHEX(DBG_ERROR, (uint8_t)(i + 0x81)); |
|||
_payload->receive = false; //stop further eval
|
|||
return (uint8_t)0xFF; |
|||
} |
|||
} // end if-else
|
|||
} // end if-else
|
|||
|
|||
return (uint8_t)(i + 0x81); |
|||
} |
|||
|
|||
|
|||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
//
|
|||
// output and decoding functions
|
|||
//
|
|||
//
|
|||
/**
|
|||
* output of sorted packets with MAC-header, all info included |
|||
*/ |
|||
static bool returnMACPackets(invPayload_t *_payload) { |
|||
for (uint8_t i = 0; i < (_payload->maxPackId); i++) { |
|||
Serial.print(F("\nrMAC:ch")); |
|||
if (_payload->rxChIdx < 10) Serial.print(F(" ")); |
|||
Serial.print(_payload->rxChIdx); |
|||
Serial.print(F(": ")); |
|||
hmRadio.dumpBuf(NULL, &_payload->data[i][0], _payload->len[i]); |
|||
} // end for()
|
|||
Serial.print(F(":rt")); |
|||
Serial.print(_payload->retransmits); |
|||
Serial.print(F(":")); |
|||
return true; |
|||
} |
|||
|
|||
/**
|
|||
* output of pure user payload message |
|||
*/ |
|||
static uint8_t returnPayload(invPayload_t *_payload, uint8_t *_user_payload, uint8_t _ulen) { |
|||
// iv->ts = mPayload[iv->id].ts;
|
|||
memset(_user_payload, 0, _ulen); |
|||
static uint8_t _offs, _ixpl, _len; |
|||
_offs = 0; |
|||
_ixpl = 0; |
|||
if (_payload->isMACPacket) { |
|||
_ixpl = 10; // index of position of payload start after pid
|
|||
} // end if()
|
|||
|
|||
for (uint8_t i = 0; i < (_payload->maxPackId); i++) { |
|||
memcpy(&_user_payload[_offs], &_payload->data[i][_ixpl], (_payload->len[i] - _ixpl - 1)); |
|||
_offs += (_payload->len[i] - _ixpl); |
|||
} // end for()
|
|||
|
|||
_offs -= 2; |
|||
Serial.print(F("\nrPayload(")); |
|||
Serial.print(_offs); |
|||
Serial.print(F("): ")); |
|||
hmRadio.dumpBuf(NULL, _user_payload, _offs); |
|||
Serial.print(F(":")); |
|||
return _offs; |
|||
} // end if returnPaylout
|
|||
|
|||
|
|||
/**
|
|||
* simple decoding of 2ch HM-inverter only |
|||
*/ |
|||
static void decodePayload(uint8_t _cmd, uint8_t *_user_payload, uint8_t _ulen, uint32_t _ts, char* _strout, uint8_t _strlen, uint16_t _invtype) { |
|||
volatile uint32_t _val = 0L; |
|||
byteAssign_t _bp; |
|||
volatile uint8_t _x; |
|||
volatile uint8_t _end; |
|||
volatile uint8_t _dot_val; |
|||
volatile float _fval; |
|||
|
|||
//_str80[80] = '\0';
|
|||
snprintf_P(_strout, _strlen, PSTR("\ndata age: %d sec"), (millis() - _ts)/1000); |
|||
Serial.print(_strout); |
|||
snprintf_P(_strout, _strlen, PSTR("\nInvertertype %Xxxxxxxxx "), _invtype); |
|||
Serial.print(_strout); |
|||
|
|||
if (_cmd == 0x95 and _ulen == 42) { |
|||
//!!! simple HM600/700/800 2ch decoding for cmd=0x95 only !!!!
|
|||
for (_x = 0; _x < HM2CH_LIST_LEN; _x++) { |
|||
// read values from given positions in payload
|
|||
_bp = hm2chAssignment[_x]; |
|||
_val = 0L; |
|||
_end = _bp.start + _bp.num; |
|||
|
|||
snprintf_P(_strout, _strlen, PSTR("\nHM800/ch%02d/"), _bp.ch); |
|||
Serial.print(_strout); |
|||
//Serial.print(F("\nHM800/ch"));
|
|||
//Serial.print(_bp.ch);
|
|||
//Serial.print(F("/"));
|
|||
//strncpy_P(_strout, (PGM_P)pgm_read_word(&(PGM_fields[_bp.fieldId])), _strlen); // read from PROGMEM array into RAM, works for A-Nano, not for esp8266
|
|||
//snprintf_P(_strout, );
|
|||
strncpy_P(_strout, &(PGM_fields[_bp.fieldId][0]), _strlen); |
|||
Serial.print(_strout); |
|||
Serial.print(F(": ")); |
|||
|
|||
if (CMD_CALC != _bp.div && _bp.div != 0) { |
|||
do { |
|||
_val <<= 8; |
|||
_val |= _user_payload[_bp.start]; |
|||
} while (++_bp.start != _end); |
|||
_fval = _val / (float)_bp.div; |
|||
|
|||
if (_bp.unitId == UNIT_NONE) { |
|||
Serial.print(_val); |
|||
continue; |
|||
} |
|||
Serial.print(_fval,2); //arduino nano does not sprintf.. float values, but print() does
|
|||
//Serial.print(units[_bp.unitId]);
|
|||
strncpy_P(_strout, &PGM_units[_bp.unitId][0], _strlen); |
|||
Serial.print(_strout); |
|||
|
|||
} else { |
|||
// do calculations
|
|||
Serial.print(F("not yet")); |
|||
} |
|||
} // end for()
|
|||
} else { |
|||
Serial.print(F("NO DECODER ")); |
|||
Serial.print(_cmd, HEX); |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
///////////////////////////////////////////////////////////
|
|||
//
|
|||
// Interrupt handling
|
|||
//
|
|||
#if defined(ESP8266) || defined(ESP32) |
|||
IRAM_ATTR void handleIntr(void) { |
|||
hmRadio.handleIntr(); |
|||
} |
|||
#else |
|||
static void handleIntr(void) { |
|||
hmRadio.handleIntr(); |
|||
} |
|||
#endif |
|||
///////////////////////////////////////////////////////////
|
|||
|
@ -0,0 +1,233 @@ |
|||
|
|||
//2022 mb for AHOY-UL (Arduino Nano, USB light IF)
|
|||
//todo: make class
|
|||
|
|||
#include <stdint.h> |
|||
#include <stdio.h> |
|||
#include <Arduino.h> |
|||
#include "dbg.h" |
|||
|
|||
|
|||
//declaration of functions
|
|||
static int serReadUntil(char, char*, int, uint16_t); |
|||
static int c_remove(char, char*, int); |
|||
static void c_replace(char, char, char*, int); |
|||
//static uint8_t* serGetHex(char*, int8_t*, uint16_t);
|
|||
//static uint16_t x2b(char, char);
|
|||
static uint16_t x2b(char *); |
|||
static int serReadTo_ms(char, char*, byte, uint16_t); |
|||
static int serReadBytes_ms(char *, byte, uint16_t ); |
|||
static boolean eval_uart_smac_cmd(char*, uint8_t, packet_t*, uint8_t*); |
|||
static uint32_t eval_uart_single_val(char*, char*, uint8_t, uint8_t, uint8_t, int); |
|||
|
|||
|
|||
#define MAX_SERBYTES 150 //max buffer length serial
|
|||
static char ser_buffer[MAX_SERBYTES+1]; //one extra byte for \0 termination
|
|||
static uint8_t byte_buffer[MAX_SERBYTES/2]; |
|||
static char inSer; |
|||
static int ser_len; |
|||
|
|||
|
|||
|
|||
/******************************************************************************************************
|
|||
* serial read function with timeout |
|||
* With ATMEGA328p it can only detect 64byte at once (serial input buffer size) at the baudrate of 115200baud (~5ms per byte) |
|||
*/ |
|||
static int serReadUntil(char _ch, char *_str, int _len, uint16_t _TIMEOUT) { |
|||
static volatile uint16_t _backupTO; |
|||
static volatile int thislen; |
|||
|
|||
_backupTO = Serial.getTimeout(); |
|||
thislen = 0; |
|||
|
|||
Serial.setTimeout(_TIMEOUT); |
|||
thislen = Serial.readBytesUntil(_ch, _str, _len); //can only be used unto max serial input buffer of 64bytes
|
|||
_str[thislen] = '\0'; //terminate char* str with '\0', data are available external now
|
|||
if( _len > 0 ) { |
|||
DPRINT( DBG_DEBUG, F("ser Rx ")); _DPRINT( DBG_DEBUG, _str); _DPRINT( DBG_DEBUG, F(" len ")); _DPRINT( DBG_DEBUG, thislen); |
|||
} else { |
|||
DPRINT( DBG_DEBUG, F("ser TIMEOUT")); |
|||
} |
|||
Serial.setTimeout(_backupTO); |
|||
return thislen; |
|||
}//end serReadUntil()
|
|||
|
|||
|
|||
|
|||
static int c_remove(char _chrm, char* _str, int _len) { |
|||
static int _ir = 0; |
|||
static int _iw = 0; |
|||
_ir = 0; //! must be initialized here explicitly with zero, instanciation above is only done once !
|
|||
_iw = 0; |
|||
while (_str[_ir] && _ir < _len) { |
|||
if (_str[_ir]!=_chrm) { |
|||
_str[_iw++] = _str[_ir]; |
|||
} |
|||
_ir++; |
|||
} |
|||
_str[_iw]='\0'; |
|||
return _iw; |
|||
}//end c_remove()
|
|||
|
|||
static void c_replace(char _crpl, char _cnew, char* _str, int _len) { |
|||
int _ir = 0; |
|||
_ir = 0; |
|||
while (_str[_ir] && _ir < _len) { |
|||
if (_str[_ir] ==_crpl) { |
|||
_str[_ir] = _cnew; |
|||
} |
|||
_ir++; |
|||
} |
|||
return; |
|||
}//end c_replace
|
|||
|
|||
static void c_lower(char* _str, int _len) { |
|||
int _ir = 0; |
|||
_ir = 0; |
|||
while (_str[_ir] && _ir < _len) { |
|||
if (_str[_ir] >= 'A' && _str[_ir] < 'Z') { |
|||
_str[_ir] = (char) (_str[_ir] + 0x20); |
|||
} |
|||
_ir++; |
|||
} |
|||
return; |
|||
}//end c_lower
|
|||
|
|||
|
|||
|
|||
//todo: use strtol(const char *str, char **endptr, int base) instead
|
|||
/**
|
|||
* converts 2 digits hex string to uint8_t byte, the high byte is FF in case of non-hex digit |
|||
*/ |
|||
/*
|
|||
static uint16_t x2b(char high, char low) { |
|||
static volatile uint16_t onebyte=0; |
|||
onebyte = 0; |
|||
if(high >= '0' && high <= '9') onebyte = (uint16_t) high - '0'; |
|||
else if (high >='A' && high <= 'F') onebyte = (uint16_t) high - 'A' + 10; |
|||
else if (high >='a' && high <= 'f') onebyte = (uint16_t) high - 'a' + 10; |
|||
else return 0xF000; |
|||
onebyte = onebyte << 4; |
|||
|
|||
if(low >= '0' && low <= '9') onebyte |= (uint16_t) low - '0'; |
|||
else if (low >='A' && low <= 'F') onebyte |= (uint16_t) low - 'A' + 10; |
|||
else if (low >='a' && low <= 'f') onebyte |= (uint16_t) low - 'a' + 10; |
|||
else return 0x0F00; |
|||
|
|||
return onebyte & 0x00FF; |
|||
} //end x2i()
|
|||
*/ |
|||
|
|||
static uint16_t x2b(char *_hexbyte) { |
|||
uint16_t onebyte=0; |
|||
if(_hexbyte[0] >= '0' && _hexbyte[0] <= '9') onebyte = (uint16_t)_hexbyte[0] - '0'; |
|||
else if (_hexbyte[0] >='A' && _hexbyte[0] <= 'F') onebyte = (uint16_t) _hexbyte[0] - 'A' + 10; |
|||
else if (_hexbyte[0] >='a' && _hexbyte[0] <= 'f') onebyte = (uint16_t) _hexbyte[0] - 'a' + 10; |
|||
else return 0xF000; |
|||
onebyte = onebyte << 4; |
|||
|
|||
if(_hexbyte[1] >= '0' && _hexbyte[1] <= '9') onebyte |= (uint16_t)_hexbyte[1] - '0'; |
|||
else if (_hexbyte[1] >='A' && _hexbyte[1] <= 'F') onebyte |= (uint16_t)_hexbyte[1] - 'A' + 10; |
|||
else if (_hexbyte[1] >='a' && _hexbyte[1] <= 'f') onebyte |= (uint16_t)_hexbyte[1] - 'a' + 10; |
|||
else return 0x0F00; |
|||
|
|||
return onebyte & 0x00FF; |
|||
} //end x2i()
|
|||
|
|||
|
|||
/*********************************************************************************************************************************************************************
|
|||
* faster serial buffer reading function with timeout, value parsing after reading; function works for more than 64byte UART buffer, only for baudrates <= 57600baud |
|||
*/ |
|||
static int serReadBytes_ms(char *buf, byte _lenMAX, uint16_t TIMEOUT_ms) { |
|||
volatile uint32_t _startTime = 0L; |
|||
volatile uint8_t _thislen; |
|||
volatile int _len; |
|||
volatile uint16_t _backupTO; |
|||
|
|||
_thislen = 0; |
|||
_len = 0; |
|||
_backupTO = Serial.getTimeout(); |
|||
Serial.setTimeout(100); |
|||
_startTime = millis(); |
|||
|
|||
//fast reading loop of full buffer,
|
|||
while(Serial.available()) { |
|||
_thislen = Serial.readBytes(&buf[_len], _lenMAX - _len); |
|||
_len += _thislen; |
|||
} //end while()
|
|||
buf[_len] = '\0'; |
|||
Serial.setTimeout(_backupTO); |
|||
|
|||
return _len; |
|||
}//end serReadBytes_ms()
|
|||
|
|||
|
|||
/**
|
|||
* eval the serial command smac<txchannel>:<data>:rc<rxchannel>:, e.g. s:ch03:958180....:rc40: and puts it to the packet_t structure |
|||
* the data payload is addressed directly to the RF interface with the given channels |
|||
*/ |
|||
static boolean eval_uart_smac_cmd(char *_serdata, uint8_t _slen, packet_t *packet, uint8_t *rxch) { |
|||
char *p = NULL; |
|||
char *m = NULL; |
|||
|
|||
if(_slen<10) { |
|||
DPRINT(DBG_ERROR, F("slen low")); _DPRINT(DBG_ERROR, _slen); _DPRINT(DBG_ERROR, F(" ERROR")); |
|||
return false; |
|||
} |
|||
|
|||
_slen = c_remove(' ', _serdata, _slen); |
|||
c_lower(_serdata, _slen); |
|||
p = strtok(_serdata, ":"); |
|||
m = strstr(p, "mac"); |
|||
if(m==NULL) return false; |
|||
p = strtok(NULL, ":"); |
|||
m = strstr(p, "ch"); |
|||
if (m) { |
|||
m += 2; |
|||
packet->rfch = atoi(m); |
|||
DPRINT(DBG_DEBUG, F("smac_txch ")); _DPRINT(DBG_DEBUG, packet->rfch); |
|||
p = strtok(NULL, ":"); |
|||
// next section
|
|||
DPRINT(DBG_DEBUG, F("smac_data ")); _DPRINT(DBG_DEBUG, p); _DPRINT(DBG_DEBUG, F(", len ")); _DPRINT(DBG_DEBUG, strlen(p)); |
|||
uint8_t _i = 0; |
|||
for (_i = 0; _i < strlen(p); _i += 2) { |
|||
packet->data[_i / 2] = (uint8_t)x2b(&p[_i]); |
|||
} // end for()
|
|||
packet->plen = _i / 2; |
|||
//eval rx channel input
|
|||
p = strtok(NULL, ":"); |
|||
m = strstr(p, "rc"); |
|||
if (m) { |
|||
m += 2; |
|||
*rxch = atoi(m); |
|||
} else { |
|||
*rxch = 0; |
|||
}//end if()
|
|||
DPRINT(DBG_DEBUG, F("smac_rxch ")); _DPRINT(DBG_DEBUG, *rxch); |
|||
return true; |
|||
} else { |
|||
DPRINT(DBG_ERROR, F("smac ERROR")); |
|||
return false; |
|||
}//end if()
|
|||
}//end eval_uart_cmd()
|
|||
|
|||
|
|||
static uint32_t eval_uart_decimal_val(const char* _info, char* _serdata, uint8_t _len, uint32_t inMIN, uint32_t inMAX, int outFACTOR) { |
|||
volatile uint32_t _tmp32; |
|||
|
|||
DISABLE_IRQ; |
|||
_tmp32 = inMIN * outFACTOR; |
|||
if ((_len > 1) && (_len < 12)) { |
|||
_serdata[_len] = '\0'; |
|||
_tmp32 = atol(_serdata); |
|||
if ((_tmp32 <= inMIN) && (_tmp32 >= inMAX)) { |
|||
_tmp32 = inMIN; |
|||
} |
|||
_tmp32 = outFACTOR * _tmp32; |
|||
} |
|||
RESTORE_IRQ; |
|||
DPRINT(DBG_INFO, _info); |
|||
_DPRINT(DBG_INFO,_tmp32); |
|||
DPRINT(DBG_INFO, F("OK")); |
|||
return _tmp32; |
|||
} //end eval_simple_cmd()
|
@ -0,0 +1,11 @@ |
|||
|
|||
This directory is intended for PlatformIO Test Runner and project tests. |
|||
|
|||
Unit Testing is a software testing method by which individual units of |
|||
source code, sets of one or more MCU program modules together with associated |
|||
control data, usage procedures, and operating procedures, are tested to |
|||
determine whether they are fit for use. Unit testing finds problems early |
|||
in the development cycle. |
|||
|
|||
More information about PlatformIO Unit Testing: |
|||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html |
Loading…
Reference in new issue