Евгений Петрович, попробую показать на примере пары классов из реального проекта.
Начнем с класса (структуры) SParametr (сделал все inline).
В проекте реализация SParametr следующая:
#ifndef _PARAMETR_H_
#define _PARAMETR_H_
#include <Arduino.h>
#include <EEPROM.h>
struct SParametr : public Printable {
// инициализируем на этапе компиляции и больше не меняем
constexpr SParametr(const uint8_t &line_width, const uint8_t eeprom_addr, int16_t &value, const int16_t minimum, const int16_t maximum, const uint8_t precision,
const char *const name, const char *const unit, const char *const postfix, const bool *is_print_value = nullptr)
: _line_width(line_width), _eeprom_addr(eeprom_addr), _value(value), _minimum(minimum), _maximum(maximum), _precision(precision), _name(name), _unit(unit),
_postfix(postfix), _del(intPow(10, precision)), _is_print_value(is_print_value), _name_len(strlenUtf8(name)), _unit_len(strlenUtf8(unit)), _postfix_len(strlenUtf8(postfix)) {}
// длина строки в UTF-8
static constexpr size_t strlenUtf8(const char *const ptr) {
return !*ptr ? 0 : ((((*ptr & 0xc0) != 0x80) ? 1 : 0) + strlenUtf8(ptr + 1));
}
// возвращает значение статического параметра LINE_WIDTH (максимальная длина строки), используется сторонними объектами
static constexpr uint8_t lineWidth(void) {
return LINE_WIDTH;
}
// возвращает указатель на себя, используется сторонними объектами
SParametr *self(void) {
return this;
}
// возвращает количество символов параметра
size_t length(void) const {
return _name_len + _unit_len + valueLength();
}
// возвращает количество символов имени параметра
constexpr size_t nameLength(void) {
return _name_len;
}
// возвращает количество символов единцы измерения параметра
constexpr size_t unitLength(void) {
return _unit_len;
}
// возвращает количество символов значения параметра
size_t valueLength(void) const {
size_t res = 0;
uint16_t val = abs(_value);
// считаем количество десятичных разрядов
do {
res++;
val /= 10;
} while (val);
// добавляем дробные символы
if (_precision) {
if (res > 2) res++; // просто добавляем символ "."
else res += 2; // добавляем символ "." и 1 разряд
}
// если число отрицательное, то еще знак '-'
if (_value < 0) res++;
return res;
}
// методы для работы с EEPROM
void toEEPROM(void) {
if (_eeprom_addr) {
checkBounds();
EEPROM.put(_eeprom_addr, _value);
}
}
void fromEEPROM(void) {
if (_eeprom_addr) {
EEPROM.get(_eeprom_addr, _value);
if (!checkBounds()) EEPROM.put(_eeprom_addr, _value);
}
}
// Метод для изменения значения параметра
void change(const int8_t value) { // val может быть как положительным, так и отрицательным
_value += value;
// не проверял, но должно быть быстрее закомментированного варианта
if (value == 10) { // если value равно 10
int16_t fract = _value % 10; // а _value не кратно 10,
if (fract) _value -= fract; // то округляем (вниз) значение _value до десятков
}
// проверяем выход значения параметра за границы диапазона и корректируем при необходимости
checkBounds();
}
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 {
// печатаем имя параметра
p.print(_name);
uint8_t res = _name_len;
// печатем значение параметра
if (_is_print_value == nullptr || *_is_print_value) {
// выводим значение параметра
if (_del) {
int16_t val = _value / (int16_t)_del;
uint16_t fract = abs(_value) % _del;
res += p.print(val);
res += p.print(".");
size_t len = p.print(fract);
// если число разрядов дробной части меньше количества знаков после точки, то забиваем остаток нулями
while (len < (size_t)_precision) len += p.print("0");
res += len;
} else {
res += p.print(_value);
}
} else {
// забиваем место параметра пробелами
size_t len = valueLength();
res += len;
while (len--) p.print(" ");
}
// печатем единицу измерения
p.print(_unit);
res += _unit_len;
// печатаем постфикс, если есть
if (_postfix_len) { // если есть постфикс, то забиваем недостающие места (до конца строки) пробелами и печатам его
res += _postfix_len;
while (res < LINE_WIDTH) res += p.print(" "); // LINE_WIDTH - длина строки у конкретного ЖКИ (1602)
p.print(_postfix);
}
return res;
}
protected:
const uint8_t _eeprom_addr; // адрес параметра в EEPROM
int16_t &_value; // указатель на значение переменной
const int16_t _minimum; // минимальное значение переменной
const int16_t _maximum; // максимальное значение переменной
const uint8_t _precision; // количество разрядов после запятой
const char *const _name; // короткое имя
const char *const _unit; // единица измерения
const char *const _postfix; // постфикс
const uint8_t _del; // делитель (1 байт, поддерживает максимум 2 знака после запятой), вычисляется на этапе компиляции
const bool *_is_print_value; // указатель на признак вывода значения переменной на экран
const size_t _name_len; // длина строки _name, вычисляется на этапе компиляции
const size_t _unit_len; // длина строки _unit, вычисляется на этапе компиляции
const size_t _postfix_len; // длина строки _postfix, вычисляется на этапе компиляции
static const uint8_t LINE_WIDTH; // максимальная длина строки (экрана) - статический параметр общий для всех экземпляров;
};
#endif // _PARAMETR_H_
Описаны все возможные варианты внутри единой структуры SParametr.
Структура выводит на экран значение передаваемого по ссылке числа в формате с фиксированной запятой, выводит его на экран (с учетом наименования, единицы измерения, постфикса (если есть)). Число можно менять, читать (сохранять) его значение из (в) EEPROM. При изменении значения параметра он будет мигать.
Соответственно в структуре имеются все необходимые для работы с параметром поля.
Далее есть структура SScreen, предназначенная для отрисовки параметра на экране. К каждому экземпляру этой структуры привязан конкретный параметр.
Реализация SScreen (тоже все inline)
#ifndef _SCREEN_H_
#define _SCREEN_H_
#include "parametr.h"
// класс экрана, выводит на экран параметр (2 строка) с заголовком (1 строка)
// в данной реализации остаток строки до конца экрана забивается пробелами, если нужно другое то переопределяйте printTo()!!!
struct SScreen : public Printable {
public:
// инициализируем на этапе компиляции и больше не меняем
constexpr SScreen(SParametr ¶metr, const char *const title)
: _parametr(parametr), _title(title), _title_len(SParametr::strlenUtf8(title)) {}
// изменяет значение привязанного параметра
void changeValue(const int8_t value) {
_parametr.change(value);
}
// возвращает указатель на привязанный параметр
SParametr *getParametr(void) {
return _parametr.self();
}
protected:
// потоковый вывод параметра
virtual size_t printTo(Print &p) const {
p.print('\f'); // управляющий символ FORM FEED (FF), устанавливает начало новой страницы, экрана
// 1 строка
p.print(_title); // количество напечатанных символов берем не из print(),
size_t res = _title_len; // а расчитанное функцией strlenUtf8(), поскольку UTF-8 больше 1 байта
// добиваем пробелами до конца строки
while (res < _parametr.lineWidth()) res += p.print(" "); // lineWidth() - длина строки у конкретного ЖКИ (1602)
res += p.println();
// 2 строка
size_t len = p.print(_parametr); // значение параметра выводится учетом кодировки UTF-8, количество символов правильное
// добиваем пробелами до конца строки
while (len < _parametr.lineWidth()) len += p.print(" "); // lineWidth() - длина строки у конкретного ЖКИ (1602)
res += len;
return res;
}
protected:
SParametr &_parametr; // ссылка на параметр
const char *const _title; // заголовок (наименование параметра)
const uint8_t _title_len; // длина заголовка в UTF-8
};
#endif // _SCREEN_H_
Ссылка на конкретный экземпляр SScreen передается экземпляру SMenuItem и выводится на экран ЖКИ в его методе show(). Реализацию SMenuItem не буду тут приводить. Она построена на основе Вашего урока про связанные списки с некоторыми дополнениями и изменениями.
В файле *.ino эти экземпяры SParametr и SScreen объявляются
следующим образом:
constexpr uint8_t LCD_COLUMNS_COUNT = 16;
constexpr uint8_t SParametr::LINE_WIDTH = LCD_COLUMNS_COUNT;
bool is_value_visible = true; // признак мигания параметра
int16_t temp_current_top, // текущая температура (верх)
temp_current_middle, // текущая температура (середина)
temp_current_bottom, // текущая температура (низ)
fan_rmp, // обороты кулера
pwm_duty, // скважность ШИМ кулера текущая
pwm_duty_min, // скважность ШИМ кулера (минимальная)
pwm_duty_max, // скважность ШИМ кулера (максимальная
backlight_lvl, // уровень подсветки ЖКИ
temp_set, // заданная температура
temp_critical, // критическая температура
reg_Kp, // настройка регулятора Кп
reg_Ki, // настройка регулятора Ки
reg_Kd; // настройка регулятора Кд
SParametr prm_temp_top(0, temp_current_top, 0, 0, 1, "T1=", "°C", ""),
prm_temp_middle(0, temp_current_middle, 0, 0, 1, "T2=", "°C", ""),
prm_temp_bottom(0, temp_current_bottom, 0, 0, 1, "T3=", "°C", ""),
prm_temp_critical(2, temp_critical, 40, 80, 0, "Tc=", "°C", "", &is_value_visible),
prm_temp_set(4, temp_set, 20, 40, 0, "Ts=", "°C", "", &is_value_visible),
prm_fan_rpm(0, fan_rmp, 0, 0, 0, "RPM=", "", ""),
prm_pwm_duty(6, pwm_duty, 0, 255, 0, "PWM=", "", "", &is_value_visible),
prm_backligth_lvl(8, backlight_lvl, 0, 255, 0, "Value=", "", "", &is_value_visible),
prm_reg_Kp(10, reg_Kp, 0, 100, 1, "Kp=", "", "(prop)", &is_value_visible),
prm_reg_Ki(12, reg_Ki, 0, 100, 1, "Ki=", "", "(int)", &is_value_visible),
prm_reg_Kd(14, reg_Kd, 0, 100, 1, "Kd=", "", "(diff)", &is_value_visible),
prm_pwm_min(16, pwm_duty_min, 0, 255, 0, "Minimum=", "", "", &is_value_visible),
prm_pwm_max(18, pwm_duty_max, 0, 255, 0, "Maximum=", "", "", &is_value_visible);
SScreen scr_temp_top(prm_temp_top, "Current temp:"),
scr_temp_middle(prm_temp_middle, "Current temp:"),
scr_temp_bottom(prm_temp_bottom, "Current temp:"),
scr_temp_critical(prm_temp_critical, "Critical temp:"),
scr_temp_set(prm_temp_set, "Set temp:"),
scr_fan_rpm(prm_fan_rpm, "Fan rate:"),
scr_pwm_duty(prm_pwm_duty, "PWM duty:"),
scr_backligth_lvl(prm_backligth_lvl, "Backligth level:"),
scr_reg_Kp(prm_reg_Kp, "Regulator tune:"),
scr_reg_Ki(prm_reg_Ki, "Regulator tune:"),
scr_reg_Kd(prm_reg_Kd, "Regulator tune:"),
scr_pwm_min(prm_pwm_min, "PWM duty tune:"),
scr_pwm_max(prm_pwm_max, "PWM duty tune:");
Так вот. Параметры prm_temp_top, prm_temp_middle, prm_temp_bottom, prm_fan_rpm я вручную не меняю, их значения считываются с датчиков. Соответственно их не надо хранить в EEPROM, знать их максимум и минимум и т.д. А постфикс есть только у scr_reg_Kp, scr_reg_Ki и scr_reg_Kd.
Значит их поля, отвечающий за этот функционал, лишние и просто отжирают память.
Это сподвигло меня заняться оптимизацией кода, что и привело к созданию данной темы.
Код SParametr с костылями
#ifndef _PARAMETR_H_
#define _PARAMETR_H_
#include <Arduino.h>
#include <EEPROM.h>
enum PRM_ID { SIMPLE = 0,
VARIABLE,
POSTFIX };
// базовый класс, выводит на экран параметр со значением value в формате с фиксированной запятой. Тут же описаны все методы, которые есть у потомков, вызываются по ID класса из классов-потомков
struct SParametr : public Printable {
public:
constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit)
: SParametr(value, precision, name, unit, PRM_ID::SIMPLE) {}
// длина строки в UTF-8
static constexpr size_t strlenUtf8(const char *const ptr) {
return !*ptr ? 0 : ((((*ptr & 0xc0) != 0x80) ? 1 : 0) + strlenUtf8(ptr + 1));
}
// возвращает значение статического параметра LINE_WIDTH (максимальная длина строки), используется сторонними объектами
static constexpr uint8_t lineWidth(void) {
return LINE_WIDTH;
}
// возвращает указатель на себя, используется сторонними объектами
SParametr *self(void) {
return this;
}
// возвращает количество символов параметра
size_t length(void) const {
return _name_len + _unit_len + valueLength();
}
// возвращает количество символов имени параметра
constexpr size_t nameLength(void) {
return _name_len;
}
// возвращает количество символов единцы измерения параметра
constexpr size_t unitLength(void) {
return _unit_len;
}
// возвращает количество символов значения параметра
size_t valueLength(void) const {
size_t res = 0;
uint16_t val = abs(_value);
// считаем количество десятичных разрядов
do {
res++;
val /= 10;
} while (val);
// добавляем дробные символы
if (_precision) {
if (res > 2) res++; // просто добавляем символ "."
else res += 2; // добавляем символ "." и 1 разряд
}
// если число отрицательное, то еще знак '-'
if (_value < 0) res++;
return res;
}
// Метод возвращает идентификатор класса
uint8_t id(void) const {
return _id;
}
// Метод для изменения значения параметра
void change(const int8_t value);
// Методы для работы с EEPROM
void toEEPROM(void);
void fromEEPROM(void);
protected:
// Конструктор
constexpr SParametr(int16_t &value, const uint8_t precision, const char *const name, const char *const unit, const uint8_t id)
: _value(value), _precision(precision), _del(intPow(10, precision)), _name(name), _unit(unit), _name_len(strlenUtf8(name)), _unit_len(strlenUtf8(unit)), _id(id) {}
// потоковый вывод переменной
virtual size_t printTo(Print &p) const {
// печатаем имя параметра
p.print(_name); // количество напечатанных символов берем не из print(),
size_t res = _name_len; // а расчитанное функцией strlenUtf8(), поскольку UTF-8 больше 1 байта
// печатем значение параметра
res += printParam(p);
// печатем единицу измерения
p.print(_unit); // количество напечатанных символов берем не из print(),
res += _unit_len; // а расчитанное функцией strlenUtf8(), поскольку UTF-8 больше 1 байта
res += printPostfix(p); // печатаем постфикс, если есть (в наследниках)
return res;
}
// вывод значения параметра
size_t printValue(Print &p) const {
size_t res;
// выводим значение параметра
if (_del) { // если дробное число
int16_t val = _value / (int16_t)_del;
uint8_t fract = abs(_value) % _del;
res = p.print(val);
res += p.print(".");
size_t len = p.print(fract);
// если число разрядов дробной части меньше количества знаков после точки, то забиваем остаток нулями
while (len < (size_t)_precision) len += p.print("0");
res += len;
} else {
res = p.print(_value);
}
return res;
}
// вывод параметра, будет переопределяться в потомках
size_t printParam(Print &p) const;
// вывод постфикса, будет переопределяться в потомках
size_t printPostfix(Print &p) const;
// целочисленное возведение в степень
static constexpr uint16_t intPow(const uint8_t value, const uint8_t exp) {
return !exp ? 1 : value * intPow(value, exp - 1);
}
protected:
int16_t &_value; // ссылка на значение переменной
const uint8_t _precision; // количество разрядов после запятой
const uint8_t _del; // делитель (1 байт, поддерживает максимум 2 знака после запятой), вычисляется на этапе компиляции
const char *const _name; // короткое имя
const char *const _unit; // единица измерения
const size_t _name_len; // длина строки _name, вычисляется на этапе компиляции
const size_t _unit_len; // длина строки _unit, вычисляется на этапе компиляции
const uint8_t _id; // идентификатор класса
static const uint8_t LINE_WIDTH; // максимальная длина строки (экрана) - статический параметр общий для всех экземпляров;
};
// потомок класса SParametr, можно изменять значение value в заданных пределах и сохранять в EEPROM
struct SVariableParametr : public SParametr {
public:
// Конструктор
constexpr SVariableParametr(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
const char *const name, const char *const unit, const bool *is_print_value = nullptr)
: SVariableParametr(eeprom_addr, value, precision, minimum, maximum, name, unit, is_print_value, PRM_ID::VARIABLE) {}
// методы для работы с EEPROM
void toEEPROM(void) {
if (_eeprom_addr) {
checkBounds();
EEPROM.put(_eeprom_addr, _value);
}
}
void fromEEPROM(void) {
if (_eeprom_addr) {
EEPROM.get(_eeprom_addr, _value);
if (!checkBounds()) EEPROM.put(_eeprom_addr, _value);
}
}
// Метод для изменения значения параметра
void change(const int8_t value) { // val может быть как положительным, так и отрицательным
_value += value;
// не проверял, но должно быть быстрее закомментированного варианта
if (value == 10) { // если value равно 10
int16_t fract = _value % 10; // а _value не кратно 10,
if (fract) _value -= fract; // то округляем (вниз) значение _value до десятков
}
// проверяем выход значения параметра за границы диапазона и корректируем при необходимости
checkBounds();
}
protected:
// Конструктор
constexpr SVariableParametr(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
const char *const name, const char *const unit, const bool *is_print_value, const uint8_t id)
: SParametr(value, precision, name, unit, id), _eeprom_addr(eeprom_addr), _minimum(minimum), _maximum(maximum), _is_print_value(is_print_value) {}
// проверка выхода значения параметра за границы диапазона. Возвращает true, если в границах
bool checkBounds(void) const {
int16_t temp = _value;
_value = _value < _minimum ? _minimum : (_value > _maximum ? _maximum : _value);
return temp == _value;
}
// печать параметра
size_t printParam(Print &p) const {
size_t res;
if (_is_print_value == nullptr || *_is_print_value) { // если параметр мигания не привязан или его значение true
// выводим значение параметра
res = printValue(p);
} else {
// забиваем место параметра пробелами
size_t len = valueLength();
res = len;
while (len--) p.print(" ");
}
return res;
}
protected:
const int8_t _eeprom_addr; // адрес параметра в EEPROM
const int16_t _minimum; // минимальное значение переменной
const int16_t _maximum; // максимальное значение переменной
const bool *_is_print_value; // указатель на признак вывода значения переменной на экран при мигании параметра
};
// потомок класса SVariableParametr, добавляет в конце выводимого параметра постфикс, выравнивание по ширине строки (LINE_WIDTH)
struct SVariableParametrPostfix : public SVariableParametr {
public:
// Конструктор
constexpr SVariableParametrPostfix(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
const char *const name, const char *const unit, const char *const postfix, const bool *is_print_value = nullptr)
: SVariableParametrPostfix(eeprom_addr, value, precision, minimum, maximum, name, unit, postfix, is_print_value, PRM_ID::POSTFIX) {}
// печать параметра
size_t printPostfix(Print &p) const {
size_t res = 0;
if (_postfix_len) { // если есть постфикс, то забиваем недостающие места (до конца строки) пробелами и печатам его
res = _postfix_len;
while (res < LINE_WIDTH) res += p.print(" "); // LINE_WIDTH - длина строки у конкретного ЖКИ (1602)
p.print(_postfix);
}
return res;
}
protected:
// Конструктор
constexpr SVariableParametrPostfix(const int8_t eeprom_addr, int16_t &value, const uint8_t precision, const int16_t minimum, const int16_t maximum,
const char *const name, const char *const unit, const char *const postfix, const bool *is_print_value, const uint8_t id)
: SVariableParametr(eeprom_addr, value, precision, minimum, maximum, name, unit, is_print_value, id), _postfix(postfix), _postfix_len(strlenUtf8(postfix)) {}
protected:
const char *const _postfix; // постфикс
const size_t _postfix_len; // длина строеи _postfix, вычисляется на этапе компиляции
};
void SParametr::change(const int8_t value) {
// если тип SVariableParametr (также включает SVariableParametrPostfix)
switch (this->id()) {
case PRM_ID::VARIABLE:
case PRM_ID::POSTFIX:
static_cast<SVariableParametr *>(this)->change(value);
}
}
void SParametr::toEEPROM(void) {
// если тип SVariableParametr (также включает SVariableParametrPostfix)
switch (this->id()) {
case PRM_ID::VARIABLE:
case PRM_ID::POSTFIX:
static_cast<SVariableParametr *>(this)->toEEPROM();
}
}
void SParametr::fromEEPROM(void) {
// если тип SVariableParametr (также включает SVariableParametrPostfix)
switch (this->id()) {
case PRM_ID::VARIABLE:
case PRM_ID::POSTFIX:
static_cast<SVariableParametr *>(this)->fromEEPROM();
}
}
size_t SParametr::printParam(Print &p) const {
switch (this->id()) {
// если тип SVariableParametr (также включает SVariableParametrPostfix)
case PRM_ID::VARIABLE:
case PRM_ID::POSTFIX:
return static_cast<const SVariableParametr *>(this)->printValue(p);
}
// если тип SParametr
return this->printValue(p);
}
size_t SParametr::printPostfix(Print &p) const {
// если тип SVariableParametrPostfix
if (this->id() == PRM_ID::POSTFIX)
static_cast<const SVariableParametrPostfix *>(this)->printPostfix(p);
// иначе ничего не выводим
return 0;
}
#endif // _PARAMETR_H_
Тогда объявление экземпляров SParametr и его наследников в файле *.ino
будет выглядеть так:
SParametr prm_temp_top(temp_current_top, 1, "T1=", "°C"),
prm_temp_middle(temp_current_middle, 1, "T2=", "°C"),
prm_temp_bottom(temp_current_bottom, 1, "T3=", "°C"),
prm_fan_rpm(fan_rmp, 0, "RPM=", "");
SVariableParametr prm_temp_critical(2, temp_critical, 40, 80, 0, "Tc=", "°C", &is_value_visible),
prm_temp_set(4, temp_set, 20, 40, 0, "Ts=", "°C", &is_value_visible),
prm_pwm_duty(6, pwm_duty, 0, 255, 0, "PWM=", "", &is_value_visible),
prm_backligth_lvl(8, backlight_lvl, 0, 255, 0, "Value=", "", &is_value_visible),
prm_pwm_min(16, pwm_duty_min, 0, 255, 0, "Minimum=", "", &is_value_visible),
prm_pwm_max(18, pwm_duty_max, 0, 255, 0, "Maximum=", "", &is_value_visible);
SVariableParametrPostfix prm_reg_Kp(10, reg_Kp, 0, 100, 1, "Kp=", "", "(prop)", &is_value_visible),
prm_reg_Ki(12, reg_Ki, 0, 100, 1, "Ki=", "", "(int)", &is_value_visible),
prm_reg_Kd(14, reg_Kd, 0, 100, 1, "Kd=", "", "(diff)", &is_value_visible);
Все остальное остается без изменений.
Вариант с виртуальными методами не буду приводить. В нем виртуальными были объвлены все методы, которые не нужны в базовом классе SParametr, но есть у его потомков.
Теперь что ксается шаблонов. Вы уже показывали реализацию на шаблонах функции подсчета символов в формате UTF-8. Но я, к своему стыду, так и не удосужился в этих шаблонах разобраться. Буду наверстывать упущенное. А до той поры мне не понятен Ваш пример из предыдущего сообщения.