1

I'm working on a library for Arduino/ESP8266/Teensy that use serial to send AT commands. I want to make it as customizable as possible, so I avoided to hardcode the serial and I ask for a reference in the startup method:

void MyClass::init(HardwareSerial* hwSerialPtr)
{
   _serialPtr = hwSerialPtr;
   ...
}

On some devices, it will be useful to use SoftwareSerial instead of the HW one, due to only one serial is available and so keep it for debugging purposes or other features.

So my question is: how can I accept both types? I read both HW and SW serial have Stream as base-clase, but it does not have methods like begin() etc.

A possible solution I though is so have two pointers (one for HWSerial and one for SWSerial) then wrap some methods like begin(), println(), ... in my class and, ond the base of which pointer has been set, use one or the other.

Something like this:

class MyClass{
   public:
      void init(HardwareSerial* hwSerialPtr);
      void init(SoftwareSerial* swSerialPtr);

   private:
      HardwareSerial* _hwSerialPtr;
      SoftwareSerial* _swSerialPtr;

      void begin(long baud);
      void println(const String &s)
};

then

void MyClass::begin(long baud)
{
   if( _hwSerialPtr != null ){ _hwSerialPtr->begin(baudrate); }
   else if( _swSerialPtr != null ){ _swSerialPtr->begin(baudrate); }
}

...etc...

but this solution seems to me very tricky and not optimized.

Any better ideas?

5

The trick here is to go "up a level". Both HardwareSerial and SoftwareSerial inherit from the Stream class. It is that class that provides the majority of the interface that you actually use.

So you can provide a constructor with a Stream object alone and use that:

class Foo {
    private:
        Stream *_dev;

    public:
        foo(Stream *dev) : _dev(dev) {}
        foo(Stream &dev) : _dev(&dev) {}

        void begin() {
            _dev->println("I am ready");
        }
};


Foo myFoo(Serial1);
SoftareSerial btSerial(11,12);
Foo myOtherFoo(btSerial);

What the Stream class doesn't provide, though, is .begin() for setting the baud rate. It has to be left to the user to configure the port before using it.

Serial1.begin(115200);
myFoo.begin(); // --> "I am ready" on Serial 1

or:

btSerial.begin(9600);
myOtherFoo.begin(); // --> "I am ready" on Software serial pins 11/12.

This is known as polymorphism, where an object is seen as one of the parent classes in the inheritance chain and is one of the most powerful aspects of object-oriented programming.

Not having begin in your library is no big deal really. It increases the flexibility (a user can more easily select their baud rate), and it means you don't force them to include SoftwareSerial if they don't need it.

There are other things that inherit Stream, as well, such as a networking Client. So instantly you class can be used with network connections as well as just serial.

So stick to using Stream. It's the accepted way of doing things, and perfectly normal. For example, it's how my ICSC library handles serial objects. (I used to use the multiple pointer method, but it really really got messy when trying to support other platforms. I ended up with HardwareSerial, SoftwareSerial, USBSerial, Client... a real mess. So much simpler to just use Stream.)


BTW, before you ask, I have two constructors with very slight differences so that you can construct an object either by passing a Stream object by reference (as I do in the examples) or as a pointer (which can be great if you want to use this in another class which itself has a pointer to a Stream object).


Another option is to combine the Stream pointer with a dynamic_cast to try and identify the type of object being passed.

void Foo::begin(int baud) {
    if (HardwareSerial *hs = dynamic_cast<HardwareSerial *>(_dev)) {
        hs->begin(baud);
    } else if (SoftwareSerial *ss = dynamic_cast<SoftwareSerial *>(_dev)) {
        ss->begin(baud);
    } else { 
        // We don't know what to do here
        // so do nothing?
    }
}

In this way dynamic_cast is being used like Java's instanceof operator to work out what kind of object it is that is being used.

Of course, that then means that you're really restricting the library to only working with those two types of object, but it is still possible to pass other class types as long as the user pre-configures them before calling your begin function. Which gets confusing - which type does the user need to .begin() and which types have .begin() called for you? It's just cleaner to always have it as the user must call .begin() themselves - there's no ambiguity then.

2

I second Majenko's advice: inherit from Stream and let the user handle the call to begin() if at all possible. However, if you really insist in calling begin() from your class, then I suggest you make your class a template, like this:

// T is the type of the serial port, typically HardwareSerial or
// SoftwareSerial.
template<class T> class MyClass {
    public:
        MyClass(T& port) : port(port) {}
        void begin(unsigned long baud) { port.begin(baud); }
    private:
        T& port;
};

Your user would then instantiate it as

MyClass<HardwareSerial> myObject(Serial);
MyClass<SoftwareSerial> myOtherObject(mySerial);

Of course, it doesn't look nice, and it may confuse your users, but it has the advantage that it can work with anything that remotely looks like HardwareSerial, e.g. AltSoftSerial or other alternative software serial implementations.

Your Answer

By clicking "Post Your Answer", you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.