Как написать библиотеку, которая поддерживает как последовательную связь HW, так и SW и позволяет пользователю выбирать, какую из них использовать?

Я работаю над библиотекой для Arduino/ESP8266/Teensy, которая использует последовательный для отправки команд AT. Я хочу сделать его максимально настраиваемым, поэтому я избегал жесткого кода серийного номера и прошу ссылку в методе запуска:

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

На некоторых устройствах будет полезно использовать SoftwareSerial вместо HW, поскольку доступен только один последовательный, поэтому сохраните его для целей отладки или других функций.

Итак, мой вопрос: как я могу принять оба типа? Я читал, что как HW, так и SW serial имеют поток в качестве базового класса, но в нем нет таких методов, как begin() и т. Д.

Возможное решение, которое я считаю, состоит в том, чтобы иметь два указателя (один для HWSerial и один для SWSerial), затем обернуть некоторые методы, такие как begin (), println (), ... в моем классе и, на основе которого был установлен указатель, использовать один или другой.

Что-то вроде этого:

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)
};

затем

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

...etc...

но это решение кажется мне очень сложным и не оптимизированным.

Есть идеи получше?

, 👍3

Обсуждение

и то, как используются примеры библиотеки WiFiEsp, не очень хорошо? https://github.com/bportaluri/WiFiEsp/blob/master/examples/WebClient/WebClient.ino, @Juraj

почему вы хотите заняться этим в библиотеке? и существуют альтернативные последовательные реализации программного обеспечения, @Juraj


2 ответа


6

Хитрость здесь в том, чтобы подняться "на уровень выше". Как HardwareSerial, так и SoftwareSerial наследуются от класса Stream. Именно этот класс обеспечивает большую часть интерфейса, который вы фактически используете.

Таким образом, вы можете предоставить конструктору только потоковый объект и использовать его:

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);

Однако класс Stream не предоставляет .begin() для настройки скорости передачи в бодах. Пользователю необходимо предоставить возможность настроить порт перед его использованием.

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

или:

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

Это известно как полиморфизм, когда объект рассматривается как один из родительских классов в цепочке наследования и является одним из наиболее мощных аспектов объектно-ориентированного программирования.

Отсутствие начала в вашей библиотеке на самом деле не имеет большого значения. Это повышает гибкость (пользователю легче выбрать скорость передачи данных), и это означает, что вы не заставляете их включать программное обеспечение, если оно им не нужно.

Существуют и другие вещи, которые наследуют поток, например сетевой клиент. Таким образом, мгновенно ваш класс можно использовать как с сетевыми подключениями, так и просто с последовательными.

Так что придерживайтесь использования потока. Это общепринятый способ ведения дел, и совершенно нормальный. Например, так моя библиотека ICSC обрабатывает последовательные объекты. (Раньше я использовал метод с несколькими указателями, но он действительно стал запутанным при попытке поддерживать другие платформы. В итоге яполучил HardwareSerial, SoftwareSerial, USBSerial, Клиент... настоящий беспорядок. Так намного проще просто использовать поток.)


Кстати, прежде чем вы спросите, у меня есть два конструктора с очень небольшими различиями, так что вы можете создать объект либо передавая потоковый объект по ссылке (как в примерах) или в качестве указателя (который может быть большим, если вы хотите использовать это в другом классе, который имеет указатель на объект потока).


Другой вариант - объединить указатель потока с dynamic_cast, чтобы попытаться определить тип передаваемого объекта.

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 { 
        // Мы не знаем, что здесь делать
        // значит, ничего не делать?
    }
}

Таким образом, dynamic_cast используется как оператор instanceof Java для определения типа используемого объекта.

Конечно, это означает, что вы действительно ограничиваете библиотеку работой только с этими двумя типами объектов, но по-прежнему можно передавать другие типы классов, если пользователь предварительно настроит их перед вызовом функции begin. Что приводит к путанице - какой тип нужен пользователю .begin() и какие типы .begin() вызываются для вас? Просто чище всегда иметь его, так как пользователь должен сам вызывать .begin () - тогда нет никакой двусмысленности.

,

1

Я повторяю совет Майенко: унаследуйте от Stream и позвольте пользователю обработать вызов begin (), если это вообще возможно. Однако, если вы действительно настаиваете на вызове begin() из своего класса, я предлагаю вам сделать свой класс шаблоном, например:

// T - тип последовательного порта, обычно HardwareSerial или
// SoftwareSerial.
template<class T> class MyClass {
    public:
        MyClass(T& port) : port(port) {}
        void begin(unsigned long baud) { port.begin(baud); }
    private:
        T& port;
};

Затем ваш пользователь создаст его как

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

Конечно, это выглядит не очень красиво и может сбить с толку ваших пользователей, но у него есть преимущество в том, что он может работать со всем, что отдаленно похоже на HardwareSerial, например, с AltSoftSerial или другими альтернативными последовательными реализациями программного обеспечения.

,