IoT-решение за 1,5 часа
Или как мы зажгли лампочку со смартфона через облачную службу на глазах изумленных студентов НГУ.
Полное техническое описание решения мы приводим внизу, а начнем с лирическо-философского пролога.
Глава 1. Лирическая
Практически все наши сотрудники получили высшее образование и очень многие именно в Новосибирском государственном университете. Кто-то буквально недавно, кто-то 10 — 20 лет назад, и все сталкивались с выбором будущей профессии. На последних курсах студентами мы выбирали кафедру на которой проходили специализацию и защищали дипломы. И была такая замечательная традиция как Дни открытых дверей в институтах, лабораториях и компаниях, где сотрудники рассказывали, чем они занимаются, какие темы сейчас стоят перед наукой и технологиями, и как можно в этом поучаствовать.
Что самое интересное в Днях открытых дверей для студента? Это — ходить, задавать вопросы, смотреть на реальных людей, которые занимаются настоящим делом, которое кому-то нужно.
По словам одного из наших сотрудников, для него, как для будущего специалиста, который никогда не видел практически применения своих знаний в рамках Дней открытых дверей было очень интересно увидеть вживую тренажер стыковки для станции Мир, систему перехвата телефонных звонков, систему управления ТЭЦ, виртуальную студию, услышать и поговорить с людьми все это уже реализовавшими. И именно такие встречи помогли решить, где будет написан диплом и в какой из областей хотелось бы развиваться в профессиональном плане.
Сегодня Дни открытых дверей сменили Дни карьеры. Но нам хотелось сохранить дух этого мероприятия в своей презентации.
Мы поставили цель — сделать все согласно старым добрым и проверенным традициям с одной стороны, а с другой — показать студентам, чтобы есть фундаментальные принципы и мир достаточно сложная вещь, но это и хорошо. Это вызов и это возможность реализовать себя в профессиональном ИТ-обществе.
Именно с такими мыслями мы начали подготовку к Дням карьеры в НГУ — ведущей кузницы кадров среди Новосибирских вузов для любой уважающей себя ИТ-компании Новосибирска и многих других городов и стран.
Поэтому мы устроили стендовую сессию и мастер-класс, на который пришли руководители проектов и ведущие разработчики, участвующие в интересных боевых проектах для наших ведущих клиентов. Говоря просто — мы не ограничились девушками из HR, а выставили действующий «ИТ-спецназ» с шикарной темой.
Мы решили провязать несколько модных ИТ-тем и опыт из реального проекта:
- Интернет вещей
- Программирование микроконтроллеров
- Облачные сервисы
- Мобильные технологии
И придумали управлять лампочками со смартфона. Да, вот так просто и наглядно показать, как за полтора часа можно написать код и получить работающее комплексное решение.
Но давайте по порядку.
Глава 2. Вводная. Философия интернета вещей
Если говорить в общем про интернет вещей, то мы бы выделили 2 направления, в разработке решений.
- Удаленное управление «вещами». Например, открытие закрытие дверей, включение выключение охранных систем, или в нашем случае освещения.
- Сбор данных с удаленных датчиков, анализ этих данных, а также прогнозирование функционирования исследуемых систем, с возможным применением, технологий машинного обучения.
Конечно в будущие решения класса IoT будут не выбирать одно из двух направлений, а сочетать их. Однако 2-й вариант, кажется пока несколько сложным для проведения мастер-класса, т.е. написания его в online-режиме за полтора часа.
А вот что касается первого, то на основе опыта в разработке таких решений мы вполне смогли его продемонстрировать. Несколько упростив один из наших проектов. Мы решили построить следующее.
- Есть, настольная лампа. Лампа подключена к контроллеру, который периодически опрашивает на сервис на предмет того должна ли она гореть или нет.
- Есть, сервис, который «живет» в AWS (Amazon Web Services). Хранить в себе состояние «включена/выключена», а также предоставляет API, для получения или изменения этого состояния
- Есть мобильное приложение, которое позволяет управлять этим состоянием.
Ребята из .NET взяли на себя программирование микроконтроллера, парни из Java — облачную службу на Amazon, а наши «мобильные» коллеги — Android-приложение.
В презентации для студентов это звучало так:
Глава 3. Подготовительная. Несколько дней до дня Д.
Для демонстрации на стендовой сессии мы решили сделать отдельный девайс. Мы не поленились и сделали его классным в стимпанковском стиле — состарили деревянную коробку и нашли винтажные лампы.
Пара выходных и вечеров и решение было готово, и стильный девайс собран и работает.
Пара рабочих дней сверху и у нас готовы крутые дизайны плакатов, буклетов, стенда и футболок.
Как люди ответственные мы решили отрепетировать наш мастер-класс. Подготовили презентацию, собрали всех технических специалистов и устроили прогон. Программа мастер-класса была такая: последовательно показываем, как программируем контроллер, пишем службу, пишем прилож и в завершении каждой части зажигаем лампочку.
В течение трех прогонов мы научились укладываться в отведенный тайминг, отшлифовали код и текст и подготовили пару каверзных вопросов для студентов.
Глава 4. Зазывательная. День Д. Стендовая сессия
С утра мы одни из первых появились в НГУ, развернули стенд, убрали унылый стандартный стол и поставили нашу фирменную тумбу и на нее прям в центре выставили наш девайс. Он пользовался громадным успехом. На него приходили смотреть и фотографировать все — студенты, преподаватели, фотографы, конкуренты и даже охранники. Наши буклеты и девайс сработали прекрасно — мы собрали полную аудиторию на следующий день!
Глава 5. Героическая. День Д+1. Мастер-класс.
И вот настал день мастер-класса. Мы приехали в НГУ и тут мы поняли, что как не готовься, а в таких мероприятиях без затыков не бывает. Полная аудитория студентов, а у нас не подключется Mac к проектору, не пускает некоторые девайсы в универовский Wi-Fi и даже Амазоновские облака пытались встать в позу и не задеплоить наши 5 раз проверенные сервисы. Но не зря мы выставили дрим-тим и все было порешано «на лету» не выпадая из общего тайминга и структуры презентации и разработки ПО.
Мы дали общую вводную про нашу компанию, интернет-вещей и как мы это будем делать. Затем по шагам прошли все стадии программирования комплексного решения, раздавая сувениры и брендированные кружки наиболее активным, умным и хитрым студентам. Хитрым, потому что некоторые из них попробовали нас хакнуть, и подход был довольно неплох, за что были награждены отдельно :).
Да, это сильно напоминало хрестоматийное киношное провальное свидание, на котором все идет не так, но финал, как и положено по жанру, был счастливым. Программное обеспечение создано, сервисы развернуты в облаке, мобильное приложение встало на телефоны и лампочка зажглась в строго отведенное на это время. Студенты разглядели нашу «душу» ;) и сказали нам «да», оставшись после завершения презентации обсуждать с нашими ребятами проекты, решения, технологии.
А теперь к самому интересному для хабрачитателей — к коду.
Глава 6. Техническая
Итак, напоминаем, что наше решение должно состоять из 3-х компонентов.
- Микроконтроллер который включает/выключает лампочку. Этот микроконтроллер периодически запрашивает состояние лампочки у сервиса. Тут внимательный читатель может спросить почему мы не использовали некие технологии, позволяющие реализовать Push, со стороны сервера и будет прав. В реальном проекте использовалось соединение с помощью WebSocket. Однако, чтобы уложиться в столь короткое время как мастер-класс, мы решили максимально упростить систему.
- RESTfull Service который «хостится» в AWS, а также тестовая страничка которая позволяет им управлять.
- Мобильное приложение, которое также позволяет управлять состоянием лампочки.
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 resource = new ConcurrentHashMap<>();
@GET
public Map getAll() {
return resource;
}
@GET
@Path("/{resource}")
public Boolean get(@PathParam("resource") String r) {
return resource.get(r);
}
@PUT
@Path("/{resource}")
public Boolean put(@PathParam("resource") String r, Boolean status) {
if (resource.containsKey(r)) {
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(r);
}
}
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
Собираем приложение. Идём в рутовую директорию нашего приложения и выполняем команду:
mvn clean install
После идём в созданную maven«ом папку target и забераем *.war файл, деплоим его на томкат с помощью сервиса BeansTalk.
Просто нажмём кнопку и всё должно взлететь:
Проверяем что всё работает:
- Дергаем URL нашего приложения методом get [http://какойто.адрес/ ] и в ответ получим пустой json: {}
- С помощью любого REST клиента выполняем запрос методом POST [http://какойто.адрес/light01 ] в тело запроса пишем true
- И видим, что лампочка загорелась) (можно ещё раз дёрнуть 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, который как никто другой хорошо подходит для управления лампочкой, почти как настоящий выключатель:
Мы дали нашему компоненту уникальное имя 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
В нашем интерфейсе всего 2 метода:
Здесь мы указываем библиотеке retrofit свой базовый url, сообщаем, что запросы и ответы будут в формате JSON и указываем только что написанный интерфейс, после чего HTTP-клиент уже готов к работе.
Шаг 3. Дружим UI и HTTP-клиент
Во-первых, нужно получить и отобразить актуальное состояние лампочки. Сделаем GET-запрос и поищем в вернувшемся словаре лампочку с именем «light01»:
webApi.list().enqueue(new Callback
Во-вторых, при переключении лампочки на клиенте нужно уведомлять об этом сервер. Мы уже написали обработчик клика по переключателю на предыдущем шаге, теперь добавим в него выполнение http-запроса:
@OnClick(R.id.light_switch)
void onLightSwitchClicked(Switch lightSwitch){
boolean checked = lightSwitch.isChecked();
webApi.switchBulb("light01", checked).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
Toast.makeText(MainActivity.this, "Light01 changed", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(Call call, Throwable t) {
}
});
}
И-и-и, лампочка гори!
Глава 7. Короткая, заключительная.
С полным кодом приложения для устройства можно ознакомиться здесь: github.com/EBTRussia/nsucareerdays2016/blob/master/hw/app.js
Мастер-классом и этим примером мы хотели показать — классные и интересные вещи можно делать за короткое время и минимумом усилий. Нужна идея. Дерзайте! Именно из простых и нужных вещей рождаются целые отрасли, такие как интернет вещей.
И приходите к нам работать :). У нас действительно уникальный коллектив и очень интересные проекты.
До встречи!