Начните с рекомендованного вам примера “Блинк визаут делей” - ваше решение там, поверьте
Вы не с того начинаете.
У Вас на самом деле довольна сложная логика. Вам кажется, что Вы её расписали. Но это не так. Вы расписали хотелку. А от хотелки до описания логики - «дистанции огромного размера».
Написать программу по хотелке теоретически возможно, но это требует высочайшей квалификации программиста, коей Вы не располагаете. Вы видели фильм “Формула любви”? Там у заказчика была только хотелка - чувственная идея, которую он хотел реализовать, а Калиостро сказал ему великую фразу: «Материализация чувственных идей суть сложнейшая задача научной магии». Поверьте, в программировании это именно так.
Однако, если Вы потратите какое-то время на формализацию вашей хотелки и превратите её в действительно строгое описание логики программы, то само программирование займёт совсем немного времени и ошибки (кроме описок) очень маловероятны.
У меня сегодня и завтра в принципе есть время и я могу с Вами пройти этот путь, если Вы этого хотите и будете делать, что говорят. Только если мы возьмёмся, Вы не парьтесь, если я буду периодически исчезать, это не значит, что я бросил это дело, просто - всякие дела … сделаю и вернусь.
Хорошо, информацию принял
Если я правильно понял, на форуме нет личных сообщений поэтому всё пишется здесь? Я написал приблизительно, как рыбу, понятно что нормальное ТЗ будет другое и с рельными значениями. Наверное ТЗ накидаю в ворде, могу скинуть куда нужно, либо прямо сюда
нет у меня такого))) не дорос видимо ещё. Сколько записей в теме показывает, а синего окошка сообщение нет
Накидывают навоз на грядки. Зачем Вам ТЗ? Вам нужна логика Вашей программы и её нужно проработать на совесть. Тогда программирование не займёт ни времени, ни усилий. Я Вас спросил, Вы хотите, чтобы я Вас провёл за ручку по этому проекту и показал как его надо правильно делать, или не хотите. Ответа пока не было, потому я и не дёргаюсь.
Пусть будет логика, я её пишу, просто там в кан нужно конкретные данные отправлять, их тоже сразу укажу. Спасибо за отклик помощи
Странно, товарисчи, выглядит не так уж сложно.
Пните если ошибаюсь то основная конструкция решается
cmdlist = [cmd1, … , cmd15]
for c in cmd list:{
send(c)
read()
}
Дальше правильно приправить таймингами и сравнениями прочитаного значения
но, да вместо делеев везде милис
ошибаетесь.
Почитайте что-нибудь в инете по словосочетаниям “конечный автомат” или “finite state machine”.
Насколько я вижу, главное отличие того что вы написали - отсутствие проверки условий при переходе между состояниями. Когда вы их добавите, вся “простота” у вас исчезнет
Не сочтите за спор, а сугубо обмен мнениями и взглядами.
Из озвученного Первостепенное отправлять, Второстепенное - читать.
Предложенный мной алгоритм вполне способен гарантировать отправку, а в промежутках выделить время на чтение и проверки.
так чтобы одна посылка ушла потом управление передается на поток опроса, как 100мс прошло обратно в потом посылок
По моему всё как озвучено. Функция read() сама через милис принимает решение когда вышло 100мс и пора вернуться к основной задаче.
она же меняет состояние светодиода при получении “правильной” посылки и выключении его.
Где я нее правильно понял логику или надо расписывать более подробно все функции?
Нужно через “blink w/o delay” делать send(), а в оставшееся время ожидать пакета и сравнивать в нем два байта с другими двумя байтами, да светодиод поджигать.
Я б так делал.
Я собственно то же и предложил или меня не так поняли?
Всё верно будим блок каждые 100мс и ждем между интервалами не ответит ли блок чего.
Ну, раз поступило пожелание попробовать научиться, давайте приступать.
То, что В делаете, называется автоматом. Программировать автомат в лоб на основе описание вроде такого
Пример Вашего описания
- Будим блок посылая по адресу 0x12F - 7D 5A 1A EE E1 01 31 06 c интервалом 100мс.
- В паузах нужно слушать шину по адресу 0x1E7 и 0x1E8 на предмет появления F1 FF если это возникло, то установить пин допустим 7 для первого и 8 для второго адреса соответственно в высокое состояние на время приема посылки.
- Было бы супер еще включить в паузы возможность транслировать один из двух пакетов посылок, управляемых кнопками, т.е. первый пакет адреса 0x678 2мс 0х6F4 2мс 0x678 2мс 0х6F4 (содержимое тоже будет), а второй пакет 0x678 2мс 0х6F4 (содержимое тоже будет). Пакет из 4-х посылок включает тест режим, а пакет из 2-х посылок выключает тест режим.
на самом деле крайне трудно. Всегда что-то путается, события (типа пришедшего пакета) происходят в самый неожиданный момент и т.п. В результате, программа может и заработать, а потом начинаются глюки типа “ой, блин, сигнал пришёл, как раз когда я дребезг кнопки обрабатывал и всё сломалось”. В третьем примере ниже, я покажу совершенно реальный проект, совсем несложный, но тем не менее, я не делал его в лоб - это и для меня трудно.
Давайте делать это не в лоб, а по системе.
Итак, прежде, чем начинать, Вы должны
- выписать (прямо физически на бумаге или в файле) все состояния в которых может находиться Ваше изделие. “Находиться в состоянии” означает ничего не делать и ждать внешнего события (например, прихода пакета, нажатия кнопки, истечения временного интервала и т.п.);
- прописать какое состояние является стартовым (т.е. при запуске системы в каком она состоянии);
- возможно, определить “конечное” состояние - в котором уже ничего не происходит и больше не будет происходить (не всегда, но такие бывают);
- выписать все события, которые могут произойти. Все!
- составить таблицу в которой какждая строка - состояние системы, а каждый столбец - событие;
- на пересечении вписываете что система должна сделать если наступило событие, а она находится в данном состоянии. Обычно это выглядит как “сделать то-то и перейти в новое состояние такое-то”. Действия (“сделать то-то”) должны быть тривиальными и не зависеть ни от какой иной логики, например, перевести пин в HIGH или что-то подобное). Если в каком-то состоянии некоторое событие игнорируется (или невозможно) - оставляем пустую клетку;
Теперь таблицу надо внимательно проверить. Как проверять таблицу? Сначала формально.
- в таблице не должно быть пустых столбцов. Если есть пустой столбец, значит это событие не нужно ни для чего;
- в таблице не должно быть пустых строк, исключение - строка конечного состояния;
- не должно быть недостижимых состояний. Т.е. если в какое-то состояние (кроме стартового) система никогда не переходит - это признак ошибки - это состояние лишнее;
- и наоборот, из любого состояния (кроме “конечного”) должен быть выход в какое-то другое.
Далее, надо очень внимательно смотреть на таблицу и проверять логику. Никакой другой логики, кроме отражённой в таблице, быть не должно, если чего-то не хватает, добавляйте состояния и описывайте ВСЮ логику на языке состояний, событий и переходов.
Только после того, как такая таблица составлена и на семь раз проверена можно приступать к программированию. Эта таблица - наше всё. У меня они аккуратно хранятся в папках проектов - это основная документация на случай внесения правок. Я Вам покажу сегодня такую таблицу от реального проекта.
Когда у Вас есть такая таблица, программирование превращается в чисто механическую формальность. Вероятность ошибок (логических, а не опечаток) почти нулевая.
Ну, давайте перейдём к примерам, как это делается.
Пример №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 Гц и мигает пока кнопка удерживается.
- После отпускания кнопки выжидает 5 секунд.
- Если в течение этих пяти секунд снова нажать кнопку, начнёт мигать с частотой 10 Гц и тоже будет мигать пока удерживается кнопка.
- Если же пять секунд прошло и ничего не нажато, всё забываем и переходим к п. 1.
- Если кнопка была нажата (и мигала на 10 Гц), то после отпускания, опять же всё забываем и переходим к п.1.
Можете использовать библиотеки интервала и кнопки из моих примеров, больше Вам тут, вроде, ничего не нужно.
Когда Вы это сделаете и мы обсудим Вашу таблицу и программу, тогда перейдём к Вашей задаче. Впрочем, тогда, возможно, Вам моя помощь уже не будет нужна. Давайте, делайте.
Позволю себе терминологическое уточнение. В CAN-bus есть понятие ID, но это не обязательно адрес. Туда суют всё, что плохо лежит - тип пакета, например.
Т.е. из CAN-bus приходит пакет с неким ID и payload. Затем ID и анализируется. “Слушать шину по адресу” - это несколько коряво…
Точно так же в CAN нет “посылки по адресу”. Есть посылка фрейма с неким ID и payload.
Первоначальная табличка, но она мне не очень нравится, принимаю конструктивную критику. Блоки реакций на нажатие кнопок написаны неверно в части содержимого кан посылки, но это исправлю.
Сейчас нет возможности смотреть подробно, но вот сразу “loop в течение 100мс” - это расписывать надо через событие (истечение 100 мс) и что за loop тоже не очень понятно…
Кроме того, Вас просили упражнение сделать. Если бы сделали, Вы бы сейчас такого не написали.
Вобщем-то и на приём написаны так себе. Нет “чтения ID из шины”. Получаете всё, что летит оттуда, если фильтры не поставили. Затем сравниваете - с нужным ID прилетел пакет или нет. Если нет, то ничего не делаете, в обратном случае анализируете payload. ID 0x1E7 никогда не будет F1 FF. Будет ID 0x1E7 и payload F1 FF (например).
Вы и сами путаетесь и тот, кто помочь может, ловит когнитивный диссонанс и бросает разбираться в вашей проблеме.