Робимо Smart Point або «Інтернет-річ» своїми руками

    У цій статті я опишу концепцію і приклад практичної реалізації компактної платформи для створення рішень в області домашньої автоматики та Інтернету Речей.
 
 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 += ©;
        }    
  }    
  
    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.  
  3. Прийом даних від контролера і обробка їх відповідно до завантаженими правилами
  4.  
 
На рівні вихідного коду це виглядає наступним чином:
 
 Файл налаштувань (/ 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, витрачений час безцінне.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.