Интерфейс без виртуализации2

Ужос! Ты оказался прав. Всё к тому идёт.(

Кстати, проверил без директив препроцессора. С оптимизатором результат одинаковый.

Спойлер
#define DEBUG         1
#define FREQ2         1000
#define FREQ1         500

void setup() {
  #if DEBUG
  Serial.begin(9600);
  Serial.println();
  Serial.println("Calc Prescaler T2, T1: " __DATE__ " " __TIME__);
  TCCR2A = TCCR2B = 0;
  TCCR1A = TCCR1B = 0;
  #endif

  // Prescaler TCCR2B:
  enum { T2_DIV1 = 1, T2_DIV8, T2_DIV32, T2_DIV64, T2_DIV128, T2_DIV256, T2_DIV1024 };
  if (F_CPU / 256 / 1 <= FREQ2) {
    TCCR2B = T2_DIV1;
    OCR2A = F_CPU / 1 / FREQ2 - 1;
  }
  else if (F_CPU / 256 / 8 <= FREQ2) {
    TCCR2B = T2_DIV8;
    OCR2A = F_CPU / 8 / FREQ2 - 1;
  }
  else if (F_CPU / 256 / 32 <= FREQ2) {
    TCCR2B = T2_DIV32;
    OCR2A = F_CPU / 32 / FREQ2 - 1;
  }
  else if (F_CPU / 256 / 64 <= FREQ2) {
    TCCR2B = T2_DIV64;
    OCR2A = F_CPU / 64 / FREQ2 - 1;
  }
  else if (F_CPU / 256 / 128 <= FREQ2) {
    TCCR2B = T2_DIV128;
    OCR2A = F_CPU / 128 / FREQ2 - 1;
  }
  else if (F_CPU / 256 / 256 <= FREQ2) {
    TCCR2B = T2_DIV256;
    OCR2A = F_CPU / 256 / FREQ2 - 1;
  }
  else if (F_CPU / 256 / 1024 <= FREQ2) {
    TCCR2B = T2_DIV1024;
    OCR2A = F_CPU / 1024 / FREQ2 - 1;
  }

  // Prescaler TCCR1B:
  enum { T1_DIV1 = 1, T1_DIV8, T1_DIV64, T1_DIV256, T1_DIV1024};
  if (F_CPU / 65536 / 1 <= FREQ1) {
    TCCR1B = T1_DIV1;
    OCR1B = F_CPU / 1 / FREQ1 - 1;
  }
  else if (F_CPU / 65536 / 8 <= FREQ1) {
    TCCR1B = T1_DIV8;
    OCR1B = F_CPU / 8 / FREQ1 - 1;
  }
  else if (F_CPU / 65536 / 64 <= FREQ1) {
    TCCR1B = T1_DIV64;
    OCR1B = F_CPU / 64 / FREQ1 - 1;
  }
  else if (F_CPU / 65536 / 256 <= FREQ1) {
    TCCR1B = T1_DIV256;
    OCR1B = F_CPU / 256 / FREQ1 - 1;
  }
  else if (F_CPU / 65536 / 1024 <= FREQ1) {
    TCCR1B = T1_DIV1024;
    OCR1B = F_CPU / 1024 / FREQ1 - 1;
  }
  #if DEBUG
  Serial.print("FREQ2: ");  Serial.println(FREQ2);
  Serial.print("TCCR2B: "); Serial.println(TCCR2B);
  Serial.print("OCR2A: ");  Serial.println(OCR2A);
  Serial.println();
  Serial.print("FREQ1: ");  Serial.println(FREQ1);
  Serial.print("TCCR1B: "); Serial.println(TCCR1B);
  Serial.print("OCR1B: ");  Serial.println(OCR1B);
  #endif
}

void loop() {}

Скетч использует 474 байт (1%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 9 байт (0%) динамической памяти, оставляя 2039 байт для локальных переменных. Максимум: 2048 байт.

Да, не, ну, понятно, что там всё нормально.

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

1 лайк

Прошу подробностей!

Я не мог присутствовать вчера. До 5 утра переставлял айфон 13 ПМ в новый корпус и порвал пару шлейфов… :sob:

а говорят израиль - богатая страна :slight_smile:

Друг заднее стекло разбил. А я его подвел вчера… Хоть работает, пока придут порванные кусочки.

Про “богатство” не понял. Я не пользуюсь айфонами, на моих фото надпись “Redmi note 9 pro” это телефон за 100 долларов, даже меньше.
Но друг другой. У него и этот 13ПМ с терабайтом и 16ПМ пойдет покупать такой-же…
Дорогой телефон, на мой взгляд - только понты. А Айфон еще и неудобный, для меня. И двинутый на странно понимаемой безопасности данных.

1 лайк

Зато с айфона с банка деньги не утащат.
//у меня всё эпловское, на днях правда купил вендокомп 10х10см! и монитор 32” к нему, для солидвёрка. Ато аймак 2010г, 15лет и никак не сдохнет.

1 лайк

Да, ладно! Изучайте – Google Search

это не относится к афону, туда например нельзя поставить стороннее приложение. Реально не слышал про взлом эплотехники, только слухи и пугалки. У меня она с 2007года. Ни взломов, ни вирусов НИКТО не показал, одни басни.

Чота ржу !!!

4 лайка

Да, неужели?

Ну, Вы не слышали, а вот мужики с Apple Support – слышали.

1 лайк

и там не слышали - это все варианты нае… обмануть живого дурачка. К технике не относится. Варианта дистанционного взлома эплотехники не зафиксировано, только басни, пугалки, “новости”.

Впрочем, щас и винду не сломать насколько знаю, андройка хз, в неё проще впялить приложение.

А главное: эплотехника это для глупых понторезов, смысла её покупать здравомыслящим людям нет никакого. Это всем известно.

Да ладно… Евреи для американцев похачили ифон какого-то террориста. В газетах про это писали.

…держа его В РУКАХ.

Ну, допустим.

При iLeaks-е никто ничего не держал, а сиськофотки попёрли у селебсов.

Простым брутфорсом долбанули клауд и делу конец: 2014 celebrity nude photo leak - Wikipedia

То, что у конкретного Петра Петровича из Петрово не ломали ифон, еще не значит, что это невозможно.

В любой технике самое слабое место это человек. В связи с этой аксиомой неуязвимых устройств нет.

1 лайк

Ломали самого Петровича, а не его айфон ахаха

согласен, и называется эта связка - социальная инженерия.

3 лайка

Почти месяц прошел с момента создания данной темы.
Много водки выпито, скурено документации по CPP и в перерывах работы поработано за это время. Да и сама тема уже канула в Лету…
В общем я не поверил на слово всем, кто справедливо отговаривал меня от идентификации классов по ID. И все же добил эту тему до конца.
Тема с ID имеет место быть и работает. НО, добавление любого нового функционала требует правки ВСЕХ связанных методов. Крайне не удобно. И усложняет отладку. Все как и было мне сказано.
Никогда сам так больше делать не буду. Но, не попробовать и не убедиться в этом лично я не мог.
Евгений Петрович мне тогда написал:

Так вот я думаю, что в Вашем случаю программу тоже можно собрать вот так из кирпичиков…

Современное решение – создать шаблонную функцию…

Начал я с класса SParametr. Выделил из него часть функционала, отвечающего за изменение и сохранение параметра в EEPROM , и упаковал все это в отдельный класс STunable. А в SParametr передается указатель на экземпляр класса STunable или nullptr, если изменение не требуется.
Теперь вроде как каждый объект класса SParametr собирается “из кирпичиков”. Если я правильно понял Евгения Петровича. И “избыточными” тут в каждом объекте можно условно назвать только 2 байта - адрес связанного экземпляра класса STunable.

Текст parametr.h под катом
#ifndef _PARAMETR_H_
#define _PARAMETR_H_

#include <stdint.h>
#include <Arduino.h>
#include <EEPROM.h>

struct STunable {
public:
  constexpr STunable(const uint8_t eeprom_addr, const int16_t min, const int16_t max)
    : _eeprom_addr(eeprom_addr), _minimum(min), _maximum(max), _blinking(true) {}

  // разрешает (true), запрещает (false) мигание значения параметра
  void setBlinking(const bool value) {
    _blinking = value;
  }
  // возвращает true, если мигание значения параметра разрешено
  bool blinking(void) const {
    return _blinking;
  }
  // возвращает true, если в данный момент значения параметра видимо
  bool visible(void) const {
    return IS_VISIBLE;
  }
  // возвращает адрес значения параметра в EEPROM
  uint8_t address(void) const {
    return _eeprom_addr;
  }
  // возвращает минимальное значение параметра
  int16_t minimum(void) const {
    return _minimum;
  }
  // возвращает максимальное значение параметра
  int16_t maximum(void) const {
    return _maximum;
  }

protected:
  const uint8_t _eeprom_addr;  // адрес параметра в EEPROM
  const int16_t _minimum;      // минимальное значение переменной
  const int16_t _maximum;      // максимальное значение переменной
  bool _blinking;              // признак мигания значения переменной на экране

  static const bool &IS_VISIBLE;  // ссылка на статический признак вывода значения переменной на экран
};

struct SParametr : public Printable {
public:
  constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit, const char *const postfix, STunable *tunable)
    : _value(value), _precision(precision), _name(name), _unit(unit), _postfix(postfix), _tunable(tunable),
      /*_del(intPow(10, precision)), */ _name_len(strlenUtf8(name)), _unit_len(strlenUtf8(unit)), _postfix_len(strlenUtf8(postfix)) {}
  constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit, const char *const postfix)
    : SParametr(value, precision, name, unit, postfix, nullptr) {}
  constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit)
    : SParametr(value, precision, name, unit, nullptr, nullptr) {}
  constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit, STunable *tunable)
    : SParametr(value, precision, name, unit, nullptr, tunable) {}

  // расчет количества символов в UTF-8
  static constexpr size_t strlenUtf8(const char *const ptr) {
    return !ptr ? 0 : (!*ptr ? 0 : ((((*ptr & 0xc0) != 0x80) ? 1 : 0) + strlenUtf8(ptr + 1)));
  }
  // длина строки экрана
  static uint8_t lineWidth(void) {
    return LINE_WIDTH;
  }
  // возвращает указатель на себя, для использования в SScreen и SMenuItem
  SParametr *self(void) {
    return this;
  }
  // возвращает количество символов параметра
  size_t length(void) const {
    return _name_len + _unit_len + valueLength();
  }
  // изменение значения параметра
  void change(const int8_t val) const {  // val может быть как положительным, так и отрицательным
    if (_tunable) {
      _value += val;
      // если val == 10, то _value должно быть кратно 10 (корректируем его, если это не так)

      /*
      if (!(val % 10) && _value % 10) {  // если val кратно 10, а _value нет, то корректируем значение до десятков
        _value /= 10;                    // реализация в лоб, надо бы это все оптимизировать!!!
        _value *= 10;
      }
      */

      // попытка оптимизации
      if (abs(val) == 10) {  // val по модулю может быть либо 1, либо 10 (другие варианты проектом не предусмотрены), потому упрощаем
        int16_t temp_fract = _value / 10;
        int16_t temp_value = temp_fract * 10;
        _value = _value != temp_value ? temp_value : _value;  // если числа не равны, то заменяем _value значением, кратным 10
      }
      // коррктировка значения при выходе за границы диапазона
      _value = constrain(_value, _tunable->minimum(), _tunable->maximum());
    }
  }
  // методы для работы с EEPROM
  // сохранение значения параметра в EEPROM
  void toEEPROM(void) {
    if (_tunable) {
      if (_tunable->address()) {  // если _eeprom_addr == 0, то ничего не сохраняем
        EEPROM.put(_tunable->address(), _value);
      }
    }
  }
  // чтение значения параметра из EEPROM
  void fromEEPROM(void) const {
    if (_tunable) {
      if (_tunable->address()) {  // если _eeprom_addr == 0, то ничего не читаем
        EEPROM.get(_tunable->address(), _value);
        // коррктировка значения при выходе за границы диапазона
        _value = constrain(_value, _tunable->minimum(), _tunable->maximum());
      }
    }
  }

protected:
  /*
  // целочисленное возведение в степень
  static constexpr uint16_t intPow(const uint8_t value, const uint8_t exp) {
    return !exp ? 1 : value * intPow(value, exp - 1);
  }
*/
  // потоковый вывод параметра
  size_t printTo(Print &p) const {
    size_t res = 0;
    // печать наименования параметра
    if (_name_len) {
      p.print(_name);
      res += _name_len;  // количество напечатанных символов с учетом формата UTF-8 (_name_len рассчитана на этапе компиляции)
    }
    // печать значения параметра (если параметр изменяемый, мигание разрешено, и в данный момент параметр не должен быть виден, то забиваем пробелами)
    if (_tunable && _tunable->blinking() && !_tunable->visible()) {
      uint8_t size = valueLength();
      while (size--) {
        res += p.print(' ');
      }
    } else {
      res += printValue(p);
    }
    // печать единицы измерения
    if (_unit_len) {
      p.print(_unit);
      res += _unit_len;  // количество напечатанных символов с учетом формата UTF-8 (_unit_len рассчитана на этапе компиляции)
    }
    // если есть постфикс, то забиваем недостающие места (до конца строки) пробелами и печатам его (постфикс всегда выравнивается по правой границе)
    if (_postfix_len) {
      res += _postfix_len;                           // количество напечатанных символов с учетом формата UTF-8 (_postfix_len рассчитана на этапе компиляции)
      while (res < LINE_WIDTH) res += p.print(' ');  // LINE_WIDTH - длина строки у конкретного ЖКИ (1602)
      p.print(_postfix);
    }
    return res;
  }
  // печать значения параметра
  size_t printValue(Print &p) const {
    size_t res = 0;
    // если число отрицательное, то печатаем знак минуса и отбрасываем его; дальше работаем с положительным числом
    if (_value < 0) res += p.print('-');
    uint16_t value = abs(_value);
    if (_precision) {                             // если число дробное, то разбираем его и печатаем
      uint8_t _del = _precision == 1 ? 10 : 100;  // всего 2 варианта, потому нет смысла считать делитель при компиляции и хранить лишний байт
      uint16_t temp = value / _del;
      // печатаем целую часть числа и точку
      res += p.print(temp);
      res += p.print('.');
      // вычисляем дробную часть
      temp = value % _del;
      // тут проверяем количество знаков дробной части; если меньше разрядности, то добавляем нули
      uint8_t n = temp < 10 ? 1 : 2;            // если меньше 10, то 1 символ, иначе 2 символа (больше быть не может, т.к. максимальная разрядность - 2 [_del = 100])
      if (n < _precision) res += p.print('0');  // добиваем нулем позицию перед значащим символом (в нашем случае только 2 символа макисимум после запятой)
      // печатаем дробную часть
      res += p.print(temp);
    } else {  // целое число, печатаем как есть
      res += p.print(value);
    }
    return res;
  }
  // количество символов значения параметра
  size_t valueLength(void) const {
    size_t res = 0;
    // если отрицательное, до добавляем символ "-"
    if (_value < 0) res++;
    uint16_t value = abs(_value);
    if (_precision) {  // если число дробное
      // тут просто выписал все возможные варианты образования чисел, помещающихся в uint16_t, с разрядностью <= 2 и нашел закономерность
      // получается существенно быстрее, чем с использованием деления; легко воспроизвести и проверить хоть в excel
      if (value < 100) {
        res += (_precision + 2);
      } else {
        res += (_len(value) + 1);
      }
    } else {
      res += _len(value);
    }
    return res;
  }

private:
  // расчет длины значения параметра (uint16_t - макисимум 5 символов)
  // намного быстрее деления
  size_t _len(const uint16_t &val) const {
    return (val < 10 ? 1 : (val < 100 ? 2 : (val < 1000 ? 3 : (val < 10000 ? 4 : 5))));
  }

protected:
  int16_t &_value;             // указатель на значение переменной
  const uint8_t _precision;    // количество разрядов после запятой
  const char *const _name;     // короткое имя
  const char *const _unit;     // единица измерения
  const char *const _postfix;  // постфикс
  STunable *_tunable;          // указатель на объект STunable (с методами изменения и сохранения значения параметра)
  // const uint8_t _del;          // делитель (1 байт, поддерживает максимум 2 знака после запятой), вычисляется на этапе компиляции
  const uint8_t _name_len;     // длина строеи _name, вычисляется на этапе компиляции
  const uint8_t _unit_len;     // длина строеи _unit, вычисляется на этапе компиляции
  const uint8_t _postfix_len;  // длина строеи _postfix, вычисляется на этапе компиляции

  static const uint8_t &LINE_WIDTH;  // максимальная длина строки (экрана) - статический параметр общий для всех экземпляров;
};

#endif  // _PARAMETR_H_

Далее. Класс отрисовки экрана SScreen может быть двух типов: стандартный, когда в верхней строке выводится название данного экрана, а во второй строке - сам привязанный к данному объекту экземпляр класса SParametr. Во втором типе за отрисовку экрана отвечает внешняя функция, указатель на которую передается экземпляру класса SScreen.
Принял решение разделитить эти классы, унаследовав их от общего класса SScreenBase.

Получилось следующее (screen.h)
#ifndef _SCREEN_H_
#define _SCREEN_H_

#include "param.h"

struct SScreenBase : public Printable {
public:
  // инициализируем на этапе компиляции и больше не меняем
  constexpr SScreenBase(SParametr *parametr)
    : _parametr(parametr) {}
  // изменяет значение привязанного параметра
  void changeValue(const int8_t value) const {
    if (_parametr) _parametr->change(value);
  }
  // возвращает указатель на привязанный параметр
  SParametr *getParametr(void) const {
    return _parametr->self();
  }

protected:
  SParametr *_parametr;  // ссылка на параметр
};

// стандартный класс SScreen: в 1 строке ЖКИ выводится название элемента (title), во 2 строке печатается параметр (parametr)
struct SScreen : public SScreenBase {
public:
  // инициализируем на этапе компиляции и больше не меняем
  constexpr SScreen(SParametr *parametr, const char *const title)
    : SScreenBase(parametr), _title(title), _title_len(SParametr::strlenUtf8(title)) {}

protected:
  // потоковый вывод параметра
  size_t printTo(Print &p) const {
    p.print('\f');  // управляющий символ FORM FEED (FF), устанавливает начало новой страницы, экрана (для ЖКИ - setCursor(0, 0)
    // 1 строка
    p.print(_title);
    size_t res = _title_len;                                   // длина заголовка в UTF-8
    while (res < _parametr->lineWidth()) res += p.print(" ");  // lineWidth() - длина строки у конкретного ЖКИ (1602)
    res += p.println();
    // 2 строка
    size_t len = p.print(*_parametr);
    while (len++ < _parametr->lineWidth()) res += p.print(" ");  // lineWidth() - длина строки у конкретного ЖКИ (1602)
    res += len;

    return res;
  }

protected:
  const char *const _title;  // заголовок (наименование параметра)
  const uint8_t _title_len;  // длина заголовка в UTF-8
};

// кастомный класс ScreenCustom: за отрисовку экрана отвечает функция pShow(), параметр (parametr) нужен для возможности его изменения (например в MenuItem)
// pShow() содержать и другие объекты класса SParametr, но мы про них ничего не знаем
struct SScreenCustom : public SScreenBase {
public:
  // инициализируем на этапе компиляции и больше не меняем
  constexpr SScreenCustom(SParametr *parametr, size_t (*pShow)(Print &p))
    : SScreenBase(parametr), _pShow(pShow) {}

protected:
  // потоковый вывод параметра
  size_t printTo(Print &p) const {
    p.print('\f');  // управляющий символ FORM FEED (FF), устанавливает начало новой страницы, экрана (для ЖКИ - setCursor(0, 0)
    return _pShow(p);
  }

protected:
  size_t (*_pShow)(Print &p);
};


#endif  // _SCREEN_H_

Класс SMenuItem, экземплярам которого передаются указатели на связанные экземпляры классов SScreen и SScreenCustom сделал шаблонным. И его сильно урезанная, но компилируемая версия

выглядит так (menu.h)
#ifndef _MENU_H_
#define _MENU_H_

#include "screen.h"

template<typename TScreen>
struct SMenuItem {
  // Конструктор - создать элемент
  SMenuItem(SMenuItem *parent, SMenuItem *prev, TScreen *screen)
    : _parent(parent), _next(nullptr), _prev(prev), _child(nullptr), _screen(screen) {

    if (_parent) {                                                  // Если у нас есть родитель
      if (_parent->getChild() == nullptr) _parent->setChild(this);  // если у родителя пока нет детей, записываемся началом списка детей
    }
    if (_prev) {             // Если у нас есть предыдущий элемент
      _prev->setNext(this);  // записываемся ему в "следующие"
    }
  }

  // Функции возврата указателей на элементы структуры
  SMenuItem *getParent(void) {
    return _parent;
  }
  SMenuItem *getChild(void) {
    return _child;
  }
  SMenuItem *getNext(void) {
    return _next;
  }
  SMenuItem *getPrevious(void) {
    return _prev;
  }

  // Функции устновки указателей на элементы структуры
  void setParent(SMenuItem *item) {
    _parent = item;
  }
  void setChild(SMenuItem *item) {
    _child = item;
  }
  void setPrevious(SMenuItem *item) {
    _prev = item;
  }
  void setNext(SMenuItem *item) {
    _next = item;
  }

  // Функция отрисовки элемента меню
  void show(void) {
    DEV_OUT.print(*_screen);
  }
  void changeValue(const int8_t val) {
    if (_screen) _screen->changeValue(val);
  }
  void saveValue(void) {
    _screen->getParametr()->saveToEEPROM();
  }

protected:
  SMenuItem *_parent;  // адрес родительского элемента (nullptr - если нет родителей)
  SMenuItem *_next;    // следующий элемент данного уровня (nullptr - если этот - последний)
  SMenuItem *_prev;    // предыдущий элемент данного уровня (nullptr - если этот - первый)
  SMenuItem *_child;   // адрес первого ребенка в списке (nullptrll - если это лист)
  TScreen *_screen;

  static Print &DEV_OUT;  // статическая ссылка на устройство вывода (Serial, LCD и т.д., унаследованных от Print)
};

#endif  // _MENU_H_

Ну и собственно тестирование всего этого безобразия:

файл *.ino
#include "menu.h"
#include <Printable.h>

bool is_visible = true;

constexpr uint8_t LCD_COLS = 16;
const uint8_t &SParametr::LINE_WIDTH = LCD_COLS;
const bool &STunable::IS_VISIBLE = is_visible;

template<typename TScreen>
constexpr Print &SMenuItem<TScreen>::DEV_OUT = Serial;

int16_t int16, vint16, vint162,
  temp_current,  // текущая температура
  fan_rmp,       // обороты кулера
  pwm_duty,      // скважность ШИМ кулера текущая
  temp_set;      // заданная температура

STunable tnbl(2, 0, 999);
SParametr prm(int16, 1, "T=", "*C");
SParametr vprm(vint16, 1, "L=", "km", &tnbl);
SParametr vprm2(vint162, 2, "P=", "kPa", new STunable(4, 100, 999));

SParametr prm_temp_current(temp_current, 1, "T=", "°C"),
  prm_fan_rpm(fan_rmp, 0, "RPM=", ""),
  prm_pwm_duty(pwm_duty, 0, "PWM=", "", new STunable(6, 0, 255)),
  prm_temp_set(temp_set, 1, "Ts=", "°C", new STunable(8, 0, 40));

size_t scrTop(Print &p) {
  // 1 строка
  size_t size = p.print(prm_temp_current);
  size += prm_temp_set.length();
  while (size < LCD_COLS) size += p.print(" ");  // LCD_COLUMNS_COUNT - длина строки у конкретного ЖКИ (1602)
  p.println(prm_temp_set);
  // 2 строка
  size = p.print(prm_fan_rpm);
  size += prm_pwm_duty.length();
  while (size < LCD_COLS) size += p.print(" ");  // LCD_COLUMNS_COUNT - длина строки у конкретного ЖКИ (1602)
  p.print(prm_pwm_duty);

  return size;
}

SMenuItem<SScreen> item1(nullptr, nullptr, new SScreen(&vprm, "Trmperature"));
SMenuItem<SScreenCustom> item2(nullptr, nullptr, new SScreenCustom (nullptr, scrTop));

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  delay(100);

  Serial.println(prm);
  Serial.println(vprm);
  Serial.println(vprm2);

  item1.show();
  Serial.println();
  item2.show();
}

void loop() {
  // put your main code here, to run repeatedly:
}

Это все тоже работает.

Вопрос наверно больше к Евгению Петровичу.
Правильно ли я понял предлагаемую им концепцию?
Или вообще не по тому пути пошел.
Если вообще все не так надо делать, то просьба наставить на путь истинный.