Пишем свою библиотеку

Как написать свою библиотеку?


В этом уроке мы научимся писать собственные библиотеки для Arduino и разберём некоторые типовые вопросы по взаимодействию кода в библиотеке и кода в скетче (в главном файле программы). Это третий урок, который относится к библиотекам: обязательно прочитайте и усвойте урок про объекты и классы из блока программирования, и урок по использованию библиотек из блока базовых уроков, а также урок про создание функций. В этом уроке мы будем использовать все наши предыдущие знания, так что рекомендую разобраться со всем, что было непонятно. Писать библиотеки очень удобно в текстовом редакторе Notepad++ (официальный сайт) - так называемом блокноте программиста. Данный блокнот распознаёт и подсвечивает синтаксис, умеет в автодополнение текста и расширенный поиск, и многое многое другое. Безумно рекомендую работать именно в нём, если вы не умеете пользоваться Microsoft Visual Studio и прочими серьёзными средами разработки. Также рекомендую к прочтению вот этот урок с сайта Arduino.ru, в нём кратко пошагово рассказывают о создании библиотеки без излишеств. Если будете компилировать пример из этой статьи - замените WProgram.h на Arduino.h.

Разбираемся с файлами


Библиотека - это в первую очередь текстовый файл с кодом, который мы можем подключить в свой скетч и использовать имеющиеся там команды. Библиотека может иметь несколько файлов или даже папок с файлами, но подключается всегда один - главный заголовочный файл с расширением .h, а он в свою очередь подтягивает остальные необходимые файлы. В общем случае библиотека имеет такую структуру (название библиотеки testLib):

  • testLib - папка библиотеки
    • examples - папка с примерами
    • testLib.h - заголовочный файл
    • testLib.cpp - файл реализации
    • keywords.txt - карта подсветки синтаксиса

Иногда файлы .h и .cpp могут находиться в папке src. Все файлы и папки, кроме заголовочного .h, являются необязательными и могут отсутствовать, т.е. библиотека может состоять только из заголовочного файла. В таком виде библиотека лежит в папке со всеми остальными библиотеками и может быть подключена в скетч при помощи команды #include. Вообще есть два места, где программа будет искать библиотеку (именно файл библиотеки):

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

Соответственно команда include имеет два варианта поиска файла, название заключается в <> или "":

  • #include <файл.h> - будет искать файл в папке с библиотеками
  • #include "файл.h" - попробует найти файл в папке со скетчем, если не найдёт - пойдёт искать в папку с библиотеками

Выглядит это вот так:

Основа библиотеки


Давайте заполним наш файл testLib.h, нашу тестовую библиотеку, минимальным кодом для работы:

#ifndef testLib_h
#define testLib_h
#include <Arduino.h>

// код библиотеки

#endif

Конструкция из директив препроцессора запрещает повторное подключение библиотеки и в целом является необязательной, но лучше не лениться и писать так. Файл библиотеки testLib.h находится в папке testLib в папке со всеми остальными библиотеками. Также мы подключаем основной файл Arduino.h для использования ардуино-функций в своём коде. Если таковых нет - его можно не подключать. Также подключаем testLib.h в наш тестовый скетч, как на скриншоте в прошлой главе. Конструкцию с #ifndef-define вы найдёте практически во всех библиотеках. На текущих версиях IDE (и, соответственно версии компилятора) можно делать так:

#pragma once
// подключаем Ардуино.н
// код библиотеки

Конструкция pragma once говорит компилятору, что данный файл нужно подключить только один раз, это просто короткая альтернатива #ifndef-define. Дальше будем использовать её

Пишем класс


Давайте воспользуемся наработками из урока объекты и классы и вставим финальную версию класса в testLib.h

testLib.h
#pragma once
#include <Arduino.h>

// описание класса
class Color {   // класс Color
public:
 Color(byte color = 5, byte bright = 30);
 void setColor(byte color);
 void setBright(byte bright);
 byte getColor();
 byte getBright();
private:
 byte _color;  // переменная цвета
 byte _bright; // переменная яркости
};

// реализация методов
Color::Color(byte color = 5, byte bright = 30) { // конструктор
 _color = color;   // запоминаем
 _bright = bright;
}
void Color::setColor(byte color) {_color = color;}
void Color::setBright(byte bright) {_bright = bright;}
byte Color::getColor() {return _color;}
byte Color::getBright() {return _bright;}
testSketch.ino
#include <testLib.h>

Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

void setup() {
}

void loop() {
}
Собственно вот так мы разместили наш класс в отдельном файле, подключили его в основную программу и воспользовались кодом: просто создали несколько объектов. Давайте проверим, работает ли оно: выведем возвращающие методы в порт:
testSketch.ino
#include <testLib.h>
Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

void setup() {
  Serial.begin(9600);
  Serial.println(myColor.getColor());   // 10
  Serial.println(myColor2.getBright()); // 20
  Serial.println(myColor3.getColor());  // 5
}

void loop() {
}
Код выводит значения из класса, выводит правильно. Собственно вот мы и написали свою библиотеку! Далее можно разделить описание и реализацию, создав файл testLib.cpp
testLib.h
#pragma once
#include <Arduino.h>

// описание класса
class Color {   // класс Color
public:
 Color(byte color = 5, byte bright = 30);
 void setColor(byte color);
 void setBright(byte bright);
 byte getColor();
 byte getBright();
private:
 byte _color;  // переменная цвета
 byte _bright; // переменная яркости
};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

// реализация методов
Color::Color(byte color = 5, byte bright = 30) { // конструктор
 _color = color;   // запоминаем
 _bright = bright;
}

void Color::setColor(byte color) {_color = color;}
void Color::setBright(byte bright) {_bright = bright;}
byte Color::getColor() {return _color;}
byte Color::getBright() {return _bright;}
testSketch.ino
#include <testLib.h>
Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

void setup() {
  Serial.begin(9600);
  Serial.println(myColor.getColor());   // 10
  Serial.println(myColor2.getBright()); // 20
  Serial.println(myColor3.getColor());  // 5
}

void loop() {
}
Важный момент: если в библиотеке есть файл имябиблиотеки.cpp, то реализация методов и функций должна находиться именно там! В файле имябиблиотеки.h реализацию указывать нельзя, будет ошибка.
Если библиотека состоит только из заголовочного файла имябиблиотеки.h, то реализацию можно расписать в нём.

И вот уже у нас полноценная взрослая библиотека, разбитая на файлы. Можно дополнить её файлом keywords.txt, чтобы наши методы подсвечивались в коде.

Keywords.txt


keywords.txt это файл, в котором содержится "карта" подсветки синтаксиса, то есть каким цветом какие слова подсвечивать. Синтаксис построения этого файла очень прост: с новой строки перечисляются названия функций/методов, и через табуляцию (нажатие клавиши TAB) - тип ключевого слова.

  • KEYWORD1 - жирный оранжевый, подсветка для типов данных и названий классов
  • KEYWORD2 - оранжевый цвет, для методов и функций
  • LITERAL1 - голубой цвет, для констант

Вот так будет выглядеть keywords.txt для нашей библиотеки:

# комментарий
testLib	KEYWORD1
Color	KEYWORD1

setColor	KEYWORD2
setBright	KEYWORD2
getColor	KEYWORD2
getBright	KEYWORD2

Можно оставлять комментарии, здесь они начинаются с решётки #. Констант у нас нет, поэтому LITERAL1 не использовал. Давайте посмотрим, как выглядит код с подсветкой наших команд из библиотеки. Важный момент: чтобы изменения вступили в силу, нужно закрыть все окна Arduino IDE и открыть скетч заново.   Почему Color не выделен жирным, да и вообще уже выделен в скетче без подсветки? Дело в том, что Arduino IDE собирает keywords из всех библиотек, и где-то имя Color видимо уже используется. Собственно вот и всё!

Примеры реализации


Структуру создания библиотеки мы разобрали, давайте рассмотрим некоторые частные варианты с примерами. Я буду делать примеры именно с классами, а не с функциями, потому что механика работы с классом, с библиотекой, гораздо сложнее, а мы тут с вами учимся библиотеки писать. Во всех примерах у меня создана тестовая библиотека testLib.h, и тестирую я её в скетче testSketch.

Библиотека без класса


В библиотеке необязательно должен быть класс, может быть просто набор функций:

testLib.h
#pragma once
#include <Arduino.h>

void printLol() {
  Serial.println("lol");
}
testSketch.ino
#include <testLib.h>

void setup() {
  Serial.begin(9600);
  printLol(); // выведет lol
}

void loop() {
}
Очевидные вариации: более грамотно будет написать объявление отдельно от реализации функции. Или даже поместить реализацию в файл .cpp.
testLib.h
#pragma once
#include <Arduino.h>

// объявление
void printLol();
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

// реализация
void printLol() {
  Serial.println("lol");
}
testSketch.ino
#include <testLib.h>

void setup() {
  Serial.begin(9600);
  printLol(); // выведет lol
}

void loop() {
}

Обернём в namespace


Данный пример относится к примеру выше: в "библиотеке" мы создали функции, имена этих функций могут совпасть с другими функциями в скетче, что приведёт к проблемам. Вместо написания класса, функции можно обернуть в "пространство имён" - namespace. Смотрите пример, я думаю всё станет понятно.

testLib.h
#pragma once
#include <Arduino.h>

// пространство имён myFunc
namespace myFunc {
  void printLol();
};

// реализация
void myFunc::printLol() {
  Serial.println("lol");
}
testSketch.ino
#include <testLib.h>
void setup() {
  Serial.begin(9600);
  // выведет kek из функции скетча
  printLol();

  // выведет lol из функции библиотеки
  myFunc::printLol();
}

void printLol() {
  Serial.println("kek");
}

void loop() {
}
Использование namespace позволяет разделить функции с одинаковыми названиями из разных документов, обращение к функции из пространства имён выглядит точно так же, как к классу: имяПространстваИмён::имяФункции.

Передача и вывод значения в класс


Рассмотрим такой пример: нужно передать в класс некое значение, обработать его и вернуть результат обратно в скетч. В качестве примера просто вернём умноженное на 10 число:

testLib.h
#pragma once
#include <Arduino.h>

class testClass {
  public:
    long get_x10(int value);
  private:
};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

long testClass::get_x10(int value) {
  return value*10;
}
testSketch.ino
#include <testLib.h>
testClass testObject;

void setup() {
  Serial.begin(9600);
  Serial.println(testObject.get_x10(450)); // выведет 4500
}

void loop() {
}
Рассмотрим более сложную ситуацию: нужно принять значение в класс, записать в приватную переменную, и отдельным методом получить её:
testLib.h
#pragma once
#include <Arduino.h>

class testClass {
  public:
    void setValue(int val);
    int getValue();
  private:
    int _value = 0;
};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

void testClass::setValue(int val) {
  // берём внешнюю val и пишем в свою _value
  _value = val;
}
int testClass::getValue() {
  return _value;	// вернуть переменную из класса
}
testSketch.ino
#include <testLib.h>
testClass testObject;

void setup() {
  Serial.begin(9600);
  testObject.setValue(666);
  Serial.println(testObject.getValue()); // выведет 666
}

void loop() {
}

Изменение переменной из класса


Рассмотрим такую ситуацию: нам нужно при помощи метода/функции библиотеки изменить значение переменной в скетче. Тут есть два варианта: присваивать напрямую, или использовать указатель. Рассмотрим оба варианта в одном примере:

testLib.h
#pragma once
#include <Arduino.h>

class testClass {
  public:
    int multTo5(int value);
    void multTo7(int* value);
  private:
    
};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

int testClass::multTo5(int value) {
  // вернуть значение, умноженное на 5
  return value * 5;
}

void testClass::multTo7(int* value) {
  // умножить переменную на 7
  *value = *value * 7;
}
testSketch.ino
#include <testLib.h>
testClass testObject;

void setup() {
  int a = 10;
  a = testObject.multTo5(a);
  // a == 50
  
  testObject.multTo7(&a);
  // a == 350
}

void loop() {
}
В первом варианте мы передаём значение переменной, внутри метода умножаем его на 5 и возвращаем обратно, и можем приравнять эту же переменную в скетче к новому значению. В случае с указателем всё работает более интересно: мы передаём методу адрес переменной, умножаем эту переменную на 7 внутри класса, и всё. Грубо говоря, в этом примере *value является куклой вуду для переменной a: что мы будем делать с *value внутри метода - это сразу же будет отражаться на a. Данную тему можно развить до такого варианта: мы можем хранить в классе адрес переменной, и класс всегда будет иметь прямой доступ к значению переменной, его не нужно будет передавать каждый раз!
testLib.h
#pragma once
#include <Arduino.h>

class testClass {
  public:
    void takeControl(int* value);
    void multTo6();
  private:
    int *_value;	// храним адрес
};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

void testClass::takeControl(int* value) {
  _value = value;
}
void testClass::multTo6() {
  *_value = *_value * 6;
}
testSketch.ino
#include <testLib.h>
testClass testObject;
int a = 10;

void setup() {
  // передали адрес a
  testObject.takeControl(&a);

  // сейчас а == 10
  testObject.multTo6(); // тут а станет 60

  a = 5;
  testObject.multTo6(); // тут а станет 30
  testObject.multTo6(); // тут а станет 180
}

void loop() {
}
Таким образом класс и его методы могут иметь полный контроль над переменной в основной программе!

Передача массива в класс


Попробуем передать массив в класс, чтобы методами класса можно было, например, сложить сумму элементов массива и вернуть её!

testLib.h
#pragma once
#include <Arduino.h>

class testClass {
  public:
    long getSum(int *array, byte length);
  private:

};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

long testClass::getSum(int *array, byte length) {
  long sum = 0;
  // вычисляем длину массива
  length = length / sizeof(int);
  
  for (byte i = 0; i < length; i++) {
    sum += array[i];
  }
  return sum;
}
testSketch.ino
#include <testLib.h>
testClass testObject;

void setup() {
  // делаем массив
  int myArray[] = {5, 33, 58, 251, 91};

  // передаём массив и его размер (в байтах)
  long arraySum = testObject.getSum((int*)myArray, sizeof(myArray));
  // arraySum == 438
}

void loop() {
}
Основной механизм я думаю понятен, оставлю тут ещё пример, как передать структуру
Передача структуры по указателю
// передаем структуру по указателю
struct foo_param_t
{
 float *u; int n; float b; float c;
}

void foo(foo_param_t *p)
{
 for (int i=0; i<p->n; i++)
 {
  float x = i*M_PI;
  p->u[i] = 1.0+p->b*x+p->c*x*x;
 }
}

void bar()
{
 const int N = 10;
 float a[N];
 foo_param_t p = {(float*)&a, N, 1.23, -4.56};
 foo(&p);
}
Передача структуры по ссылке
// передаем структуру по ссылке
struct foo_param_t
{
 float *u; int n; float c; float b;
}

void foo(foo_param_t& p)
{
 for (int i=0; i<p.n; i++)
 {
  float x = i*M_PI;
  p.u[i] = 1.0+p.b*x+p.c*x*x;
 }
}

void bar()
{
 const int N = 10;
 float a[N];
 foo_param_t p = {(float*)&a, N, 1.23, -4.56};
 foo(p);
}

Передача функции в класс


Я думаю вы помните, как работают штуки типа attachInterrupt: мы указываем имя функции, которую можно будет вызвать из другой функции. Делается это через указатель на функцию. Посмотрим простой пример без класса:

testLib.h
#pragma once
#include <Arduino.h>

// тут хранится приаттааченная функция
void (*atatchedF)();

// подключаем функцию
void attachFunction(void (*function)()) {
 atatchedF = *function;
}

// вызов приаттаченной функции
void callFunction() {
 (*atatchedF)();
}
testSketch.ino
#include <testLib.h>

void setup() {
  Serial.begin(9600);
  // подключили функцию printKek
  attachFunction(printKek);

  // вызвали подключенную функцию
  callFunction();
  // вызовет printKek
}

void printKek() {
  Serial.println("kek");
}

void loop() {
}
Теперь засунем это всё в класс и будем хранить адрес прикреплённой функции внутри класса. Тут есть два варианта, покажу оба, просто чтобы было тут в одном месте это всё. Первый вариант:
testLib.h
#pragma once
#include <Arduino.h>

class testClass {
  public:
    void attachFunction(void (*function)());
    void callFunction();
  private:
    void (*atatchedF)();
};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

void testClass::attachFunction(void (*function)()) {
 atatchedF = *function;	
}

void testClass::callFunction() {
 (*atatchedF)();
}
testSketch.ino
#include <testLib.h>
testClass testObj;
void setup() {
  Serial.begin(9600);
  // подключили функцию printKek
  testObj.attachFunction(printKek);

  // вызвали подключенную функцию
  testObj.callFunction();
  // вызовет printKek
}

void printKek() {
  Serial.println("kek");
}

void loop() {
}
Второй вариант:
testLib.h
#pragma once
#include <Arduino.h>

extern "C" {
  typedef void (*func)(void);
}

class testClass {
  public:
    void attachFunction(func newFunc);
    void callFunction();
  private:
    func _attachedFunc;
};
testLib.cpp
#include <testLib.h>	// подключаем заголовок обязательно

void testClass::attachFunction(func newFunc) {
 _attachedFunc = newFunc;	
}

void testClass::callFunction() {
 _attachedFunc();
}
testSketch.ino
#include <testLib.h>
testClass testObj;
void setup() {
  Serial.begin(9600);
  // подключили функцию printKek
  testObj.attachFunction(printKek);

  // вызвали подключенную функцию
  testObj.callFunction();
  // вызовет printKek
}

void printKek() {
  Serial.println("kek");
}

void loop() {
}

Автоматическое создание объекта


Создание класса подразумевает также создание объекта, но иногда библиотека пишется только для одного объекта (например - библиотека для работы с одним интерфейсом), и создание объекта в скетче выглядит как лишний код. Но, если вы откроете любой пример с использованием библиотеки Wire.h, вы не найдёте там создания объекта Wire, а он используется! Например:

#include <Wire.h>

void setup() {
  Wire.begin();
}
// .............

Мы используем объект Wire, но мы его не создавали! Иногда это может быть удобно, давайте покажу, как это сделать: нужно всего лишь добавить в заголовочный файл строчку:

extern имя_класса имя_объекта;

А в .cpp, если он есть, добавить:

имя_класса имя_объекта = имя_класса();

Таким образом объект будет создан внутри библиотеки, и мы сможем им пользоваться  из скетча. Давайте возьмём самый первый пример из урока, из главы "Передача и вывод значения в класс", и избавимся от лишнего создания объекта:

testLib.h
#pragma once
#include <Arduino.h>

class testClass {
  public:
    long get_x10(int value);
  private:
};
extern testClass testObject;
testLib.cpp
#include <testLib.h> // подключаем заголовок обязательно

long testClass::get_x10(int value) {
  return value*10;
}
testClass testObject = testClass();
testSketch.ino
#include <testLib.h>
// объект не создаём!
void setup() {
  Serial.begin(9600);
  Serial.println(testObject.get_x10(450)); // выведет 4500
}

void loop() {
}

Задание размера массива при создании объекта


Вы должны помнить из урока про массивы, что размер массива должен быть известен до начала выполнения программы. А что делать, если в классе нам нужен массив с возможностью задать его размер? Если объект в программе будет один, или для всех объектов размер массива будет одинаковый, то очевидно можно сделать вот так:

#define ARRAY_LEN 20

class myClass {
  public:
    byte vals[ARRAY_LEN];
  private:    
};

myClass obj1;  // тут у obj1.vals будет 20 ячеек
myClass obj2;  // тут у obj2.vals будет 20 ячеек
myClass obj3;  // тут у obj3.vals будет 20 ячеек

Если мы хотим иметь возможность задать размер массива каждому объекту, то тут есть варианты:

При создании объектов появится соответственно <параметр>

template < int ARRAY_LEN >
class myClass {
  public:
    byte vals[ARRAY_LEN];
  private:    
};

myClass<10> obj1; // тут у obj1.vals будет 10 ячеек 
myClass<20> obj2; // тут у obj2.vals будет 20 ячеек 
myClass<30> obj3; // тут у obj3.vals будет 30 ячеек

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

template < int ARRAY_LEN >
class myClass {
  public:
    byte vals[ARRAY_LEN];
    byte arrSize = ARRAY_LEN;
  private:
};

myClass<30> obj1;
// obj1.vals имеет 30 ячеек
// obj1.arrSize имеет значение 30

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

Можно выделить "массив" динамически и хранить его как указатель. В качестве "константного" значения используется фишка С++ под названием список инициализации (двоеточие после myClass(int x) ):

class myClass {
  public:
    int* arr;
    myClass(int x) : arr(new int[x]) {
      // конструктор
    }
  private:
};

myClass obj(5);

void setup() {
  Serial.begin(9600);
  obj.arr[0] = 1;
  obj.arr[1] = 2;
  obj.arr[2] = 3;
  obj.arr[3] = 4;
  obj.arr[4] = 5;

  for (byte i = 0; i < 5; i++) {
    Serial.println(obj.arr[i]);
    // выведет 1 2 3 4 5 с переносом
  }
}

void loop() {
}

Даже при глобальном создании объекта такой массив будет храниться в динамической памяти и компилятор не сможет посчитать его размер!

Делаем константы


Вы наверное часто видели в библиотеках передачу константы в функцию, далеко ходить не надо: digitalWrite(13, HIGH); , где HIGH - что это? Если вы откроете Arduino.h, то найдёте там HIGH, это - константа, дефайн:

#define HIGH 0x1

А в keywords.txt она указана как LITERAL1, что и даёт ей синий цвет. Давайте сделаем библиотеку, которая выводит текст в зависимости от указанной константы:

testLib.h
#pragma once
#include <Arduino.h>

// константы
#define KEK 0
#define LOL 1
#define AZAZA 2
#define HELLO 3

class testClass {
  public:
    void printer(byte value);
  private:
};
testLib.cpp
#include <testLib.h> // подключаем заголовок обязательно

void testClass::printer(byte value) {
 switch (value) {
 case 0: Serial.println("kek");
  break;
 case 1: Serial.println("lol");
  break;
 case 2: Serial.println("azaza");
  break;
 case 3: Serial.println("hello");
  break;  
 }	
}
testSketch.ino
#include <testLib.h>
testClass testObject;
void setup() {
  Serial.begin(9600);
  testObject.printer(KEK);  // выведет kek
  testObject.printer(LOL);  // выведет lol
  testObject.printer(AZAZA);  // выведет azaza
  testObject.printer(HELLO);  // выведет hello
}
void loop() {
}

Вот так можно передать вместо значения - слово, и работать с такой библиотекой будет удобнее. Заметьте, мы использовали константы (дефайн), это не очень правильно: если в другом подключенном ниже документе или в самом скетче наш дефайн совпадёт с названием другой переменной, функции или другого дефайна, то программа будет работать некорректно! Дефайн ведь распространяется на другие документы, включая главную программу (скетч). Что же делать? Можно называть свои константы настолько уникально, чтобы никто никогда с ними не пересекался, например добавлять префикс с названием библиотеки: MYLIB_CONSTANT. Ещё можно заменить дефайн перечислением, тогда  ваша библиотека не будет влиять на другие и на главный документ, но вот другие библиотеки и внешние дефайны могут залезть и в вашу библиотеку... Смотрим пример, такой же как выше, но на enum:

testLib.h
#pragma once
#include <Arduino.h>

// константы
enum printModes {
 KEK,
 LOL,
 AZAZA,
 HELLO,
};

class testClass {
public:
 void printer(printModes value);
private:
};
testLib.cpp
#include <testLib.h> // подключаем заголовок обязательно

void testClass::printer(printModes value) {
 switch (value) {
 case KEK: Serial.println("kek");
  break;
 case LOL: Serial.println("lol");
  break;
 case AZAZA: Serial.println("azaza");
  break;
 case HELLO: Serial.println("hello");
  break;
  
 }	
}
testSketch.ino
#include <testLib.h>
testClass testObject;
void setup() {
  Serial.begin(9600);
  testObject.printer(KEK);  // выведет kek
  testObject.printer(LOL);  // выведет lol
  testObject.printer(AZAZA);  // выведет azaza
  testObject.printer(HELLO);  // выведет hello
}
void loop() {
}
Использование enum в голом виде тоже может приводить к проблемам: имена enum (как имя самого enum, так и его констант) не должны пересекаться в подключенных документах, иначе будет ошибка двойного объявления. О чём конкретно я говорю:
Пересекающиеся enum
// документ doc1.h
enum engineControl {
  start,
  stop,
  restart,
};

// документ doc2.h
enum soundControl {
  play,
  pause,
  stop,
  replay,
};

// основной документ
#include "doc1.h"
#include "doc2.h"
// уже приведёт к ошибке "значение stop объявлено в другом месте"
Чтобы разделить enum в разных файлах, т.е. изолировать их значения друг от друга, есть три способа:

  • Пространство имён namespace
  • enum class
  • Внесение enum в основной класс библиотеки

Рассмотрим пример через namespace

namespace
// документ doc1.h
namespace engine {
enum engineControl {
  start,
  stop,
  restart,
};
};

// документ doc2.h
namespace sound {
enum soundControl {
  play,
  pause,
  stop,
  replay,
};
};

// основной документ
#include "doc1.h"
#include "doc2.h"

// используем соответствующие namespace'ы
engine::engineControl control1 = engine::stop;
sound::soundControl sound1 = sound::stop;
enum class
// документ doc1.h
enum class engineControl {
  start,
  stop,
  restart,
};

// документ doc2.h
enum class soundControl {
  play,
  pause,
  stop,
  replay,
};

// основной документ
#include "doc1.h"
#include "doc2.h"

// используем пространство имён для ЗНАЧЕНИЙ enum
engineControl control1 = engineControl::stop;
soundControl sound1 = soundControl::stop;
Переделаем наш самый первый пример под enum class, спрятав таким образом константы от других файлов:
testLib.h
#pragma once
#include <Arduino.h>
// константы
enum class printModes {
  KEK,
  LOL,
  AZAZA,
  HELLO,
};

class testClass {
  public:
    void printer(printModes value);
  private:
};
testLib.cpp
#include "testLib.h" // подключаем заголовок обязательно

void testClass::printer(printModes value) {
  switch (value) {
    case printModes::LOL: Serial.println("kek");
      break;
    case printModes::KEK: Serial.println("lol");
      break;
    case printModes::AZAZA: Serial.println("azaza");
      break;
    case printModes::HELLO: Serial.println("hello");
      break;
  }
}
testSketch.ino
#include "testLib.h"
testClass testObject;

void setup() {
  Serial.begin(9600);
  testObject.printer(printModes::KEK);  // выведет kek
  testObject.printer(printModes::LOL);  // выведет lol
  testObject.printer(printModes::AZAZA);  // выведет azaza
  testObject.printer(printModes::HELLO);  // выведет hello
}

void loop() {
}
Ещё одним вариантом является объявление enum внутри класса, тогда обращаться к нему нужно будет через ИМЯ_КЛАССА::
testLib.h
#include <Arduino.h> // нужно для ардуино-функций
class testClass {
  public:
    enum printModes {
      KEK,
      LOL,
      AZAZA,
      HELLO,
    };
    void printer(printModes value);
  private:
};
testLib.cpp
#include "testLib.h" // подключаем заголовок обязательно

void testClass::printer(printModes value) {
  switch (value) {
    case LOL: Serial.println("kek");
      break;
    case KEK: Serial.println("lol");
      break;
    case AZAZA: Serial.println("azaza");
      break;
    case HELLO: Serial.println("hello");
      break;
  }
}
testSketch.ino
#include "testLib.h"
testClass testObject;

void setup() {
  Serial.begin(9600);
  testObject.printer(testClass::KEK);  // выведет kek
  testObject.printer(testClass::LOL);  // выведет lol
  testObject.printer(testClass::AZAZA);  // выведет azaza
  testObject.printer(testClass::HELLO);  // выведет hello
}

void loop() {
}

Вмешательство в компиляцию


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

Вот так можно.h
// lib.h
class testClass {
  public:
    int func() {return 10;}
  private:
};
А вот так нельзя.h
// lib.h
class testClass {
  public:
    int func();
  private:
};

int testClass::func() {
  return 10;
}
Ну и пример как работает дефайн, "влезающий" в библиотеку:
testLib.h
#pragma once
#include <Arduino.h>

void printResult() {
// если определена SEND_NUDES
#ifdef SEND_NUDES
 Serial.begin(9600);
 Serial.println("nudes");
#endif
}
testSketch.ino
// дефайним SEND_NUDES
// ДО подключения библиотеки
#define SEND_NUDES
#include <testLib.h>

void setup() {
  // выведет "nudes" если задефайнен SEND_NUDES
  printResult();
}
void loop() {
}
Зачем это нужно? Условная компиляция позволяет управлять компиляцией кода, то есть жёстко задавать, какие части кода будут компилироваться, а какие - нет. Более подробно об опасностях и тонкостях работы с define, в том числе и для создания библиотек, читайте в предыдущем уроке про директивы препроцессора.

 

Полезные страницы


5 1 голос
Рейтинг статьи
Подписаться
Уведомить о
guest

12 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх