FastLED - настраиваем пины и порядок цветов "на лету" или как работать с наследованием классов в C++

vortigont

★★★★★✩✩
24 Апр 2020
910
489
Saint-Petersburg, Russia
"Ламподелам" на заметку.
Всем известная библиотека FastLED от рождения страдает тем что в ней нельзя динамически назначать пин вывода для подключения адресных лент и порядок следования цветов в них. Проблема известная и планов решить её нет и не предвидится.
В свое время билиотека создавалась под слабенькие, по нынешним меркам, 8-битные контроллеры с максимальной оптимизацией и вносить радикальные изменения в архитектуру похоже не планируется.
На гитхабе годами висят похожие тикеты #282, #826 с просьбами сделать что-нибудь с этим. А из предлагаемых костыльных вариантов есть только создание ветвистых конструкций из всех возможных наборов пинов и цветов

switch:
// для пинов
switch (pin_num) {
  case 1: FastLED.addLeds<WS2811,1>(leds, NUM_LEDS); break;
  case 2: FastLED.addLeds<WS2811,2>(leds, NUM_LEDS); break;
  case 3: FastLED.addLeds<WS2811,3>(leds, NUM_LEDS); break;
  ...
  default: break;
}

// для цветов
switch (color_mode) {
  case RGB: FastLED.addLeds<WS2811,PIN,RGB>(leds, NUM_LEDS); break;
  case BGR: FastLED.addLeds<WS2811,PIN,BGR>(leds, NUM_LEDS); break;
  ...
  default: break;
}
Если же надо задавать И пин И порядок цветов, то число комбинаций начинает расти в прогрессии. Всё это а) некрасиво б) приводит к генерации излишнего кода

Попробуем решить эту проблему более элегантным способом с помощью ООП и наследования. Условие - обойтись без правки самой библиотеки т.к. это сделает неудобным процесс установки/обновления.

В качестве платформы возмем чипы семейства esp32 как наиболее доступные и популярные сейчас для поделок на адресных лентах.
Изучаем документацию на чип и смотрим как реализован вывод данных на адресную ленту. В библиотеке под есп32 для этого есть поддержка двух движков - RMT и I2S. Оба этих движка позволяют подключать на вывод любой доступный пин через коммутационную матрицу. Т.е. технически абсолютно неважно в какой пин выводить данные, оптимизация на уровне компилируемого кода никак не завязанна на конкретные пины или связанные с ними регистры.

Теперь смотрим на реализацию FastLED - движёк RMT для адресных лент находится в файле clockless_rmt_esp32.h
Смотрим конструктор класса
C++:
ESP32RMTController(int DATA_PIN, int T1, int T2, int T3, int maxChannel, int memBlocks);
и видим что, собственно, пин куда цепляется движёк задается при создании экземпляра класса, а не при компиляции! Т.е. по сути динамическая конфигурация по пину уже есть! Нужно только решить вопрос как создавать этот объект и цеплять его к движку фастлед.

Далее смотрим как происходит инициализация движка фастлед, обычно это строка в скетче типа
C++:
FastLED.addLeds<LED_TYPE,DATA_PIN,COLOR_ORDER>(leds, NUM_LEDS);
FastLED.addLeds - это вызов метода экземпляра класса CFastLED высокоуровнего контроллера, который собственно и управляет связкой кадрового буфера с движком вывода. Это шаблонизированный медод:
C++:
    /// Add a clockless based CLEDController instance to the world.
    template<template<uint8_t DATA_PIN, EOrder RGB_ORDER> class CHIPSET, uint8_t DATA_PIN, EOrder RGB_ORDER>
    static CLEDController &addLeds(struct CRGB *data, int nLedsOrOffset, int nLedsIfOffset = 0) {
        static CHIPSET<DATA_PIN, RGB_ORDER> c;
        return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset);
    }
т.е. на этапе компиляции должно быть известно значение используемого чипа, вывод подключения и цветовой порядок. Нам это не подходит, сделать тут ничего нельзя. Ищем какие еще методы есть у CFastLED которые не используют шаблоны и находим интересный метод
C++:
static CLEDController &addLeds(CLEDController *pLed, struct CRGB *data, int nLedsOrOffset, int nLedsIfOffset = 0);
Т.е. можно прицепить к фастлед некий CLEDController объект и кадровый буфер. Похоже на то что нужно! Значит нам нужно создать объект CLEDController или используя принципы наследования объектов что-то унаследованное от CLEDController.
Далее разбираем цепочки наследования классов в библиотеке и получаем примерно следующее:

наследование:
class CPixelLEDController : public CLEDController
    class ClocklessController : public CPixelLEDController
        class WS2812Controller800Khz : public ClocklessController
            class WS2812B : public WS2812Controller800Khz
вот и откопали - от CLEDController до WS2812B лежит 4 промежуточных класса и почти все они написаны в виде шаблонов от различных параметров, в.т.ч. пина и чередования цветов.
Т.е. что нам нужно - написать свои шаблоны классов по цепочке так что бы функционал и методы сохранились как в оригиналах, а из параметров шаблонов убрать пин и порядок цветов. Возможно ли это? Да, возможно! Хоть не так просто.
Надо сказать от пина в параметрах я избавился довольно легко, т.к. исходный класс движка уже был от него отвязан. А вот с чередованием цветов пришлось поломать голову.
Дело в том что метод FastLED.show() который используется для вывода кадра в ленту тоже использует шаблоны для компиляции оптимизированного кода преобразования цветов. Оптимизация, правда, там весьма своеобразная и состоит из статических функций возвращающих нужный байт из тройки RGB. Но от них никуда не дется если не переписывать саму библиотеку фастлед или не заниматься реимплементацией её методов (чего хотелось бы избежать).

В итоге вызов show() сводится к такой функции класса CPixelLEDController:
C++:
   virtual void showPixels(PixelController<RGB_ORDER,LANES,MASK> & pixels) = 0;
функция чисто виртуальная, т.е. она должна быть реализованна в дочерних классах и в качестве аргумента в ней идет объект от шаблона с параметром RGB_ORDER.
Казалось бы в чем проблема? В том что нам нужно "научить" этот метод кушать не конкретный объект типа PixelController<RGB_ORDER,LANES,MASK>, а некое подмножество объектов с разными вариантами параметра RGB_ORDER
как-то так?
C++:
template<typename T>
virtual void showPixels(T & pixels) = 0;
и конечно же так сделать нельзя :) Почему? А потому что виртуальные методы классов в с++ не могут быть основаны на шаблонах.
Эту забавную задачу можно решить с помошью crtp. Т.е. делегировать реализацию шаблона в дочерний класс, а в родительском оставить вызов на дочерний класс который пока еще не известен.

Что в итоге у меня получилось - легкий заголовочный файл с декларациями классов-обёрток над движком из FastLED полностью сохраняющих её АПИ.
Пользоваться им довольно просто - кладем заголовочник в каталог с проектом,включаем в скетч и создаем экземпляр нашего объекта адресной ленты с параметрами пина и цвета вычитанными из какого-нибудь джейсона или еепром или еще какого конфига.

C++:
#include "w2812-rmt.hpp"

// задаём пин
int gpio_num = 10;
// задаём чередование цветов
EOrder color_order = GRB;
// размер буфера
int CRGB_buffersize = 256;

CRGB* CRGB_buffer;

setup(){
CRGB_buffer = new CRGB[numofleds];
// создаём объект ленты
ESP32RMT_WS2812B wsstrip(gpio_num, color_order);
// цепляем нашу ленту к контроллеру
FastLED.addLeds(&wsstrip, CRGB_buffer, CRGB_buffersize);  
}
Из негативных последствий использования модифицированного класса это, теоритически, более медленная работа вызова show() из-за добавленного условия сравнения с константой _rgb_order определяющей порядок чередования цветов текущего экземпляра класса. Но на скоростях ЦПУ есп32 в сравнении с общим временем требуемым для последовательного вывода данных кадра в ленту практически измерить разницу врядли представляется хоть сколько-нибудь возможным.

Надо сказать спасибо авторам фастлед что классы они раскидали довольно аккуратно хоть и не без недостатков. Например в существующей реализации библиотеки нельзя на лету изменить конфигурацию движка - убрать ленту с одного пина и перевесить на другой без перезагрузки контроллера. Но это уже совсем экстравагантные случаи без которых вполне можно обойтись.
 
Изменено:

bort707

★★★★★★✩
21 Сен 2020
2,898
862
Из негативных последствий использования модифицированного класса это, теоритически, более медленная работа вызова show() из-за добавленного условия сравнения с константой _rgb_order определяющей порядок чередования цветов текущего экземпляра класса.
Вообще-то именно для этого шаблоны и используются - чтобы выбирать нужный вариант чередования цветов не во время выполнения, а заранее компилировать только его в исходный код.

Честно говоря, я не очень понял, зачем менять порядок цветов во время исполнения? - обычно этот параметр аппаратно задан для каждой конкретной ленты и не меняется.

Ну и кстати, если хочется добавить свой метод в фастлед - можно ведь выкатить PR, команда вряд ли будет против готового решения.
 

vortigont

★★★★★✩✩
24 Апр 2020
910
489
Saint-Petersburg, Russia
не очень понял, зачем менять порядок цветов во время исполнения?
затем же зачем иметь возможность определить пин во время выполнения - что бы не нужно было перекомпилировать и перезаливать прошивку под каждый конкретный пин и порядок цветов. Проект собирается, заливается в плату, затем открывается вебморда и настраивается всё что требуется уже на лету. В идеале вообще не нужно собирать проект, а можно взять готовый собраный бинарник и просто залить его в плату.
Все имеющиеся проекты "ламп" начинаются с того что нужно открыть какой-нибудь "config.h" и копаться в десятках различных определениях и флагов, менять, пересобирать, перезаливать и так по кругу без конца. По этим конфигам уже можно талмуды составлять. Мне такой подход не нравится - гораздо понятнее, удобнее и быстрее проводить настройку из веб-морды. В своем проекте я от подобного подхода отказался, благо ресурсы современных контроллеров это с легкостью позволяют.

По поводу PR - это не вариант, подобные тикеты висят уже годами и никаких вариантов решения даже не обсуждается. Один из свежих тикетов, там же в комменте расписано почему это не будет реализованно. Я уже поднимал эту тему, но никакого энтузиазма она встретила.
В целом разработака фастлед стагнирует, это уже отмечали. Что будет дальше - посмотрим. Реализованный мною способ это просто пример гибкости с++ и вариант решения задачи где не нужно патчить/править саму библиотеку. Далеко не всегда это возможно.
 
Изменено:

bort707

★★★★★★✩
21 Сен 2020
2,898
862
В целом разработака фастлед стагнирует, это уже отмечали.
так автор умер, репо перешло какому-то временщику, для которого это дело не первой важности.
В таком случае можно форкнуть свой репо и развивать его отдельно, вставляя ссылки в свои проекты ламп и прочего.
Мне приходилось так делать, например, с аддоном для СТМ32, который перестал развиваться после ухода автора, Кларка
 

vortigont

★★★★★✩✩
24 Апр 2020
910
489
Saint-Petersburg, Russia
@bort707, да я так и делаю когда проект или совсем мертв или обойтись обёртками не получается или твои патчи противоречат "идеям проекта". Чаще всего так и бывает. Почти все сторонние либы в моем проекте патченые мою же.
Но фастлед ещё теплится, поэтому было интересно решить эту задачу на уровне своих деривативов, не нарушая совместимость с самой либой.
Подобного "красивого" решения я нигде не нашел.
 
  • Лойс +1
Реакции: bort707