Делаем Smart Point или «Интернет-вещь» своими руками
В этой статье я опишу концепцию и пример практической реализации компактной платформы для создания решений в области домашней автоматики и Интернета Вещей.
Заинтересовашихся прошу под кат.
Вместо введенияВ последнее время наблюдается ярко выраженная тенденция роста интереса к такой области информационных технологий, как автоматизация жизнедеятельности. Автоматизация сама по себе явление далеко не новое и уже десятки лет для большинства промышленных производств является не прихотью, а необходимостью, без которой просто немыслимо выживание бизнеса в условиях жёсткой конкуренции. Так почему же только сейчас мы так много слышим про Интернет Вещей (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
Микроконтроллер Arduino Nano:
В качестве первоначального набора периферии были выбраны следующие элементы:
Набор периферии, как вы понимаете, может быть другим, но в данном примере я взял именно этот исходя из упомянутого выше принципа «практической полезности». Таким образом, устройство у нас сможет реагировать на нажатие кнопки, на движение, на изменение температуры, а так же принимать данные от внешних датчиков (в данном случае использовался описанный ранее на хабре протокол) и управлять силовыми модулями системы Noolite (про модуль управления отдельная история и на фотографии не коммерческий экземпляр модуля, а один из ранних прототипов от производителя, попавший ко мне на испытания).
Объединив наброски по реализации и первоначальные требования, получаем вот такую структурную схему устройства:
Пояснения к схеме:
Устройство состоит из микроконтроллера, взаимодействующего с проводной/беспроводной периферией, и ядра, отвечающего за логику обработки входящих данных и интерфейсы Имеется API и веб-интерфейс для приёма команд от внешних «терминалов» (компьютеры, телефоны и т.п.) Устройство на связи с внешним сервисом для загрузки правил, отправки уведомлений и приёма команд Подготовка микроконтроллера У микроконтроллера две основные задачи: во-первых, выдавать в консоль события от внешних устройств, и, во-вторых, принимать из консоли команды для передачи на подключенную периферию.Ниже приведён текст скетча с учётом специфики перечисленной выше периферии. В нашем случае кнопка подключена на PIN4, датчик движения на PIN3, датчик температуры на PIN9, радиоприёмник на PIN8 и модуль Noolite на PIN-ы 10, 11.
Скетч для контроллера
#include
#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 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 и драйверы для подключения микроконтроллера. Алгоритм работы хост-модуля можно представить следующими пунктами: Инициализация — загрузка правил работы данного устройства из внешнего веб-сервиса (при его доступности), а так же установка канала связи с микроконтроллером
Приём данных от контроллера и обработка их в соответствии с загруженными правилами
На уровне исходного кода это выглядит следующим образом: Файл настроек (/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-команды. Со стороны пользователя интерфейс настройки выглядит примерно так: Более подробно об использовании системы правил можно почитать на одной из страниц документации проекта. Интерфейс и 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 »
Warning:»\
»
Unrecognized variable \'$1\' passed.
»
;;
esac done
# Set value #ARG_OP=«set»
#echo $ARG_OP
if [[ »$ARG_OP» == «set» ]]
then
# echo «Set operation
»
echo -n »$ARG_VALUE»>$DATA_PATH/$ARG_VAR.dat
echo «OK»
fi
if [[ »$ARG_OP» == «get» ]]
then
# echo «Get operation
»
cat $DATA_PATH/$ARG_VAR.dat
fi
if [[ »$ARG_OP» == «send» ]]
then
# echo «Send
»
echo -n $ARG_LINE>>$DATA_PATH/incoming_data.txt
echo «OK»
fi
if [[ »$ARG_OP» == «action» ]]
then
# echo «Action
»
echo -n $ARG_LINE>>$DATA_PATH/incoming_action.txt
echo «OK»
fi
if [[ »$ARG_OP» == «refresh» ]]
then
# echo «Send
»
echo «Web»>$DATA_PATH/reboot
echo «OK»
fi
if [[ »$ARG_OP» == «run» ]]
then
# echo «Run
»
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 был веб-интерфейс. С ним обошлись так же, как и с правилами — настраиваем его на веб-сервисе и обновляем на устройстве на том же этапе инициализации. Вот как выглядит интерфейс создания меню управления для устройства:
Чтобы не изобретать колесо, был взят легковесный 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-код для работы с устройством. Один из тестовых примеров ниже:
Задачи на будущее Вся описанная конструкция работает достаточно стабильно — с момента запуска и того времени, как я решил написать статью, прошло уже, пожалуй, пара месяцев, а устройство исправно выполняет заложенные в него функции. Однако, всё реализовано, что называется, без излишеств. Для проверки концепции этого достаточно, но для массового внедрения устройств на данной (или подобной ей) платформе я бы поработал по следующим направлениям: Безопасность (шифрование, пароли доступа к интерфейсам и т.п.) Производительность на стороне сервера (хоть пока проблем не было, но самодельный socket-сервер это далеко не лучший вариант реализации) UI/UX (как для устройства, так и для личного кабинета) Железо («Ардуино? Роутер?! Я вас умоляю…») Заключение В статье описаны не все детали настройки и некоторые вещи типа настроек автозапуска скриптов я намеренно опустил, пытаясь донести основные возможности и суть концепции. Недостающие детали можно узнать на страницах документации.Конкретно это устройство и весь процесс его создания был экспериментом для проверки работы отдельных компонентов и технологий. В процессе возникали и воплощались идеи в других устройствах и системах, а кое-что перекочевало из-вне в этот проект, так что в целом время было потрачено далеко не зря. Буду рад, если мой опыт реализации окажется полезен.Если развивать тему коммерческого применения концепции, то можно говорить о менее универсальных, но, скорее, прикладных реализациях. Например:
Домашний сторож — сообщает владельцу о том, что кто-то пришёл домой и температуру в помещении Контроллер освещения — управление светом по расписанию/событию Климат-контроль — получение информации от внешних датчиков температуры/влажности и управление исполнительными механизмами Контроль самочувствия — отправление уведомления при нажатии на «тревожную» кнопку либо при отсутствии движения длительное время Таким образом, имея одну и ту же базу можно создать множество прикладных «коробочных» решений, интегрируя подобные «Интернет-вещи» с информационными системами на более высоком уровне.
P.S. Долго думал выкладывать ли «живую» фотографию получившегося устройства, но про экспериментальный характер всей затеи я уже предупредил, так что картонный корпус (или его макет, если хотите) вполне соответствует:
P.P. S. Чуть не забыл, стоимость данного устройства со всеми перечисленными компонентами выходит около $60, потраченное время бесценно.