IoT-рішення за 1,5 години

Або як ми запалили лампочку зі смартфона через хмарну службу на очах здивованих студентів НГУ.

Повний технічний опис рішення ми наводимо внизу, а почнемо з лирическо-філософського контексту.

Глава 1. Лірична
Практично всі наші співробітники отримали вищу освіту і дуже багато саме в Новосибірському державному університеті. Хтось буквально нещодавно, хтось 10 – 20 років тому, і всі стикалися з вибором майбутньої професії. На останніх курсах студентами ми вибирали кафедру на якій проходили спеціалізацію і захищали дипломи. І була така чудова традиція як Дні відкритих дверей в інститутах, лабораторіях і компаніях, де співробітники розповідали, чим вони займаються, які теми зараз стоять перед наукою і технологіями, і як можна в цьому взяти участь.
Що найцікавіше у Днях відкритих дверей для студента? Це – ходити, задавати питання, дивитися на реальних людей, які займаються цим ділом, яке комусь потрібно.

За словами одного з наших співробітників, для нього, як для майбутнього фахівця, який ніколи не бачив практично застосування своїх знань у рамках Днів відкритих дверей було дуже цікаво побачити наживо тренажер для стикування станції Світ, систему перехоплення телефонних дзвінків, систему управління ТЕЦ, віртуальну студію, почути і поговорити з людьми все це вже реализовавшими. І саме такі зустрічі допомогли вирішити, де буде написаний диплом і в якій з областей хотілося б розвиватися в професійному плані.

Сьогодні Дні відкритих дверей змінили Дні кар'єри. Але нам хотілося зберегти дух цього заходу в своїй презентації.

Ми поставили мету – зробити все згідно старим добрим і перевіреним традицій з одного боку, а з іншого – показати студентам, щоб є фундаментальні принципи і світ досить складна річ, але це і добре. Це виклик і це можливість реалізувати себе в професійному ІТ-суспільстві.

Саме з такими думками ми почали підготовку до Днів кар'єри в НГУ – провідної кузні кадрів серед Новосибірських вузів для будь-якої поважаючої себе ІТ-компанії Новосибірська та багатьох інших міст і країн.

Тому ми влаштували стендову сесію і майстер-клас, на який прийшли керівники проектів та провідні розробники, які беруть участь у цікавих бойових проектах для наших провідних клієнтів. Кажучи просто – ми не обмежилися дівчатами з HR, а виставили діючий «ІТ-спецназ» з шикарною темою.

Ми вирішили пров'язати кілька модних ІТ-тим і досвід з реального проекту:
  • Інтернет речей
  • Програмування мікроконтролерів
  • Хмарні сервіси
  • Мобільні технології
І придумали управляти лампочками зі смартфона. Так, ось так просто і наочно показати, як за півтори години можна написати код і отримати працює комплексне рішення.

Але давайте по порядку.

Розділ 2. Вступна. Філософія інтернету речей
Якщо говорити в загальному про інтернет речей, то ми б виділили 2 напрямки, у розробці рішень.
  1. Віддалене управління «речами». Наприклад, відкриття закриття дверей, включення вимикання охоронних систем, або в нашому випадку освітлення.
  2. Збір даних з віддалених датчиків, аналіз цих даних, а також прогнозування функціонування досліджуваних систем, з можливим застосуванням, технологій машинного навчання.


Звичайно в майбутні рішення класу IoT будуть не вибирати одне з двох напрямків, а поєднувати їх. Однак 2-й варіант, здається дещо складним для проведення майстер-класу, тобто написання його в online-режимі за півтори години.

А ось що стосується першого, то на основі досвіду в розробці таких рішень ми цілком змогли його продемонструвати. Кілька спростивши один з наших проектів. Ми вирішили побудувати наступне.
  1. Є, настільна лампа. Лампа підключена до контролера, який періодично опитує на сервіс на предмет того, чи повинна вона горіти чи ні.
  2. Є, сервіс, який «живе» у AWS (Amazon Web Services). Зберігати в собі стан «увімкнено/вимкнено», а також надає API, для отримання або зміни цього стану
  3. Є мобільний додаток, яке дозволяє керувати цим станом.
Хлопці з .NET взяли на себе програмування мікроконтролера, хлопці з Java – хмарну службу на Amazon, а наші «мобільні» колеги – Android-додаток.

В презентації для студентів це звучало так:



Глава 3. Підготовча. Кілька днів до дня Д.
Для демонстрації на стендовій сесії ми вирішили зробити окремий девайс. Ми не полінувалися і зробили його класним в стимпанковском стилі – зістарили дерев'яну коробку і знайшли вінтажні лампи.

Пара вихідних і вечорів і рішення було готове, і стильний девайс зібраний і працює.


Пара робочих днів зверху і у нас готові круті дизайни плакатів, буклетів, стенда і футболок.


Як люди відповідальні ми вирішили повторити наш майстер-клас. Підготували презентацію, зібрали всіх технічних фахівців і влаштували прогін. Програма майстер-класу була така: послідовно показуємо, як програмуємо контролер, пишемо службу, пишемо програми і в завершенні кожної частини запалюємо лампочку.




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

Глава 4. Зазывательная. День Д. Стендова сесія
З ранку ми одні з перших з'явилися в НГУ, розгорнули стенд, прибрали сумовитий стандартний стіл і поставили нашу фірмову тумбу і на неї прям в центрі виставили наш девайс. Він користувався величезним успіхом. На нього приходили дивитися і фотографувати все – студенти, викладачі, фотографи, конкуренти і навіть охоронці. Наші буклети та девайс спрацювали чудово – ми зібрали повну аудиторію на наступний день!




Глава 5. Героїчна. День Д+1. Майстер-клас.
І ось настав день майстер-класу. Ми приїхали в НГУ і тут ми зрозуміли, що як не готуйся, а в таких заходах без затыков не буває. Повна аудиторія студентів, а у нас не подключется Mac до проектора, не пускає деякі девайси в универовский Wi-Fi і навіть Амазонівські хмари намагалися встати в позу і не задеплоить наші 5 разів перевірені сервіси. Але не дарма ми виставили дрім-тім і все було порешано «на льоту» не випадаючи із загального таймінгу і структури презентації і розробки ПЗ.

Ми дали загальну вступну про нашу компанію, інтернет-речей і як ми це будемо робити. Потім по кроках пройшли всі стадії програмування комплексного рішення, роздаючи сувеніри і брендовані гуртки найбільш активним, розумним і хитрим студентам. Хитрим, тому що деякі з них спробували нас хакнути, і підхід був досить непоганий, за що були нагороджені окремо :).


Так, це сильно нагадувало хрестоматійне кіношне провальне побачення, на якому все йде не так, але фінал, як і належить за жанром, був щасливим. Програмне забезпечення створене, сервіси розгорнуті в хмарі, мобільний додаток встало на телефони і лампочка запалилася в строго відведений на це час. Студенти розгледіли нашу «душу» ;) і нам сказали «так», залишившись після завершення презентації обговорювати з нашими хлопцями проекти, рішення, технології.


А тепер до найцікавішого для хабрачитателей – до коду.

Глава 6. Технічна
Отже, нагадуємо, що наше рішення має складатися з 3-х компонентів.


  1. Мікроконтролер який включає/вимикає лампочку. Цей мікроконтролер періодично запитує стан лампочки у сервісу. Тут уважний читач може запитати, чому ми не використовували якісь технології, що дозволяють реалізувати Push, з боку сервера і буде правий. В реальному проекті використовувалося сполучення з допомогою WebSocket. Однак, щоб укластися в такий короткий час, як майстер-клас, ми вирішили максимально спростити систему.
  2. RESTfull Service який «хоститься» в AWS, а також тестова сторінка яка дозволяє їм управляти.
  3. Мобільний додаток, яке також дозволяє керувати станом лампочки.


6.1. Програмуємо мікроконтролер.
Серцем нашого пристрою є плата NodeMCU на базі контролера ESP8266.


З усього списку можливостей цієї плати, нас цікавлять підтримка бездротових мереж Wi-Fi і GPIO — вводи/висновки загального призначення. Також, незважаючи на те, що для цієї плати немає ОС в звичному її розумінні, різні варіанти прошивок підтримують виконання програм на мовах C/C++, Lua, JavaScript і MicroPython.

Ми зупинилися на прошивці SMART.JS програми для якої пишуться на мові JavaScript. З можливостей цієї прошивки будемо використовувати http-клієнт.



Нас цікавить висновок номер 5 (GPIO5). Це цифровий висновок. Це означає, що на виході в нього може бути логічний «0» або логічна «1». При логічному 0 реле буде вимкнено, при логічній 1 – реле буде включено, і лампочка буде горіти.

Пререквизиты:
1. SMART.JS документація: docs.cesanta.com/smartjs/latest
2. SMART.JS прошивка: github.com/cesanta/smart.js
3. FNC (утиліта для завантаження прошивок і програм на JavaScript): github.com/cesanta/fnc
4. Virtual COM port drivers: www.silabs.com/products/mcu/Pages/USBtoUARTBridgeVCPDrivers.aspx
5. Тестовий сервіс: Тестовий сервіс, який GET запит видає JSON у форматі:
{
"resource_name": true/false
...
}


Отже, до справи.

Для початку прошиваємо на нашу плату прошивку SMART.JS за допомогою програми FNC. Після цього у нас пристрій починає працювати в режимі точки доступу і з'являється мережа SMARTJS_???? (наприклад, SMARTJS_FA352), пароль можна підглянути в консолі FNC.

Підключаємося до точки доступу і відкриваємо адреса 192.168.4.1. У нас відкривається конфігураційна сторінка, на якій ми вводимо SSID і пароль для нашої мережі. Зберігаємо зміни, пристрій перевантажується, і ми готові писати наш додаток.


Беремо ваш улюблений текстовий редактор і створюємо файл app.js. Для початку виведемо яке-небудь вітальне повідомлення в нашу консоль і визначимо висновок, до якого підключено реле, і ім'я ресурсу, асоційованого з лампочкою:
console.log('device started.');
var pin = 5;
var resource = 'light01';


Ініціалізуємо наш висновок і встановимо на виході логічний 0:
GPIO.setmode(pin, 0, 0);
GPIO.write(pin, false);

Встановимо callback функцію на зміни стану підключення:
Wifi.changed(changedFunc);

Функція приймає параметром числовий статус підключення. Поки просто виведемо його на консоль разом з текстовим представленням цього статусу:
function changedFunc(state) {
console.log('Wifi state: ', state, Wifi.status());
}

Запустивши програму ми бачимо, що статусу «got ip» відповідає код 2. Тепер давайте при успішному підключенні до мережі відправимо запит до сервісу:
function changedFunc(state) {
console.log('Wifi state: ', state, Wifi.status());
if (state == 2) {
mainFunc();
}
}

function mainFunc() {
Http.request({
hostname: 'ngurestexample.us-east-1.elasticbeanstalk.com',
port: 80,
path: '/',
method: 'GET'
}, function (response){
console.log(response.body);


})
.end()
.setTimeout(5000, function(){
console.log('timeout error');
});
}

Нам приходить відповідь у вигляді рядка: {«light01»: false}. Перетворимо його в JS об'єкт і встановлюємо стан на нашому висновку у відповідності з тим, що отримали з сервісу:
var states = JSON.parse(response.body);
GPIO.write(pin !!states[resource]);

Ну, і щоб наш пристрій періодично опитували сервіс, поставимо повторний виклик mainFunc через setTimeout у функцію обробки відповіді сервісу та функцію обробки таймауту запиту:
setTimeout(mainFunc, 1000);

Тепер наше пристрій готовий до роботи.

6.2. Служба у AWS

Elastic Beanstalk – це один з корисних сервісів AWS для швидкого розгортання і масштабування веб-додатків. Його використання звільняє нас від необхідності самостійно створювати і настроювати оточення. Сервіс на вибір надає кілька заздалегідь сконфігурованих. Все, що від нас потрібно – це вибрати відповідне оточення і завантажити зібране додаток, використовуючи інтуїтивно зрозумілий UI. Решту зробить сервіс Beanstalk. На виході ми отримаємо URL, за яким додаток буде доступно з HTTP.

Виберемо оточення Tomcat (сервлет контейнер) і наступну конфігурацію:
  • операційну систему Amazon Linux;
  • відключимо балансувальник навантаження, т. к. він нам не знадобиться;
  • виберемо невелику віртуальну машину t1.micro.


Ці ж дії в картинках:










Крім цього Beanstalk надає можливість:
  • налаштовувати параметри сервера додатків (налаштування JVM) і передавати змінні оточення;
  • підключати вбудовані засоби моніторингу CloudWatch;
  • вибрати відповідну базу даних чи сховище;
Перейдемо до створення нашого веб-додатки.

Мета — реалізувати невелика і проста програма яке б могло зберігати стан лампочок (off/on) і змінювати цей стан. Клієнти абсолютно різні (мікроконтроллер і мобільний додаток), тому розумно використовувати архітектуру REST.
Як підсумок наше додаток буде надавати таке стандартне REST API:
  • [get] "/" – поверне стан всіх лампочок. Приклад: { «лампочка-1»: false, “лампочка-2": true }
  • [get] "/{resource}" – поверне стан запитуваної лампочки
  • [delete] "/{resource}" – видалить лампочку зі списку лампочок
  • [put] "/{resource}" – оновить стан заданої лампочки
  • [post] "/{resource}" – створить нову лампочку
Писати код ми будемо на Java, збирати результати нашої роботи за допомогою Maven'а, а для написання коду використовуємо бібліотеку resteasy.

З допомогою Maven підключимо необхідні залежності
[ github.com/EBTRussia/nsucareerdays2016/edit/master/cloud/sample-web-app-rest]easy/pom.xml ]:
resteasy-jaxrs – для роботи з jaxrs
resteasy-servlet-initializer – для інтеграції з томкатом
resteasy-jackson2-provider – для роботи з json

Чому resteasy. У світі Java існує багато інструментів, які дозволяють створювати rest-додатки (Jersey, Spring, Spark, тощо). Ми просто вибрали один з них, який до речі входить у стандартну поставку сервера додатків WildFly.

[ github.com/EBTRussia/nsucareerdays2016/blob/master/cloud/sample-web-app-resteasy/src/main/java/ru/ebt/LightAppController.java ]
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class LightAppController {
private ConcurrentMap<String, Boolean> resource = new ConcurrentHashMap<>();

@GET
public Map getAll() {
return resource;
}

@GET
@Path("/{resource}")
public Boolean get(@PathParam("resource") String r) {
return resource.get®;
}

@PUT
@Path("/{resource}")
public Boolean put(@PathParam("resource") String r, Boolean status) {
if (resource.containsKey®) {
resource.put(r, status);
return status;
}
throw new WebApplicationException(Response.Status.NOT_FOUND);
}

@POST
@Path("/{resource}")
public Boolean post(@PathParam("resource") String r, Boolean status) {
resource.put(r, status);
return status;
}

@DELETE
@Path("/{resource}")
public void delete(@PathParam("resource") String r) {
resource.remove®;
}
}

GET, PUT
POST, DELETE – Анотації з JAX-RS, які показують якими http методами звертатися до нашим api

Path("/{resource}") &
@PathParam(«resource») показують за яким URL звертатися до api і яку частину URL ми хочемо обробляти як параметр у логікою програми. В нашому випадку, resource — ім'я/id для лампочки.

Для зберігання стану наших ламп ми використовуємо ConcurrentHashMap [ docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html ], потокобезопасный словник ключів і значень.

Тепер перетворимо наш звичайний клас у справжній rest-сервіс. Для цього створимо файл клас BaseApplication, який успадкуємо від javax.ws.rs.core.Application. Усередині методу getSingletons() перерахуємо всі класи, які будуть REST сервісами.
@ApplicationPath("")
public class BaseApplication extends Application {
@Override
public Set<Object> getSingletons() {
HashSet<Object> objects = new HashSet<>();
objects.add(new LightAppController());
return objects;
}
}


Збираємо додаток. Йдемо в рутовую директорію нашого додатка і виконуємо команду:
mvn install clean

Після йдемо в створену maven'ом папку target і забераем *.war файл, деплоим його на томкат за допомогою сервісу BeansTalk.

Просто натиснемо кнопку і все має злетіти:


Перевіряємо що все працює:
  1. Смикаємо URL нашого додатка методом get [http://какойто.адрес/ ] і у відповідь отримаємо порожній json: {}
  2. За допомогою будь-якого REST клієнта виконуємо запит методом POST [http://какойто.адрес/light01 ] в тіло запиту пишемо true
  3. І бачимо, що лампочка загорілася) (можна ще раз смикнути get "/" і у відповідь отримаємо {«light01»: true})


6.3. Android-додаток



Крок 0. Підготовчий
Для створення Android-додатків використовується система автоматичного складання Gradle. В його скрипті build.gradle ми підключимо кілька залежностей, які спростять нам і написання коду, і життя:
dependencies {
compile 'com.jakewharton:butterknife:7.0.1'
compile 'com.squareup.retrofit2:retrofit:2.0.0'
compile 'com.squareup.retrofit2:converter-gson:2.0.0'
}

Бібліотека Butterknife допоможе нам простіше налаштовувати графічний інтерфейс, а Retrofit ідеально підходить в якості HTTP клієнта, якщо ви плануєте взаємодіяти з REST-сервісом.

Крок 1. Графічний інтерфейс
На порожній екран (activity) додамо компонент Switch, який як ніхто інший добре підходить для управління лампочкою, майже як справжній вимикач:
<Switch
android:id="@+id/light_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Лампочка"/>

Ми дали нашому компоненту унікальне ім'я light_switch і написали текст, який буде видний користувачеві.

Тепер додамо перемикач в код нашої activity:
public class MainActivity extends AppCompatActivity {

@Bind(R. id.light_switch) SwitchCompat lightSwitch;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R. layout.activity_main);
ButterKnife.bind(this);
}

@OnClick(R. id.light_switch)
void onLightSwitchClicked(Switch lightSwitch) {
boolean checked = lightSwitch.isChecked();
Toast.makeText(this, "Switch checked = " + checked, Toast.LENGTH_SHORT).show();
}

}


Ми скористалися двома уривками з бібліотеки ButterKnife: Bind і OnClick. Перша пов'язує наш перемикач, оголошений в xml розмітки з тим перемикачем, що оголошений в коді. Другий встановлює метод onLightSwitchClicked() в якості обробника кліка по нашому перемикачу.

Крок 2. HTTP-клієнт
Створити http-клієнт зовсім не складно, якщо вдатися до допомоги бібліотеки retrofit. Потрібно лише створити інтерфейс і описати в ньому всі запити до сервера в якості його методів, після чого згодувати цей інтерфейс retrofit, який самостійно створить відповідну реалізацію нашого інтерфейсу.
public interface WebApi {

@GET("/")
Call<Map<String, Boolean>> list();

@POST("/{resource}")
Call<Boolean> switchBulb(@Path("resource") String resource, @Body Boolean enabled);
}

У нашому інтерфейсі всього 2 методу:
  • list() — GET запит для отримання всіх лампочок і їх станів у вигляді словника <String, Boolean>
  • switchBulb(...) — POST запит, що створює (або пересоздающий) стан лампочки:
    webApi = new Retrofit.Builder()
    .baseUrl(getString(R. string.api_url))
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    .create(WebApi.class);
    

Тут ми вказуємо бібліотеці retrofit свій базовий url, повідомляємо, що запити і відповіді будуть у форматі JSON і вказуємо тільки що написаний інтерфейс, після чого HTTP-клієнт вже готовий до роботи.

Крок 3. Дружимо UI і HTTP-клієнт
По-перше, потрібно одержати і відобразити актуальний стан лампочки. Зробимо GET-запит і пошукаємо в вернувшемся словника лампочку з ім'ям «light01»:
webApi.list().enqueue(new Callback<Map<String, Boolean>>() {
@Override
public void onResponse(Call<Map<String, Boolean>> call, Response<Map<String, Boolean>> response) {
Boolean light01Enabled = response.body().get("light01");
lightSwitch.setChecked(Boolean.TRUE == light01Enabled);
}

@Override
public void onFailure(Call<Map<String, Boolean>> call, Throwable t) {

}
});

По-друге, при перемиканні лампочки на клієнті потрібно повідомляти про це сервер. Ми вже написали обробник кліка по перемикачу на попередньому кроці, тепер додамо в нього виконання http-запиту:
@OnClick(R. id.light_switch)
void onLightSwitchClicked(Switch lightSwitch){
boolean checked = lightSwitch.isChecked();
webApi.switchBulb("light01", checked).enqueue(new Callback<Boolean>() {
@Override
public void onResponse(Call<Boolean> call, Response<Boolean> response) {
Toast.makeText(MainActivity.this, "Light01 changed", Toast.LENGTH_SHORT).show();
}

@Override
public void onFailure(Call<Boolean> call, Throwable t) {

}
});
}



І-і-і, лампочка горі!


Розділ 7. Коротка, заключна.
З повним кодом програми для пристрою можна ознайомитися тут: github.com/EBTRussia/nsucareerdays2016/blob/master/hw/app.js

Майстер-класом і цим прикладом ми хотіли показати – класні і цікаві речі можна робити за короткий час і мінімумом зусиль. Потрібна ідея. Дерзайте! Саме з простих і потрібних речей народжуються цілі галузі, такі як інтернет речей.

І приходьте до нас працювати :). У нас дійсно унікальний колектив і дуже цікаві проекти.
До зустрічі!

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

0 коментарів

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