RS 485 Bidirectional Struct Capable Library

I've been trying to find a library for communicating between two (or more) Arduinos (an ESP8266 as master and Nanos as slaves) using Max485 modules. I have not been able to find one that checks all the requirements I would like to have and I'm not knowledgeable enough to write my own library for this yet. Nick Gammon's RS485_non_blocking library seems close, with the exception of a few things.

Desired Functionality:

  • Non-Blocking
  • Master w/ Multiple Slaves (Addressing each slave would be ideal?)
  • Bidirectional communication
  • Sending data using Structs
  • Works on AVR, ESP8266, and Teensy architectures

I believe Nick's non-blocking library checks all of those boxes except sending the data as Structs and addressing slaves. Does anyone know how to set up a function to convert structs to and from bytes for use with Nick's library? Also how to setup for use with multiple slaves?

Thanks for your help in advance!
Nathan

1 Like

You can convert a struct to an array of bytes and back quite easily using a union.

Setting up for multiple slaves is relatively easy to do and can be a simple extension to your sending and receiving of a struct.

If you make one of your struct elements a destination address - a simple byte should be sufficient - they you have 256 unique slave addresses. At a simple level only addresses 1 to 254 are used, leaving address 0 and 255 as specials.

Each slave always receives every message. It's up to the individual slave to compare the destination address with its own address. If there's a match, then the message is for that slave, otherwise it's simply discarded.

You can use address 255 as a broadcast address so that every slave receives the message at the same time. Generally this type of message doesn't get an acknowledgement from a slave, and this type of message is used to pass a piece of information to multiple slaves at the same time - maybe a synchronising message for a timer or an instruction to turn all outputs off (or on).

If you are using RS485, then each slave would also need to control the direction of flow of the data into and out of its MAX485 line driver chip. Generally, every slave sits in receive mode waiting for a message addressed to it. When the slave gets a message addressed to it, then it decodes it, decides what to do with the message, formulates a response (if needed), switches its line driver into Tx mode, sends the response and then finally switches its line driver back into Rx mode.

If you are using Nanos, then you will probably want to use a software serial port implementation for your comms port. You will be limited to 9600 baud, maybe 19200 but probably not any faster. This assumes that you want to keep your hardware port for a USB-serial connection. You could use your hardware serial port for your RS485 comms and a software serial port for debugging. You would need to disconnect your RS485 setup in order to reload software if you did this.

Thanks for the advice. Am I correct then that a library to do these functions doesn't exist? I need to develop my own communication library for this?

You could use the MODBUS library that's been around for a while. I don't know if it will suit your needs.

I'm not aware of a library that does what you want.

Don't worry about a library, you can simply write code in your sketch to do what you want for now. Maybe split the code out into a library later on when you gain more knowledge.

A good way to "start" a library, is to move some of the code into .h and .cpp files in the project folder. They will appear as extra tabs in the IDE window, so they are easy to edit and view. When you're ready, you almost have a library, just a few finishing touches...

The sendMsg function in my library takes a pointer and a length, so you can "cast" your struct to a const byte pointer, supply its length, and send it that way. Similarly to receive the struct.

The rolling master version (described on my page here: http://gammon.com.au/forum/?id=11428 ) has provision for each slave having an address. That is a protocol thing anyway, you would make sure each slave knows its own address, and put the address into the message, so for example one slave might send a message to another slave (ie. message from 6 to 9). There is no particular need for a single master, and indeed the system I described it intended to work even if some slaves are not functional.

Look, Gammon's been here.

:tada:

4 Likes

I would consider to use Modbus protocol. Even if there is no such thing as "sending data using Structs" I assume this task can be solved with other measures within the protocol.

Can you describe your project in more detail ... what information do you want to send between your Master and your Slaves (better Client and Servers how we would call them today)? What should they do?

Thanks for all the advice. I've decided to try preexisting Modbus libraries to see if they can meet my requirements. However I have not been able to achieve reliable communication between 2 nanos (for testing) using the example sketches provided for either the Arduino Modbus Library or with the Simple Modbus Library. I still need to do more troubleshooting. If I'm still unsuccessful, I will post my code. If anyone has example master/slave (client/server) sketches to use with one of these libraries that work right out of the box, let me know.

@n_spot
when we are talking about Modbus I think you should do some paper work upfront.
You know that Modbus is working with "function codes". Each with a specific task.
Further more "registers" play an important role for Modbus.

So my advice is write down your requirement - what functionality should be interchanged between the client and slave? read input, set outputs, get sensor data, change parameters ...

Afterwards assign each task to the proper Function Code and a register in terms of Modbus.
I think this is an important step before you start development or even before you chose a library.

longer story:
Personally I have used the origin "Simple Modbus Library" for a long time. It worked very reliable for reading data. Imho it's not the best choice for one time tasks like "write coil".

As client / master library I'm using GitHub - 4-20ma/ModbusMaster: Enlighten your Arduino to be a Modbus master - This lib is available via library manager.

As server / slave library I have written my own library which uses very similar naming convention like doc Walkers ModbusMaster. There are two folders in the examples: one for server/slave and one with examples for the client/master (which requires the ModbusMaster library).
Link to Noiasca Modbus Server Simple Library

If you have written down your requirements and want to go the way with DocWalkers ModbusMaster & Noiasca Modbus Server Simple - I assume I could give you some start.

Or you can use SerialTransfer.h

That was my preferred method that I tried first, but to get long distance I had to use RS485 which really complicates things for two-way communication. Do you have a solution for using your SerialTransfer Library with RS485 and two way (master/slave) communication?

Hi,

do all your slaves send different structs with varying size, or does each slave send the same struct to the master?

If using multiple structs, I have a few examples I can send you when I get home, which send different sized structs (or any variable) using the UART protocol (rx and tx pins), both with and without the SerialTransfer library and bidirectionally. RS485 works through the UART protocol, except it's half duplex (only 1 node can send at any one time).

The uart_RX and uart_TX examples in the serialtransfer library are what you need to look through to get started (if you choose to use that library)

If you could send (or post) those examples for sending different size structs to the master, that would be very helpful. I'd prefer examples using the SerialTransfer library. Thanks in advance!!

Here is the multi-packet SerialTransfer example:

Pretend that the single Sender sketch represents 5 different RS485 slaves, and the receiver sketch represents a single master.

You can have each slave identified through a particular "packetID" byte.

When the master reads that ID byte, it knows which particular slave sent the transmission, and it then transforms the received packet, into the data which is associated with that particular slave.

You could also have a single slave, which can send multiple packets to the master, through giving that slave multiple ID bytes. For example; slave 1 has ID byte "1" and "2", and slave 2 has ID byte "3" and "4".

Finally, as you can see from "case 5" in the sender sketch, you can have multiple structs (or any group of variable's) sent in a single transmission. Very versatile!

On the opposite end, we have the single master sending data to each one of the slaves individually (probably). That is, there is 1 transmitter and multiple listeners. Every listener is going to have data sent to them, even if the data is not for them. However, each slave can accept or ignore the transmission ,based on the same packet ID byte system. That is, if the slave picks up a packet containing their particular ID byte, they accept it and parse the data. If they pick up an ID byte that isn't theirs, they dump the packet.

Of course, there is a bit more involved with getting this working bidirectionally and with RS485, such as turning on/off the write enable pins, as well as setting up timeouts if a node isn't responding. Remember, since '485 is half-duplex, you can only have 1 sender at any given time.

  • final notes:
    • The sketches uses software UART pins, but a hardware UART is preferable
    • The sketches were tested between 2 regular Nano's (5v), but different MCU's can have different sized variable's. However, this can be solved by specifying the sizes of the integers between the boards, such as using uint16_t for an unsigned int and uint32_t for an unsigned long.
    • Struct sizes can also vary between boards. See here for fixes; Size of struct differences?
    • potential fix for esp32 boards using the library Library work with Arduino UNO and ESP32 · Issue #46 · PowerBroker2/SerialTransfer · GitHub
    • Level shifting is needed between board's of different logic levels.

Disclaimer: I've never actually used RS485, but this is going off what I've read from some decent sources (incl. Gammon's).

Sender:

#include<SoftwareSerial.h>
SoftwareSerial mySerial(6, 7);

#include "SerialTransfer.h"

SerialTransfer myTransfer;

byte packetID = 0;  //packetID byte used to select;
//1. which variable(s) Master will send
//2. which variable(s) Slave will receive

//VARIABLES TO SEND


float singleVariableTX = 3.14;


struct structOne_TX {
  int variable_1_struct_1;
  unsigned long variable_2_struct_1;
  char variable_3_struct_1;
}
structOneTX;


struct structTwo_TX {
  boolean variable_1_struct_2;
  byte variable_2_struct_2;
  int variable_3_struct_2;
  long variable_4_struct_2;
}
structTwoTX;


char charArrayTX[21];


unsigned long previousMillis = 0;
unsigned long interval = 1000;

void setup()
{
  Serial.begin(115200);
  mySerial.begin(9600);
  myTransfer.begin(mySerial);

  structOneTX = {100, 2000, 'a'};
  structTwoTX = {1, 20, 300, 4000};
  strcpy(charArrayTX, "Sent from Arduino A");

}

void loop()
{
  //LOOPING THROUGH 5 PACKETS TO SEND...

  if (millis() - previousMillis >= interval) {
    previousMillis = millis(); //will keep looping until response
    packetID++;
    sending();

    if (packetID == 5) { //4 options to send
      packetID = 0;
    }
  }
}

void sending() {
  // use this variable to keep track of how many
  // bytes we're stuffing in the transmit buffer
  uint16_t sendSize = 0;

  switch (packetID) {//selector byte chooses the variable(s) to send.
    case 1:
      sendSize = myTransfer.txObj(singleVariableTX, sendSize);
      break;
    case 2:
      sendSize = myTransfer.txObj(structOneTX, sendSize);
      break;
    case 3:
      sendSize = myTransfer.txObj(structTwoTX, sendSize);
      break;
    case 4:
      sendSize = myTransfer.txObj(charArrayTX, sendSize);
      break;
    case 5:
      sendSize = myTransfer.txObj(structOneTX, sendSize);
      sendSize = myTransfer.txObj(structTwoTX, sendSize);
      break;

  }
  myTransfer.sendData(sendSize, packetID); //packetID is read by the receiver, to indicate which variables are coming through

  Serial.print("Sent Packet #");
  Serial.println(packetID);
}

Receiver

#include<SoftwareSerial.h>
SoftwareSerial mySerial(6, 7);

#include "SerialTransfer.h"

SerialTransfer myTransfer;

//VARIABLES TO RECEIVE

float singleVariableRX;


struct structOne_RX {
  int variable_1_struct_1;
  unsigned long variable_2_struct_1;
  char variable_3_struct_1;
}
structOneRX;


struct structTwo_RX {
  boolean variable_1_struct_2;
  byte variable_2_struct_2;
  int variable_3_struct_2;
  long variable_4_struct_2;
}
structTwoRX;

char charArrayRX[21];

byte packetID;
bool flag1 = false;

void setup()
{
  Serial.begin(115200);
  mySerial.begin(9600);
  myTransfer.begin(mySerial);
}

void loop()
{

  readAndParseData();
  showNewData();

}

void readAndParseData() {

  if (myTransfer.available())
  {
    // use this variable to keep track of how many
    // bytes we've processed from the receive buffer
    uint16_t recSize = 0;

    packetID = myTransfer.currentPacketID();

    //searches for the packet ID send by sender.
    switch (packetID) {

      case 1:
        recSize = myTransfer.rxObj(singleVariableRX, recSize);
        break;

      case 2:
        recSize = myTransfer.rxObj(structOneRX, recSize);
        break;

      case 3:
        recSize = myTransfer.rxObj(structTwoRX, recSize);
        break;

      case 4:
        recSize = myTransfer.rxObj(charArrayRX, recSize);
        break;

      case 5:
        recSize = myTransfer.rxObj(structOneRX, recSize);
        recSize = myTransfer.rxObj(structTwoRX, recSize);
        break;
    }

    flag1 = true;

  }
}

void showNewData() {
  if (flag1 == true) {

    Serial.println("//////////////////////////////////");
    Serial.println(" ");
    Serial.print("Packet #");
    Serial.println(myTransfer.currentPacketID());
    Serial.println(" ");

    switch (packetID) {
      case 1:
        Serial.print("Single Float = ");
        Serial.println(singleVariableRX);
        Serial.println("");
        break;

      case 2:
        Serial.println("Struct 1...");
        Serial.print("Int = ");
        Serial.println(structOneRX.variable_1_struct_1);
        Serial.print("Unsigned Long = ");
        Serial.println(structOneRX.variable_2_struct_1);
        Serial.print("Character = ");
        Serial.println(structOneRX.variable_3_struct_1);
        Serial.println("");
        break;

      case 3:
        Serial.println("Struct 2...");
        Serial.print("Boolean = ");
        Serial.println(structTwoRX.variable_1_struct_2);
        Serial.print("Byte = ");
        Serial.println(structTwoRX.variable_2_struct_2);
        Serial.print("int = ");
        Serial.println(structTwoRX.variable_3_struct_2);
        Serial.print("Long = ");
        Serial.println(structTwoRX.variable_4_struct_2);
        Serial.println("");
        break;

      case 4:
        Serial.print("Char array = ");
        Serial.println(charArrayRX);
        Serial.println("");
        break;

      case 5:
        Serial.println(" ");
        Serial.println("Struct 1...");
        Serial.print("Int = ");
        Serial.println(structOneRX.variable_1_struct_1);
        Serial.print("Unsigned Long = ");
        Serial.println(structOneRX.variable_2_struct_1);
        Serial.print("Character = ");
        Serial.println(structOneRX.variable_3_struct_1);
        Serial.println("");

        Serial.println("Struct 2...");
        Serial.print("Boolean = ");
        Serial.println(structTwoRX.variable_1_struct_2);
        Serial.print("Byte = ");
        Serial.println(structTwoRX.variable_2_struct_2);
        Serial.print("int = ");
        Serial.println(structTwoRX.variable_3_struct_2);
        Serial.print("Long = ");
        Serial.println(structTwoRX.variable_4_struct_2);
        Serial.println("");
        break;
    }

    Serial.println("//////////////////////////////////");

    flag1 = false;
  }

}

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