Настройка проекта в PlatformIO

В предыдущей статье я описал общий дизайн r2lora и выбрал инструменты для разработки. Теперь можно остановится на конфигурировании проекта и разбиении его на отдельные модули. PlatformIO отлично интегрирован с множеством различных инструментов и позволяет делать сложные вещи почти так же просто, как и в Java.

Управление зависимостями

Бич любой разработки на C/C++ - это управление зависимостями. В языке и экосистеме просто нет стандартного способа для этого. Каждый делает это по-своему. Есть широко известный в узких кругах Conan. Но в нём нет множества библиотек. Есть библиотеки в операционных системах, но они не подходят для микроконтроллеров и их версии определяются операционными системами, а не приложениями.

В PlatformIO есть свой репозиторий библиотек, который удобным образом интегрирован в платформу.

Здесь можно отфильтровать библиотеки по типу микроконтроллера, фреймворку, назначению и многим другим параметрам. Это необычайно полезно, потому что существует великое множество различных микроконтроллеров. И все они разные. Разная архитектура, разные наборы регистров, разные объёмы памяти. И то, что PlatformIO изначально это учитывает, позволяет хоть как-то бороться с хаосом.

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

[env]
lib_deps = 
	jgromes/RadioLib@^4.6.0
	prampec/IotWebConf@^3.2.0
	bblanchon/ArduinoJson@^6.18.5
	thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.2.1

Это добавит 4 библиотеки:

  • jgromes/RadioLib - библиотека для работы с чипом LoRa. Она предоставляет универсальный интерфейс для отправки и получения данных, а также скрывает некоторые особенности реализации самих чипов.
  • prampec/IotWebConf - библиотека для конфигурирования. Она поднимает точку доступа, хранит конфигурацию системы в постоянной памяти и полностью отвечает за начальную конфигурацию системы.
  • bblanchon/ArduinoJson - сериализация и десериализация JSON.
  • thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays - удобная библиотека для работы со встроенным дисплеем.

Все зависимости просто скачиваются из git репозитория и складываются в специальную папку внутри проекта. По сути это очень похоже на работу с зависимостями в npm. Эти зависимости компилируются как часть проекта в единый бинарник - firmware.bin, который прошивается в микроконтроллер. Если какие-то библиотечные функции не используются, то они и не попадают в финальный дистрибутив. Это особенно актуально для микроконтроллеров, так как размер памяти там обычно маленький. Для примера плата TTGO LoRa32-OLED V2 имеет всего 4мб встроенной флеш памяти и 320кб оперативной. И это считается очень много.

Платы

Если говорить о платах, то в PlatformIO есть специальная сущность - “board” (плата). Каждая плата характеризуется типом процессора, частотой работы, размером оперативной памяти и многими другими параметрами. Если в проекте планируется поддержка разных плат, то их достаточно объявить в конфигурации и тогда PlatformIO будет собирать отдельную прошивку для каждой из них.

[env:ttgo-lora32-v2]
platform = espressif32
board = ttgo-lora32-v2
build_flags = 
    ${env.build_flags}
	-DPIN_CS=18
	-DPIN_DI0=26
	-DPIN_RST=23
	-DPIN_OLED_SDA=21
	-DPIN_OLED_SCL=22

В примере выше я объявил некоторую конфигурацию (“ttgo-lora32-v2”), которую нужно использовать для сборки приложения под плату ttgo-lora32-v2 с дополнительными флагами компиляции. ${env.build_flags} наследует общую конфигурацию build_flags из объявления env. Остальные параметры специфичны для конкретной платы. Из-за того, что каждая плата имеет фиксированные номера пинов, то их можно объявить прямо в конфигурации.

r2lora поддерживает все платы, в которых используется LoRa чип:

Никто не мешает прикрутить LoRa чип вручную к плате. В таком случае достаточно будет объявить новый env и прописать каждый из пинов.

Для каждой платы может быть собственный набор библиотек и своя собственная папка для сборки:

Платформа и фреймворк

Помимо плат, в PlatformIO есть понятие платформы и фреймворка. Платформа обычно соответствует типу микроконтроллера. В случае с ESP32 - это espressif32. Иногда микроконтроллер может поддерживать разные архитектуры, тогда каждая из них имеет свой id платформы. Ближайший аналог - это arm64 или armhf. За тем исключением, что платформа описывает конкретный чип. Нечто похожее я пытался сделать, когда компилировал приложения для конкретных процессоров.

Все платы, которые содержат LoRa чипы основаны на ESP32, поэтому platform = espressif32.

Фреймворк - это нечто вроде SDK, который обычно предоставляет производитель микроконтроллеров. Обычно с помощью одного SDK можно разрабатывать под разные модели микроконтроллеров. Иногда есть универсальные SDK. Для ESP32 можно разрабатывать как с помощью framework = espidf, так и framework = arduino. Я выбрал arduino, так как с помощью Arduino API написано множество библиотек. Несмотря на то, что ESP32 несовместим напрямую в Arduino, существует промежуточный слой framework-arduinoespressif32, который конвертирует Arduino API в вызовы ESP32. Немного неоптимально, но ESP32 достаточно мощный, и никто сильно этим не заморачивается.

Структура проекта

При использовании framework = arduino проект должен быть структурирован следующим образом:

Внутри папки src находятся исходники и входная точка в программу. Обычно это main с методами void loop() и void setup(). Можно программировать на С или С++. Однако, если некоторые библиотеки написаны на С++ и не поддерживают подключение из С, то придётся писать на С++ (sigh!).

Есть ещё папка lib. Она должна содержать небольшие компоненты приложения, которые нужно оттестировать. Выглядит это так:

  • PlatformIO будет брать каждую подпапку внутри lib и компилировать
  • линковать с тестами
  • собирать firmware.bin
  • прошивать на плату и получать результаты выполнения теста

Тестирование проекта

PlatformIO позволяет запускать тесты прямо на микроконтроллере! Тесты можно разбить на отдельные Test Suite и внутри каждого сделать несколько юнит тестов. Каждый Test Suite - это отдельная прошивка, которая имеет одну точку входа и заливается на микроконтроллер стандартным способом.

В скриншоте выше я сделал несколько Test Suite:

  • api - тестирование REST API интерфейса
  • embedded (lora) - тестирование LoRa модуля. Достаточно стартовать приём и остановить. Проверяется правильная конфигурация пинов и использование чипа
  • fota - тестирование Firmware-Over-The-Air (FOTA). Относительно сложный тест, который по-умолчанию не запускается. Он скачивает новую прошивку из S3 и применяет её.
  • util - тестирование небольшого вспомогательного класса.

Я не стал заморачиваться с 80% покрытием кода тестами (его ещё и не подсчитать просто так!), поэтому постарался по-максимуму оттестировать код. А уж, что не получилось, то не получилось.

Каждый Test Suite содержит несколько юнит тестов. Для написания юнит тестов используется библиотека unity. Поскольку юнит тест должен выполняться один раз, то метод loop не должен делать ничего полезного. Например, он может моргать светодиодом:

void loop() {
  digitalWrite(13, HIGH);
  delay(100);
  digitalWrite(13, LOW);
  delay(500);
}

Вся логика должна находится в методе setup:

void setup() {
  // NOTE!!! Wait for >2 secs
  // if board doesn't support software reset via Serial.DTR/RTS
  delay(2000);

  UNITY_BEGIN();
  RUN_TEST(test_success_start);
  RUN_TEST(test_no_request);
  RUN_TEST(test_invalid_json);
  RUN_TEST(test_begin_failed);
  RUN_TEST(test_success_stop_even_if_not_running);
  RUN_TEST(test_pull);
  RUN_TEST(test_frames_after_stop);
  RUN_TEST(test_cant_tx_during_receive);
  RUN_TEST(test_invalid_tx_request);
  RUN_TEST(test_empty_tx_request);
  RUN_TEST(test_invalid_lora_tx_code);
  RUN_TEST(test_invalid_tx_data_request);
  RUN_TEST(test_success_tx);
  UNITY_END();
}

В примере выше я добавляю в Test Suite несколько юнит тестов. Каждый юнит тест это отдельная функция:

void test_success_start(void) {
  ApiHandler handler(&web, &mock, NULL, NULL);
  String output;
  int code = handler.handleStart(VALID_RX_REQUEST, &output);
  TEST_ASSERT_EQUAL_INT(200, code);
  assertStatus(&output, "SUCCESS");
}

Здесь не должно быть ничего неожиданного. Обычное тестирование какое можно встретить даже в Java проекте.

Можно запускать как отдельные Test Suite, так и все вместе.

pio test --test-port /dev/ttyACM0 -e ttgo-lora32-v2 -f fota

В результате PlatformIO выведет на экран разную статистику:

Более детальную документацию можно найти на сайте проекта: unit testing.

Далее

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