Делаем Smart Point или «Интернет-вещь» своими руками

в 9:29, , рубрики: Без рубрики

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

image

Заинтересовашихся прошу под кат.

Вместо введения

В последнее время наблюдается ярко выраженная тенденция роста интереса к такой области информационных технологий, как автоматизация жизнедеятельности. Автоматизация сама по себе явление далеко не новое и уже десятки лет для большинства промышленных производств является не прихотью, а необходимостью, без которой просто немыслимо выживание бизнеса в условиях жёсткой конкуренции. Так почему же только сейчас мы так много слышим про Интернет Вещей (Internet of Things), M2M (Machine-to-machine) коммуникации и прочие “умные” технологии? Возможно, причиной является то, что, как и во многих подобных случаях, была набрана некая “критическая масса” инноваций в купе с доступностью элементной базы для широкой публики. Так же, как когда-то развитие Интернета и доступность интернет-технологий породило целую волну информационных проектов, меняющих мир до сих пор, так и сейчас мы становимся свидетелями того, как из таких “кирпичиков” как программирование, микро-электроника, Интернет создаётся множество интересных бытовых решений. Далеко не все из них “взлетят” и это абсолютно нормально, но многие из них могут быть основой (или вдохновением) для чего-то действительно потрясающего.

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

Имея в “багаже” проект платформы Умного Дома, я задумался о том, что хоть он и является очень гибким в применении, но большое количество возможностей требует соответствующего оборудования, что не всегда удобно и практично. Для каких-то задач “малой” автоматизации можно обойтись и одним микроконтроллером, но здесь уже теряем в гибкости и повышаем требования к квалификации пользователя. Для меня показалось очевидным, что есть необходимость в неком промежуточном варианте – достаточно компактном и энерго-эффективном, но при этом гибком в настройке и использовании. Дадим рабочее название этому варианту “Умная Точка” или SmartPoint. Попутно сформировался целый список пожеланий по возможностям, которые было бы здорово в этом устройстве получить.

Задача

Итак, от лирики к практике. Вот основные требования к устройству SmartPoint:

  • Гибкая система правил для реакции на события от сенсоров
  • Веб-интерфейс для “ручного” управления
  • HTTP API для интеграции в более сложный комплекс
  • Работа ONLINE – доступ к веб-интерфейсу устройства через Интернет без статического IP и “проброса” портов на маршрутизаторе
  • Работа OFFLINE – функционирование настроенного устройства не должно зависеть от наличия доступа в Интернет

Дополнительные (практические) пожелания для устройства:

  • Работа по WiFi
  • Наличие встроенных сенсоров и исполнительных модулей (устройство должно иметь практическую пользу сразу “из коробки”, а не “в теории”)
  • Беспроводной “локальный” интерфейс для взаимодействия с более простыми датчиками/исполнительными модулями
  • Интернет-сервис (личный кабинет) для настройки и мониторинга работы устройства
Контроллер, хост, периферия

Обдумывая снова и снова концепцию, а так же немалый набор “хотелок” пришёл к выводу, что одним микроконтроллером обойтись не получится. Во-первых, я всё-таки не настолько хорошо умею их программировать, чтобы на низком уровне реализовать всё задуманное, а во-вторых, далеко не всякий контроллер вынесет такой аппетит пожеланий. Было решено пойти по пути наименьшего сопротивления – разделить устройство на две логические части: одна (“контроллер”) будет на базе микроконтроллера и отвечать за элементарное взаимодействие с “железом”, а вторая (“хост”) на базе встроенного Linux, отвечать за более высокий уровень (интерфейс, система правил, API). В качестве первого блока был выбран (угадайте!) микроконтроллер Arduino, а в качестве второго блока в дело пошёл роутер TP-Link WR703N с прошивкой OpenWRT (заметка: было успешно собрано пара аналогичных устройств на роутере DLink Dir-320). Предвидя праведный гнев, спешу напомнить, что задача у нас в первую очередь проверить на прототипе жизнеспособность концепции, а не спроектировать и собрать коммерческое устройство. Кроме того, использование данных компонентов облегчает повторение устройства — да здравствует open-source! Использование же Arduino позволяет применить опыт подключения бесконечного разнообразия датчиков и исполнительных модулей к нашему устройству.

Роутер TP-Link WR703N:

image

Микроконтроллер Arduino Nano:

image

В качестве первоначального набора периферии были выбраны следующие элементы:

  • Кнопка image
  • Датчик движения image
  • Датчик температуры DS18B20 image
  • Приёмник 433Mhz image
  • Передатчик Noolite для управления светом image

Набор периферии, как вы понимаете, может быть другим, но в данном примере я взял именно этот исходя из упомянутого выше принципа “практической полезности”. Таким образом, устройство у нас сможет реагировать на нажатие кнопки, на движение, на изменение температуры, а так же принимать данные от внешних датчиков (в данном случае использовался описанный ранее на хабре протокол) и управлять силовыми модулями системы Noolite (про модуль управления отдельная история и на фотографии не коммерческий экземпляр модуля, а один из ранних прототипов от производителя, попавший ко мне на испытания).

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

image

Пояснения к схеме:

  • Устройство состоит из микроконтроллера, взаимодействующего с проводной/беспроводной периферией, и ядра, отвечающего за логику обработки входящих данных и интерфейсы
  • Имеется API и веб-интерфейс для приёма команд от внешних “терминалов” (компьютеры, телефоны и т.п.)
  • Устройство на связи с внешним сервисом для загрузки правил, отправки уведомлений и приёма команд
Подготовка микроконтроллера

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

Ниже приведён текст скетча с учётом специфики перечисленной выше периферии. В нашем случае кнопка подключена на PIN4, датчик движения на PIN3, датчик температуры на PIN9, радиоприёмник на PIN8 и модуль Noolite на PIN-ы 10, 11.

Скетч для контроллера

#include <OneWire.h>
#include <DallasTemperature.h>
#include <VirtualWire.h>
#include <EasyTransferVirtualWire.h>
#include <EEPROM.h> //Needed to access the eeprom read write functions
#include <SoftwareSerial.h>

#define PIN_LED (13) // INDICATOR
#define PIN_PIR (3) // BUTTON
#define PIN_BUTTON (4) // BUTTON
#define PIN_LED_R (6) // INDICATOR RED
#define PIN_LED_G (5) // INDICATOR GREEN
#define PIN_LED_B (7) // INDICATOR BLUE
#define PIN_RF_RECEIVE (8) // EASYRF RECEIVER
#define PIN_TEMP (9) // TEMPERATURE SENSOR
#define PIN_NOO_RX (10) // RX PIN (connect to TX on noolite controller)
#define PIN_NOO_TX (11) // TX PIN (connect to RX on noolite controller)
#define TEMP_ACC (0.3) // temperature accuracy
#define PERIOD_READ_TEMP (20) // seconds
#define PERIOD_SEND_TEMP (600) // seconds (10 minutes)
#define PERIOD_SEND_UPTIME (300) // seconds (5 minutes)

#define NOO_BUF_LEN (12)


unsigned int unique_device_id = 0;

long int uptime = 0;
long int old_uptime = 0;
float sent_temperature=0;
int sent_pir=0;
int sent_button=0;
int sent_button_longlick=0;
long int timeCheckedTemp=0;
long int timeSentTemp=0;
long int timeSentUptime=0;
long int timeButtonPressed=0;

String inData;


//create objects
SoftwareSerial mySerial(PIN_NOO_RX, PIN_NOO_TX); // RX, TX
OneWire oneWire(PIN_TEMP);
DallasTemperature sensors(&oneWire);
EasyTransferVirtualWire ET; 

unsigned int last_packet_id = 0;

struct SEND_DATA_STRUCTURE{
  //put your variable definitions here for the data you want to send
  //THIS MUST BE EXACTLY THE SAME ON THE OTHER ARDUINO
  //Struct can'e be bigger then 26 bytes for VirtualWire version
  unsigned int device_id;
  unsigned int destination_id;  
  unsigned int packet_id;
  byte command;
  int data;
};

//give a name to the group of data
SEND_DATA_STRUCTURE mydata;

//This function will write a 2 byte integer to the eeprom at the specified address and address + 1
void EEPROMWriteInt(int p_address, unsigned int p_value)
      {
      byte lowByte = ((p_value >> 0) & 0xFF);
      byte highByte = ((p_value >> 8) & 0xFF);

      EEPROM.write(p_address, lowByte);
      EEPROM.write(p_address + 1, highByte);
      }

//This function will read a 2 byte integer from the eeprom at the specified address and address + 1
unsigned int EEPROMReadInt(int p_address)
      {
      byte lowByte = EEPROM.read(p_address);
      byte highByte = EEPROM.read(p_address + 1);

      return ((lowByte << 0) & 0xFF) + ((highByte << 8) & 0xFF00);
      }

void nooSend(byte channel, byte buf[NOO_BUF_LEN]) {
 buf[0]=85;
 buf[1]=B01010000; //
 buf[4]=0;
 buf[5]=channel;
 buf[9]=0;
 int checkSum;
 for(byte i=0;i<(NOO_BUF_LEN-2);i++) {
  checkSum+=buf[i];
 }
 buf[10]=lowByte(checkSum);
 buf[11]=170; 
 Serial.print("Sending: ");
 for(byte i=0;i<(NOO_BUF_LEN);i++) {
  Serial.print(buf[i]);
  if (i!=(NOO_BUF_LEN-1)) {  Serial.print('-'); }
 } 
 Serial.println("");
 for(byte i=0;i<(NOO_BUF_LEN);i++) {
  mySerial.write(buf[i]);
 } 
}

void noolitePair(byte channel) {
byte buf[NOO_BUF_LEN];
  for(byte i=0;i<(NOO_BUF_LEN);i++) {
   buf[i]=0;
  }
  buf[2]=15;
  buf[3]=0;
  nooSend(channel,buf);
}

void nooliteUnPair(byte channel) {
byte buf[NOO_BUF_LEN];
  for(byte i=0;i<(NOO_BUF_LEN);i++) {
   buf[i]=0;
  }
  buf[2]=9;
  buf[3]=0;
  nooSend(channel,buf);
}

void nooliteTurnOn(byte channel) {
byte buf[NOO_BUF_LEN];
  for(byte i=0;i<(NOO_BUF_LEN);i++) {
   buf[i]=0;
  }
  buf[2]=2;
  buf[3]=0;
  nooSend(channel,buf);
}

void nooliteTurnOff(byte channel) {
byte buf[NOO_BUF_LEN];
  for(byte i=0;i<(NOO_BUF_LEN);i++) {
   buf[i]=0;
  }
  buf[2]=0;
  buf[3]=0;
  nooSend(channel,buf);  
}

void nooliteSwitch(byte channel) {
byte buf[NOO_BUF_LEN];
  for(byte i=0;i<(NOO_BUF_LEN);i++) {
   buf[i]=0;
  }
  buf[2]=4;
  buf[3]=0;
  nooSend(channel,buf);  
}

void nooliteLevel(byte channel,byte level) {
byte buf[NOO_BUF_LEN];
  for(byte i=0;i<(NOO_BUF_LEN);i++) {
   buf[i]=0;
  }
  buf[2]=6;
  buf[3]=1;
  buf[6]=level;
  nooSend(channel,buf);  
}


void blinking(int count) {
 for(int i=0;i<count;i++) {
  digitalWrite(PIN_LED, HIGH); 
  delay(200);
  digitalWrite(PIN_LED, LOW);
  delay(200);
 }
}

void setColor(int r,int g, int b) {
 digitalWrite(PIN_LED_R, r); 
 digitalWrite(PIN_LED_G, g);  
 digitalWrite(PIN_LED_B, b);   
}

void setup()
{
    randomSeed(analogRead(0));
    pinMode(PIN_LED, OUTPUT);
    pinMode(PIN_LED_R, OUTPUT);    
    pinMode(PIN_LED_G, OUTPUT);        
    pinMode(PIN_LED_B, OUTPUT);            
    pinMode(PIN_PIR, INPUT);       
    pinMode(PIN_BUTTON, INPUT);       
    
    Serial.begin(9600); // Debugging only


    ET.begin(details(mydata));
    // Initialise the IO and ISR
    vw_set_rx_pin(PIN_RF_RECEIVE);
    vw_setup(2000);      // Bits per sec
    vw_rx_start();       // Start the receiver PLL running
    
  // Device ID
  Serial.print("Getting Device ID... "); 
  unique_device_id=EEPROMReadInt(0);
  if (unique_device_id<10000 || unique_device_id>60000 || unique_device_id==26807) {
   Serial.print("N/A, updating... "); 
   unique_device_id=random(10000, 60000);
   EEPROMWriteInt(0, unique_device_id);
  }
  Serial.println(unique_device_id);
  
  pinMode(PIN_NOO_RX, INPUT);
  pinMode(PIN_NOO_TX, OUTPUT);  
  mySerial.begin(9600);  
  
}

void loop()
{
  uptime=round(millis()/1000);
  if (uptime!=old_uptime) {
    Serial.print("Up: ");
    Serial.println(uptime);
    old_uptime=uptime;
    if (((uptime-timeSentUptime)>PERIOD_SEND_UPTIME) || (timeSentUptime>uptime)) {    
      timeSentUptime=uptime;
      
         Serial.print("P:");
         Serial.print(random(65535));
         Serial.print(";F:");
         Serial.print("0");
         Serial.print(";T:0;C:");         
         Serial.print("24");
         Serial.print(";D:");
         Serial.print(uptime);         
         Serial.println(";");      
    }
  }
  
  int current_pir=digitalRead(PIN_PIR);
  if (current_pir!=sent_pir)  {   
    Serial.print(millis()/1000);
    Serial.print(" Motion sensor: ");
    Serial.println(current_pir); 
     
         Serial.print("P:");
         Serial.print(random(65535));
         Serial.print(";F:");
         Serial.print("0");
         Serial.print(";T:0;C:");         
         Serial.print("12");
         Serial.print(";D:");
         Serial.print("1");         
         Serial.println(";");
         
    sent_pir=(int)current_pir;
  }   
  
  int current_button=digitalRead(PIN_BUTTON);
  if (current_button!=sent_button)  {   
    delay(50);
    int confirm_current_button=digitalRead(PIN_BUTTON);
    if (confirm_current_button==current_button) {

     if (current_button==1) {
       timeButtonPressed=millis();
       sent_button_longlick=0;
     } 
     
     if (current_button==0) {
       if (sent_button_longlick!=1) {
        Serial.print(millis()/1000);
        Serial.print(" Button press: ");
        Serial.println(current_button); 
        
         Serial.print("P:");
         Serial.print(random(65535));
         Serial.print(";F:");
         Serial.print("0");
         Serial.print(";T:0;C:");         
         Serial.print("23");
         Serial.print(";D:");
         Serial.print("3");         
         Serial.println(";");
         
       }
     }
     sent_button=(int)current_button;
    }
  } else {
    if (current_button==1) {
      int passed=millis()-timeButtonPressed;
      if ((passed>3000) && (sent_button_longlick!=1)) {
        sent_button_longlick=1; 
        Serial.print(millis()/1000);
        Serial.print(" Button long press: ");
        Serial.println(current_button);        
        
         Serial.print("P:");
         Serial.print(random(65535));
         Serial.print(";F:");
         Serial.print("0");
         Serial.print(";T:0;C:");         
         Serial.print("23");
         Serial.print(";D:");
         Serial.print("4");         
         Serial.println(";");        
        
      }
    } else {
      sent_button_longlick=0;
    }
  }
  
 if (((uptime-timeCheckedTemp)>PERIOD_READ_TEMP) || (timeCheckedTemp>uptime)) {
  // TEMP SENSOR 1
  float current_temp=0;
  sensors.requestTemperatures();
  current_temp=sensors.getTempCByIndex(0);
  if (current_temp>-100 && current_temp<50) {
   timeCheckedTemp=uptime;
   Serial.print("Temp sensor: "); 
   Serial.println(current_temp);
   float diff=(float)sent_temperature-(float)current_temp;
   if ((abs(diff)>=TEMP_ACC) || ((uptime-timeSentTemp)>PERIOD_SEND_TEMP)) {
    // 
    timeSentTemp=uptime;   
    sent_temperature=(float)current_temp;   
    
         Serial.print("P:");
         Serial.print(random(65535));
         Serial.print(";F:");
         Serial.print("0");
         Serial.print(";T:0;C:");         
         Serial.print("10");
         Serial.print(";D:");
         Serial.print((int)(current_temp*100));         
         Serial.println(";");    
    
   }
  } else {
   //Serial.print("Incorrect T: ");
   //Serial.println(current_temp);
  }
 }  
  
  
  if (Serial.available()) {
    char c=Serial.read();
    if (c == 'n' || c == ';')
        {
          Serial.println(inData);
          int commandProcessed=0;
          if (inData.equals("blink")) {
           Serial.println("BLINKING!");
           blinking(3);
           commandProcessed=1;            
          } 
          if (inData.startsWith("pair")) {
            commandProcessed=1;            
            inData.replace("pair","");
            noolitePair(inData.toInt());
          }
          if (inData.startsWith("on")) {
            commandProcessed=1;            
            inData.replace("on","");
            nooliteTurnOn(inData.toInt());
          }
          if (inData.startsWith("off")) {
            commandProcessed=1;            
            inData.replace("off","");
            nooliteTurnOff(inData.toInt());
          }           
          if (inData.startsWith("switch")) {
            commandProcessed=1;            
            inData.replace("switch","");
            nooliteSwitch(inData.toInt());
          }
          if (inData.startsWith("level")) {
            commandProcessed=1;            
            inData.replace("level","");
            int splitPosition;
            splitPosition=inData.indexOf('-');
            if(splitPosition != -1) {
              String paramString=inData.substring(0,splitPosition);
              int channel=paramString.toInt();
              inData=inData.substring(splitPosition+1,inData.length());
              nooliteLevel(channel,inData.toInt());
            }
            
          }          
          if (inData.startsWith("unpair")) {
            commandProcessed=1;            
            inData.replace("unpair","");
            nooliteUnPair(inData.toInt());
          }                      
          if (inData.startsWith("color-")) {
            commandProcessed=1;            
            inData.replace("color-","");
            if (inData.equalsIgnoreCase("r")) {
              setColor(255,0,0);
            }
            if (inData.equalsIgnoreCase("g")) {
              setColor(0,255,0);
            }            
            if (inData.equalsIgnoreCase("b")) {
              setColor(0,0,255);
            }            
            if (inData.equalsIgnoreCase("w")) {
              setColor(255,255,255);
            }
            if (inData.equalsIgnoreCase("off")) {
              setColor(0,0,0);
            }            
          }                     
          if (commandProcessed==0) {
            Serial.print("Unknown command: ");
            Serial.println(inData);
          }                  
          inData="";
          Serial.flush();
        } else {
          inData += (c);
        }    
  }    
  
    if(ET.receiveData())
    {
        digitalWrite(PIN_LED, HIGH);
        if (last_packet_id!=(int)mydata.packet_id) {
         Serial.print("P:");
         Serial.print(mydata.packet_id);
         Serial.print(";F:");        
         Serial.print(mydata.device_id);
         Serial.print(";T:");                
         Serial.print(mydata.destination_id);        
         Serial.print(";C:");
         Serial.print(mydata.command);
         Serial.print(";D:");
         Serial.print(mydata.data);
         Serial.println(";");
         last_packet_id=(int)mydata.packet_id;
        }
        digitalWrite(PIN_LED, LOW);             
    }
    
  if (mySerial.available())
    Serial.write(mySerial.read());    
    
   
    
}

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

Подготовка хост-модуля (роутера)

Очень подробно останавливаться на прошивке роутера системой OpenWRT и последующей настройке в рамках данной статьи я не буду, а лучше дам ссылку на более полную инструкцию. В итоге у нас должен быть роутер в режиме клиента локальной WiFi-сети с выходом в интернет, а так же корректно определяющий подключенный микроконтроллер в качестве COM-порта.

Следующий шаг это трансформация нашего роутера в хост-модуль. Я использовал интерпретатор Bash для написания скриптов хост-модуля, т.к. мне показался он достаточно удобным и универсальным, т.е. не привязывающим платформу хост-модуля к какой-то определённой “железной” реализации – вместо роутера с OpenWRT может быть любое устройство со встроенным Linux-ом, лишь бы был Bash и драйверы для подключения микроконтроллера.

Алгоритм работы хост-модуля можно представить следующими пунктами:

  1. Инициализация – загрузка правил работы данного устройства из внешнего веб-сервиса (при его доступности), а так же установка канала связи с микроконтроллером
  2. Приём данных от контроллера и обработка их в соответствии с загруженными правилами

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

Файл настроек (/ect/master/settings.sh)

MASTER_ID="AAAA-BBBB-CCCC-DDDD"
ARDUINO_PORT=/dev/ttyACM0
ARDUINO_PORT_SPEED=9600
UPDATES_URL="http://connect.smartliving.ru/rules/"
DATA_PATH="/etc/master/data"
WEB_PATH="/www"
ONLINE_CHECK_HOST="8.8.8.8"
LOCAL_BASE_URL="http://connect.dev"

Файл основного скрипта обработки (/etc/master/cycle.sh)

#!/bin/bash

# settings
. /etc/master/settings.sh

# STEP 0
# wait to be online
COUNTER=0
while [ $COUNTER -lt 5 ]; do
ping -c 1 $ONLINE_CHECK_HOST
if [[ $? = 0 ]];
then
echo Network available.
break;
else
echo Network not available. Waiting...
sleep 5
fi
let COUNTER=COUNTER+1
done

#---------------------------------------------------------------------------
# START

if [ ! -d "$DATA_PATH" ]; then
  mkdir $DATA_PATH
  chmod 0666 $DATA_PATH
fi

while : 
do

#---------------------------------------------------------------------------
# Downloading the latest rules from the web
echo Getting rules from $UPDATES_URL?id=$MASTER_ID
wget -O $DATA_PATH/rules_set.tmp  $UPDATES_URL?id=$MASTER_ID
if grep -Fq "Rules set" $DATA_PATH/rules_set.tmp
then
mv $DATA_PATH/rules_set.tmp $DATA_PATH/rules_set.sh
else
echo Incorrect rules file
fi

#---------------------------------------------------------------------------

# Reading all data and sending to the web
ALL_DATA_FILE=$DATA_PATH/all_data.txt
rm -f $ALL_DATA_FILE
echo -n id=$MASTER_ID>>$ALL_DATA_FILE
echo -n "&data=">>$ALL_DATA_FILE
FILES=$DATA_PATH/*.dat
for f in $FILES
do
#echo "Processing $f file..."
OLD_DATA=`cat $f`
fname=${f##*/}
PARAM=${fname/.dat/}
echo -n "$PARAM|$OLD_DATA;">>$ALL_DATA_FILE
done
ALL_DATA=`cat $ALL_DATA_FILE`
echo Posting: $UPDATES_URL?$ALL_DATA
wget -O $DATA_PATH/data_post.tmp $UPDATES_URL?$ALL_DATA
rm -f $DATA_PATH/*.dat
#---------------------------------------------------------------------------

# Downloading the latest menu from the web
echo Getting menu from $UPDATES_URL/menu2.php?download=1&id=$MASTER_ID
wget -O $DATA_PATH/menu.tmp  $UPDATES_URL/menu2.php?download=1&id=$MASTER_ID
if grep -Fq "stylesheet" $DATA_PATH/menu.tmp
then
mv $DATA_PATH/menu.tmp $WEB_PATH/menu.html
else
echo Incorrect menu file
fi
#---------------------------------------------------------------------------

START_TIME="$(date +%s)"
# main cycle
stty -F $ARDUINO_PORT ispeed $ARDUINO_PORT_SPEED ospeed $ARDUINO_PORT_SPEED cs8 ignbrk -brkint -imaxbel -opost -onlcr -isig -icanon -iexten -echo -echoe -echok -echoctl -echoke noflsh -ixon -crtscts

#---------------------------------------------------------------------------
while read LINE; do

echo $LINE

PASSED_TIME="$(($(date +%s)-START_TIME))"

# Processing incoming URLs from controller
REGEX='^GET (.+)$'
if [[ $LINE =~ $REGEX ]]
then
URL=$LOCAL_BASE_URL${BASH_REMATCH[1]}
#-URL=$LOCAL_BASE_URL
wget -O $DATA_PATH/http.tmp $URL
echo Getting URL
echo $URL
fi

PACKET_ID=""
DATA_FROM=""
DATA_TO=""
DATA_COMMAND=""
DATA_VALUE=""

REGEX='^P:([0-9]+);F:([0-9]+);T:([0-9]+);C:([0-9]+);D:([0-9]+);$'

if [[ $LINE =~ $REGEX ]]
then
PACKET_ID=${BASH_REMATCH[1]}
DATA_FROM=${BASH_REMATCH[2]}
DATA_TO=${BASH_REMATCH[3]}
DATA_COMMAND=${BASH_REMATCH[4]}
DATA_VALUE=${BASH_REMATCH[5]}
DATA_FILE=$DATA_PATH/$DATA_FROM-$DATA_COMMAND.dat
echo -n $DATA_VALUE>$DATA_FILE
fi

if [ -f $DATA_PATH/incoming_data.txt ];
then
 echo "New incoming data:";
 echo `cat $DATA_PATH/incoming_data.txt`
 cat $DATA_PATH/incoming_data.txt>$ARDUINO_PORT
 rm -f $DATA_PATH/incoming_data.txt
fi

ACTION_RECEIVED=""
if [ -f $DATA_PATH/incoming_action.txt ];
then
 ACTION_RECEIVED=`cat $DATA_PATH/incoming_action.txt`
 echo "New incoming action: $ACTION_RECEIVED"
 rm -f $DATA_PATH/incoming_action.txt
fi


. $DATA_PATH/rules_set.sh

if [ -f $DATA_PATH/reboot ];
then
echo "REBOOT FLAG"
rm -f $DATA_PATH/reboot
break;
fi
done < $ARDUINO_PORT
done
#---------------------------------------------------------------------------
echo Cycle stopped.

В настройках можно видеть, что у устройства есть уникальный идентификатор (MASTER_ID), который используется для взаимодействия с веб-сервисом (напомню, что наличие постоянного соединения с ним не обязательно).

В ходе работы основного скрипта используется каталог /etc/master/data/ для хранения загруженного кода правил, значений последних показаний датчиков, а так же для работы некоторых конструкций системы правил (например, таймеров).

Полный набор файлов можно загрузить по данной ссылке.

Система правил

О системе правил было в общих чертах сказано выше, так что здесь остановлюсь на ней немного подробнее. Фактически, каждое правило представляет собой набор bash-инструкций. Первая часть этого набора, назовём её Активатор, проверяет входящие данные на предмет соответствия данному правилу, а вторая часть (Исполнитель) непосредственно исполняет какие-то действия.

Возможные условия активации правила:

  • Получение строки определённого формата от микроконтроллера
  • Получение команды определённого формата от внутреннего (кнопка, движение, температура) либо внешнего (беспроводного) датчика
  • “Ручная” активации через API или другое правило (запуск сценария)

Возможные действия:

  • Установка значения переменной
  • Отправка строки/команды в контроллер датчиков (для внутренней обработки либо для внешнего устройства)
  • HTTP-запрос на внешнюю веб-систему
  • Запуск shell-комадны (Linux)
  • Запуск сценария
  • Отложенные действия по таймеру

Пример исходного кода правила

# RULE 2 Forwarder RCSwitch (regex)
MATCHED_RULE2='0'
REGEX='^RCSwitch:(.+)$'
if [[ $LINE =~ $REGEX ]]
then
 MATCHED_RULE2="1"
fi

# RULE 2 ACTIONS
if [[ "$MATCHED_RULE2" == "1" ]]
then

#Action 2.1 (http) 
echo "HTTP request: http://192.168.0.17/objects/?script=RCSwitch&rcswitch=${BASH_REMATCH[1]}"
wget -O $DATA_PATH/http.tmp http://192.168.0.17/objects/?script=RCSwitch&rcswitch=${BASH_REMATCH[1]}
fi

Настройка правил производится через личный кабинет пользователя после регистрации устройства в веб-системе (сейчас вся серверная составляющая реализована как часть проекта connect.smartliving.ru). Программировать при этом не нужно, веб-система сама преобразует заданные пользователем правила в bash-команды. Со стороны пользователя интерфейс настройки выглядит примерно так:

image

Более подробно об использовании системы правил можно почитать на одной из страниц документации проекта.

Интерфейс и API

В принципе, вышеперечисленного вполне достаточно для создания автономного модуля, однако, список пожеланий был длинным, как и путь к реализации. Следующим шагом стало создание веб-интерфейса и API. Шаг этот достаточно не сложный, по сравнению с предыдущими, и реализован он был по схожему принципу. На хост-устройстве уже имеется веб-сервер, так что для реализации API был создан ещё один bash-скрипт и размещён в /www/cgi-bin/master

Исходный код скрипта /www/cgi-bin/master

#!/bin/bash

DATA_PATH="/etc/master/data"

echo "Content-type: text/plain"
echo ""

# Save the old internal field separator.
  OIFS="$IFS"

# Set the field separator to & and parse the QUERY_STRING at the ampersand.
  IFS="${IFS}&"
  set $QUERY_STRING
  Args="$*"
  IFS="$OIFS"

# Next parse the individual "name=value" tokens.

  ARG_VALUE=""
  ARG_VAR=""
  ARG_OP=""
  ARG_LINE=""

  for i in $Args ;do

#       Set the field separator to =
        IFS="${OIFS}="
        set $i
        IFS="${OIFS}"

        case $1 in
                # Don't allow "/" changed to " ". Prevent hacker problems.
                var) ARG_VAR="`echo -n $2 | sed 's|[]||g' | sed 's|%20| |g'`"
                       ;;
                #
                value) ARG_VALUE=$2
                       ;;
                line) ARG_LINE=$2
                       ;;
                op) ARG_OP=$2
                       ;;
                *)     echo "<hr>Warning:"
                            "<br>Unrecognized variable '$1' passed.<hr>"
                       ;;

        esac
  done

# Set value
#ARG_OP="set"

#echo $ARG_OP

if [[ "$ARG_OP" == "set" ]]
then
# echo "Set operation<br>"
 echo -n "$ARG_VALUE">$DATA_PATH/$ARG_VAR.dat
 echo "OK"
fi

if [[ "$ARG_OP" == "get" ]]
then
# echo "Get operation<br>"
 cat $DATA_PATH/$ARG_VAR.dat
fi

if [[ "$ARG_OP" == "send" ]]
then
# echo "Send<br>"
 echo -n $ARG_LINE>>$DATA_PATH/incoming_data.txt
 echo "OK"
fi

if [[ "$ARG_OP" == "action" ]]
then
# echo "Action<br>"
 echo -n $ARG_LINE>>$DATA_PATH/incoming_action.txt
 echo "OK"
fi

if [[ "$ARG_OP" == "refresh" ]]
then
# echo "Send<br>"
 echo "Web">$DATA_PATH/reboot
 echo "OK"
fi

if [[ "$ARG_OP" == "run" ]]
then
# echo "Run<br>"
 echo `$ARG_LINE`
fi

Этот скрипт обеспечивает следующие команды API:

Установка значения переменной
http://адрес_устройства/cgi-bin/master?op=set&var=Variable1&value=Value1
Устанавливает значение переменной Variable1 в Value1

Получение значения переменной
http://адрес_устройства/cgi-bin/master?op=get&var=Variable1
Возвращает значение переменной Variable1

Отправка данных в контроллер
http://адрес_устройства/cgi-bin/master?op=send&line=SomeData
Отправляет строчку SomeData в подключенный контроллер

Активация действия
http://адрес_устройства/cgi-bin/master?op=action&line=SomeAction
Инициализирует действие SomeAction, описанное в правилах (тип «Активные действия»)

Принудительно обновление правил
http://адрес_устройства/cgi-bin/master?op=refresh
Инициализирует принудительное обновление (скачивание) правил и веб-интерфейса без перезагрузки устройства

Системная команда
http://адрес_устройства/cgi-bin/master?op=run&line=SomeCommand
Инициализирует выполнение SomeCommand в оболочке системы (например, использование «reboot» перезапустит устройство)

После API был веб-интерфейс. С ним обошлись так же, как и с правилами – настраиваем его на веб-сервисе и обновляем на устройстве на том же этапе инициализации. Вот как выглядит интерфейс создания меню управления для устройства:

image

Чтобы не изобретать колесо, был взят легковесный frontend-фрэймворк Kraken и закинут в папку /www/kraken-master. После инициализации в папке /www/ появляется файл menu.html и соответственно обращаться к нашему настроенному веб-интерфейсу можно по адресу http://адрес_устройства/menu.html. Такой вид адреса выбран не случайно, а для совместимости с приложением MajorDroid – мелкая деталь, но я за универсальность и совместимость всего и вся, так что, почему бы и нет.

Работа в режиме Online

“Ух, ну и системка получается и это ещё не всё?” — спросите вы. Ну почти, осталась самая малость. Точнее «малость» для пользователя, но большой этап для разработчика (так часто бывает). А именно – работа с устройством через Интернет. Казалось бы, имеется веб-интерфейс, пробрасывай порты на роутере и пользуйся на здоровье. Но это не наши методы, наши методы в упрощении жизни окружающим (и усложнении себе). Предположим худшее – нет возможности изменить настройки роутера и сделать форвард портов. Или же предполагается использование множества подобных устройств в одной сети и к каждой (гипотетически) хочется иметь возможность обращаться извне. Решение было таковым – устройство само должно инициировать и поддерживать канал с внешним сервером для обмена данными и командами, внешний же сервер дублировал у себя заданный для конкретного устройства веб-интерфейс и организовывал передачу команд от пользователя по этому каналу. Канал представляет собой socket-соединение, которое с одной стороны (на устройстве) создаёт отдельный bash-скрипт и с другой стороны (на сервере) socket-сервер.

На устройстве скрипт находится в /etc/master/socket_client

Исходный код скрипта /etc/master/socket_client

#!/bin/bash

# settings
. /etc/master/settings.sh

# STEP 0
# wait to be online
COUNTER=0
while [ $COUNTER -lt 5 ]; do
ping -c 1 $ONLINE_CHECK_HOST
if [[ $? = 0 ]];
then
echo Network available.
break;
else
echo Network not available. Waiting...
sleep 5
fi
let COUNTER=COUNTER+1
done

#---------------------------------------------------------------------------
# START

if [ ! -d "$DATA_PATH" ]; then
  mkdir $DATA_PATH
  chmod 0666 $DATA_PATH
fi

while : 
do

TEST_FILE=$DATA_PATH/data_sent.txt
touch $TEST_FILE

SOCKET_HOST=connect.smartliving.ru
SOCKET_PORT=11444

exec 3<>/dev/tcp/$SOCKET_HOST/$SOCKET_PORT

NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo " Sending: Hello!"
echo "Hello!">&3
read  -t 60 ok <&3
NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo -n " Received: "
echo "$ok";

REGEX='^Please'
if [[ ! $ok =~ $REGEX ]]
then
 NOW=$(date +"%H:%M:%S")
 echo -n $NOW
 echo " Connection failed!"
 continue
fi

NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo " Sending: auth:$MASTER_ID"
echo "auth:$MASTER_ID">&3
read -t 60 ok <&3
NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo -n " Received: "
echo "$ok";

REGEX='^Authorized'
if [[ ! $ok =~ $REGEX ]]
then
 NOW=$(date +"%H:%M:%S")
 echo -n $NOW
 echo " Authorization failed!"
 exit 0
fi

NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo " Sending: Hello again!"
echo "Hello again!">&3
read -t 60 ok <&3
NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo -n " Received: "
echo "$ok";

while read -t 120 LINE; do

NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo -n " Got line: "
echo $LINE

# Ping reply
REGEX='^PING'
if [[ $LINE =~ $REGEX ]]
then
echo -n $NOW
echo " Sending: PONG!"
echo PONG!>&3
fi

# Run action
REGEX='^ACTION:(.+)$'
if [[ $LINE =~ $REGEX ]]
then
DATA_RECEIVED=${BASH_REMATCH[1]}
NOW=$(date +"%H:%M:%S")
echo -n $NOW
echo -n " Action received: "
echo $DATA_RECEIVED
echo -n $DATA_RECEIVED>>$DATA_PATH/incoming_action.txt
fi


# Pass data
REGEX='^DATA:(.+)$'
if [[ $LINE =~ $REGEX ]]
then
DATA_RECEIVED=${BASH_REMATCH[1]}
echo -n $NOW
echo -n " Data received: "
echo $DATA_RECEIVED
echo -n $DATA_RECEIVED>>$DATA_PATH/incoming_data.txt
fi

# Pass data
REGEX='^URL:(.+)$'
if [[ $LINE =~ $REGEX ]]
then
DATA_RECEIVED=${BASH_REMATCH[1]}
echo -n $NOW
echo -n " URL received: "
echo 
wget -O $DATA_PATH/data_post.tmp http://localhost$DATA_RECEIVED
fi



# Check files modified
FILES=$DATA_PATH/*.dat
for f in $FILES
do
 if [ $f -nt $TEST_FILE ]; then 
  echo "Processing $f ..."
  FNAME=${f##*/}
  PARAM=${FNAME/.dat/}
  CONTENT=`cat $f`
  echo -n $NOW
  echo " Sending: DATA:$PARAM|$CONTENT;"
  echo "data:$PARAM|$CONTENT;">&3
 fi
done
touch $TEST_FILE


done <&3

done
#---------------------------------------------------------------------------

echo Cycle stopped.

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

image

Задачи на будущее

Вся описанная конструкция работает достаточно стабильно – с момента запуска и того времени, как я решил написать статью, прошло уже, пожалуй, пара месяцев, а устройство исправно выполняет заложенные в него функции. Однако, всё реализовано, что называется, без излишеств. Для проверки концепции этого достаточно, но для массового внедрения устройств на данной (или подобной ей) платформе я бы поработал по следующим направлениям:

  • Безопасность (шифрование, пароли доступа к интерфейсам и т.п.)
  • Производительность на стороне сервера (хоть пока проблем не было, но самодельный socket-сервер это далеко не лучший вариант реализации)
  • UI/UX (как для устройства, так и для личного кабинета)
  • Железо (“Ардуино? Роутер!? Я вас умоляю...”)
Заключение

В статье описаны не все детали настройки и некоторые вещи типа настроек автозапуска скриптов я намеренно опустил, пытаясь донести основные возможности и суть концепции. Недостающие детали можно узнать на страницах документации.

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

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

  • Домашний сторож – сообщает владельцу о том, что кто-то пришёл домой и температуру в помещении
  • Контроллер освещения – управление светом по расписанию/событию
  • Климат-контроль – получение информации от внешних датчиков температуры/влажности и управление исполнительными механизмами
  • Контроль самочувствия – отправление уведомления при нажатии на “тревожную” кнопку либо при отсутствии движения длительное время

Таким образом, имея одну и ту же базу можно создать множество прикладных “коробочных” решений, интегрируя подобные “Интернет-вещи” с информационными системами на более высоком уровне.

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

image

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

Автор: Jey

Источник

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


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