Pool heatpump device communication

Hello

Inspired by several post and Github projects I am trying to achieve my Poolex pool heatpump control via an ESP 32. Even if i am quitye a noob in serial communication i wanted to try to go as deep as i can in this topic :slight_smile:

The hardware :

Heatpump controler

The UART parser/ESP32 setup

As you may notice in the picture, the Heatpump is comunicating with the remote controller thru 2 wires : Green & Yellow

I did connect a MAX485 module to sniff frames

Serial analysis

Thanks to glcem UART parser, i was able to analyse the frame structure, the conclusion is that my frames are 80 octets long, good start :slight_smile:

I also did use Saleae Logic2 serial anaylser to extract frames and perform the analysis with excel

Even if it may look like MODUS communication protocol, it is not

Seems to be a traditionnal serial communication at 9600 baud, 8N1

Here is a look of green wire signal frames

The strange thing for me is about the yellow one, it’s coming with many many frames errors, i decided to ignore it for now

Frames structure

As we can see in the Logic extract, frames are starting with 0xD2, 30ms after 0xCC is coming and then 300ms after 0xDD is poping up

After many many extracts and excel analysis i was able to retreive some interesting informations :

  • The 10 first octets of 0xD2 and 0xCC begining frames are ALWAYS the same :
Octet1 Octet2 Octet3 Octet4 Octet5 Octet6 Octet7 Octet8 Octet9 Octet10 Octet11
0xD2 0x5B 0x00 0x01 0x01 0x2D 0x08 0x23 0x1E 0x34 0x05
Octet1 Octet2 Octet3 Octet4 Octet5 Octet6 Octet7 Octet8 Octet9 Octet10 Octet11
0xCC 0x5B 0x00 0x01 0x01 0x2D 0x08 0x23 0x1E 0x34 0x05
  • Octet 12 is coding the target temperature in °C

  • Pressing a button on the headpump remote control make 0xCC begining frames swith to 0xCD with the same 10 first octets :

    Octet1 Octet2 Octet3 Octet4 Octet5 Octet6 Octet7 Octet8 Octet9 Octet10 Octet11
    0xCD 0x5B 0x00 0x01 0x01 0x2D 0x08 0x23 0x1E 0x34 0x05

I am quite happy but a little bit stuck at this point :slight_smile:

How can i pursue the analysis ?

How can i test to send a serial sequence with the ESP32 to have a look and see if the heatpump is changing set temperature ?

Thanks for any advice and help :smiley:

Poolex Logic analysis V3 August Only.zip (2.4 MB)

Please read the forum guide in the sticky post, so you know how to post code on this forum, and other important guidance.

Hello camariad_popof

Simply use a terminal programme to carry out a further interface analysis.

looking at the photo in post 1 it looks like you have the A and B outputs of the RS485 SP3485 Breakout module connected to the ESP8266 Rx and TX

1 Like

Can you explain what's going on on that image.
Looks like you have rs485 differential lines connected to esp uart pins?

Hello

Thanks for your interest in my topic !

Yes i used this Github repo indication to connect the Max485 module

This was essentially to sniff frames and use glcem repo

The frame analysis has been done thru Logic

Fyi At this stage i didnt write any code (I’m not a C++ specialist), working on

you did not explain why you have connected the A and B outputs of the RS485 SP3485 Breakout module to the ESP8266 Rx and TX
by doing so you may have damaged the SP3485 module and/or the ESP8266

are you sure the heatpump controller communicated using RS485

I suggest you to read it again. It clearly instructs to connect RS485 lines to the converter not to Esp.

1 Like

Thanks horace

Here are the pictures of the remote controller with the Ti 3082 RS485 transceiver chip

I just discovered there is a wifi module, will investigate that also

I suggest you to read it again. It clearly instructs to connect RS485 lines to the converter not to Esp.

Ok i will review the wiring

If you are handy with soldering iron, this approach could be interesting option:

Yes I will try seems not the same WiFi module, i’m not able to identify it at this point

let you know

thanks !

Hello gentlemen

Here are some updates ragarding the project

  1. I did wire correctly (i think) the Rs485 module to the Esp8266 :

  1. I did code a script to capture frames and open a telnet connection (originaly with french comments, did ask Gemini to translate it in english)

    #include <Arduino.h> // Includes the Arduino core library for basic functions.
    #include <ESP8266WiFi.h> // Includes the ESP8266 WiFi library for connecting to a wireless network.
    #include <SoftwareSerial.h> // Includes the SoftwareSerial library for creating serial ports on digital pins.
    #include "secrets.h" // Includes a file containing your WiFi credentials (SSID and password).  This is good practice for security!
    
    // --- RS485 Configuration ---
    #define MAX485_RXD D4 // Defines the digital pin connected to the RX (Receive) data line of the MAX485 transceiver.
    #define MAX485_TXD D1 // Defines the digital pin connected to the TX (Transmit) data line of the MAX485 transceiver.
    SoftwareSerial rs485(MAX485_RXD, MAX485_TXD); // Creates a SoftwareSerial object named 'rs485' using the defined RX and TX pins.  This allows you to communicate over RS485.
    
    // --- Parameters (Constants) ---
    const int FRAME_SIZE = 80; // Defines the maximum size of the received frames in bytes.
    const unsigned long SHORT_DELAY = 30;    // Milliseconds (timeout for 0xD2/0xCC/0xCD frame reception).  If a byte isn't received within this time, it's considered a timeout.
    const unsigned long LONG_DELAY = 400;     // Milliseconds (timeout for 0xDD frame reception). Longer timeout for frames that are expected to be larger.
    const unsigned long LOG_DURATION = 120000; // 2 minutes in milliseconds.  The duration of the logging period.
    const int MAX_LOGS = 100;                 // Maximum number of logs stored in memory.
    
    // --- Telnet Server ---
    WiFiServer telnetServer(23); // Creates a WiFiServer object listening on port 23 (the standard Telnet port).
    WiFiClient telnetClient;     // Creates a WiFiClient object to handle connections from the Telnet client.
    
    // --- Global Variables ---
    byte frameBuffer[FRAME_SIZE]; // An array to store the received data of an RS485 frame.
    int frameIndex = 0;          // Index used to track the current position within the 'frameBuffer' array.
    bool inFrame = false;         // A flag indicating whether a frame is currently being received.
    unsigned long lastByteTime = 0; // Stores the timestamp of the last byte received, used for timeout detection.
    byte currentHeader = 0;       // Stores the header byte (e.g., 0xD2, 0xCC) of the current frame.
    bool pauseDisplay = false;    // A flag to control whether the display of incoming frames is paused or not.
    
    // --- Log Entry Structure ---
    struct LogEntry { // Defines a structure to hold information about each log entry.
      String message;      // The text message of the log entry.
      unsigned long timestamp; // The time when the log entry was recorded.
      const char* color;   // ANSI escape code for coloring the log message (e.g., blue, green).
    };
    
    LogEntry logBuffer[MAX_LOGS]; // An array to store the log entries.
    int logIndex = 0;             // Index used to track the current position within the 'logBuffer' array.
    unsigned long logStartTime = 0;  // Stores the timestamp when logging started.
    bool isLogging = false;        // A flag indicating whether logging is currently active.
    
    // --- ANSI Color Codes ---
    const char* ANSI_RESET = "\033[0m"; // Reset color to default.
    const char* ANSI_BLUE = "\033[34m";  // Blue text.
    const char* ANSI_GREEN = "\033[32m"; // Green text.
    const char* ANSI_YELLOW = "\033[33m"; // Yellow text.
    const char* ANSI_RED = "\033[31m";   // Red text.
    const char* ANSI_CYAN = "\033[36m";  // Cyan text.
    
    // --- Function Prototypes (Declarations) ---
    void logMessage(const String &message, const char* color = ANSI_RESET); // Function to display messages with optional coloring.
    void sendRS485Frame(const byte *frame, int size);                         // Function to transmit an RS485 frame.
    int decodeTemperature(byte tempByte);                                      // Function to decode the temperature value from a received byte.
    void startLogging();                                                       // Function to begin recording logs.
    void dumpLogs();                                                           // Function to display the recorded logs.
    
    void setup() { // This function runs once at the beginning of the program.
      Serial.begin(9600); // Initializes serial communication with the computer at 9600 baud rate for debugging and Telnet interaction.
      rs485.begin(9600);  // Initializes serial communication with the RS485 device at 9600 baud rate.
    
      // Connect to WiFi
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD); // Connects to your WiFi network using the SSID and password from the "secrets.h" file.
      Serial.print("Connecting to WiFi...");
      while (WiFi.status() != WL_CONNECTED) { // Loops until a connection is established.
        delay(500);
        Serial.print(".");
      }
      Serial.println("\nWiFi connected!"); // Prints a success message when the WiFi connection is established.
      Serial.print("IP address: ");
      Serial.println(WiFi.localIP());       // Prints the IP address assigned to your ESP8266 device.
    
      // Start Telnet Server
      telnetServer.begin();                 // Starts the Telnet server, listening for incoming connections on port 23.
      logMessage("\n[RS485 Monitor - Compact format with temperature analysis and logs]", ANSI_GREEN); // Displays a welcome message in green.
      logMessage("IP: " + WiFi.localIP().toString(), ANSI_GREEN); // Prints the IP address of the ESP8266 device.
      logMessage("Type 'help' for help.", ANSI_GREEN); // Informs the user how to get help.
    }
    
    void loop() { // This function runs repeatedly after setup().
      // --- Telnet Connection Management ---
      if (!telnetClient.connected()) { // Checks if a client is currently connected to the Telnet server.
        telnetClient = telnetServer.available(); // If not, it tries to accept an incoming connection.
      } else {
        // --- User Command Handling ---
        if (telnetClient.available()) { // Checks if there's any data available from the connected client.
          String command = telnetClient.readStringUntil('\n'); // Reads a line of text sent by the client until a newline character is encountered.
          command.trim();                   // Removes leading and trailing whitespace from the command.
    
          if (command == "help") { // If the user types "help".
            logMessage("\nAvailable commands:", ANSI_YELLOW); // Displays a list of available commands in yellow.
            logMessage("  help       - Shows this help", ANSI_YELLOW);
            logMessage("  pause      - Pauses/resumes scrolling", ANSI_YELLOW);
            logMessage("  log        - Starts logging (2 min)", ANSI_YELLOW);
            logMessage("  dump       - Displays recorded logs", ANSI_YELLOW);
            logMessage("  send D2    - Sends a frame with header 0xD2", ANSI_YELLOW);
            logMessage("  send CC    - Sends a frame with header 0xCC", ANSI_YELLOW);
            logMessage("  send DD    - Sends a frame with header 0xDD", ANSI_YELLOW);
          } else if (command == "pause") { // If the user types "pause".
            pauseDisplay = !pauseDisplay;   // Toggles the pauseDisplay flag.
            logMessage(pauseDisplay ? "Scrolling paused. Type 'pause' to resume." : "Scrolling resumed.", ANSI_YELLOW); // Displays a message indicating whether scrolling is paused or resumed.
          } else if (command == "log") { // If the user types "log".
            startLogging();                // Starts the logging process.
          } else if (command == "dump") { // If the user types "dump".
            dumpLogs();                    // Displays the recorded logs.
          } else if (command.startsWith("send ")) { // If the user enters a command starting with "send ".
            byte header = 0;                // Initializes a byte variable to store the frame header.
            if (command.endsWith("D2")) header = 0xD2;  // Sets the header to 0xD2 if the command ends with "D2".
            else if (command.endsWith("CC")) header = 0xCC; // Sets the header to 0xCC if the command ends with "CC".
            else if (command.endsWith("DD")) header = 0xDD; // Sets the header to 0xDD if the command ends with "DD".
            else {                         // If none of the above conditions are met.
              logMessage("Error: Invalid header. Use D2, CC or DD.", ANSI_RED); // Displays an error message in red.
              return;                       // Exits the current function block.
            }
    
            // Create a sample frame
            byte frame[FRAME_SIZE] = {0};  // Initializes a byte array of size FRAME_SIZE with all elements set to 0.
            frame[0] = header;             // Sets the first element of the frame (index 0) to the value of the header variable.
            for (int i = 1; i < FRAME_SIZE; i++) { // Loops through the remaining elements of the frame array, starting from index 1.
              frame[i] = i & 0xFF;         // Assigns the value of 'i' (bitwise ANDed with 0xFF) to each element of the frame array.
            }
    
            logMessage("\nSending a frame with header 0x" + String(header, HEX) + "...", ANSI_YELLOW); // Displays a message indicating that a frame is being sent.
            sendRS485Frame(frame, FRAME_SIZE);  // Sends the created frame over RS485.
          }
        }
      }
    
      // --- RS485 Frame Reading ---
      if (!pauseDisplay) { // Checks if scrolling is not paused.
        while (rs485.available()) { // Loops as long as there's data available to read from the RS485 serial port.
          byte receivedByte = rs485.read(); // Reads a byte of data from the RS485 serial port and stores it in the receivedByte variable.
          unsigned long currentTime = millis(); // Gets the current time in milliseconds since the program started.
    
          // Detect frame start (0xD2, 0xCC, 0xCD or 0xDD)
          if (!inFrame && (receivedByte == 0xDD || receivedByte == 0xCC || receivedByte == 0xD2 || receivedByte == 0xCD)) { // Checks if a new frame is starting.
            inFrame = true;                // Sets the inFrame flag to true, indicating that we are now receiving a frame.
            frameIndex = 0;               // Resets the frameIndex variable to 0, as we're starting a new frame.
            frameBuffer[frameIndex++] = receivedByte; // Stores the received byte into the frameBuffer at index 0 and increments frameIndex.
            currentHeader = receivedByte;   // Sets the currentHeader variable to the value of the received byte (the header).
            lastByteTime = currentTime;     // Updates lastByteTime with the current time.
          }
          // Store bytes in frame buffer
          else if (inFrame) { // If we are currently receiving a frame.
            frameBuffer[frameIndex++] = receivedByte; // Stores the received byte into the frameBuffer at index frameIndex and increments frameIndex.
            lastByteTime = currentTime;     // Updates lastByteTime with the current time.
    
            // Frame complete
            if (frameIndex == FRAME_SIZE) { // Checks if the entire frame has been received.
              String frameStr = "[" + String(currentHeader, HEX) + "] "; // Creates a string to represent the frame, starting with the header in hexadecimal format.
              const char* color = ANSI_RESET;  // Initializes a constant character pointer to ANSI_RESET (no color).
              if (currentHeader == 0xD2) color = ANSI_BLUE;   // Sets the color to blue if the header is 0xD2.
              else if (currentHeader == 0xCC) color = ANSI_GREEN; // Sets the color to green if the header is 0xCC.
              else if (currentHeader == 0xCD) color = ANSI_CYAN;  // Sets the color to cyan if the header is 0xCD.
              else if (currentHeader == 0xDD) color = ANSI_YELLOW; // Sets the color to yellow if the header is 0xDD.
    
              // Octets 0 to 11
              for (int i = 0; i < 12; i++) { // Loops through the first 12 bytes of the frame.
                if (frameBuffer[i] < 0x10) frameStr += "0";  // Adds a leading zero if the byte value is less than 0x10 (decimal 16).
                frameStr += String(frameBuffer[i], HEX); // Appends the byte value in hexadecimal format to the frameStr.
                if (i < 11) frameStr += " ";  // Adds a space after each byte except for the last one.
              }
    
              // Octet 12 (temperature) in red
              frameStr += " "; // Add a space before temperature value
              if (frameBuffer[11] < 0x10) frameStr += "0";  // Adds a leading zero if the byte value is less than 0x10.
              frameStr += String(frameBuffer[11], HEX); // Appends the byte value in hexadecimal format to the frameStr.
    
              // Decode temperature if header is 0xCD
              if (currentHeader == 0xCD) { // If the header is 0xCD, decode the temperature.
                int temp = decodeTemperature(frameBuffer[11]); // Calls the decodeTemperature function to get the temperature value.
                frameStr += " (Temp: " + String(temp) + "\033[31m°C" + String(ANSI_RESET) + ")"; // Appends the decoded temperature with a red color code and degree symbol.
              }
    
              // Octets 13 to 79
              for (int i = 12; i < FRAME_SIZE; i++) { // Loops through the remaining bytes of the frame.
                if (frameBuffer[i] < 0x10) frameStr += " 0";  // Adds a leading zero if the byte value is less than 0x10.
                else frameStr += " "; // Add space
                frameStr += String(frameBuffer[i], HEX); // Appends the byte value in hexadecimal format to the frameStr.
                if ((i + 1) % 4 == 0 && i != FRAME_SIZE - 1) frameStr += " ";  // Adds a space after every four bytes, except for the last one.
              }
    
              // Store log if logging is enabled
              if (isLogging) { // If logging is active.
                logBuffer[logIndex].message = frameStr; // Stores the frame string in the current log entry.
                logBuffer[logIndex].timestamp = currentTime; // Stores the timestamp of the received frame.
                logBuffer[logIndex].color = color; // Stores the assigned color for this log entry.
                logIndex = (logIndex + 1) % MAX_LOGS; // Increments the log index, wrapping around if it reaches MAX_LOGS.
              }
    
              logMessage(frameStr, color); // Displays the frame string with the appropriate color in the serial monitor and Telnet client.
              inFrame = false;                // Resets the inFrame flag to false, indicating that we are no longer receiving a frame.
              frameIndex = 0;               // Resets the frameIndex variable to 0, preparing for the next frame.
            }
          }
    
          // Timeout management
          if (inFrame && (currentTime - lastByteTime > ((currentHeader == 0xDD) ? LONG_DELAY : SHORT_DELAY))) { // Checks if a timeout has occurred while receiving a frame.
            logMessage("[TIMEOUT] Trame 0x" + String(currentHeader, HEX) + " incomplète.", ANSI_RED); // Logs a timeout message with the header value in red.
            inFrame = false;                // Resets the inFrame flag to false.
            frameIndex = 0;               // Resets the frameIndex variable to 0.
          }
        }
      }
    
      // Stop logging after 2 minutes
      if (isLogging && (millis() - logStartTime >= LOG_DURATION)) { // Checks if the logging duration has exceeded 2 minutes.
        isLogging = false;                // Stops the logging process.
        logMessage("Enregistrement des logs terminé (2 minutes écoulées).", ANSI_YELLOW); // Logs a message indicating that logging has stopped.
      }
    }
    
    // --- Decoding of temperature ---
    int decodeTemperature(byte tempByte) {
      return tempByte;  // Example: 0x1C = 28°C
    }
    
    // --- Sending an RS485 frame ---
    void sendRS485Frame(const byte *frame, int size) {
      for (int i = 0; i < size; i++) {
        rs485.write(frame[i]); // Sends each byte of the frame through the RS485 serial port.
      }
      rs485.flush(); // Ensures that all data in the buffer is sent.
      logMessage("Trame envoyée (mode réception permanente).", ANSI_YELLOW); // Logs a message indicating that the frame has been sent.
    }
    
    // --- Display messages ---
    void logMessage(const String &message, const char* color) {
      Serial.println(message); // Prints the message to the serial monitor without colors.
      if (telnetClient.connected()) { // Checks if a Telnet client is connected.
        telnetClient.print(color); // Prints the specified color code to the Telnet client.
        telnetClient.print(message); // Prints the message to the Telnet client.
        telnetClient.print(ANSI_RESET); // Resets the color code to default after printing the message.
        telnetClient.println(); // Adds a newline character to move the cursor to the next line in the Telnet client.
      }
    }
    

Next step, gather log files

The good news

It seems the frames are really 80 octet long, also the 0xCD begining frame happen sometimes even if i do not touch the panel

I will try to send temperature target

Please let me now if you see something i didn’t get or have any advice

Thanks !

(i hope i did format well my message)

PS : original code has been done with Mistral AI help

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.