Начинающим на 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 
#else
  #include 
#endif

// Ваш код здесь

#endif


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


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


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


#include "MyLib.h"


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


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


#if ARDUINO >= 100
  #include 
#else
  #include 
#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 
#else
 #include 
#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:


np9doelgukzdbidufsdavkyip3y.png


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


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

© Habrahabr.ru