Приложение для Android на Raspberry Pi

В наших предыдущих уроках по IoT мы рассказали, как настроить Raspberry Pi и подключить его к серверу Parse Server с помощью Back4App API для сохранения объектов на сервере и выполнения запросов и Live Queries.

Теперь мы рассмотрим, как воспроизвести все, что было сделано на стороне Raspberry, на стороне приложения. В этом руководстве мы опишем приложение для Android, которое будет взаимодействовать с IoT-устройством, настроенным ранее. Со стороны сервера Parse приложение выполняет те же задачи, что и на Raspberry: записывает объекты, выполняет запросы и Live-запросы. Обратите внимание, что эти функции могут быть полезны, даже если вы не планируете разрабатывать IoT-приложение!

Parse зарекомендовал себя как замечательный фреймворк для создания IoT-приложений. В 2020 году, с добавлением протокола GraphQL API, он предоставляет еще более удобный способ получения данных.

Мы предоставляем наши коды в качестве первого шага для разработки желаемых приложений.

Необходимые условия

Единственным предварительным условием является прохождение нашего учебника по Android QuickStart. Для этого вам нужно перейти по ссылке, указанной ниже.

https://www.back4app.com/docs/pages/android/how-to-build-an-android-app-on-back4app

Здесь находится учебник по LiveQuery, который будет необходим в разделе 3 этого учебника серии IoT. Однако вам не обязательно выполнять его заранее.

https://docs.back4app.com/docs/android/live-query/

Если у вас возникнут вопросы по ходу урока, вы можете решить их, перейдя по официальному руководству по Parse для Android по ссылке ниже.

http://docs.parseplatform.org/android/guide/

Раздел 1: Основы создания приложения и подключение к Back4App

В этом проекте мы назовем наш класс “MainActivity.java”. Вот основной код, который поможет вам начать работу.

public class MainActivity extends AppCompatActivity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
     // Вставляем панель инструментов в приложение. Дальнейшее описание этой панели находится в файле menu_main_activity.xml 
      Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
      setSupportActionBar(toolbar); 
   }
   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
     // Раздуваем меню; это добавляет элементы на панель действий, если она присутствует. 
      getMenuInflater().inflate(R.menu.menu_main_activity, menu);
     return true;
   }
   @Override
   public boolean onOptionsItemSelected(MenuItem item) {
     // Здесь обрабатываются нажатия на элементы панели действий. Панель действий будет
      // автоматически обрабатывать нажатия на кнопку Home/Up, пока
      // если вы укажете родительскую активность в AndroidManifest.xml. 
     int id = item.getItemId();
     //noinspection SimplifiableIfStatement 
     if (id == R.id.action_settings) {
        возвращаем true;
      }
      return super.onOptionsItemSelected(item);
   }
}

Вам может понадобиться добавить следующие импорты:

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.Button;
import android.widget.ToggleButton;
import android.view.View.OnClickListener;

В XML-файл макета обязательно добавьте следующий код:

<?xml version="1.0" encoding="utf-8"?
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:id="@+id/myCoordinatorLayout"
   tools:context="com.example.guilherme.myiotapplication.MainActivity">
   <includelayout="@layout/content_main_activity" />
   <android.support.design.widget.AppBarLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:theme="@style/AppTheme.AppBarOverlay">
      <android.support.v7.widget.Toolbar
          android:id="@+id/toolbar"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:background="?attr/colorPrimary"
          app:popupTheme="@style/AppTheme.PopupOverlay" />
   </android.support.design.widget.AppBarLayout
</android.support.design.widget.CoordinatorLayout>.

Чтобы подключить ваше приложение к Back4App, добавьте следующий код в функцию onCreate() вашего класса.

// Инициализация сервера Parse
Parse.initialize(new Parse.Configuration.Builder(this))
    .applicationId("YOUR_APP_ID") // из Core Settings, на "Features" 
    .clientKey("YOUR_CLIENT_KEY") // из основных настроек, в разделе "Features" 
    .server("https://parseapi.back4app.com/")
    .build()
);

Не забудьте следовать пошаговым инструкциям в QuickStart для предоставления доступа к Интернету вашему приложению.

Раздел 2: Сохранение и извлечение объектов при Parse и отображение в приложении

Начните с добавления приведенных ниже импортов

import com.parse.FindCallback;
import com.parse.Parse;
import com.parse.ParseException;
import com.parse.ParseObject;
import com.parse.ParseQuery;
import com.parse.SaveCallback;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;

В этом разделе мы сохраним объекты класса “CommandGPIO1” по нажатию кнопки. Каждый объект Parse Object имеет три атрибута по умолчанию: objectId, createdAt и updatedAt. У нашего класса будет еще два дополнительных атрибута: content и destination. Значения для content будут либо “on”, либо “off” и будут представлять собой статус, который будет отправлен на светодиод. Для назначения все объекты будут содержать “command”, но мы можем определить разные строки для разных светодиодов.

Например:

Мы создаем две кнопки, и каждая из них записывает в контент разное значение. Чтобы создать кнопку, добавьте следующий код в XML-файл макета:

<Button
   android:id="@+id/buttonSendOn"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_gravity="top|start"
   android:layout_marginBottom="0dp"
   android:layout_marginTop="120dp"
   android:layout_marginEnd="0dp"
   android:layout_marginRight="0dp"
   android:layout_marginLeft="16dp"
   android:layout_marginStart="16dp"
   android:text="Send ON"/>

В файле класса создать кнопку так же просто, как и написать:

Button buttonOn = (Button) findViewById(R.id.buttonSendOn);

Если вы новичок в программировании для Android, обратите внимание, что аргумент “findViewById” содержит “buttonSendOn”, а это Android:Id, который мы определили в XML.

Мы хотим, чтобы при нажатии эта кнопка сохраняла объект на сервере Parse Server. Для этого добавьте следующий код:

buttonOn.setOnClickListener( new OnClickListener(){
   @Override
  public void onClick(final View view) {
     // Создаем новый объект и присваиваем ему соответствующие атрибуты 
      ParseObject command = new ParseObject("CommandGPIO1");
      command.put("content","on");
      command.put("destination", "command");
      command.saveInBackground(new SaveCallback(){
      @Override
     public void done(ParseException e){
         Snackbar.make(view, "Отправлено на выход", Snackbar.LENGTH_LONG )
         .setAction("Действие", null).show();
      }
   }); 
});

Обратите внимание, что мы можем добавить любую функцию, которую хотим выполнить в функции обратного вызова onClick. Здесь мы создаем объект класса “CommandGPIO1”, устанавливаем содержимое как “on”, а назначение как “command”.

Полезно знать, что нам НЕ нужно определять этот класс заранее на приборной панели Parse! Если впоследствии вы решите добавить новый атрибут или изменить имя класса, то вам нужно будет просто поработать над кодом, и изменения автоматически обновятся на панели.

На этом этапе вам лучше протестировать свое приложение и проверить, создается ли объект!

SaveCallback отображает snackbar, который представляет собой легкую обратную связь в нижней части экрана, как показано на рисунке выше.

Скопируйте этот код, чтобы создать кнопку, которая записывает объекты с “off”. Измените следующие строки в XML макета

android:id="@+id/buttonSendOff"
android:layout_gravity="top|center"
android:layout_marginLeft="0dp"
android:layout_marginStart="0dp"
android:text="Send OFF"/>

Создайте новую кнопку и измените строку, в которую записывается содержимое:

Button buttonOff = (Button) findViewById(R.id.buttonSendOff);
command.put("content","off");

Теперь, когда все работает правильно на сервере Parse Server, мы хотим обеспечить постоянную обратную связь с пользователем приложения. Для этого мы используем элемент EditText, как показано ниже.

Определите этот элемент в XML макета:

<EditText
    android:id="@+id/status1Text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="top|start"
    android:maxLines="2"
    android:ems="10"
    android:singleLine="false"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

В области видимости onCreate() вашего класса добавьте следующий код:

// Определение текстового элемента для отображения статуса выходного пина, который получает команды от приложения.
// Команды отправляются кнопками, которые позже будут описаны в этом коде.
final EditText textOutPin = (EditText) findViewById(R.id.status1Text);
textOutPin.setFocusable(false);
textOutPin.setClickable(true);
textOutPin.setText("Загрузка состояния выходного пина...");

Первая строка создает объект. Вторая строка делает его нередактируемым для пользователей. Третья строка делает его кликабельным, чтобы его можно было копировать и вставлять. Четвертая строка задает начальный текст.

Мы хотим записать в этот объект EditText поле содержимого последнего объекта “CommandGPIO1”, сохраненного при Parse. Хотя мы могли бы сделать это, используя некоторую логику в коде, на самом деле мы будем получать объекты из Parse, выполняя ParseQuery, поскольку это дает более реалистичные и надежные результаты.

Приведенный ниже код объявляет, устанавливает параметры и печатает результаты запроса Parse.

ParseQuery<ParseObject> queryOut = ParseQuery.getQuery("CommandGPIO1");
queryOut.whereEqualTo("destination", "command");
queryOut.addDescendingOrder("createdAt");
queryOut.setLimit(1);
queryOut.findInBackground(new FindCallback<ParseObject>() {
   @Override
  public void done(List<ParseObject> objects, ParseException e) {
     if (e == null){
         textOutPin.setText("Output is " + objects.get(0).getString("content"));
      }
     else{
         textOutPin.setText("Ошибка: " + e.getMessage());
      }
   }
});

Первая строка создает запрос. Вторая добавляет ограничение для выбора только тех объектов, поле назначения которых содержит “command”. Третья строка перебирает объекты, начиная с самых новых. Четвертая строка ограничивает результаты до одного, гарантируя, что мы получим самый новый объект.

Пятая строка активирует запрос и вызывает функцию обратного вызова по его завершении. Здесь мы записываем содержимое полученного объекта в ранее определенный объект EditText.

Мы добавим этот фрагмент кода сразу после объявления объекта EditText, чтобы запрос выполнялся при открытии приложения, а также в SaveCallback при сохранении нового объекта на Parse, чтобы автоматически обновлять текст при создании новых объектов.

На этом этапе ваше приложение должно работать так, как показано на следующих снимках экрана.

Наконец, мы добавим кнопку обновления, чтобы пользователи могли выполнить приведенный выше запрос, когда захотят. Это будет сделано с помощью кнопки другого стиля, то есть плавающей кнопки действия (Floating Action Button).

Добавьте этот код в XML-файл макета:

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="top|end"
    android:layout_marginBottom="0dp"
    android:layout_marginTop="120dp"
    android:layout_marginEnd="16dp"
    android:layout_marginRight="16dp"
    android:layout_marginLeft="0dp"
    android:layout_marginStart="0dp"
    app:srcCompat="@android:drawable/ic_popup_sync" />

Теперь в области видимости onCreate() в классе добавьте следующий код:

// Кнопка обновления для получения статуса вывода по запросу.
// Здесь выполняется тот же запрос, что и в первом случае
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener(){
    @Override
   public void onClick(final View view){
        ParseQuery<ParseObject> queryOut = ParseQuery.getQuery("CommandGPIO1");
        queryOut.whereEqualTo("destination", "command");
        queryOut.addDescendingOrder("createdAt");
        queryOut.setLimit(1);
        queryOut.findInBackground(new FindCallback<ParseObject>() {
            @Override
           public void done(List<ParseObject> objects, ParseException e) {
              if (e == null){
                   textOutPin.setText("Output is " + objects.get(0).getString("content"));
               }
              else{
                   textOutPin.setText("Ошибка: " + e.getMessage());
               }
               Snackbar.make(view, "Обновленный статус вывода", Snackbar.LENGTH_LONG ).setAction("Действие", null).show();
            }
        });
     }
 });

Теперь мы можем сохранять объекты на сервере Parse Server, извлекать из них информацию и отображать их в вашем приложении! Ваше приложение должно работать так, как показано на рисунке ниже:

Раздел 3: Прослушивание событий в реальном времени с помощью Live Query и отображение в приложении

В этом разделе мы будем в реальном времени отслеживать класс “InputGPIO” в Parse Dashboard и отображать “содержимое” объектов для пользователя.

Начнем с определения нового EditText в XML макета:

<EditText
    android:id="@+id/status2Text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center|start"
    android:layout_marginBottom="0dp"
    android:maxLines="2"
    android:ems="10"
    android:singleLine="false"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

В области видимости onCreate() создайте новый объект EditText и сразу после этого выполните инициализационный запрос (пока не живой).

final EditText textInPin = (EditText) findViewById(R.id.status2Text);
textInPin.setFocusable(false);
textInPin.setClickable(true);
textInPin.setText("Loading status of input pin...");
// Начальный (не живой) запрос для получения последнего сохраненного статуса пина
ParseQuery<ParseObject> queryIn = ParseQuery.getQuery("InputGPIO");
queryIn.whereEqualTo("type", "interrupt");
queryIn.addDescendingOrder("createdAt");
queryIn.setLimit(1);
queryIn.findInBackground(new FindCallback<ParseObject>() {
    @Override
   public void done(List<ParseObject> objects, ParseException e) {
       if (e == null){
            textInPin.setText("Input is " + objects.get(0).getString("content"));
        }
       else{
            textInPin.setText("Ошибка: " + e.getMessage());
        }
    }
});

Обратите внимание, что атрибутами “InputGPIO” являются content и type, причем последний играет роль назначения для класса “CommandGPIO”.

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

Теперь мы займемся реализацией Live Query. Здесь пригодится учебник, на который мы ссылались ранее.

https://docs.back4app.com/docs/android/live-query/

Убедитесь, что вы выполнили шаг 1 в нашем руководстве по Live Query, чтобы выбрать, в каких классах будет включен Live Query, и определить имя поддомена. Вам может понадобиться создать класс “InputGPIO” (если он еще не существует), чтобы включить в нем эту функцию. Выполните шаг 2 в руководстве, чтобы настроить клиент Live Query на Android.

Не забудьте добавить следующие импорты:

import tgio.parselivequery.BaseQuery;
import tgio.parselivequery.LiveQueryClient;
import tgio.parselivequery.LiveQueryEvent;
import tgio.parselivequery.Subscription;
import tgio.parselivequery.interfaces.OnListener;

Добавьте следующий код для инициализации LiveQuery и определения его параметров.

// Инициализация LiveQuery
LiveQueryClient.init("wss:YOUR_SUBDOMAIN_NAME.back4app.io", "YOUR_APP_ID", true );
LiveQueryClient.connect();
// Определение атрибутов LiveQuery 
 Subscription subIn = new BaseQuery.Builder("InputGPIO")
    .where("type","interrupt")
    .addField("content")
    .build()
    .subscribe();

При определении подписки мы берем только те элементы, у которых атрибут type равен “interrupt”, и получаем содержимое поля.

Теперь добавьте следующий код, чтобы реагировать всякий раз, когда на сервере Parse Server создается объект, определенный подпиской.

// Начинаем слушать события LiveQuery CREATE, получать его содержимое и писать
subIn.on(LiveQueryEvent.CREATE, new OnListener() {
    @Override
   public void on(JSONObject object) {
       try {
           final String subInContent = (String) ((JSONObject) object.get("object")).get("content");
            runOnUiThread(new Runnable() {
                @Override
               public void run() {
                   textInPin.setText("Input is " + subInContent);
                   Snackbar.make(findViewById(R.id.myCoordinatorLayout), "Input pin was changed to " + subInContent.toUpperCase(), Snackbar.LENGTH_LONG ).setAction("Action", null).show();
                }
            });
        } catch (JSONException e){
            e.printStackTrace();
        }
    }
});

Поле содержимого объекта будет отображаться в нашем новом элементе EditText.

Того, что мы сделали к этому моменту, достаточно для отображения любого ввода, отправленного IoT-устройством. Однако, чтобы проверить, как работает приложение, мы реализуем другой тип кнопки, а именно Toggle Button, которая создаст объект класса “InputGPIO” и сохранит его на Parse Server.

Добавьте следующий код в XML-файл макета:

<ToggleButton
    android:id="@+id/toggleTest"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center|end"
    android:layout_marginBottom="0dp"
    android:layout_marginTop="100dp"
    android:layout_marginEnd="0dp"
    android:layout_marginRight="0dp"
    android:layout_marginLeft="0dp"
    android:layout_marginStart="0dp"
    android:textOn = "вход включен"
    android:textOff = "input off"/>

Добавьте приведенный ниже код в область видимости функции onCreate() в классе MainActivity.

// Кнопка переключения находится здесь только для эмуляции объектов, создаваемых аппаратным обеспечением
ToggleButton toggleTest = (ToggleButton) findViewById(R.id.toggleTest);
toggleTest.setOnClickListener( new OnClickListener(){
    @Override
   public void onClick(final View view) {
        ParseObject command = new ParseObject("InputGPIO");
       if(gpioStatusTest.equals("off"))
           gpioStatusTest = "on";
       else
            gpioStatusTest = "выключено";
        command.put("type","interrupt");
        command.put("content", "From Toggle: " + gpioStatusTest);
        command.saveInBackground(new SaveCallback(){
            @Override
           public void done(ParseException e){
                Snackbar.make(view, "Изменен входной пин", Snackbar.LENGTH_LONG ).setAction("Action", null).show();
            }
        });
    }
});

Также объявите строку String в области видимости MainActivity.

String gpioStatusTest = "off";

Мы закончили работу над приложением! В конце этого урока ваше приложение должно выглядеть примерно так, как показано на рисунке ниже.

Если вы хотите продолжить разработку нашего IoT-приложения, пожалуйста, прочитайте нашу серию IoT Series, пошаговое руководство, которое научит вас основам настройки Raspberry Pi и конкретным приложениям, таким как получение и сохранение объектов с Parse Server с помощью JavaScript.

Наши коды доступны по следующей ссылке:

https://github.com/back4app/iot-raspberry-node

Ссылки

Подключение Raspberry Pi к серверу Parse Server

Настройка Raspberry Pi

Как взаимодействовать Android-приложению с IoT-устройством?

Раздел 1: Основы создания приложения и подключения к серверной части.
Раздел 2: Сохранение и получение объектов в Parse и отображение их в приложении.
Раздел 3: Прослушивание событий в реальном времени с использованием Live Query и отображение их в приложении.


Leave a reply

Your email address will not be published.