Умная розетка. Часть 2 – Готовое устройство.

После проведения тестов GMS-модуля и анализа предыдущих результатов, у нас появились следующие идеи для улучшения функциональности умной розетки:

1. Безопасность при потере сети:
В текущей версии, если розетка включена, а модуль по какой-либо причине теряет связь с сетью, розетка остается активной. Это может привести к нежелательным последствиям. Поэтому предлагается разработать алгоритм, который будет регулярно проверять регистрацию модуля в сети. В случае потери связи, розетка будет автоматически отключаться, что повысит безопасность системы.

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

3. Гибкость управления:
Сейчас управление устройством возможно только с номера, жестко зашитого в коде. Однако было бы удобно предусмотреть возможность добавления других номеров для управления. Это сделает систему более гибкой и удобной в использовании.

4. Функция таймера:
Также стоит добавить возможность установки таймера. Например, пользователь сможет задать время работы розетки (скажем, 1 час), после чего она автоматически отключится. Это особенно полезно для экономии энергии и контроля за устройствами.

5. Информирование через СМС:
Наконец, для удобства пользователя можно реализовать функцию получения информации через СМС. Например, запросив статус, пользователь сможет узнать:

  • Какие номера имеют доступ к управлению;
  • На какое время установлен таймер;
  • Текущее состояние устройства (включено/выключено).

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

Скетч полностью

#include <SoftwareSerial.h>
#include <EEPROM.h>

SoftwareSerial mySerial(5, 6);

String phoneNumber = ""; //Для сохранения номера, от которого пришла СМС
String trustedNumber1 = ""; //Доверенный номер 1 (Получаем далее из EEPROM)
String trustedNumber2 = ""; //Доверенный номер 2 (Получаем далее из EEPROM)
String serviceNumber = "7**********"; //Доверенный номер по умолчанию
//Переменная для отсчитывания времени для периодической проверки соединения
unsigned long timerCheckConnection = 0; 
//Переменная для отсчитывания времени перезагрузки, если GSM-модуль теряет связь
unsigned long timerReset = 0;
//Переменная для отсчета времени получения ответа, при отправке команды CRAG
unsigned long timerCRAG = 0;
//Переменная для посчета времени работы
unsigned long timerWork = 0;
//Время работы установленное в данное время
unsigned long currWork = 0;
//Хранит в себе статус соединен ли сейчас GSM-модуль (нужна для отсчета времени на //перезагрузку)
bool connected = true;

//Ставится в true, если была послана команда CRAG
bool waitingCRAG = false;
//Ставится в true, если потеряна связь по UART с GSM-модулем
bool loseModule = false;

//Планировалось использовать переменную для отключения таймера работы
bool timerEnable = true;

void setup()
{
  pinMode(13, OUTPUT); //Управляющий выход на реле
  digitalWrite(13, HIGH); //Отключаем реле

  pinMode(7, OUTPUT); //Выход на диод (индикатор связи с модулем)
  pinMode(4, INPUT);  //По входу будем определять работает ли в данный момент розетка

  Serial.begin(19200);                   
  mySerial.begin(19200);  //UART для GSM -модуля              
  Serial.println("Initializing..."); 
  timerCheckConnection = millis();

  delay(5000);
  sendATCommand("AT+CMGF=1"); //Устанавливаем текстовой режим работы СМС     

  trustedNumber1 = getEEPROMPhone(0); //Получение доверенного номера 1 из памяти
  trustedNumber2 = getEEPROMPhone(11); //Получение доверенного номера 2 из памяти
  getWorkTime(); //Получаем значение таймера из памяти
}
 
void loop()
{
  updateSerial();
  checkConnection();
  checkReset();
  checkModule();
  checkWorkTime();
}
 
void updateSerial()
{
  while (Serial.available()) 
  {
    mySerial.println(Serial.readString());
  }

  while(mySerial.available()) 
  {
    String SMSText = "";
    String signalFromSIM = mySerial.readString();
    
    if(signalFromSIM.indexOf("CMGR") != -1){
      if(checkPhoneNumber(signalFromSIM)){
        SMSText = getSMSText(signalFromSIM);
        Serial.println(SMSText);
        executeSMSCommand(SMSText);
      }
      sendATCommand("AT+CMGDA=\"DEL ALL\"");

    }else if(signalFromSIM.indexOf("CMTI") != -1){
      
      String SMSNumber = "";
      SMSNumber = getSMSNumber(signalFromSIM);
      sendATCommand("AT+CMGR=" + SMSNumber);

    } else if (signalFromSIM.indexOf("CREG") != -1){
      if(loseModule){
        sendATCommand("AT+CMGF=1");
        loseModule = false;
      }
      waitingCRAG = false;
      if(!checkCREGStatus(signalFromSIM)){
        digitalWrite(13, HIGH);
        digitalWrite(7, LOW);
        if(connected) {
          timerReset = millis();
        }
        connected = false;
      } else {
        digitalWrite(7, HIGH);
        connected = true;
      }

    }else{
      Serial.println(signalFromSIM);
    }
  }
}

String getSMSNumber(String message){
  int simbolPos;
  simbolPos = message.lastIndexOf(',');
  return message.substring(simbolPos+1);
}

void sendATCommand(String command){
  mySerial.println(command);
}

String getSMSText(String allText){
  int simbolPos;
  String SMSText = "";
  simbolPos = allText.lastIndexOf('"');
  SMSText = allText.substring(simbolPos+1);

  SMSText = SMSText.substring(SMSText.indexOf('\n') + 1);
  SMSText = SMSText.substring(0, SMSText.indexOf('\r'));
  return SMSText;
}

void executeSMSCommand(String SMSText){
  if(SMSText == "1"){
    digitalWrite(13, LOW);
    currWork = timerWork + millis();
    Serial.println(currWork);
  } else if(SMSText == "0") {
    digitalWrite(13, HIGH);
  } else if(SMSText == "2"){
    sendInfo();
  } else if(SMSText.indexOf("N=") != -1){
    String number = "";
    String memoryPosition = SMSText.substring(0, 1);
    number = numberFromSMS(SMSText);
    if(number != ""){
      if(memoryPosition == "1"){
        setEEPROMNumberWrite(number,0);
        trustedNumber1 = getEEPROMPhone(0);
      } 
      else{
        setEEPROMNumberWrite(number,11);
        trustedNumber2 = getEEPROMPhone(11); 
      }
    }
  } else if(SMSText.indexOf("T=") != -1){
      setWorkTime(SMSText);
      getWorkTime();
      Serial.println(timerWork);
  }
}

bool checkPhoneNumber(String SMSText){
  int simbolPos;
  simbolPos = SMSText.indexOf('+', 20);
  phoneNumber = SMSText.substring(simbolPos+1, SMSText.indexOf('"', simbolPos));
  return (phoneNumber == trustedNumber1 || phoneNumber == trustedNumber2 || phoneNumber == serviceNumber);
}

bool checkCREGStatus(String SMSText){
  int simbolPos;
  simbolPos = SMSText.indexOf(',');
  if (SMSText.substring(simbolPos+1, simbolPos+2) == "1"){
    return true;
  }
  return false;
}

void sendInfo(){
  String text = "";
  if(digitalRead(4)){
    text ="Status=off\n";
  }else{
    text ="Status=on\n";
  }
  sendSMS(text + "1N="+ trustedNumber1 + "\n2N="+ trustedNumber2 + "\nTimer=" + String(timerWork/60/1000));
}

String getEEPROMPhone(int seek){
  String tempTrustedNumber = "";
  tempTrustedNumber += (char)EEPROM.read(4 + seek);
  tempTrustedNumber += (char)EEPROM.read(5 + seek);
  tempTrustedNumber += (char)EEPROM.read(6 + seek);
  tempTrustedNumber += (char)EEPROM.read(7 + seek);
  tempTrustedNumber += (char)EEPROM.read(8 + seek);
  tempTrustedNumber += (char)EEPROM.read(9 + seek);
  tempTrustedNumber += (char)EEPROM.read(10 + seek);
  tempTrustedNumber += (char)EEPROM.read(11 + seek);
  tempTrustedNumber += (char)EEPROM.read(12 + seek);
  tempTrustedNumber += (char)EEPROM.read(13 + seek);
  tempTrustedNumber += (char)EEPROM.read(14 + seek);
  return tempTrustedNumber;
}

void setEEPROMNumberWrite(String newPhoneNumber, int seek){
  EEPROM.write(4 + seek,newPhoneNumber[0]);
  EEPROM.write(5 + seek,newPhoneNumber[1]);
  EEPROM.write(6 + seek,newPhoneNumber[2]);
  EEPROM.write(7 + seek,newPhoneNumber[3]);
  EEPROM.write(8 + seek,newPhoneNumber[4]);
  EEPROM.write(9 + seek,newPhoneNumber[5]);
  EEPROM.write(10 + seek,newPhoneNumber[6]);
  EEPROM.write(11 + seek,newPhoneNumber[7]);
  EEPROM.write(12 + seek,newPhoneNumber[8]);
  EEPROM.write(13 + seek,newPhoneNumber[9]);
  EEPROM.write(14 + seek,newPhoneNumber[10]);
}

String numberFromSMS(String text){
  String newNumber = "";
  if(text.indexOf("N=")==1){
    newNumber = text.substring(3);
    return newNumber;
  }
  return "";
}

void resetModule(){
  sendATCommand("AT+CFUN=1,1");
  delay(5000);
  sendATCommand("AT+CMGF=1");
}

unsigned long getTimeInterval(unsigned long timer, unsigned long currTime){
  if(currTime > timer){
    return currTime - timer;
  }
}

void checkConnection(){
  if(millis() - timerCheckConnection > 15000){
    waitingCRAG = true;
    timerCRAG = millis();
    sendATCommand("AT+CREG?");
    timerCheckConnection = millis();
  }
}

void checkReset(){
  if((!connected) && ((millis() - timerReset) > 60000)){
    timerReset = millis();
    resetModule(); 
  }
}

void checkModule(){
  if(waitingCRAG && ((millis() - timerCRAG) > 5000)){
    digitalWrite(13, HIGH);
    digitalWrite(7, LOW);
    waitingCRAG = false;
    loseModule = true;
  }
}

void checkWorkTime(){
  if(timerEnable && millis()>currWork && !digitalRead(4)){
    digitalWrite(13, HIGH);
  }
}

void sendSMS(String text){
  sendATCommand("AT+CMGS=\"+"+phoneNumber+"\"");
  delay(100);
  sendATCommand(text);
  delay(100);
  mySerial.println((char)26); 
  delay(100);
  mySerial.println();
  delay(4000);
}

void getWorkTime(){
  String txt = "";
  txt += (char)EEPROM.read(1);
  txt += (char)EEPROM.read(2);
  txt += (char)EEPROM.read(3);
  timerWork = txt.toInt() * 60 * 1000;
  Serial.println(timerWork);
}

void setWorkTime(String text){
  Serial.println(text);
  String time = text.substring(2,5);
  Serial.println(time);
  EEPROM.write(1,time[0]);
  EEPROM.write(2,time[1]);
  EEPROM.write(3,time[2]);
}

Область объявления переменных подписана комментариями. Роль отдельных функций рассмотрим далее.

Описание функций

Основные

В функции setup() создаем собственный Serial порт с помощью библиотеки SerialSoftware, устанавливаем роль цифровых входов/выходов, выключаем реле и устанавливаем значение необходимых переменных. Получаем доверенные номера и значение таймера из памяти Arduino.

В loop() прописаны 5 основных функций:

updateSerial(). Функция проверяет команды, приходящие от Arduino и от GSM-модуля. Это основная функция, в не проверяется факт прихода СМС, ответы на все команды и .т.д.

Контроль соединения

checkConnection(). Функция, которая каждые 15 секунд отправляет на GSM-модуль команду CRAG. После каждой отправки начинает отсчет (5 секунд) ожидания ответа на команду. В updateSerial – проверяется пришел ли ответ на команду, и каким он был. Если ответ показал, что модуль не в сети – специальная переменная connected устанавливается в false.

checkReset(). Если переменная connected установлена в false более 60 секунд (отслеживается с помощью переменной timerReset) (модуль не получил регистрации в сети), GSM-модуль перезагружается, таймер перезагрузки сбрасывается

checkModule(). После отправки на GSM-модуль команды CRAG, переменная waitingCRAG устанавливается в true и начинается отсчет времени. Если в течении 5 секунд не пришел ответ на команду. Считаем, что модуль отвалился и отключаем розетку.

checkWorkTime(). Функция проверяет, сколько розетка находится во включенном состоянии и отключает ее, если пришло время.

Большая часть всех проверок и функционала находится в updateSerial().

На каждой итерации, входящие сообщения проверяются на команды: ответ CRAG и приход СМС (CMTI)

executeCommand(…) проверяет команду, пришедшую в СМС и выполняет ее.

Может быть 5 команд:

1 – включить розетку;

0 – выключить розетку;

2 – статус устройства. В ответ на эту команду приходит СМС вида:

Status=<on/off>
1
N=<номер для управления 1>
2
N=<номер для управления 2>
Timer=<Таймер работы устройства> 

T=<таймер работы устройства в min> – устанавливает таймер для работы в минутах, минимум = 0, максимум = 999 (изначально таймер =0, поэтому, перед использованием нужно выставить нужное время)

<номер в памяти 1 или 2>N=<номер для управления> – устанавливает доверенный номер для управления. (Например, 1N=79999996655. Первая цифра означает номер в памяти, доступно слота для номеров).

Для отслеживания включено устройство или нет, вывод 13 (вывод на реле), соединен с выводом 4. По выводу 4 определяется замкнуто ли реле в данный момент.

Дополнительные

sendInfo() функция формирует ответ на запрос информации (Команда «2»).

Для установки доверенных номеров и таймера используется внутренняя память устройства. Запись производится с помощью библиотеки EEPROM.h. Первые три символа памяти используются под таймер (максимальное время работы 999 минут). Далее 22 символа под доверенные номера для управления (11 символов номера, начиная с «7»).

Установка номера производится при получении GSM-модулем смс начинающуюся на номер в памяти и буквы N. Далее вызывается функция для записи номера setEEPROMNumberWrite(String newPhoneNumber, int seek).

Первый параметр – это записываемый номер, второй – сдвиг в памяти. Если запись производится в ячейку 1 – то сдвиг передается 0, если 2- то 11.

При загрузке устройства (или при переустановке номера), номера читаются из памяти с помощью функции getEEPROMPhone(int seek),

Параметр seek отвечает за сдвиг в памяти. Запись производится в переменные trustedNumber1 и trustedNumber2

Установка таймера Производится с помощью СМС с текстом T=<время работы в минутах>. При получении СМС подобного формата вызывается функция setWorkTime(String text) Она записывает время в первые 3 ячейки памяти. Время следует отправлять со всеми заполненными разрядами. Например, если нужно установить время 10 минут- то следует выслать смс «T=010», если 1 минуту – то «T=001».

Время работы считывается при запуске розетки, и при переустановке времени по СМС с помощью функции getWorkTime() в переменную timerWork.

Это краткое описание работы алгоритма. Возможно в нем есть некоторые огрехи, которые, возможно, будут в дальнейшем исправляться. В любом случае, актуальный алгоритм будет доступен на GitHub. Прошу строго не судить. Также готов выслушать все комментарии.

Аппаратная часть

Для автономной работы был приобретен еще преобразователь 220 в 9 вольт, чтобы все работало от бытовой сети.

Итоговая схема выглядит следующим образом:

Схема 1
Рисунок 1. Итоговая схема.

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

Управляющая плата
Рисунок 2. Плата с основными элементами (лицевая сторона)
Управляющая плата
Рисунок 3. Плата с основными компонентами (оборотная сторона)

В первой статье я писал, что планирую заменить Arduino NANO на digispark ATtiny85. Но в процессе разработки алгоритма скетч разросся до 10 Кб, что уже многовато для ATtiny 85 (в нем только 8 Кб памяти). Тем более, алгоритм, возможно, будет разрастаться

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

Готовая розетка
Рисунок 4. Готовое устройство (крышка открыта)
Готовая розетка
Рисунок 5. Готовое устройство в работе.

Синий синий диод горит, когда GSM-модуль в сети, и ардуино имеет связь с модулем.

На этом считаю свой небольшой проект законченным. Это мой первый опыт сборки и программирования микроконтроллеров. Далее планирую продолжить создавать подобные устройства. 

Статьи по проекту

Умная розетка. Часть 1 – Тесты.

2 комментария к “Умная розетка. Часть 2 – Готовое устройство.”

Оставьте комментарий