В прошлой статье про написание конечных автоматов я обещал упаковать наш гениальный код в виде класса на 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 в частности, есть много статей наверняка. Попробуйте поискать. Если не найдёте, я напишу как им пользуюсь я.

Комментарии (52)


  1. 4eyes
    03.01.2018 20:50
    +1

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

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

    Вы не думали над тем, чтобы реализвовать полиморфизм времени компиляции с помощью CRTP? Результат будет тот же, размер объекта меньше на 1 указатель.


    1. nwwind Автор
      03.01.2018 21:13

      Я лишь описал очевидный подход. Глубоко не копал. Надеюсь, компилятор меня понял и сунул всё по максимуму на флеш :)
      Когда начинаешь считать байты, вся красота сходит и получается голый Си и портянка кода в 50 страниц одним куском.

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

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

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

      Беда начинается, если бы были указатели на объекты.


      1. 4eyes
        03.01.2018 22:50

        Интрига: а почему 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


        1. nwwind Автор
          03.01.2018 23:45

          Да-да. Вкурил, спасибо.
          Утоптал в 6 байтов в итоге, уже лучше.


    1. nwwind Автор
      03.01.2018 21:14

      Размер SmartButton 11 байт, унаследованного PressButton 12.


    1. nwwind Автор
      03.01.2018 21:18

      Я думал про шаблон, да. Я пока не настолько владею плюсами. Я закончил кодить, когда они только появились и были странным глумлением над С.
      Мне не нравится, например, что надо конструкторы у нового класса надо писать. Лучше б сгенерить.
      CRTP не даст ли доступ к приватным переменным порождённому классу? Это было бы плохо и создаёт потенциальные проблемы.


      1. 4eyes
        03.01.2018 21:44

        CRTP не даст ли доступ к приватным переменным порождённому классу?
        Нет, конечно. private член доступен только внутри класса. Он может быть переопределен в наследнике, хоть с шаблонами, хоть без (это один из красивых сюрпризов С++), во вызов или доступ к нему возможны только изнутри класса.

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

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


        1. nwwind Автор
          03.01.2018 21:46

          Я не совсем понял, что имеется в виду под «писать».

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


          1. olegator99
            04.01.2018 08:02

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


            using SmartButton;


            1. nwwind Автор
              04.01.2018 12:06

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

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


              1. olegator99
                04.01.2018 12:12

                вот так:


                class newclass: public baseclass {
                public:
                   using baseclass;
                ...
                };


                1. nwwind Автор
                  04.01.2018 20:09

                  Спасибо, попробую. Эх, отстал от моды я на 30 лет…


        1. nwwind Автор
          03.01.2018 21:47

          это один из красивых сюрпризов С++

          Надо покурить это на досуге, спасибо.


          1. 4eyes
            03.01.2018 22:13

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

            Подробнее хорошо описано тут: 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 
            }


            1. nwwind Автор
              03.01.2018 22:21

              Это позднее связывание. Фуфу. Не наш метод. Оно как раз подъедает по 4 байта в ардуине на указатель. :)
              Если можно обойтись ранним, лучше им.


            1. nwwind Автор
              03.01.2018 23:37

              Наотимизировал SmartButton и он теперь 6 байт жрёт.
              причём 2 забирает слово virtual :( причём один раз и путь лучше в базовом классе.


  1. Amomum
    03.01.2018 23:21

    Вы как-то очень странно расставляете отступы для препроцессорных директив. Если я правильно помню стандарт, отступ перед # вообще ставить нельзя (хотя все известные мне компиляторы на это внимания не обращают). Но почему у вас написано:

    #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. Это сильно снижает количество глупых ошибок.


    1. nwwind Автор
      03.01.2018 23:42

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


      1. Amomum
        03.01.2018 23:48

        Отступ, хмм… не так принципиально ведь?

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




        1. nwwind Автор
          04.01.2018 00:07

          ща проморгаюсь от глазного… уффуфууу.


        1. nwwind Автор
          04.01.2018 00:09

          Про прагму ванс слышал, но так как-то привычнее, ну старорежимный я, и спокойнее.
          Я использовал
          #ifndef A
          #define A
          //
          #endif
          ещё в 1985 году, когда только Цэ у нас появился.
          Привычка.


          1. Amomum
            04.01.2018 00:18

            После третьей совершенно мистической ошибки компиляции, которая была вызвана копированием файла без исправления стража включения, я понял, что прагма рулит. И писать меньше и править не надо, если файл переименовывается. И опять-таки ошибиться почти невозможно.
            Остаются, конечно, некоторые сложные случаи, когда она не работает, но они редки.

            Но дело ваше :)


            1. nwwind Автор
              04.01.2018 12:09

              Тут рекомендуют и то и то. Оно друг другу не мешает, оказывается.
              Задумался.


  1. MrGobus
    04.01.2018 09:55

    Вот тут думаю стоило бы развернуть:

    #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) и если в них встречается строчка


    1. nwwind Автор
      04.01.2018 12:08

      Согласен.


  1. c4boomb
    04.01.2018 11:50

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


    1. nwwind Автор
      04.01.2018 12:08
      -1

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


      1. c4boomb
        04.01.2018 12:22

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


      1. c4boomb
        04.01.2018 12:27

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


        1. nwwind Автор
          04.01.2018 20:14

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


          1. olegator99
            04.01.2018 22:24

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


            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. nwwind Автор
              04.01.2018 23:40

              Ой, отсыпь…
              Это для меня, увы, слишком сложно. Я в такие дебри ещё не лазал.
              На буднях кодеров попрошу пояснить, что это такое. :)


              1. 4eyes
                05.01.2018 02:49

                Мне кажется, можно проще. Прошу прощения, ардуины под рукой нет, проверить не могу, но вот синтаксически верный набросок на С++:

                godbolt.org/g/yy3TWp


          1. c4boomb
            05.01.2018 21:41

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


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


            1. nwwind Автор
              06.01.2018 00:11

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


            1. nwwind Автор
              06.01.2018 00:12

              Универсальность да, хочется, согласен.
              Покурю про обсерверы ещё.


          1. c4boomb
            05.01.2018 21:50

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


            1. nwwind Автор
              06.01.2018 00:10

              Да вроде как старался следовать…


  1. Tarik02
    04.01.2018 13:56

    «inline virtual»?


    1. nwwind Автор
      04.01.2018 20:15

      Звучит странно, но работает. На откуп компилятору.


  1. a-tk
    04.01.2018 16:35

    Arduino IDE позволяет использовать синтаксис C++11, оказывается. То есть, там очень развитый объектно-ориентированный язык.

    Совсем не так. Там полноценный С++, но из среды выполнения выпилили исключения и нет всяких STL, ибо на эмбеддед мало памяти и вообще динамическая аллокация — зло.
    Надо будет набросать статейку про тюнинг ардуиновского тулчейна…


    1. nwwind Автор
      04.01.2018 20:16

      Давай! Буде статья — дай знать!


      1. a-tk
        04.01.2018 22:47

        Накорябал, жду модерации


  1. Alex_ME
    04.01.2018 20:41

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


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


    #include "lib/SomeLib/SomeLib.h"

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


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


    1. nwwind Автор
      04.01.2018 21:07

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

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

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


      1. Alex_ME
        05.01.2018 00:26

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

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


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


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


    1. Wilk
      04.01.2018 22:35

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

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

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


      1. nwwind Автор
        04.01.2018 23:38

        Начинают же домохозяйки :) и для них Arduino IDE прекрасно во всём :)
        Platformio поставил, смотрю. Прикольно. Примерно всё то же самое. Редактор (Atom) такой же неудобный. Меня бы больше устроил из командной строки make, в редактор есть Sublime :)
        Посмотрю на вс-коре


        1. Wilk
          04.01.2018 23:44

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

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


          1. nwwind Автор
            05.01.2018 00:04

            С vs code намного лучше, хмм…
            С гитом дружит, ооочень хорошо это.

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

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


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


      1. Alex_ME
        05.01.2018 00:31

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


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


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


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


        • скачайте такие-то библиотеки
        • скачайте platform.io


        1. nwwind Автор
          05.01.2018 01:31

          Platformio пока нравится. Играю вот.