Динамическая инициализация глобальной константы

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

Доброго дня, уважаемые!

На днях захотелось мне странного. Вопрос вызывает у меня чисто-академический интерес (пока).

Есть класс, описывающий поведение определенного оборудования - модульного резервуара для жидкости. Должен быть создан один глобальный объект класса. Одно из основных свойств - максимальная вместимость, константа из констант. Соответственно, так она в классе и описана. Но существует крайне-редкий случай, когда значение этого свойства должно быть изменено пользователем - увеличение/уменьшение количества присоединенных модулей. 

Вопрос, думаю, теперь понятен: как можно поменять предустановленное значение константы без правки скетча и перепрошивки модуля (ESP32)?

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

Собственно, с самой динамической инициализацией проблем никаких нет. На старте конструктор класса выполняет инициализацию константы расчетным путем при помощи другого метода класса. Но в этой ситуации новое значение уже должно быть доступно классу. А вот где его хранить пре рестарте контроллера я не понимаю. EEPROM не прокатывает, поскольку i2c еще не инициализируется на этот момент. Чтение EEPROM возвращает 0хFF. Если объект класса инициализировать локально (в сетапе), все работает как дОлжно, но этот вариант для меня не вариант. 

Скетч

#include "I2c_FM_EEPROM.h"
i2cEEPROM i2c(0x57);

uint16_t v1{0};   // debug
const uint16_t maxCapacity{2000};

class Tank{
public:
  Tank();
  uint16_t  getCap() {return _maxLevel;}
private:
  const uint16_t _maxLevel;
  uint16_t getMaxCap();
};

Tank::Tank(): _maxLevel{getMaxCap()}{}

uint16_t Tank::getMaxCap(){
  uint16_t def{maxCapacity};               // Дефолтное значение
  uint16_t mem{i2c.read16(100)};    // Попытка чтения альтернативного значения из i2c EEPROM 
  v1 = mem;                         // Отладка: результат чтения 
  if(mem%1000 == 0) return mem;     // Корректное значение должно быть кратно 1000 (размер одного модуля)
  return def;
}

  Tank tank;

void setup() {
//  Tank tank;        // c локальным объектом все работает корректно
  Serial.begin(115200);
  Serial.print("v1: "); Serial.println(v1);                               // Что было прочитано из EEPROM - возвращает 65535
  Serial.print("i2c.read16(100): "); Serial.println(i2c.read16(100));     // Что на самом деле в EEPROM - возвращает 6000
  Serial.print("tank.getCap(): "); Serial.println(tank.getCap());         // Как отработал конструктор  - возвращает 2000
  i2c.write16(100, 6000);                // Запись пользовательского значения в EEPROM
}

void loop(){}

Возврат, дословно:

65535

6000

2000

Инициализация Wire в библиотеке EEPROM:

#pragma once
#include <Wire.h>
// Превышение максимального размера буфера чтения/записи I2C_BUFFER_LENGTH не отслеживается!

class i2cEEPROM {
  public:
    i2cEEPROM(uint8_t addr) {_i2cAddr = addr; Wire.begin(); _selfTest();}
    uint16_t  write(uint16_t addr, void* buf, size_t len);
    uint8_t   write8(uint16_t addr, uint8_t buf);
    uint16_t  write16(uint16_t addr, uint16_t buf);
    uint16_t  write32(uint16_t addr, uint32_t buf);
    uint16_t  read(uint16_t addr, void* buf, size_t len);
    uint8_t   read8(uint16_t addr);
    uint16_t  read16(uint16_t addr);
    uint32_t  read32(uint16_t addr);
    void      printDump();
    uint16_t  maxAddress() {return _maxaddr;}
    void      erase(uint8_t tmpl = 0);
  private:
    void      _selfTest();
    uint8_t   _i2cAddr;
    uint16_t  _maxaddr = 65535;
};

Вижу 2 боковых варианта: при обновлении оборудования шить новую прошивку, забить на константность. Но очень не хочется идти на компромисс.  

Идеи, наставления и оскорбления приветствуются))

 

 

rkit
Offline
Зарегистрирован: 23.11.2016

Убери const и не придумывай себе сложностей на ровном месте.

sadman41
Offline
Зарегистрирован: 19.10.2016

Описать глобальный указатель, сделать на него new после инита eeprom? 

А зачем const?

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

rkit пишет:

Убери const и не придумывай себе сложностей на ровном месте.

В "боевой" версии проекта так и есть. 

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

sadman41 пишет:

Описать глобальный указатель, сделать на него new после инита eeprom? 

А зачем const?

Идею с указателем протестирую, спасибо. Видится const, потому что по физическому смыслу это константа при данном наборе оборудования

и потому, что рукам покоя нет дома на выходных :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Благородный дон, Вы вместо того, чтобы толком объяснить задачу, пытаетесь объяснять уже придуманное решение.

Первый вопрос - сколько вариантов ёмкости вообще возможно? Сколько вариантов "нацеплять модули"? Наверняка ведь 2-3, ну 4 варианта и не больше. Так, на практике. Нет? Ну пусть даже 8. И что, ради этих несчастных 3 вариантов Вы собрались писать фичу ввода новой ёмкости и извращаться? Не проще ли заранее выписать в документации все варианты, например так:

№№ ёмкость (литры)
0 10
1 15
2 20
3 25

На два свободных пина завести вот такой переключатель (или два обычных джампера) и на нём набирать двоичный код варианта. Поменялось что-то - щелкнул тумблерами (поставил как надо джамперы) и нажал на ресет. Ну, а конструктор класса тупо читает пины и выставляет выбранную ёмкость. Для 4 вариантов требуется сдвоенный переключатель, для восьми - строенный (как на картинке) - они разные продаются.

sadman41
Offline
Зарегистрирован: 19.10.2016

Ну, не знаю. Мне тоже кажется, что const надуман тут. Кто переменную изменит извне?  

Можно с сеттерами/геттерами извращаться, но тоже не особо вижу смысла.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Да, дело не в const, а в том, что ради этого предполагается ещё ввод значения от юзера писать. По мне так это совершенно лишнее.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Rumata пишет:

Если объект класса инициализировать локально (в сетапе), все работает как дОлжно, но этот вариант для меня не вариант. 

А, кстати, почему?

С учетом особенностей языка Си++, а также специфики микроконтроллеров вообще и Ардуино в частности - вполне стандартное решение. Более того, нередко вообще единственно возможное.

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

sadman41, для меня вопрос из области саморазвития. Безусловно, я уже перерос тот уровень, когда мог накосячить с произвольным изменением переменных. Идея с new кажется перспективной. Даже если не получится (хотя не вижу, почему не должно), подтяну практику в этой области.

ЕвгенийП, Евгений Петрович, поскольку оно не работает, это не решение :). Просто хотел показать, что уже пытался предпринять. 

Общая емкость хранения от 2 кубометров до 6 с шагом 1 куб. Сейчас 2, к концу мая будет разовое увеличение до 4. К концу лета возможно еще увеличение на 1-2 единицы   То-есть число вариантов очень ограничено, 3-х битного переключателя должно хватить. Как альтернатива EEPROM - вполне. Но при таком раскладе склонюсь к использования неконстантной переменной

 

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

andriano пишет:

С учетом особенностей языка Си++, а также специфики микроконтроллеров вообще и Ардуино в частности - вполне стандартное решение. Более того, нередко вообще единственно возможное.

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

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

Спасибо откликнувшимся! На текущий момент с большим отрывом лидирует идея не заморачиваться константами. Если не взлетит new, так и будет. 

Kakmyc
Offline
Зарегистрирован: 15.01.2018

А идея создавать объектов по максимуму, а пользоваться только теми что инициализировались не появлялась ?

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

Kakmyc пишет:
А идея создавать объектов по максимуму, а пользоваться только теми что инициализировались не появлялась ?

Не понял, как это? Отдельно придется всем "смежникам" объяснять с каким экземпляром нужно работать. Уж больно мудрЕно

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Rumata пишет:

Нужна глобальная область видимости и статическое время жизни.

И каким образом это противоречит стандартному для Ардуино способу инициализации? (когда аппаратнозависимая инициализация статической переменной происходит не в конструкторе, а в методе begin/init, вызываемом в setup)

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

andriano пишет:

И каким образом это противоречит стандартному для Ардуино способу инициализации? (когда аппаратнозависимая инициализация статической переменной происходит не в конструкторе, а в методе begin/init, вызываемом в setup)

Это не статическая, а константа. Я не знал, что их можно инициализировать не в конструкторе.. Это же присвоение получается, а не инициализация? Или я чего недогоняю?

sadman41
Offline
Зарегистрирован: 19.10.2016

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

В данном случае я все ещё с трудом понимаю необходимость const. Каков профит от этой конструкции?

Пример с "отложенной" ициализацией: https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPi... стр.131

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

sadman41 пишет:
Пример с "отложенной" ициализацией: https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPi... стр.131

Если я правильно понял логику происходящего, это сишная вариация на тему #2

Rumata
Rumata аватар
Offline
Зарегистрирован: 29.03.2019

sadman41, спасибо за идею с указателем. Сейчас добрался до макетки - это работает. 

#include "I2c_FM_EEPROM.h"
i2cEEPROM i2c(0x57);

uint16_t v1{0};   // debug
const uint16_t maxCapacity{2000};

class Tank{
public:
  Tank();
  uint16_t  getCap() {return _maxLevel;}
private:
  const uint16_t _maxLevel;
  uint16_t getMaxCap();
};

Tank::Tank(): _maxLevel{getMaxCap()}{}


uint16_t Tank::getMaxCap(){
  uint16_t def{maxCapacity};               // Дефолтное значение
  uint16_t mem{i2c.read16(100)};    // Попытка чтения альтернативного значения из i2c EEPROM 
  v1 = mem;                         // Отладка: результат чтения 
  if(mem%1000 == 0) return mem;     // Корректное значение должно быть кратно 1000 (размер одного модуля)
  return def;
}


  Tank *tank;

void setup() {
  tank = new Tank;   
  Serial.begin(115200);
  Serial.print("v1: "); Serial.println(v1);                               // Что было прочитано из EEPROM 
  Serial.print("i2c.read16(100): "); Serial.println(i2c.read16(100));     // Что на самом деле в EEPROM 
  i2c.write16(100, 6000);                // Запись пользовательского значения в EEPROM
}

void loop(){
    Serial.print("loop::tank->getCap(): "); Serial.println(tank->getCap());         // Как отработал конструктор
    while(1);
  }

Возврат:

v1: 6000
i2c.read16(100): 6000
tank->getCap(): 6000
loop::tank->getCap(): 6000