- PVSM.RU - https://www.pvsm.ru -

IoT-решение за 1,5 часа

Или как мы зажгли лампочку со смартфона через облачную службу на глазах изумленных студентов НГУ.
IoT-решение за 1,5 часа - 1
Полное техническое описание решения мы приводим внизу, а начнем с лирическо-философского пролога.

Глава 1. Лирическая

Практически все наши сотрудники получили высшее образование и очень многие именно в Новосибирском государственном университете. Кто-то буквально недавно, кто-то 10 – 20 лет назад, и все сталкивались с выбором будущей профессии. На последних курсах студентами мы выбирали кафедру на которой проходили специализацию и защищали дипломы. И была такая замечательная традиция как Дни открытых дверей в институтах, лабораториях и компаниях, где сотрудники рассказывали, чем они занимаются, какие темы сейчас стоят перед наукой и технологиями, и как можно в этом поучаствовать.
Что самое интересное в Днях открытых дверей для студента? Это – ходить, задавать вопросы, смотреть на реальных людей, которые занимаются настоящим делом, которое кому-то нужно.

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

Сегодня Дни открытых дверей сменили Дни карьеры. Но нам хотелось сохранить дух этого мероприятия в своей презентации.

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

Именно с такими мыслями мы начали подготовку к Дням карьеры в НГУ – ведущей кузницы кадров среди Новосибирских вузов для любой уважающей себя ИТ-компании Новосибирска и многих других городов и стран.

Поэтому мы устроили стендовую сессию и мастер-класс, на который пришли руководители проектов и ведущие разработчики, участвующие в интересных боевых проектах для наших ведущих клиентов. Говоря просто – мы не ограничились девушками из HR, а выставили действующий «ИТ-спецназ» с шикарной темой.

Мы решили провязать несколько модных ИТ-тем и опыт из реального проекта:

  • Интернет вещей
  • Программирование микроконтроллеров
  • Облачные сервисы
  • Мобильные технологии

И придумали управлять лампочками со смартфона. Да, вот так просто и наглядно показать, как за полтора часа можно написать код и получить работающее комплексное решение.
IoT-решение за 1,5 часа - 2
Но давайте по порядку.

Глава 2. Вводная. Философия интернета вещей

Если говорить в общем про интернет вещей, то мы бы выделили 2 направления, в разработке решений.

  1. Удаленное управление “вещами”. Например, открытие закрытие дверей, включение выключение охранных систем, или в нашем случае освещения.
  2. Сбор данных с удаленных датчиков, анализ этих данных, а также прогнозирование функционирования исследуемых систем, с возможным применением, технологий машинного обучения.

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

А вот что касается первого, то на основе опыта в разработке таких решений мы вполне смогли его продемонстрировать. Несколько упростив один из наших проектов. Мы решили построить следующее.

  1. Есть, настольная лампа. Лампа подключена к контроллеру, который периодически опрашивает на сервис на предмет того должна ли она гореть или нет.
  2. Есть, сервис, который “живет” в AWS (Amazon Web Services). Хранить в себе состояние “включена/выключена”, а также предоставляет API, для получения или изменения этого состояния
  3. Есть мобильное приложение, которое позволяет управлять этим состоянием.

Ребята из .NET взяли на себя программирование микроконтроллера, парни из Java – облачную службу на Amazon, а наши «мобильные» коллеги – Android-приложение.

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

IoT-решение за 1,5 часа - 3

Глава 3. Подготовительная. Несколько дней до дня Д.

Для демонстрации на стендовой сессии мы решили сделать отдельный девайс. Мы не поленились и сделали его классным в стимпанковском стиле – состарили деревянную коробку и нашли винтажные лампы.

Пара выходных и вечеров и решение было готово, и стильный девайс собран и работает.

Пара рабочих дней сверху и у нас готовы крутые дизайны плакатов, буклетов, стенда и футболок.
IoT-решение за 1,5 часа - 4

Как люди ответственные мы решили отрепетировать наш мастер-класс. Подготовили презентацию, собрали всех технических специалистов и устроили прогон. Программа мастер-класса была такая: последовательно показываем, как программируем контроллер, пишем службу, пишем прилож и в завершении каждой части зажигаем лампочку.
IoT-решение за 1,5 часа - 5

IoT-решение за 1,5 часа - 6

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

Глава 4. Зазывательная. День Д. Стендовая сессия

С утра мы одни из первых появились в НГУ, развернули стенд, убрали унылый стандартный стол и поставили нашу фирменную тумбу и на нее прям в центре выставили наш девайс. Он пользовался громадным успехом. На него приходили смотреть и фотографировать все – студенты, преподаватели, фотографы, конкуренты и даже охранники. Наши буклеты и девайс сработали прекрасно – мы собрали полную аудиторию на следующий день!
IoT-решение за 1,5 часа - 7

IoT-решение за 1,5 часа - 8

Глава 5. Героическая. День Д+1. Мастер-класс.

И вот настал день мастер-класса. Мы приехали в НГУ и тут мы поняли, что как не готовься, а в таких мероприятиях без затыков не бывает. Полная аудитория студентов, а у нас не подключется Mac к проектору, не пускает некоторые девайсы в универовский Wi-Fi и даже Амазоновские облака пытались встать в позу и не задеплоить наши 5 раз проверенные сервисы. Но не зря мы выставили дрим-тим и все было порешано «на лету» не выпадая из общего тайминга и структуры презентации и разработки ПО.

Мы дали общую вводную про нашу компанию, интернет-вещей и как мы это будем делать. Затем по шагам прошли все стадии программирования комплексного решения, раздавая сувениры и брендированные кружки наиболее активным, умным и хитрым студентам. Хитрым, потому что некоторые из них попробовали нас хакнуть, и подход был довольно неплох, за что были награждены отдельно :).
IoT-решение за 1,5 часа - 9

Да, это сильно напоминало хрестоматийное киношное провальное свидание, на котором все идет не так, но финал, как и положено по жанру, был счастливым. Программное обеспечение создано, сервисы развернуты в облаке, мобильное приложение встало на телефоны и лампочка зажглась в строго отведенное на это время. Студенты разглядели нашу «душу» ;) и сказали нам «да», оставшись после завершения презентации обсуждать с нашими ребятами проекты, решения, технологии.
IoT-решение за 1,5 часа - 10

А теперь к самому интересному для хабрачитателей – к коду.

Глава 6. Техническая

Итак, напоминаем, что наше решение должно состоять из 3-х компонентов.

IoT-решение за 1,5 часа - 11

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

6.1. Программируем микроконтроллер.

Сердцем нашего устройства является плата NodeMCU на базе контроллера ESP8266.

IoT-решение за 1,5 часа - 12
Из всего списка возможностей этой платы, нас интересуют поддержка беспроводных сетей Wi-Fi и GPIO — вводы/выводы общего назначения. Также, несмотря на то, что для этой платы нету ОС в привычном ее понимании, различные варианты прошивок поддерживают выполнение программ на языках C/C++, Lua, JavaScript и MicroPython.

Мы остановились на прошивке SMART.JS, программы для которой пишутся на языке JavaScript. Из возможностей этой прошивки будем использовать только http-клиент.

IoT-решение за 1,5 часа - 13

Нас интересует вывод номер 5 (GPIO5). Это цифровой вывод. Это означает, что на выходе у него может быть логический “0” или логическая “1”. При логическом 0 реле будет выключено, при логической 1 – реле будет включено, и лампочка будет гореть.

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

{
     “resource_name”:  true/false
      …
}

Итак, к делу.

Для начала прошиваем на нашу плату прошивку SMART.JS с помощью программы FNC. После этого у нас устройство начинает работать в режиме точки доступа и появляется сеть SMARTJS_???? (например, SMARTJS_FA352), пароль можно подсмотреть в консоли FNC.

Подключаемся к точке доступа и открываем адрес 192.168.4.1. У нас открывается конфигурационная страница, на которой мы вводим SSID и пароль для нашей сети. Сохраняем изменения, устройство перегружается, и мы готовы писать наше приложение.
IoT-решение за 1,5 часа - 14

Берем ваш любимый текстовый редактор и создаем файл 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

IoT-решение за 1,5 часа - 15
Elastic Beanstalk – это один из полезных сервисов AWS для быстрого развертывания и масштабирования веб-приложений. Его использование освобождает нас от необходимости самостоятельно создавать и настраивать окружение. Сервис на выбор предоставляет несколько заранее сконфигурированных. Все, что от нас потребуется – это выбрать подходящее окружение и загрузить собранное приложение, используя интуитивно понятный UI. Остальное сделает сервис Beanstalk. На выходе мы получим URL, по которому приложение будет доступно по HTTP.

Выберем окружение Tomcat (сервлет контейнер) и следующую конфигурацию:

  • операционную систему Amazon Linux;
  • отключим балансировщик нагрузки, т.к. он нам не понадобится;
  • выберем небольшую виртуальную машину t1.micro.

Эти же действия в картинках:
IoT-решение за 1,5 часа - 16

IoT-решение за 1,5 часа - 17

IoT-решение за 1,5 часа - 18

IoT-решение за 1,5 часа - 19

IoT-решение за 1,5 часа - 20

Кроме этого 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 [5]]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 [6] ]

@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(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 [7], PUT [8],
POST [9], DELETE [10] – Анотации из JAX-RS, которые показывают какими http методами обращаться к нашиму api

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

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

Теперь превратим наш обычный класс в настоящий 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 clean install

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

Просто нажмём кнопку и всё должно взлететь:
IoT-решение за 1,5 часа - 21

Проверяем что всё работает:

  1. Дергаем URL нашего приложения методом get [http://какойто.адрес/ ] и в ответ получим пустой json: {}
  2. С помощью любого REST клиента выполняем запрос методом POST [http://какойто.адрес/light01 ] в тело запроса пишем true
  3. И видим, что лампочка загорелась) (можно ещё раз дёрнуть get “/” и в ответ получим {“light01”: true})

6.3. Android-приложение

IoT-решение за 1,5 часа - 22

Шаг 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 [13] и OnClick [14]. Первая связывает наш переключатель, объявленный в 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) {

           }
       });
   }

IoT-решение за 1,5 часа - 23

И-и-и, лампочка гори!
IoT-решение за 1,5 часа - 24

Глава 7. Короткая, заключительная.

С полным кодом приложения для устройства можно ознакомиться здесь: github.com/EBTRussia/nsucareerdays2016/blob/master/hw/app.js [15]

Мастер-классом и этим примером мы хотели показать – классные и интересные вещи можно делать за короткое время и минимумом усилий. Нужна идея. Дерзайте! Именно из простых и нужных вещей рождаются целые отрасли, такие как интернет вещей.

И приходите к нам работать :). У нас действительно уникальный коллектив и очень интересные проекты.
До встречи!

Автор: EastBanc Technologies

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/117440

Ссылки в тексте:

[1] docs.cesanta.com/smartjs/latest: https://docs.cesanta.com/smartjs/latest/

[2] github.com/cesanta/smart.js: https://github.com/cesanta/smart.js

[3] github.com/cesanta/fnc: https://github.com/cesanta/fnc

[4] www.silabs.com/products/mcu/Pages/USBtoUARTBridgeVCPDrivers.aspx: https://www.silabs.com/products/mcu/Pages/USBtoUARTBridgeVCPDrivers.aspx

[5] github.com/EBTRussia/nsucareerdays2016/edit/master/cloud/sample-web-app-rest: https://github.com/EBTRussia/nsucareerdays2016/edit/master/cloud/sample-web-app-rest

[6] github.com/EBTRussia/nsucareerdays2016/blob/master/cloud/sample-web-app-resteasy/src/main/java/ru/ebt/LightAppController.java: https://github.com/EBTRussia/nsucareerdays2016/blob/master/cloud/sample-web-app-resteasy/src/main/java/ru/ebt/LightAppController.java

[7] GET: https://habrahabr.ru/users/get/

[8] PUT: https://habrahabr.ru/users/put/

[9] POST: https://habrahabr.ru/users/post/

[10] DELETE: https://habrahabr.ru/users/delete/

[11] Path: https://habrahabr.ru/users/path/

[12] docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html

[13] Bind: https://habrahabr.ru/users/bind/

[14] OnClick: https://habrahabr.ru/users/onclick/

[15] github.com/EBTRussia/nsucareerdays2016/blob/master/hw/app.js: https://github.com/EBTRussia/nsucareerdays2016/blob/master/hw/app.js

[16] Источник: https://habrahabr.ru/post/281238/