Slave arduino receiving commands from two serials

Hello! I have a slave arduino mega2560 connected to a master arduino mega2560 (through UART) and to a RPi (through USB). The slave arduino is set to monitor various SHT85 (through I2C) and NTC sensors. What I would like to do is to have the slave arduino respond to both commands from the master and the RPi.

The code I have works ok for the NTCs but it is sometimes problematic for the SHT sensors. From times to times (every ~10 minutes) some of the sensors do not send back all requested bytes. The functions I am using to write and read are the following (full code is way longer):

// I2C write
void cmdI2Cwrite(int address, char *cmd) {
  Wire.beginTransmission(address);

  int c = 0;
  for (int i = 0; i < strlen(cmd); i += 2) {
    sscanf(&cmd[i], "%02x", &c);
    Wire.write(c);
  }

  if (Wire.endTransmission() != 0) {
    MySerial->println(F("end trasmission failed"));
  }

  if(address == 112) 
  {
    if(cmd[1] == '0')
    {
      MySerial->println(F("OK - unlocked"));
      IsLocked = false;
      }
    else
    {
      MySerial->println(F("OK - locked"));
      IsLocked = true;
    } 
  }
  else 
  {
    MySerial->println(F("OK"));
  }
}

and for read:

// I2C read
void cmdI2Cread(int address, unsigned int nBytes) {
  delay(15);
  Wire.requestFrom(address, nBytes);

  unsigned char c;
  char cstr[4];

  // check if the same number of bytes are received that are requested.
  if (Wire.available() != nBytes) {
    MySerial->println("Requested bytes not returned");
  }

  for (unsigned int i = 0; i < nBytes; i++) {
      c = Wire.read();
      sprintf(cstr, "%02x", c);
      MySerial->print(cstr);
  }
  MySerial->println();
}

The command: Wire.available() != nBytes is sometimes satisfied and basically I receive no response from the SHT

Both the master and Rpi serials are set to a baud rate of 115200.

I have tried changing cables, shielding cables, reducing the baud rate, implementing some kind of lock mechanism...

The problem doesn't seem to appear when I disconnect either the RPi or the master from the slave (so it seems there is some kind of cross talk.. not sure).

I am wondering if this could also be hardware related and that the serial connection might not be the best choice for an arduino master-arduino slave communication.

Any help would be appreciated

Thank you

did you connect the GND between the 2 arduinos?
how are things powered?

without the full code hard to help

You're right. The code is rather long and it's a bit complicated to provide a working version here. But the main loop of the slave code where the communication switch from master arduino to rpi occurs should be shared:

// The main loop looks for commands
void loop() {
    unsigned int length = 0;
    // Priority goes to interlock commands
    if (StringComplete_INT && (!IsLocked || MySerial == &Serial2)) {
        MySerial = &Serial2;
        strncpy(command_int, Command_INT.c_str(),
                sizeof(command_int));  
        length = Command_INT.length();
        Command_INT = "";
        StringComplete_INT = false;

        // There is a command! Process it... 
        char *c = command_int;
        while (*c) {
            *c = toupper(*c);
            c++;
        }
        command_int[length - 2] = '\0';
        strncpy(command, command_int, sizeof(command_int));
        runcommand();
    }
    // need some elseif statement to handle when we start listening again on the
    // serial we used to ignore due to the isLocked

    else if (StringComplete_RPI && (!IsLocked || MySerial == &Serial)) {
        MySerial = &Serial;
        strncpy(command_rpi, Command_RPI.c_str(),
                sizeof(command_rpi)); 
        length = Command_RPI.length();
        Command_RPI = "";
        StringComplete_RPI = false;

        // There is a command! Process it...
        char *c = command_rpi;
        while (*c) {
            *c = toupper(*c);
            c++;
        }
        command_rpi[length - 2] = '\0';
        strncpy(command, command_rpi, sizeof(command_rpi));
        runcommand();
    }
}

/*
  SerialEvent occurs whenever a new data comes in the hardware serial RX. This
  routine is run between each time loop() runs, so using delay inside loop can
  delay response. Multiple bytes of data may be available.
*/
void serialEvent() {
    // RPI serial
    while (Serial.available()) {
        // get the new byte:
        char inChar = (char)Serial.read();
        // add it to the command
        Command_RPI += inChar;
        // if the incoming character is a newline, set a flag so the main loop
        // can do something about it:
        if (Command_RPI.c_str()[Command_RPI.length() - 2] == '\r' &&
            Command_RPI.c_str()[Command_RPI.length() - 1] == '\n') {
            StringComplete_RPI = true;
            break;
        }
    }
}
void serialEvent2() {
    // INT serial
    while (Serial2.available()) {
        // get the new byte:
        char inChar = (char)Serial2.read();
        // add it to the command
        Command_INT += inChar;
        // if the incoming character is a newline, set a flag so the main loop
        // can do something about it:
        if (Command_INT.c_str()[Command_INT.length() - 2] == '\r' &&
            Command_INT.c_str()[Command_INT.length() - 1] == '\n') {
            StringComplete_INT = true;
            break;
        }
    }
}

Basically commands from master arduino (INT because supposed to serve as interlock) and RPi are treated in such a way that if one port is available, then lock communication until the whole command is processed, then switch to the other port (in principle). runcommand() was coded to take care of various commands. Here the ones we are interested in are cmdI2Cread and cmdI2Cwrite.
The boolean isLocked should serve as the locking system though we are not very sure it's providing the expected result.
SerialEvent(2)() gathers the next command and is specific to each serial port (here using Serial for RPi and Serial2 for the arduino master.

Also, to answer the first question, the slave is powered independently via power supply while the arduino master is powered via USB since we are still running tests via the Arduino IDE on PC.

Hope it's a bit clearer this way!

UART communication requires the same GND, so on top of Rx and Tx you need to connect GND between the two MEGAs

Thank you for your reply! "Unfortunately" both arduinos are connected to GND via 4-pin cables, that doesn't seem to be the issue...

the question is "is it the same GND"?

can you post an actual schema of your wiring / power

Yes, it's the same GND. For the setup I need to get a proper photo of it, at the moment I am not close to it.

I may have a question on the i2c locking, we are using a boolean, but I don't think this is a very effective system. Could a mutex lock be used on arduinos?

A drawing with a ruler a pen with just the used pins and power description is better than a picture

Hi, apologies for the delay. Here's a very simplified scheme of the setup, where only the connection between slave and master arduino is shown. However I can add the connections to the sensors if needed. Basically both arduinos are connected using this "mega shield". Hope this is understandable!

so the 4 wires are GND, 5V, Rx and Tx ?

Thank you for your prompt reply! They are GND, VCC, Rx, Tx.

OK so the GNDs are connected ( I would be worried about connecting the 5V pins on both sides together) and aren’t those ports the same on both so you connected Rx to Rx and Tx to Tx or is the cable twisted in some way?

Yes, you can't see it well here but the cable is twisted, so Rx is actually connected to Tx and vice versa.

OK you should remove the 5V wire as I can't see it doing any good to have them connected
Why do you have a power supply on the slave ? it does get power from the USB from the RPi

have you tried a simple code to see if the Serial coms work:

Python code for example:
RPI is listening for keyboard entries and sending it to Slave
RPI is listening for Serial input from the Slave and printing it on screen

I've this code from my "tutorial" in French

#!/usr/bin/python3

# ============================================
# code is placed under the MIT license
#  Copyright (c) 2023 J-M-L
#  For the Arduino Forum : https://forum.arduino.cc/u/j-m-l
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.
#  ===============================================


import sys, threading, queue, serial
import serial.tools.list_ports

baudRate = 115200
arduinoQueue = queue.Queue()
localQueue = queue.Queue()

def selectArduino():
    ports = serial.tools.list_ports.comports()
    choices = []
    print('PORT\tDEVICE\t\t\tMANUFACTURER')
    for index,value in enumerate(sorted(ports)):
        if (value.hwid != 'n/a'):
            choices.append(index)
            print(index, '\t', value.name, '\t', value.manufacturer) # https://pyserial.readthedocs.io/en/latest/tools.html#serial.tools.list_ports.ListPortInfo

    choice = -1
    while choice not in choices:
        answer = input("➜ Select your port: ")
        if answer.isnumeric() and int(answer) <= int(max(choices)):
            choice = int(answer)
    print('selecting: ', ports[choice].device)
    return ports[choice].device


def listenToArduino():
    message = b''
    while True:
        incoming = arduino.read()
        if (incoming == b'\n'):
            arduinoQueue.put(message.decode('utf-8').strip().upper())
            message = b''
        else:
            if ((incoming != b'') and (incoming != b'\r')):
                 message += incoming

def listenToLocal():
    while True:
        command = sys.stdin.readline().strip().upper()
        localQueue.put(command)

def configureUserInput():
    localThread = threading.Thread(target=listenToLocal, args=())
    localThread.daemon = True
    localThread.start()


def configureArduino():
    global arduinoPort
    arduinoPort = selectArduino()
    global arduino
    arduino = serial.Serial(arduinoPort, baudrate=baudRate, timeout=.1)
    arduinoThread = threading.Thread(target=listenToArduino, args=())
    arduinoThread.daemon = True
    arduinoThread.start()

# ---- CALLBACKS UPON MESSAGES -----

def handleLocalMessage(aMessage):
    print("=> [" + aMessage + "]")
    arduino.write(aMessage.encode('utf-8'))
    arduino.write(bytes('\n', encoding='utf-8'))

def handleArduinoMessage(aMessage):
    print("<= [" + aMessage + "]")

# ---- MAIN CODE -----

configureArduino()                                      # will reboot AVR based Arduinos
configureUserInput()                                    # handle stdin 

print("Waiting for Arduino")

# --- A good practice would be to wait for a know message from the Arduino
# for example at the end of the setup() the Arduino could send "OK"
while True:
    if not arduinoQueue.empty():
        if arduinoQueue.get() == "OK":
            break
print("Arduino Ready")

# --- Now you handle the commands received either from Arduino or stdin
while True:
    if not arduinoQueue.empty():
        handleArduinoMessage(arduinoQueue.get())

    if not localQueue.empty():
        handleLocalMessage(localQueue.get())

The master just echoes what it gets from the console (Serial) to Serial1 (Slave)

void setup() {
  Serial.begin(115200); 
  Serial1.begin(115200);
}

void loop() {
  if (Serial.available())  Serial1.write(Serial.read());
}

The slave is listening on both Serial and Serial1 and buffering until it gets a full line and then echo that back to the RPI on Serial, something like this (typed here, fully untested)

#define RPI Serial
#define MASTER Serial1

const byte maxMessageSize = 50;
char messageFromRPI[maxMessageSize + 1];
char messageFromMaster[maxMessageSize + 1];

bool gotMessageFromRPI() {
  static byte index = 0;
  bool messageReady = false;

  int r = RPI.read();

  if (r != -1) { // got something
    if (r == '\n') { // end of line terminates the message
      messageFromRPI[index] = '\0';
      index = 0; // get ready for next time
      messageReady = true;
    } else {
      if (index < maxMessageSize) messageFromRPI[index++] = r; // don't overflow
    }
  }
  return messageReady;
}

bool gotMessageFromMaster() {
  static byte index = 0;
  bool messageReady = false;

  int r = RPI.read();

  if (r != -1) { // got something
    if (r == '\n') { // end of line terminates the message
      messageFromMaster[index] = '\0';
      index = 0; // get ready for next time
      messageReady = true;
    } else {
      if (index < maxMessageSize) messageFromMaster[index++] = r; // don't overflow
    }
  }
  return messageReady;
}


void setup() {
  RPI.begin(115200);
  MASTER.begin(115200);
  Serial.println("OK"); // let the python code on RPi know we are ready
}

void loop() {
  if (gotMessageFromRPI()) {
    RPI.print(F("Message from RPI: "));
    RPI.println(messageFromRPI);
  }

  if (gotMessageFromMaster()) {
    RPI.print(F("Message from MASTER: "));
    RPI.println(messageFromMaster);
  }
}

Have you implemented a way to prevent the master arduino and the RPI from both sending commands simultaneously?

Does executing a command from one of the devices take long enough that a command from the other device can overflow the serial Rx buffer?

Can you implement some type of checksum or other mechanism to ensure a command is received without error? Sending a corrupt command to the I2C devices would be problematic.

Personally I would eliminate the use of String and use char arrays for receiving the serial data.

(if those questions are for me)

There is no need for that. The hardware serial line and the associated buffer is deep enough to accommodate for some level of synchronicity. What matters is that you don't write blocking code. listen to what's available when it's available. If there is nothing available then move on and do something else. that's what you see in the loop here

or on the python side with queues and threads

that could be the risk if indeed the execution of a command is long. Again you should not write long blocking code. if something requires time, write that as a state machine for example and let the loop() loop to check other stuff along the way

sure, the code I shared above collects everything until a new line '\n' is received. You could define a frame format that adds some information to verify the integrity.

there is no String in my arduino code above

The questions were for the OP.

OK - sorry for that

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