RS485 ModBus between Solar Charger Inverter and Battery setup

Hello

I have some PylonTech US2000 Plus batteries connected to a Sofar ME3000SP inverter which are communicating with each other using an RS485 ModBus. I wish to "tap" in to the line to gather certain data. I've got a MAX485 module setup and working by tapping in to the A&B lines on the cable using the following code :-

#include <SPI.h>
#include <MsTimer2.h>

#define MESSAGE_GAP_TIMEOUT_IN_MS 5
#define LED_TOGGLE_MSGS 50

int MESSAGE_COUNT = 0;

char RecvBuffer[256];     

unsigned int recvIndex = 0;
unsigned long msgIndex = 0;
unsigned long receiveTime = 0;

bool LED_STATUS = true;

char receiveTimeString[32];
char msgIndexString[32];

void setup()
{
  
  Serial.begin(9600); // PC USB output
  Serial1.begin(115200); // RS485 module
  
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LED_STATUS);

  // set an end-of-message detection timeout of 5ms
  MsTimer2::set(MESSAGE_GAP_TIMEOUT_IN_MS, onTimer);

  Serial.println("Setup complete");
}

bool dumpData = false;
bool timerStarted = false;

// If this timer expires, this means no additional character was received for a while: notify main loop
void onTimer() {
  dumpData = true;        
}

void loop()
{
   char received;

   if (Serial1.available() > 0) {
      received = Serial1.read();     
      RecvBuffer[recvIndex++] = received;
   }

    // first time we get an empty buffer after receiving stuff:
    // this could be the end of the message, (re)start the end-of-message detection timer. 
    if (recvIndex>0) {

      if (!timerStarted){
        MsTimer2::start();
        timerStarted = true;
      }
    } 

   // If the timer expired and positioned this var, we should now dump the received message
   // into a UDP packet to the remote host/logger.
   if (dumpData) {

      LED_STATUS = !LED_STATUS;
      digitalWrite(LED_BUILTIN, LED_STATUS);
    
      //Serial.print("Message received ");
      //Serial.println(String(MESSAGE_COUNT));

      Serial.print(RecvBuffer[0]);

      Serial.println((char *)RecvBuffer);
      
      MESSAGE_COUNT++;
      receiveTime = micros() - MESSAGE_GAP_TIMEOUT_IN_MS*1000; 
      
      // reinitialize vars for next detection/dump
      dumpData = false;
      MsTimer2::stop();
      timerStarted = false;   

      msgIndex++ ;
      if (msgIndex > 999) msgIndex = 0;
     
      // build and send UDP message
      //Udp.beginPacket(remote_ip, 8888);
      
      //sprintf(receiveTimeString, "%015lu:", receiveTime);
      //Udp.write(receiveTimeString, strlen(receiveTimeString));

      //sprintf(msgIndexString, "%04lu:", msgIndex);
      //Udp.write(msgIndexString, strlen(msgIndexString));
      
      //Udp.write(RecvBuffer, recvIndex);
    
      //Udp.endPacket();

      // reset index for next message
      recvIndex = 0;
   } 
}

This is being done on a Arduino MEGA.

I've attached 2 "serial" files showing the output I'm getting from tapping in to the wire.

I suspect that the data I'm seeing is just coming from the battery to the invertor, but the data I require is only available from the inverter.

From the code below you can see what data I'm trying to get out of the inverter :-

#include <SoftwareSerial.h>
#include <ModbusMaster.h>

// rs485 enable
#define MAX485_ENABLE 2

// instantiate ModbusMaster object
ModbusMaster node;

void preTransmission()
{
  digitalWrite(MAX485_ENABLE, 1);
}

void postTransmission()
{
  digitalWrite(MAX485_ENABLE, 0);
}

void setup() {

  // pin mode for enable
  pinMode(MAX485_ENABLE, OUTPUT);
  
  // Init in receive mode
  postTransmission();

  // start USB serial
  Serial.begin(115200);

  // Modbus communication runs at 115200 baud
  // set the data rate for the SoftwareSerial port
  Serial1.begin(115200);

  // Modbus slave ID 1
  node.begin(128, Serial1);
  // Callbacks allow us to configure the RS485 transceiver correctly
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);

}

bool state = true;


void loop() {
  Serial.print("Loop ... ");
  // put your main code here, to run repeatedly:
  uint8_t result;
  uint16_t data[16];
  
  // Toggle the coil at address 0x0002 (Manual Load Control)
  //result = node.writeSingleCoil(0x02, state);
  //state = !state;

  // Read registers
  result = node.readInputRegisters(0x0200, 16);
  Serial.println(String(result));
  Serial.println(String(node.ku8MBSuccess));
  if (true)  // result == node.ku8MBSuccess
  {
    Serial.print("Charge/discharge power: ");
    Serial.println(node.getResponseBuffer(0x0D));
    Serial.print("Battery capacity: ");
    Serial.println(node.getResponseBuffer(0x10));
    Serial.print("Feed in/out power: ");
    Serial.println(node.getResponseBuffer(0x12));
    Serial.print("Load power: ");
    Serial.println(node.getResponseBuffer(0x13));
    Serial.print("Input/output power: ");
    Serial.println(node.getResponseBuffer(0x14));
    Serial.print("Generation power: ");
    Serial.println(node.getResponseBuffer(0x15));
    Serial.print("Generation today: ");
    Serial.println(node.getResponseBuffer(0x18));
    Serial.print("To grid today: ");
    Serial.println(node.getResponseBuffer(0x19));
    Serial.print("From grid today: ");
    Serial.println(node.getResponseBuffer(0x1A));
    Serial.print("Consumption today: ");
    Serial.println(node.getResponseBuffer(0x1B));
    Serial.print("Battery cycles: ");
    Serial.println(node.getResponseBuffer(0x2C));
  }

  delay(5000);
}

I've commented out "if result == node.ku8MBSuccess" because I'm never getting a success reading. I've got DE & RE of the 485 module connected to pin 2 of the MEGA.

The invertor documentation is attached but I've not managed to get any battery documentation from the invertor.

I failing to understand the principle of slave/master on ModBus as I suspect the battery is the master and inverter is the slave, so do I need another master on the network or can a slave talk to another slave?

Any help would be appreciated.

SOFARSOLAR ModBus-RTU Communication Protocol.pdf (270 KB)

serial_output_text.txt (15.8 KB)

      Serial.print(RecvBuffer[0]);

      Serial.println((char *)RecvBuffer);

Printing binary data to the serial monitor doesn't make sense. As RecvBuffer is not null-terminated you print anything but probably not what the devices sent.
Decoding snooped Modbus traffic is difficult because you don't know if the master sent a message or a slave but to decode it you have to know that.
The easiest way to get there is not to snoop the traffic but intercept it and forward everything. That way you know that everything that is coming from one side is master requests while everything from the other side is slave responses.

Thanks for you reply.

What is the best way to intercept it whilst keeping both of the existing devices on the network?

What is the best way to intercept it whilst keeping both of the existing devices on the network?

I don't see a way for this. But that way you don't know which bytes are coming from the master and which from the slave. As you cannot distinguish this by the message content (at least not unambiguously) you probably have to use the technique explained in answer #1.

Ah, so if I'm understanding this correctly - I'd need 2 serial interfaces, one connected to each device and then "forward" the traffic from one interface to the other? Then I should be able to determine which one is acting as a slave an which one is master and also snoop on the data?

Then I should be able to determine which one is acting as a slave an which one is master and also snoop on the data?

No, you must know where the master is connected and where the slave(s) as you need that information to decode the Modbus traffic.

I'd need 2 serial interfaces, one connected to each device and then "forward" the traffic from one interface to the other?

You need to do that in both directions and keep a look at the timing as Modbus may be quite picky about that.

Thanks, I'll give that a go.

Does that mean masters/slaves format is different? Is there a simple doc reference I can use to start with?

I'm pretty sure which device is the slave and which one is the master. I've created the code that I think could work but I'm waiting on another RS485 converter as I've only got one at the moment.

Does that mean masters/slaves format is different? Is there a simple doc reference I can use to start with?

The Modbus specification isn't that hard to read and it shows you how the requests are structured as well as the responses.

The assumption is that you have half-duplex RS-485, meaning the A/B wires will be used for transmit from the master and receive by the master. Unlike RS-232 which is full duplex and has dedicated TX and RX wires RS-485 half duplex does not. RS-485 (4 wire) full duplex does but it is unlikely that is what you have.

Since you have half duplex RS-485, when there is data on the A/B wires you don't know which way the data is going (master to slave or slave to master).

You could snoop on this by modifying a modbus library (I suggest smarmengol modbus to remove all the write functions and setup it up as a slave with "multiple" node addresses. In this way the slave.poll routines will execute and identify a valid telegram for both the slave and master node addresses. With no write functions, it will not respond on the network. The can be further guaranteed by hardwiring the RE/DE pins into receive mode.

You could snoop on this by modifying a modbus library (I suggest smarmengol modbus to remove all the write functions and setup it up as a slave with "multiple" node addresses.

That won't work because if you configure the slave address of an existing slave it will decode the message from the master correctly but it will fail to decode the slave reply correctly. It will be quite difficult to distinguish a transmission error from a slave reply.

Ok, so I've got my "RS485 forwarder" working and the battery and inverter appear to be working normally. It took a while to get it working so that neither the battery or the inverter would know the data had changed, but it's working great now.

Attachment1: RS485_Forwarder.ino - code for forwarding
Attachment2: Serial1_Battery.txt - data being received from the inverter in HEX format
Attachment3: Serial2_Inverter.txt - data being received from the battery in HEX format

The code basically forward anything received on Serial1 to Serial2 and vice versa.

The question is now, how do I construct a message to send to the inverter to get the data back in the range 0x0200 to 0x0245 as referenced in the Inverter manual I attached in my original post (starts at page 9)?

I have checked the data coming from the inverter (as attached - Serial2) by converting them in to Integers (8 bit) and Words (16 bit) and they do not correspond to any data I'm expecting so I believe the data I'm getting is unrelated to the information I want.

RS485_Forwarder.ino (5.01 KB)

Serial1_Battery.txt (2.92 KB)

Serial2_Inverter.txt (19.2 KB)

One line of Serial1_Battery.txt is 39 bytes, so this cannot be bytes encoded in hex. Is it possible that the D at the end should be a 0D?

The data doesn't look like Modbus. Are you sure that the devices talk Modbus?

The data is in ASCII:

~20054642E00205FD2D

It might be Modbus ASCII but definitely not Modbus RTU.

pylon:
That won't work because if you configure the slave address of an existing slave it will decode the message from the master correctly but it will fail to decode the slave reply correctly. It will be quite difficult to distinguish a transmission error from a slave reply.

I have one built with a 80C51 that works quite well from back in the 90s. It was originally build to spy on a custom made multi-drop RS-232 network (it was multi-drop because custom line drivers were created, so techinically not RS-232 but was +/-12V and the same frame and timing). The trick is to check for a list of addresses. ie., setup a node that accepts and reads all properly formatted telegrams "almost" independent of node address.

pylon:
Are you sure that the devices talk Modbus?
It might be Modbus ASCII but definitely not Modbus RTU.

No, I'm not. I can only go on the SOFARSOLAR document which states ModBus RTU and it talks directly to the battery so I can only assume that does the same.

I'll give the code a check over to ensure I'm getting 2 digits for each byte received as it doesn't look like that's happening. I've had a quick look and I can't see any hex pairs that start with 0 so maybe that's the issue.

i think I've fixed the issue and attached samples of the new output.

Does that look like the data you're expecting?

Serial1B_From_Battery.txt (754 Bytes)

Serial2B_From_Inverter.txt (3.86 KB)

Has anyone had chance to verify the new output I captured?

I don't know what protocol that is but it is not Modbus RTU.

Old thread, but very interesting.
I have two BST batteries that do look a bit like Pylontech batteries. I have logged the traffic between the batteries and it looks very much like the traffic you logged.

Examples from master battery.
7e 32 35 30 34 34 36 39 30 30 30 30 30 46 44 41 32 0d
7e 32 35 30 35 34 36 39 30 30 30 30 30 46 44 41 31 0d

So the question is, did you manage to get anywhere with this?