Сделать Millis или использовать Thread

Начните с рекомендованного вам примера “Блинк визаут делей” - ваше решение там, поверьте

1 лайк
2 лайка

Вы не с того начинаете.

У Вас на самом деле довольна сложная логика. Вам кажется, что Вы её расписали. Но это не так. Вы расписали хотелку. А от хотелки до описания логики - «дистанции огромного размера».

Написать программу по хотелке теоретически возможно, но это требует высочайшей квалификации программиста, коей Вы не располагаете. Вы видели фильм “Формула любви”? Там у заказчика была только хотелка - чувственная идея, которую он хотел реализовать, а Калиостро сказал ему великую фразу: «Материализация чувственных идей суть сложнейшая задача научной магии». Поверьте, в программировании это именно так.

Однако, если Вы потратите какое-то время на формализацию вашей хотелки и превратите её в действительно строгое описание логики программы, то само программирование займёт совсем немного времени и ошибки (кроме описок) очень маловероятны.

У меня сегодня и завтра в принципе есть время и я могу с Вами пройти этот путь, если Вы этого хотите и будете делать, что говорят. Только если мы возьмёмся, Вы не парьтесь, если я буду периодически исчезать, это не значит, что я бросил это дело, просто - всякие дела … сделаю и вернусь.

Хорошо, информацию принял

Если я правильно понял, на форуме нет личных сообщений поэтому всё пишется здесь? Я написал приблизительно, как рыбу, понятно что нормальное ТЗ будет другое и с рельными значениями. Наверное ТЗ накидаю в ворде, могу скинуть куда нужно, либо прямо сюда

Есть. Нажмите на кружочек с ником, кому хотите написать - и справа будет “Сообщение”

1 лайк

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

Накидывают навоз на грядки. Зачем Вам ТЗ? Вам нужна логика Вашей программы и её нужно проработать на совесть. Тогда программирование не займёт ни времени, ни усилий. Я Вас спросил, Вы хотите, чтобы я Вас провёл за ручку по этому проекту и показал как его надо правильно делать, или не хотите. Ответа пока не было, потому я и не дёргаюсь.

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

Странно, товарисчи, выглядит не так уж сложно.
Пните если ошибаюсь то основная конструкция решается

cmdlist = [cmd1, … , cmd15]
for c in cmd list:{
send(c)
read()
}

Дальше правильно приправить таймингами и сравнениями прочитаного значения
но, да вместо делеев везде милис

ошибаетесь.
Почитайте что-нибудь в инете по словосочетаниям “конечный автомат” или “finite state machine”.
Насколько я вижу, главное отличие того что вы написали - отсутствие проверки условий при переходе между состояниями. Когда вы их добавите, вся “простота” у вас исчезнет

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

так чтобы одна посылка ушла потом управление передается на поток опроса, как 100мс прошло обратно в потом посылок

По моему всё как озвучено. Функция read() сама через милис принимает решение когда вышло 100мс и пора вернуться к основной задаче.
она же меняет состояние светодиода при получении “правильной” посылки и выключении его.

Где я нее правильно понял логику или надо расписывать более подробно все функции?

Нужно через “blink w/o delay” делать send(), а в оставшееся время ожидать пакета и сравнивать в нем два байта с другими двумя байтами, да светодиод поджигать.

Я б так делал.

Я собственно то же и предложил или меня не так поняли?

Всё верно будим блок каждые 100мс и ждем между интервалами не ответит ли блок чего.

Ну, раз поступило пожелание попробовать научиться, давайте приступать.

То, что В делаете, называется автоматом. Программировать автомат в лоб на основе описание вроде такого

Пример Вашего описания
  1. Будим блок посылая по адресу 0x12F - 7D 5A 1A EE E1 01 31 06 c интервалом 100мс.
  2. В паузах нужно слушать шину по адресу 0x1E7 и 0x1E8 на предмет появления F1 FF если это возникло, то установить пин допустим 7 для первого и 8 для второго адреса соответственно в высокое состояние на время приема посылки.
  3. Было бы супер еще включить в паузы возможность транслировать один из двух пакетов посылок, управляемых кнопками, т.е. первый пакет адреса 0x678 2мс 0х6F4 2мс 0x678 2мс 0х6F4 (содержимое тоже будет), а второй пакет 0x678 2мс 0х6F4 (содержимое тоже будет). Пакет из 4-х посылок включает тест режим, а пакет из 2-х посылок выключает тест режим.

на самом деле крайне трудно. Всегда что-то путается, события (типа пришедшего пакета) происходят в самый неожиданный момент и т.п. В результате, программа может и заработать, а потом начинаются глюки типа “ой, блин, сигнал пришёл, как раз когда я дребезг кнопки обрабатывал и всё сломалось”. В третьем примере ниже, я покажу совершенно реальный проект, совсем несложный, но тем не менее, я не делал его в лоб - это и для меня трудно.

Давайте делать это не в лоб, а по системе.

Итак, прежде, чем начинать, Вы должны

  1. выписать (прямо физически на бумаге или в файле) все состояния в которых может находиться Ваше изделие. “Находиться в состоянии” означает ничего не делать и ждать внешнего события (например, прихода пакета, нажатия кнопки, истечения временного интервала и т.п.);
  2. прописать какое состояние является стартовым (т.е. при запуске системы в каком она состоянии);
  3. возможно, определить “конечное” состояние - в котором уже ничего не происходит и больше не будет происходить (не всегда, но такие бывают);
  4. выписать все события, которые могут произойти. Все!
  5. составить таблицу в которой какждая строка - состояние системы, а каждый столбец - событие;
  6. на пересечении вписываете что система должна сделать если наступило событие, а она находится в данном состоянии. Обычно это выглядит как “сделать то-то и перейти в новое состояние такое-то”. Действия (“сделать то-то”) должны быть тривиальными и не зависеть ни от какой иной логики, например, перевести пин в HIGH или что-то подобное). Если в каком-то состоянии некоторое событие игнорируется (или невозможно) - оставляем пустую клетку;

Теперь таблицу надо внимательно проверить. Как проверять таблицу? Сначала формально.

  1. в таблице не должно быть пустых столбцов. Если есть пустой столбец, значит это событие не нужно ни для чего;
  2. в таблице не должно быть пустых строк, исключение - строка конечного состояния;
  3. не должно быть недостижимых состояний. Т.е. если в какое-то состояние (кроме стартового) система никогда не переходит - это признак ошибки - это состояние лишнее;
  4. и наоборот, из любого состояния (кроме “конечного”) должен быть выход в какое-то другое.

Далее, надо очень внимательно смотреть на таблицу и проверять логику. Никакой другой логики, кроме отражённой в таблице, быть не должно, если чего-то не хватает, добавляйте состояния и описывайте ВСЮ логику на языке состояний, событий и переходов.

Только после того, как такая таблица составлена и на семь раз проверена можно приступать к программированию. Эта таблица - наше всё. У меня они аккуратно хранятся в папках проектов - это основная документация на случай внесения правок. Я Вам покажу сегодня такую таблицу от реального проекта.

Когда у Вас есть такая таблица, программирование превращается в чисто механическую формальность. Вероятность ошибок (логических, а не опечаток) почти нулевая.

Ну, давайте перейдём к примерам, как это делается.

Пример №1 блинк на 1 Гц

Итак, хотим сделать, чтобы светодиод полсекунды светился, потом полсекунды не светился, затем всё повторяется. Поехали.

Состояния:

А – светодиод не светится (начальное)
B – светодиод светится

События:

Timeout – истёк временной интервал

Предварительная подготовка:

  • Перевести светодиод в OUTPUT;
  • Запустить временной интервал.

Пояснения:

Временной интервал длительностью, указанной при его создании, имеет методы:

  • Запустить (начинается отсчёт времени). Если уже был запущен, время начинает отсчитываться с нуля.
  • Прервать (сброс подсчёта времени)
  • Узнать, не истёк ли. При этом:
    • Если не был запущен, возвращает «нет»
    • Если был запущен, но время не истекло, возвращает «нет»
    • Если был запущен, и время истекло, отключается (становится незапущенным) и возвращает «да»

Для представления интервала будем использовать вот эту простенькую библиотечку:

Библиотека Interval.h
#ifndef	INTERVAL_H
#define	INTERVAL_H

template <uint32_t cTimeSpan>
class Interval {
private:
	uint32_t m_startTime;
	bool m_started;
public:
	Interval(void) : m_startTime(0), m_started(false) {}
	void start(void) { m_started = true; m_startTime = millis(); }
	void abort(void) { m_started = false; }
	bool isElapsed(void) { 
		const bool elapsed = m_started && (millis() - m_startTime >= cTimeSpan);
		if (elapsed) m_started = false;
		return elapsed;
	}
};

#endif	//		INTERVAL_H

Таблица решений:

Состояние Событие
Timeout
A LED_ON, запустить интервал, => B
B LED_OFF, запустить интервал, => A

(лучше называть состояния не A и B, как у меня, а как-то разумно)

Проверяем таблицу, вроде, всё нормально.

Это слишком тривиальный пример, и, возможно, в лоб написать было бы проще, но уже в следующем примере мы увидим, что сложность логики растёт, а сложность программы остаётся на том же уровне - в этом и суть метода. Можно написать довольно сложные системы, а программа буде не шибко сложнее простого блинка.

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

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

static inline void ledOn(void) { digitalWrite(LED_BUILTIN, HIGH); }
static inline void ledOff(void) { digitalWrite(LED_BUILTIN, LOW); }

Начинаем писать основную программу. Вот она:

Программа первого примера
#include "Interval.h"

// Временной интервал 500 мс
static Interval<500> interval;

// Все состояния автомата
enum AutomataState { A, B };

//	Переменная текущего состояния, инициализируется стартовым состоянием
static AutomataState aState = A;

// Функции - исполнители
static inline void ledOn(void) { digitalWrite(LED_BUILTIN, HIGH); }
static inline void ledOff(void) { digitalWrite(LED_BUILTIN, LOW); }

// функции - "произошло ли событие"
static inline bool intervalElapsed(void) { return interval.isElapsed(); }

void setup(void) {
	pinMode(LED_BUILTIN, OUTPUT);
	interval.start();
}

void loop(void) {
	const bool timeoutElapsed = intervalElapsed();
	
	switch (aState) {
		case A:
			if (timeoutElapsed) {
				ledOn();
				interval.start();
				aState = B;
			}
			break;
		case B:
			if (timeoutElapsed) {
				ledOff();
				interval.start();
				aState = A;
			}
			break;
	}
}

Функция в строке №17 - чистое эстетство. Она используется в строке №25. Можно было там написать “внутренность” функции.

В функции setup() записано то, что мы с Вами раньше отметили как “предварительную подготовку”.

Функция же loop() в автоматных программах выглядит всегда одинаково - идёт перебор всех возможных состояний и для каждого состояния проверяются не произошли ли те события, которые для него важны и, если произошли, то выполнятся необходимые действия. Т.е. функция loop() - это просто переписанная таблица. Положите текст таблицы и текст функции рядом и сравните! Это та же таблица, просто записанная на языке программирования. И она такая всегда.

Ну, что, можно запустить и убедиться, что оно работает.

Обратите внимание, что в принципе, мы могли бы обойтись и одним состоянием. Просто по событию (истечению интервала) инвертировать светодиод и оставаться в том же состоянии. Но тогда пример бы выродился.

Теперь, давайте усложним пример.

Пример №2 двухчастотный блинк

Хотим сделать, чтобы светодиод мигал как в прошлом примере, а если нажать кнопку, то пока она (кнопка) удерживается, он мигал гораздо чаще (частота мигания, скажем, 10 Гц). Если же кнопку отпустить - частота снова становится 1 Гц. Если снова нажать, то пока держишь - 10 Гц, ну и т.д.

Мигать будем мигать в одном состоянии. Т.е. всего состояний будет два: “мигаем 1Гц” и “мигаем 10 Гц”.

Состояния:

А – мигает (1 Гц) (начальное)
B – мигает (10 Гц)

События:

Timeout500 – истёк временной интервал 500ms
Timeout50 – истёк временной интервал 50ms
BtnPressed – кнопка нажата
BtnReleased – кнопка отпущена

Подготовка:

  • Перевести светодиод в OUTPUT;
  • Запустить временной интервал 500ms.

Пояснения:

Временной интервал такой же, как был в предыдущем примере.

Кнопка имеет методы:

  • Освежить состояние (считать новое и сделать его текущим).
  • Узнать было ли нажатие (текущее состояние – «нажата», а предыдущее – «отпущена»)
  • Узнать было ли отпускание (текущее состояние – «отпущена», а предыдущее – «нажата»)

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

Будем использовать вот такую маленькую библиотечку для кнопки:

Библиотека Btn.h
#ifndef	BTN_H
#define	BTN_H

template <uint8_t pin>
class Btn {
private:
	bool m_oldState;
	bool m_newState;
	bool stateChanged(void) const { return m_oldState != m_newState; }
public:
	Btn(void) { pinMode(pin, INPUT_PULLUP); m_newState = m_oldState = digitalRead(pin); }
	void refreshState(void) { m_oldState = m_newState; m_newState = digitalRead(pin); }
	bool isPressed(void) const { return stateChanged() && (! m_newState); }
	bool isReleased(void) const { return stateChanged() && m_newState; }
};

#endif	//		BTN_H

В таблице присутствует операция LED_TOGGLE - инвертировать светодиод (т.е. погасить, если светится и зажечь, если не светится). Интервал на 500 мс. там называется инт500, а на 50 мс., соответственно, инт50.

Вот таблица:

Заметьте, когда кнопку не давят, система вечно остаётся в одном и том же состоянии, но при этом светодиод мигает. Нажатие же (и отпускание) кнопки просто переводит систему из одного состояния в другое.

Программа будет понятна, если была понята предыдущая. Всё также. Заметьте, теперь в каждом состоянии нас интересуют два события (см. таблицу), соответственно в функции loop() под каждым состояние два оператора if.

Программа второго примера
#include "Interval.h"
#include "Btn.h"

// Кнопка подключена к этому пину и к Gnd.
static constexpr uint8_t pinButton = A0;

// Временной интервал 500 мс
static Interval<500> int500;
// Временной интервал 50 мс
static Interval<50> int50;

// Кнопка
Btn<pinButton> btn;

// Все состояния автомата
enum AutomataState { A, B };

//	Переменная текущего состояния, инициализируется стартовым состоянием
static AutomataState aState = A;

// Функции - исполнители
static inline void ledToggle(void) { digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); }

// функции - "произошло ли событие"
static inline bool interval500Elapsed(void) { return int500.isElapsed(); }
static inline bool interval50Elapsed(void) { return int50.isElapsed(); }
static inline bool btnPressed(void) { return btn.isPressed(); }
static inline bool btnReleased(void) { return btn.isReleased(); }


void setup(void) {
	pinMode(LED_BUILTIN, OUTPUT);
	int500.start();
}

void loop(void) {
	btn.refreshState();
	const bool timeout500Elapsed = interval500Elapsed();
	const bool timeout50Elapsed = interval50Elapsed();
	const bool pressed = btnPressed();
	const bool released = btnReleased();
	
	switch (aState) {
		case A:
			if (timeout500Elapsed) {
				ledToggle();
				int500.start();
				aState = A; // это можно опустить, она и так А
			}
			if (pressed) {
				int500.abort();
				int50.start();
				aState = B;
			}
			break;
		case B:
			if (timeout50Elapsed) {
				ledToggle();
				int50.start();
				aState = B; // это можно опустить, она и так B
			}
			if (released) {
				int50.abort();
				int500.start();
				aState = A;
			}
			break;
	}
}

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

Теперь

живой пример из моей недавней практики.

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

Сначала хотелка:

Плата радио-реле содержит приёмник, собственно бистабильное реле, а также кнопку и светодиод для “обучения”. При включении питания она переходит в режим ожидания. При этом, если в памяти есть код(ы) брелоков, то просто ждёт сигнала или нажатия кнопки для обучения, а если в памяти пусто, то показывает это подмигиванием (500мс не светится - 5 мс светится и так постоянно). Если нажать кнопку, светодиод начинает светиться и делает это 10 секунд. Если в это время отпустить кнопку, то плата ждёт сигнала брелока и записывает его в память. Если же 10 сек. прошло, а кнопка всё ещё удерживается, светодиод начинает мигать 1 Гц блинком. Если в это время отпустить кнопку, то плата ждёт сигнала брелока, чтобы стереть такой сигнал из памяти, если он там есть. Если же прошло ещё 10 сек, а кнопку всё ещё держать, то плата начинает мигать 10 Гц блинком. Если отпустить кнопку в этот момент, то плата ждёт сигнала брелока, чтобы стереть всю память и очиститься. В любом из случаев, когда “плата ждёт сигнала брелока”, она делает это 30 секунд. Если за это время код пришёл и действие выполнено, то (в случае добавления или удаления), она снова ждёт 30 сек., может юзер пришлёт ещё код для добавления/удаления. Наконец, если сигнал не поступил в течение 30 сек, она переходит в режим ожидания (в котором была до нажатия кнопки).

Ну, вот как-то так, если я ничего не забыл.

Обратите внимание, логика не то, чтобы сложна, но … довольно много всего и писать программу в лоб было бы не так, чтобы просто. Пошёл по пути описанному здесь. Получилась таблица из семи событий (столбцов) и восьми состояний (строк) (полюбуйтесь на неё, там есть и описания состояний на второй странице, и короткие комментарии). Этот файл сохранён в проекте в качестве документации. И, после этого, программирование стало предельно простым.

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

Основная программа радио-реле
void runAutomata(void) {
	const uint32_t rSignal = getRadioSignal();
	const bool pressed = isButtonPressed();
	const bool released = isButtonReleased();
	const bool s5elapsed = buttonWait.elapsed();
	const bool s30elapsed = codeWait.elapsed();
	const bool ms50elapsed = blinkShort.elapsed();
	const bool ms500elapsed = blinkLong.elapsed();

	switch (state) {
		case NO_CODES:
			if (pressed) {
				buttonWait.start();
			}
			if (released) {
				buttonWait.stop();
			}
			if (s5elapsed) {
				blinkShort.stop();
				blinkLong.stop();
				ledOn();
				buttonWait.start();
				setState(SECOND_PHASE);
				break;
			}
			if (ms500elapsed) {
				blinkShort.start();
				ledOn();
			}
			if (ms50elapsed) {
				blinkLong.start();
				ledOff();
			}
		break;
		case CODES:
			if (rSignal != 0)  {
				relaySwitch(rSignal);
			}
			if (pressed) {
				buttonWait.start();
			}
			if (released) {
				buttonWait.stop();
			}
			if (s5elapsed) {
				ledOn();
				buttonWait.start();
				setState(SECOND_PHASE);
				break;
			}
			break;
		case SECOND_PHASE:
			if (released) {
				buttonWait.stop();
				codeWait.start();
				setState(WAIT_IN);
				break;
			}
			if (s5elapsed) {
				ledOff();
				buttonWait.start();
				blinkLong.start();
				setState(THIRD_PHASE);
				break;
			}
		break;
		case THIRD_PHASE:
			if (released) {
				buttonWait.stop();
				codeWait.start();
				setState(WAIT_OUT);
				break;
			}
			if (s5elapsed) {
				ledOff();
				buttonWait.start();
				blinkShort.start();
				setState(FOURTH_PHASE);
				break;
			}
			if (ms500elapsed) {
				blinkLong.start();
				ledToggle();
			}
		break;
		case FOURTH_PHASE:
			if (released) {
				buttonWait.stop();
				codeWait.start();
				setState(WAIT_WIPE);
				break;
			}
			if (ms50elapsed) {
				blinkShort.start();
				ledToggle();
			}
		break;
		case WAIT_IN:
			if (rSignal != 0)  {
				ledOff();
				addCode(rSignal);
				codeWait.start();
				ledOn();
			}
			if (s30elapsed) {
				ledOff();
				setStartState();
				break;
			}
		break;
		case WAIT_OUT:
			if (rSignal != 0)  {
				ledOff();
				removeCode(rSignal);
				codeWait.start();
			}
			if (ms500elapsed) {
				blinkLong.start();
				ledToggle();
			}
			if (s30elapsed) {
				ledOff();
				blinkLong.stop();
				setStartState();
				break;
			}
		break;
		case WAIT_WIPE:
			if (rSignal != 0)  {
				ledOff();
				codeWait.start();
				blinkShort.stop();
				wipeCodes(rSignal);
				setStartState();
				break;
			}
			if (ms50elapsed) {
				blinkShort.start();
				ledToggle();
			}
			if (s30elapsed) {
				ledOff();
				blinkShort.stop();
				setStartState();
				break;
			}
		break;
	}
}

Ну, и напоследок. Если Вы во всём этом разобрались, то Вам нетрудно будет сделать вот такое упражнение:

Имеем светодиод и кнопку.

  1. При включении ничего не светится - ждёт нажатия кнопки.
  2. Если нажать кнопку, начинает мигать с частотой 1 Гц и мигает пока кнопка удерживается.
  3. После отпускания кнопки выжидает 5 секунд.
  4. Если в течение этих пяти секунд снова нажать кнопку, начнёт мигать с частотой 10 Гц и тоже будет мигать пока удерживается кнопка.
  5. Если же пять секунд прошло и ничего не нажато, всё забываем и переходим к п. 1.
  6. Если кнопка была нажата (и мигала на 10 Гц), то после отпускания, опять же всё забываем и переходим к п.1.

Можете использовать библиотеки интервала и кнопки из моих примеров, больше Вам тут, вроде, ничего не нужно.

Когда Вы это сделаете и мы обсудим Вашу таблицу и программу, тогда перейдём к Вашей задаче. Впрочем, тогда, возможно, Вам моя помощь уже не будет нужна. Давайте, делайте.

2 лайка

Позволю себе терминологическое уточнение. В CAN-bus есть понятие ID, но это не обязательно адрес. Туда суют всё, что плохо лежит - тип пакета, например.

Т.е. из CAN-bus приходит пакет с неким ID и payload. Затем ID и анализируется. “Слушать шину по адресу” - это несколько коряво…
Точно так же в CAN нет “посылки по адресу”. Есть посылка фрейма с неким ID и payload.

1 лайк

Первоначальная табличка, но она мне не очень нравится, принимаю конструктивную критику. Блоки реакций на нажатие кнопок написаны неверно в части содержимого кан посылки, но это исправлю.

Сейчас нет возможности смотреть подробно, но вот сразу “loop в течение 100мс” - это расписывать надо через событие (истечение 100 мс) и что за loop тоже не очень понятно…

Кроме того, Вас просили упражнение сделать. Если бы сделали, Вы бы сейчас такого не написали.

Вобщем-то и на приём написаны так себе. Нет “чтения ID из шины”. Получаете всё, что летит оттуда, если фильтры не поставили. Затем сравниваете - с нужным ID прилетел пакет или нет. Если нет, то ничего не делаете, в обратном случае анализируете payload. ID 0x1E7 никогда не будет F1 FF. Будет ID 0x1E7 и payload F1 FF (например).
Вы и сами путаетесь и тот, кто помочь может, ловит когнитивный диссонанс и бросает разбираться в вашей проблеме.