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

в 12:47, , рубрики: arduino, Atmega, diy или сделай сам, nRF24L01+, микроконтроллеры avr, Программинг микроконтроллеров, программирование микроконтроллеров, своими руками, умный дом, метки: , , , , , ,

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

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

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

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

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

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

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

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

Макет

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

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

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

Напомню:

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

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

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

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

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

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

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

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

Результат

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

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

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

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

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

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

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

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

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

Автор: avstepanov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js