├── Modbus_Proxy ├── .gitignore ├── platformio.ini.tmp.54544.1758793245345 ├── src │ ├── credentials.h.example │ ├── evcc_api.h │ ├── modbus_proxy.h │ ├── debug.h │ ├── mqtt_handler.h │ ├── dtsu666.h │ ├── config.h │ ├── evcc_api.cpp │ ├── ModbusRTU485.cpp │ ├── mqtt_handler.cpp │ ├── modbus_proxy.cpp │ ├── main.cpp │ └── dtsu666.cpp ├── platformio.ini.tmp.54544.1758822885267 ├── test │ └── README ├── scripts │ └── upload_and_monitor.ps1 ├── platformio.ini ├── include │ ├── README │ └── ModbusRTU485.h └── Modbus-Proxy-FSD.md ├── .gitignore └── README.md /Modbus_Proxy/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /Modbus_Proxy/platformio.ini.tmp.54544.1758793245345: -------------------------------------------------------------------------------- 1 | [env:esp32-s3-devkitc-1] 2 | platform = espressif32 3 | board = esp32-s3-devkitc-1 4 | framework = arduino 5 | monitor_speed = 115200 6 | lib_deps = 7 | bblanchon/ArduinoJson@^6.19.4 8 | knolleary/PubSubClient@^2.8 -------------------------------------------------------------------------------- /Modbus_Proxy/src/credentials.h.example: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // WiFi credentials - UPDATE THESE FOR YOUR SETUP 4 | static const char* ssid = "YOUR_WIFI_SSID"; 5 | static const char* password = "YOUR_WIFI_PASSWORD"; 6 | 7 | // MQTT broker settings 8 | static const char* mqttServer = "192.168.0.100"; 9 | 10 | // EVCC API settings 11 | static const char* evccApiUrl = "http://192.168.0.100:7070/api/state"; 12 | -------------------------------------------------------------------------------- /Modbus_Proxy/platformio.ini.tmp.54544.1758822885267: -------------------------------------------------------------------------------- 1 | [env:esp32-s3-devkitc-1] 2 | platform = espressif32 3 | board = esp32-s3-devkitc-1 4 | framework = arduino 5 | monitor_speed = 115200 6 | upload_protocol = esptool 7 | upload_mode = default 8 | upload_port = COM6 9 | monitor_port = COM6 10 | monitor_dtr = 0 11 | monitor_rts = 0 12 | lib_deps = 13 | bblanchon/ArduinoJson@^6.19.4 14 | knolleary/PubSubClient@^2.8 -------------------------------------------------------------------------------- /Modbus_Proxy/test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Test Runner and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html 12 | -------------------------------------------------------------------------------- /Modbus_Proxy/scripts/upload_and_monitor.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$Port 3 | ) 4 | 5 | $ErrorActionPreference = 'Stop' 6 | 7 | if (-not (Get-Command pio -ErrorAction SilentlyContinue)) { 8 | Write-Error "PlatformIO CLI 'pio' not found. Install from https://platformio.org/install and ensure 'pio' is in PATH." 9 | exit 1 10 | } 11 | 12 | $projectRoot = Split-Path -Parent $PSScriptRoot 13 | Push-Location $projectRoot 14 | try { 15 | Write-Host "Uploading to ESP32 (env: esp32dev)..." 16 | $uploadArgs = @('run','-e','esp32dev','-t','upload') 17 | if ($Port) { $uploadArgs += @('--upload-port', $Port) } 18 | pio @uploadArgs 19 | 20 | Write-Host "Starting serial monitor at 115200. Press Ctrl+] to exit." 21 | $monitorArgs = @('device','monitor','-b','115200') 22 | if ($Port) { $monitorArgs += @('--port', $Port) } 23 | pio @monitorArgs 24 | } 25 | finally { 26 | Pop-Location 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Modbus_Proxy/src/evcc_api.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "config.h" 8 | 9 | // Thread-safe shared EVCC data structure 10 | struct SharedEVCCData { 11 | SemaphoreHandle_t mutex; // Mutex for thread-safe access 12 | float chargePower; // Charge power from EVCC API (W) 13 | uint32_t timestamp; // When data was last updated (millis) 14 | bool valid; // Whether data is valid 15 | uint32_t updateCount; // Number of successful API calls 16 | uint32_t errorCount; // Number of failed API calls 17 | }; 18 | 19 | // Function declarations 20 | bool initEVCCAPI(); 21 | bool pollEvccApi(SharedEVCCData& sharedData); 22 | float calculatePowerCorrection(const SharedEVCCData& evccData); 23 | bool isEVCCDataValid(const SharedEVCCData& evccData); 24 | void getEVCCData(const SharedEVCCData& sharedData, float& chargePower, bool& valid); 25 | 26 | // HTTP client utilities 27 | bool httpGetJSON(const char* url, JsonDocument& doc); 28 | void logAPIError(const String& error, int httpCode = 0); -------------------------------------------------------------------------------- /Modbus_Proxy/platformio.ini: -------------------------------------------------------------------------------- 1 | 2 | [env:esp32-c3-base] 3 | ; Common settings for both OTA and Serial environments 4 | platform = espressif32 5 | board = esp32-c3-devkitm-1 6 | framework = arduino 7 | monitor_speed = 115200 8 | monitor_dtr = 0 9 | monitor_rts = 0 10 | build_flags = 11 | -DARDUINO_USB_CDC_ON_BOOT=0 12 | -std=gnu++17 13 | board_build.usb_cdc = false 14 | board_build.arduino.memory_type = qio_qspi 15 | lib_deps = 16 | bblanchon/ArduinoJson@^6.19.4 17 | knolleary/PubSubClient@^2.8 18 | ArduinoOTA 19 | 20 | ; ---------------------------------------------------------------------- 21 | ; --- ENVIRONMENT 1: SERIAL Upload/Monitor (for initial flash) --- 22 | [env:esp32-c3-serial] 23 | extends = env:esp32-c3-base 24 | ; Use the default serial protocol 25 | upload_protocol = esptool 26 | upload_port = COM6 27 | monitor_port = COM6 28 | 29 | ; ---------------------------------------------------------------------- 30 | ; --- ENVIRONMENT 2: OTA Upload (for wireless updates) --- 31 | [env:esp32-c3-ota] 32 | extends = env:esp32-c3-base 33 | ; Use the network protocol 34 | upload_protocol = espota 35 | upload_port = 192.168.0.177 36 | upload_flags = --auth=modbus_ota_2023 37 | ; Monitor port remains COM6 (inherited) for viewing serial logs wirelessly -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PlatformIO 2 | .pio/ 3 | .pioenvs/ 4 | .piolibdeps/ 5 | .vscode/ 6 | .clang_complete 7 | .gcc-flags.json 8 | .ccls 9 | .cache/ 10 | 11 | # Build artifacts 12 | build/ 13 | .build/ 14 | *.bin 15 | *.elf 16 | *.map 17 | *.hex 18 | *.lst 19 | 20 | # Compiled Object files 21 | *.o 22 | *.obj 23 | 24 | # Libraries 25 | lib/ 26 | !lib/readme.txt 27 | 28 | # IDE files 29 | *.code-workspace 30 | .vscode/settings.json 31 | .vscode/launch.json 32 | .vscode/c_cpp_properties.json 33 | # Keep .vscode/extensions.json and .vscode/tasks.json for project setup 34 | 35 | # Claude Code IDE 36 | .claude/ 37 | CLAUDE.md 38 | 39 | # OS generated files 40 | .DS_Store 41 | .DS_Store? 42 | ._* 43 | .Spotlight-V100 44 | .Trashes 45 | ehthumbs.db 46 | Thumbs.db 47 | *~ 48 | 49 | # Credentials and sensitive data 50 | credentials.h 51 | credentials.h.bak 52 | *_backup.* 53 | *.secret 54 | 55 | # Logs 56 | *.log 57 | debug.log 58 | serial.log 59 | 60 | # Temporary files 61 | *.tmp 62 | *.temp 63 | *.swp 64 | *.swo 65 | *~ 66 | 67 | # ESP32 specific 68 | sdkconfig 69 | sdkconfig.old 70 | components/ 71 | main/component.mk 72 | 73 | # Arduino IDE 74 | *.fqbn 75 | *.bak 76 | 77 | # Python 78 | __pycache__/ 79 | *.py[cod] 80 | *$py.class 81 | *.so 82 | .Python 83 | env/ 84 | venv/ 85 | .venv/ 86 | .env 87 | 88 | # Node.js (if using any web tools) 89 | node_modules/ 90 | npm-debug.log* 91 | yarn-debug.log* 92 | yarn-error.log* -------------------------------------------------------------------------------- /Modbus_Proxy/include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /Modbus_Proxy/src/modbus_proxy.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "ModbusRTU485.h" 6 | #include "config.h" 7 | #include "dtsu666.h" 8 | #include "evcc_api.h" 9 | #include "mqtt_handler.h" 10 | 11 | // FreeRTOS task declarations 12 | void proxyTask(void *pvParameters); 13 | void watchdogTask(void *pvParameters); 14 | 15 | // Proxy initialization 16 | bool initModbusProxy(); 17 | bool initSerialInterfaces(); 18 | 19 | // Core proxy functions 20 | void handleModbusTraffic(); 21 | bool processMessage(const ModbusMessage& msg, bool fromSUN2000); 22 | void forwardMessage(const uint8_t* data, size_t len, bool toSUN2000); 23 | 24 | // Power correction functions 25 | void calculatePowerCorrection(); 26 | bool shouldApplyCorrection(float wallboxPower); 27 | void updateSharedData(const uint8_t* responseData, uint16_t responseLen, const DTSU666Data& parsedData); 28 | 29 | // Health monitoring 30 | void updateTaskHealthbeat(bool isProxyTask); 31 | void performHealthCheck(); 32 | void reportSystemError(const char* subsystem, const char* error, int code = 0); 33 | 34 | // Message validation 35 | bool isValidModbusMessage(const uint8_t* data, size_t len); 36 | bool validateCRC(const uint8_t* data, size_t len); 37 | 38 | // Task synchronization globals 39 | extern SemaphoreHandle_t proxyTaskHealthMutex; 40 | extern SemaphoreHandle_t mqttTaskHealthMutex; 41 | extern uint32_t proxyTaskLastSeen; 42 | extern uint32_t mqttTaskLastSeen; 43 | 44 | // Serial interfaces 45 | extern HardwareSerial SerialSUN; 46 | extern HardwareSerial SerialDTU; 47 | 48 | // ModbusRTU485 instances 49 | extern ModbusRTU485 modbusSUN; 50 | extern ModbusRTU485 modbusDTU; 51 | 52 | // Shared data structures 53 | extern SharedDTSUData sharedDTSU; 54 | extern SharedEVCCData sharedEVCC; 55 | extern DTSU666Data dtsu666Data; 56 | extern LastRequestInfo g_lastReq; 57 | 58 | // Power correction variables 59 | extern float powerCorrection; 60 | extern bool powerCorrectionActive; 61 | extern uint32_t lastCorrectionTime; 62 | extern uint32_t lastHttpPoll; -------------------------------------------------------------------------------- /Modbus_Proxy/include/ModbusRTU485.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | enum class MBType : uint8_t { 5 | Unknown = 0, 6 | Request = 1, 7 | Reply = 2, 8 | Exception = 3 9 | }; 10 | 11 | struct ModbusMessage { 12 | bool valid = false; 13 | MBType type = MBType::Unknown; 14 | uint8_t id = 0; 15 | uint8_t fc = 0; 16 | uint16_t len = 0; 17 | 18 | // 0x03/0x04 request 19 | uint16_t startAddr = 0; 20 | uint16_t qty = 0; 21 | 22 | // 0x03/0x04 reply 23 | uint8_t byteCount = 0; 24 | 25 | // 0x06 single write (req/rep) 26 | uint16_t wrAddr = 0; 27 | uint16_t wrValue = 0; 28 | 29 | // 0x10 multiple write 30 | uint16_t wrQty = 0; 31 | uint8_t wrByteCount = 0; 32 | 33 | // Exception data 34 | uint8_t exCode = 0; 35 | 36 | // Raw frame data 37 | const uint8_t* raw = nullptr; 38 | }; 39 | 40 | class ModbusRTU485 { 41 | public: 42 | ModbusRTU485() {} 43 | 44 | void begin(HardwareSerial& port, uint32_t baud); 45 | 46 | // Passive read with timeout (ms). Returns true if a full frame was parsed. 47 | bool read(ModbusMessage& out, uint32_t timeoutMs = 100); 48 | 49 | // Write/forward a MODBUS message. Returns true if successfully transmitted. 50 | bool write(const ModbusMessage& msg, uint32_t timeoutMs = 100); 51 | 52 | // Write raw frame with automatic CRC calculation. Returns true if successful. 53 | bool write(const uint8_t* data, size_t len, uint32_t timeoutMs = 100); 54 | 55 | // Public CRC calculation for response modification 56 | static uint16_t crc16(const uint8_t* p, size_t n); 57 | 58 | private: 59 | HardwareSerial* _ser = nullptr; 60 | uint32_t _baud = 0; 61 | uint32_t _tChar = 0; // us per 1 char (11 bits @ 8N1 safety) 62 | uint32_t _t3_5 = 0; // 3.5 char in us 63 | uint32_t _t1_5 = 0; // 1.5 char in us 64 | 65 | static constexpr size_t BUF_SIZE = 512; 66 | uint8_t _buf[BUF_SIZE]; 67 | size_t _len = 0; 68 | 69 | static inline uint16_t be16(const uint8_t* p) { 70 | return (uint16_t(p[0]) << 8) | p[1]; 71 | } 72 | 73 | bool parse(const uint8_t* f, uint16_t n, ModbusMessage& m); 74 | }; -------------------------------------------------------------------------------- /Modbus_Proxy/src/debug.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "config.h" 5 | 6 | // Telnet debug support 7 | #if defined(ENABLE_TELNET_DEBUG) && ENABLE_TELNET_DEBUG 8 | #include 9 | 10 | class TelnetDebug { 11 | private: 12 | WiFiServer* server; 13 | WiFiClient client; 14 | char buffer[512]; 15 | size_t bufferPos; 16 | 17 | public: 18 | TelnetDebug() : server(nullptr), bufferPos(0) {} 19 | 20 | void begin(uint16_t port = 23) { 21 | if (!server) { 22 | server = new WiFiServer(port); 23 | server->begin(); 24 | server->setNoDelay(true); 25 | } 26 | } 27 | 28 | void handle() { 29 | if (server && server->hasClient()) { 30 | if (!client || !client.connected()) { 31 | if (client) client.stop(); 32 | client = server->available(); 33 | client.setNoDelay(true); 34 | client.println("\n=== ESP32-C3 MODBUS Proxy Debug ==="); 35 | client.println("Connected to telnet debug port\n"); 36 | } 37 | } 38 | } 39 | 40 | void print(const char* str) { 41 | if (client && client.connected()) { 42 | client.print(str); 43 | } 44 | } 45 | 46 | void println(const char* str) { 47 | if (client && client.connected()) { 48 | client.println(str); 49 | } 50 | } 51 | 52 | void println() { 53 | if (client && client.connected()) { 54 | client.println(); 55 | } 56 | } 57 | 58 | void printf(const char* format, ...) { 59 | if (client && client.connected()) { 60 | va_list args; 61 | va_start(args, format); 62 | vsnprintf(buffer, sizeof(buffer), format, args); 63 | va_end(args); 64 | client.print(buffer); 65 | } 66 | } 67 | }; 68 | 69 | extern TelnetDebug telnetDebug; 70 | 71 | #define DEBUG_PRINT(x) telnetDebug.print(x) 72 | #define DEBUG_PRINTLN(x) telnetDebug.println(x) 73 | #define DEBUG_PRINTF(...) telnetDebug.printf(__VA_ARGS__) 74 | #define DEBUG_HANDLE() telnetDebug.handle() 75 | #elif ENABLE_SERIAL_DEBUG 76 | #define DEBUG_PRINT(x) Serial.print(x) 77 | #define DEBUG_PRINTLN(x) Serial.println(x) 78 | #define DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__) 79 | #define DEBUG_HANDLE() 80 | #else 81 | #define DEBUG_PRINT(x) 82 | #define DEBUG_PRINTLN(x) 83 | #define DEBUG_PRINTF(...) 84 | #define DEBUG_HANDLE() 85 | #endif 86 | -------------------------------------------------------------------------------- /Modbus_Proxy/src/mqtt_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "config.h" 8 | #include "dtsu666.h" 9 | 10 | // MQTT MBUS/SENSOR data structure 11 | struct MQTTSensorData { 12 | String time; 13 | String smid; 14 | // Active power (kW) 15 | float pi, po; // Total import/export power 16 | float pi1, pi2, pi3; // Phase import power (W) 17 | float po1, po2, po3; // Phase export power (W) 18 | // Voltage (V) 19 | float u1, u2, u3; // Phase voltages 20 | // Current (A) 21 | float i1, i2, i3; // Phase currents 22 | // Frequency (Hz) 23 | float f; // Grid frequency 24 | // Energy (kWh) 25 | float ei, eo; // Total import/export energy 26 | float ei1, ei2; // Import energy tariff 1/2 27 | float eo1, eo2; // Export energy tariff 1/2 28 | // Reactive energy (kVArh) 29 | float q5, q6, q7, q8; // Quadrant reactive energy 30 | float q51, q52, q61, q62, q71, q72, q81, q82; // Tariff reactive energy 31 | }; 32 | 33 | // Thread-safe shared MQTT received data structure 34 | struct SharedMQTTReceivedData { 35 | SemaphoreHandle_t semaphore; // Semaphore for thread-safe access 36 | MQTTSensorData data; // The MQTT sensor data 37 | uint32_t timestamp; // When data was last updated 38 | bool dataValid; // Whether data is valid 39 | bool newPackageArrived; // Flag for new package arrival 40 | }; 41 | 42 | // MQTT data queue structure 43 | struct MQTTDataQueue { 44 | SemaphoreHandle_t mutex; 45 | static const size_t QUEUE_SIZE = 10; 46 | struct QueueItem { 47 | MQTTSensorData data; 48 | uint32_t timestamp; 49 | bool valid; 50 | } items[QUEUE_SIZE]; 51 | size_t head; 52 | size_t tail; 53 | size_t count; 54 | }; 55 | 56 | // System health structure 57 | struct SystemHealth { 58 | uint32_t uptime; 59 | uint32_t freeHeap; 60 | uint32_t minFreeHeap; 61 | uint32_t mqttReconnects; 62 | uint32_t dtsuUpdates; 63 | uint32_t evccUpdates; 64 | uint32_t evccErrors; 65 | uint32_t proxyErrors; 66 | float lastPowerCorrection; 67 | bool powerCorrectionActive; 68 | uint32_t lastHealthReport; 69 | }; 70 | 71 | // Function declarations 72 | bool initMQTT(); 73 | bool connectToMQTT(); 74 | void mqttTask(void *pvParameters); 75 | void onMqttMessage(char* topic, byte* payload, unsigned int length); 76 | 77 | // Data publishing functions 78 | bool publishDTSUData(const DTSU666Data& data); 79 | bool publishSystemHealth(const SystemHealth& health); 80 | bool publishPowerData(const DTSU666Data& dtsuData, float correction, bool correctionActive); 81 | void queueCorrectedPowerData(const DTSU666Data& finalData, const DTSU666Data& originalData, 82 | bool correctionApplied, float correction); 83 | 84 | // MQTT utilities 85 | bool mqttPublish(const char* topic, const char* payload, bool retained = false); 86 | bool mqttPublishJSON(const char* topic, const JsonDocument& doc, bool retained = false); 87 | void processMQTTQueue(); 88 | 89 | // Data conversion functions 90 | void convertDTSUToMQTT(const DTSU666Data& dtsu, MQTTSensorData& mqtt, const String& time, const String& smid); 91 | 92 | // Debug functions 93 | void debugMQTTData(const String& time, const String& smid, const DTSU666Data& data); 94 | 95 | // Global MQTT objects (declared extern to be defined in mqtt_handler.cpp) 96 | extern WiFiClient wifiClient; 97 | extern PubSubClient mqttClient; 98 | extern MQTTDataQueue mqttDataQueue; 99 | extern SystemHealth systemHealth; 100 | extern uint32_t mqttReconnectCount; -------------------------------------------------------------------------------- /Modbus_Proxy/src/dtsu666.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "config.h" 5 | #include "ModbusRTU485.h" 6 | 7 | // DTSU-666 parsed data structure — 40 FP32 values (80 registers: 2102–2181) 8 | struct DTSU666Data { 9 | // 2102–2106: Currents (A) 10 | float current_L1, current_L2, current_L3; 11 | 12 | // 2108–2114: Line-to-neutral voltages (V) 13 | float voltage_LN_avg, voltage_L1N, voltage_L2N, voltage_L3N; 14 | 15 | // 2116–2124: Line-to-line voltages (V) and frequency (Hz) 16 | float voltage_LL_avg, voltage_L1L2, voltage_L2L3, voltage_L3L1; 17 | float frequency; 18 | 19 | // 2126–2132: Active power (W) — inverted in meter to simulate production 20 | float power_total, power_L1, power_L2, power_L3; 21 | 22 | // 2134–2140: Reactive power (var) 23 | float reactive_total, reactive_L1, reactive_L2, reactive_L3; 24 | 25 | // 2142–2148: Apparent power (VA) 26 | float apparent_total, apparent_L1, apparent_L2, apparent_L3; 27 | 28 | // 2150–2156: Power factor (0..1) 29 | float pf_total, pf_L1, pf_L2, pf_L3; 30 | 31 | // 2158–2164: Active power demand (W) — inverted in meter 32 | float demand_total, demand_L1, demand_L2, demand_L3; 33 | 34 | // 2166–2172: Import energy (kWh) 35 | float import_total, import_L1, import_L2, import_L3; 36 | 37 | // 2174–2180: Export energy (kWh) 38 | float export_total, export_L1, export_L2, export_L3; 39 | }; 40 | 41 | // Optional: U_WORD metadata block (read via separate 0x03 requests) 42 | struct DTSU666Meta { 43 | uint16_t status = 0; // 2001 44 | uint16_t version = 0; // 2214 45 | uint16_t passcode = 0; // 2215 46 | uint16_t zero_clear_flag = 0; // 2216 47 | uint16_t connection_mode = 0; // 2217 48 | uint16_t irat = 0; // 2218 49 | uint16_t urat = 0; // 2219 50 | uint16_t protocol = 0; // 2220 51 | uint16_t address = 0; // 2221 52 | uint16_t baud = 0; // 2222 53 | uint16_t meter_type = 0; // 2223 54 | }; 55 | 56 | // Track last MODBUS request to associate replies for decoding 57 | struct LastRequestInfo { 58 | bool valid = false; 59 | uint8_t id = 0; 60 | uint8_t fc = 0; 61 | uint16_t startAddr = 0; 62 | uint16_t qty = 0; 63 | uint32_t ts = 0; 64 | }; 65 | 66 | // Thread-safe shared DTSU data structure for dual-task architecture 67 | struct SharedDTSUData { 68 | SemaphoreHandle_t mutex; // Mutex for thread-safe access 69 | bool valid; // Whether data is valid 70 | uint32_t timestamp; // When data was last updated (millis) 71 | uint8_t responseBuffer[165]; // Latest DTSU response with corrections applied 72 | uint16_t responseLength; // Length of response 73 | DTSU666Data parsedData; // Parsed data for power corrections 74 | uint32_t updateCount; // Number of successful updates 75 | }; 76 | 77 | // Function declarations 78 | bool parseDTSU666Data(uint16_t startAddr, const ModbusMessage& msg, DTSU666Data& data); 79 | bool parseDTSU666MetaWords(uint16_t startAddr, const ModbusMessage& msg, DTSU666Meta& meta); 80 | bool encodeDTSU666Response(const DTSU666Data& data, uint8_t* buffer, size_t bufferSize); 81 | bool applyPowerCorrection(uint8_t* raw, uint16_t len, float correction); 82 | bool parseDTSU666Response(const uint8_t* raw, uint16_t len, DTSU666Data& data); 83 | 84 | // Utility functions 85 | float parseFloat32(const uint8_t* data, size_t offset); 86 | void encodeFloat32(float value, uint8_t* data, size_t offset); 87 | int16_t parseInt16(const uint8_t* data, size_t offset); 88 | uint16_t parseUInt16(const uint8_t* data, size_t offset); 89 | 90 | // Debug functions 91 | void debugDTSUData(const DTSU666Data& data); 92 | void debugMQTTData(const String& time, const String& smid, const DTSU666Data& data); 93 | void printHexDump(const char* label, const uint8_t* buf, size_t len); 94 | 95 | // Constants 96 | extern const char* dtsuRegisterNames[40]; -------------------------------------------------------------------------------- /Modbus_Proxy/src/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Debug settings - Telnet wireless debugging for ESP32-C3 4 | #define ENABLE_SERIAL_DEBUG false 5 | #define ENABLE_TELNET_DEBUG false 6 | 7 | // ESP32-C3 Pin definitions - safe GPIOs for UART 8 | #define RS485_SUN2000_RX_PIN 7 9 | #define RS485_SUN2000_TX_PIN 10 10 | #define RS485_DTU_RX_PIN 1 11 | #define RS485_DTU_TX_PIN 0 12 | 13 | // Status LED pin (GPIO8, inverted logic - LOW=ON, HIGH=OFF) 14 | #define STATUS_LED_PIN 8 15 | #define LED_INVERTED true 16 | 17 | // LED control macros (handles inverted logic) 18 | #if LED_INVERTED 19 | #define LED_ON() digitalWrite(STATUS_LED_PIN, LOW) 20 | #define LED_OFF() digitalWrite(STATUS_LED_PIN, HIGH) 21 | #else 22 | #define LED_ON() digitalWrite(STATUS_LED_PIN, HIGH) 23 | #define LED_OFF() digitalWrite(STATUS_LED_PIN, LOW) 24 | #endif 25 | 26 | // MODBUS communication settings 27 | #define MODBUS_BAUDRATE 9600 28 | 29 | // DTSU-666 Register definitions 30 | #define DTSU_CURRENT_L1_REG 2102 31 | #define DTSU_CURRENT_L2_REG 2104 32 | #define DTSU_CURRENT_L3_REG 2106 33 | #define DTSU_VOLTAGE_LN_AVG_REG 2108 34 | #define DTSU_VOLTAGE_L1N_REG 2110 35 | #define DTSU_VOLTAGE_L2N_REG 2112 36 | #define DTSU_VOLTAGE_L3N_REG 2114 37 | #define DTSU_VOLTAGE_LL_AVG_REG 2116 38 | #define DTSU_VOLTAGE_L1L2_REG 2118 39 | #define DTSU_VOLTAGE_L2L3_REG 2120 40 | #define DTSU_VOLTAGE_L3L1_REG 2122 41 | #define DTSU_FREQUENCY_REG 2124 42 | #define DTSU_POWER_TOTAL_REG 2126 43 | #define DTSU_POWER_L1_REG 2128 44 | #define DTSU_POWER_L2_REG 2130 45 | #define DTSU_POWER_L3_REG 2132 46 | #define DTSU_REACTIVE_TOTAL_REG 2134 47 | #define DTSU_REACTIVE_L1_REG 2136 48 | #define DTSU_REACTIVE_L2_REG 2138 49 | #define DTSU_REACTIVE_L3_REG 2140 50 | #define DTSU_APPARENT_TOTAL_REG 2142 51 | #define DTSU_APPARENT_L1_REG 2144 52 | #define DTSU_APPARENT_L2_REG 2146 53 | #define DTSU_APPARENT_L3_REG 2148 54 | #define DTSU_PF_TOTAL_REG 2150 55 | #define DTSU_PF_L1_REG 2152 56 | #define DTSU_PF_L2_REG 2154 57 | #define DTSU_PF_L3_REG 2156 58 | #define DTSU_DEMAND_TOTAL_REG 2158 59 | #define DTSU_DEMAND_L1_REG 2160 60 | #define DTSU_DEMAND_L2_REG 2162 61 | #define DTSU_DEMAND_L3_REG 2164 62 | #define DTSU_IMPORT_TOTAL_REG 2166 63 | #define DTSU_IMPORT_L1_REG 2168 64 | #define DTSU_IMPORT_L2_REG 2170 65 | #define DTSU_IMPORT_L3_REG 2172 66 | #define DTSU_EXPORT_TOTAL_REG 2174 67 | #define DTSU_EXPORT_L1_REG 2176 68 | #define DTSU_EXPORT_L2_REG 2178 69 | #define DTSU_EXPORT_L3_REG 2180 70 | #define DTSU_VERSION_REG 2214 71 | 72 | // Additional U_WORD configuration/status registers 73 | #define DTSU_PASSCODE_REG 2215 74 | #define DTSU_ZERO_CLEAR_FLAG_REG 2216 75 | #define DTSU_CONNECTION_MODE_REG 2217 76 | #define DTSU_CT_RATIO_REG 2218 77 | #define DTSU_VT_RATIO_REG 2219 78 | #define DTSU_PROTOCOL_SWITCH_REG 2220 79 | #define DTSU_COMM_ADDRESS_REG 2221 80 | #define DTSU_BAUD_REG 2222 81 | #define DTSU_METER_TYPE_REG 2223 82 | 83 | // Power correction settings 84 | const float CORRECTION_THRESHOLD = 1000.0f; 85 | 86 | // EVCC API settings 87 | const uint32_t HTTP_POLL_INTERVAL = 10000; 88 | const uint32_t EVCC_DATA_MAX_AGE_MS = 10000; 89 | 90 | // Task timing constants 91 | const uint32_t WATCHDOG_TIMEOUT_MS = 60000; 92 | const uint32_t HEALTH_CHECK_INTERVAL = 5000; 93 | const uint32_t MQTT_PUBLISH_INTERVAL = 1000; 94 | 95 | // Memory thresholds 96 | const uint32_t MIN_FREE_HEAP = 20000; 97 | 98 | // MQTT settings 99 | const int mqttPort = 1883; 100 | 101 | // MQTT topics - hierarchical structure under MBUS-PROXY 102 | #define MQTT_TOPIC_ROOT "MBUS-PROXY" 103 | #define MQTT_TOPIC_POWER MQTT_TOPIC_ROOT "/power" 104 | #define MQTT_TOPIC_HEALTH MQTT_TOPIC_ROOT "/health" 105 | #define MQTT_TOPIC_STATUS MQTT_TOPIC_ROOT "/status" 106 | #define MQTT_TOPIC_DTSU MQTT_TOPIC_ROOT "/dtsu" 107 | #define MQTT_TOPIC_DEBUG MQTT_TOPIC_ROOT "/debug" -------------------------------------------------------------------------------- /Modbus_Proxy/src/evcc_api.cpp: -------------------------------------------------------------------------------- 1 | #include "evcc_api.h" 2 | #include "credentials.h" 3 | #include "config.h" 4 | #include "debug.h" 5 | 6 | // HTTP client mutex for thread safety 7 | SemaphoreHandle_t httpClientMutex = NULL; 8 | 9 | bool initEVCCAPI() { 10 | httpClientMutex = xSemaphoreCreateMutex(); 11 | if (httpClientMutex == NULL) { 12 | DEBUG_PRINTLN("❌ Failed to create HTTP client mutex"); 13 | return false; 14 | } 15 | DEBUG_PRINTLN("✅ HTTP client mutex created"); 16 | return true; 17 | } 18 | 19 | bool pollEvccApi(SharedEVCCData& sharedData) { 20 | // Protect HTTP client access with mutex 21 | if (xSemaphoreTake(httpClientMutex, pdMS_TO_TICKS(5000)) != pdTRUE) { 22 | DEBUG_PRINTLN("❌ Failed to acquire HTTP mutex"); 23 | return false; 24 | } 25 | 26 | HTTPClient http; 27 | http.begin(evccApiUrl); 28 | http.setTimeout(5000); 29 | http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); 30 | 31 | int httpResponseCode = http.GET(); 32 | 33 | if (httpResponseCode != HTTP_CODE_OK) { 34 | http.end(); 35 | xSemaphoreGive(httpClientMutex); // Release HTTP mutex 36 | 37 | if (xSemaphoreTake(sharedData.mutex, 10 / portTICK_PERIOD_MS) == pdTRUE) { 38 | sharedData.errorCount++; 39 | xSemaphoreGive(sharedData.mutex); 40 | } 41 | 42 | logAPIError("HTTP request failed", httpResponseCode); 43 | return false; 44 | } 45 | 46 | String payload = http.getString(); 47 | http.end(); 48 | xSemaphoreGive(httpClientMutex); // Release HTTP mutex after HTTP operations complete 49 | 50 | if (payload.isEmpty()) { 51 | if (xSemaphoreTake(sharedData.mutex, 10 / portTICK_PERIOD_MS) == pdTRUE) { 52 | sharedData.errorCount++; 53 | xSemaphoreGive(sharedData.mutex); 54 | } 55 | logAPIError("Empty response"); 56 | return false; 57 | } 58 | 59 | 60 | StaticJsonDocument<8192> doc; // Back to stack-based with larger task stack 61 | DeserializationError error = deserializeJson(doc, payload); 62 | 63 | if (error) { 64 | if (xSemaphoreTake(sharedData.mutex, 10 / portTICK_PERIOD_MS) == pdTRUE) { 65 | sharedData.errorCount++; 66 | xSemaphoreGive(sharedData.mutex); 67 | } 68 | DEBUG_PRINTF("❌ JSON parse error: %s\n", error.c_str()); 69 | logAPIError("JSON parsing failed: " + String(error.c_str())); 70 | return false; 71 | } 72 | 73 | 74 | if (!doc.containsKey("loadpoints") || !doc["loadpoints"].is() || 75 | doc["loadpoints"].size() == 0 || !doc["loadpoints"][0].containsKey("chargePower")) { 76 | if (xSemaphoreTake(sharedData.mutex, 10 / portTICK_PERIOD_MS) == pdTRUE) { 77 | sharedData.errorCount++; 78 | xSemaphoreGive(sharedData.mutex); 79 | } 80 | logAPIError("Missing loadpoints[0].chargePower field"); 81 | return false; 82 | } 83 | 84 | float chargePower = doc["loadpoints"][0]["chargePower"].as(); 85 | 86 | if (xSemaphoreTake(sharedData.mutex, 10 / portTICK_PERIOD_MS) == pdTRUE) { 87 | sharedData.chargePower = chargePower; 88 | sharedData.timestamp = millis(); 89 | sharedData.valid = true; 90 | sharedData.updateCount++; 91 | xSemaphoreGive(sharedData.mutex); 92 | return true; 93 | } 94 | 95 | return false; 96 | } 97 | 98 | float calculatePowerCorrection(const SharedEVCCData& evccData) { 99 | if (!isEVCCDataValid(evccData)) { 100 | return 0.0f; 101 | } 102 | 103 | float wallboxPower = 0.0f; 104 | bool valid = false; 105 | getEVCCData(evccData, wallboxPower, valid); 106 | 107 | if (!valid || fabs(wallboxPower) <= CORRECTION_THRESHOLD) { 108 | return 0.0f; 109 | } 110 | 111 | return wallboxPower; 112 | } 113 | 114 | bool isEVCCDataValid(const SharedEVCCData& evccData) { 115 | if (xSemaphoreTake(evccData.mutex, 5 / portTICK_PERIOD_MS) == pdTRUE) { 116 | bool valid = evccData.valid && (millis() - evccData.timestamp) <= EVCC_DATA_MAX_AGE_MS; 117 | xSemaphoreGive(evccData.mutex); 118 | return valid; 119 | } 120 | return false; 121 | } 122 | 123 | void getEVCCData(const SharedEVCCData& sharedData, float& chargePower, bool& valid) { 124 | chargePower = 0.0f; 125 | valid = false; 126 | 127 | if (xSemaphoreTake(sharedData.mutex, 5 / portTICK_PERIOD_MS) == pdTRUE) { 128 | if (sharedData.valid && (millis() - sharedData.timestamp) <= EVCC_DATA_MAX_AGE_MS) { 129 | chargePower = sharedData.chargePower; 130 | valid = true; 131 | } 132 | xSemaphoreGive(sharedData.mutex); 133 | } 134 | } 135 | 136 | bool httpGetJSON(const char* url, JsonDocument& doc) { 137 | HTTPClient http; 138 | http.begin(url); 139 | http.setTimeout(5000); 140 | 141 | int httpResponseCode = http.GET(); 142 | if (httpResponseCode != HTTP_CODE_OK) { 143 | http.end(); 144 | return false; 145 | } 146 | 147 | String payload = http.getString(); 148 | http.end(); 149 | 150 | if (payload.isEmpty()) { 151 | return false; 152 | } 153 | 154 | DeserializationError error = deserializeJson(doc, payload); 155 | return error == DeserializationError::Ok; 156 | } 157 | 158 | void logAPIError(const String& error, int httpCode) { 159 | if (httpCode != 0) { 160 | DEBUG_PRINTF("❌ EVCC API Error: %s (HTTP %d)\n", error.c_str(), httpCode); 161 | } else { 162 | DEBUG_PRINTF("❌ EVCC API Error: %s\n", error.c_str()); 163 | } 164 | } -------------------------------------------------------------------------------- /Modbus_Proxy/src/ModbusRTU485.cpp: -------------------------------------------------------------------------------- 1 | #include "ModbusRTU485.h" 2 | 3 | void ModbusRTU485::begin(HardwareSerial& port, uint32_t baud) { 4 | _ser = &port; 5 | _baud = baud ? baud : 9600; 6 | 7 | // conservative: 11 bits per 8N1 char (accounts for small jitter) 8 | _tChar = (11UL * 1000000UL) / _baud; 9 | if (_tChar == 0) _tChar = 1000; // guard 10 | 11 | _t3_5 = (uint32_t)(3.5 * _tChar) + 2; // + tiny guard 12 | _t1_5 = (uint32_t)(1.5 * _tChar) + 2; 13 | } 14 | 15 | uint16_t ModbusRTU485::crc16(const uint8_t* p, size_t n) { 16 | uint16_t crc = 0xFFFF; 17 | for (size_t i = 0; i < n; ++i) { 18 | crc ^= p[i]; 19 | for (uint8_t b = 0; b < 8; ++b) { 20 | if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; 21 | else crc >>= 1; 22 | } 23 | } 24 | return crc; 25 | } 26 | 27 | bool ModbusRTU485::parse(const uint8_t* f, uint16_t n, ModbusMessage& m) { 28 | m = ModbusMessage{}; 29 | if (n < 4) return false; 30 | 31 | // CRC check 32 | uint16_t given = (uint16_t)f[n-2] | ((uint16_t)f[n-1] << 8); 33 | uint16_t calc = crc16(f, n - 2); 34 | if (calc != given) return false; 35 | 36 | m.valid = true; 37 | m.id = f[0]; 38 | m.fc = f[1]; 39 | m.len = n; 40 | 41 | // Exception? 42 | if ((m.fc & 0x80) && n >= 5) { 43 | m.type = MBType::Exception; 44 | m.fc &= 0x7F; 45 | m.exCode = f[2]; 46 | return true; 47 | } 48 | 49 | switch (m.fc) { 50 | case 0x03: // Read Holding 51 | case 0x04: // Read Input 52 | // Either request (8 bytes total) or reply (3 + byteCount + 2 CRC) 53 | if (n == 8) { 54 | m.type = MBType::Request; 55 | m.startAddr = be16(&f[2]); 56 | m.qty = be16(&f[4]); 57 | } else { 58 | uint8_t bc = f[2]; 59 | if (n == (uint16_t)bc + 5) { 60 | m.type = MBType::Reply; 61 | m.byteCount = bc; 62 | } else { 63 | m.type = MBType::Unknown; 64 | } 65 | } 66 | break; 67 | 68 | case 0x06: // Write Single Register 69 | // req/rep have same format (8 bytes) 70 | if (n == 8) { 71 | m.type = MBType::Request; // or Reply 72 | m.wrAddr = be16(&f[2]); 73 | m.wrValue= be16(&f[4]); 74 | } else { 75 | m.type = MBType::Unknown; 76 | } 77 | break; 78 | 79 | case 0x10: // Write Multiple Registers 80 | // Request: ID FC AddrHi AddrLo QtyHi QtyLo ByteCount Data... CRC 81 | // Reply: ID FC AddrHi AddrLo QtyHi QtyLo CRC 82 | if (n == 8) { 83 | m.type = MBType::Reply; 84 | m.wrAddr= be16(&f[2]); 85 | m.wrQty = be16(&f[4]); 86 | } else if (n >= 9) { 87 | uint8_t bc = f[6]; 88 | if (n == (uint16_t)bc + 9) { 89 | m.type = MBType::Request; 90 | m.wrAddr = be16(&f[2]); 91 | m.wrQty = be16(&f[4]); 92 | m.wrByteCount = bc; 93 | } else { 94 | m.type = MBType::Unknown; 95 | } 96 | } else { 97 | m.type = MBType::Unknown; 98 | } 99 | break; 100 | 101 | default: 102 | m.type = MBType::Unknown; 103 | break; 104 | } 105 | 106 | return true; 107 | } 108 | 109 | bool ModbusRTU485::read(ModbusMessage& out, uint32_t timeoutMs) { 110 | if (!_ser) return false; 111 | 112 | uint32_t start = millis(); 113 | _len = 0; 114 | 115 | // Wait for first byte (within timeout) 116 | while (_ser->available() == 0) { 117 | if (timeoutMs && (millis() - start >= timeoutMs)) return false; 118 | delay(1); 119 | } 120 | 121 | // Consume a frame until inter-char gap >= 3.5T 122 | uint32_t lastUs = micros(); 123 | while (true) { 124 | // Check overall timeout to prevent infinite loop 125 | if (timeoutMs && (millis() - start >= timeoutMs)) return false; 126 | 127 | while (_ser->available()) { 128 | int b = _ser->read(); 129 | if (b < 0) break; 130 | if (_len < BUF_SIZE) _buf[_len++] = (uint8_t)b; 131 | lastUs = micros(); 132 | } 133 | // Check inter-char gap 134 | if ((micros() - lastUs) >= _t3_5) break; 135 | // small idle 136 | delayMicroseconds(50); 137 | } 138 | 139 | // Try parse (and attach raw pointer) 140 | if (!parse(_buf, (uint16_t)_len, out)) return false; 141 | out.raw = _buf; // expose raw frame to callers (valid until next read) 142 | return true; 143 | } 144 | 145 | bool ModbusRTU485::write(const ModbusMessage& msg, uint32_t timeoutMs) { 146 | if (!_ser || !msg.valid || !msg.raw) return false; 147 | 148 | // Ensure proper inter-frame gap before transmission (3.5T) 149 | delayMicroseconds(_t3_5); 150 | 151 | // Write the raw frame directly 152 | size_t written = _ser->write(msg.raw, msg.len); 153 | 154 | // Verify all bytes were written 155 | if (written != msg.len) return false; 156 | 157 | // Wait for transmission to complete 158 | _ser->flush(); 159 | 160 | return true; 161 | } 162 | 163 | bool ModbusRTU485::write(const uint8_t* data, size_t len, uint32_t timeoutMs) { 164 | if (!_ser || !data || len < 2) return false; 165 | 166 | // Ensure proper inter-frame gap before transmission (3.5T) 167 | delayMicroseconds(_t3_5); 168 | 169 | // Calculate and append CRC if not already present 170 | uint8_t frame[256]; // Buffer for frame with CRC 171 | if (len > 254) return false; // Frame too large 172 | 173 | // Copy data to frame buffer 174 | memcpy(frame, data, len); 175 | 176 | // Calculate CRC for the data portion 177 | uint16_t crc = crc16(data, len); 178 | 179 | // Append CRC (little-endian format for MODBUS) 180 | frame[len] = crc & 0xFF; // CRC low byte 181 | frame[len + 1] = (crc >> 8) & 0xFF; // CRC high byte 182 | 183 | // Write complete frame with CRC 184 | size_t totalLen = len + 2; 185 | size_t written = _ser->write(frame, totalLen); 186 | 187 | // Verify all bytes were written 188 | if (written != totalLen) return false; 189 | 190 | // Wait for transmission to complete 191 | _ser->flush(); 192 | 193 | return true; 194 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔌 ESP32 MODBUS RTU Intelligent Proxy 2 | 3 | [![Platform: ESP32-C3](https://img.shields.io/badge/Platform-ESP32--C3-blue.svg)](https://github.com/SensorsIot/Modbus_Proxy/tree/main) 4 | [![Platform: ESP32-S3](https://img.shields.io/badge/Platform-ESP32--S3-green.svg)](https://github.com/SensorsIot/Modbus_Proxy/tree/S3) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 6 | [![PlatformIO](https://img.shields.io/badge/PlatformIO-Ready-orange.svg)](https://platformio.org) 7 | 8 | > **Intelligent power monitoring and correction system for solar installations with EV charging** 9 | 10 | A sophisticated MODBUS RTU proxy that sits between a SUN2000 solar inverter and DTSU-666 energy meter, providing real-time power correction by integrating wallbox charging data from an EVCC system. 11 | 12 | --- 13 | 14 | ## 📋 Table of Contents 15 | 16 | - [Features](#-features) 17 | - [Hardware Platforms](#-hardware-platforms) 18 | - [Quick Start](#-quick-start) 19 | - [System Architecture](#-system-architecture) 20 | - [Configuration](#-configuration) 21 | - [MQTT Topics](#-mqtt-topics) 22 | - [Development](#-development) 23 | - [Documentation](#-documentation) 24 | - [License](#-license) 25 | 26 | --- 27 | 28 | ## ✨ Features 29 | 30 | - 🔄 **Transparent MODBUS Proxy**: Seamless communication between SUN2000 inverter and DTSU-666 meter 31 | - ⚡ **Intelligent Power Correction**: Real-time correction for wallbox charging (EVCC integration) 32 | - 📊 **MQTT Publishing**: Live power data and system health monitoring 33 | - 🛡️ **Watchdog Monitoring**: Automatic fault detection and recovery 34 | - 🔧 **Dual Platform Support**: ESP32-C3 (single-core) and ESP32-S3 (dual-core) 35 | - 📡 **OTA Updates**: Wireless firmware updates 36 | - 🐛 **Debug Options**: USB Serial and Telnet wireless debugging 37 | 38 | --- 39 | 40 | ## 🔧 Hardware Platforms 41 | 42 | ### 📱 ESP32-C3 (Branch: `main`) 43 | ![ESP32-C3](https://img.shields.io/badge/Core-Single--Core%20RISC--V-blue) 44 | ![Speed](https://img.shields.io/badge/Speed-160MHz-blue) 45 | ![RAM](https://img.shields.io/badge/RAM-320KB-blue) 46 | 47 | - **Board**: ESP32-C3-DevKitM-1 48 | - **Architecture**: Single-core RISC-V @ 160MHz 49 | - **UARTs**: 2 available (UART0/UART1 for RS485) 50 | - **LED**: GPIO 8 (inverted logic) 51 | - **Debug**: Telnet only (USB CDC disabled) 52 | 53 | **Pin Configuration**: 54 | ``` 55 | SUN2000: UART0 (RX=GPIO7, TX=GPIO10) 56 | DTSU-666: UART1 (RX=GPIO1, TX=GPIO0) 57 | LED: GPIO8 (LOW=ON, HIGH=OFF) 58 | ``` 59 | 60 | ### 🚀 ESP32-S3 (Branch: `S3`) 61 | ![ESP32-S3](https://img.shields.io/badge/Core-Dual--Core%20Xtensa-green) 62 | ![Speed](https://img.shields.io/badge/Speed-240MHz-green) 63 | ![RAM](https://img.shields.io/badge/RAM-320KB-green) 64 | 65 | - **Board**: Lolin S3 Mini 66 | - **Architecture**: Dual-core Xtensa @ 240MHz 67 | - **UARTs**: 3 available (UART0 for USB, UART1/UART2 for RS485) 68 | - **LED**: GPIO 48 (normal logic) 69 | - **Debug**: USB Serial or Telnet 70 | 71 | **Pin Configuration**: 72 | ``` 73 | SUN2000: UART1 (RX=GPIO18, TX=GPIO17) 74 | DTSU-666: UART2 (RX=GPIO16, TX=GPIO15) 75 | LED: GPIO48 (HIGH=ON, LOW=OFF) 76 | ``` 77 | 78 | **Core Distribution**: 79 | - **Core 0**: MQTT publishing, EVCC API polling, Watchdog 80 | - **Core 1**: MODBUS proxy with power correction (dedicated) 81 | 82 | --- 83 | 84 | ## 🚀 Quick Start 85 | 86 | ### Prerequisites 87 | 88 | - [PlatformIO](https://platformio.org/) installed (VSCode extension or CLI) 89 | - ESP32-C3 or ESP32-S3 development board 90 | - Two RS485 adapters 91 | - WiFi network access 92 | 93 | ### Installation 94 | 95 | 1. **Clone the repository**: 96 | ```bash 97 | git clone https://github.com/SensorsIot/Modbus_Proxy.git 98 | cd Modbus_Proxy 99 | ``` 100 | 101 | 2. **Choose your platform**: 102 | ```bash 103 | # For ESP32-C3 (main branch) 104 | git checkout main 105 | 106 | # For ESP32-S3 (S3 branch) 107 | git checkout S3 108 | ``` 109 | 110 | 3. **Configure credentials**: 111 | ```bash 112 | cd Modbus_Proxy/src 113 | cp credentials.h.example credentials.h 114 | # Edit credentials.h with your WiFi and MQTT settings 115 | ``` 116 | 117 | 4. **Build and upload**: 118 | ```bash 119 | cd Modbus_Proxy 120 | 121 | # Serial upload (first time) 122 | pio run -e esp32-c3-serial --target upload # For ESP32-C3 123 | pio run -e esp32-s3-serial --target upload # For ESP32-S3 124 | 125 | # OTA upload (after initial flash) 126 | pio run -e esp32-c3-ota --target upload # For ESP32-C3 127 | pio run -e esp32-s3-ota --target upload # For ESP32-S3 128 | ``` 129 | 130 | 5. **Monitor output**: 131 | ```bash 132 | pio device monitor # USB Serial 133 | telnet 23 # Telnet (if enabled) 134 | ``` 135 | 136 | --- 137 | 138 | ## 🏗️ System Architecture 139 | 140 | ``` 141 | ┌─────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ 142 | │ Grid │────▶│ Wallbox │────▶│ DTSU-666 │────▶│ SUN2000 │ 143 | │ (L&G) │ │ (4.2kW) │ │ (Meter) │ │ Inverter │ 144 | └─────────────┘ └─────┬────┘ └────┬─────┘ └─────────────┘ 145 | │ │ 146 | │ ┌───▼────┐ 147 | │ │ ESP32 │ 148 | │ │ Proxy │ 149 | │ └───┬────┘ 150 | │ │ 151 | │ WiFi/MQTT 152 | │ │ 153 | ┌─────▼────────────────▼─────┐ 154 | │ EVCC System │ 155 | │ (EV Charging Controller) │ 156 | └────────────────────────────┘ 157 | ``` 158 | 159 | ### Power Flow 160 | 161 | 1. **DTSU-666** measures grid connection power 162 | 2. **ESP32 Proxy** reads MODBUS data from DTSU-666 163 | 3. **EVCC API** provides wallbox charging power 164 | 4. **Power Correction** adds wallbox power to DTSU reading 165 | 5. **SUN2000** receives corrected power values 166 | 167 | --- 168 | 169 | ## ⚙️ Configuration 170 | 171 | ### credentials.h 172 | 173 | ```cpp 174 | static const char* ssid = "YOUR_WIFI_SSID"; 175 | static const char* password = "YOUR_WIFI_PASSWORD"; 176 | static const char* mqttServer = "192.168.0.203"; 177 | static const char* evccApiUrl = "http://192.168.0.202:7070/api/state"; 178 | ``` 179 | 180 | ### config.h (Platform-Specific) 181 | 182 | **Debug Settings**: 183 | ```cpp 184 | #define ENABLE_SERIAL_DEBUG true // USB serial (ESP32-S3) 185 | #define ENABLE_TELNET_DEBUG false // Wireless telnet 186 | ``` 187 | 188 | **Key Parameters**: 189 | - `CORRECTION_THRESHOLD`: 1000W (minimum wallbox power for correction) 190 | - `HTTP_POLL_INTERVAL`: 10000ms (EVCC API polling) 191 | - `WATCHDOG_TIMEOUT_MS`: 60000ms (task heartbeat timeout) 192 | 193 | --- 194 | 195 | ## 📡 MQTT Topics 196 | 197 | ### MBUS-PROXY/power 198 | Published every MODBUS transaction (~1/second) 199 | 200 | ```json 201 | { 202 | "dtsu": 94.1, // DTSU-666 reading (W) 203 | "wallbox": 1840.0, // Wallbox power (W) 204 | "sun2000": 1934.1, // Corrected value to SUN2000 (W) 205 | "active": true // Correction applied 206 | } 207 | ``` 208 | 209 | ### MBUS-PROXY/health 210 | Published every 60 seconds 211 | 212 | ```json 213 | { 214 | "timestamp": 123456, 215 | "uptime": 123456, 216 | "free_heap": 250000, 217 | "min_free_heap": 200000, 218 | "mqtt_reconnects": 0, 219 | "dtsu_updates": 1234, 220 | "evcc_updates": 123, 221 | "evcc_errors": 0, 222 | "proxy_errors": 0, 223 | "power_correction": 1840.0, 224 | "correction_active": true 225 | } 226 | ``` 227 | 228 | --- 229 | 230 | ## 🛠️ Development 231 | 232 | ### Building 233 | 234 | ```bash 235 | cd Modbus_Proxy 236 | 237 | # Build only (no upload) 238 | pio run -e esp32-c3-serial # ESP32-C3 239 | pio run -e esp32-s3-serial # ESP32-S3 240 | 241 | # Clean build 242 | pio run --target clean 243 | ``` 244 | 245 | ### Debugging 246 | 247 | **USB Serial** (ESP32-S3 only): 248 | ```bash 249 | pio device monitor -b 115200 250 | ``` 251 | 252 | **Telnet** (both platforms): 253 | ```bash 254 | telnet 23 255 | ``` 256 | 257 | ### OTA Password 258 | 259 | Default OTA password: `modbus_ota_2023` 260 | 261 | --- 262 | 263 | ## 📚 Documentation 264 | 265 | - **[Modbus-Proxy-FSD.md](Modbus_Proxy/Modbus-Proxy-FSD.md)**: Complete Functional Specification Document (v3.0) 266 | 267 | --- 268 | 269 | ## 🔬 Technical Details 270 | 271 | ### MODBUS Configuration 272 | - **Baud Rate**: 9600, 8N1 273 | - **Slave ID**: 11 (DTSU-666) 274 | - **Function Codes**: 0x03, 0x04 275 | - **Register Range**: 2102-2181 (80 registers, IEEE 754 floats) 276 | 277 | ### Memory Usage 278 | 279 | | Platform | RAM | Flash | 280 | |----------|-----|-------| 281 | | ESP32-C3 | 14.8% (48KB) | 74.5% (977KB) | 282 | | ESP32-S3 | 16.5% (54KB) | 72.3% (947KB) | 283 | 284 | ### Task Priorities 285 | 286 | | Task | Priority | Stack | 287 | |------|----------|-------| 288 | | Watchdog | 3 (highest) | 2KB | 289 | | Proxy | 2 | 4KB | 290 | | MQTT | 1 (lowest) | 16KB | 291 | 292 | --- 293 | 294 | ## 🤝 Contributing 295 | 296 | Contributions welcome! Please read the FSD document first to understand the system architecture. 297 | 298 | 1. Fork the repository 299 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 300 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 301 | 4. Push to the branch (`git push origin feature/amazing-feature`) 302 | 5. Open a Pull Request 303 | 304 | --- 305 | 306 | ## 📝 License 307 | 308 | This project is licensed under the MIT License - see the LICENSE file for details. 309 | 310 | --- 311 | 312 | ## 👨‍💻 Author 313 | 314 | **Andreas Spiess** / Claude Code 315 | 316 | - YouTube: [@AndreasSpiess](https://www.youtube.com/AndreasSpiess) 317 | - GitHub: [@SensorsIot](https://github.com/SensorsIot) 318 | 319 | --- 320 | 321 | ## 🙏 Acknowledgments 322 | 323 | - ESP32 Arduino Core team 324 | - PlatformIO team 325 | - EVCC project 326 | - MODBUS community 327 | 328 | --- 329 | 330 | ## 📞 Support 331 | 332 | - 📺 YouTube videos on Andreas Spiess channel 333 | - 🐛 [GitHub Issues](https://github.com/SensorsIot/Modbus_Proxy/issues) 334 | - 💬 Community discussions 335 | 336 | --- 337 | 338 |
339 | 340 | **Made with ❤️ for the solar and EV charging community** 341 | 342 | [![YouTube](https://img.shields.io/badge/YouTube-Andreas%20Spiess-red?style=for-the-badge&logo=youtube)](https://www.youtube.com/AndreasSpiess) 343 | [![GitHub](https://img.shields.io/badge/GitHub-SensorsIot-black?style=for-the-badge&logo=github)](https://github.com/SensorsIot) 344 | 345 |
346 | -------------------------------------------------------------------------------- /Modbus_Proxy/src/mqtt_handler.cpp: -------------------------------------------------------------------------------- 1 | #include "mqtt_handler.h" 2 | #include "credentials.h" 3 | #include "debug.h" 4 | #include "evcc_api.h" 5 | #include "modbus_proxy.h" 6 | 7 | // Global MQTT objects 8 | WiFiClient wifiClient; 9 | PubSubClient mqttClient(wifiClient); 10 | MQTTDataQueue mqttDataQueue = {NULL, {}, 0, 0, 0}; 11 | SystemHealth systemHealth = {0}; 12 | uint32_t mqttReconnectCount = 0; 13 | 14 | // MQTT publish queue 15 | QueueHandle_t mqttPublishQueue; 16 | 17 | struct MQTTPublishItem { 18 | DTSU666Data correctedData; 19 | DTSU666Data originalData; 20 | bool correctionApplied; 21 | float correctionValue; 22 | uint32_t timestamp; 23 | }; 24 | 25 | bool initMQTT() { 26 | DEBUG_PRINTF("🔗 Setting up MQTT connection to %s:%d\n", mqttServer, mqttPort); 27 | 28 | mqttClient.setServer(mqttServer, mqttPort); 29 | mqttClient.setBufferSize(1024); 30 | mqttClient.setKeepAlive(60); 31 | mqttClient.setSocketTimeout(15); 32 | mqttClient.setCallback(onMqttMessage); 33 | 34 | mqttPublishQueue = xQueueCreate(10, sizeof(MQTTPublishItem)); 35 | if (mqttPublishQueue == NULL) { 36 | DEBUG_PRINTLN("❌ Failed to create MQTT publish queue"); 37 | return false; 38 | } 39 | 40 | mqttDataQueue.mutex = xSemaphoreCreateMutex(); 41 | if (mqttDataQueue.mutex == NULL) { 42 | DEBUG_PRINTLN("❌ Failed to create MQTT data queue mutex"); 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | 49 | bool connectToMQTT() { 50 | if (mqttClient.connected()) { 51 | return true; 52 | } 53 | 54 | mqttReconnectCount++; 55 | systemHealth.mqttReconnects = mqttReconnectCount; 56 | 57 | DEBUG_PRINTF("🔌 MQTT reconnection attempt #%lu...", mqttReconnectCount); 58 | 59 | String clientId = "MBUS_PROXY_" + WiFi.macAddress(); 60 | clientId.replace(":", ""); 61 | if (mqttClient.connect(clientId.c_str(), NULL, NULL, NULL, 0, false, NULL, true)) { 62 | DEBUG_PRINTLN(" ✅ CONNECTED!"); 63 | return true; 64 | } else { 65 | DEBUG_PRINTF(" ❌ FAILED (state=%d)\n", mqttClient.state()); 66 | return false; 67 | } 68 | } 69 | 70 | void mqttTask(void *pvParameters) { 71 | (void)pvParameters; 72 | uint32_t lastReportTime = 0; 73 | uint32_t lastDebugTime = 0; 74 | uint32_t lastEvccPoll = 0; 75 | uint32_t loopCount = 0; 76 | 77 | DEBUG_PRINTLN("📡 MQTT TASK STARTED"); 78 | DEBUG_PRINTF("🧠 Task Info: Core=%d, Priority=%d\n", xPortGetCoreID(), uxTaskPriorityGet(NULL)); 79 | 80 | while (1) { 81 | uint32_t loopStart = millis(); 82 | loopCount++; 83 | 84 | // Debug output every 10 seconds 85 | if (millis() - lastDebugTime > 10000) { 86 | uint32_t freeHeap = ESP.getFreeHeap(); 87 | uint32_t minFreeHeap = ESP.getMinFreeHeap(); 88 | DEBUG_PRINTF("🐛 MQTT Task Debug: Loop #%lu, Connected=%s, Heap=%u (min=%u)\n", 89 | loopCount, mqttClient.connected() ? "YES" : "NO", freeHeap, minFreeHeap); 90 | lastDebugTime = millis(); 91 | } 92 | 93 | // Update heartbeat for watchdog 94 | updateTaskHealthbeat(false); 95 | systemHealth.lastHealthReport = millis(); 96 | 97 | // MQTT connection handling 98 | if (!mqttClient.connected()) { 99 | DEBUG_PRINTF("🔄 MQTT reconnecting... (state=%d)\n", mqttClient.state()); 100 | if (!connectToMQTT()) { 101 | DEBUG_PRINTLN("⚠️ MQTT connection failed, continuing anyway"); 102 | } 103 | } 104 | 105 | // Process MQTT loop 106 | uint32_t loopStartTime = millis(); 107 | mqttClient.loop(); 108 | uint32_t loopDuration = millis() - loopStartTime; 109 | if (loopDuration > 1000) { 110 | DEBUG_PRINTF("⚠️ MQTT loop took %lu ms\n", loopDuration); 111 | } 112 | 113 | // Process MQTT queue 114 | uint32_t queueStartTime = millis(); 115 | processMQTTQueue(); 116 | uint32_t queueDuration = millis() - queueStartTime; 117 | if (queueDuration > 1000) { 118 | DEBUG_PRINTF("⚠️ MQTT queue processing took %lu ms\n", queueDuration); 119 | } 120 | 121 | // EVCC API polling 122 | if (millis() - lastEvccPoll > HTTP_POLL_INTERVAL) { 123 | DEBUG_PRINTLN("🌐 Polling EVCC API..."); 124 | uint32_t pollStart = millis(); 125 | bool success = pollEvccApi(sharedEVCC); 126 | uint32_t pollDuration = millis() - pollStart; 127 | 128 | DEBUG_PRINTF("🌐 EVCC API poll %s in %lu ms\n", 129 | success ? "SUCCESS" : "FAILED", pollDuration); 130 | 131 | if (pollDuration > 5000) { 132 | DEBUG_PRINTF("⚠️ EVCC API poll took %lu ms (blocking!)\n", pollDuration); 133 | } 134 | 135 | lastEvccPoll = millis(); 136 | } 137 | 138 | // System health reporting 139 | if (millis() - lastReportTime > 60000) { 140 | lastReportTime = millis(); 141 | DEBUG_PRINTLN("📊 Publishing system health..."); 142 | publishSystemHealth(systemHealth); 143 | } 144 | 145 | uint32_t totalLoopTime = millis() - loopStart; 146 | if (totalLoopTime > 5000) { 147 | DEBUG_PRINTF("⚠️ MQTT task loop took %lu ms (too long!)\n", totalLoopTime); 148 | } 149 | 150 | vTaskDelay(pdMS_TO_TICKS(100)); 151 | } 152 | } 153 | 154 | void onMqttMessage(char* topic, byte* payload, unsigned int length) { 155 | // Currently not subscribing to any MQTT topics 156 | // All data comes from EVCC HTTP API 157 | } 158 | 159 | bool publishDTSUData(const DTSU666Data& data) { 160 | if (!mqttClient.connected()) { 161 | return false; 162 | } 163 | 164 | StaticJsonDocument<1024> doc; 165 | doc["timestamp"] = millis(); 166 | doc["device"] = "ESP32_ModbusProxy"; 167 | doc["source"] = "DTSU-666"; 168 | 169 | doc["power_total"] = data.power_total; 170 | doc["power_L1"] = data.power_L1; 171 | doc["power_L2"] = data.power_L2; 172 | doc["power_L3"] = data.power_L3; 173 | 174 | doc["voltage_L1N"] = data.voltage_L1N; 175 | doc["voltage_L2N"] = data.voltage_L2N; 176 | doc["voltage_L3N"] = data.voltage_L3N; 177 | 178 | doc["current_L1"] = data.current_L1; 179 | doc["current_L2"] = data.current_L2; 180 | doc["current_L3"] = data.current_L3; 181 | 182 | doc["frequency"] = data.frequency; 183 | 184 | return mqttPublishJSON(MQTT_TOPIC_DTSU, doc); 185 | } 186 | 187 | bool publishSystemHealth(const SystemHealth& health) { 188 | if (!mqttClient.connected()) { 189 | return false; 190 | } 191 | 192 | StaticJsonDocument<512> doc; 193 | doc["timestamp"] = millis(); 194 | doc["uptime"] = health.uptime; 195 | doc["free_heap"] = health.freeHeap; 196 | doc["min_free_heap"] = health.minFreeHeap; 197 | doc["mqtt_reconnects"] = health.mqttReconnects; 198 | doc["dtsu_updates"] = health.dtsuUpdates; 199 | doc["evcc_updates"] = health.evccUpdates; 200 | doc["evcc_errors"] = health.evccErrors; 201 | doc["proxy_errors"] = health.proxyErrors; 202 | doc["power_correction"] = health.lastPowerCorrection; 203 | doc["correction_active"] = health.powerCorrectionActive; 204 | 205 | return mqttPublishJSON(MQTT_TOPIC_HEALTH, doc); 206 | } 207 | 208 | bool publishPowerData(const DTSU666Data& dtsuData, float correction, bool correctionActive) { 209 | if (!mqttClient.connected()) { 210 | return false; 211 | } 212 | 213 | StaticJsonDocument<512> doc; 214 | doc["timestamp"] = millis(); 215 | doc["dtsu_power"] = dtsuData.power_total; 216 | doc["correction"] = correction; 217 | doc["correction_active"] = correctionActive; 218 | doc["sun2000_power"] = dtsuData.power_total + (correctionActive ? correction : 0.0f); 219 | 220 | return mqttPublishJSON(MQTT_TOPIC_STATUS, doc); 221 | } 222 | 223 | void queueCorrectedPowerData(const DTSU666Data& finalData, const DTSU666Data& originalData, 224 | bool correctionApplied, float correction) { 225 | MQTTPublishItem item; 226 | item.correctedData = finalData; 227 | item.originalData = originalData; 228 | item.correctionApplied = correctionApplied; 229 | item.correctionValue = correction; 230 | item.timestamp = millis(); 231 | 232 | if (xQueueSend(mqttPublishQueue, &item, 0) != pdTRUE) { 233 | DEBUG_PRINTLN("⚠️ MQTT publish queue full, dropping data"); 234 | } 235 | } 236 | 237 | bool mqttPublish(const char* topic, const char* payload, bool retained) { 238 | if (!mqttClient.connected()) { 239 | return false; 240 | } 241 | 242 | return mqttClient.publish(topic, payload, retained); 243 | } 244 | 245 | bool mqttPublishJSON(const char* topic, const JsonDocument& doc, bool retained) { 246 | String jsonString; 247 | serializeJson(doc, jsonString); 248 | return mqttPublish(topic, jsonString.c_str(), retained); 249 | } 250 | 251 | void processMQTTQueue() { 252 | MQTTPublishItem item; 253 | if (xQueueReceive(mqttPublishQueue, &item, 10 / portTICK_PERIOD_MS) == pdTRUE) { 254 | if (mqttClient.connected()) { 255 | StaticJsonDocument<256> doc; 256 | 257 | doc["dtsu"] = item.originalData.power_total; 258 | doc["wallbox"] = item.correctionValue; 259 | doc["sun2000"] = item.correctedData.power_total; 260 | doc["active"] = item.correctionApplied; 261 | 262 | bool success = mqttPublishJSON(MQTT_TOPIC_POWER, doc); 263 | // Only report MQTT failures, not successes 264 | if (!success) { 265 | DEBUG_PRINTF("❌ MQTT publish FAILED (state=%d)\n", mqttClient.state()); 266 | } 267 | } else { 268 | DEBUG_PRINTLN("⚠️ MQTT not connected, dropping queued data"); 269 | } 270 | } 271 | } 272 | 273 | void convertDTSUToMQTT(const DTSU666Data& dtsu, MQTTSensorData& mqtt, const String& time, const String& smid) { 274 | mqtt.time = time; 275 | mqtt.smid = smid; 276 | 277 | mqtt.pi = dtsu.power_total > 0 ? dtsu.power_total / 1000.0f : 0.0f; 278 | mqtt.po = dtsu.power_total < 0 ? -dtsu.power_total / 1000.0f : 0.0f; 279 | 280 | mqtt.pi1 = dtsu.power_L1 > 0 ? dtsu.power_L1 : 0.0f; 281 | mqtt.pi2 = dtsu.power_L2 > 0 ? dtsu.power_L2 : 0.0f; 282 | mqtt.pi3 = dtsu.power_L3 > 0 ? dtsu.power_L3 : 0.0f; 283 | 284 | mqtt.po1 = dtsu.power_L1 < 0 ? -dtsu.power_L1 : 0.0f; 285 | mqtt.po2 = dtsu.power_L2 < 0 ? -dtsu.power_L2 : 0.0f; 286 | mqtt.po3 = dtsu.power_L3 < 0 ? -dtsu.power_L3 : 0.0f; 287 | 288 | mqtt.u1 = dtsu.voltage_L1N; 289 | mqtt.u2 = dtsu.voltage_L2N; 290 | mqtt.u3 = dtsu.voltage_L3N; 291 | 292 | mqtt.i1 = dtsu.current_L1; 293 | mqtt.i2 = dtsu.current_L2; 294 | mqtt.i3 = dtsu.current_L3; 295 | 296 | mqtt.f = dtsu.frequency; 297 | 298 | mqtt.ei = dtsu.import_total; 299 | mqtt.eo = dtsu.export_total; 300 | } 301 | 302 | void debugMQTTData(const String& time, const String& smid, const DTSU666Data& data) { 303 | DEBUG_PRINTF("🔍 MQTT Data: %s [%s]\n", time.c_str(), smid.c_str()); 304 | DEBUG_PRINTF(" Power: %.1fW (L1:%.1f L2:%.1f L3:%.1f)\n", 305 | data.power_total, data.power_L1, data.power_L2, data.power_L3); 306 | DEBUG_PRINTF(" Voltage: %.1fV (L1:%.1f L2:%.1f L3:%.1f)\n", 307 | data.voltage_LN_avg, data.voltage_L1N, data.voltage_L2N, data.voltage_L3N); 308 | DEBUG_PRINTF(" Current: %.2fA (L1:%.2f L2:%.2f L3:%.2f)\n", 309 | (data.current_L1 + data.current_L2 + data.current_L3) / 3.0f, 310 | data.current_L1, data.current_L2, data.current_L3); 311 | DEBUG_PRINTF(" Frequency: %.2fHz\n", data.frequency); 312 | } -------------------------------------------------------------------------------- /Modbus_Proxy/src/modbus_proxy.cpp: -------------------------------------------------------------------------------- 1 | #include "modbus_proxy.h" 2 | #include "debug.h" 3 | 4 | // Serial interfaces 5 | // ESP32-C3 only has UART0 and UART1 (UART0 for debug, UART1 for RS485) 6 | // We'll use UART0 for SUN2000 and UART1 for DTU 7 | HardwareSerial SerialSUN(0); // UART0 for SUN2000 8 | HardwareSerial SerialDTU(1); // UART1 for DTU 9 | 10 | // ModbusRTU485 instances 11 | ModbusRTU485 modbusSUN; 12 | ModbusRTU485 modbusDTU; 13 | 14 | // Shared data structures 15 | SharedDTSUData sharedDTSU = {NULL, false, 0, {}, 0, {}, 0}; 16 | SharedEVCCData sharedEVCC = {NULL, 0.0f, 0, false, 0, 0}; 17 | DTSU666Data dtsu666Data; 18 | LastRequestInfo g_lastReq; 19 | 20 | // Power correction variables 21 | float powerCorrection = 0.0f; 22 | bool powerCorrectionActive = false; 23 | uint32_t lastCorrectionTime = 0; 24 | uint32_t lastHttpPoll = 0; 25 | 26 | // Task synchronization 27 | SemaphoreHandle_t proxyTaskHealthMutex; 28 | SemaphoreHandle_t mqttTaskHealthMutex; 29 | uint32_t proxyTaskLastSeen = 0; 30 | uint32_t mqttTaskLastSeen = 0; 31 | 32 | bool initModbusProxy() { 33 | proxyTaskHealthMutex = xSemaphoreCreateMutex(); 34 | mqttTaskHealthMutex = xSemaphoreCreateMutex(); 35 | 36 | if (!proxyTaskHealthMutex || !mqttTaskHealthMutex) { 37 | DEBUG_PRINTLN("❌ Failed to create health monitoring mutexes"); 38 | return false; 39 | } 40 | 41 | sharedDTSU.mutex = xSemaphoreCreateMutex(); 42 | if (!sharedDTSU.mutex) { 43 | DEBUG_PRINTLN("❌ Failed to create DTSU data mutex"); 44 | return false; 45 | } 46 | 47 | sharedEVCC.mutex = xSemaphoreCreateMutex(); 48 | if (!sharedEVCC.mutex) { 49 | DEBUG_PRINTLN("❌ Failed to create EVCC data mutex"); 50 | return false; 51 | } 52 | 53 | return initSerialInterfaces(); 54 | } 55 | 56 | bool initSerialInterfaces() { 57 | DEBUG_PRINTLN("🔧 Initializing RS485 interfaces..."); 58 | 59 | SerialSUN.begin(MODBUS_BAUDRATE, SERIAL_8N1, RS485_SUN2000_RX_PIN, RS485_SUN2000_TX_PIN); 60 | DEBUG_PRINTF(" ✅ SUN2000 interface: UART2, %d baud, pins %d(RX)/%d(TX)\n", 61 | MODBUS_BAUDRATE, RS485_SUN2000_RX_PIN, RS485_SUN2000_TX_PIN); 62 | 63 | SerialDTU.begin(MODBUS_BAUDRATE, SERIAL_8N1, RS485_DTU_RX_PIN, RS485_DTU_TX_PIN); 64 | DEBUG_PRINTF(" ✅ DTSU-666 interface: UART1, %d baud, pins %d(RX)/%d(TX)\n", 65 | MODBUS_BAUDRATE, RS485_DTU_RX_PIN, RS485_DTU_TX_PIN); 66 | 67 | modbusSUN.begin(SerialSUN, MODBUS_BAUDRATE); 68 | DEBUG_PRINTLN(" ✅ SUN2000 MODBUS handler initialized"); 69 | 70 | modbusDTU.begin(SerialDTU, MODBUS_BAUDRATE); 71 | DEBUG_PRINTLN(" ✅ DTSU-666 MODBUS handler initialized"); 72 | 73 | return true; 74 | } 75 | 76 | 77 | void proxyTask(void *pvParameters) { 78 | DEBUG_PRINTLN("🐌 Simple Proxy Task started - Direct SUN2000 ↔ DTSU proxying"); 79 | 80 | ModbusMessage sunMsg; 81 | uint32_t proxyCount = 0; 82 | 83 | DEBUG_PRINTLN("\n🔍 MODBUS PROXY DEBUG MODE ACTIVE"); 84 | DEBUG_PRINTF(" SUN2000 interface: RX=GPIO%d, TX=GPIO%d\n", RS485_SUN2000_RX_PIN, RS485_SUN2000_TX_PIN); 85 | DEBUG_PRINTF(" DTU interface: RX=GPIO%d, TX=GPIO%d\n", RS485_DTU_RX_PIN, RS485_DTU_TX_PIN); 86 | DEBUG_PRINTLN(" Waiting for MODBUS traffic...\n"); 87 | 88 | uint32_t lastDebugTime = 0; 89 | uint32_t noTrafficCount = 0; 90 | 91 | while (true) { 92 | updateTaskHealthbeat(true); 93 | 94 | // Periodic status report every 10 seconds 95 | if (millis() - lastDebugTime > 10000) { 96 | DEBUG_PRINTF("⏱️ No MODBUS traffic for %lu seconds (waiting on SUN2000 RX=GPIO%d)\n", 97 | noTrafficCount * 10, RS485_SUN2000_RX_PIN); 98 | lastDebugTime = millis(); 99 | noTrafficCount++; 100 | } 101 | 102 | if (modbusSUN.read(sunMsg, 2000)) { 103 | // Reset no-traffic counter 104 | noTrafficCount = 0; 105 | lastDebugTime = millis(); 106 | 107 | // Flash LED to indicate SUN2000 interface activity 108 | LED_ON(); 109 | 110 | proxyCount++; 111 | uint32_t proxyStart = millis(); 112 | 113 | // Only process and debug DTSU (ID=11) messages 114 | if (sunMsg.id == 11 && sunMsg.type == MBType::Request) { 115 | uint32_t dtsuStart = millis(); 116 | size_t written = SerialDTU.write(sunMsg.raw, sunMsg.len); 117 | SerialDTU.flush(); 118 | 119 | if (written == sunMsg.len) { 120 | ModbusMessage dtsuMsg; 121 | if (modbusDTU.read(dtsuMsg, 1000)) { 122 | uint32_t dtsuTime = millis() - dtsuStart; 123 | 124 | if (dtsuMsg.type == MBType::Exception) { 125 | DEBUG_PRINTF(" ❌ DTSU EXCEPTION: Code=0x%02X\n", dtsuMsg.exCode); 126 | systemHealth.proxyErrors++; 127 | reportSystemError("MODBUS", "DTSU exception", dtsuMsg.exCode); 128 | } else if (dtsuMsg.fc == 0x03 && dtsuMsg.len >= 165) { 129 | DTSU666Data dtsuData; 130 | if (parseDTSU666Response(dtsuMsg.raw, dtsuMsg.len, dtsuData)) { 131 | dtsu666Data = dtsuData; 132 | calculatePowerCorrection(); 133 | 134 | DTSU666Data finalData = dtsuData; 135 | bool correctionAppliedSuccessfully = false; 136 | 137 | if (powerCorrectionActive && fabs(powerCorrection) >= CORRECTION_THRESHOLD) { 138 | uint8_t correctedResponse[165]; 139 | memcpy(correctedResponse, dtsuMsg.raw, dtsuMsg.len); 140 | 141 | if (applyPowerCorrection(correctedResponse, dtsuMsg.len, powerCorrection)) { 142 | static uint8_t staticCorrectedResponse[165]; 143 | memcpy(staticCorrectedResponse, correctedResponse, dtsuMsg.len); 144 | dtsuMsg.raw = staticCorrectedResponse; 145 | 146 | if (parseDTSU666Response(staticCorrectedResponse, dtsuMsg.len, finalData)) { 147 | correctionAppliedSuccessfully = true; 148 | } else { 149 | finalData.power_total += powerCorrection; 150 | finalData.power_L1 += powerCorrection / 3.0f; 151 | finalData.power_L2 += powerCorrection / 3.0f; 152 | finalData.power_L3 += powerCorrection / 3.0f; 153 | correctionAppliedSuccessfully = true; 154 | } 155 | } 156 | } 157 | 158 | queueCorrectedPowerData(finalData, dtsuData, correctionAppliedSuccessfully, 159 | correctionAppliedSuccessfully ? powerCorrection : 0.0f); 160 | 161 | float currentWallboxPower = 0.0f; 162 | bool valid = false; 163 | getEVCCData(sharedEVCC, currentWallboxPower, valid); 164 | 165 | float sun2000Value = dtsuData.power_total; 166 | if (correctionAppliedSuccessfully && powerCorrection > 0) { 167 | sun2000Value = finalData.power_total; 168 | } 169 | 170 | // Single line debug output with all three values 171 | DEBUG_PRINTF("DTSU: %.1fW | Wallbox: %.1fW | SUN2000: %.1fW (%.1fW %c %.1fW)\n", 172 | dtsuData.power_total, 173 | currentWallboxPower, 174 | sun2000Value, 175 | dtsuData.power_total, 176 | correctionAppliedSuccessfully && powerCorrection >= 0 ? '+' : '-', 177 | fabs(correctionAppliedSuccessfully ? powerCorrection : 0.0f)); 178 | 179 | updateSharedData(dtsuMsg.raw, dtsuMsg.len, finalData); 180 | systemHealth.dtsuUpdates++; 181 | } 182 | } 183 | 184 | // Send response to SUN2000 185 | size_t sunWritten = SerialSUN.write(dtsuMsg.raw, dtsuMsg.len); 186 | SerialSUN.flush(); 187 | 188 | if (sunWritten != dtsuMsg.len) { 189 | DEBUG_PRINTF(" ❌ Failed to write to SUN2000: %u/%u bytes\n", sunWritten, dtsuMsg.len); 190 | systemHealth.proxyErrors++; 191 | reportSystemError("MODBUS", "SUN2000 write failed", sunWritten); 192 | } 193 | } else { 194 | DEBUG_PRINTLN("❌ DTSU TIMEOUT"); 195 | systemHealth.proxyErrors++; 196 | reportSystemError("MODBUS", "DTSU timeout", 0); 197 | } 198 | } else { 199 | DEBUG_PRINTLN("❌ DTSU WRITE FAILED"); 200 | systemHealth.proxyErrors++; 201 | reportSystemError("MODBUS", "DTSU write failed", written); 202 | } 203 | } 204 | 205 | // Turn off LED after SUN2000 transaction completes 206 | LED_OFF(); 207 | } 208 | 209 | vTaskDelay(10 / portTICK_PERIOD_MS); 210 | } 211 | } 212 | 213 | void calculatePowerCorrection() { 214 | float wallboxPower = 0.0f; 215 | bool valid = false; 216 | 217 | getEVCCData(sharedEVCC, wallboxPower, valid); 218 | 219 | if (!valid) { 220 | powerCorrection = 0.0f; 221 | powerCorrectionActive = false; 222 | return; 223 | } 224 | 225 | if (fabs(wallboxPower) > CORRECTION_THRESHOLD) { 226 | powerCorrection = wallboxPower; 227 | powerCorrectionActive = true; 228 | lastCorrectionTime = millis(); 229 | systemHealth.lastPowerCorrection = powerCorrection; 230 | systemHealth.powerCorrectionActive = true; 231 | } else { 232 | if (powerCorrectionActive) { 233 | DEBUG_PRINTF("\n⚡ POWER CORRECTION DEACTIVATED:\n"); 234 | DEBUG_PRINTF(" No significant wallbox charging detected\n"); 235 | } 236 | 237 | powerCorrection = 0.0f; 238 | powerCorrectionActive = false; 239 | systemHealth.powerCorrectionActive = false; 240 | } 241 | } 242 | 243 | bool shouldApplyCorrection(float wallboxPower) { 244 | return fabs(wallboxPower) > CORRECTION_THRESHOLD; 245 | } 246 | 247 | void updateSharedData(const uint8_t* responseData, uint16_t responseLen, const DTSU666Data& parsedData) { 248 | if (xSemaphoreTake(sharedDTSU.mutex, 10 / portTICK_PERIOD_MS) == pdTRUE) { 249 | sharedDTSU.valid = true; 250 | sharedDTSU.timestamp = millis(); 251 | sharedDTSU.responseLength = responseLen; 252 | memcpy(sharedDTSU.responseBuffer, responseData, responseLen); 253 | sharedDTSU.parsedData = parsedData; 254 | sharedDTSU.updateCount++; 255 | xSemaphoreGive(sharedDTSU.mutex); 256 | } 257 | } 258 | 259 | void updateTaskHealthbeat(bool isProxyTask) { 260 | uint32_t currentTime = millis(); 261 | if (isProxyTask) { 262 | proxyTaskLastSeen = currentTime; 263 | } else { 264 | mqttTaskLastSeen = currentTime; 265 | } 266 | } 267 | 268 | void performHealthCheck() { 269 | uint32_t currentTime = millis(); 270 | 271 | if (currentTime - proxyTaskLastSeen > WATCHDOG_TIMEOUT_MS) { 272 | DEBUG_PRINTF("🚨 PROXY TASK TIMEOUT: %lu ms since last heartbeat\n", 273 | currentTime - proxyTaskLastSeen); 274 | reportSystemError("WATCHDOG", "Proxy task timeout", currentTime - proxyTaskLastSeen); 275 | } 276 | 277 | if (currentTime - mqttTaskLastSeen > WATCHDOG_TIMEOUT_MS) { 278 | DEBUG_PRINTF("🚨 MQTT TASK TIMEOUT: %lu ms since last heartbeat\n", 279 | currentTime - mqttTaskLastSeen); 280 | reportSystemError("WATCHDOG", "MQTT task timeout", currentTime - mqttTaskLastSeen); 281 | } 282 | 283 | systemHealth.uptime = currentTime; 284 | systemHealth.freeHeap = ESP.getFreeHeap(); 285 | systemHealth.minFreeHeap = ESP.getMinFreeHeap(); 286 | 287 | if (systemHealth.freeHeap < MIN_FREE_HEAP) { 288 | DEBUG_PRINTF("🚨 LOW MEMORY WARNING: %lu bytes free (threshold: %lu)\n", 289 | systemHealth.freeHeap, MIN_FREE_HEAP); 290 | reportSystemError("MEMORY", "Low heap memory", systemHealth.freeHeap); 291 | } 292 | } 293 | 294 | void reportSystemError(const char* subsystem, const char* error, int code) { 295 | DEBUG_PRINTF("🚨 SYSTEM ERROR [%s]: %s", subsystem, error); 296 | if (code != 0) { 297 | DEBUG_PRINTF(" (code: %d)", code); 298 | } 299 | DEBUG_PRINTLN(); 300 | } 301 | 302 | void watchdogTask(void *pvParameters) { 303 | (void)pvParameters; 304 | DEBUG_PRINTLN("🐕 Watchdog Task started - Independent system health monitoring"); 305 | DEBUG_PRINTF("🐕 Running on Core %d with priority %d\n", xPortGetCoreID(), uxTaskPriorityGet(NULL)); 306 | 307 | while (true) { 308 | performHealthCheck(); 309 | vTaskDelay(pdMS_TO_TICKS(HEALTH_CHECK_INTERVAL)); 310 | } 311 | } 312 | 313 | bool isValidModbusMessage(const uint8_t* data, size_t len) { 314 | if (!data || len < 4) return false; 315 | return validateCRC(data, len); 316 | } 317 | 318 | bool validateCRC(const uint8_t* data, size_t len) { 319 | if (len < 2) return false; 320 | 321 | uint16_t crcGiven = (uint16_t)data[len-2] | ((uint16_t)data[len-1] << 8); 322 | uint16_t crcCalc = ModbusRTU485::crc16(data, len-2); 323 | 324 | return crcGiven == crcCalc; 325 | } -------------------------------------------------------------------------------- /Modbus_Proxy/Modbus-Proxy-FSD.md: -------------------------------------------------------------------------------- 1 | # ESP32 MODBUS RTU Intelligent Proxy - Functional Specification Document 2 | 3 | **Version:** 3.0 4 | **Date:** January 2025 5 | **Platform:** ESP32-S3 (dual-core) / ESP32-C3 (single-core) 6 | **Author:** Andreas Spiess / Claude Code 7 | 8 | --- 9 | 10 | ## 1. System Overview 11 | 12 | ### 1.1 Purpose 13 | The ESP32 MODBUS RTU Intelligent Proxy is a sophisticated power monitoring system that sits between a SUN2000 solar inverter and a DTSU-666 energy meter, providing real-time power correction by integrating wallbox charging data from an EVCC system. 14 | 15 | ### 1.2 Primary Goals 16 | - **Intelligent Power Correction**: Integrate wallbox power consumption into solar inverter energy management 17 | - **Transparent MODBUS Proxy**: Seamless bidirectional communication between SUN2000 and DTSU-666 18 | - **System Reliability**: Comprehensive auto-restart and health monitoring capabilities 19 | - **Real-time Monitoring**: MQTT-based power data publishing and system health status 20 | 21 | --- 22 | 23 | ## 2. Hardware Variants 24 | 25 | ### 2.1 ESP32-S3 Configuration (Branch: S3) 26 | 27 | **Hardware:** 28 | - **MCU**: ESP32-S3 (dual-core Xtensa, 240MHz) 29 | - **Board**: Lolin S3 Mini 30 | - **UARTs**: 3 available (UART0 for USB CDC, UART1/UART2 for RS485) 31 | - **Status LED**: GPIO 48 (normal logic) 32 | 33 | **Pin Configuration:** 34 | - **SUN2000 Interface**: UART1 (RX=GPIO18, TX=GPIO17) 35 | - **DTSU-666 Interface**: UART2 (RX=GPIO16, TX=GPIO15) 36 | 37 | **Task Distribution:** 38 | - **Core 0**: MQTT task (Priority 1), Watchdog task (Priority 3) 39 | - **Core 1**: Proxy task (Priority 2, dedicated MODBUS processing) 40 | 41 | **Debug Options:** 42 | - USB Serial debugging (115200 baud) 43 | - Telnet wireless debugging (port 23) 44 | 45 | ### 2.2 ESP32-C3 Configuration (Branch: main) 46 | 47 | **Hardware:** 48 | - **MCU**: ESP32-C3 (single-core RISC-V, 160MHz) 49 | - **Board**: ESP32-C3-DevKitM-1 50 | - **UARTs**: 2 available (UART0/UART1 for RS485, no USB CDC) 51 | - **Status LED**: GPIO 8 (inverted logic) 52 | 53 | **Pin Configuration:** 54 | - **SUN2000 Interface**: UART0 (RX=GPIO7, TX=GPIO10) 55 | - **DTSU-666 Interface**: UART1 (RX=GPIO1, TX=GPIO0) 56 | 57 | **Task Distribution:** 58 | - **Single Core**: All tasks (MQTT Priority 1, Proxy Priority 2, Watchdog Priority 3) 59 | 60 | **Debug Options:** 61 | - Telnet wireless debugging only (USB CDC disabled to free UART0) 62 | 63 | --- 64 | 65 | ## 3. System Architecture 66 | 67 | ### 3.1 Hardware Flow 68 | 69 | ``` 70 | Grid ←→ L&G Meter ←→ Wallbox ←→ DTSU-666 ←→ SUN2000 Inverter 71 | (Reference) (4.2kW) (Proxy) (Solar) 72 | ↑ 73 | ESP32 Proxy 74 | (WiFi/HTTP/MQTT) 75 | ↓ 76 | EVCC System 77 | ``` 78 | 79 | ### 3.2 Software Architecture (ESP32-S3 Dual-Core) 80 | 81 | ``` 82 | Core 0: Core 1: 83 | ├── MQTT Task (Priority 1) ├── Proxy Task (Priority 2) 84 | │ │ Stack: 16KB │ │ Stack: 4KB 85 | │ ├── EVCC API polling (10s) │ ├── MODBUS proxy 86 | │ ├── JSON parsing (8KB buf) │ ├── Power correction 87 | │ └── MQTT publishing │ ├── LED activity indication 88 | ├── Watchdog Task (Priority 3) │ └── Heartbeat updates 89 | │ ├── Health monitoring (5s) 90 | │ ├── Failure detection 91 | │ └── Auto-restart triggers 92 | ``` 93 | 94 | ### 3.3 Software Architecture (ESP32-C3 Single-Core) 95 | 96 | ``` 97 | Single Core: 98 | ├── Proxy Task (Priority 2) ← Highest priority for MODBUS timing 99 | │ ├── MODBUS proxy 100 | │ ├── Power correction 101 | │ └── LED activity indication 102 | ├── MQTT Task (Priority 1) 103 | │ ├── EVCC API polling (10s) 104 | │ ├── JSON parsing (8KB buf) 105 | │ └── MQTT publishing 106 | └── Watchdog Task (Priority 3) 107 | ├── Health monitoring (5s) 108 | └── Auto-restart triggers 109 | ``` 110 | 111 | --- 112 | 113 | ## 4. Communication Protocols 114 | 115 | ### 4.1 MODBUS RTU Specification 116 | - **Slave ID**: 11 (DTSU-666 meter) 117 | - **Baud Rate**: 9600, 8N1 118 | - **Function Codes**: 0x03 (Read Holding), 0x04 (Read Input) 119 | - **Register Range**: 2102-2181 (80 registers, 160 bytes IEEE 754) 120 | - **Data Format**: Big-endian IEEE 754 32-bit floats 121 | - **CRC Validation**: Automatic validation and recalculation 122 | 123 | ### 4.2 EVCC HTTP API 124 | - **URL**: `http://192.168.0.202:7070/api/state` 125 | - **Method**: GET (JSON response) 126 | - **Data Path**: `loadpoints[0].chargePower` (watts) 127 | - **Polling Interval**: 10 seconds 128 | - **Buffer Size**: StaticJsonDocument<8192> 129 | - **Hostname**: "MODBUS-Proxy" 130 | 131 | ### 4.3 MQTT Communication 132 | 133 | **Broker Configuration:** 134 | - **Server**: 192.168.0.203:1883 135 | - **Client ID**: MBUS_PROXY_{MAC_ADDRESS} 136 | - **Buffer Size**: 1024 bytes 137 | - **Keep Alive**: 60 seconds 138 | 139 | **Published Topics:** 140 | 141 | 1. **MBUS-PROXY/power** (every MODBUS transaction) 142 | ```json 143 | { 144 | "dtsu": -18.5, 145 | "wallbox": 0.0, 146 | "sun2000": -18.5, 147 | "active": false 148 | } 149 | ``` 150 | 151 | 2. **MBUS-PROXY/health** (every 60 seconds) 152 | ```json 153 | { 154 | "timestamp": 123456, 155 | "uptime": 123456, 156 | "free_heap": 250000, 157 | "min_free_heap": 200000, 158 | "mqtt_reconnects": 0, 159 | "dtsu_updates": 1234, 160 | "evcc_updates": 123, 161 | "evcc_errors": 0, 162 | "proxy_errors": 0, 163 | "power_correction": 0.0, 164 | "correction_active": false 165 | } 166 | ``` 167 | 168 | --- 169 | 170 | ## 5. Power Correction Algorithm 171 | 172 | ### 5.1 Correction Logic 173 | 174 | **Threshold**: 1000W minimum wallbox power for correction activation 175 | 176 | **Calculation**: 177 | ``` 178 | corrected_power = dtsu_power + wallbox_power 179 | ``` 180 | 181 | **Distribution** (when correction applied): 182 | - Total power corrected by full wallbox power 183 | - Phase powers distributed evenly (wallbox_power / 3 per phase) 184 | - Demand values adjusted proportionally 185 | 186 | ### 5.2 MODBUS Frame Modification 187 | 188 | **Process**: 189 | 1. Read MODBUS response from DTSU-666 190 | 2. Parse IEEE 754 float values 191 | 3. Apply correction if wallbox power > 1000W 192 | 4. Write modified values back to frame 193 | 5. Recalculate and update CRC16 194 | 6. Forward to SUN2000 195 | 196 | **Modified Registers**: 197 | - Total power (register 2126) 198 | - Phase L1 power (register 2128) 199 | - Phase L2 power (register 2130) 200 | - Phase L3 power (register 2132) 201 | - Total demand (register 2158) 202 | - Phase demands (registers 2160, 2162, 2164) 203 | 204 | --- 205 | 206 | ## 6. Debug Output Format 207 | 208 | ### 6.1 Serial/Telnet Output 209 | 210 | **Single-line format per MODBUS transaction**: 211 | ``` 212 | DTSU: 94.1W | Wallbox: 0.0W | SUN2000: 94.1W (94.1W + 0.0W) 213 | ``` 214 | 215 | **Success-only logging**: 216 | - No verbose packet dumps 217 | - MQTT publish failures logged only 218 | - Internal SUN2000 messages (ID=5) hidden 219 | 220 | --- 221 | 222 | ## 7. Build Configuration 223 | 224 | ### 7.1 PlatformIO Environments 225 | 226 | **ESP32-S3**: 227 | - `esp32-s3-serial`: Serial upload (COM port) 228 | - `esp32-s3-ota`: OTA wireless upload 229 | 230 | **ESP32-C3**: 231 | - `esp32-c3-serial`: Serial upload (COM port) 232 | - `esp32-c3-ota`: OTA wireless upload 233 | 234 | ### 7.2 Build Flags 235 | 236 | **ESP32-S3**: 237 | ```ini 238 | build_flags = 239 | -DARDUINO_USB_CDC_ON_BOOT=1 240 | -std=gnu++17 241 | board_build.usb_cdc = true 242 | ``` 243 | 244 | **ESP32-C3**: 245 | ```ini 246 | build_flags = 247 | -DARDUINO_USB_CDC_ON_BOOT=0 248 | -std=gnu++17 249 | board_build.usb_cdc = false 250 | board_build.arduino.memory_type = qio_qspi 251 | ``` 252 | 253 | ### 7.3 Dependencies 254 | - ArduinoJson @ ^6.19.4 255 | - PubSubClient @ ^2.8 256 | - ArduinoOTA 257 | 258 | --- 259 | 260 | ## 8. Configuration Files 261 | 262 | ### 8.1 credentials.h (User-Specific) 263 | 264 | **Note**: This file is gitignored. Copy from `credentials.h.example` 265 | 266 | ```cpp 267 | static const char* ssid = "YOUR_WIFI_SSID"; 268 | static const char* password = "YOUR_WIFI_PASSWORD"; 269 | static const char* mqttServer = "192.168.0.203"; 270 | static const char* evccApiUrl = "http://192.168.0.202:7070/api/state"; 271 | ``` 272 | 273 | ### 8.2 config.h (Platform-Specific) 274 | 275 | **Debug Settings**: 276 | - `ENABLE_SERIAL_DEBUG`: USB serial debugging (ESP32-S3 only) 277 | - `ENABLE_TELNET_DEBUG`: Wireless telnet debugging 278 | 279 | **Timing Constants**: 280 | - `CORRECTION_THRESHOLD`: 1000.0f (watts) 281 | - `HTTP_POLL_INTERVAL`: 10000 (ms) 282 | - `WATCHDOG_TIMEOUT_MS`: 60000 (ms) 283 | - `HEALTH_CHECK_INTERVAL`: 5000 (ms) 284 | 285 | --- 286 | 287 | ## 9. Memory Management 288 | 289 | ### 9.1 Task Stack Sizes 290 | 291 | | Task | ESP32-S3 | ESP32-C3 | 292 | |------|----------|----------| 293 | | Proxy | 4KB | 4KB | 294 | | MQTT | 16KB | 16KB | 295 | | Watchdog | 2KB | 2KB | 296 | 297 | ### 9.2 Heap Requirements 298 | 299 | - **Minimum Free Heap**: 20KB threshold 300 | - **MQTT Buffer**: 1024 bytes 301 | - **JSON Buffer**: 8192 bytes (StaticJsonDocument) 302 | - **Typical Free Heap**: ~250KB (ESP32-S3), ~200KB (ESP32-C3) 303 | 304 | --- 305 | 306 | ## 10. Error Handling & Recovery 307 | 308 | ### 10.1 Watchdog Monitoring 309 | 310 | **Timeouts**: 311 | - Task heartbeat timeout: 60 seconds 312 | - MQTT reconnection: Automatic with exponential backoff 313 | - EVCC API failure: Continue with last valid data 314 | 315 | ### 10.2 Auto-Restart Triggers 316 | 317 | - Initialization failures (MODBUS, MQTT, EVCC) 318 | - Watchdog task timeout 319 | - Critical memory shortage (< 20KB free heap) 320 | 321 | ### 10.3 Error Reporting 322 | 323 | **MQTT Error Messages**: 324 | - Subsystem identifier (MODBUS, MEMORY, WATCHDOG) 325 | - Error description 326 | - Numeric error code 327 | 328 | --- 329 | 330 | ## 11. OTA Updates 331 | 332 | ### 11.1 Configuration 333 | 334 | - **Port**: 3232 335 | - **Password**: modbus_ota_2023 336 | - **Hostname**: MODBUS-Proxy 337 | 338 | ### 11.2 Update Process 339 | 340 | 1. Device advertises on network 341 | 2. PlatformIO connects via espota 342 | 3. Firmware uploaded with progress reporting 343 | 4. Automatic restart after successful upload 344 | 345 | --- 346 | 347 | ## 12. Network Configuration 348 | 349 | ### 12.1 WiFi Settings 350 | 351 | - **Mode**: Station (STA) mode 352 | - **Hostname**: "MODBUS-Proxy" 353 | - **Power Save**: Disabled for stability 354 | - **Connection Timeout**: 30 seconds 355 | 356 | ### 12.2 Services 357 | 358 | - **mDNS**: Enabled (MODBUS-Proxy.local) 359 | - **Telnet**: Port 23 (if enabled) 360 | - **OTA**: Port 3232 361 | - **MQTT**: Port 1883 362 | 363 | --- 364 | 365 | ## 13. Performance Characteristics 366 | 367 | ### 13.1 Timing 368 | 369 | - **MODBUS Response Time**: < 100ms typical 370 | - **Power Correction Latency**: < 1ms (in-line processing) 371 | - **MQTT Publish Rate**: Per MODBUS transaction (~1/second) 372 | - **API Polling**: 10 second interval 373 | 374 | ### 13.2 Resource Usage 375 | 376 | **ESP32-S3**: 377 | - **RAM**: 16.5% (54KB / 320KB) 378 | - **Flash**: 72.4% (949KB / 1.3MB) 379 | - **CPU**: Dual-core utilization 380 | 381 | **ESP32-C3**: 382 | - **RAM**: 14.8% (48KB / 320KB) 383 | - **Flash**: 74.5% (977KB / 1.3MB) 384 | - **CPU**: Single-core time-sliced 385 | 386 | --- 387 | 388 | ## 14. Testing & Validation 389 | 390 | ### 14.1 Functional Tests 391 | 392 | - ✅ MODBUS proxy passthrough 393 | - ✅ Power correction calculation 394 | - ✅ EVCC API polling 395 | - ✅ MQTT publishing 396 | - ✅ Watchdog monitoring 397 | - ✅ Auto-restart recovery 398 | 399 | ### 14.2 Stress Tests 400 | 401 | - ✅ Continuous operation (24+ hours) 402 | - ✅ Network disconnection recovery 403 | - ✅ MQTT broker reconnection 404 | - ✅ EVCC API timeout handling 405 | 406 | --- 407 | 408 | ## 15. Future Enhancements 409 | 410 | ### 15.1 Potential Features 411 | 412 | - Historical data logging to SD card 413 | - Web interface for configuration 414 | - Support for multiple wallboxes 415 | - Advanced power flow visualization 416 | - Integration with Home Assistant 417 | 418 | ### 15.2 Known Limitations 419 | 420 | - Single DTSU-666 meter support only 421 | - Fixed EVCC API endpoint 422 | - No authentication on telnet debug port 423 | - Limited to 9600 baud MODBUS communication 424 | 425 | --- 426 | 427 | ## Appendix A: Register Map 428 | 429 | See DTSU-666 datasheet for complete register definitions (2102-2181) 430 | 431 | ## Appendix B: Troubleshooting 432 | 433 | **Common Issues**: 434 | 1. **No MODBUS traffic**: Check GPIO pin assignments, RS485 wiring 435 | 2. **MQTT disconnects**: Verify broker address, check network stability 436 | 3. **Wrong power values**: Verify EVCC API URL and data structure 437 | 4. **OTA fails**: Check WiFi signal strength, verify password 438 | 439 | --- 440 | 441 | **Document Version History**: 442 | - v3.0 (January 2025): Dual-platform support (ESP32-S3/C3), telnet debugging 443 | - v2.0 (September 2024): MQTT implementation, power correction 444 | - v1.0 (Initial): Basic MODBUS proxy functionality 445 | -------------------------------------------------------------------------------- /Modbus_Proxy/src/main.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // ESP32 MODBUS RTU Intelligent Proxy with Power Correction - ESP32-S3 Version 3 | // ========================================================================== 4 | // 5 | // Modular architecture with separated concerns for improved maintainability 6 | 7 | #include 8 | #include "freertos/FreeRTOS.h" 9 | #include "freertos/task.h" 10 | #include "ModbusRTU485.h" 11 | #include 12 | #include "credentials.h" 13 | 14 | #include "config.h" 15 | #include "debug.h" 16 | #include "dtsu666.h" 17 | #include "evcc_api.h" 18 | #include "mqtt_handler.h" 19 | #include "modbus_proxy.h" 20 | #include 21 | 22 | // Global variable definitions are in credentials.h 23 | 24 | // Telnet debug instance 25 | #if defined(ENABLE_TELNET_DEBUG) && ENABLE_TELNET_DEBUG 26 | TelnetDebug telnetDebug; 27 | #endif 28 | 29 | // Output options 30 | #ifndef PRINT_FANCY_TABLE 31 | #define PRINT_FANCY_TABLE false 32 | #endif 33 | 34 | // WiFi and MQTT setup functions 35 | void setupWiFi(); 36 | void setupMQTT(); 37 | void discoverModbusPins(); 38 | 39 | void setupWiFi() { 40 | // Disable WiFi power saving to prevent spinlock issues 41 | WiFi.setSleep(false); 42 | WiFi.mode(WIFI_STA); 43 | 44 | DEBUG_PRINTF("📡 Connecting to WiFi SSID: %s\n", ssid); 45 | WiFi.setHostname("MODBUS-Proxy"); 46 | int status = WiFi.begin(ssid, password); 47 | DEBUG_PRINTF("WiFi.begin() returned: %d\n", status); 48 | 49 | uint32_t startTime = millis(); 50 | const uint32_t WIFI_TIMEOUT = 30000; // 30 second timeout 51 | 52 | // Phase 2: WiFi connection - Blink during attempts 53 | while (WiFi.status() != WL_CONNECTED) { 54 | if (millis() - startTime > WIFI_TIMEOUT) { 55 | DEBUG_PRINTLN(); 56 | DEBUG_PRINTF("❌ WiFi connection timeout - Final status: %d\n", WiFi.status()); 57 | DEBUG_PRINTLN("Restarting..."); 58 | ESP.restart(); 59 | } 60 | 61 | // Blink LED 62 | LED_ON(); 63 | delay(350); 64 | LED_OFF(); 65 | delay(350); 66 | 67 | DEBUG_PRINTF("[%d]", WiFi.status()); 68 | } 69 | 70 | DEBUG_PRINTLN(); 71 | DEBUG_PRINTF("WiFi connected! IP address: %s\n", WiFi.localIP().toString().c_str()); 72 | DEBUG_PRINTLN("✅ WiFi power saving disabled for stability"); 73 | 74 | // Blink 2 times to indicate WiFi connection success 75 | for (int i = 0; i < 2; i++) { 76 | LED_ON(); 77 | delay(200); 78 | LED_OFF(); 79 | delay(200); 80 | } 81 | } 82 | 83 | void setupMQTT() { 84 | initMQTT(); 85 | } 86 | 87 | void setupOTA() { 88 | ArduinoOTA.setHostname("MODBUS-Proxy"); 89 | ArduinoOTA.setPassword("modbus_ota_2023"); 90 | 91 | ArduinoOTA.onStart([]() { 92 | DEBUG_PRINTLN("🔄 OTA Update starting..."); 93 | LED_ON(); 94 | }); 95 | 96 | ArduinoOTA.onEnd([]() { 97 | DEBUG_PRINTLN("✅ OTA Update completed"); 98 | LED_OFF(); 99 | }); 100 | 101 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 102 | int percentage = (progress * 100) / total; 103 | DEBUG_PRINTF("📊 OTA Progress: %u%%\n", percentage); 104 | if ((percentage % 20) < 10) LED_ON(); else LED_OFF(); 105 | }); 106 | 107 | ArduinoOTA.onError([](ota_error_t error) { 108 | DEBUG_PRINTF("❌ OTA Error[%u]: ", error); 109 | if (error == OTA_AUTH_ERROR) DEBUG_PRINTLN("Auth Failed"); 110 | else if (error == OTA_BEGIN_ERROR) DEBUG_PRINTLN("Begin Failed"); 111 | else if (error == OTA_CONNECT_ERROR) DEBUG_PRINTLN("Connect Failed"); 112 | else if (error == OTA_RECEIVE_ERROR) DEBUG_PRINTLN("Receive Failed"); 113 | else if (error == OTA_END_ERROR) DEBUG_PRINTLN("End Failed"); 114 | 115 | // Flash LED rapidly on error 116 | for (int i = 0; i < 10; i++) { 117 | LED_ON(); 118 | delay(100); 119 | LED_OFF(); 120 | delay(100); 121 | } 122 | }); 123 | 124 | ArduinoOTA.begin(); 125 | DEBUG_PRINTLN("🔗 Arduino OTA Ready"); 126 | } 127 | 128 | void setup() { 129 | #if ENABLE_SERIAL_DEBUG 130 | Serial.begin(115200); 131 | // Wait briefly for USB CDC to be ready on ESP32-C3 132 | delay(100); 133 | #endif 134 | 135 | // Initialize status LED 136 | pinMode(STATUS_LED_PIN, OUTPUT); 137 | LED_OFF(); 138 | 139 | // Phase 1: Startup - Blink 5 times 140 | for (int i = 0; i < 5; i++) { 141 | LED_ON(); 142 | delay(100); 143 | LED_OFF(); 144 | delay(100); 145 | } 146 | 147 | DEBUG_PRINTLN("🚀 ESP32-C3 MODBUS PROXY starting..."); 148 | DEBUG_PRINTF("📅 Build: %s %s\n", __DATE__, __TIME__); 149 | DEBUG_PRINTLN("🎯 Mode: Modular ESP32-C3 single-core proxy with configurable GPIO pins"); 150 | DEBUG_PRINTLN("\n⚙️ Configuration Parameters:"); 151 | DEBUG_PRINTF(" WiFi SSID: '%s'\n", ssid); 152 | DEBUG_PRINTF(" WiFi Password: '%s'\n", password); 153 | DEBUG_PRINTF(" MQTT Server: %s:%d\n", mqttServer, mqttPort); 154 | DEBUG_PRINTF(" EVCC API URL: %s\n", evccApiUrl); 155 | DEBUG_PRINTF(" RS485 SUN2000: RX=%d, TX=%d\n", RS485_SUN2000_RX_PIN, RS485_SUN2000_TX_PIN); 156 | DEBUG_PRINTF(" RS485 DTU: RX=%d, TX=%d\n", RS485_DTU_RX_PIN, RS485_DTU_TX_PIN); 157 | DEBUG_PRINTF(" Status LED: GPIO %d\n", STATUS_LED_PIN); 158 | DEBUG_PRINTF(" MODBUS Baudrate: %d\n", MODBUS_BAUDRATE); 159 | DEBUG_PRINTF(" Power Correction Threshold: %.0f W\n", CORRECTION_THRESHOLD); 160 | DEBUG_PRINTF(" HTTP Poll Interval: %d ms\n", HTTP_POLL_INTERVAL); 161 | DEBUG_PRINTF(" Watchdog Timeout: %d ms\n", WATCHDOG_TIMEOUT_MS); 162 | DEBUG_PRINTF(" Serial Debug: %s\n\n", ENABLE_SERIAL_DEBUG ? "ENABLED" : "DISABLED"); 163 | 164 | // Initialize WiFi FIRST, before any other subsystems 165 | setupWiFi(); 166 | setupOTA(); 167 | 168 | // Initialize Telnet debug server after WiFi 169 | #if defined(ENABLE_TELNET_DEBUG) && ENABLE_TELNET_DEBUG 170 | telnetDebug.begin(23); 171 | DEBUG_PRINTLN("📡 Telnet debug server started on port 23"); 172 | DEBUG_PRINTF(" Connect via: telnet %s 23\n", WiFi.localIP().toString().c_str()); 173 | #endif 174 | 175 | // Initialize system health monitoring 176 | uint32_t currentTime = millis(); 177 | systemHealth.uptime = currentTime; 178 | systemHealth.freeHeap = ESP.getFreeHeap(); 179 | systemHealth.minFreeHeap = ESP.getMinFreeHeap(); 180 | DEBUG_PRINTLN("🏥 System health monitoring initialized"); 181 | 182 | // Initialize MQTT after WiFi 183 | setupMQTT(); 184 | 185 | // Run pin discovery to find active MODBUS RX pins 186 | discoverModbusPins(); 187 | 188 | // Initialize modular components 189 | if (!initModbusProxy()) { 190 | DEBUG_PRINTLN("❌ Failed to initialize MODBUS proxy"); 191 | ESP.restart(); 192 | } 193 | 194 | if (!initEVCCAPI()) { 195 | DEBUG_PRINTLN("❌ Failed to initialize EVCC API"); 196 | ESP.restart(); 197 | } 198 | 199 | // Create MQTT task (lowest priority) - increased stack for EVCC API + JSON 200 | xTaskCreate( 201 | mqttTask, 202 | "MQTTTask", 203 | 16384, // Increased to 16KB for HTTP + large JSON buffer 204 | NULL, 205 | 1, 206 | NULL 207 | ); 208 | DEBUG_PRINTLN(" ✅ MQTT task created (Priority 1)"); 209 | 210 | // Create proxy task (medium priority) 211 | xTaskCreate( 212 | proxyTask, 213 | "ProxyTask", 214 | 4096, 215 | NULL, 216 | 2, 217 | NULL 218 | ); 219 | DEBUG_PRINTLN(" ✅ Proxy task created (Priority 2)"); 220 | 221 | // Create watchdog task (highest priority) 222 | xTaskCreate( 223 | watchdogTask, 224 | "WatchdogTask", 225 | 2048, 226 | NULL, 227 | 3, 228 | NULL 229 | ); 230 | DEBUG_PRINTLN(" ✅ Watchdog task created (Priority 3)"); 231 | 232 | DEBUG_PRINTLN("🔗 Modular ESP32-C3 proxy initialized!"); 233 | DEBUG_PRINTLN(" 📡 MQTT publishing and EVCC API polling"); 234 | DEBUG_PRINTLN(" 🔄 MODBUS proxy with power correction"); 235 | DEBUG_PRINTLN(" 🐕 Independent health monitoring"); 236 | DEBUG_PRINTLN("⚡ Ready for operations!"); 237 | 238 | // Phase 4: Setup complete - Blink 5 times 239 | for (int i = 0; i < 5; i++) { 240 | LED_ON(); 241 | delay(100); 242 | LED_OFF(); 243 | delay(100); 244 | } 245 | } 246 | 247 | void discoverModbusPins() { 248 | DEBUG_PRINTLN("\n🔍 MODBUS PIN DISCOVERY MODE"); 249 | DEBUG_PRINTLN("Scanning configured GPIO pins for MODBUS traffic..."); 250 | 251 | // Test only the configured pins 252 | const uint8_t testPins[] = {RS485_SUN2000_RX_PIN, RS485_SUN2000_TX_PIN, RS485_DTU_RX_PIN, RS485_DTU_TX_PIN}; 253 | const uint8_t numPins = sizeof(testPins) / sizeof(testPins[0]); 254 | 255 | struct PinActivity { 256 | uint8_t pin; 257 | uint32_t byteCount; 258 | uint32_t transitions; 259 | }; 260 | 261 | PinActivity activity[numPins] = {}; 262 | 263 | // Initialize all test pins as INPUT 264 | for (uint8_t i = 0; i < numPins; i++) { 265 | pinMode(testPins[i], INPUT); 266 | activity[i].pin = testPins[i]; 267 | activity[i].byteCount = 0; 268 | activity[i].transitions = 0; 269 | } 270 | 271 | DEBUG_PRINTLN("⏱️ Monitoring for 15 seconds..."); 272 | 273 | // Monitor for 15 seconds 274 | uint32_t startTime = millis(); 275 | uint32_t lastReportTime = millis(); 276 | uint8_t lastState[numPins]; 277 | 278 | // Initialize last state 279 | for (uint8_t i = 0; i < numPins; i++) { 280 | lastState[i] = digitalRead(testPins[i]); 281 | } 282 | 283 | while (millis() - startTime < 15000) { 284 | for (uint8_t i = 0; i < numPins; i++) { 285 | uint8_t currentState = digitalRead(testPins[i]); 286 | if (currentState != lastState[i]) { 287 | activity[i].transitions++; 288 | lastState[i] = currentState; 289 | } 290 | } 291 | 292 | // Progress indicator every 3 seconds 293 | if (millis() - lastReportTime > 3000) { 294 | DEBUG_PRINTF(" ⏳ %lu seconds elapsed...\n", (millis() - startTime) / 1000); 295 | lastReportTime = millis(); 296 | } 297 | 298 | // Feed watchdog and yield to prevent WDT reset 299 | yield(); 300 | delayMicroseconds(500); // Sample at ~2kHz (sufficient for 9600 baud) 301 | } 302 | 303 | DEBUG_PRINTLN("\n📊 DISCOVERY RESULTS:"); 304 | DEBUG_PRINTLN("GPIO | Transitions | Likely"); 305 | DEBUG_PRINTLN("-----|-------------|-------"); 306 | 307 | uint8_t candidateRxPins[4] = {255, 255, 255, 255}; 308 | uint8_t candidateCount = 0; 309 | 310 | for (uint8_t i = 0; i < numPins; i++) { 311 | const char* likely = ""; 312 | if (activity[i].transitions > 100) { 313 | likely = "← ACTIVE (RX candidate)"; 314 | if (candidateCount < 4) { 315 | candidateRxPins[candidateCount++] = activity[i].pin; 316 | } 317 | } else if (activity[i].transitions > 10) { 318 | likely = "← Some activity"; 319 | } 320 | 321 | DEBUG_PRINTF(" %2d | %11lu | %s\n", 322 | activity[i].pin, 323 | activity[i].transitions, 324 | likely); 325 | } 326 | 327 | DEBUG_PRINTLN(); 328 | 329 | if (candidateCount >= 2) { 330 | DEBUG_PRINTLN("✅ FOUND CANDIDATE RX PINS:"); 331 | DEBUG_PRINTF(" Likely SUN2000 RX: GPIO %d (or GPIO %d)\n", 332 | candidateRxPins[0], candidateRxPins[1]); 333 | if (candidateCount > 2) { 334 | DEBUG_PRINTF(" Likely DTSU RX: GPIO %d (or GPIO %d)\n", 335 | candidateRxPins[2], candidateCount > 3 ? candidateRxPins[3] : candidateRxPins[1]); 336 | } 337 | DEBUG_PRINTLN("\n💡 Update config.h with these pins and reflash."); 338 | DEBUG_PRINTLN(" Continuing with current configuration...\n"); 339 | } else if (candidateCount == 1) { 340 | DEBUG_PRINTF("⚠️ WARNING: Only found 1 active pin (GPIO %d)\n", candidateRxPins[0]); 341 | DEBUG_PRINTLN(" Expected 2 active RX pins (SUN2000 and DTSU)"); 342 | DEBUG_PRINTLN(" Check your connections and try again.\n"); 343 | } else { 344 | DEBUG_PRINTLN("❌ ERROR: No MODBUS traffic detected on any GPIO!"); 345 | DEBUG_PRINTLN(" Possible issues:"); 346 | DEBUG_PRINTLN(" - RS485 adapters not connected"); 347 | DEBUG_PRINTLN(" - SUN2000 not polling DTSU"); 348 | DEBUG_PRINTLN(" - Wrong baud rate or wiring"); 349 | DEBUG_PRINTLN(" - RS485 A/B lines swapped"); 350 | DEBUG_PRINTLN("\n⏸️ Halting - fix connections and restart.\n"); 351 | 352 | // Blink LED rapidly to indicate error 353 | while (true) { 354 | LED_ON(); 355 | delay(100); 356 | LED_OFF(); 357 | delay(100); 358 | } 359 | } 360 | } 361 | 362 | void loop() { 363 | ArduinoOTA.handle(); 364 | DEBUG_HANDLE(); // Handle telnet client connections 365 | vTaskDelay(100); // Faster loop for responsive telnet 366 | } -------------------------------------------------------------------------------- /Modbus_Proxy/src/dtsu666.cpp: -------------------------------------------------------------------------------- 1 | #include "dtsu666.h" 2 | #include "debug.h" 3 | 4 | // Register name mapping for debugging 5 | const char* dtsuRegisterNames[40] = { 6 | "I_L1", "I_L2", "I_L3", 7 | "U_LN_AVG", "U_L1N", "U_L2N", "U_L3N", 8 | "U_LL_AVG", "U_L1L2", "U_L2L3", "U_L3L1", "FREQ", 9 | "P_TOT(-)", "P_L1(-)", "P_L2(-)", "P_L3(-)", 10 | "Q_TOT", "Q_L1", "Q_L2", "Q_L3", 11 | "S_TOT", "S_L1", "S_L2", "S_L3", 12 | "PF_TOT", "PF_L1", "PF_L2", "PF_L3", 13 | "DMD_TOT(-)", "DMD_L1(-)", "DMD_L2(-)", "DMD_L3(-)", 14 | "E_IMP_T", "E_IMP_L1", "E_IMP_L2", "E_IMP_L3", 15 | "E_EXP_T", "E_EXP_L1", "E_EXP_L2", "E_EXP_L3" 16 | }; 17 | 18 | // Byte order definitions for IEEE 754 float parsing 19 | #define DTSU_BYTE_ORDER_ABCD 1 // Big Endian: A B C D (Most common) 20 | #define DTSU_BYTE_ORDER_DCBA 2 // Little Endian: D C B A 21 | #define DTSU_BYTE_ORDER_BADC 3 // Mid-Big Endian: B A D C 22 | #define DTSU_BYTE_ORDER_CDAB 4 // Mid-Little Endian: C D A B 23 | 24 | // Current byte order setting 25 | #define DTSU_CURRENT_ORDER DTSU_BYTE_ORDER_ABCD 26 | 27 | int16_t parseInt16(const uint8_t* data, size_t offset) { 28 | return (int16_t)((data[offset] << 8) | data[offset + 1]); 29 | } 30 | 31 | uint16_t parseUInt16(const uint8_t* data, size_t offset) { 32 | return (uint16_t)((data[offset] << 8) | data[offset + 1]); 33 | } 34 | 35 | float parseFloat32(const uint8_t* data, size_t offset) { 36 | union { 37 | uint32_t u; 38 | float f; 39 | } converter; 40 | 41 | #if DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_ABCD 42 | converter.u = ((uint32_t)data[offset] << 24) | 43 | ((uint32_t)data[offset + 1] << 16) | 44 | ((uint32_t)data[offset + 2] << 8) | 45 | ((uint32_t)data[offset + 3]); 46 | #elif DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_DCBA 47 | converter.u = ((uint32_t)data[offset + 3] << 24) | 48 | ((uint32_t)data[offset + 2] << 16) | 49 | ((uint32_t)data[offset + 1] << 8) | 50 | ((uint32_t)data[offset]); 51 | #elif DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_BADC 52 | converter.u = ((uint32_t)data[offset + 1] << 24) | 53 | ((uint32_t)data[offset] << 16) | 54 | ((uint32_t)data[offset + 3] << 8) | 55 | ((uint32_t)data[offset + 2]); 56 | #elif DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_CDAB 57 | converter.u = ((uint32_t)data[offset + 2] << 24) | 58 | ((uint32_t)data[offset + 3] << 16) | 59 | ((uint32_t)data[offset] << 8) | 60 | ((uint32_t)data[offset + 1]); 61 | #endif 62 | 63 | return converter.f; 64 | } 65 | 66 | void encodeFloat32(float value, uint8_t* data, size_t offset) { 67 | union { 68 | uint32_t u; 69 | float f; 70 | } converter; 71 | 72 | converter.f = value; 73 | 74 | #if DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_ABCD 75 | data[offset] = (converter.u >> 24) & 0xFF; 76 | data[offset + 1] = (converter.u >> 16) & 0xFF; 77 | data[offset + 2] = (converter.u >> 8) & 0xFF; 78 | data[offset + 3] = converter.u & 0xFF; 79 | #elif DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_DCBA 80 | data[offset] = converter.u & 0xFF; 81 | data[offset + 1] = (converter.u >> 8) & 0xFF; 82 | data[offset + 2] = (converter.u >> 16) & 0xFF; 83 | data[offset + 3] = (converter.u >> 24) & 0xFF; 84 | #elif DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_BADC 85 | data[offset] = (converter.u >> 16) & 0xFF; 86 | data[offset + 1] = (converter.u >> 24) & 0xFF; 87 | data[offset + 2] = converter.u & 0xFF; 88 | data[offset + 3] = (converter.u >> 8) & 0xFF; 89 | #elif DTSU_CURRENT_ORDER == DTSU_BYTE_ORDER_CDAB 90 | data[offset] = (converter.u >> 8) & 0xFF; 91 | data[offset + 1] = converter.u & 0xFF; 92 | data[offset + 2] = (converter.u >> 24) & 0xFF; 93 | data[offset + 3] = (converter.u >> 16) & 0xFF; 94 | #endif 95 | } 96 | 97 | bool parseDTSU666Data(uint16_t startAddr, const ModbusMessage& msg, DTSU666Data& data) { 98 | if (!msg.valid || msg.type != MBType::Reply || !msg.raw) { 99 | return false; 100 | } 101 | 102 | const uint8_t* payload = msg.raw + 3; 103 | uint8_t payloadSize = msg.raw[2]; 104 | 105 | if (payloadSize != 160) { 106 | return false; 107 | } 108 | 109 | const float volt_scale = 1.0f; 110 | const float amp_scale = 1.0f; 111 | const float power_scale = -1.0f; 112 | 113 | size_t offset = 0; 114 | 115 | data.current_L1 = parseFloat32(payload, offset) * amp_scale; offset += 4; 116 | data.current_L2 = parseFloat32(payload, offset) * amp_scale; offset += 4; 117 | data.current_L3 = parseFloat32(payload, offset) * amp_scale; offset += 4; 118 | 119 | data.voltage_LN_avg = parseFloat32(payload, offset) * volt_scale; offset += 4; 120 | data.voltage_L1N = parseFloat32(payload, offset) * volt_scale; offset += 4; 121 | data.voltage_L2N = parseFloat32(payload, offset) * volt_scale; offset += 4; 122 | data.voltage_L3N = parseFloat32(payload, offset) * volt_scale; offset += 4; 123 | 124 | data.voltage_LL_avg = parseFloat32(payload, offset) * volt_scale; offset += 4; 125 | data.voltage_L1L2 = parseFloat32(payload, offset) * volt_scale; offset += 4; 126 | data.voltage_L2L3 = parseFloat32(payload, offset) * volt_scale; offset += 4; 127 | data.voltage_L3L1 = parseFloat32(payload, offset) * volt_scale; offset += 4; 128 | data.frequency = parseFloat32(payload, offset); offset += 4; 129 | 130 | data.power_total = parseFloat32(payload, offset) * power_scale; offset += 4; 131 | data.power_L1 = parseFloat32(payload, offset) * power_scale; offset += 4; 132 | data.power_L2 = parseFloat32(payload, offset) * power_scale; offset += 4; 133 | data.power_L3 = parseFloat32(payload, offset) * power_scale; offset += 4; 134 | 135 | data.reactive_total = parseFloat32(payload, offset); offset += 4; 136 | data.reactive_L1 = parseFloat32(payload, offset); offset += 4; 137 | data.reactive_L2 = parseFloat32(payload, offset); offset += 4; 138 | data.reactive_L3 = parseFloat32(payload, offset); offset += 4; 139 | 140 | data.apparent_total = parseFloat32(payload, offset); offset += 4; 141 | data.apparent_L1 = parseFloat32(payload, offset); offset += 4; 142 | data.apparent_L2 = parseFloat32(payload, offset); offset += 4; 143 | data.apparent_L3 = parseFloat32(payload, offset); offset += 4; 144 | 145 | data.pf_total = parseFloat32(payload, offset); offset += 4; 146 | data.pf_L1 = parseFloat32(payload, offset); offset += 4; 147 | data.pf_L2 = parseFloat32(payload, offset); offset += 4; 148 | data.pf_L3 = parseFloat32(payload, offset); offset += 4; 149 | 150 | data.demand_total = parseFloat32(payload, offset) * power_scale; offset += 4; 151 | data.demand_L1 = parseFloat32(payload, offset) * power_scale; offset += 4; 152 | data.demand_L2 = parseFloat32(payload, offset) * power_scale; offset += 4; 153 | data.demand_L3 = parseFloat32(payload, offset) * power_scale; offset += 4; 154 | 155 | data.import_total = parseFloat32(payload, offset); offset += 4; 156 | data.import_L1 = parseFloat32(payload, offset); offset += 4; 157 | data.import_L2 = parseFloat32(payload, offset); offset += 4; 158 | data.import_L3 = parseFloat32(payload, offset); offset += 4; 159 | 160 | data.export_total = parseFloat32(payload, offset); offset += 4; 161 | data.export_L1 = parseFloat32(payload, offset); offset += 4; 162 | data.export_L2 = parseFloat32(payload, offset); offset += 4; 163 | data.export_L3 = parseFloat32(payload, offset); offset += 4; 164 | 165 | return true; 166 | } 167 | 168 | static inline uint16_t be16u(const uint8_t* p) { 169 | return (uint16_t(p[0]) << 8) | p[1]; 170 | } 171 | 172 | bool parseDTSU666MetaWords(uint16_t startAddr, const ModbusMessage& msg, DTSU666Meta& meta) { 173 | if (!msg.valid || msg.type != MBType::Reply || !msg.raw) return false; 174 | 175 | const uint8_t* payload = msg.raw + 3; 176 | size_t o = 0; 177 | 178 | if (startAddr == 2001 && msg.raw[2] >= 2) { 179 | meta.status = be16u(payload); 180 | return true; 181 | } 182 | 183 | if (startAddr == DTSU_VERSION_REG && msg.raw[2] >= 20) { 184 | meta.version = be16u(&payload[o]); o += 2; 185 | meta.passcode = be16u(&payload[o]); o += 2; 186 | meta.zero_clear_flag = be16u(&payload[o]); o += 2; 187 | meta.connection_mode = be16u(&payload[o]); o += 2; 188 | meta.irat = be16u(&payload[o]); o += 2; 189 | meta.urat = be16u(&payload[o]); o += 2; 190 | meta.protocol = be16u(&payload[o]); o += 2; 191 | meta.address = be16u(&payload[o]); o += 2; 192 | meta.baud = be16u(&payload[o]); o += 2; 193 | meta.meter_type = be16u(&payload[o]); 194 | return true; 195 | } 196 | 197 | return false; 198 | } 199 | 200 | bool encodeDTSU666Response(const DTSU666Data& data, uint8_t* buffer, size_t bufferSize) { 201 | if (bufferSize < 165) { 202 | return false; 203 | } 204 | 205 | buffer[0] = 0x0B; 206 | buffer[1] = 0x03; 207 | buffer[2] = 0xA0; 208 | 209 | size_t offset = 0; 210 | const float power_scale = -1.0f; 211 | 212 | encodeFloat32(data.current_L1, buffer + 3, offset); offset += 4; 213 | encodeFloat32(data.current_L2, buffer + 3, offset); offset += 4; 214 | encodeFloat32(data.current_L3, buffer + 3, offset); offset += 4; 215 | 216 | encodeFloat32(data.voltage_LN_avg, buffer + 3, offset); offset += 4; 217 | encodeFloat32(data.voltage_L1N, buffer + 3, offset); offset += 4; 218 | encodeFloat32(data.voltage_L2N, buffer + 3, offset); offset += 4; 219 | encodeFloat32(data.voltage_L3N, buffer + 3, offset); offset += 4; 220 | 221 | encodeFloat32(data.voltage_LL_avg, buffer + 3, offset); offset += 4; 222 | encodeFloat32(data.voltage_L1L2, buffer + 3, offset); offset += 4; 223 | encodeFloat32(data.voltage_L2L3, buffer + 3, offset); offset += 4; 224 | encodeFloat32(data.voltage_L3L1, buffer + 3, offset); offset += 4; 225 | encodeFloat32(data.frequency, buffer + 3, offset); offset += 4; 226 | 227 | encodeFloat32(data.power_total * power_scale, buffer + 3, offset); offset += 4; 228 | encodeFloat32(data.power_L1 * power_scale, buffer + 3, offset); offset += 4; 229 | encodeFloat32(data.power_L2 * power_scale, buffer + 3, offset); offset += 4; 230 | encodeFloat32(data.power_L3 * power_scale, buffer + 3, offset); offset += 4; 231 | 232 | encodeFloat32(data.reactive_total, buffer + 3, offset); offset += 4; 233 | encodeFloat32(data.reactive_L1, buffer + 3, offset); offset += 4; 234 | encodeFloat32(data.reactive_L2, buffer + 3, offset); offset += 4; 235 | encodeFloat32(data.reactive_L3, buffer + 3, offset); offset += 4; 236 | 237 | encodeFloat32(data.apparent_total, buffer + 3, offset); offset += 4; 238 | encodeFloat32(data.apparent_L1, buffer + 3, offset); offset += 4; 239 | encodeFloat32(data.apparent_L2, buffer + 3, offset); offset += 4; 240 | encodeFloat32(data.apparent_L3, buffer + 3, offset); offset += 4; 241 | 242 | encodeFloat32(data.pf_total, buffer + 3, offset); offset += 4; 243 | encodeFloat32(data.pf_L1, buffer + 3, offset); offset += 4; 244 | encodeFloat32(data.pf_L2, buffer + 3, offset); offset += 4; 245 | encodeFloat32(data.pf_L3, buffer + 3, offset); offset += 4; 246 | 247 | encodeFloat32(data.demand_total * power_scale, buffer + 3, offset); offset += 4; 248 | encodeFloat32(data.demand_L1 * power_scale, buffer + 3, offset); offset += 4; 249 | encodeFloat32(data.demand_L2 * power_scale, buffer + 3, offset); offset += 4; 250 | encodeFloat32(data.demand_L3 * power_scale, buffer + 3, offset); offset += 4; 251 | 252 | encodeFloat32(data.import_total, buffer + 3, offset); offset += 4; 253 | encodeFloat32(data.import_L1, buffer + 3, offset); offset += 4; 254 | encodeFloat32(data.import_L2, buffer + 3, offset); offset += 4; 255 | encodeFloat32(data.import_L3, buffer + 3, offset); offset += 4; 256 | 257 | encodeFloat32(data.export_total, buffer + 3, offset); offset += 4; 258 | encodeFloat32(data.export_L1, buffer + 3, offset); offset += 4; 259 | encodeFloat32(data.export_L2, buffer + 3, offset); offset += 4; 260 | encodeFloat32(data.export_L3, buffer + 3, offset); offset += 4; 261 | 262 | uint16_t crc = 0xFFFF; 263 | for (int i = 0; i < 163; i++) { 264 | uint8_t b = buffer[i]; 265 | crc ^= (uint16_t)b; 266 | for (int j = 0; j < 8; j++) { 267 | if (crc & 0x0001) { 268 | crc = (crc >> 1) ^ 0xA001; 269 | } else { 270 | crc >>= 1; 271 | } 272 | } 273 | } 274 | 275 | buffer[163] = crc & 0xFF; 276 | buffer[164] = (crc >> 8) & 0xFF; 277 | 278 | return true; 279 | } 280 | 281 | static inline uint16_t crc16_modbus(const uint8_t* p, size_t n) { 282 | uint16_t crc = 0xFFFF; 283 | for (size_t i = 0; i < n; i++) { 284 | crc ^= p[i]; 285 | for (int j = 0; j < 8; j++) { 286 | if (crc & 1) { 287 | crc = (crc >> 1) ^ 0xA001; 288 | } else { 289 | crc >>= 1; 290 | } 291 | } 292 | } 293 | return crc; 294 | } 295 | 296 | void printHexDump(const char* label, const uint8_t* buf, size_t len) { 297 | DEBUG_PRINTF(" %s [%zu]: ", label, len); 298 | for (size_t i = 0; i < len; i += 16) { 299 | DEBUG_PRINTF("\n "); 300 | for (size_t j = 0; j < 16 && (i+j) < len; ++j) DEBUG_PRINTF("%02X ", buf[i+j]); 301 | } 302 | DEBUG_PRINTLN(); 303 | } 304 | 305 | bool applyPowerCorrection(uint8_t* raw, uint16_t len, float correction) { 306 | if (!raw || len < 165) return false; 307 | 308 | uint8_t* payload = raw + 3; 309 | union { uint32_t u; float f; } converter; 310 | 311 | // 1. Correct phase powers (distribute wallbox power evenly across 3 phases) 312 | float wallboxPowerPerPhase = correction / 3.0f; 313 | 314 | // Power L1 at offset 13*4 = 52 315 | uint8_t* powerL1Bytes = payload + 52; 316 | converter.u = (uint32_t(powerL1Bytes[0]) << 24) | (uint32_t(powerL1Bytes[1]) << 16) | 317 | (uint32_t(powerL1Bytes[2]) << 8) | uint32_t(powerL1Bytes[3]); 318 | float correctedPowerL1 = converter.f + wallboxPowerPerPhase; 319 | converter.f = correctedPowerL1; 320 | powerL1Bytes[0] = (converter.u >> 24) & 0xFF; 321 | powerL1Bytes[1] = (converter.u >> 16) & 0xFF; 322 | powerL1Bytes[2] = (converter.u >> 8) & 0xFF; 323 | powerL1Bytes[3] = converter.u & 0xFF; 324 | 325 | // Power L2 at offset 14*4 = 56 326 | uint8_t* powerL2Bytes = payload + 56; 327 | converter.u = (uint32_t(powerL2Bytes[0]) << 24) | (uint32_t(powerL2Bytes[1]) << 16) | 328 | (uint32_t(powerL2Bytes[2]) << 8) | uint32_t(powerL2Bytes[3]); 329 | float correctedPowerL2 = converter.f + wallboxPowerPerPhase; 330 | converter.f = correctedPowerL2; 331 | powerL2Bytes[0] = (converter.u >> 24) & 0xFF; 332 | powerL2Bytes[1] = (converter.u >> 16) & 0xFF; 333 | powerL2Bytes[2] = (converter.u >> 8) & 0xFF; 334 | powerL2Bytes[3] = converter.u & 0xFF; 335 | 336 | // Power L3 at offset 15*4 = 60 337 | uint8_t* powerL3Bytes = payload + 60; 338 | converter.u = (uint32_t(powerL3Bytes[0]) << 24) | (uint32_t(powerL3Bytes[1]) << 16) | 339 | (uint32_t(powerL3Bytes[2]) << 8) | uint32_t(powerL3Bytes[3]); 340 | float correctedPowerL3 = converter.f + wallboxPowerPerPhase; 341 | converter.f = correctedPowerL3; 342 | powerL3Bytes[0] = (converter.u >> 24) & 0xFF; 343 | powerL3Bytes[1] = (converter.u >> 16) & 0xFF; 344 | powerL3Bytes[2] = (converter.u >> 8) & 0xFF; 345 | powerL3Bytes[3] = converter.u & 0xFF; 346 | 347 | // 2. Correct total power (add wallbox power) 348 | size_t totalPowerOffset = 12 * 4; 349 | uint8_t* powerBytes = payload + totalPowerOffset; 350 | converter.u = (uint32_t(powerBytes[0]) << 24) | (uint32_t(powerBytes[1]) << 16) | 351 | (uint32_t(powerBytes[2]) << 8) | uint32_t(powerBytes[3]); 352 | 353 | float originalPower = converter.f; 354 | float correctedPower = originalPower + correction; // ADD wallbox power (back to addition) 355 | converter.f = correctedPower; 356 | 357 | powerBytes[0] = (converter.u >> 24) & 0xFF; 358 | powerBytes[1] = (converter.u >> 16) & 0xFF; 359 | powerBytes[2] = (converter.u >> 8) & 0xFF; 360 | powerBytes[3] = converter.u & 0xFF; 361 | 362 | // 3. Correct phase demands (distribute wallbox power evenly across 3 phases) 363 | // Demand L1 at offset 29*4 = 116 364 | uint8_t* demandL1Bytes = payload + 116; 365 | converter.u = (uint32_t(demandL1Bytes[0]) << 24) | (uint32_t(demandL1Bytes[1]) << 16) | 366 | (uint32_t(demandL1Bytes[2]) << 8) | uint32_t(demandL1Bytes[3]); 367 | float correctedDemandL1 = converter.f + wallboxPowerPerPhase; 368 | converter.f = correctedDemandL1; 369 | demandL1Bytes[0] = (converter.u >> 24) & 0xFF; 370 | demandL1Bytes[1] = (converter.u >> 16) & 0xFF; 371 | demandL1Bytes[2] = (converter.u >> 8) & 0xFF; 372 | demandL1Bytes[3] = converter.u & 0xFF; 373 | 374 | // Demand L2 at offset 30*4 = 120 375 | uint8_t* demandL2Bytes = payload + 120; 376 | converter.u = (uint32_t(demandL2Bytes[0]) << 24) | (uint32_t(demandL2Bytes[1]) << 16) | 377 | (uint32_t(demandL2Bytes[2]) << 8) | uint32_t(demandL2Bytes[3]); 378 | float correctedDemandL2 = converter.f + wallboxPowerPerPhase; 379 | converter.f = correctedDemandL2; 380 | demandL2Bytes[0] = (converter.u >> 24) & 0xFF; 381 | demandL2Bytes[1] = (converter.u >> 16) & 0xFF; 382 | demandL2Bytes[2] = (converter.u >> 8) & 0xFF; 383 | demandL2Bytes[3] = converter.u & 0xFF; 384 | 385 | // Demand L3 at offset 31*4 = 124 386 | uint8_t* demandL3Bytes = payload + 124; 387 | converter.u = (uint32_t(demandL3Bytes[0]) << 24) | (uint32_t(demandL3Bytes[1]) << 16) | 388 | (uint32_t(demandL3Bytes[2]) << 8) | uint32_t(demandL3Bytes[3]); 389 | float correctedDemandL3 = converter.f + wallboxPowerPerPhase; 390 | converter.f = correctedDemandL3; 391 | demandL3Bytes[0] = (converter.u >> 24) & 0xFF; 392 | demandL3Bytes[1] = (converter.u >> 16) & 0xFF; 393 | demandL3Bytes[2] = (converter.u >> 8) & 0xFF; 394 | demandL3Bytes[3] = converter.u & 0xFF; 395 | 396 | // 4. Correct total demand (add wallbox power) 397 | size_t demandTotalOffset = 28 * 4; 398 | uint8_t* demandBytes = payload + demandTotalOffset; 399 | converter.u = (uint32_t(demandBytes[0]) << 24) | (uint32_t(demandBytes[1]) << 16) | 400 | (uint32_t(demandBytes[2]) << 8) | uint32_t(demandBytes[3]); 401 | 402 | float originalDemand = converter.f; 403 | float correctedDemand = originalDemand + correction; // ADD wallbox power (back to addition) 404 | converter.f = correctedDemand; 405 | 406 | demandBytes[0] = (converter.u >> 24) & 0xFF; 407 | demandBytes[1] = (converter.u >> 16) & 0xFF; 408 | demandBytes[2] = (converter.u >> 8) & 0xFF; 409 | demandBytes[3] = converter.u & 0xFF; 410 | 411 | // 5. Recalculate CRC after all modifications 412 | uint16_t newCrc = ModbusRTU485::crc16(raw, len - 2); 413 | raw[len - 2] = newCrc & 0xFF; 414 | raw[len - 1] = (newCrc >> 8) & 0xFF; 415 | 416 | return true; 417 | } 418 | 419 | bool parseDTSU666Response(const uint8_t* raw, uint16_t len, DTSU666Data& data) { 420 | if (!raw || len < 165) return false; 421 | 422 | const uint8_t* payload = raw + 3; 423 | 424 | auto parseFloat = [&](size_t offset) -> float { 425 | const uint8_t* bytes = payload + offset; 426 | uint32_t bits = (uint32_t(bytes[0]) << 24) | (uint32_t(bytes[1]) << 16) | 427 | (uint32_t(bytes[2]) << 8) | uint32_t(bytes[3]); 428 | union { uint32_t u; float f; } converter; 429 | converter.u = bits; 430 | return converter.f; 431 | }; 432 | 433 | data.current_L1 = parseFloat(0); 434 | data.current_L2 = parseFloat(4); 435 | data.current_L3 = parseFloat(8); 436 | 437 | data.voltage_LN_avg = parseFloat(12); 438 | data.voltage_L1N = parseFloat(16); 439 | data.voltage_L2N = parseFloat(20); 440 | data.voltage_L3N = parseFloat(24); 441 | 442 | data.voltage_LL_avg = parseFloat(28); 443 | data.voltage_L1L2 = parseFloat(32); 444 | data.voltage_L2L3 = parseFloat(36); 445 | data.voltage_L3L1 = parseFloat(40); 446 | data.frequency = parseFloat(44); 447 | 448 | data.power_total = parseFloat(48); 449 | data.power_L1 = parseFloat(52); 450 | data.power_L2 = parseFloat(56); 451 | data.power_L3 = parseFloat(60); 452 | 453 | return true; 454 | } --------------------------------------------------------------------------------