TimerList от DetSimen - чего-то не догоняю((

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Пробема, наверное, пустяковая, но моих скромных познаний в C++ недостаточно для понимания.
В общем, суть в следующем. Делаю неспешно себе часы. В общих чертах они работают уже пару месяцев, но до завершения еще далеко. Попалась мне библиотека от DetSimen для работы с таймерами. А т.к. у меня в коде есть несколько (а будет еще больше) интервалов, которые я отмеряю миллисом, то решил немного упростить себе жизнь, а заодно и код чуть красивше сделать. У меня почти получилось, спасибо тебе, Дед, дай бог тебе и твоему коту здоровья и сытости )))
Но возникло одно но, которого я не понимаю

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020
Получилось у меня пять таймеров, и почти все они работают нормально, правильно и с поставленными задачами справляются полностью.
1) Первый таймер - работает постоянно с интервалом 500 мс, обеспечивает "блинк", т.е. мигание двоеточия в режиме показа времени или настраиваемого значения в режимах настроек
void setup()
{
  hBlinkTimer = TimerList.Add(500, tmrBlink);
}

void tmrBlink(void)
{
  blink_flag = !blink_flag;
}

Работает без нареканий

2) Второй таймер - работает постоянно с интервалом 1000 мс, опрашивает датчик температуры DS18b20, каждое нечетное срабатывание - дает команду на конвертацию, четное срабатывание - считывает температуру и кладет ее в переменную для использования ее там, где это понадобится
void setup()
{
  hTemperature = TimerList.Add(1000, tmrTemperature);
}

void tmrTemperature(void)
{
  static bool flagDall = true; // Признак операции
  if (flagDall)
  {
    ds.reset();
    ds.write(0xCC); // Обращение ко всем датчикам
    ds.write(0x44); // Команда на конвертацию
  }
  else
  {
    int temp;
    ds.reset();
    ds.select(addr);
    ds.write(0xBE);                      // Считывание значения с датчика
    temp = (ds.read() | ds.read() << 8); // Принимаем два байта температуры
    temperature = round((float)temp / 16.0);
  }
  flagDall = !flagDall; // Инверсия признака операции для следующей итерации
}

Работает без нареканий. Знаю, опрашивать можно и реже, но как уж есть ))

3) Третий таймер - включается в конце каждой минуты с интервалом 1000 мс, после шести срабатываний останавливается. Он обеспечивает изменение данных на экране - в конце каждой минуты у меня выводятся дата, день недели и текущая температура.
void setup()
{
  hAutoShow = TimerList.Add(1000, tmrAutoDate, true);
}

void tmrAutoDate(void)
{
  auto_show_timeout++;
  if (auto_show_timeout > 5)
  {
    auto_show_timeout = 0;
    TimerList.Stop(hAutoShow);
  }
  resetPrintData(true);
}

Работает без нареканий.

4) Четвертый таймер - включается только "по нужде", его задача - выйти из текущего режима (показ даты или настройки времени/даты) в режим показа текущего времени. Запускается при входе в соответствующий режим, перезапускается при нажатии на любую кнопку,  интервал зависит от режима - 5000 или 10000 мс.
void setup()
{
  hDisplayTimer = TimerList.Add(10000, tmrDisplay, true);
}

void tmrDisplay(void)
{
  display_timeout = false;
  TimerList.Stop(hDisplayTimer);
}

Работает без нареканий.

5) Пятый таймер - работает постоянно с интервалом 200 мс, его задача - устанавливать яркость матрицы в зависимости от текущего освещения
void setup()
{
  hMatrixIntensity = TimerList.Add(200, tmrMatrixIntensity);
}
void tmrMatrixIntensity(void)
{
  setMatrixIntensity();
}

void setMatrixIntensity()
{
  static byte x = 0;
  x = byte((map(analogRead(PHOTORESISTOR_PIN), 0, 1023, 1, 15) * 2 + x) / 3); 
  matrix.setIntensity(x);
}

И вот с ним у меня абсолютное непонимание. Часы зависают в интервале от 20 минут до 2 часов работы после включения. Причем, сама процедура установки яркости старая, раньше работала без нареканий. Если я делаю вот так

void tmrMatrixIntensity(void)
{
  // setMatrixIntensity();
}

void setMatrixIntensity()
{
  static byte x = 0;
  static unsigned long t = 0;
  if (millis() - t >= 200)
  {
    t = millis();
    x = byte((map(analogRead(PHOTORESISTOR_PIN), 0, 1023, 1, 15) * 2 + x) / 3); 
    matrix.setIntensity(x);
  }
}

void loop()
{
  setMatrixIntensity()
.....
}

То часы опять таки работают без зависаний вне зависимости, работает ли пятый таймер с пустой процедурой или я его вообще отключаю. Т.е. проблема возникает только в случае, если я вызываю процедуру setMatrixIntensity() в таймере.

ЗЫ: откатился полностью до "дотаймерового" состояния и добавил один единственный таймер с регулировкой яркости. Часы зависают буквально через несколько минут.
 
Может быть я просто по незнанию чего-то не учитываю?
 
Помогай, Дед ))
asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

Попробуй разрешить прерывания перед setMatrixIntensity() в tmrMatrixIntensity(void)

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

asam пишет:

Попробуй разрешить прерывания перед setMatrixIntensity() в tmrMatrixIntensity(void)

Так?

void tmrMatrixIntensity(void)
{
  interrupts();
  setMatrixIntensity();
}

А что это даст?

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Шли код на dap68@mail.ru.

Макс число таймеров, которые я использовал, было 8, косяков замечено не было, надо в твоём коде разбираца. 

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

Нет, но я посмотрел код TimerList и прерывания там и так разрешаются перед вызовом фунции из TimerLisr.

Дело в том что эти функции вызываеются в контексте прерывания таймера и, видимо, setMatrixIntensity() не очень хорошо себя чувствует. Вызывай ее из loop() и забей.

 

PS Кстати, а сколько у тебя свободной памяти остается (dynamic)?

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

сначала, пропробуй в 127 строчке TimerList.h 

//			sei();				// разрешаем прерывания

убрать комментарий

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

там, да, мой косяк, Callback вызывается с запрещенными прерываниями, зачем-то. 

TCounterDown &operator --(int) {

		uint8_t sreg = SREG;   // на всякий случай запомним состояние прерываний

		cli();				// запрещаем прерывания

		if ((isActive()) && (!isEmpty()) && ((--fWorkCount) == 0)) {  // если счетчик досчитал до 0
			fWorkCount = fInitCount; // перезагружаем рабочий счётчик начальным значением, чтобы считать заново
//			sei();				// разрешаем прерывания
			fCallback();		// и вызываем нашу фунцию обратного вызова
		}
		
		SREG = sreg;     // восстанавливаем прерывания
		return *this;
	}

Пошто я так сделал, счас не упомню. Правильней - прерывания разрешать перед вызовом

 

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

DetSimen пишет:

сначала, пропробуй в 127 строчке TimerList.h 

//			sei();				// разрешаем прерывания

убрать комментарий


 

Ох, блин, не заметил что она закоментирована. Ну, тогда разрешение прервываний, скорее всего, поможет.

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

DetSimen пишет:

сначала, пропробуй в 127 строчке TimerList.h 

//			sei();				// разрешаем прерывания

убрать комментарий

ОК, подправил, загрузил, через пару-тройку часов будет видно))

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

ну, если setMatrixIntensity() память не распахивает, то, наерна, поможет. 

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

я тоже на git-е поправил :)  Спасибо. 

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

asam пишет:

Нет, но я посмотрел код TimerList и прерывания там и так разрешаются перед вызовом фунции из TimerLisr.

Дело в том что эти функции вызываеются в контексте прерывания таймера и, видимо, setMatrixIntensity() не очень хорошо себя чувствует. Вызывай ее из loop() и забей.

 

PS Кстати, а сколько у тебя свободной памяти остается (dynamic)?

Наверное, я делаю неправильно, но у меня действо крутится не только в лупе. Все дополнительные экраны - вывод даты, температуры, настройки даты и времени - у меня делаются в отдельных циклах, соответственно, вызов setMatrixIntensity() (и опрос датчика температуры до кучи) нужно пихать в каждый из них. А таймеры это дело решают малой кровью ))

Скетч использует 19004 байт (61%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 669 байт (32%) динамической памяти, оставляя 1379 байт для локальных переменных. Максимум: 2048 байт.

 

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

DetSimen пишет:

я тоже на git-е поправил :)  Спасибо. 

Пожалуйста. 

Кстати, cli()  в TCounterDown &operator --(int) особого смысла не имеет. Прерывания и так запрещены в данном месте.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

у тя, кста, если ты матрицу использоваешь, должно памяти мало оставаца.  Таймеры, оне, с.ка, динамическую память жруть, не менее 110  байт для Atmega328, посмотри, чтоб точно хватало. 

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

asam пишет:

Кстати, cli()  в TCounterDown &operator --(int) особого смысла не имеет. Прерывани и так запрещены в данном месте.

Понимаешь ли.  Как написано в описании, TCounterDown это отдельный класс, который может вызываться не только в контексте прерывания от таймера, а и из твоей программы тоже, мануально, в качестве счётчика каких-нить событий.  Поэтому, щёччик должен уменьшаться атоммарно, потому и cli();

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

DetSimen пишет:

у тя, кста, если ты матрицу использоваешь, должно памяти мало оставаца.  Таймеры, оне, с.ка, динамическую память жруть, не менее 110  байт для Atmega328, посмотри, чтоб точно хватало. 

Ага, учту. 

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

DetSimen пишет:

я тоже на git-е поправил :)  Спасибо. 

можно обновить с гита и попробовать мой маячок?

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

маячок тоже обновил, бери, пробовай

https://github.com/DetSimen/MorzeRepeater

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Я примерно до 17 NSK (13 MSK) буду занят, на вопросы потом отвечу, по мере поступления. 

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Не, все равно зависло((

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

могу посоветовать только в Callback-е от таймера просто выставлять флаг "запрос обновления матрицы", а вычислять и устанавливать интенсивность уже в loop() по взведенному флагу.  Похоже, сётаки, что где-то портица память. 

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

ОК. Есть еще мысль убрать map() и заменить ее прямым вычислением. Будем пробовать ))

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Скока у тебе уровней яркости у матрицы? Если 16, как я здесь разглядел

map(analogRead(PHOTORESISTOR_PIN), 0, 1023, 1, 15)

то это лехко переделывается в 

Value = (analogRead(PHOTORESISTOR_PIN) >> 6);

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Ага, сделал так:

x = byte(((analogRead(PHOTORESISTOR_PIN) / 64) * 2 + x) / 3);

И два часа работало. А вот только что глянул - уже висит. Рано радовался ))

Выходит, что analogRead() пакостит

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

v258 пишет:

Выходит, что analogRead() пакостит

Нет.

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

v258 пишет:

И два часа работало. А вот только что глянул - уже висит. Рано радовался ))

Выходит, что analogRead() пакостит

analogRead() прост как 3 копейки и пакостить не может. Привел бы полный текст твоего творения, может  и посоветовали бы чего. 

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

asam пишет:

Привел бы полный текст твоего творения, может  и посоветовали бы чего. 

Там больше тысячи строк, плюс еще два файла, взять можно здесь

Провел некоторый эксперимент. Строка 85

x = byte(((analogRead(PHOTORESISTOR_PIN) >> 6) * 2 + x) / 3);

В таком виде отработало три часа и не зависло. Решил соптимизировать - т.к. (х / 64) * 2 ((x>>6) * 2) эквивалентно x / 32 (x>>5), то заменил такой строкой

x = byte(((analogRead(PHOTORESISTOR_PIN) >> 5) + x) / 3);

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

while (auto_show_timeout < 5)

где auto_show_timeout - переменная, которая меняется в другом таймере. Просто останавливается после auto_show_timeout == 1, на экране отображается текущий год - и все, никаких реакций. Но при этом яркость экрана регулируется, палец к фоторезистору - экран притухает, палец убираешь - яркость увеличивается. Получается, этот таймер таки работает, но останавливается (останавливается ли?) другой. Который к нему каким боком?

Да и остановкой таймера автопоказа даты тоже ничего не объяснить, т.к. год отображается не нормально, а слегка потрепанными цифрами, как будто данные попытались измениться, но на полпути все замерло. Т.е. останавливается именно цикл while.

Грешным делом подумал, что я ж, идиот, не объявил auto_show_timeout как volatile, полез посмотреть - действительно, не объявил. Но объявление 

volatile byte auto_show_timeout = 0;

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

Сейчас опять сделал строку 85 

x = byte(((analogRead(PHOTORESISTOR_PIN) >> 6) * 2 + x) / 3);

Пока работает.

Как-то запутанно все - стреляет вроде в лоб, а попадает в задницу ))

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

Ох как все запутано...

Подобавляй в разные места отладочный вывод (Serial.print()) всяких переменных включая аuto_show_timeout причем с указанием функциии откуда печатается . Только скорость поставь 115200 

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

А зачем ты DS18b20 используешь, если в RTC датчик температуры есть?

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Ставил. Именно на аuto_show_timeout и ставил. В самом начале, когда грешил именно на этот таймер. И за два часа ни разу не зависло)) Как только сериал убрал - тут же снова начало зависать. Тогда начал откатываться назад и проблемы ушли после отключения таймера настройки яркости. А потом вообще откатывался назад, когда ни одного таймера не было, добавлял только один - с настройкой яркости, и все начинало зависать. И даже если этот таймер оставить включенным, но убрать из него вызов setMatrixIntensity() - все опять же волшебным образом лечится. Т.е. проблема таки в ней ))

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

asam пишет:

А зачем ты DS18b20 используешь, если в RTC датчик температуры есть?

Фигня там, а не датчик. Точность +/- несколько градусов, причем, погрешность плавающая. Я даже коррекцию сделал - но она для разных температур нужна разная.

Ну и все это находится в корпусе, т.е. температуру он выдает внутрикорпусную, а не комнатную ))

А DS18b20 я на заднюю стенку вывел - и порядок

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

Похоже на проблемы с памятью. Я бы выкинул

#include <Adafruit_GFX.h>
#include <gfxfont.h>
#include <Max72xxPanel.h>
 
И использовал MD_MAX72xx.h . Она намного меньше
v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Это придется практически с нуля писать

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

Я с кодом особо не разбирался, но, насколько я увидел, ты ничего особенного с дисплеем не делаешь. Выводишь текст/цифры (в одном размере?) плюс пара спец символов. Все это легко переписывается. 

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Часы/минуты выводятся своим шрифтом 6х8 с возможностью вывода полужирным начертанием.

Плюс к тому стандартная проблема этих матриц - вывод знаков на матрицу боком. Здесь есть встроенная возможность повернуть как надо, в других библиотеках эту проблему придется решать самому

На самом деле с памятью проблем пока нету - работает же при вызове регулировки яркости из лупа, проблема только в связке с таймером. Да и то последний вариант вроде работает нормально. К утру скажу точно ))

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Я бы тебе посоветовал поменять архитектуру своего приложения. :-)

Например, я, своими таймерами в их изначальном виде уже и не пользуюсь.  Сначала я отказался от Callback функции, заменив её лямбдой прям в обьявлении. 

THandle hColon		= TimerList.Add(500, []() {SendMessage(msg_TimerEnd, hColon); }, TIMER_STOPPED);
THandle h10Sec		= TimerList.AddSeconds(10, []() {SendMessage(msg_TimerEnd, h10Sec); }, TIMER_STOPPED);
THandle hNormLed	= TimerList.Add(100, []() {SendMessage(msg_TimerEnd, hNormLed); }, TIMER_STOPPED);

потом посмотрел, что создание таймеров однотипно, да совсем убрал FCallback из структуры, список теперь сам шлёт сообщения в очередь, если какой-то из таймеров закончился.  Во-первых, я тогда не исполняю код в контексте прерывания, во вторых, приложение, которое не ждёт событий, а управляется ими, более устойчиво КМК, ну а в третьих - иканомица память, что на AVR более чем актуально. Теперь у мня структура таймера выглядит так:

	uint32_t	fWorkCount;  // 4 bytes	Рабочий счетчик, уменьшается на 1 каждый Tick()
	uint32_t	fInitCount : 31; // 4 bytes  Начальный счетчик, хранит значение от которого считать до 0
	bool		fActive : 1;

Максимальная выдершка, канеш, сократилась до 24,5 суток, но я на таких интервалах не работаю, а сыканомить удалось целый байт на таймер. :) 

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

а обрабатывается это вот всё уже в контексте задачи, в функции Dispatch().

bool	Dispatch(const TMessage &AMsg) {
	bool result = true;
	switch (AMsg.Message)
	{

	case msg_TimerEnd:

		if (AMsg.LoParam == h10Sec) { // меняем на дисплее время/температуру
			if (DisplayState == enDisplayState::Time)
				SetAppState(enDisplayState::Temp);
			else
				SetAppState(enDisplayState::Time);
                 
                 Display();
		}

		if (AMsg.LoParam == hColon) Disp.ToggleColon(); // мигаем двоеточием

		if (AMsg.LoParam == hNormLed) { // Alive светодиод
			ledNorm.Toggle();
			TimerList.SetNewInterval(hNormLed, ledNorm.isOn() ? 100 : 4900);
		}

		break;


	default:
         break;
	}

 

b707
Offline
Зарегистрирован: 26.05.2017

v258 пишет:

Плюс к тому стандартная проблема этих матриц - вывод знаков на матрицу боком. Здесь есть встроенная возможность повернуть как надо, в других библиотеках эту проблему придется решать самому

этот поворот пишется за 10 минут, проверено лично

asam
asam аватар
Offline
Зарегистрирован: 12.12.2018

b707 пишет:

v258 пишет:

Плюс к тому стандартная проблема этих матриц - вывод знаков на матрицу боком. Здесь есть встроенная возможность повернуть как надо, в других библиотеках эту проблему придется решать самому

этот поворот пишется за 10 минут, проверено лично

MD_MAX72xx.h умеет работать с разными вариантами матриц и выводит в нужном направлении,  так что и поворота писать не надо. Правда  там один шрифт, но кто мешает нужные цифры/буквы самому забитмапить.

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Не, не, лень-матушка же ))) Тем более, что необходимость под вопросом. Сейчас убрал вызов setMatrixIntensity() из таймера, и все работает. Таймер теперь просто поднимает флаг, а в setMatrixIntensity() он проверяется и, ежели поднят, считывается датчик и выставляется яркость. Вызываю из лупа. И все работает без сбоев, т.е. проблем с памятью таки нету.

Проблему вызывала именно работа из таймера. Думаю, что таки беда с контроллером матрицы, зависало обычно в одом и том же месте, где кроме отрисовки ничего и не было. Может что в библиотеках, но, видимо, сбоит обмен с контроллером матрицы в условиях манипуляции прерываниями. Однако, переписывать, пока во всяком случае, не буду))

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

v258 пишет:

Таймер теперь просто поднимает флаг, а в setMatrixIntensity() он проверяется и, ежели поднят, считывается датчик и выставляется яркость. 

 И это праильна. 

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

DetSimen пишет:

v258 пишет:

Таймер теперь просто поднимает флаг, а в setMatrixIntensity() он проверяется и, ежели поднят, считывается датчик и выставляется яркость. 

 И это праильна. 

Иеххх, зато теперь яркость регулируется только в основном режиме, в циклах вывода даты или настройках для регулировки тоже вызов надо ставить. Как раньше было. Я-то думал с таймером про это дело ваще забыть )))

b707
Offline
Зарегистрирован: 26.05.2017

А зачем ее постоянно регулировать? Запустил регулировку раз в минуту или там раз в час, запомнил полученное число - и используешь для времени, даты и всего что нужно

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Так часы же в комнате стоят. Ладно там в течение дня яркость почти не меняется, но ночью, после выключения света, они должны таки сразу притухнуть. Иначе с регулировкой вообще заморачиваться не стоит ))

b707
Offline
Зарегистрирован: 26.05.2017

v258 пишет:

Так часы же в комнате стоят. Ладно там в течение дня яркость почти не меняется, но ночью, после выключения света, они должны таки сразу притухнуть. Иначе с регулировкой вообще заморачиваться не стоит ))

так я все равно не понял, в чем проблема. остальные данные. кроме времени - показываются 6 секунд в минуту. Ну эти 6 секунд можно яркость снова и не регулировать. оставить ту. что была 6 сек назад :)

А вообще. если регулировка яркости в лупе - она по идее должна вызываться раз 100 в секунду, как минимум... у вас луп с какой скоростью оборачивается7

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Что-то мешает. Через разные промежутки времени, но не более, чем через два часа, все зависает. Только яркость регулируется - таймер продолжает работать. А часы висят. И только при вызове процедуры регулировки из таймера. Остальные четыре таймера проблем не доставляют, а с этим вот такая фигня ))

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

b707 пишет:

А вообще. если регулировка яркости в лупе - она по идее должна вызываться раз 100 в секунду, как минимум... у вас луп с какой скоростью оборачивается7

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

void setMatrixIntensity()
{
  if (intensity_flag)
  {
    static byte x = 0;
    x = byte(((analogRead(PHOTORESISTOR_PIN) >> 6) * 2 + x) / 3);
    constrain(x, 1, 15);
    matrix.setIntensity(x);
    intensity_flag = false;
  }
}

 

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Если хочешь, я опишу архитектуру своих приложений, управляемых событиями, на примере какогонить заоконного термометра с часами. Моя электропочта elf-basic@yandex.ru

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

Давай, пригодится ))