В предыдущих постах мы спроектировали, сделали и всесторонне протестировали блок двухканального радиоуправляемого выключателя.
![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/ca4/f33/68c/ca4f3368cf462b7f12ba8ae3e149937e.jpg)
Но до сих пор это была «бездушная железка», которая несмотря на всю свою потенциальную мощь, заложенную в МК, — ничего не умеет.
В общем-то, наше основное устройство (если не рассматривать подключение радиомодуля) — нисколько не сложнее самой обычной Ардуинки, к которой подключено две кнопки и пара светодиодов (в результирующем устройстве — светодиоды заменены на транзисторные ключи, управляющие релюшками, но суть это не меняет).
Изготовленный модуль радиовыключателя не очень располагает к тому, чтобы прямо на нем производить разработку и отладку:
Но, как я раньше уже заметил, для «оживления» нашего модуля всего-то требуется написать скетч, который бы отрабатывал различные нажатия (две кнопки) и мог бы по нашему алгоритму включать/выключать две нагрузки (в макете это будет пара светодиодов). Естественно, это «базовый функционал», после того, как разберемся с ним — добавим и «радиоканальные» функции.
Итак, чтобы получить «удобную» среду для подготовки нашего скетча, возьмем беспаечную макетку, любую ардуино-совместимую плату (в моем случае это cArduino Nano), две тактовые кнопки, два светодиода (с токоограничительными резисторами) и несколько перемычек:
![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/359/4de/737/3594de737102d38783c8b5b9d4a2cfae.jpg)
Собираем макет, согласно принципиальной схемы из первого поста.
Напомню:
Собственно, такой макет позволит нам написать и отладить основной функционал.
В дальнейшем нужно будет этот скетч загрузить с помощью программатора в финальное устройство без переделок.
Перед началом разработки следует зафиксировать базовые функции, которые хотелось бы реализовать.
Естественно, этот список «хотелок» находится в голове еще перед началом работы над проектом, сейчас просто сформулирую.
Двухканальный выключатель будет использоваться для управления светом и вентиляцией в санузле, поэтому список возможностей получился такой:
Эти функции будут реализовываться чуть позже, но их сразу стоит держать в голове (меньше придется переписывать):
В ходе создания ПО для реализации базовых функций будем учитывать следующее:
Первое требование приводит к использованию массива структур для хранения параметров работы модуля, а второе — диктует использование сторожевого таймера (watchdog).
Для хранения параметров канала я создал следующую структуру:
Теперь уже можно написать несложный скетч.
В функции setup() проводим всю необходимую инициализацию и взводим «сторожевую собаку».
Дальше все просто: в основном цикле программы (loop()) будем последовательно делать следующие шаги:
Если дополнительная логика работы не нужна (в моем случае это автоматическое включение и выключение вентиляции в зависимости от состояния света) — функцию chkLogic() можно просто удалить.
Базовые функции работают ровно так, как хотелось.
Короткие нажатия кнопок включают соответствующие светодиоды, доп.логика срабатывает. По длинному нажатию любой кнопки — на одну секунду зажигается встроенный светодиод (D13) на ардуино.
Теперь можно реализовывать и беспроводные функции.
Для этого обратимся к одному из моих ранних постов: Беспроводные коммуникации «умного дома».
Основные принципы, которые я там описывал — выдержали проверку временем и претерпели очень незначительные изменения.
Для работы с параметрами подойдет структура:
Для передаваемых данных буду использовать следующую структуру:
Согласно вышесказанного, мой модуль будет описываться следующим образом:
Видно, что все ключевые параметры, описывающие текущее состояние и временные параметры, присутствуют.
Собственно, теперь осталось прошить наш модуль.
Для прошивки я использую программатор USBtinyISP.
Прошил, проверил работу — все ок, но обнаружилось, что в «чистом» МК все байты EEPROM установлены в 255, что дает соответствующие задержки.
По коду, который приведен выше, видно, что установка всех временных параметров производится только через радиоканал. Но про «управляющий модуль» я еще ничего не написал — поэтому надо как-то «изолированно» решить эту проблему.
Для этого можно воспользоваться примерами из библиотеки EEPROM и прямо из них прописать первичные (более актуальные) значения в соответствующие ячейки энергонезависимой памяти.
Последующая проверка показала, что теперь все работает как раз так, как хотелось.
Теперь устройство самодостаточно и готово выполнять свою основную функцию (даже без радиоканала). Можно монтировать.
Радиоуправляемый модуль будет монтироваться внутрь стены из гипсокартона — поэтому выбрал подходящий корпус (чтобы в него влез собственно модуль и блок питания для него и чтобы этот корпус можно было без проблем пропихнуть в отверстие для установки монтажной коробки).
Плату блока питания взял там же, где и в прошлый раз — распилил блок питания для iPhone. В принципе, можно сделать конденсаторный блок питания или поискать уже готовые варианты (например, тут).
Получилось как-то так (тут уже все подключено — проводил последние тесты перед монтажом в стену):
![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/ca4/f33/68c/ca4f3368cf462b7f12ba8ae3e149937e.jpg)
Корпус оказался несколько великоват, но имеющийся в хозяйстве более мелкий — не подошел.
Правильнее было бы, конечно, сначала выбрать конкретный корпус и делать под него, но у меня не было особых ограничений на размер, поэтому «как получилось».
Теперь можно заняться непосредственно «встраиванием» модуля в стену (к сожалению, увлекся процессом и забыл фотографировать, поэтому только текстовое описание):
Все, готово. Включаем электричество и проверяем, что все работает так, как хотелось.
Созданное устройство успешно смонтировано и отлично заменило «тупой» выключатель, добавив к нему чуточку «ума» (экономию электроэнергии в случаях «забывчивости» хозяев, автоматическое включение/выключение вытяжки и т.п.).
Продолжение следует...
P.S. В обсуждении первого поста были вопросы по поводу использования другой элементной базы, в том числе и для достижения более компактных размеров.
Это я к тому, что не стоит ко всему относиться как к догме (повторять все проекты «один в один») — ищите, подбирайте наиболее адекватные (для каждой конкретной задачи) решения, модифицируйте!
Полезные ссылки:
Спасибо Nikita_Rogatnev за помощь в подготовке материала к публикации.
![](https://habrastorage.org/getpro/habr/post_images/ca4/f33/68c/ca4f3368cf462b7f12ba8ae3e149937e.jpg)
Но до сих пор это была «бездушная железка», которая несмотря на всю свою потенциальную мощь, заложенную в МК, — ничего не умеет.
В общем-то, наше основное устройство (если не рассматривать подключение радиомодуля) — нисколько не сложнее самой обычной Ардуинки, к которой подключено две кнопки и пара светодиодов (в результирующем устройстве — светодиоды заменены на транзисторные ключи, управляющие релюшками, но суть это не меняет).
Изготовленный модуль радиовыключателя не очень располагает к тому, чтобы прямо на нем производить разработку и отладку:
- нет возможности получить диагностические сообщения в «мониторе порта»,
- отсутствует визуальное подтверждение, какое из реле и в каком состоянии находится и т.п.
Но, как я раньше уже заметил, для «оживления» нашего модуля всего-то требуется написать скетч, который бы отрабатывал различные нажатия (две кнопки) и мог бы по нашему алгоритму включать/выключать две нагрузки (в макете это будет пара светодиодов). Естественно, это «базовый функционал», после того, как разберемся с ним — добавим и «радиоканальные» функции.
Вообще, конечно, с «макетки» правильнее было бы начать, но в данном случае — так получилось, что прототип делался позже, чем результирующее устройство.
Макет
Итак, чтобы получить «удобную» среду для подготовки нашего скетча, возьмем беспаечную макетку, любую ардуино-совместимую плату (в моем случае это cArduino Nano), две тактовые кнопки, два светодиода (с токоограничительными резисторами) и несколько перемычек:
![](https://habrastorage.org/getpro/habr/post_images/359/4de/737/3594de737102d38783c8b5b9d4a2cfae.jpg)
Собираем макет, согласно принципиальной схемы из первого поста.
Напомню:
- Кнопку для первого канала подключаем между пином A1 и «землей» (GND),
- Кнопку второго канала — A0 и GND.
- Светодиоды (индикаторы работы соответствующих транзисторных ключей и реле в радиовыключателе) подключаем к D3 и D4, соответственно.
Собственно, такой макет позволит нам написать и отладить основной функционал.
В дальнейшем нужно будет этот скетч загрузить с помощью программатора в финальное устройство без переделок.
Перед началом разработки следует зафиксировать базовые функции, которые хотелось бы реализовать.
Желаемый функционал
Естественно, этот список «хотелок» находится в голове еще перед началом работы над проектом, сейчас просто сформулирую.
Базовые функции
Двухканальный выключатель будет использоваться для управления светом и вентиляцией в санузле, поэтому список возможностей получился такой:
- По краткому нажатию включать/выключать соответствующий канал нагрузки (канал 1 — свет, канал 2 — вентиляция).
- По длинному нажатию (более 2 секунд) — фиксировать факт такого нажатия («взводить флаг»), но пока ничего не делать дополнительно.
- Если свет включен более, чем 1,5 минуты — автоматически включить вытяжку (к примеру, кто-то пошел в душ и забыл включить вентиляцию).
- Если были включены оба канала и первый канал выключается, автоматически выключить второй канал через 10 минут.
- В случае, если любую нагрузку включили, но забыли выключить — автоматически выключить (у каждого канала — свое время автовыключения: 60 и 10 минут соответственно).
При формировании списка функций — активно общайтесь с домашними. К примеру, мне разумно подсказали, что время, после которого должно происходить автоматическое включение вентиляции слишком мало и будут ненужные срабатывания и вообще, все временные параметры надо иметь возможность в ходе эксплуатации корректировать.
Радиоуправление
Эти функции будут реализовываться чуть позже, но их сразу стоит держать в голове (меньше придется переписывать):
- Команды включения/выключения, поступившие по радиоканалу должны отрабатываться так, как если бы физически нажимались кнопки выключателя (т.е. полное сохранение базовой логики).
- Через радиоканал нужно иметь возможность изменять все временные параметры работы выключателя.
- Временные параметры работы включателя должны храниться в энергонезависимой памяти (чтобы после каждого выключения электричества не приходилось «переучивать» модуль).
- Все параметры (текущее состояние, флаги «длинного нажатия», временные) должны быть доступны по радиоканалу как по запросу (ответ на запрос), так и на регулярной основе (раз в 15 секунд — «флуд» в эфир с текущими значениями параметров).
Программирование
В ходе создания ПО для реализации базовых функций будем учитывать следующее:
- Сейчас каналов два, но в дальнейшем их может быть больше/меньше и код должен быть таким, чтобы это можно было просто корректировать (без существенного переписывания).
- Устройство встраиваемое и в случае какого-либо сбоя доставать его из стены крайне проблематично.
Первое требование приводит к использованию массива структур для хранения параметров работы модуля, а второе — диктует использование сторожевого таймера (watchdog).
Для хранения параметров канала я создал следующую структуру:
typedef struct {
int button; // пин кнопки
int relay; // пин реле
boolean state; // состояние (вкл/выкл)
unsigned long power_on; // время, когда нагрузка была включена
unsigned long auto_off; // время, через которое нагрузку автоматически выключить
unsigned long time_off; // время, автовыключения
boolean autostate; // флаг, означающий, что ждем автовыключение
unsigned long press_start; // время, когда кнопку нажали
unsigned long press_stop; // время, когда кнопку отпустили
}
Channel;
Теперь уже можно написать несложный скетч.
В функции setup() проводим всю необходимую инициализацию и взводим «сторожевую собаку».
Дальше все просто: в основном цикле программы (loop()) будем последовательно делать следующие шаги:
- Работаем с кнопками (функция button_read()).
- Отрабатываем автовыключение (autoOff()).
- Реализуем дополнительную логику работы (chkLogic()).
- Сбрасываем сторожевой таймер (wdt_reset()).
Если дополнительная логика работы не нужна (в моем случае это автоматическое включение и выключение вентиляции в зависимости от состояния света) — функцию chkLogic() можно просто удалить.
У меня получился вот такой скетч
//подключаем библиотеки
#include <avr/wdt.h>
#include <Bounce.h>
//#define DEBUG // если нужен вывод отладочных сообщений - закомментировать
// определим количество каналов
#define CH 2
// определим задержку для длинного нажатия
#define LONGPRESS 2000 // 2 секунды
// создадим структуру для хранения параметров "канала"
typedef struct {
int button; // пин кнопки
int relay; // пин реле
boolean state; // состояние (вкл/выкл)
unsigned long power_on; // время, когда нагрузка была включена
unsigned long auto_off; // время, через которое нагрузку автоматически выключить
unsigned long time_off; // время, автовыключения
boolean autostate; // флаг, означающий, что ждем автовыключение
unsigned long press_start; // время, когда кнопку нажали
unsigned long press_stop; // время, когда кнопку отпустили
}
Channel;
// определим параметры каналов
Channel MySwitch[CH] = {
15, 3, LOW, 0, 3600000, 0, false, 0, 0,
14, 4, LOW, 0, 600000, 0, false, 0, 0
};
// создаем объекты класса Bounce. Указываем пины, к которым подключены кнопки и время дребезга в мс.
Bounce bouncer0 = Bounce(MySwitch[0].button,5);
Bounce bouncer1 = Bounce(MySwitch[1].button,5);
// флаг для дополнительной логики
boolean logicFlag = false;
boolean onFlag = false;
boolean offFlag = false;
void setup() {
wdt_disable(); // бесполезная строка до которой не доходит выполнение при bootloop
#ifndef DEBUG
Serial.begin(9600);
Serial.println("Start!");
pinMode(13, OUTPUT);
#endif
// реле
pinMode(MySwitch[0].relay, OUTPUT);
pinMode(MySwitch[1].relay, OUTPUT);
// кнопки
pinMode(MySwitch[0].button, INPUT);
pinMode(MySwitch[1].button, INPUT);
// включим подтягивающие резисторы для кнопок
digitalWrite(MySwitch[0].button, HIGH);
digitalWrite(MySwitch[1].button, HIGH);
//delay(5000); // Задержка, чтобы было время перепрошить устройство в случае bootloop
#ifndef DEBUG
Serial.println("Ready!");
#endif
wdt_enable (WDTO_8S); // Для тестов не рекомендуется устанавливать значение менее 8 сек.
}
void loop() {
// работаем с кнопками
button_read();
// отработаем автовыключение
autoOff();
// проверим дополнительную логику работы
chkLogic();
// сбросим сторожевую собаку
wdt_reset();
}
void button_read(){
//если сменилось состояние кнопки 1
if ( bouncer0.update() ) {
if ( bouncer0.read() == LOW) {
// фиксируем время старта нажатия
MySwitch[0].press_start = millis();
}
else {
// определяем, какое нажатие было (короткое или длинное) и делаем, что требуется.
pressDetect(0, millis());
}
}
//если сменилось состояние кнопки 2
if ( bouncer1.update() ) {
if ( bouncer1.read() == LOW) {
MySwitch[1].press_start = millis();
}
else {
pressDetect(1, millis());
}
}
}
// реализация выключателя
void doSwitch(int ch, boolean state){
// изменяем состояние выключателя
MySwitch[ch].state = state;
// если выключатель перешел в положение "ВКЛ" и время автоотключения больше нуля
if (MySwitch[ch].state == HIGH) {
// зафиксируем время включения
MySwitch[ch].power_on = millis();
if(MySwitch[ch].auto_off > 0) {
// посчитаем время, когда надо будет автоматически выключить
MySwitch[ch].time_off = MySwitch[ch].power_on + MySwitch[ch].auto_off;
MySwitch[ch].autostate = true;
}
#ifndef DEBUG
Serial.print("ON ");
Serial.println(ch);
#endif
}
else {
// отключим режим автовыключения
MySwitch[ch].autostate = false;
// сбросим время автовыключения
MySwitch[ch].time_off = 0;
#ifndef DEBUG
Serial.print("OFF ");
Serial.println(ch);
#endif
}
digitalWrite(MySwitch[ch].relay,MySwitch[ch].state);
}
// автовыключение
void autoOff(){
// цикл по всем каналам
for (int i=0; i < CH; i++) {
// если время выключения подошло - щелкнем выключателем
if ((millis() >= MySwitch[i].time_off) && MySwitch[i].autostate) {
MySwitch[i].autostate = false;
doSwitch(i, LOW);
#ifndef DEBUG
Serial.print("Auto OFF ");
Serial.println(i);
#endif
}
}
}
// определяем длину нажатия на клавишу и выполняем действия
void pressDetect(int ch, unsigned long p_stop) {
if (MySwitch[ch].press_start != 0) {
if ((p_stop-MySwitch[ch].press_start) < LONGPRESS) {
// короткое нажатие
MySwitch[ch].press_stop = p_stop;
#ifndef DEBUG
Serial.print("Short press ");
Serial.println(ch);
#endif
doSwitch(ch, MySwitch[ch].state ? LOW : HIGH);
}
else {
// длинное нажатие
#ifndef DEBUG
Serial.print("Long press ");
Serial.println(ch);
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
#endif
}
}
}
// дополнительная логика работы
void chkLogic(){
/* дополнительная логика (для с/у)
0-канал - свет с/у
1-канал - вытяжка с/у
если 0 канал включен больше, чем 1.5 минуты, то нужно включить и 1 канал.
после выключение 0 канала - 1 канал выключить через 10 минут
*/
// если свет горит больше 1,5 минуты, а вытяжка не включена - нужно включить вытяжку
if ((onFlag == false) && (millis() > (MySwitch[0].power_on + 90000)) && (MySwitch[0].state == HIGH) && (MySwitch[1].state == LOW) && (MySwitch[1].press_stop < MySwitch[0].power_on)) {
// включаем вытяжку
doSwitch(1, HIGH);
// взводим флаг автовключения
onFlag = true;
logicFlag = true;
// выключаем режим автовыключения по таймауту
MySwitch[1].autostate = false;
#ifndef DEBUG
Serial.println("Auto Logic ON");
#endif
}
// если вытяжка включена - дадим ей новое время выключения - через 10 минут после выключения света
if ((logicFlag == true) && (offFlag == false) && (MySwitch[1].state == HIGH) && (MySwitch[0].state == LOW)) {
MySwitch[1].time_off = millis() + 600000;
MySwitch[1].autostate = true;
offFlag = true;
#ifndef DEBUG
Serial.println("Auto Logic OFF started");
#endif
}
// если все выключено, сбрасываем все флаги
if ((logicFlag == true) && (MySwitch[0].state == LOW) && (MySwitch[1].state == LOW)) {
offFlag = false;
onFlag = false;
logicFlag = false;
#ifndef DEBUG
Serial.println("Logic reset");
#endif
}
// если при включенной вытяжке выключатель выключили вручную - запускаем автовыключение вытяжки
if ((logicFlag == false) && (offFlag == false) && (MySwitch[0].press_stop > MySwitch[1].power_on) && (MySwitch[1].state == HIGH) && (MySwitch[0].state == LOW)) {
logicFlag = true;
#ifndef DEBUG
Serial.println("Auto OFF 1 after manual OFF 0");
#endif
}
}
Базовые функции работают ровно так, как хотелось.
Короткие нажатия кнопок включают соответствующие светодиоды, доп.логика срабатывает. По длинному нажатию любой кнопки — на одну секунду зажигается встроенный светодиод (D13) на ардуино.
Теперь можно реализовывать и беспроводные функции.
Для этого обратимся к одному из моих ранних постов: Беспроводные коммуникации «умного дома».
Основные принципы, которые я там описывал — выдержали проверку временем и претерпели очень незначительные изменения.
Для работы с параметрами подойдет структура:
typedef struct{
float Value; // значение
boolean Status; // статус
// 0 - RO
// 1 - RW
char Note[16]; // комментарий
}
Parameter;
Для передаваемых данных буду использовать следующую структуру:
typedef struct{
int SensorID; // идентификатор датчика
int CommandTo; // команда модулю номер ...
int Command; // команда
// 0 - ответ
// 1 - получить значение
// 2 - установить значение
int ParamID; // идентификатор параметра
float ParamValue; // значение параметра
boolean Status; // статус
// 0 - только для чтения (RO)
// 1 - можно изменять (RW)
char Comment[16]; // комментарий
}
Message;
Согласно вышесказанного, мой модуль будет описываться следующим образом:
#define SID 701 // идентификатор датчика
#define NumSensors 8 // количество параметров
Parameter MySensors[NumSensors+1] = { // описание датчиков (и первичная инициализация)
NumSensors,0,"BR 2Floor", // информация о модуле
0,1,"Ch.1 (Light)", // состояние канала 1 (свет)
0,1,"Ch.2 (Vent)", // состояние канала 2 (вентиляция)
0,1,"Ch.1 (LP)", // флаг длинного нажатия в 1 канале
0,1,"Ch.2 (LP)", // флаг длинного нажатия в 2 канале
0,1,"Auto-delayON", // время до автоматического включения вытяжки (после включения света), в минутах
0,1,"Auto-delayOFF", // время до автоматического выключения вытяжки (после выключения света), в минутах
0,1,"Ch.1 AutoOFF", // время автовыключения в 1 канале, в минутах
0,1,"Ch.2 AutoOFF" // время автовыключения в 2 канале, в минутах
};
Message sensor;
Видно, что все ключевые параметры, описывающие текущее состояние и временные параметры, присутствуют.
Еще немного программирования и код готов.
//подключаем библиотеки
#include <avr/wdt.h>
#include <Bounce.h>
#include <SPI.h>
#include "RF24.h"
#include <EEPROM.h>
#define DEBUG // если нужна отладка - закомментировать
// определим количество каналов
#define CH 2
// определим задержку для длинного нажатия
#define LONGPRESS 2000 // 2 секунды
// описание параметров модуля
#define SID 701 // идентификатор датчика
#define NumSensors 8 // количество параметров
// создадим структуру для хранения параметров "канала"
typedef struct {
int button; // пин кнопки
int relay; // пин реле
boolean state; // состояние (вкл/выкл)
unsigned long power_on; // время, когда нагрузка была включена
unsigned long auto_off; // время, через которое нагрузку автоматически выключить
unsigned long time_off; // время, автовыключения
boolean autostate; // флаг, означающий, что ждем автовыключение
unsigned long press_start; // время, когда кнопку нажали
unsigned long press_stop; // время, когда кнопку отпустили
}
Channel;
// определим параметры каналов
Channel MySwitch[CH] = {
15, 3, LOW, 0, 0, 0, false, 0, 0,
14, 4, LOW, 0, 0, 0, false, 0, 0
};
// создаем структуру для описания параметров
typedef struct{
float Value; // значение
boolean Status; // статус
// 0 - RO
// 1 - RW
char Note[16]; // комментарий
}
Parameter;
// создаём структуру для передачи значений
typedef struct{
int SensorID; // идентификатор датчика
int CommandTo; // команда модулю номер ...
int Command; // команда
// 0 - ответ
// 1 - получить значение
// 2 - установить значение
int ParamID; // идентификатор параметра
float ParamValue; // значение параметра
boolean Status; // статус
// 0 - только для чтения (RO)
// 1 - можно изменять (RW)
char Comment[16]; // комментарий
}
Message;
/////////////////////////////////////////////////////////////////////////////
Parameter MySensors[NumSensors+1] = { // описание датчиков (и первичная инициализация)
NumSensors,0,"701 (2F, bath)", // в поле "комментарий" указываем пояснительную информацию о датчике и количество сенсоров
0,1,"Ch.1 (Light)", // состояние канала 1 (свет)
0,1,"Ch.2 (Vent)", // состояние канала 2 (вентиляция)
0,1,"Ch.1 (LP)", // флаг длинного нажатия в 1 канале
0,1,"Ch.2 (LP)", // флаг длинного нажатия в 2 канале
0,1,"Auto-delayON", // время до автоматического включения вытяжки (после включения света), в минутах
0,1,"Auto-delayOFF", // время до автоматического выключения вытяжки (после выключения света), в минутах
0,1,"Ch.1 AutoOFF", // время автовыключения в 1 канале, в минутах
0,1,"Ch.2 AutoOFF" // время автовыключения в 2 канале, в минутах
};
Message sensor;
/////////////////////////////////////////////////////////////////////////////
// создаем объекты класса Bounce. Указываем пины, к которым подключены кнопки и время дребезга в мс.
Bounce bouncer0 = Bounce(MySwitch[0].button,5);
Bounce bouncer1 = Bounce(MySwitch[1].button,5);
// флаг для дополнительной логики
boolean logicFlag = false;
boolean onFlag = false;
boolean offFlag = false;
//RF24 radio(CE,CSN);
RF24 radio(10,9);
unsigned long measureTime;
#define DELTAMEASURE 15000 // раз в 15 секунд будем флудить в эфир
const uint64_t pipes[2] = {
0xF0F0F0F0A1LL, 0xF0F0F0F0A2LL };
volatile boolean waitRF24 = false;
void setup() {
wdt_disable(); // бесполезная строка до которой не доходит выполнение при bootloop
// прочитаем параметры из EEPROM
prepareFromEEPROM();
#ifndef DEBUG
Serial.begin(9600);
Serial.println("Start!");
pinMode(13, OUTPUT);
#endif
for(int i=0; i<CH; i++) {
// реле
pinMode(MySwitch[i].relay, OUTPUT);
// кнопки
pinMode(MySwitch[i].button, INPUT);
// включим подтягивающие резисторы для кнопок
digitalWrite(MySwitch[i].button, HIGH);
}
// радио
initRF24();
// включим обработчик прерывания (когда что-то приходит через радиоканал)
attachInterrupt(0, isr_RF24, FALLING);
measureTime = millis()+DELTAMEASURE;
//delay(5000); // Задержка, чтобы было время перепрошить устройство в случае bootloop
#ifndef DEBUG
Serial.println("Ready!");
#endif
wdt_enable (WDTO_8S); // Для тестов не рекомендуется устанавливать значение менее 8 сек.
}
void loop() {
// работаем с кнопками
button_read();
// отработаем автовыключение
autoOff();
// проверим дополнительную логику работы
chkLogic();
// послушаем радио
listenRF24();
// если пора - пофлудим в эфир
floodRF24();
// сбросим сторожевую собаку
wdt_reset();
}
void button_read(){
//если сменилось состояние кнопки 1
if ( bouncer0.update() ) {
if ( bouncer0.read() == LOW) {
// фиксируем время старта нажатия
MySwitch[0].press_start = millis();
}
else {
// определяем, какое нажатие было (короткое или длинное) и делаем, что требуется.
pressDetect(0, millis());
}
}
//если сменилось состояние кнопки 2
if ( bouncer1.update() ) {
if ( bouncer1.read() == LOW) {
MySwitch[1].press_start = millis();
}
else {
//MySwitch[1].press_stop = millis();
pressDetect(1, millis());
}
}
}
// реализация выключателя
void doSwitch(int ch, boolean state){
// устанавливаем требуемое состояние выключателя
MySwitch[ch].state = state;
// если выключатель перешел в положение "ВКЛ" и время автоотключения больше нуля
if (MySwitch[ch].state == HIGH) {
// зафиксируем время включения
MySwitch[ch].power_on = millis();
if((MySwitch[ch].auto_off > 0) && (MySwitch[ch].auto_off != 0)) {
// посчитаем время, когда надо будет автоматически выключить
MySwitch[ch].time_off = MySwitch[ch].power_on + MySwitch[ch].auto_off;
MySwitch[ch].autostate = true;
}
#ifndef DEBUG
Serial.print("ON ");
Serial.println(ch);
#endif
}
else {
// отлючим режим автовыключения
MySwitch[ch].autostate = false;
// сбросим время автовыключения
MySwitch[ch].time_off = 0;
#ifndef DEBUG
Serial.print("OFF ");
Serial.println(ch);
#endif
}
digitalWrite(MySwitch[ch].relay,MySwitch[ch].state);
}
// автовыключение
void autoOff(){
// цикл по всем каналам
for (int i=0; i < CH; i++) {
// если время выключения подошло - щелкнем выключателем
if ((millis() >= MySwitch[i].time_off) && MySwitch[i].autostate) {
MySwitch[i].autostate = false;
doSwitch(i, LOW);
#ifndef DEBUG
Serial.print("Auto OFF ");
Serial.println(i);
#endif
}
}
}
// определяем длину нажатия на клавишу и выполняем действия
void pressDetect(int ch, unsigned long p_stop) {
if (MySwitch[ch].press_start != 0) {
if (((p_stop-MySwitch[ch].press_start) < LONGPRESS) && (p_stop-MySwitch[ch].press_start) > 0) {
// короткое нажатие
MySwitch[ch].press_stop = p_stop;
#ifndef DEBUG
Serial.print("Short press ");
Serial.println(ch);
#endif
doSwitch(ch, MySwitch[ch].state ? LOW : HIGH);
}
else {
// длинное нажатие
#ifndef DEBUG
Serial.print("Long press ");
Serial.println(ch);
digitalWrite(13, !digitalRead(13));
#endif
// взводим соответствующий флаг в структуре параметров
MySensors[ch+3].Value = 1;
// сброс этого флага оставим на "совести" управляющего блока
// управляющий блок получает "взведенный" флаг, что-то делает (согласно своей логики)
// и после завершения соответствующих действий по радиоканалу "сбрасывает" флаг
}
}
}
// дополнительная логика работы
void chkLogic(){
/* дополнительная логика (для с/у)
0-канал - свет с/у
1-канал - вытяжка с/у
если 0 канал включен больше, чем 1.5 минуты, то нужно включить и 1 канал.
после выключение 0 канала - 1 канал выключить через 10 минут
*/
// если свет горит больше заданного интервала (параметр MySensors[5].Value и он - ненулевой), а вытяжка не включена - нужно включить вытяжку
if ((onFlag == false) && (millis() > (MySwitch[0].power_on + MySensors[5].Value*60000)) && (MySensors[5].Value != 0) && (MySwitch[0].state == HIGH) && (MySwitch[1].state == LOW) && (MySwitch[1].press_stop < MySwitch[0].power_on)) {
// включаем вытяжку
doSwitch(1, HIGH);
// взводим флаг автовключения
onFlag = true;
logicFlag = true;
// выключаем режим автовыключения по таймауту
MySwitch[1].autostate = false;
#ifndef DEBUG
Serial.println("Auto Logic ON");
#endif
}
// если вытяжка включена - дадим ей новое время выключения - через заданный интервал ((параметр MySensors[6].Value и он - ненулевой) после выключения света
if ((logicFlag == true) && (offFlag == false) && (MySensors[6].Value != 0) && (MySwitch[1].state == HIGH) && (MySwitch[0].state == LOW)) {
MySwitch[1].time_off = millis() + MySensors[6].Value*60000;
MySwitch[1].autostate = true;
offFlag = true;
#ifndef DEBUG
Serial.println("Auto Logic OFF started");
#endif
}
// если все выключено, сбрасываем все флаги
if ((logicFlag == true) && (MySwitch[0].state == LOW) && (MySwitch[1].state == LOW)) {
offFlag = false;
onFlag = false;
logicFlag = false;
#ifndef DEBUG
Serial.println("Logic reset");
#endif
}
// если при включенной вытяжке выключатель выключили вручную - запускаем автовыключение вытяжки
if ((logicFlag == false) && (offFlag == false) && (MySwitch[0].press_stop > MySwitch[1].power_on) && (MySwitch[1].state == HIGH) && (MySwitch[0].state == LOW)) {
logicFlag = true;
#ifndef DEBUG
Serial.println("Auto OFF 1 after manual OFF 0");
#endif
}
}
void floodRF24(){
// пофлудим в эфире (1 раз в DELTAMEASURE милисекунд)
// имя датчика не передаем! имя датчика - только в ответ на прямой запрос!
if (millis() > measureTime){
getValue();
// если нужно отправлять все параметры
//for (int i=1; i<=NumSensors; i++) {
// флудим только актуальными параметрами (параметры, отвечающие за настройки - не передаем)
for (int i=1; i<=4; i++) {
sendSlaveMessage(0, i);
delay(20);
}
measureTime = millis()+DELTAMEASURE;
}
}
void getValue(){
MySensors[1].Value = MySwitch[0].state;
MySensors[2].Value = MySwitch[1].state;
return;
}
// обработчик прерывания для прослушивания эфира
void isr_RF24(){
waitRF24 = true;
}
// отправить сообщение (от, кому, идентификатор параметра) - универсальная функция (slave)
// ! нет проверки на валидность ParamID
void sendSlaveMessage(int To, int ParamID) {
// отключаем режим приёма
radio.stopListening();
radio.openWritingPipe(pipes[0]);
radio.openReadingPipe(1,pipes[1]);
delay(20);
//подготовим данные в структуру для передачи
sensor.SensorID = SID;
sensor.CommandTo = To;
sensor.Command = 0;
sensor.ParamID = ParamID;
sensor.ParamValue = MySensors[ParamID].Value;
sensor.Status = MySensors[ParamID].Status;
memcpy(&sensor.Comment,(char*)MySensors[ParamID].Note, 16);
//отправляем данные по RF24
bool ok = radio.write( &sensor, sizeof(sensor) );
delay (20);
// включим режим приёма
radio.openWritingPipe(pipes[1]);
radio.openReadingPipe(1,pipes[0]);
radio.startListening();
}
// слушаем радио
void listenRF24(){
// слушать имеет смысл, если по прерыванию был взведен флаг
if (waitRF24) {
waitRF24 = false;
// разберем, что пришло
// если получена команда
if (radio.available()) {
bool done = false;
while (!done)
{
done = radio.read( &sensor, sizeof(sensor) );
// если команда этому модулю - обрабатываем
if (sensor.CommandTo == SID) {
// исполнить команду (от кого, команда, параметр, комментарий)
doCommand(sensor.SensorID, sensor.Command, sensor.ParamID, sensor.ParamValue, sensor.Status, sensor.Comment);
}
}
}
}
}
// исполнить команду (от кого, команда, IDпараметра, значение параметра, статус, комментарий) - универсальная функция
void doCommand(int From, int Command, int ParamID, float ParamValue, boolean Status, char* Comment) {
// тут можно добавить условие - проверка от кого можно обрабатывать команды, а от кого - нет
switch (Command) {
case 0:
// ничего не делаем
break;
case 1:
getValue();
// читаем и отправляем назад
sendSlaveMessage(From, ParamID);
break;
case 2:
// устанавливаем
setValue(From, ParamID, ParamValue, Comment);
// отчитываемся
sendSlaveMessage(From, ParamID);
break;
default:
break;
}
}
// установка значений (от, что, значение, комментарий)
void setValue(int From, int ParamID, float ParamValue, char* Comment) {
// если требуется установить уже и так установленное состояние - просто игнорируем команду
if(MySensors[ParamID].Value != ParamValue){
// если требуется включить/выключить - делаем (по параметрам) "имитацию" короткого нажатия соответствующей кнопки
// опять же не "дергаем" выключатель почем зря (только если состояние требуется изменить)
if((ParamID<3) && (MySwitch[ParamID-1].state != (boolean)ParamValue)) {
// "нажали кнопку"
MySwitch[ParamID-1].press_start = millis()-50;
// "отпустили кнопку" (система по "отпусканию" сама реализует обработку)
pressDetect(ParamID-1, millis());
}
else { // просто делаем
MySensors[ParamID].Value = ParamValue;
// если передаются параметры, задающие временные интервалы - фиксируем в EEPROM
if (ParamID > 4){
EEPROM.write(ParamID-5, MySensors[ParamID].Value);
// обновим параметры канала
if(ParamID > 6) {
MySwitch[ParamID-7].auto_off = ((unsigned long)MySensors[ParamID].Value)*60000;
}
}
}
}
}
void initRF24(){
radio.begin();
radio.setRetries(15,15);
// номер выбранного частотного канала (подобрать свой)
radio.setChannel(100);
radio.openWritingPipe(pipes[0]);
radio.openReadingPipe(1,pipes[1]);
radio.startListening(); // включаем режим приёма
}
void prepareFromEEPROM() {
// 4 параметра = 4 ячейки памяти по 1 байту:
// 0 - время до автоматического включения вытяжки (после включения света), в минутах
// 1 - время до автоматического выключения вытяжки (после выключения света), в минутах
// 2 - время автовыключения в 1 канале, в минутах
// 3 - время автовыключения в 2 канале, в минутах
for(int i=0; i<4; i++) {
MySensors[i+5].Value = EEPROM.read(i);
}
// теперь заполним соответствующие параметры для времени автовыключения
for(int i=0; i<CH; i++) {
MySwitch[i].auto_off = ((unsigned long)MySensors[i+7].Value)*60000;
}
}
Собственно, теперь осталось прошить наш модуль.
Для прошивки я использую программатор USBtinyISP.
Прошил, проверил работу — все ок, но обнаружилось, что в «чистом» МК все байты EEPROM установлены в 255, что дает соответствующие задержки.
По коду, который приведен выше, видно, что установка всех временных параметров производится только через радиоканал. Но про «управляющий модуль» я еще ничего не написал — поэтому надо как-то «изолированно» решить эту проблему.
Для этого можно воспользоваться примерами из библиотеки EEPROM и прямо из них прописать первичные (более актуальные) значения в соответствующие ячейки энергонезависимой памяти.
Последующая проверка показала, что теперь все работает как раз так, как хотелось.
Еще раз повторю свой основной принцип устройств моего «умного дома»: каждое созданное устройство сделано для достижения какой-то определенной цели и оно должно работать самостоятельно.
Теперь устройство самодостаточно и готово выполнять свою основную функцию (даже без радиоканала). Можно монтировать.
Установка модуля
Радиоуправляемый модуль будет монтироваться внутрь стены из гипсокартона — поэтому выбрал подходящий корпус (чтобы в него влез собственно модуль и блок питания для него и чтобы этот корпус можно было без проблем пропихнуть в отверстие для установки монтажной коробки).
Плату блока питания взял там же, где и в прошлый раз — распилил блок питания для iPhone. В принципе, можно сделать конденсаторный блок питания или поискать уже готовые варианты (например, тут).
Получилось как-то так (тут уже все подключено — проводил последние тесты перед монтажом в стену):
![](https://habrastorage.org/getpro/habr/post_images/ca4/f33/68c/ca4f3368cf462b7f12ba8ae3e149937e.jpg)
Корпус оказался несколько великоват, но имеющийся в хозяйстве более мелкий — не подошел.
Правильнее было бы, конечно, сначала выбрать конкретный корпус и делать под него, но у меня не было особых ограничений на размер, поэтому «как получилось».
Теперь можно заняться непосредственно «встраиванием» модуля в стену (к сожалению, увлекся процессом и забыл фотографировать, поэтому только текстовое описание):
- Обесточиваем соответствующую цепь освещения.
- Демонтируем имеющийся выключатель (не забываем промаркировать, какие пары идут на свет, а какие — на вытяжку).
- Снимаем монтажную коробку
- Подключаем радиовыключатель к соответствующим проводам (попутно избавляясь от «скруток», которые оставили «добрые строители»).
- Аккуратно заталкиваем все провода и радиовыключатель в промежуток между листами гипсокартона (я решил расположить модуль выше выключателя, чтобы его было проще достать при необходимости).
- Выводим провода, к которым будем подключать кнопочный выключатель в отверстие для установки монтажной коробки (специально взял принципиально отличающийся от остальной проводки кабель — МГТФ, чтобы в случае чего электрику было понятно, что тут «что-то странное» и с этим надо сначала разобраться).
- Теперь можно установить монтажную коробку и подключить кнопочный выключатель.
Все, готово. Включаем электричество и проверяем, что все работает так, как хотелось.
Результат
Созданное устройство успешно смонтировано и отлично заменило «тупой» выключатель, добавив к нему чуточку «ума» (экономию электроэнергии в случаях «забывчивости» хозяев, автоматическое включение/выключение вытяжки и т.п.).
Продолжение следует...
P.S. В обсуждении первого поста были вопросы по поводу использования другой элементной базы, в том числе и для достижения более компактных размеров.
Недавно в руки мне попал вот такой зверек:![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/786/45c/28f/78645c28f1a80099a1904c369506a278.jpg)
Это обычное реле (очень тихое) с двумя группами коммутируемых контактов. Может включать/выключать цепи на 220В (мощность небольшая, но для светодиодных ламп — вполне подойдет). Управляется 5В, можно подключать напрямую к выводу МК (без транзистора).
![](https://habrastorage.org/getpro/habr/post_images/786/45c/28f/78645c28f1a80099a1904c369506a278.jpg)
Это обычное реле (очень тихое) с двумя группами коммутируемых контактов. Может включать/выключать цепи на 220В (мощность небольшая, но для светодиодных ламп — вполне подойдет). Управляется 5В, можно подключать напрямую к выводу МК (без транзистора).
Это я к тому, что не стоит ко всему относиться как к догме (повторять все проекты «один в один») — ищите, подбирайте наиболее адекватные (для каждой конкретной задачи) решения, модифицируйте!
Полезные ссылки:
- Arduino watchdog или автоматический RESET в случае зависания
- Беспроводные коммуникации «умного дома»
- Конденсаторное питание
Спасибо Nikita_Rogatnev за помощь в подготовке материала к публикации.