Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modbus pass through from TCP to RTU (gateway) #101

Closed
eskildsf opened this issue Dec 29, 2020 · 5 comments
Closed

Modbus pass through from TCP to RTU (gateway) #101

eskildsf opened this issue Dec 29, 2020 · 5 comments

Comments

@eskildsf
Copy link

eskildsf commented Dec 29, 2020

Hi

Thank you for this library. It is very nice to use. I appreciate your effort!

I have an application in mind which is to use an Arduino with Ethernet shield as a pass through device from TCP to RTU (gateway).
This is realized in the sketch below, however it is quite limited due to the max number of registers which can fit into Arduino memory.

Do you think it is possible to use this library to construct a transparent Modbus gateway that does not require pre-definition of registers in the Arduino, but only configuration of slave ID and network configuration (IP, MAC, gateway...)?

I imagine amending the code to implement OnGetAny and OnSetAny callbacks and using these much in the same fashion as my current callbackGet and callbackSet. This would eliminate the need to store register information and thus reduce the memory requirement such that it is feasible on an Arduino.

Once again thank you for this library.

Br Eskild

How to

  1. Program Snippet 1 into an Arduino with Ethernet adaptor.
  2. Program snippet 2 into an Arduino.
  3. Connect Arduino with Ethernet adaptor to other Arduino via UART.
  4. Connect Arduino with Ethernet adaptor to computer with Python via Ethernet and execute Snippet 3.

Snippet 1: Arduino Gateway

/*
 * Modbus TCP to RTU gateway
 * Pre-requisites
 *  - https://github.com/emelianov/modbus-esp8266
 *  - https://github.com/UIPEthernet/UIPEthernet if using ENC28J60 Ethernet adaptor
 * 
 * For this to work, you have to remove/comment void begin(uint16_t port=0) { } in ModbusEthernet.h
*/

// RTU
#include <ModbusRTU.h>
#define SLAVE_ID 1
ModbusRTU mbRTU;

// Ethernet
#include <UIPEthernet.h> // Replace by Ethernet.h if NOT using ENC28J60 Ethernet adaptor
#include <ModbusEthernet.h>
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
IPAddress ip(192, 168, 1, 9);
ModbusEthernet mbTCP;

// Register
#define REGN 10

uint16_t callbackGet(TRegister* reg, uint16_t val) {
  uint16_t res[1];
  if ( !mbRTU.slave() ) {
    mbRTU.readHreg(SLAVE_ID, reg->address.address, res);
    while( mbRTU.slave() ) {
      mbRTU.task();
    }
  }
  return res[0];
}

uint16_t callbackSet(TRegister* reg, uint16_t val) {
  if ( !mbRTU.slave() ) {
    uint16_t res = {val};
    mbRTU.writeHreg(SLAVE_ID, reg->address.address, res);
    while( mbRTU.slave() ) {
      mbRTU.task();
    }
  }
  return val;
}

void setup() {
  Serial.begin(9600, SERIAL_8N1);
  mbRTU.begin(&Serial);
  mbRTU.master();

  Ethernet.begin(mac, ip);
  mbTCP.server();
  mbTCP.addHreg(REGN);
  mbTCP.onGetHreg(REGN, callbackGet);
  mbTCP.onSetHreg(REGN, callbackSet);
}

void loop() {
  mbTCP.task();
  delay(50);
}

Snippet 2: Arduino RTU slave

#include <ModbusRTU.h>
#define SLAVE_ID 1
ModbusRTU mbRTU;

// Register
#define REGN 10

void setup() {
  Serial.begin(9600, SERIAL_8N1);
  mbRTU.begin(&Serial);
  mbRTU.setBaudrate(9600);
  mbRTU.slave(SLAVE_ID);
  mbRTU.addHreg(REGN, 100, 1);
}

void loop() {
  mbRTU.task();
  yield();
}

Snippet 3: Python test snippet

# Prerequisite: pip install pymodbus
# First execution of this code should print 100, next one 101, then 102...
from pymodbus.client.sync import ModbusTcpClient

REGN = 10
c = ModbusTcpClient('192.168.1.9')
r = c.read_holding_registers(REGN, 1)
print(r, r.registers)
c.write_registers(REGN, [r.registers[0]+1])
@emelianov
Copy link
Owner

Hello,
It's not so easy. onGetAny/onSetAny alone are not enough. Let me couple of days to think if it's possible to extend callback's API without breaking changes.
With your Snippet 1 i see potential problem if mbTCP got a request for reading/writing multiple (100 for example) Hregs.

By the way, if the library if working with UIPEthernet.h? (Haven't tested with UIP, just with W5x00)

@eskildsf
Copy link
Author

Hi

Your library works great with UIPEthernet.h without any modifications in either library. I just happened to have an ENC28J60 board for and Arduino Nano on hand, and with UIPEthernet.h it worked fine.
So far I have Snippet 1 working with 16 Hregs. More registers than that and Snippet 3 is unable to get a response.

Thank you for taking my use case into consideration. I imagine the onGetAny/onSetAny callbacks as fall-back methods which handle requests concerning registers that have not been registered with an e.g. mb.Hreg statement. I look forward to hearing your thoughts on this. It would accomplish both goals: (i) A catch all method for all types of Modbus FC and (ii) avoiding the memory consumption of registering Modbus registers in code.

I appreciate your effort and look forward to following and supporting potential progress.

Br Eskild

@emelianov
Copy link
Owner

Adding onGetAny/onSetAny implementation requires massive internal logic changes and additional code that will executed on each request. It's not good approach for this very specific case. Instead I've added some slight helpers to the library code and suggest a bit hacky way.
Main idea is to override TRegister* searchRegister(TAddress addr) method that is used internally to get registers way it starts returning single register for all requests and save original requested register.
Notes:

  1. The code is ready to run at ESP32. I believe it's not a problem for you to modify if for ethernet.
  2. This implementation still not effective as splits request for read/write multiple registers to number of one-register requests. There is a way to optimise this behaviour but it ill take some time...
  3. No error processing is implemented
#include <ModbusRTU.h>
#define SLAVE_ID 1
// Register
#define REGN 10


ModbusRTU mbRTU;

#if defined(ESP32)
#include <WiFi.h>
#include <ModbusTCP.h>
class ModbusMap : public ModbusTCP {
#else
// Ethernet
//#include <UIPEthernet.h> // Replace by Ethernet.h if NOT using ENC28J60 Ethernet adaptor
#include <Ethernet.h>
#include <ModbusEthernet.h>
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
IPAddress ip(192, 168, 1, 9);
class ModbusMap : public ModbusEthernet {
#endif

public:
  TAddress lastRegister;
protected:
  TRegister* searchRegister(TAddress addr) override {
    lastRegister = addr; // Save real register requested
#if defined(ESP32)
    if (addr.isHreg()) // This version overrides only Hregs
      return ModbusTCP::searchRegister(HREG(REGN)); // Always return specific register. Next read or write callback will be called against this register.
    return ModbusTCP::searchRegister(addr);
#else
    return ModbusEthernet::searchRegister(addr);
#endif
  }
};

ModbusMap mbTCP;

uint16_t callbackGet(TRegister* reg, uint16_t val) {
  uint16_t res = 0xBEEF;
  if ( !mbRTU.slave() ) {
    mbRTU.readHreg(SLAVE_ID, mbTCP.lastRegister.address, &res); // Note that readHreg is called against saved original register not register passed to callback parameter
    while( mbRTU.slave() ) {
      mbRTU.task();
    }
  }
  return res;
}

uint16_t callbackSet(TRegister* reg, uint16_t val) {
  if ( !mbRTU.slave() ) {
    uint16_t res = val;
    mbRTU.writeHreg(SLAVE_ID, mbTCP.lastRegister.address, res);
    while( mbRTU.slave() ) {
      mbRTU.task();
    }
  }
  return val;
}

void setup() {
#if defined(ESP32)
  Serial.begin(115200);
  Serial1.begin(9600, SERIAL_8N1, 19, 18);
  mbRTU.begin(&Serial1, 17);
#else
  Serial.begin(9600, SERIAL_8N1);
  mbRTU.begin(&Serial);
#endif
  mbRTU.master();

#if defined(ESP32)
  WiFi.begin("E2", "fOlissio92");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
#else
  Ethernet.begin(mac, ip);
#endif
  mbTCP.server();
  mbTCP.addHreg(REGN);
  mbTCP.onGetHreg(REGN, callbackGet);
  mbTCP.onSetHreg(REGN, callbackSet);
}

void loop() {
  mbTCP.task();
  delay(50);
}

@LiamVu2k
Copy link

LiamVu2k commented Dec 1, 2021

Hello, I'm very interested in this topic, the code works so well with default slave unit ID 1. Moreover, Is there any way to set the SLAVE_ID to another value if the UnitID in the MBAP header isn't 01 ?

@emelianov
Copy link
Owner

There is no easy way to do it with current library version.
Can be done with enhanced bridge functionality that is in development stage now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants