Радиоуправляемый выключатель своими руками. Часть 3 — Софт выключателя

от автора

В предыдущих постах мы спроектировали, сделали и всесторонне протестировали блок двухканального радиоуправляемого выключателя.

Но до сих пор это была «бездушная железка», которая несмотря на всю свою потенциальную мощь, заложенную в МК, — ничего не умеет.

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

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

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

Но, как я раньше уже заметил, для «оживления» нашего модуля всего-то требуется написать скетч, который бы отрабатывал различные нажатия (две кнопки) и мог бы по нашему алгоритму включать/выключать две нагрузки (в макете это будет пара светодиодов). Естественно, это «базовый функционал», после того, как разберемся с ним — добавим и «радиоканальные» функции.

Вообще, конечно, с «макетки» правильнее было бы начать, но в данном случае — так получилось, что прототип делался позже, чем результирующее устройство.

Макет

Итак, чтобы получить «удобную» среду для подготовки нашего скетча, возьмем беспаечную макетку, любую ардуино-совместимую плату (в моем случае это cArduino Nano), две тактовые кнопки, два светодиода (с токоограничительными резисторами) и несколько перемычек:

Собираем макет, согласно принципиальной схемы из первого поста.

Напомню:

  • Кнопку для первого канала подключаем между пином A1 и «землей» (GND),
  • Кнопку второго канала — A0 и GND.
  • Светодиоды (индикаторы работы соответствующих транзисторных ключей и реле в радиовыключателе) подключаем к D3 и D4, соответственно.

Собственно, такой макет позволит нам написать и отладить основной функционал.

В дальнейшем нужно будет этот скетч загрузить с помощью программатора в финальное устройство без переделок.

Перед началом разработки следует зафиксировать базовые функции, которые хотелось бы реализовать.

Желаемый функционал

Естественно, этот список «хотелок» находится в голове еще перед началом работы над проектом, сейчас просто сформулирую.

Базовые функции

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

  • По краткому нажатию включать/выключать соответствующий канал нагрузки (канал 1 — свет, канал 2 — вентиляция).
  • По длинному нажатию (более 2 секунд) — фиксировать факт такого нажатия («взводить флаг»), но пока ничего не делать дополнительно.
  • Если свет включен более, чем 1,5 минуты — автоматически включить вытяжку (к примеру, кто-то пошел в душ и забыл включить вентиляцию).
  • Если были включены оба канала и первый канал выключается, автоматически выключить второй канал через 10 минут.
  • В случае, если любую нагрузку включили, но забыли выключить — автоматически выключить (у каждого канала — свое время автовыключения: 60 и 10 минут соответственно).

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

Радиоуправление

Эти функции будут реализовываться чуть позже, но их сразу стоит держать в голове (меньше придется переписывать):

  • Команды включения/выключения, поступившие по радиоканалу должны отрабатываться так, как если бы физически нажимались кнопки выключателя (т.е. полное сохранение базовой логики).
  • Через радиоканал нужно иметь возможность изменять все временные параметры работы выключателя.
  • Временные параметры работы включателя должны храниться в энергонезависимой памяти (чтобы после каждого выключения электричества не приходилось «переучивать» модуль).
  • Все параметры (текущее состояние, флаги «длинного нажатия», временные) должны быть доступны по радиоканалу как по запросу (ответ на запрос), так и на регулярной основе (раз в 15 секунд — «флуд» в эфир с текущими значениями параметров).

Программирование

В ходе создания ПО для реализации базовых функций будем учитывать следующее:

  1. Сейчас каналов два, но в дальнейшем их может быть больше/меньше и код должен быть таким, чтобы это можно было просто корректировать (без существенного переписывания).
  2. Устройство встраиваемое и в случае какого-либо сбоя доставать его из стены крайне проблематично.

Первое требование приводит к использованию массива структур для хранения параметров работы модуля, а второе — диктует использование сторожевого таймера (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. В принципе, можно сделать конденсаторный блок питания или поискать уже готовые варианты (например, тут).

Получилось как-то так (тут уже все подключено — проводил последние тесты перед монтажом в стену):

Корпус оказался несколько великоват, но имеющийся в хозяйстве более мелкий — не подошел.

Правильнее было бы, конечно, сначала выбрать конкретный корпус и делать под него, но у меня не было особых ограничений на размер, поэтому «как получилось».

Теперь можно заняться непосредственно «встраиванием» модуля в стену (к сожалению, увлекся процессом и забыл фотографировать, поэтому только текстовое описание):

  • Обесточиваем соответствующую цепь освещения.
  • Демонтируем имеющийся выключатель (не забываем промаркировать, какие пары идут на свет, а какие — на вытяжку).
  • Снимаем монтажную коробку
  • Подключаем радиовыключатель к соответствующим проводам (попутно избавляясь от «скруток», которые оставили «добрые строители»).
  • Аккуратно заталкиваем все провода и радиовыключатель в промежуток между листами гипсокартона (я решил расположить модуль выше выключателя, чтобы его было проще достать при необходимости).
  • Выводим провода, к которым будем подключать кнопочный выключатель в отверстие для установки монтажной коробки (специально взял принципиально отличающийся от остальной проводки кабель — МГТФ, чтобы в случае чего электрику было понятно, что тут «что-то странное» и с этим надо сначала разобраться).
  • Теперь можно установить монтажную коробку и подключить кнопочный выключатель.

Все, готово. Включаем электричество и проверяем, что все работает так, как хотелось.

Результат

Созданное устройство успешно смонтировано и отлично заменило «тупой» выключатель, добавив к нему чуточку «ума» (экономию электроэнергии в случаях «забывчивости» хозяев, автоматическое включение/выключение вытяжки и т.п.).

Продолжение следует…

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

Недавно в руки мне попал вот такой зверек:

Это обычное реле (очень тихое) с двумя группами коммутируемых контактов. Может включать/выключать цепи на 220В (мощность небольшая, но для светодиодных ламп — вполне подойдет). Управляется 5В, можно подключать напрямую к выводу МК (без транзистора).

Это я к тому, что не стоит ко всему относиться как к догме (повторять все проекты «один в один») — ищите, подбирайте наиболее адекватные (для каждой конкретной задачи) решения, модифицируйте!

Полезные ссылки:

Спасибо Nikita_Rogatnev за помощь в подготовке материала к публикации.

ссылка на оригинал статьи http://habrahabr.ru/post/215177/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *