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


В этом уроке мы научимся писать собственные библиотеки для 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

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

#include <testLib.h>

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

void setup() {
}

void loop() {
}

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

#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


#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; // переменная яркости
};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.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;}

#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.

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


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


#pragma once
#include <Arduino.h>

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

#include <testLib.h>

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

void loop() {
}

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


#pragma once
#include <Arduino.h>

// объявление
void printLol();

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.h>	// нужно для ардуино-функций

// реализация
void printLol() {
  Serial.println("lol");
}

#include <testLib.h>

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

void loop() {
}

Обернём в namespace


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

#pragma once
#include <Arduino.h>

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

// реализация
void myFunc::printLol() {
  Serial.println("lol");
}

#include <testLib.h>
void setup() {
  Serial.begin(9600);
  // выведет kek из функции скетча
  printLol();

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

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

void loop() {
}

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

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


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


#pragma once
#include <Arduino.h>

class testClass {
  public:
    long get_x10(int value);
  private:
};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.h>	// нужно для ардуино-функций

long testClass::get_x10(int value) {
  return value*10;
}

#include <testLib.h>
testClass testObject;

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

void loop() {
}

Рассмотрим более сложную ситуацию: нужно принять значение в класс, записать в приватную переменную, и отдельным методом получить её:


#pragma once
#include <Arduino.h>

class testClass {
  public:
    void setValue(int val);
    int getValue();
  private:
    int _value = 0;
};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.h>	// нужно для ардуино-функций

void testClass::setValue(int val) {
  // берём внешнюю val и пишем в свою _value
  _value = val;
}
int testClass::getValue() {
  return _value;	// вернуть переменную из класса
}

#include <testLib.h>
testClass testObject;

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

void loop() {
}

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


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


#pragma once
#include <Arduino.h>

class testClass {
  public:
    int multTo5(int value);
    void multTo7(int* value);
  private:
    
};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.h>	// нужно для ардуино-функций

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

void testClass::multTo7(int* value) {
  // умножить переменную на 7
  *value = *value * 7;
}

#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.

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


#pragma once
#include <Arduino.h>

class testClass {
  public:
    void takeControl(int* value);
    void multTo6();
  private:
    int *_value;	// храним адрес
};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.h>	// нужно для ардуино-функций

void testClass::takeControl(int* value) {
  _value = value;
}
void testClass::multTo6() {
  *_value = *_value * 6;
}

#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() {
}

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

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


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


#pragma once
#include <Arduino.h>

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

};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.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;
}

#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: мы указываем имя функции, которую можно будет вызвать из другой функции. Делается это через указатель на функцию. Посмотрим простой пример без класса:


#pragma once
#include <Arduino.h>

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

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

// вызов приаттаченной функции
void callFunction() {
 (*atatchedF)();
}

#include <testLib.h>

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

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

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

void loop() {
}

Теперь засунем это всё в класс и будем хранить адрес прикреплённой функции внутри класса. Тут есть два варианта, покажу оба, просто чтобы было тут в одном месте это всё.

Первый вариант:


#pragma once
#include <Arduino.h>

class testClass {
  public:
    void attachFunction(void (*function)());
    void callFunction();
  private:
    void (*atatchedF)();
};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.h>	// нужно для ардуино-функций

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

void testClass::callFunction() {
 (*atatchedF)();
}

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

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

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

void loop() {
}

Второй вариант:


#pragma once
#include <Arduino.h>

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

class testClass {
  public:
    void attachFunction(func newFunc);
    void callFunction();
  private:
    func _attachedFunc;
};

#include <testLib.h>	// подключаем заголовок обязательно
#include <Arduino.h>	// нужно для ардуино-функций

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

void testClass::callFunction() {
 _attachedFunc();
}

#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, если он есть, добавить:

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

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


#pragma once
#include <Arduino.h>

class testClass {
  public:
    long get_x10(int value);
  private:
};
extern testClass testObject;

#include <testLib.h> // подключаем заголовок обязательно
#include <Arduino.h> // нужно для ардуино-функций

long testClass::get_x10(int value) {
  return value*10;
}
testClass testObject = testClass();

#include <testLib.h>
// объект не создаём!
void setup() {
  Serial.begin(9600);
  Serial.println(testObject.get_x10(450)); // выведет 4500
}

void loop() {
}

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


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

#define HIGH 0x1

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


#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:
};

#include <testLib.h> // подключаем заголовок обязательно
#include <Arduino.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;
  
 }	
}

#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:


#pragma once
#include <Arduino.h>

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

class testClass {
public:
 void printer(printModes value);
private:
};

#include <testLib.h> // подключаем заголовок обязательно
#include <Arduino.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;
  
 }	
}

#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 не должны пересекаться в подключенных документах, иначе будет ошибка. О чём конкретно я говорю:

// документ 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.

// документ 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;

// документ 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, спрятав таким образом константы от других файлов:


#pragma once
#include <Arduino.h>
// константы
enum class printModes {
  KEK,
  LOL,
  AZAZA,
  HELLO,
};

class testClass {
  public:
    void printer(printModes value);
  private:
};

#include "testLib.h" // подключаем заголовок обязательно
#include <Arduino.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;
  }
}

#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() {
}

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


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

Если сделать define до подключения файла библиотеки, то этот дефайн будет “виден” из заголовочного файла библиотеки и его можно использовать для операторов условной компиляции.


#pragma once
#include <Arduino.h>

void printResult() {
// если определена SEND_NUDES
#ifdef SEND_NUDES
 Serial.begin(9600);
 Serial.println("nudes");
#endif
}

// дефайним SEND_NUDES
// ДО подключения библиотеки
#define SEND_NUDES
#include <testLib.h>

void setup() {
  // выведет "nudes" если задефайнен SEND_NUDES
  printResult();
}
void loop() {
}

Зачем это нужно? Условная компиляция позволяет управлять компиляцией кода, то есть жёстко задавать, какие части кода будут компилироваться, а какие – нет. Более подробно об опасностях и тонкостях работы с define, в том числе и для создания библиотек, читайте в следующем уроке про директивы препроцессора.

Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Последнее обновление Октябрь 12, 2019