/* * ESP32 Modbus RTU zu TCP Gateway für Avarma Wärmepumpe * * v14 * Hardware: * - ESP32 DevKit * - MAX485 RS485 Interface * - GPIO16 (RX2), GPIO17 (TX2), GPIO4 (DE/RE) */ #include #include #include // WiFi Konfiguration - HIER ANPASSEN! const char* ssid = "xxx"; const char* password = "xxx"; // Modbus Konfiguration #define MAX485_DE_RE 4 // GPIO4 für Direction Control #define MODBUS_SLAVE_ID 1 // Slave ID der Wärmepumpe #define MODBUS_BAUDRATE 9600 #define MODBUS_TIMEOUT 2000 // Avarma Register Definitionen (erweitert) #define REG_AC_HEATING_TEMP 4098 // P2: A/C Heating Temperature Setting #define REG_INDOOR_TEMP 4101 // P5: Indoor Temperature Setting #define REG_COIL_TEMP 4358 // C00: Coil temp (Verdampfer) #define REG_DISCHARGE_TEMP 4359 // C01: Discharge temp (Heißgas) #define REG_AMBIENT_TEMP 4360 // C02: Ambient temp (Außentemperatur) #define REG_WATER_INLET_TEMP 4365 // C07: Water inlet temperature #define REG_WATER_OUTLET_TEMP 4366 // C08: Water outlet temperature #define REG_MAIN_CIRC_TEMP 4369 // C11: Main circulation temperature #define REG_HIGH_PRESSURE 4371 // C13: High pressure #define REG_LOW_PRESSURE 4372 // C14: Low pressure #define REG_COMPRESSOR_FREQ 4373 // C15: Compressor running frequency #define REG_FAN_MOTOR1 4374 // C16: Fan motor 1 #define REG_FAN_MOTOR2 4375 // C17: Fan motor 2 #define REG_COMPRESSOR_TARGET 4378 // C20: Compressor target frequency #define REG_COMPRESSOR_CURRENT 4379 // C21: Compressor input current #define REG_ROOM_TEMP_T2 4384 // C26: Room Temperature (T2) #define REG_EVAPORATOR_TEMP 4385 // C27: Evaporator temperature #define REG_CONDENSER_TEMP 4386 // C28: Condenser temperature #define REG_OPERATING_STATUS 4354 // Operating status #define REG_WATER_PUMP_SPEED 4611 // C51: Water pump speed #define REG_RUNNING_MODE 4612 // C52: Running mode // Neue Kompressor Parameter #define REG_COMP_DOWN_FREQ 8233 // P43: Compressor down frequency #define REG_COMP_SHUTDOWN_CURRENT 8234 // P44: Compressor shutdown current #define REG_COMP_RESPONSE 8235 // P45: Compressor response #define REG_DC_FAN_MAX_SPEED 8250 // P60: DC fan maximum speed #define REG_MIN_WATER_FLOW 8251 // P61: Minimum water flow #define REG_WATER_FLOW 4368 // C22: Water flow #define REG_COMP_TOTAL_RUN_TIME 4608 // C50: Compressor total running times #define REG_COMP_CURRENT_RUN_TIME 4610 // C51: Compressor current running time #define REG_COMP_MIN_FREQ 8236 // P46: Compressor minimum frequency #define REG_COMP_DEFROST_FREQ 8237 // P47: Compressor defrost frequency // TCP Server WiFiServer tcpServer(502); // Standard Modbus TCP Port // Modbus Master ModbusMaster node; HardwareSerial ModbusSerial(2); // UART2 für RS485 // Status Variables bool wifiConnected = false; unsigned long lastModbusRead = 0; // Modbus Register Cache struct ModbusData { // Temperaturen float ambient_temp; float water_inlet_temp; float water_outlet_temp; float main_circulation_temp; float room_temp_t2; float coil_temp; float discharge_temp; float evaporator_temp; float condenser_temp; // Drücke float high_pressure; float low_pressure; // Betriebsdaten uint16_t compressor_freq; uint16_t fan_motor1_speed; uint16_t fan_motor2_speed; uint16_t compressor_target_freq; float compressor_current; uint16_t water_pump_speed; uint16_t running_mode; uint16_t operating_status; uint16_t dc_fan_max_speed; // Sollwerte float indoor_temp_setting; float ac_heating_temp_setting; // Kompressor Parameter float comp_down_freq; float comp_shutdown_current; uint16_t comp_response; float minimum_water_flow; float water_flow; // C22: Water flow uint16_t comp_total_run_time; // C50: Compressor total running times uint16_t comp_current_run_time; // C51: Compressor current running time uint16_t comp_min_freq; // P46: Compressor minimum frequency uint16_t comp_defrost_freq; // P47: Compressor defrost frequency } wpData; void setup() { Serial.begin(115200); Serial.println("ESP32 Modbus Gateway für Avarma WP - Vereinfacht"); // RS485 Direction Control Pin pinMode(MAX485_DE_RE, OUTPUT); digitalWrite(MAX485_DE_RE, LOW); // Receive Mode // Modbus Serial initialisieren ModbusSerial.begin(MODBUS_BAUDRATE, SERIAL_8N1, 16, 17); // RX=GPIO16, TX=GPIO17 // Modbus Master konfigurieren node.begin(MODBUS_SLAVE_ID, ModbusSerial); node.preTransmission(preTransmission); node.postTransmission(postTransmission); // WiFi verbinden connectWiFi(); // TCP Server starten tcpServer.begin(); Serial.println("Modbus TCP Server gestartet auf Port 502"); Serial.println("Setup abgeschlossen!"); Serial.println("Keine Web-Oberfläche - nur Modbus TCP Gateway"); } void loop() { // WiFi Status prüfen if (WiFi.status() != WL_CONNECTED) { if (wifiConnected) { Serial.println("WiFi Verbindung verloren!"); wifiConnected = false; } connectWiFi(); } // TCP Clients behandeln handleTCPClients(); // Periodisches Lesen der Modbus Daten (alle 10 Sekunden) if (millis() - lastModbusRead > 10000) { readModbusData(); lastModbusRead = millis(); } delay(100); } void connectWiFi() { if (WiFi.status() == WL_CONNECTED) return; WiFi.begin(ssid, password); Serial.print("Verbinde mit WiFi"); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); Serial.print("."); attempts++; } if (WiFi.status() == WL_CONNECTED) { wifiConnected = true; Serial.println(); Serial.print("WiFi verbunden! IP: "); Serial.println(WiFi.localIP()); Serial.println("Modbus TCP verfügbar auf Port 502"); } else { Serial.println("\nWiFi Verbindung fehlgeschlagen!"); delay(5000); } } void preTransmission() { digitalWrite(MAX485_DE_RE, HIGH); // Transmit Mode } void postTransmission() { digitalWrite(MAX485_DE_RE, LOW); // Receive Mode } void handleTCPClients() { WiFiClient client = tcpServer.available(); if (client) { Serial.println("Neuer TCP Client verbunden"); while (client.connected()) { if (client.available()) { // Modbus TCP Request lesen uint8_t request[256]; int requestLength = client.available(); client.readBytes(request, requestLength); // Modbus TCP zu RTU konvertieren und weiterleiten uint8_t response[256]; int responseLength = processModbusRequest(request, requestLength, response); if (responseLength > 0) { client.write(response, responseLength); } } delay(10); } client.stop(); Serial.println("TCP Client getrennt"); } } int processModbusRequest(uint8_t* request, int requestLength, uint8_t* response) { // Vereinfachte Modbus TCP zu RTU Verarbeitung if (requestLength < 8) return 0; // Extrahiere Function Code und Register uint8_t functionCode = request[7]; uint16_t startRegister = (request[8] << 8) | request[9]; Serial.printf("Modbus Request: FC=%d, Start=%d\n", functionCode, startRegister); // Function Code 3: Read Holding Registers if (functionCode == 3) { uint16_t numRegisters = (request[10] << 8) | request[11]; Serial.printf(", Count=%d\n", numRegisters); return handleReadHoldingRegisters(startRegister, numRegisters, response, request); } // Function Code 6: Write Single Register else if (functionCode == 6) { uint16_t value = (request[10] << 8) | request[11]; Serial.printf(", Value=%d\n", value); return handleWriteSingleRegister(startRegister, value, response, request); } // Function Code 16: Write Multiple Registers else if (functionCode == 16) { uint16_t numRegisters = (request[10] << 8) | request[11]; Serial.printf(", Count=%d\n", numRegisters); return handleWriteMultipleRegisters(startRegister, numRegisters, response, request); } else { Serial.printf(" - Unsupported Function Code!\n"); return 0; } } int handleWriteSingleRegister(uint16_t addr, uint16_t value, uint8_t* response, uint8_t* request) { Serial.printf("→ Schreibe Register %d = %d\n", addr, value); // Modbus RTU Write an Wärmepumpe senden uint8_t result = node.writeSingleRegister(addr, value); Serial.printf("← Write Result: %d\n", result); if (result == node.ku8MBSuccess) { // Erfolgreiche Write Response memcpy(response, request, 12); // Komplette Anfrage als Antwort Serial.printf("✓ Register %d erfolgreich geschrieben\n", addr); return 12; } else { Serial.printf("✗ Write Fehler: %d - ", result); printModbusError(result); // Fehler-Response memcpy(response, request, 6); response[4] = 0; response[5] = 3; response[6] = request[6]; response[7] = request[7] | 0x80; // Error flag response[8] = 0x02; // Exception: Illegal data address return 9; } } int handleWriteMultipleRegisters(uint16_t startAddr, uint16_t count, uint8_t* response, uint8_t* request) { Serial.printf("→ Schreibe %d Register ab %d\n", count, startAddr); // Daten aus Request extrahieren uint8_t byteCount = request[12]; // Werte in den Transmit Buffer der ModbusMaster Bibliothek laden for (int i = 0; i < count; i++) { uint16_t value = (request[13 + (i * 2)] << 8) | request[14 + (i * 2)]; node.setTransmitBuffer(i, value); Serial.printf(" Register %d = %d\n", startAddr + i, value); } // ModbusMaster API: setTransmitBuffer() dann writeMultipleRegisters() uint8_t result = node.writeMultipleRegisters(startAddr, count); Serial.printf("← Write Multiple Result: %d\n", result); if (result == node.ku8MBSuccess) { // Write Multiple Response memcpy(response, request, 6); response[4] = 0; response[5] = 6; response[6] = request[6]; response[7] = 16; // Function code response[8] = request[8]; // Start address high response[9] = request[9]; // Start address low response[10] = request[10]; // Count high response[11] = request[11]; // Count low Serial.printf("✓ %d Register erfolgreich geschrieben\n", count); return 12; } else { Serial.printf("✗ Write Multiple Fehler: %d - ", result); printModbusError(result); // Fehler-Response memcpy(response, request, 6); response[4] = 0; response[5] = 3; response[6] = request[6]; response[7] = request[7] | 0x80; response[8] = 0x02; return 9; } } int handleReadHoldingRegisters(uint16_t startAddr, uint16_t count, uint8_t* response, uint8_t* request) { // Modbus RTU Request an Wärmepumpe senden Serial.printf("→ Sende RTU Request: Addr=%d, Count=%d\n", startAddr, count); // Warten vor dem Request (wichtig für Timing!) delay(50); uint8_t result = node.readHoldingRegisters(startAddr, count); Serial.printf("← RTU Result: %d\n", result); if (result == node.ku8MBSuccess) { // TCP Header von Original Request kopieren memcpy(response, request, 6); // Länge anpassen int dataLength = 3 + (count * 2); response[4] = 0; response[5] = dataLength; // Unit ID, Function Code, Byte Count response[6] = request[6]; response[7] = 3; response[8] = count * 2; // Daten kopieren und anzeigen for (int i = 0; i < count; i++) { uint16_t value = node.getResponseBuffer(i); response[9 + (i * 2)] = (value >> 8) & 0xFF; response[10 + (i * 2)] = value & 0xFF; Serial.printf(" Register %d: Raw=%d, Bytes=[0x%02X, 0x%02X]\n", startAddr + i, value, response[9 + (i * 2)], response[10 + (i * 2)]); } int responseLength = 9 + (count * 2); Serial.printf("→ Sende TCP Response: %d bytes\n", responseLength); return responseLength; } else { Serial.printf("✗ RTU Fehler: %d - ", result); printModbusError(result); // Bei Fehler eine Fehler-Response senden memcpy(response, request, 6); response[4] = 0; response[5] = 3; // Error response length response[6] = request[6]; // Unit ID response[7] = request[7] | 0x80; // Function code + error flag response[8] = 0x02; // Exception code: Illegal data address return 9; } } void printModbusError(uint8_t result) { switch (result) { case node.ku8MBIllegalFunction: Serial.println("Illegal Function"); break; case node.ku8MBIllegalDataAddress: Serial.println("Illegal Data Address - Register existiert nicht!"); break; case node.ku8MBIllegalDataValue: Serial.println("Illegal Data Value"); break; case node.ku8MBSlaveDeviceFailure: Serial.println("Slave Device Failure"); break; case node.ku8MBInvalidSlaveID: Serial.println("Invalid Slave ID"); break; case node.ku8MBInvalidFunction: Serial.println("Invalid Function"); break; case node.ku8MBResponseTimedOut: Serial.println("Response Timed Out - WP antwortet nicht!"); break; case node.ku8MBInvalidCRC: Serial.println("Invalid CRC"); break; default: Serial.printf("Unknown Error (%d)\n", result); break; } } void readModbusData() { uint8_t result; Serial.println("=== Lese Avarma WP Daten ==="); // Außentemperatur (C02) result = node.readHoldingRegisters(REG_AMBIENT_TEMP, 1); if (result == node.ku8MBSuccess) { wpData.ambient_temp = (int16_t)node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Außentemp: %.1f°C\n", wpData.ambient_temp); } delay(100); // Wassereintrittstemperatur (C07) result = node.readHoldingRegisters(REG_WATER_INLET_TEMP, 1); if (result == node.ku8MBSuccess) { wpData.water_inlet_temp = (int16_t)node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Wasser Eintritt: %.1f°C\n", wpData.water_inlet_temp); } delay(100); // Wasseraustrittstemperatur (C08) result = node.readHoldingRegisters(REG_WATER_OUTLET_TEMP, 1); if (result == node.ku8MBSuccess) { wpData.water_outlet_temp = (int16_t)node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Wasser Austritt: %.1f°C\n", wpData.water_outlet_temp); } delay(100); // Raumtemperatur T2 (C26) result = node.readHoldingRegisters(REG_ROOM_TEMP_T2, 1); if (result == node.ku8MBSuccess) { wpData.room_temp_t2 = (int16_t)node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Raumtemp: %.1f°C\n", wpData.room_temp_t2); } delay(100); // Coil Temperatur (C00) result = node.readHoldingRegisters(REG_COIL_TEMP, 1); if (result == node.ku8MBSuccess) { wpData.coil_temp = (int16_t)node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Coil Temp: %.1f°C\n", wpData.coil_temp); } delay(100); // Discharge Temperatur (C01) result = node.readHoldingRegisters(REG_DISCHARGE_TEMP, 1); if (result == node.ku8MBSuccess) { wpData.discharge_temp = (int16_t)node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Discharge Temp: %.1f°C\n", wpData.discharge_temp); } delay(100); // Kompressor-Frequenz (C15) result = node.readHoldingRegisters(REG_COMPRESSOR_FREQ, 1); if (result == node.ku8MBSuccess) { wpData.compressor_freq = node.getResponseBuffer(0); Serial.printf("✓ Kompressor: %d Hz\n", wpData.compressor_freq); } delay(100); // Kompressor-Strom (C21) result = node.readHoldingRegisters(REG_COMPRESSOR_CURRENT, 1); if (result == node.ku8MBSuccess) { wpData.compressor_current = node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Kompressor Strom: %.1f A\n", wpData.compressor_current); } delay(100); // Wasserpumpen-Geschwindigkeit (C51) result = node.readHoldingRegisters(REG_WATER_PUMP_SPEED, 1); if (result == node.ku8MBSuccess) { wpData.water_pump_speed = node.getResponseBuffer(0); Serial.printf("✓ Wasserpumpe: %d%%\n", wpData.water_pump_speed); } delay(100); // Betriebsmodus (C52) result = node.readHoldingRegisters(REG_RUNNING_MODE, 1); if (result == node.ku8MBSuccess) { wpData.running_mode = node.getResponseBuffer(0); String mode = (wpData.running_mode == 0) ? "Aus" : (wpData.running_mode == 1) ? "Warmwasser" : (wpData.running_mode == 2) ? "Heizen" : (wpData.running_mode == 3) ? "Heizen+WW" : "Unbekannt"; Serial.printf("✓ Betriebsmodus: %d (%s)\n", wpData.running_mode, mode.c_str()); } delay(100); // Raumtemperatur-Sollwert (P5) result = node.readHoldingRegisters(REG_INDOOR_TEMP, 1); if (result == node.ku8MBSuccess) { wpData.indoor_temp_setting = (int16_t)node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Raum-Sollwert: %.1f°C\n", wpData.indoor_temp_setting); } delay(100); // Neue Parameter testen // DC Fan Max Speed (P60) result = node.readHoldingRegisters(REG_DC_FAN_MAX_SPEED, 1); if (result == node.ku8MBSuccess) { wpData.dc_fan_max_speed = node.getResponseBuffer(0); Serial.printf("✓ DC Fan Max: %d RPM\n", wpData.dc_fan_max_speed); } delay(100); // Kompressor Down Freq (P43) result = node.readHoldingRegisters(REG_COMP_DOWN_FREQ, 1); if (result == node.ku8MBSuccess) { wpData.comp_down_freq = node.getResponseBuffer(0) / 10.0; Serial.printf("✓ Comp Down Freq: %.1f A\n", wpData.comp_down_freq); } delay(100); Serial.println("=== Status Zusammenfassung ==="); Serial.printf("🏠 Außen: %.1f°C | Raum: %.1f°C (Soll: %.1f°C)\n", wpData.ambient_temp, wpData.room_temp_t2, wpData.indoor_temp_setting); Serial.printf("💧 Wasser: %.1f°C → %.1f°C (ΔT: %.1fK)\n", wpData.water_inlet_temp, wpData.water_outlet_temp, wpData.water_outlet_temp - wpData.water_inlet_temp); Serial.printf("⚡ Kompressor: %d Hz, %.1f A | Pumpe: %d%% | Modus: %d\n", wpData.compressor_freq, wpData.compressor_current, wpData.water_pump_speed, wpData.running_mode); Serial.printf("🌡️ Coil: %.1f°C | Discharge: %.1f°C\n", wpData.coil_temp, wpData.discharge_temp); Serial.println("============================="); }