Начинающим на Arduino: Упаковываем конечный автомат в отдельный класс и библиотеку

    В прошлой статье про написание конечных автоматов я обещал упаковать наш гениальный код в виде класса на C++ для повторного удобного использования. Делать буду так же на примере своей старой разработки SmartButton. Итак, влезаем в непонятный мир ардуининых библиотек и ООП.


    Папки с библиотеками


    Зачем всё это нужно?


    Arduino IDE позволяет использовать синтаксис C++11, оказывается. То есть, там очень развитый объектно-ориентированный язык. Нам же хочется сосредотачиваться на нашем гениальном коде и размазанная по программе лишняя логика частенько мешает сосредоточиться. Взять, например, всякие дисплейчики, кнопочки, датчики и релюшки — у каждого же своя логика, зачем её смешивать с общей логикой программы. Тот же, например, дисплей. У него много полей, статических и изменяемых. Ой, поле — это же класс. Поле может входить в меню (класс меню) или нет, быть часть частью виртуального дисплея (класс), которых на физическом эеране может быть насколько (дисплеи: рабочий, настроек, диагностики и т.п.). Меню, в свою очередь, управляется кнопками (классы кнопок могут быть разными) или джойстиком (класс). Всё это вместе — класс "дисплей", который можно объявить в своей программе как:


    #include "Display.h"
    Display disp(куча параметров и настроек);

    Если вы делаете проект не совсем на коленке не кое как и собираетесь что-то потом менять или повторно использовать какие-то свои наработки — лучше оформить сделанное в виде библиотек Arduino. В идеале, конечно же, положить в Github для других людей, если вам не жалко и вы не против, что кто-то ваш код исправит или дополнит.


    Раз уж мы в прошлой статье делали кнопочку, давайте её оформим как класс и библиотеку?


    Итак, наша задача сделать так, чтобы мы могли в своих скетчах писать:


    #include "myButton.h"
    myButton b1(4),b2(5),b3(12); // три кнопки на пинах 4, 5 и 12.
    
    loop() {
      b1.run();
      b2.run();
      b3.run();
      // ...
      if (b1.clicked()) doSomething(); // так или другим каким способом, есть варианты.
      // ...
    }

    Как сделать библиотеку Arduino?


    Это просто!


    Сначала надо решить, как ваша библиотека будет называться. Пусть для примера, это будет MyLib.


    Найдите, где лежат ваши скетчи на компьютере. Они лежат каждый в своей папочке, а рядом с ними есть папка libraries (библиотеки). Например, на маке /Users/Пользователь/Documents/Arduino/libraries и на виндоусе c:\Users\Пользователь\Документы\Arduino\libraries. Я сам сижу на маке и пути в виндах не знаю. Найдёте.


    Вот в этой папке libraries создайте новую папку MyLib, то есть с именем своей библиотеки. Перейдите туда.


    В этой новой папке надо создать как минимум один файл MyLib.h, тот, что вы будет включать в ваш проект. Минимальное его содержимое выглядит примерно так:


    #ifndef MYLIB_H
    #define MYLIB_H
    
    #if ARDUINO >= 100
      #include <Arduino.h>
    #else
      #include <WProgram.h>
    #endif
    
    // Ваш код здесь
    
    #endif

    Расскажу, что здесь зачем. Конструкция ниже позволяет включать вашу библиотеку в код несколько раз без ошибок. Лучше использовать название вашей библиотеки большими буквами. Это не сурово прямо обязательно, но все так делают и вы не выделяйтесь. Задача стоит придумать уникальное слово, в нашем случае MYLIB_H, идентификатор для этого заголовочного файла.


    #ifndef MYLIB_H
    #define MYLIB_H
      // Ваш код
    #endif

    То есть, в вашем скетче может оказаться несколько таких строк:


    #include "MyLib.h"

    Вы скажете "тю, да я, да я слежу, да я..." и будете неправы. Лучше один раз написать в одном файле вот такую конструкцию, чем исправлять ваши готовые скетчи, если вдруг вы захотите вложить один в другой или ваша библиотека будет включена в другую итд. Данный код проверяет, определено ли слово MYLIB_H, если нет, то определяет его и включает дальнейший код. Если же слово уже определено, то второй раз код компилировать не нужно.


    Следующий важный кусок кода:


    #if ARDUINO >= 100
      #include <Arduino.h>
    #else
      #include <WProgram.h>
    #endif

    Включает определения из исполняющей системы Arduino UDE. Без этого ваша библиотека просто не скомпилируется.


    Всё. Закройте Arduino IDE, Откройте заново. Создайте новый скетч, пропишите там #include "MyLib.h" и ура, ваша библиотека есть и подключена!


    Я смотрел, в библиотеке вроде как много файлов должно быть?


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


    Чтобы я мог помещать сюда куски своего кода копипастом, я назову библиотеку SmartButton, ладно? Болванку MyLib можно прибить за ненадобностью.


    По аналогии с предыдущим пунктом, создаём папку SmartButton, в ней:


    • SmartButton.h — То, что мы будем включать в наши программы. Там будут только определения, без кода.
    • SmartButton.cpp — Программный код класса. Это не скетч! Обратите внимание, что расширение файла cpp (C++).
    • README.md — Файл описания библиотеки "для людей", то есть, документация. "md" означает MarkDown, то есть с разметкой. Достаточно назвать просто README.
    • library.json — описание библиотеки для Arduino IDE в хитром формате JSON.
    • examples — папка с примерами, которые будут потом видны в Arduino IDE. В ней должны лежать папки с именами примеров, в а них с тем же именем файлы с расширением ino — скетчи.

    Расположение файлов в папке libraries


    SmartButton.h


    #ifndef SMART_BUTTON_H
    #define SMART_BUTTON_H
    
    #if ARDUINO >= 100
     #include <Arduino.h>
    #else
     #include <WProgram.h>
    #endif
    
    // Можно выше до include переопределить эти значения
    #ifndef SmartButton_debounce
    #define SmartButton_debounce 10
    #endif
    #ifndef SmartButton_hold
    #define SmartButton_hold 1000
    #endif
    #ifndef SmartButton_long
    #define SmartButton_long 5000
    #endif
    #ifndef SmartButton_idle
    #define SmartButton_idle 10000
    #endif
    
    class SmartButton {
      // Это внутренние переменный класса.
      // Они свои у каждого объекта и конфликта 
      //   за имена переменных не будет.
      //   не надо выдумывать для каждой кнопки свои названия.
      private:
        byte btPin;
        // Точно, как мы делали в [предыдущей статье про МКА](https://habrahabr.ru/post/345960/)
        enum state {Idle, PreClick, Click, Hold, LongHold, ForcedIdle};
        enum input {Press, Release, WaitDebounce, WaitHold, WaitLongHold, WaitIdle};
        enum state btState = Idle;
        enum input btInput = Release;
        unsigned long pressTimeStamp;
    
      // Это скрытый метод, его снаружи не видно.
      private:
        void DoAction(enum input in);
    
      // Это то, чем можно пользоваться.
      public:
        // Конструкторы и деструкторы.
        // То есть то, что создаёт и убивает объект.
        SmartButton();
        SmartButton(int pin);
        SmartButton(int pin, int mode) {btPin=pin; pinMode(pin,mode);}
        ~SmartButton();
        // В стиле Arduino IDE определим метод begin
        void begin(int p, int m) {btPin=p; pinMode(p,m);}
        // Генератор событий для помещения в loop().
        void run();
    
        // Методы для переопределения пользователем.
      public:
        inline virtual void onClick() {};       // On click.
        inline virtual void onHold() {};        // On hold.
        inline virtual void onLongHold() {};    // On long hold.
        inline virtual void onIdle() {};        // On timeout with too long key pressing.
        inline virtual void offClick() {};      // On depress after click.
        inline virtual void offHold() {};       // On depress after hold.
        inline virtual void offLongHold() {};   // On depress after long hold.
        inline virtual void offIdle() {};       // On depress after too long key pressing.
    };
    
    #endif

    Давайте поясню суть затеи. Мы не знаем, что нам будет нужно от кнопки. Наш МКА умеет находиться в состояниях Клик, Нажатие, Удержание и СлишкомДолгоеУдержание, а так же выходить из этих состояний в состояние Выключен. Так как мы делаем библиотеку универсальную, то надо предоставить возможность другому программисту вставить свой код в обработчики состояний. В ООП есть для этого замечательное средство — наследование.


    Мы делаем класс, у которого есть несколько методов (функций) и они пустые. То есть, они есть, они будут вызываться в нужный момент, но кода в них нет. Зачем это? Затем, что в скетче можно будет создать свой класс на базе нашего, определить там только нужные из методов и наполнить их своим кодом.


    Например, мы захотим сделать кнопку-переключатель, то есть, одно нажатие — включено, другое — выключено. Будем зажигать и гасить светодиод и предоставим функцию isOn() для использования в классическом виде в функции loop().


    #include "SmartButton.h"
    
    #define LED_PIN (13)
    
    // Порождаем наш новый класс от SmartButton
    class Toggle : public SmartButton {
      private:
        byte sw = 0; // состояние переключателя
        byte led;      // нога для лампочки
      public:
        Toggle(byte bt_pin, byte led_pin) : SmartButton(bt_pin) { // конструктор.
          led=led_pin;
        };
    
        // Наши методы
    
        // Включена кнопка или нет.
        byte isOn() { return sw; } 
    
        // Что делать на клик.    
        virtual void onClick() {
          if (sw) {
            // Был включен. Выключаем.
            digitalWrite(led,LOW);
            // Здесь может быть любой ваш код на выключение кнопки.
          } else {
            // Был выключен. Включаем.
            digitalWrite(led,HIGH);
            // Здесь может быть любой ваш код на включение кнопки.
          }
          sw=!sw; // Переключаем состояние.
        }
    };
    
    // Объявляем переменную bt нашего нового класса. Можно не одну.
    Toggle bt(4,LED_PIN); // Нога 4, встроенный светодиод.
    Toggle drill(12,8) // Нога 12, светодиод на ноге 8.
    
    void loop() {
      bt.run();
      drill.run();
      if (bt.isOn()) {
        // что-то делать
      } else {
        // что-то другое делать
      }
        if (drill.isOn()) {
        // что-то делать 
      } else {
        // что-то другое делать
      }
    }

    Как видите, нас совершенно здесь не интересует МКА кнопочки из предыдущей статьи, кода этой кнопки нет, он спрятан. Мы добавили свою функциональность к базовому классу и сделали переключатель по клику. Наш новый класс Toggle тоже можно оформить в виде библиотеки, кстати или положить в отдельный файл Toggle.h рядом с вашим скетчем, вам достаточно будет его подключить директивой #include. Мы так же задаём ногу со светодиодом для подсветки кнопки. Обратите внимание, что мы просто создали два объекта (bt и drill) нового класса Toggle, а МКА обработки кнопки для нас скрыт и не заботит.


    Основываясь на классе SmartButton можно сделать свои классы, что понимают двойной клик, например, водят курсор по меню или поворачивают пулемётную турель медленно-быстрее в зависимости от времени удержания кнопки. Для этого достаточно определить свои методы, описанные в SmartButton.h как virtual. Все определять не обязательно, только нужные вам.


    По просьбе целевой аудитории, вот пример класса PressButton, который предоставляет методы:


    • pressed() — кнопка была нажата, можно вызывать много раз.
    • ok() — я понял, слушай кнопку дальше, то есть сброс.

    #include "SmartButton.h"
    
    #define LED_PIN (13)
    
    // Порождаем наш новый класс от SmartButton
    class PressButton : public SmartButton {
      private:
        byte sw = 0; // состояние переключателя
      public:
        PressButton(byte bt_pin) : SmartButton(bt_pin) {}; // конструктор.
    
        // Наши методы
    
        // Была кликнута кнопка или нет.
        byte pressed() { return sw; };
        // Я всё понял, слушаем кнопку дальше.
        void ok() { sw=0; };
    
        // Что делать на клик.    
        virtual void onClick() { sw=1; };
    };
    
    // Объявляем переменную bt нашего нового класса. Можно не одну.
    PressButton bt(4); // Нога 4.
    PressButton drill(12) // Нога 12.
    
    void loop() {
      bt.run();
      drill.run();
      if (bt.pressed()) {
        // что-то делать
        bt.ok();
      } else {
        // что-то другое делать
      }
        if (drill.pressed()) {
        // что-то делать
        if (какое_то_условие) drill.ok(); 
      } else {
        // что-то другое делать
      }
    }

    Таким образом мы получаем две независимо работающие "залипающие" кнопки, которые после нажатия находятся в состоянии pressed пока их не сбросить методом ok().


    Если у вас есть меню, вы можете определить методы onClick() у кнопок "вверх" и "вниз", которые будут вызывать перемещение курсора меню на дисплее с соответствующем направлении. Определение onHold() у них может вызывать перемещение курсора в начало и конец меню, например. У кнопки "ентер" можно определить onClick() как выбор меню, onHold() как выход с сохранением, а onLongHold() как выход без сохранения.


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


    SmartButton — это просто МКА, это инструмент для реализации поведения ваших кнопок.


    Где же скрыта вся магия? Магия кроется в файле SmartButton.cpp


    #include "SmartButton.h"
    
    // Конструктор и деструктор пустые.
    SmartButton::SmartButton() {}
    SmartButton::~SmartButton() {}
    // Конструктор с инициализацией.
    // Он используется чаще всего.
    SmartButton::SmartButton(int pin) {
      btPin = pin;
      pinMode(pin, INPUT_PULLUP);
    }
    
    // Машина конечных автоматов сидит здесь:
    
    // Обратите внимание - это ровно та же функция,
    // Что мы писали в [прошлой статье](https://habrahabr.ru/post/345960/).
    // Обратите внимание на вызов виртуальных функций on* и off*.
    void SmartButton::DoAction(enum input in) {
      enum state st=btState;
      switch (in) {
        case Release:
          btState=Idle;
          switch (st) {
            case Click:
              offClick();
              break;
            case Hold:
              offHold();
              break;
            case LongHold:
              offLongHold();
              break;
            case ForcedIdle:
              onIdle();
              break;
          }
          break;
        case WaitDebounce:
          switch (st) {
            case PreClick:
              btState=Click;
              onClick();
              break;
          }
          break;
        case WaitHold:
          switch (st) {
            case Click:
              btState=Hold;
              onHold();
              break;
          }
          break;
        case WaitLongHold:
          switch (st) {
            case Hold:
              btState=LongHold;
              onLongHold();
              break;
          }
          break;
        case WaitIdle:
          switch (st) {
            case LongHold:
              btState=ForcedIdle;
              break;
          }
          break;
        case Press:
          switch (st) {
            case Idle:
              pressTimeStamp=millis();
              btState=PreClick;
              break;
          }
          break;
      }
    }
    
    // А это наш генератор событий.
    // Его надо помещать в loop()
    void SmartButton::run() {
      unsigned long mls = millis();
      if (!digitalRead(btPin))  DoAction(Press);
      else  DoAction(Release);
      if (mls - pressTimeStamp > SmartButton_debounce) DoAction(WaitDebounce);
      if (mls - pressTimeStamp > SmartButton_hold) DoAction(WaitHold);
      if (mls - pressTimeStamp > SmartButton_long) DoAction(WaitLongHold);
      if (mls - pressTimeStamp > SmartButton_idle) DoAction(WaitIdle);
    }

    Логика местами спорная, я знаю :) Но это работает.


    Теперь осталось заполнить файл README описанием вашей библиотеки и заполнить по аналогии файлик library.json, где поля вполне очевидны:


    {
      "name": "SmartButton",
      "keywords": "button, abstract class, oop",
      "description": "The SmartButton abstract class for using custom buttons in Arduino sketches.",
      "repository": {
        "type": "git",
        "url": "https://github.com/nw-wind/SmartButton"
      },
      "version": "1.0.0",
      "authors": {
        "name": "Sergei Keler",
        "url": "https://github.com/nw-wind"
      },
      "frameworks": "arduino",
      "platforms": "*"
    }

    Если у вас нет репозитория, можно эту секцию не указывать.


    Ура! Библиотека готова. Можно запаковать папку в ZIP и раздавать друзьям или копировать на другие свои компьютеры.


    По аналогии, можно сделать класс для любой МКА. Принцип общий: вы делаете класс, определяете виртуальные методы, которые потом надо будет переопределить, чтобы вставить свой код или готовые методы, если универсальность не требуется.


    Что за Github и зачем он мне?


    Github — это огромное сообщество программистов. Да, ваш код будет публично светиться на весь интернет, но… любой человек может предложить свои правки к вашему коду. Мне, например, очень помогли с SmartDelay два человека, один из которых сделал свою подобную библиотеку и мы поподсматривали чуть-чуть код друг у друга. Лучше две хорошие библиотеки, чем две глюкавые, правда?


    Чтобы поместить вашу библиотеку в Github надо сделать там аккаунт, сгенерить ключ и создать репозиторий с там же именем, что ваша библиотека (папка). Файлы можно загрузить через web-шнтерфейс.


    Для установки библиотеки из Github в Arduino IDE достаточно скопировать URL и воспользоваться утилитой git:



    Или загрузить ZIP — это будет как раз библиотека Arduino, как и все прочие библиотеки.


    Как пользоваться git вообще и Github в частности, есть много статей наверняка. Попробуйте поискать. Если не найдёте, я напишу как им пользуюсь я.

    Only registered users can participate in poll. Log in, please.

    Хороший ли это подход?

    Share post

    Similar posts

    Comments 59

      +2
      Круто, всё собирался написать что-нибудь подобное в общем виде для себя.

      Один вопрос: virtual-методы заставляют компилятор всунуть в класс указателя на vtable, в конструктор — инициализацию указателя и еще куда-нибудь — саму vtable.

      Вы не думали над тем, чтобы реализвовать полиморфизм времени компиляции с помощью CRTP? Результат будет тот же, размер объекта меньше на 1 указатель.
        0
        Я лишь описал очевидный подход. Глубоко не копал. Надеюсь, компилятор меня понял и сунул всё по максимуму на флеш :)
        Когда начинаешь считать байты, вся красота сходит и получается голый Си и портянка кода в 50 страниц одним куском.

        Например, состояния кнопки, можно впихнуть по две кнопки в байт, а верхнего уровня класс по 8 кнопок на байт (по биту) может держать.

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

        В примере с кнопкой, я бы предположил, что компилятор сделает ранее связывание и не будет никаких лишних указателей. Можно проверить сделав sizeof у объекта.
        Проверил — размер PressButton ровно на 1 байт больше (byte sw), чем у родительского SmartButton.

        Беда начинается, если бы были указатели на объекты.
          +1
          Интрига: а почему 11?

          внимание, спойлер
          byte btPin;                     // 1
          state btState = state::Idle;    // 2
          input btInput = input::Release; // 2
          unsigned long pressTimeStamp;   // 4
                                          //---
                                          // Total: 9
          								
          vtable_t* vtable                // 2
                                          // ---
                                          // Grand total: 11 :)
          

          Пруф того, что enum и указатель — 2 байта в коде выглядит так: create.arduino.cc/editor/4eyes/dcbd34bd-ff67-428a-b67e-116273eae6f2/preview
            0
            Да-да. Вкурил, спасибо.
            Утоптал в 6 байтов в итоге, уже лучше.
          0
          Размер SmartButton 11 байт, унаследованного PressButton 12.
            0
            Я думал про шаблон, да. Я пока не настолько владею плюсами. Я закончил кодить, когда они только появились и были странным глумлением над С.
            Мне не нравится, например, что надо конструкторы у нового класса надо писать. Лучше б сгенерить.
            CRTP не даст ли доступ к приватным переменным порождённому классу? Это было бы плохо и создаёт потенциальные проблемы.
              0
              CRTP не даст ли доступ к приватным переменным порождённому классу?
              Нет, конечно. private член доступен только внутри класса. Он может быть переопределен в наследнике, хоть с шаблонами, хоть без (это один из красивых сюрпризов С++), во вызов или доступ к нему возможны только изнутри класса.

              Мне не нравится, например, что надо конструкторы у нового класса надо писать. Лучше б сгенерить.
              Я не совсем понял, что имеется в виду под «писать».

              Если «реализовать» — то это не обязательно, до тех пор пока вас устраивает дефолтная реализация. Дефолтная реализация конструктора по умолчанию не делает ничего, а копирования — вызывает конструктор копирования родителя, а потом конструкторы копирования всех членов класса. Для POD типов (int, float, ..., и struct без конструкторов) — читай копирует побайтно.
                0
                Я не совсем понял, что имеется в виду под «писать».

                Вот это:
                PressButton(byte bt_pin) : SmartButton(bt_pin) {};
                  +1

                  Что бы подтянуть в наследник конструкторы базового класса можно написать так:


                  using SmartButton;
                    0
                    О! Это в каком месте? внутри конструктора наследуемого класса или вместо него?

                    class newclass: public baseclass {
                    using baseclass;
                    или
                    newclass() { using baseclass; }
                      +1

                      вот так:


                      class newclass: public baseclass {
                      public:
                         using baseclass;
                      ...
                      };
                        0
                        Спасибо, попробую. Эх, отстал от моды я на 30 лет…
                  0
                  это один из красивых сюрпризов С++

                  Надо покурить это на досуге, спасибо.
                    0
                    В кратце, это удобно для того, чтобы реализовать часть поведения в наследнике, а часть — в базовом классе.

                    Подробнее хорошо описано тут: isocpp.org/wiki/faq/strange-inheritance#private-virtuals

                    Пример
                    Вместо:
                    void Button::draw()       // public virtual
                    {
                        // Please do not forget to call this method from derived class!
                        // I promise I'll  one kitten each time you forget it
                        eraseBackground();
                        drawBorder();
                        drawText();
                    }
                    
                    void ImageButton::draw()  // public virtual
                    {
                        Button::draw();  // did not forget
                        drawImage(); 
                    }


                    Можно сделать так:
                    void Button::draw()       // public NON-virtual
                    {
                        eraseBackground();
                        drawBorder();
                        drawText();
                        
                        doCustomDraw();
                    }
                    
                    virtual void Button::doCustomDraw() {}   // private virtual
                    virtual void ImageButton::doCustomDraw() // private virtual
                    {
                        drawImage(); 
                    }


                    void Window::draw()
                    {
                        Button* someButton = getSomeButton();
                        someButton->draw(); // always erase backrgound, draw text & border. Probably draw image 
                    }


                      0
                      Это позднее связывание. Фуфу. Не наш метод. Оно как раз подъедает по 4 байта в ардуине на указатель. :)
                      Если можно обойтись ранним, лучше им.
                        0
                        Это не позднее связываение — все выполняется на этапе компиляции, и никаких 2 байт под указатель там не будет.
                        0
                        Наотимизировал SmartButton и он теперь 6 байт жрёт.
                        причём 2 забирает слово virtual :( причём один раз и путь лучше в базовом классе.
                0
                Вы как-то очень странно расставляете отступы для препроцессорных директив. Если я правильно помню стандарт, отступ перед # вообще ставить нельзя (хотя все известные мне компиляторы на это внимания не обращают). Но почему у вас написано:

                #ifndef MYLIB_H
                  #define MYLIB_H
                
                #if ARDUINO >= 100
                  #include <Arduino.h>
                #else
                  #include <WProgram.h>
                #endif
                
                // Ваш код здесь
                
                #endif


                Почему у #define MYLIB_H есть отступ, а у всего остального нет?

                К слову, раз уж взяли С++11, используйте enum class, а не просто enum. Это сильно снижает количество глупых ошибок.
                  0
                  На гитхабе уже enum class
                  Здесь чем проще, тем лучше. Для начинающих.
                  Отступ, хмм… не так принципиально ведь?
                    0
                    Отступ, хмм… не так принципиально ведь?

                    Разумеется, не принципиально, просто странно.
                    Главное — не доводить до такого
                    Осторожно, может вызвать сильное глазное кровотечение





                    (шепотом дилера) Между прочим, а вы слышали про #pragma once?
                      0
                      ща проморгаюсь от глазного… уффуфууу.
                        0
                        Про прагму ванс слышал, но так как-то привычнее, ну старорежимный я, и спокойнее.
                        Я использовал
                        #ifndef A
                        #define A
                        //
                        #endif
                        ещё в 1985 году, когда только Цэ у нас появился.
                        Привычка.
                          0
                          После третьей совершенно мистической ошибки компиляции, которая была вызвана копированием файла без исправления стража включения, я понял, что прагма рулит. И писать меньше и править не надо, если файл переименовывается. И опять-таки ошибиться почти невозможно.
                          Остаются, конечно, некоторые сложные случаи, когда она не работает, но они редки.

                          Но дело ваше :)
                            0
                            Тут рекомендуют и то и то. Оно друг другу не мешает, оказывается.
                            Задумался.
                    0
                    Вот тут думаю стоило бы развернуть:

                    #if ARDUINO >= 100
                      #include <Arduino.h>
                    #else
                      #include <WProgram.h>
                    #endif
                    


                    Цитата из robocraft.ru/blog/arduino/751.html
                    В Arduino IDE версии 1.0, разработчики переименовали файл WProgram.h в Arduino.h, поэтому, чтобы старые библиотеки заработали в новой IDE — нужно просто открыть файлы библиотеки (.h и .cpp) и если в них встречается строчка
                      0
                      Согласен.
                      0

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

                        –1
                        так и есть. городить новый. в это суть подхода. что в этом страшного?
                          0

                          в том что зачем если можно использовать классический Event-Observer в котором кнопка тригерит ивент, а обсервер по "идентфикатору кнопки" выполняет действия. Тогда вы разграничиваете логику и у вас 1 класс тугл.
                          SOLID

                            0

                            У вас нарушается и прицип инверсии зависимостей, и принцип единственной ответственности

                              +1
                              Надо почитать хоть что это.
                              Я не программист. Про бульдозер я загнул, конечно, но я закончил с программированием промышленным лет 30 назад. Я ничего не понял про евент-обсервер и нижеследующие принципы.
                              Нет ли желания показать на примере с тоглом и евент-обсервером?
                                +1

                                Наверно имеется ввиду примерно такая конструкция:


                                class IButtonClickObserver  {
                                public:
                                    virtual void onButtonToggle () = 0;
                                };
                                
                                class PushButton {
                                public:
                                    PushButton (IButtonClickObserver *observer) : observer_ (observer){}
                                private:
                                    void onToggle () {observer_->onButtonToggle();}
                                    IButtonClickObserver  *observer_;
                                };
                                
                                class MyApp : public IButtonClickObserver {
                                   MyApp () : button_ (this){}
                                
                                   void onButtonToggle () override final { /* do smthing useful */}
                                private:
                                   PushButton button_;
                                };
                                

                                Это с использованием виртуальных функций. Можно с шаблонами, код будет посложнее, однако удастся избежать лишнего указателя на vtable и убрать интерфейс observer. Можно так и так, но первый вариант на мой вкус удобочтимее.


                                #include <functional>
                                
                                class PushButton {
                                public:
                                    template <typename K, void (K::*func)()>
                                    void setObserver(K *object) {
                                        func_ = func_wrapper<K, func>;
                                        object_ = object;
                                    }
                                
                                protected:
                                    void onToggle () {func_ (object_);}
                                
                                    template <class K, void (K::*func)()>
                                    static void func_wrapper(void *obj) {
                                        return (static_cast<K *>(obj)->*func)();
                                    }
                                    std::function<void(void *obj)> func_ = nullptr;
                                    void *object_;
                                };
                                
                                class MyApp {
                                public:
                                    MyApp () {
                                        button_.setObserver<MyApp, &MyApp::onToggle> (this);
                                   }
                                    void onToggle ();
                                protected:
                                    PushButton button_;
                                };
                                  +1
                                  Ой, отсыпь…
                                  Это для меня, увы, слишком сложно. Я в такие дебри ещё не лазал.
                                  На буднях кодеров попрошу пояснить, что это такое. :)
                                    0
                                    Мне кажется, можно проще. Прошу прощения, ардуины под рукой нет, проверить не могу, но вот синтаксически верный набросок на С++:

                                    godbolt.org/g/yy3TWp
                                  0

                                  К сожалению, я не силен в C++.
                                  1) Ивент-обсервер:
                                  У вас есть события и обработчики. Событие к примеру "нажатие кнопки", "правый клик мышкой" и тд и тп
                                  Обработчик это объект который "слушает"(ждет) событие и когда оно происходит выполняет какие-то действия
                                  В этом случае ваша кнопка не знает что будет происходить по её нажатию. Условно можете это представлять как реальную кнопку, которая не знает, что будет происходить в цепи когда на неё нажмут, она лишь замыкает или размыкает контакт.
                                  По аналогии вы создали кнопку, которая может зажигать только светодиод. В реальном мире это выглядело бы как кнопка связанная с светодиодом, которая не может быть использованна ни с чем кроме светодиода. Не логично, лучше создать универсальную кнопку)


                                  Чем больше ваши классы связанны между собой тем сложнее будет расширять (масштабировать) систему в будущем.

                                    0
                                    Я в примере сделал класс «кнопка toggle с подсветкой светодиодом». Это новый вид кнопки.
                                      0

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

                                        0
                                        Хммм… Соглашусь. Да.
                                        Здесь компромисс пока что. В атмеге нет динамической памяти, STL и тп. Памяти вообще мало — 2Кб на данные.
                                        Идею я понял с обсервером, не спешу, но подумаю.

                                        А если по-старорежимному сделать класс toggle с колбеком? Хотя нет, каждый колбек 2 байта жрёт данных, это дорого для кнопочек.
                                      0
                                      Универсальность да, хочется, согласен.
                                      Покурю про обсерверы ещё.
                                      0

                                      А по поводу SOLID.
                                      Это 5 принципов, следуя которым, вы в 90% случаев напишите хороший код, который вы сможете легко тестировать, масштабировать и поддерживать в будущем

                                        0
                                        Да вроде как старался следовать…
                                          0
                                          Так советовал же CRTP — минус 2 байта на vtable, за счет отказа от виртуальных функций, и никаких указателей на колбеки.
                                            0
                                            Я помню. Для этого надо сначала освоить новую эту фичу.
                                            Я на плюсах писал в начале 90х или конце 80х, не помню. С тех пор много нового появилось и шаблоны одно из них. Я пока не вкурил тему. Курю.
                                0
                                «inline virtual»?
                                  0
                                  Звучит странно, но работает. На откуп компилятору.
                                  +1
                                  Arduino IDE позволяет использовать синтаксис C++11, оказывается. То есть, там очень развитый объектно-ориентированный язык.

                                  Совсем не так. Там полноценный С++, но из среды выполнения выпилили исключения и нет всяких STL, ибо на эмбеддед мало памяти и вообще динамическая аллокация — зло.
                                  Надо будет набросать статейку про тюнинг ардуиновского тулчейна…
                                    0
                                    Давай! Буде статья — дай знать!
                                      0
                                      Накорябал, жду модерации
                                        0
                                        Обещал — даю знать.
                                        habrahabr.ru/post/346202
                                          0
                                          О! пасипки.
                                      0

                                      Оффтоп про Arduino-библиотеки.


                                      Пытался класть библиотеку рядом со скетчем и инклюдить через относительный пути


                                      #include "lib/SomeLib/SomeLib.h"

                                      и получал ошибки линкера на функции и пр. из библиотеки.


                                      Можно как-нибудь библиотеки локально хранить, чтобы потом тем, кто этот код будет использовать не требовалось их вручную ставить?

                                        0
                                        Если положить файлы рядом, они автоматом скомпилятся тоже.

                                        Я же написал в статье, куда положить и как, чтобы IDE увидел их как библиотеки. Не надо писать пути в include. Надо выполнить эти условия, а не изобретать велосипед.

                                        Чтобы кто-то мог использовать этот код — его лучше держать на гитхабе. Да, включая сами скетчи тоже.
                                          0
                                          Чтобы кто-то мог использовать этот код — его лучше держать на гитхабе. Да, включая сами скетчи тоже.

                                          Да я об этом и веду речь.
                                          Есть некая библиотека AAA. Я ее скачал с гитхаба или через встроенный менеджер скачал — так или иначе она будет доступна по пути "C:...\Arduino\Libraries\AAA".


                                          Затем я заливаю скетч на GitHub, и мне придется указать в ReadMe, что пользователь должен скачать библиотеку AAA и корректно ее расположить, чтобы IDE подхватила. Это довольно неудобно и для меня, и, особенно, для пользователя. В случае, если библиотеки расположены в папке скетча, достаточно клонировать репозиторий и все сразу собирается.


                                          Альтернативным решением, принятым в цивилизованном мире, является использование менеджеров зависимостей (привет NuGet, npm, pip и т.д), чтобы прописать зависимости проекта и они потом автоматически подгрузились. Но, увы, среда Arduino не предоставляет таких возможностей.

                                          0
                                          Здравствуйте!

                                          Я тоже в какой-то момент столкнулся с такой проблемой. Насколько я понимаю, фактически это нарушает всю идею заголовочных файлов. Причина этого в том, как Arduino IDE (если это поделие можно называть IDE) производит обработку файлов. Повозившись немного, понял, что лучшим решением проблемы является переход на использование инструментов, разработанных для разработчиков, а не для домохозаяек.

                                          Лучшим, что нашёл, является platform.io. Порадовало наличие возможности производить тестирование кода, организация сборки под разные платформы, наличие работающей системы зависимостей (в Arduino IDE есть выкидышь на эту тему, целый пакетный менеджер, который не позволяет задавать зависимости проекта, в результате чего периодически сборка чужого проекта превращается в игру «угадай версию библиотеки, использованной разработчиком»). На данный момент меня полностью устраивает. В случае, если будете пользоваться и посмотрите в сторону IDE, рекомендую посмотреть на версию, основанную на VS Code. Как минимум на моём не молодом ноутбуке данная версию работала куда как бодрее, чем выкидышь на Atom.
                                            0
                                            Начинают же домохозяйки :) и для них Arduino IDE прекрасно во всём :)
                                            Platformio поставил, смотрю. Прикольно. Примерно всё то же самое. Редактор (Atom) такой же неудобный. Меня бы больше устроил из командной строки make, в редактор есть Sublime :)
                                            Посмотрю на вс-коре
                                              0
                                              Здравствуйте!

                                              Главная прелесть planform.io в том, что его можно запускать из командной строки. При этом можно запускать как сборку всех целей (под разные платы, если есть), так и под конкретную. Единственное, что мне показалось не совсем удобным, это способ задания последовательного порта. Но это может быть причиной поверхностного ознакомления с документацией.
                                                0
                                                С vs code намного лучше, хмм…
                                                С гитом дружит, ооочень хорошо это.

                                                Я такой минус нашёл:

                                                #include <Wire.h>
                                                #include "ssd1306.h"


                                                Пока явно не указал #include <Wire.h> — не собирался. Подозреваю, что в ssd1306.h вместо <> стоят кавычки :)
                                              0

                                              Спасибо за ценный совет.


                                              Вспомнил, пользовался platform.io как-то год назад, (правда, на атоме и лагало сильно), но довольно понравилось. Забыл, вот.


                                              Я так-то редко пишу именно под Arduino, а эту свистопляску с библиотеками обнаружил, когда один человек попросил написать для него небольшую вещь. И чтобы его не утруждать, хотел скинуть все библиотеки в проект, чтобы обойтись одним git clone, условно.


                                              Даже не знаю, какая инструкция проще:


                                              • скачайте такие-то библиотеки
                                              • скачайте platform.io
                                                0
                                                Platformio пока нравится. Играю вот.

                                          Only users with full accounts can post comments. Log in, please.