├── library.properties ├── keywords.txt ├── .clang-format ├── src ├── AsyncNETSGPClient.cpp ├── AsyncNETSGPClient.h ├── NETSGPClient.cpp └── NETSGPClient.h ├── examples ├── PollDemo │ └── PollDemo.ino └── AsyncDemo │ └── AsyncDemo.ino └── README.md /library.properties: -------------------------------------------------------------------------------- 1 | name=NETSGPClient 2 | version=2.0.1 3 | author=Aaron Christophel, Moritz Wirger 4 | maintainer=Aaron Christophel, Moritz Wirger 5 | sentence=Interface for MicroInverters speaking the so-called NETSGP protocol. 6 | paragraph=An LC12S 2.4GHz RF module is needed for this library 7 | category=Communication 8 | url=https://github.com/atc1441/NETSGPClient 9 | architectures=* 10 | depends= 11 | dot_a_linkage=true 12 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Syntax Coloring Map For NETSGPClient 3 | ####################################### 4 | 5 | ####################################### 6 | # Datatypes (KEYWORD1) 7 | ####################################### 8 | 9 | NETSGPClient KEYWORD1 10 | AsyncNETSGPClient KEYWORD1 11 | LC12S KEYWORD1 12 | RFPower KEYWORD1 13 | Baudrate KEYWORD1 14 | Settings KEYWORD1 15 | InverterStatus KEYWORD1 16 | InverterStatusCallback KEYWORD1 17 | 18 | ####################################### 19 | # Methods and Functions (KEYWORD2) 20 | ####################################### 21 | 22 | getStatus KEYWORD2 23 | readRFModuleSettings KEYWORD2 24 | writeRFModuleSettings KEYWORD2 25 | setDefaultRFSettings KEYWORD2 26 | setStatusCallback KEYWORD2 27 | registerInverter KEYWORD2 28 | deregisterInverter KEYWORD2 29 | update KEYWORD2 30 | 31 | ####################################### 32 | # Instances (KEYWORD2) 33 | ####################################### 34 | 35 | ####################################### 36 | # Constants (LITERAL1) 37 | ####################################### 38 | 39 | DEFAULT_SETTINGS LITERAL1 40 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | # Based on Webkit style 3 | BasedOnStyle: Webkit 4 | IndentWidth: 4 5 | ColumnLimit: 120 6 | --- 7 | Language: Cpp 8 | Standard: Cpp11 9 | # Pointers aligned to the left 10 | DerivePointerAlignment: false 11 | PointerAlignment: Left 12 | AccessModifierOffset: -4 13 | AllowShortFunctionsOnASingleLine: Inline 14 | AlwaysBreakTemplateDeclarations: true 15 | BreakBeforeBraces: Custom 16 | BraceWrapping: 17 | AfterClass: true 18 | AfterControlStatement: true 19 | AfterEnum: true 20 | AfterFunction: true 21 | AfterNamespace: true 22 | AfterStruct: true 23 | AfterUnion: true 24 | AfterExternBlock: true 25 | BeforeCatch: true 26 | BeforeElse: true 27 | SplitEmptyFunction: false 28 | SplitEmptyRecord: false 29 | SplitEmptyNamespace: false 30 | BreakConstructorInitializers: BeforeColon 31 | CompactNamespaces: false 32 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 33 | ConstructorInitializerIndentWidth: 4 34 | Cpp11BracedListStyle: true 35 | FixNamespaceComments: true 36 | IncludeBlocks: Regroup 37 | IncludeCategories: 38 | # C++ standard headers (no .h) 39 | - Regex: '<[[:alnum:]_-]+>' 40 | Priority: 1 41 | # Extenal libraries (with .h) 42 | - Regex: '<[[:alnum:]_./-]+>' 43 | Priority: 2 44 | # Headers from same folder 45 | - Regex: '"[[:alnum:]_.-]+"' 46 | Priority: 3 47 | # Headers from other folders 48 | - Regex: '"[[:alnum:]_/.-]+"' 49 | Priority: 4 50 | IndentCaseLabels: false 51 | NamespaceIndentation: All 52 | SortIncludes: true 53 | SortUsingDeclarations: true 54 | SpaceAfterTemplateKeyword: true 55 | SpacesInAngles: false 56 | SpacesInParentheses: false 57 | SpacesInSquareBrackets: false 58 | UseTab: Never -------------------------------------------------------------------------------- /src/AsyncNETSGPClient.cpp: -------------------------------------------------------------------------------- 1 | #include "AsyncNETSGPClient.h" 2 | 3 | #include 4 | 5 | AsyncNETSGPClient::AsyncNETSGPClient(Stream& stream, const uint8_t progPin, const uint8_t interval) 6 | : NETSGPClient(stream, progPin), mIntervalMS(1000 * interval), mDeviceIte(mDevices.begin()) 7 | { } 8 | 9 | void AsyncNETSGPClient::update() 10 | { 11 | const uint32_t currentMillis = millis(); 12 | 13 | // Send comands at mIntervalMS 14 | if (currentMillis - mLastUpdateMS >= mIntervalMS && !mCanSend) 15 | { 16 | mCanSend = true; 17 | } 18 | 19 | if (mCanSend && currentMillis - mLastSendMS >= 1010) 20 | { 21 | if (mDeviceIte != mDevices.end()) 22 | { 23 | mLastSendMS = currentMillis; 24 | mLastUpdateMS = currentMillis; 25 | sendCommand(*mDeviceIte, Command::STATUS); 26 | DEBUGF("Sent STATUS request to %#08x\n", *mDeviceIte); 27 | mCanSend = false; 28 | ++mDeviceIte; 29 | } 30 | else 31 | { 32 | mCanSend = false; // make sure we only poll every mIntervalMS 33 | mDeviceIte = mDevices.begin(); 34 | } 35 | } 36 | 37 | // Check for answers 38 | while (mStream.available() >= 27) 39 | { 40 | // Search for status message 41 | if (findAndReadReply(Command::STATUS)) 42 | { 43 | dumpBuffer(); 44 | InverterStatus status; 45 | if (fillInverterStatusFromBuffer(&mBuffer[0], status)) 46 | { 47 | if (mCallback) 48 | { 49 | mCallback(status); 50 | } 51 | mCanSend = true; 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/AsyncNETSGPClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "NETSGPClient.h" 6 | 7 | /// @brief Async version of NETSGPClient 8 | class AsyncNETSGPClient : public NETSGPClient 9 | { 10 | /// @brief Callback function type definition for inverter status updates 11 | typedef void (*InverterStatusCallback)(const NETSGPClient::InverterStatus&); 12 | 13 | public: 14 | /// @brief Construct a new AsyncNETSGPClient object. 15 | /// 16 | /// @param stream Stream to communicate with the RF module 17 | /// @param progPin Programming enable pin of RF module (active low) 18 | /// @param interval The update interval in seconds, default is 2 seconds 19 | AsyncNETSGPClient(Stream& stream, const uint8_t progPin, const uint8_t interval = 2); 20 | 21 | /// @brief Set the callback for inverter status updates 22 | /// 23 | /// @param callback Callback that gets called on updates, may be nullptr 24 | void setStatusCallback(InverterStatusCallback callback) { mCallback = callback; } 25 | 26 | /// @brief Register a new inverter to receive status updates 27 | /// 28 | /// @param deviceID The device identifier of the inverter 29 | void registerInverter(const uint32_t deviceID) { mDevices.insert(deviceID); } 30 | 31 | /// @brief Deregister an inverter to not receive status updates 32 | /// 33 | /// @param deviceID The device identifier of the inverter 34 | void deregisterInverter(const uint32_t deviceID) { mDevices.erase(deviceID); } 35 | 36 | /// @brief Update the internal state 37 | /// 38 | /// @note Needs to be called inside loop() 39 | void update(); 40 | 41 | private: 42 | uint16_t mIntervalMS; /// Update interval in milliseconds 43 | uint32_t mLastUpdateMS; /// Last update time in milliseconds 44 | uint32_t mLastSendMS; /// Makes sure we do not send to often 45 | bool mCanSend = true; /// Can the next message be sent? 46 | std::set mDevices; /// All devices to poll 47 | std::set::iterator mDeviceIte; /// Set iterator to know which device to poll 48 | InverterStatusCallback mCallback = nullptr; /// Callback for status updates 49 | }; 50 | -------------------------------------------------------------------------------- /examples/PollDemo/PollDemo.ino: -------------------------------------------------------------------------------- 1 | #include "NETSGPClient.h" 2 | 3 | constexpr const uint8_t PROG_PIN = 4; /// Programming enable pin of RF module 4 | constexpr const uint8_t RX_PIN = 16; /// RX pin of ESP32 connect to TX of RF module 5 | constexpr const uint8_t TX_PIN = 17; /// TX pin of ESP32 connect to RX of RF module 6 | constexpr const uint32_t inverterID = 0x11002793; /// Identifier of your inverter (see label on inverter) 7 | 8 | #if defined(ESP32) 9 | // On ESP32 debug output is on Serial and RF module connects to Serial2 10 | #define debugSerial Serial 11 | #define clientSerial Serial2 12 | #else 13 | // On ESP8266 or other debug output is on Serial1 and RF module connects to Serial 14 | // On D1 Mini connect RF module to pins marked RX and TX and use D4 for debug output 15 | #define debugSerial Serial1 16 | #define clientSerial Serial 17 | #endif 18 | 19 | NETSGPClient client(clientSerial, PROG_PIN); /// NETSGPClient instance 20 | 21 | void setup() 22 | { 23 | debugSerial.begin(115200); 24 | if defined(ESP32) 25 | clientSerial.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN); 26 | #else 27 | clientSerial.begin(9600); 28 | #endif 29 | pinMode(LED_BUILTIN, OUTPUT); 30 | delay(1000); 31 | debugSerial.println("Welcome to Micro Inverter Interface by ATCnetz.de and enwi.one"); 32 | 33 | // Make sure the RF module is set to the correct settings 34 | if (!client.setDefaultRFSettings()) 35 | { 36 | debugSerial.println("Could not set RF module to default settings"); 37 | } 38 | } 39 | 40 | uint32_t lastSendMillis = 0; 41 | void loop() 42 | { 43 | const uint32_t currentMillis = millis(); 44 | if (currentMillis - lastSendMillis > 2000) 45 | { 46 | lastSendMillis = currentMillis; 47 | 48 | digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); 49 | debugSerial.println(""); 50 | debugSerial.println("Sending request now"); 51 | 52 | const NETSGPClient::InverterStatus status = client.getStatus(inverterID); 53 | if (status.valid) 54 | { 55 | debugSerial.println("*********************************************"); 56 | debugSerial.println("Received Inverter Status"); 57 | debugSerial.print("Device: "); 58 | debugSerial.println(status.deviceID, HEX); 59 | debugSerial.println("Status: " + String(status.state)); 60 | debugSerial.println("DC_Voltage: " + String(status.dcVoltage) + "V"); 61 | debugSerial.println("DC_Current: " + String(status.dcCurrent) + "A"); 62 | debugSerial.println("DC_Power: " + String(status.dcPower) + "W"); 63 | debugSerial.println("AC_Voltage: " + String(status.acVoltage) + "V"); 64 | debugSerial.println("AC_Current: " + String(status.acCurrent) + "A"); 65 | debugSerial.println("AC_Power: " + String(status.acPower) + "W"); 66 | debugSerial.println("Power gen total: " + String(status.totalGeneratedPower)); 67 | debugSerial.println("Temperature: " + String(status.temperature)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/AsyncDemo/AsyncDemo.ino: -------------------------------------------------------------------------------- 1 | #include "AsyncNETSGPClient.h" 2 | 3 | constexpr const uint8_t PROG_PIN = 4; /// Programming enable pin of RF module 4 | constexpr const uint8_t RX_PIN = 16; /// RX pin of ESP32 connect to TX of RF module 5 | constexpr const uint8_t TX_PIN = 17; /// TX pin of ESP32 connect to RX of RF module 6 | constexpr const uint32_t inverterID = 0x11002793; /// Identifier of your inverter (see label on inverter) 7 | 8 | #if defined(ESP32) 9 | // On ESP32 debug output is on Serial and RF module connects to Serial2 10 | #define debugSerial Serial 11 | #define clientSerial Serial2 12 | #else 13 | // On ESP8266 or other debug output is on Serial1 and RF module connects to Serial 14 | // On D1 Mini these are the pins marked RX and TX 15 | #define debugSerial Serial1 16 | #define clientSerial Serial 17 | #endif 18 | 19 | AsyncNETSGPClient client(clientSerial, PROG_PIN); // Defaults to fetch status every 2 seconds 20 | // AsyncNETSGPClient client(clientSerial, PROG_PIN, 10); // Fetch status every 10 seconds 21 | 22 | void onInverterStatus(const AsyncNETSGPClient::InverterStatus& status) 23 | { 24 | // We do not need to check status.valid, because only valid ones are announced 25 | debugSerial.println("*********************************************"); 26 | debugSerial.println("Received Inverter Status"); 27 | debugSerial.print("Device: "); 28 | debugSerial.println(status.deviceID, HEX); 29 | debugSerial.println("Status: " + String(status.state)); 30 | debugSerial.println("DC_Voltage: " + String(status.dcVoltage) + "V"); 31 | debugSerial.println("DC_Current: " + String(status.dcCurrent) + "A"); 32 | debugSerial.println("DC_Power: " + String(status.dcPower) + "W"); 33 | debugSerial.println("AC_Voltage: " + String(status.acVoltage) + "V"); 34 | debugSerial.println("AC_Current: " + String(status.acCurrent) + "A"); 35 | debugSerial.println("AC_Power: " + String(status.acPower) + "W"); 36 | debugSerial.println("Power gen total: " + String(status.totalGeneratedPower)); 37 | debugSerial.println("Temperature: " + String(status.temperature)); 38 | } 39 | 40 | void setup() 41 | { 42 | debugSerial.begin(115200); 43 | if defined(ESP32) 44 | clientSerial.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN); 45 | #else 46 | clientSerial.begin(9600); 47 | #endif 48 | pinMode(LED_BUILTIN, OUTPUT); 49 | delay(1000); 50 | debugSerial.println("Welcome to Micro Inverter Interface by ATCnetz.de and enwi.one"); 51 | 52 | // Make sure the RF module is set to the correct settings 53 | if (!client.setDefaultRFSettings()) 54 | { 55 | debugSerial.println("Could not set RF module to default settings"); 56 | } 57 | 58 | // Make sure you set your callback to receive status updates 59 | client.setStatusCallback(onInverterStatus); 60 | // Register the inverter whose status should be read 61 | // This function can be called throughout your program to add inverters as you like 62 | // To remove an inverter whose status should not be updated anymore you can call 63 | // client.deregisterInverter(inverterID); 64 | client.registerInverter(inverterID); 65 | } 66 | 67 | void loop() 68 | { 69 | // The AsyncNETSGPClient needs to be actively updated 70 | client.update(); 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NETSGPClient 2 | Arduino Interface for cheap 2.4ghz RF enabled Solar Micro Inverters using the so-called NETSGP protocol for communication. 3 | 4 | Here is a YouTube video that shows the general function 5 | https://youtu.be/uA2eMhF7RCY 6 | [![YoutubeVideo](https://img.youtube.com/vi/uA2eMhF7RCY/0.jpg)](https://www.youtube.com/watch?v=uA2eMhF7RCY) 7 | 8 | ## Examples 9 | The `PollDemo` shows how to request a status from the Micro Inverter synchronously. 10 | Input your inverter id to get status updates. 11 | 12 | The `AsyncDemo` shows how to get a status from multiple Micro Inverters asynchronously. 13 | Input one or more inverter identifiers to get status updates inside the callback. 14 | 15 | 16 | ## Supported Devices 17 | You can find an overview of all devices and their datasheets [here](http://newenergytek.com/) 18 | 19 | | Model | Tested | Compatible | Notes | ID starting with | 20 | |:---------------|--------------------|--------------------|-------------|------------------| 21 | | `SG200LS` | :x: | :grey_question: | 200W model | :grey_question: | 22 | | `SG200MS` | :white_check_mark: | :white_check_mark: | 200W model | 11000001 | 23 | | `SG200HS` | :x: | :grey_question: | 200W model | :grey_question: | 24 | | `SG200TS` | :x: | :grey_question: | 200W model | :grey_question: | 25 | | `SG250LS` | :x: | :grey_question: | 250W model | :grey_question: | 26 | | `SG250MS` | :x: | :grey_question: | 250W model | :grey_question: | 27 | | `SG250HS` | :x: | :grey_question: | 250W model | :grey_question: | 28 | | `SG250TS` | :x: | :grey_question: | 250W model | :grey_question: | 29 | | `SG300LS` | :x: | :grey_question: | 300W model | :grey_question: | 30 | | `SG300MS` | :x: | :grey_question: | 300W model | 19000001 | 31 | | `SG300HS` | :x: | :grey_question: | 300W model | :grey_question: | 32 | | `SG300TS` | :x: | :grey_question: | 300W model | :grey_question: | 33 | | `SG350LS` | :x: | :grey_question: | 350W model | :grey_question: | 34 | | `SG350MS` | :x: | :grey_question: | 350W model | :grey_question: | 35 | | `SG350HS` | :x: | :grey_question: | 350W model | :grey_question: | 36 | | `SG350TS` | :x: | :grey_question: | 350W model | :grey_question: | 37 | | `SG400MS` | :x: | :grey_question: | 400W model | :grey_question: | 38 | | `SG400HS` | :x: | :grey_question: | 400W model | :grey_question: | 39 | | `SG400MD` | :x: | :grey_question: | 400W model | :grey_question: | 40 | | `SG400TD` | :x: | :grey_question: | 400W model | :grey_question: | 41 | | `SG450MS` | :x: | :grey_question: | 450W model | :grey_question: | 42 | | `SG450HS` | :x: | :grey_question: | 450W model | :grey_question: | 43 | | `SG450MD` | :x: | :grey_question: | 450W model | :grey_question: | 44 | | `SG450TD` | :x: | :grey_question: | 450W model | :grey_question: | 45 | | `SG500MS` | :x: | :grey_question: | 500W model | :grey_question: | 46 | | `SG500HS` | :x: | :grey_question: | 500W model | :grey_question: | 47 | | `SG500MD` | :white_check_mark: | :white_check_mark: | 500W model | 34000001 | 48 | | `SG500TD` | :x: | :grey_question: | 500W model | :grey_question: | 49 | | `SG600MD` | :white_check_mark: | :white_check_mark: | 600W model | 38000001 | 50 | | `SG600HD` | :x: | :grey_question: | 600W model | :grey_question: | 51 | | `SG600TD` | :x: | :grey_question: | 600W model | :grey_question: | 52 | | `SG700MD` | :white_check_mark: | :white_check_mark: | 700W model | 41000001 | 53 | | `SG700HD` | :x: | :grey_question: | 700W model | :grey_question: | 54 | | `SG700TD` | :x: | :grey_question: | 700W model | :grey_question: | 55 | | `SG800MD` | :white_check_mark: | :white_check_mark: | 800W model | 26000001 :grey_question: | 56 | | `SG1000MD` | :x: | :grey_question: | 1.0kW model | :grey_question: | 57 | | `SG1000HD` | :x: | :grey_question: | 1.0kW model | :grey_question: | 58 | | `SG1000MQ` | :x: | :grey_question: | 1.0kW model | 48000001 | 59 | | `SG1000TQ` | :x: | :grey_question: | 1.0kW model | :grey_question: | 60 | | `SG1200MT` | :x: | :grey_question: | 1.2kW model | :grey_question: | 61 | | `SG1200HT` | :x: | :grey_question: | 1.2kW model | :grey_question: | 62 | | `SG1200MQ` | :x: | :grey_question: | 1.2kW model | 52000001 | 63 | | `SG1400MQ` | :white_check_mark: | :white_check_mark: | 1.4kW model | :grey_question: | 64 | | `SG1400HQ` | :x: | :grey_question: | 1.4kW model | :grey_question: | 65 | 66 | ### Model number explanation 67 | Model numbers consist of three parts. Prefix, Wattage and Suffix. The suffix itself consists of two capital letters having both different meanings. The first suffix letter specifies the maximum voltage capabilities of the inverter, the second letter the amount of input MC4 connector pairs. The wattage gets split across all inputs. 68 | 69 | | Part | Value | Meaning | 70 | |:---------------------|------------|-------------| 71 | | Prefix | SG | Presumably stands for `Solar Grid` 72 | | Wattage | 200 - 1400 | Maximum power capability 73 | | Suffix first letter | L | Low operating voltage 10V-30V, MPPT voltage 14V-24V 74 | | | M | Medium operating voltage 18V-50V, MPPT voltage 24V-40V 75 | | | H | High operating voltage 30V-70V, MPPT voltage 36V-60V 76 | | | T | Top? operating voltage 70V-120V, MPPT voltage 80V-115V 77 | | Suffix second letter | S | One MC4 connector pair (Single) 78 | | | D | Two MC4 connector pairs (Dual) 79 | | | T | Three MC4 connector pairs (Triple) 80 | | | Q | Four MC4 connector pairs (Quad) 81 | -------------------------------------------------------------------------------- /src/NETSGPClient.cpp: -------------------------------------------------------------------------------- 1 | #include "NETSGPClient.h" 2 | 3 | #include 4 | 5 | NETSGPClient::NETSGPClient(Stream& stream, const uint8_t progPin) : mStream(stream), mProgPin(progPin) 6 | { 7 | pinMode(mProgPin, OUTPUT); 8 | disableProgramming(); 9 | } 10 | 11 | NETSGPClient::~NETSGPClient() { } 12 | 13 | NETSGPClient::InverterStatus NETSGPClient::getStatus(const uint32_t deviceID) 14 | { 15 | sendCommand(deviceID, Command::STATUS); 16 | InverterStatus status; 17 | if (waitForMessage() && findAndReadReply(Command::STATUS)) 18 | { 19 | fillInverterStatusFromBuffer(&mBuffer[0], status); 20 | } 21 | else 22 | { 23 | status.valid = false; 24 | } 25 | return status; 26 | } 27 | 28 | bool NETSGPClient::setPowerGrade(const uint32_t deviceID, const PowerGrade pg) 29 | { 30 | return sendCommandAndValidate(deviceID, Command::POWER_GRADE, pg); 31 | } 32 | 33 | bool NETSGPClient::activate(const uint32_t deviceID, const bool activate) 34 | { 35 | return sendCommandAndValidate(deviceID, Command::CONTROL, activate ? Control::ACTIVATE : Control::DEACTIVATE); 36 | } 37 | 38 | bool NETSGPClient::reboot(const uint32_t deviceID) 39 | { 40 | return sendCommandAndValidate(deviceID, Command::CONTROL, Control::REBOOT); 41 | } 42 | 43 | LC12S::Settings NETSGPClient::readRFModuleSettings() 44 | { 45 | uint8_t* bufferPointer = &mBuffer[0]; 46 | 47 | *bufferPointer++ = 0xAA; // command byte 48 | *bufferPointer++ = 0x5C; // command byte 49 | *bufferPointer++ = 0x00; // module identifier 50 | *bufferPointer++ = 0x00; // module identifier 51 | *bufferPointer++ = 0x00; // networking identifier 52 | *bufferPointer++ = 0x00; // networking identifier 53 | *bufferPointer++ = 0x00; // NC must be 0 54 | *bufferPointer++ = 0x00; // RF power 55 | *bufferPointer++ = 0x00; // NC must be 0 56 | *bufferPointer++ = 0x00; // Baudrate 57 | *bufferPointer++ = 0x00; // NC must be 0 58 | *bufferPointer++ = 0x00; // RF channel (0 - 127) 59 | *bufferPointer++ = 0x00; // NC must be 0 60 | *bufferPointer++ = 0x00; // NC must be 0 61 | *bufferPointer++ = 0x00; // NC must be 0 62 | *bufferPointer++ = 0x12; // Length 63 | *bufferPointer++ = 0x00; // NC must be 0 64 | *bufferPointer++ = 0x18; // Checksum 65 | 66 | enableProgramming(); 67 | 68 | mStream.write(&mBuffer[0], 18); 69 | const size_t read = mStream.readBytes(&mBuffer[0], 18); 70 | 71 | disableProgramming(); 72 | 73 | LC12S::Settings settings; 74 | if (read == 18 && mBuffer[0] == 0xAA && mBuffer[1] == 0x5D && mBuffer[17] == calcCRC(17)) 75 | { 76 | settings.valid = true; 77 | 78 | settings.moduleID = mBuffer[2] << 8 | (mBuffer[3] & 0xFF); 79 | settings.networkID = mBuffer[4] << 8 | (mBuffer[5] & 0xFF); 80 | settings.rfPower = static_cast(mBuffer[7]); 81 | settings.baudrate = static_cast(mBuffer[9]); 82 | settings.rfChannel = mBuffer[11]; 83 | } 84 | else 85 | { 86 | settings.valid = false; 87 | } 88 | return settings; 89 | } 90 | 91 | bool NETSGPClient::writeRFModuleSettings(const LC12S::Settings& settings) 92 | { 93 | uint8_t* bufferPointer = &mBuffer[0]; 94 | 95 | *bufferPointer++ = 0xAA; // command byte 96 | *bufferPointer++ = 0x5A; // command byte 97 | *bufferPointer++ = (settings.moduleID >> 8) & 0xFF; // module identifier 98 | *bufferPointer++ = settings.moduleID & 0xFF; // module identifier 99 | *bufferPointer++ = (settings.networkID >> 8) & 0xFF; // networking identifier 100 | *bufferPointer++ = settings.networkID & 0xFF; // networking identifier 101 | *bufferPointer++ = 0x00; // NC must be 0 102 | *bufferPointer++ = settings.rfPower; // RF power 103 | *bufferPointer++ = 0x00; // NC must be 0 104 | *bufferPointer++ = settings.baudrate; // Baudrate 105 | *bufferPointer++ = 0x00; // NC must be 0 106 | *bufferPointer++ = settings.rfChannel; // RF channel (0 - 127) 107 | *bufferPointer++ = 0x00; // NC must be 0 108 | *bufferPointer++ = 0x00; // NC must be 0 109 | *bufferPointer++ = 0x00; // NC must be 0 110 | *bufferPointer++ = 0x12; // Length 111 | *bufferPointer++ = 0x00; // NC must be 0 112 | *bufferPointer++ = calcCRC(16); // Checksum, we can calc for 16 bytes since 17th one is 0 113 | 114 | enableProgramming(); 115 | 116 | mStream.write(&mBuffer[0], 18); 117 | const size_t read = mStream.readBytes(&mBuffer[0], 18); 118 | 119 | disableProgramming(); 120 | 121 | return read == 18 && mBuffer[0] == 0xAA && mBuffer[1] == 0x5B && mBuffer[17] == calcCRC(17); 122 | } 123 | 124 | bool NETSGPClient::setDefaultRFSettings() 125 | { 126 | LC12S::Settings settings = readRFModuleSettings(); 127 | if (settings != LC12S::DEFAULT_SETTINGS) 128 | { 129 | // Copy over default settings without moduleID since that is uinque for each module 130 | settings.networkID = LC12S::DEFAULT_SETTINGS.networkID; 131 | settings.rfPower = LC12S::DEFAULT_SETTINGS.rfPower; 132 | settings.baudrate = LC12S::DEFAULT_SETTINGS.baudrate; 133 | settings.rfChannel = LC12S::DEFAULT_SETTINGS.rfChannel; 134 | return writeRFModuleSettings(settings); 135 | } 136 | return true; 137 | } 138 | 139 | void NETSGPClient::sendCommand(const uint32_t deviceID, const Command command, const uint8_t value) 140 | { 141 | uint8_t* bufferPointer = &mBuffer[0]; 142 | 143 | *bufferPointer++ = MAGIC_BYTE; 144 | *bufferPointer++ = command; 145 | *bufferPointer++ = 0x00; // data box ID 146 | *bufferPointer++ = 0x00; // data box ID 147 | *bufferPointer++ = 0x00; 148 | *bufferPointer++ = 0x00; 149 | *bufferPointer++ = (deviceID >> 24) & 0xFF; 150 | *bufferPointer++ = (deviceID >> 16) & 0xFF; 151 | *bufferPointer++ = (deviceID >> 8) & 0xFF; 152 | *bufferPointer++ = deviceID & 0xFF; 153 | *bufferPointer++ = 0x00; 154 | *bufferPointer++ = 0x00; 155 | *bufferPointer++ = 0x00; 156 | *bufferPointer++ = value; 157 | *bufferPointer++ = calcCRC(14); 158 | 159 | mStream.write(&mBuffer[0], 15); 160 | } 161 | 162 | bool NETSGPClient::sendCommandAndValidate(const uint32_t deviceID, const Command command, const uint8_t value) 163 | { 164 | sendCommand(deviceID, command, value); 165 | if (waitForMessage() && findAndReadReply(command)) 166 | { 167 | const bool crc = mBuffer[14] == calcCRC(14); 168 | const bool valid = mBuffer[13] == value; 169 | 170 | DEBUGF("[sendCommandAndValidate] CRC %s & value %s\n", crc ? "valid" : "invalid", valid ? "valid" : "invalid"); 171 | 172 | return crc && valid; 173 | } 174 | 175 | return false; 176 | } 177 | 178 | bool NETSGPClient::waitForMessage() 179 | { 180 | const uint32_t startTime = millis(); 181 | while (millis() - startTime < 1000) 182 | { 183 | if (mStream.available()) 184 | { 185 | return true; 186 | } 187 | delay(1); 188 | } 189 | DEBUGLN("[waitForMessage] Timeout"); 190 | return false; 191 | } 192 | 193 | bool NETSGPClient::findAndReadReply(const Command command) 194 | { 195 | // Search for a reply header consisting of magic byte and one of the command bytes 196 | const char header[2] = {MAGIC_BYTE, command}; 197 | if (!mStream.find(&header[0], 2)) 198 | { 199 | DEBUGLN("[findAndReadReply] Could not find header"); 200 | return false; 201 | } 202 | 203 | size_t bytesToRead; 204 | switch (command) 205 | { 206 | case Command::STATUS: 207 | // whole message is 27 bytes 208 | bytesToRead = 25; 209 | break; 210 | case Command::CONTROL: 211 | case Command::POWER_GRADE: 212 | // whole message is 15 bytes 213 | bytesToRead = 13; 214 | break; 215 | default: 216 | bytesToRead = 0; 217 | DEBUGLN("[findAndReadReply] Unknown command"); 218 | break; 219 | } 220 | 221 | if (bytesToRead) 222 | { 223 | const size_t bytesRead = mStream.readBytes(&mBuffer[2], bytesToRead); 224 | dumpBuffer(2 + bytesRead); 225 | return bytesRead == bytesToRead; 226 | } 227 | 228 | return false; 229 | } 230 | 231 | uint8_t NETSGPClient::calcCRC(const size_t bytes) const 232 | { 233 | uint8_t crc = 0; 234 | for (size_t i = 0; i < bytes; ++i) 235 | { 236 | crc += mBuffer[i]; 237 | } 238 | return crc; 239 | } 240 | 241 | void NETSGPClient::enableProgramming() 242 | { 243 | digitalWrite(mProgPin, LOW); 244 | delay(400); 245 | } 246 | 247 | void NETSGPClient::disableProgramming() 248 | { 249 | digitalWrite(mProgPin, HIGH); 250 | } 251 | 252 | bool NETSGPClient::fillInverterStatusFromBuffer(const uint8_t* buffer, InverterStatus& status) 253 | { 254 | status.deviceID = buffer[6] << 24 | buffer[7] << 16 | buffer[8] << 8 | (buffer[9] & 0xFF); 255 | 256 | const uint32_t tempTotal = buffer[10] << 24 | buffer[11] << 16 | buffer[12] << 8 | (buffer[13] & 0xFF); 257 | status.totalGeneratedPower = *((float*)&tempTotal); 258 | 259 | status.dcVoltage = (buffer[15] << 8 | buffer[16]) / 100.0f; 260 | status.dcCurrent = (buffer[17] << 8 | buffer[18]) / 100.0f; 261 | status.dcPower = status.dcVoltage * status.dcCurrent; 262 | 263 | status.acVoltage = (buffer[19] << 8 | buffer[20]) / 100.0f; 264 | status.acCurrent = (buffer[21] << 8 | buffer[22]) / 100.0f; 265 | status.acPower = status.acVoltage * status.acCurrent; 266 | 267 | status.state = buffer[25]; // not fully reversed 268 | 269 | status.temperature = buffer[26]; // not fully reversed 270 | 271 | status.valid = buffer[14] == calcCRC(14); 272 | 273 | DEBUGF("CRC %s\n", status.valid ? "valid" : "invalid"); 274 | 275 | return status.valid; 276 | } 277 | 278 | void NETSGPClient::dumpBuffer(const size_t bytes) 279 | { 280 | #ifdef DEBUG_SERIAL 281 | if (bytes <= BUFFER_SIZE) 282 | { 283 | for (uint8_t i = 0; i < bytes; ++i) 284 | { 285 | DEBUGF("%02X", mBuffer[i]); 286 | } 287 | DEBUGLN(); 288 | } 289 | #endif 290 | } 291 | -------------------------------------------------------------------------------- /src/NETSGPClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // To enable debug output uncomment one of the below lines 6 | // #define DEBUG_SERIAL Serial 7 | // #define DEBUG_SERIAL Serial1 8 | 9 | #ifdef DEBUG_SERIAL 10 | #define DEBUG(s) DEBUG_SERIAL.print(s) 11 | #define DEBUGLN(s) DEBUG_SERIAL.println(s) 12 | #if defined(__cplusplus) && (__cplusplus > 201703L) 13 | #define DEBUGF(format, ...) DEBUG_SERIAL.printf_P(PSTR(format), __VA_OPT__(, ) __VA_ARGS__) 14 | #else // !(defined(__cplusplus) && (__cplusplus > 201703L)) 15 | #define DEBUGF(format, ...) DEBUG_SERIAL.printf_P(PSTR(format), ##__VA_ARGS__) 16 | #endif 17 | #else 18 | #define DEBUG(s) 19 | #define DEBUGLN(s) 20 | #define DEBUGF(format, ...) 21 | #endif 22 | 23 | /// @brief LC12S 2.4GHz RF module specific stuff 24 | namespace LC12S 25 | { 26 | /// @brief RF power setting in dBm 27 | enum RFPower 28 | { 29 | DBM_12 = 0, // 12 dBm 30 | DBM_10 = 1, // 10 dBm 31 | DBM_09 = 2, // 9 dBm 32 | DBM_08 = 3, // 8 dBm 33 | DBM_06 = 4, // 6 dBm 34 | DBM_03 = 5, // 3 dBm 35 | DBM_00 = 6, // 0 dBm 36 | DBM_N_02 = 7, // -2 dBm 37 | DBM_N_05 = 8, // -5 dBm 38 | DBM_N_10 = 9, // -10 dBm 39 | DBM_N_15 = 10, // -15 dBm 40 | DBM_N_20 = 11, // -20 dBm 41 | DBM_N_25 = 12, // -25 dBm 42 | DBM_N_30 = 13, // -30 dBm 43 | DBM_N_35 = 14, // -35 dBm 44 | }; 45 | 46 | /// @brief Baudrate setting in baud 47 | enum Baudrate 48 | { 49 | BPS_600 = 0, 50 | BPS_1200 = 1, 51 | BPS_2400 = 2, 52 | BPS_4800 = 3, 53 | BPS_9600 = 4, 54 | BPS_19200 = 5, 55 | BPS_38400 = 6, 56 | }; 57 | 58 | /// @brief LC12S module settings 59 | struct Settings 60 | { 61 | uint16_t moduleID; /// Unique module identifier 62 | uint16_t networkID; /// Network identifier 63 | LC12S::RFPower rfPower; /// RF power 64 | LC12S::Baudrate baudrate; /// Baudrate 65 | uint8_t rfChannel; /// RF channel 66 | bool valid; /// Is this settings object valid (true) or not (false) 67 | 68 | bool operator==(const Settings& rhs) const 69 | { 70 | return networkID == rhs.networkID && rfPower == rhs.rfPower && baudrate == rhs.baudrate 71 | && rfChannel == rhs.rfChannel && valid == rhs.valid; 72 | } 73 | bool operator!=(const Settings& rhs) const { return !operator==(rhs); } 74 | }; 75 | 76 | constexpr const Settings DEFAULT_SETTINGS = { 77 | .moduleID = 0x58AF, 78 | .networkID = 0x0000, 79 | .rfPower = RFPower::DBM_12, 80 | .baudrate = Baudrate::BPS_9600, 81 | .rfChannel = 0x64, 82 | .valid = true, 83 | }; 84 | 85 | } // namespace LC12S 86 | 87 | /// @brief Class for micro inverter communication 88 | class NETSGPClient 89 | { 90 | public: 91 | /// @brief Contains status information of a specific inverter 92 | struct InverterStatus 93 | { 94 | uint32_t deviceID; /// Unique inverter identifier 95 | 96 | uint8_t state; /// Inverter state (not reversed) 97 | uint8_t temperature; /// Inverter temperature (not reversed) 98 | 99 | bool valid; /// Validity of the contained data 100 | 101 | float totalGeneratedPower; /// Total generated power (kWh? not reversed) 102 | 103 | float dcVoltage; /// DC voltage in Volts (panel voltage) 104 | float dcCurrent; /// DC current in Amperes (panel current) 105 | float dcPower; /// DC power in Watts (panel power) 106 | 107 | float acVoltage; /// AC voltage in Volts 108 | float acCurrent; /// AC current in Amperes 109 | float acPower; /// AC power in Watts 110 | }; 111 | 112 | /// @brief All possible power grades from 0% up to 100% 113 | enum PowerGrade 114 | { 115 | PG0, 116 | PG1, 117 | PG2, 118 | PG3, 119 | PG4, 120 | PG5, 121 | PG6, 122 | PG7, 123 | PG8, 124 | PG9, 125 | PG10, 126 | PG11, 127 | PG12, 128 | PG13, 129 | PG14, 130 | PG15, 131 | PG16, 132 | PG17, 133 | PG18, 134 | PG19, 135 | PG20, 136 | PG21, 137 | PG22, 138 | PG23, 139 | PG24, 140 | PG25, 141 | PG26, 142 | PG27, 143 | PG28, 144 | PG29, 145 | PG30, 146 | PG31, 147 | PG32, 148 | PG33, 149 | PG34, 150 | PG35, 151 | PG36, 152 | PG37, 153 | PG38, 154 | PG39, 155 | PG40, 156 | PG41, 157 | PG42, 158 | PG43, 159 | PG44, 160 | PG45, 161 | PG46, 162 | PG47, 163 | PG48, 164 | PG49, 165 | PG50, 166 | PG51, 167 | PG52, 168 | PG53, 169 | PG54, 170 | PG55, 171 | PG56, 172 | PG57, 173 | PG58, 174 | PG59, 175 | PG60, 176 | PG61, 177 | PG62, 178 | PG63, 179 | PG64, 180 | PG65, 181 | PG66, 182 | PG67, 183 | PG68, 184 | PG69, 185 | PG70, 186 | PG71, 187 | PG72, 188 | PG73, 189 | PG74, 190 | PG75, 191 | PG76, 192 | PG77, 193 | PG78, 194 | PG79, 195 | PG80, 196 | PG81, 197 | PG82, 198 | PG83, 199 | PG84, 200 | PG85, 201 | PG86, 202 | PG87, 203 | PG88, 204 | PG89, 205 | PG90, 206 | PG91, 207 | PG92, 208 | PG93, 209 | PG94, 210 | PG95, 211 | PG96, 212 | PG97, 213 | PG98, 214 | PG99, 215 | PG100, 216 | }; 217 | 218 | public: 219 | /// @brief Construct a new NETSGPClient object. 220 | /// 221 | /// @param stream Stream to communicate with the RF module 222 | /// @param progPin Programming enable pin of RF module (active low) 223 | NETSGPClient(Stream& stream, const uint8_t progPin); 224 | 225 | /// @brief Destroy the NETSGPClient object 226 | ~NETSGPClient(); 227 | 228 | /// @brief Get the status of the given device. 229 | /// 230 | /// @param deviceID Unique device identifier 231 | /// @return InverterStatus Status of the inverter (InverterStatus.valid == true) or empty status 232 | /// (InverterStatus.valid == false) 233 | InverterStatus getStatus(const uint32_t deviceID); 234 | 235 | /// @brief Set the power grade of the given inverter 236 | /// 237 | /// @param deviceID Unique device identifier 238 | /// @param pg Power grade from 0-100% to set 239 | /// @return true if power grade was set successfully 240 | /// @return false if not 241 | bool setPowerGrade(const uint32_t deviceID, const PowerGrade pg); 242 | 243 | /// @brief Activate or deactivate the given inverter 244 | /// 245 | /// @param deviceID Unique device identifier 246 | /// @param activate True to activate, false to deactivate 247 | /// @return true if inverter was activated/deactivated 248 | /// @return false if not 249 | bool activate(const uint32_t deviceID, const bool activate); 250 | 251 | /// @brief Reboot the given inverter 252 | /// 253 | /// @param deviceID Unique device identifier 254 | /// @return true True if inverter will reboot 255 | /// @return false if not 256 | bool reboot(const uint32_t deviceID); 257 | 258 | /// @brief Read the settings of the RF module 259 | LC12S::Settings readRFModuleSettings(); 260 | 261 | /// @brief Change the settings of the RF module to the provided ones. 262 | /// 263 | /// @param settings Settings to write to the RF module 264 | /// @return true If settings were written successfully 265 | /// @return false If not 266 | bool writeRFModuleSettings(const LC12S::Settings& settings); 267 | 268 | /// @brief Set the RF module to its default settings if needed. 269 | /// 270 | /// This function will read the RF module settings and then compare these with the default ones and if they 271 | /// mismatch will write the default config 272 | /// @return true If settings are correct or written successfully 273 | /// @return false If settings could not be written 274 | bool setDefaultRFSettings(); 275 | 276 | protected: 277 | /// @brief All known commands 278 | enum Command 279 | { 280 | STATUS = 0xC0, /// Get status command (0xC0) 281 | CONTROL = 0xC1, /// Control command (0xC1) 282 | POWER_GRADE = 0xC3, /// Set power grade command (0xC3) 283 | }; 284 | 285 | /// @brief All known control values 286 | enum Control 287 | { 288 | ACTIVATE = 0x01, /// Activate inverter 289 | DEACTIVATE = 0x02, /// Deactivate inverter 290 | REBOOT = 0x03, /// Reboot inverter 291 | }; 292 | 293 | protected: 294 | /// @brief Send a specific command to a specific inverter with a specific value. 295 | /// 296 | /// @param deviceID Recipient inverter identifier 297 | /// @param command Command to send 298 | /// @param value Optional value to send 299 | void sendCommand(const uint32_t deviceID, const Command command, const uint8_t value = 0x00); 300 | 301 | /// @brief Send a specific command to a specific inverter with a specific value and validate the reply 302 | /// 303 | /// @param deviceID Recipient inverter identifier 304 | /// @param command Command to send 305 | /// @param value Optional value to send 306 | /// @return true If command was sent and validated 307 | /// @return false not 308 | bool sendCommandAndValidate(const uint32_t deviceID, const Command command, const uint8_t value = 0x00); 309 | 310 | /// @brief Wait for a message with a timeout of 1 second 311 | /// 312 | /// @return true If stream contains a message within timeout 313 | /// @return false If not 314 | bool waitForMessage(); 315 | 316 | /// @brief Try to find a reply in the stream and if present read it into mBuffer 317 | /// 318 | /// @param command Expected command of reply 319 | /// @return true If reply was found and read into mBuffer 320 | /// @return false If not 321 | bool findAndReadReply(const Command command); 322 | 323 | /// @brief Calculate the checksum for a message inside the buffer. 324 | /// 325 | /// @param bytes The amount of bytes to calculate the checksum for 326 | /// @return uint8_t CRC 327 | uint8_t calcCRC(const size_t bytes) const; 328 | 329 | /// @brief Enable programming mode of the RF module. 330 | /// 331 | /// This function will delay code execution for 400ms 332 | void enableProgramming(); 333 | 334 | /// @brief Disable programming mode of the RF module. 335 | /// 336 | /// This function will delay code execution for 10ms 337 | void disableProgramming(); 338 | 339 | /// @brief Fill the given inverter status from the given buffer 340 | /// 341 | /// @param buffer Bufffer containing raw inverter status data, must be at least 27 bytes in size 342 | /// @param status Inverter status to fill 343 | /// @return true If checksum is valid 344 | /// @return false If checksum is invalid 345 | bool fillInverterStatusFromBuffer(const uint8_t* buffer, InverterStatus& status); 346 | 347 | /// @brief Dump the buffer contents to debug serial 348 | /// 349 | /// @param bytes Amount of bytes to dump 350 | void dumpBuffer(const size_t bytes = BUFFER_SIZE); 351 | 352 | protected: 353 | constexpr static const size_t BUFFER_SIZE = 32; 354 | constexpr static const uint8_t MAGIC_BYTE = 0x43; /// Magic byte indicating start of messages 355 | Stream& mStream; /// Stream for communication 356 | uint8_t mProgPin; /// Programming enable pin of RF module (active low) 357 | uint8_t mBuffer[BUFFER_SIZE] = {0}; /// Inernal buffer 358 | }; 359 | --------------------------------------------------------------------------------