Но до сих пор это была «бездушная железка», которая несмотря на всю свою потенциальную мощь, заложенную в МК, — ничего не умеет.
В общем-то, наше основное устройство (если не рассматривать подключение радиомодуля) — нисколько не сложнее самой обычной Ардуинки, к которой подключено две кнопки и пара светодиодов (в результирующем устройстве — светодиоды заменены на транзисторные ключи, управляющие релюшками, но суть это не меняет).
Изготовленный модуль радиовыключателя не очень располагает к тому, чтобы прямо на нем производить разработку и отладку:
- нет возможности получить диагностические сообщения в «мониторе порта»,
- отсутствует визуальное подтверждение, какое из реле и в каком состоянии находится и т.п.
Но, как я раньше уже заметил, для «оживления» нашего модуля всего-то требуется написать скетч, который бы отрабатывал различные нажатия (две кнопки) и мог бы по нашему алгоритму включать/выключать две нагрузки (в макете это будет пара светодиодов). Естественно, это «базовый функционал», после того, как разберемся с ним — добавим и «радиоканальные» функции.
Вообще, конечно, с «макетки» правильнее было бы начать, но в данном случае — так получилось, что прототип делался позже, чем результирующее устройство.
Макет
Итак, чтобы получить «удобную» среду для подготовки нашего скетча, возьмем беспаечную макетку, любую ардуино-совместимую плату (в моем случае это cArduino Nano), две тактовые кнопки, два светодиода (с токоограничительными резисторами) и несколько перемычек:
Собираем макет, согласно принципиальной схемы из первого поста.
Напомню:
- Кнопку для первого канала подключаем между пином 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. В принципе, можно сделать конденсаторный блок питания или поискать уже готовые варианты (например, тут).
Получилось как-то так (тут уже все подключено — проводил последние тесты перед монтажом в стену):
Корпус оказался несколько великоват, но имеющийся в хозяйстве более мелкий — не подошел.
Правильнее было бы, конечно, сначала выбрать конкретный корпус и делать под него, но у меня не было особых ограничений на размер, поэтому «как получилось».
Теперь можно заняться непосредственно «встраиванием» модуля в стену (к сожалению, увлекся процессом и забыл фотографировать, поэтому только текстовое описание):
- Обесточиваем соответствующую цепь освещения.
- Демонтируем имеющийся выключатель (не забываем промаркировать, какие пары идут на свет, а какие — на вытяжку).
- Снимаем монтажную коробку
- Подключаем радиовыключатель к соответствующим проводам (попутно избавляясь от «скруток», которые оставили «добрые строители»).
- Аккуратно заталкиваем все провода и радиовыключатель в промежуток между листами гипсокартона (я решил расположить модуль выше выключателя, чтобы его было проще достать при необходимости).
- Выводим провода, к которым будем подключать кнопочный выключатель в отверстие для установки монтажной коробки (специально взял принципиально отличающийся от остальной проводки кабель — МГТФ, чтобы в случае чего электрику было понятно, что тут «что-то странное» и с этим надо сначала разобраться).
- Теперь можно установить монтажную коробку и подключить кнопочный выключатель.
Все, готово. Включаем электричество и проверяем, что все работает так, как хотелось.
Результат
Созданное устройство успешно смонтировано и отлично заменило «тупой» выключатель, добавив к нему чуточку «ума» (экономию электроэнергии в случаях «забывчивости» хозяев, автоматическое включение/выключение вытяжки и т.п.).
Продолжение следует…
P.S. В обсуждении первого поста были вопросы по поводу использования другой элементной базы, в том числе и для достижения более компактных размеров.
![](http://habrastorage.org/getpro/habr/post_images/786/45c/28f/78645c28f1a80099a1904c369506a278.jpg)
Это обычное реле (очень тихое) с двумя группами коммутируемых контактов. Может включать/выключать цепи на 220В (мощность небольшая, но для светодиодных ламп — вполне подойдет). Управляется 5В, можно подключать напрямую к выводу МК (без транзистора).
Это я к тому, что не стоит ко всему относиться как к догме (повторять все проекты «один в один») — ищите, подбирайте наиболее адекватные (для каждой конкретной задачи) решения, модифицируйте!
Полезные ссылки:
- Arduino watchdog или автоматический RESET в случае зависания
- Беспроводные коммуникации «умного дома»
- Конденсаторное питание
Спасибо Nikita_Rogatnev за помощь в подготовке материала к публикации.
ссылка на оригинал статьи http://habrahabr.ru/post/215177/
Добавить комментарий