How precisely pass pointer for a Serial port

I made a small library that I use for rs485 communication, nothing too fancy. The library can use either the hardware or a software serial port. My code works but I am 'doing it wrong'.

The top part is as follows:

#include "message.h"

#define SOFTWARE_SERIAL

#ifdef SOFTWARE_SERIAL
#include <SoftwareSerial.h>
const int   Rx   =  3 ;
const int   Tx   =  4 ;
SoftwareSerial RS485( Rx, Tx ) ;
#else
#define RS485 Serial
#endif


bool        writeBlock ;
char        message[32] ;
uint16_t    COMMAND ;
uint16_t    SLAVE_ID ;
uint16_t    DATA1 ;
uint16_t    DATA2 ;
uint8_t     rs485dir ;

void initializeSerialport( uint8_t pin )
{
    rs485dir = pin ;
    pinMode( rs485dir, OUTPUT ) ;
    RS485.begin( 9600 ) ;
}

void sendMessage( uint8_t ID, 
                  uint8_t COMMAND, 
                  uint8_t DATA1, 
                  uint8_t DATA2, 
                  uint8_t blockCode )
{
    if( COMMAND == 0 ) return ; // prevents 'empty' initialization messages from being transmitted

    digitalWrite( rs485dir, HIGH ) ;
    RS485.print(      ID ) ; RS485.write(',') ;
    RS485.print( COMMAND ) ; RS485.write(',') ;
    RS485.print(   DATA1 ) ; RS485.write(',') ;
    RS485.println( DATA2 ) ;
    
    if( blockCode )             // during initialization, the code is blocked so no overflow can occur
    {   
        RS485.flush() ;
        digitalWrite( rs485dir, LOW ) ;
    }
}

The problem is that I have to define or not define SOFTWARE_SERIAL in the library's source file in order to select a serial port.

I would prefer to use the function initializeSerialport() to pass a pointer to a serial object instead. This would also allow me to re-use this library for boards that have more than 1 hardware serial port without having to alter the source file. But I do not know how to do this.

For instance. I am guessing that my library needs a static serial object? How do I declare that?

static Stream *RS485 ; // ?

I get that I must call initializeSerialPort() with

initializeSerialPort( pin, &Serial ) ; // or &mySoftWareSerial, or &Serial1 etc
..
void initializeSerialPort( uint8_t pin, Stream *port) 
{
    RS485 = &port ; // ??
}
..
..
void sendMessage( uint8_t ID, 
                  uint8_t COMMAND, 
                  uint8_t DATA1, 
                  uint8_t DATA2, 
                  uint8_t blockCode )
{
    if( COMMAND == 0 ) return ; // prevents 'empty' initialization messages from being transmitted

    digitalWrite( rs485dir, HIGH ) ;
    RS485->print(      ID ) ; RS485->write(',') ;  //  from . to   ->
    RS485->print( COMMAND ) ; RS485->write(',') ;
    RS485->print(   DATA1 ) ; RS485->write(',') ;
    RS485->println( DATA2 ) ;
    
    if( blockCode )             // during initialization, the code is blocked so no overflow can occur
    {   
        RS485->flush() ;
        digitalWrite( rs485dir, LOW ) ;
    }
}

I am aware that software serial tends to block the entire code. For my application that is not really important. I also will not receiving and transmitting at the same time.

Any 'pointers' about my code will be appreciated.

Kind regards,

Bas

you could pass a Stream reference and your class would handle both Hardware or Software Serial without knowing :slight_smile:

class != library. This particular library ain't OO :smiley:

I usually use pointers because... i don't grasp the reference version. I get that it does the same in the end but it looks different.

Does it matter somewhow if I try to use a pointer or reference? Other than that I would have to type a -> instead of a . ??

So by reference..

Does that go like:

static Stream RS485 ;
..
void initializeSerialPort( uint8_t pin, Stream &port )
{
    RS485 = port ; // ?? do I need to use an & somewhere??
    RS485 .begin( 9600 ) ;
}

..
// and call with?
initializeSerialPort( Serial ) ; // or  mySoftwareSerial or Serial2 etc ??

Regards,

Bas

No.

First, not a problem per se, but an opinion ... if this were my project I'd wrap it up in a class.

Now, the problems.

  • The Stream class does not have a begin() function because not all classes that inherit from Stream have or need one. So, the Stream class you're using (HardwareSerial, SoftwareSerial, USBSerial, etc) should be initialized in the main code because only your main code know which Steam class object you want to use.

  • It makes no sense to try copy assignment (or copy construction) on a Stream class object. That would result in multiple objects trying to control the same hardware.

  • References can only be bound when they're created. So, in your case, you'll need to use a pointer.

So:

#include <SoftwareSerial.h>

void specifyPort(Stream &s);
void sendMessage(const char *m);

const uint8_t rx = 3 ;
const uint8_t tx  =  4 ;
SoftwareSerial RS485(rx, tx) ;

void setup() {
  RS485.begin(9600);
  specifyPort(RS485);
  sendMessage("Hello World");
}

void loop() {
}

static Stream *stream;

void specifyPort(Stream &s) {
  stream = &s;
}

void sendMessage(const char *m) {
  stream->println(m);
}

indeed but you can still pass a parameter by reference. The main difference is that a reference can't be null whereas you could pass a null pointer to your function

As @gfvalvo said, Your library would deserve to be a class though as you want a local variable (set through initializeSerialPort()) which would mean you could not use your functions with 2 destinations since you would have only one variable.

I understand your arguments about using a class. But this code is so specific to this particular project. It may send messages up to 5 comma separated messages in ascii format. The chance that I will be reusing these files is slim + I highly doubt I will ever need more than 1 RS485 interface per board.

Anyways, the fact that the stream class does not contain a begin() method, brings more problems. In order to drive the direction pin after a message is sent, I use 2 methods (1 blocking, 1 not blocking) which are also not present in the stream class.

In the send function I may use flush(). During booting, the master device needs to send alot more bytes than can fit in the transmitt buffer. So I decided to block my code by using flush() during this process.

void sendMessage( uint8_t ID, 
                  uint8_t COMMAND, 
                  uint8_t DATA1, 
                  uint8_t DATA2, 
                  uint8_t blockCode )
{
    if( COMMAND == 0 ) return ; // prevents 'empty' initialization messages from being transmitted

    digitalWrite( rs485dir, HIGH ) ;
    RS485.print(      ID ) ; RS485.write(',') ;
    RS485.print( COMMAND ) ; RS485.write(',') ;
    RS485.print(   DATA1 ) ; RS485.write(',') ;
    RS485.println( DATA2 ) ;
    
    if( blockCode )             // during initialization, the code is blocked so no overflow can occur
    {   
        RS485.flush() ;    // <-- Steam ain't go no flush() method.
        digitalWrite( rs485dir, LOW ) ;
    }
}

During normal operation, I am using availableForWrite() to determen when the transmitt buffer is empty.

void updateTransmissionLine( ) 
{
    if( RS485.availableForWrite() == 63 
    &&  digitalRead( rs485dir ) )
    {
        digitalWrite( rs485dir, LOW) ;
    }
}

Given the fact that neither availableForWrite() nor flush() exist in Stream class, I am thinking that in this particular case I am better of with the macros :smiling_face_with_tear:.

Anways thanks for the lessons, much informative!

Regards,

Bas

it's not more complicated to create a class than a collection of functions and global variables (which risks clashing with similar named variables in the main code)... So even if you don't reuse it much and have only one instance of that class, it is still relevant

if you don't want to, C++ also supports overloading so you could have functions named the same way but with HardwareSerial or SoftwareSerial as parameters if you need to differentiate.

may be your library could be in a .hpp file

communication.hpp

#ifndef COMMUNICATION_HPP
#define COMMUNICATION_HPP

#include <Arduino.h>
#include <SoftwareSerial.h>

static HardwareSerial* hwPort = nullptr;
static SoftwareSerial* swPort = nullptr;
static Stream* comPort = nullptr;

static enum : byte {None, usingHardwareSerial, usingSoftwareSerial} configuration = None;

void resetConfiguration() {
  if (hwPort != nullptr) {
    hwPort->end();
    hwPort = nullptr;
  }
  if (swPort != nullptr) {
    swPort->end();
    swPort = nullptr;
  }
  comPort = nullptr;
  configuration = None;
}

void prepareComPort(HardwareSerial& s, unsigned long baud = 115200ul) {
  resetConfiguration();
  hwPort = &s;
  comPort = hwPort;
  hwPort->begin(baud);
  configuration = usingHardwareSerial;
}

void prepareComPort(SoftwareSerial& s, unsigned long baud = 115200ul) {
  resetConfiguration();
  swPort = &s;
  comPort = hwPort;
  swPort->begin(baud);
  configuration = usingSoftwareSerial;
}

// you can add other functions here, using comPort-> for print() or write() or available() for example

#endif

your .ino file could then do

#include "communication.hpp"
#include <SoftwareSerial.h>
SoftwareSerial RS485(2, 3);

void setup() {
  prepareComPort(Serial);
  comPort->print("Hello World"); // Send over Serial

  prepareComPort(RS485);
  comPort->print("Hello World"); // Send over SS
}

void loop() {}

I understand what you are doing but I won't be able to use flush() and availableForWrite() in combination with comPort, will I?

However it can be combined. I could use comPort->print() ; and for flush() and availableForWrite() I can simply use an if-else statement like:

void updateTransmissionLine( ) 
{
    if((( configuration = usingHardwareSerial && hwPort.availableForWrite() == 63 )
    ||  ( configuration = usingSoftwareSerial && swPort.availableForWrite() == 63 ))
    &&  digitalRead( rs485dir ) )
    {
        digitalWrite( rs485dir, LOW) ;
    }
}

I suppose that is viable.

btw, Why would I want to use a .hpp file? as in what is the difference between .h and .hpp. I have never used the latter and I know that .h works fine under cpp.

Regards,

Bas

I don't see how the first of those ('begin() method) is connected to the other (control of the direction pin)???

begin() is not related but flush() and availableForWrite() are. These are used to set the direction line back to receiving at the earliest possible moment.

Because you pointed me on the fact that Stream has no begin() member, I realized that Stream also does not have flush() nor availableForWrite().

So this

static Stream* comPort
..
comPort->flush() ;
comPort->availableForWrite() ; 

will all fail.

The Print class defines those two functions (Print.h):

virtual int availableForWrite() { return 0; }
.
.
virtual void flush() {

Stream inherits from Print (Stream.h):

class Stream : public Print

So, in fact, Stream does have them and any class that inherits from Stream can implement (overload) them as needed.

o that is nice

as @gfvalvo said, you could

it's easier to get everything posted here in one file :slight_smile:

BTW on this

I'm not sure what you mean.

  • flush() blocks until everything is sent
  • availableForWrite() tells you the number of bytes available for writing in the serial buffer without blocking the write operation

Look at the code examples in post #6.

The function sendMessage() has the option to use flush() depending if the argument blockCode is true or false. Upon booting the master needs to send much more data than the buffer can hold, hence I decided to use flush() to wait for the data to be sent.

During normal operation, the master does not wait. The function updateTransmissionLine() is called continously.

This line tells me when the transmitt buffer is empty or not

if( RS485.availableForWrite() == 63  // when this is true, the buffer is empty and the rs485 dir pin can be put on receiving.
&&  digitalRead( rs485dir ) )

if the buffer is full, print() becomes blocking so it does not really matter that you send too much data during the boot phase. Your code will be blocked in the print() function until all the bytes have been pushed into the outgoing buffer

A serial port is always handled asynchronously (in correct coding) so you should not worry about what's available in the buffer unless you don't want your code to be blocked in the print() statement. typically you would not wait for the empty buffer, you would test if there is enough room is the buffer to send you payload and not be blocked

if (payloadSize <= RS485.availableForWrite()) RS485.write(payload, payloadSize);
else /* come back to it later */

Crap, I forgot about that. Tnx :wink: