diff --git a/tools/nano/AhoyUL/.gitignore b/tools/nano/AhoyUL/.gitignore new file mode 100644 index 00000000..472d3cb9 --- /dev/null +++ b/tools/nano/AhoyUL/.gitignore @@ -0,0 +1,8 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +.log +config_override.h + diff --git a/tools/nano/AhoyUL/include/README b/tools/nano/AhoyUL/include/README new file mode 100644 index 00000000..194dcd43 --- /dev/null +++ b/tools/nano/AhoyUL/include/README @@ -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 diff --git a/tools/nano/AhoyUL/lib/README b/tools/nano/AhoyUL/lib/README new file mode 100644 index 00000000..6debab1e --- /dev/null +++ b/tools/nano/AhoyUL/lib/README @@ -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 +#include + +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 diff --git a/tools/nano/AhoyUL/platformio.ini b/tools/nano/AhoyUL/platformio.ini new file mode 100644 index 00000000..23020778 --- /dev/null +++ b/tools/nano/AhoyUL/platformio.ini @@ -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 + + + + + diff --git a/tools/nano/AhoyUL/src/AhoyUL.code-workspace b/tools/nano/AhoyUL/src/AhoyUL.code-workspace new file mode 100644 index 00000000..d9c82827 --- /dev/null +++ b/tools/nano/AhoyUL/src/AhoyUL.code-workspace @@ -0,0 +1,9 @@ +{ + "folders": [ + { + "name": "AUL", + "path": ".." + } + ], + "settings": {} +} diff --git a/tools/nano/AhoyUL/src/CircularBuffer.h b/tools/nano/AhoyUL/src/CircularBuffer.h new file mode 100644 index 00000000..b784f639 --- /dev/null +++ b/tools/nano/AhoyUL/src/CircularBuffer.h @@ -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 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 diff --git a/tools/nano/AhoyUL/src/config.h b/tools/nano/AhoyUL/src/config.h new file mode 100644 index 00000000..b258a102 --- /dev/null +++ b/tools/nano/AhoyUL/src/config.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 + +//------------------------------------- +// 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__*/ diff --git a/tools/nano/AhoyUL/src/crc.cpp b/tools/nano/AhoyUL/src/crc.cpp new file mode 100644 index 00000000..29f9b82f --- /dev/null +++ b/tools/nano/AhoyUL/src/crc.cpp @@ -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 \ No newline at end of file diff --git a/tools/nano/AhoyUL/src/crc.h b/tools/nano/AhoyUL/src/crc.h new file mode 100644 index 00000000..e4da30f5 --- /dev/null +++ b/tools/nano/AhoyUL/src/crc.h @@ -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 +#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__*/ diff --git a/tools/nano/AhoyUL/src/dbg.h b/tools/nano/AhoyUL/src/dbg.h new file mode 100644 index 00000000..43ebc20f --- /dev/null +++ b/tools/nano/AhoyUL/src/dbg.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 + inline void DBGPRINT(T str) { DSERIAL.print(str); } + template + 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__*/ diff --git a/tools/nano/AhoyUL/src/defines.h b/tools/nano/AhoyUL/src/defines.h new file mode 100644 index 00000000..4c72cd35 --- /dev/null +++ b/tools/nano/AhoyUL/src/defines.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__*/ diff --git a/tools/nano/AhoyUL/src/hmDefines.h b/tools/nano/AhoyUL/src/hmDefines.h new file mode 100644 index 00000000..de139458 --- /dev/null +++ b/tools/nano/AhoyUL/src/hmDefines.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__*/ diff --git a/tools/nano/AhoyUL/src/hmRadio.h b/tools/nano/AhoyUL/src/hmRadio.h new file mode 100644 index 00000000..a85b47c5 --- /dev/null +++ b/tools/nano/AhoyUL/src/hmRadio.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 +#include +#include + +#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 +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__*/ diff --git a/tools/nano/AhoyUL/src/main.cpp b/tools/nano/AhoyUL/src/main.cpp new file mode 100644 index 00000000..a86425b8 --- /dev/null +++ b/tools/nano/AhoyUL/src/main.cpp @@ -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 +#include +#include +#include + +#include +#include +#include + +#include "CircularBuffer.h" +#include "config.h" +#include "dbg.h" +#include "defines.h" +#include "hmDefines.h" +#include "hmRadio.h" +#include "utils_serial.h" + +typedef CircularBuffer BufferType; +typedef HmRadio RadioType; +// typedef Inverter InverterType; +// typedef HmSystem 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, , 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>::", 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 +/////////////////////////////////////////////////////////// + diff --git a/tools/nano/AhoyUL/src/utils_serial.h b/tools/nano/AhoyUL/src/utils_serial.h new file mode 100644 index 00000000..32fa62de --- /dev/null +++ b/tools/nano/AhoyUL/src/utils_serial.h @@ -0,0 +1,233 @@ + +//2022 mb for AHOY-UL (Arduino Nano, USB light IF) +//todo: make class + +#include +#include +#include +#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::rc:, 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() diff --git a/tools/nano/AhoyUL/test/README b/tools/nano/AhoyUL/test/README new file mode 100644 index 00000000..9b1e87bc --- /dev/null +++ b/tools/nano/AhoyUL/test/README @@ -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