Программирование и передача данных в «Ардуино» по «воздуху» с помощью ESP8266. Часть Третья. Здравствуй, «ANDROID»

    Предлагаю вам, уважаемые читатели GeekTimes, очередную статью из цикла по использованию микросхемы ESP8266 в качестве беспроводного моста для AVR микроконтроллеров, на примере аппаратной платформы Arduino Uno (Nano). В этот раз для полета на Луну управления платформой мы задействуем вместо компьютера устройство на базе «ANDROID». Ну, например, смартфон.



    Подробности под катом:

    Для нашей работы мы воспользуемся инструментарием, который был описан в предыдущей статье — беспроводным программатором BABUINO и модулем передачи кода и данных МPDE (module for programm and data exchange) прошиваемым в ESP8266.

    Как выяснилось из откликов пользователей, сам программатор, в общем, пришелся ко двору и некоторые личности даже им успешно пользуются. Ну, в принципе, вещь действительно удобная; запустил приложение под виндой, выбрал hex файл из нужной папки и всё — через несколько секунд программка в нужном устройстве безо всяких проводов. Другое дело, что я зря с излишней горячностью нападал на пользователей софта ARDUINO, пишущих на Wiring C и пользующихся ARDUINO IDE с её скетчами и библиотеками. Безусловно, каждый делает как ему удобно — на ардуинском ли Wiring C или на С из AVR studio. Но в итоге, некоторые пользователи почему-то сразу решили, что программатор никак не совместим с ARDUINO софтом.

    На самом деле, проблем с совместимостью, конечно же, никаких нет. Абсолютно также компилируете ваш скетч, где вам удобно до состояния hex файла и совершенно также просто отправляете его через беспроводной программатор на ваш Arduino UNO или NANO.

    Обмен данными под софтом ARDUINO тоже никаких проблем не доставляет. Пишете волшебные строчки:

    Serial.begin(9600);

    а дальше что-то типа:

    receiveByte= Serial.read(); // чтение байта из буфера

    или:

    Serial.write(receiveByte); // запись байта

    И можете обмениваться байтовыми потоками по WI-FI совершенно спокойно. Ибо AVR микроконтроллер шлёт в ESP8266 и получает байты оттуда по последовательному порту UART, настроить работу с которым в Arduino может, как мы видим, любой гуманитарий.

    Теперь же вернёмся к предмету настоящей статьи. Чтобы управлять роботележкой посредством смартфона, безусловно, необходимо написать для этого смартфона соответствующее приложение.

    Я, как человек, месяц назад представлявший этот процесс крайне туманно, сейчас могу со всей ответственностью заявить, что дело в сущности это не сложное. Если конечно, у вас есть хоть какие-то начальные знания в области Java.

    Ибо при написании Android приложений исходный код пишется на Java, а затем компилируется в стандартный байт-код Java с использованием традиционного инструментария Java. Затем с кодом происходят другие интересные вещи, но эти подробности нам здесь не нужны.

    Вроде как, существуют еще возможности для написания приложений на С для отъявленных хардкорщиков ведущих счёт на наносекунды, а также где-то есть какой-то пакет для трансляции вашего кода с Питона, но здесь я ничего подсказать не могу, поскольку уже давно сделал правильный выбор.

    Итак, Java и программный пакет Android Studio — интегрированная среда разработки (IDE) для работы с платформой Android основанная на программном обеспечении IntelliJ IDEA от компании JetBrains, официальное средство разработки Android приложений.



    Если вы уже работаете с ПО от IntelliJ IDEA, то будете приятно удивлены знакомым интерфейсом.

    С основами построения приложений я знакомился по книжке Б.Филлипса и К.Стюарта — «ANDROID» программирование для профессионалов". Как я понял, профессионалами авторы считают читателей, хотя бы немного знакомых с Java SE. Чего-то архи-сложного в этой книге я не нашел, а для наших целей вполне хватит и первого десятка глав книги, благо, что в ней все примеры кода приводятся при работе именно с вышеупомянутой Android Studio.

    Отладку приложений можно проводить, как на программном эмуляторе, так и прямо на смартфоне, переключив его в «режим разработчика».

    В предыдущей статье было описано управление тележкой через оконное приложение на Windows. То есть весь код для создания HTTP и UDP соединений, а также логика управления у нас уже по идее присутствует. Поэтому взяв на вооружение слоган компании Oracle «Написано в одном месте, работает везде» мы просто перекинем эти классы в новую программу уже для Android приложения. А вот GUI — графический интерфейс пользователя, по понятным причинам придется оставить там, где он был. Но с другой стороны, на Android всё делается очень похоже и довольно быстро, поэтому в накладе мы не останемся.

    ТЫКАЕМ ПАЛЬЦЕМ В ЭКРАН



    Итак создаем новый проект «FourWheelRobotControl» в Android Studio.
    Это простое приложение и оно будет состоять из активности (activity)

    Класс MainActivity.java
    import android.content.Context;
    import android.hardware.Sensor;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.ImageButton;
    import android.widget.TextView;
    import android.widget.Toast;
    
    public class MainActivity extends AppCompatActivity
    {
    
        private ImageButton mButtonUP;
        private ImageButton mButtonDOWN;
        private ImageButton mButtonLEFT;
        private ImageButton mButtonRIGHT;
        public static byte direction = 100;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new HTTP_client(40000);
            new Udp_client();
    
            mButtonUP = (ImageButton) findViewById(R.id.imageButtonUP);
    
            mButtonUP.setOnTouchListener(new View.OnTouchListener() {
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
    
                        direction = 3;
    
                        }
                    }
                    if (event.getAction() == MotionEvent.ACTION_UP) {
    
                        direction = 100;
    
    
                    }
    
    
                    return false;
                }
    
    
            });
    
            mButtonDOWN = (ImageButton) findViewById(R.id.imageButtonDown);
    
            mButtonDOWN.setOnTouchListener(new View.OnTouchListener() {
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
    
                        direction = 4;
                        Toast.makeText(MainActivity.this, "вниз " + direction,
                                Toast.LENGTH_SHORT).show();
    
    
                    }
                    if (event.getAction() == MotionEvent.ACTION_UP) {
    
                        direction = 100;
    
                    }
    
                    return false;
                }
            });
    
            mButtonLEFT = (ImageButton) findViewById(R.id.imageButtonLeft);
    
            mButtonLEFT.setOnTouchListener(new View.OnTouchListener() {
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
    
                        direction = 1;
                        Toast.makeText(MainActivity.this, "влево " + direction,
                                Toast.LENGTH_SHORT).show();
                    }
                    if (event.getAction() == MotionEvent.ACTION_UP) {
    
                        direction = 100;
    
                    }
    
                    return false;
                }
            });
    
            mButtonRIGHT = (ImageButton) findViewById(R.id.imageButtonRight);
    
            mButtonRIGHT.setOnTouchListener(new View.OnTouchListener() {
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
                        direction = 2;
                        Toast.makeText(MainActivity.this, "вправо " + direction,
                                Toast.LENGTH_SHORT).show();
    
                    }
                    if (event.getAction() == MotionEvent.ACTION_UP) {
    
                        direction = 100;
    
                    }
    
                    return false;
                }
            });
    
    
        }
    

    и макета:

    layout
       /spoiler/
    activity_main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
        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"
        tools:context="mikhail_akhmetov.fourweelsrobotcontrol.MainActivity">
    
        <ImageButton
            android:id="@+id/imageButtonRight"
            style="@android:style/Widget.Holo.ImageButton"
            android:layout_width="52dp"
            android:layout_height="52dp"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:background="@android:color/holo_orange_dark"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintHorizontal_bias="0.417"
            app:layout_constraintLeft_toRightOf="@+id/imageButtonLeft"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.513"
            app:srcCompat="@android:drawable/ic_media_ff"/>
    
        <ImageButton
            android:id="@+id/imageButtonUP"
            style="@android:style/Widget.ImageButton"
            android:layout_width="52dp"
            android:layout_height="52dp"
            android:layout_marginBottom="8dp"
            android:layout_marginTop="8dp"
            android:background="@android:color/holo_orange_dark"
            app:layout_constraintBottom_toTopOf="@+id/imageButtonDown"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.486"
            app:srcCompat="@android:drawable/arrow_up_float"/>
    
        <ImageButton
            android:id="@+id/imageButtonDown"
            style="@android:style/Widget.ImageButton"
            android:layout_width="52dp"
            android:layout_height="52dp"
            android:layout_marginBottom="57dp"
            android:layout_marginLeft="8dp"
            android:background="@android:color/holo_orange_dark"
            android:fadingEdge="horizontal"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintHorizontal_bias="0.487"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:srcCompat="@android:drawable/arrow_down_float"
            android:layout_marginStart="8dp"/>
    
        <ImageButton
            android:id="@+id/imageButtonLeft"
            style="@android:style/Widget.ImageButton"
            android:layout_width="52dp"
            android:layout_height="52dp"
            android:layout_marginLeft="94dp"
            android:layout_marginStart="94dp"
            android:background="@android:color/holo_orange_dark"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.513"
            app:srcCompat="@android:drawable/ic_media_rew"
            />
    
        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="368dp"
            android:layout_height="227dp"
            android:layout_marginBottom="11dp"
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:layout_marginStart="8dp"
            android:orientation="vertical"
            android:weightSum="1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent">
    
            <TextView
                android:id="@+id/textViewX"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0.28"
                />
    
            <TextView
                android:id="@+id/textViewY"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0.28"
                />
    
            <TextView
                android:id="@+id/textViewZ"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0.23"
                />
    
        </LinearLayout>
    
    </android.support.constraint.ConstraintLayout>
    


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

    Теперь просто перенесём два класса из программы приведенной в предыдущей статье:

    HTTP_client.java
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.io.PrintWriter;
    import java.net.InetAddress;
    import java.net.Socket;
    
    public class HTTP_client extends Thread{
    
        int port;
        String s;
        public static  String host_address="192.168.1.138";// адрес вашего устройства
    
       public  String Greetings_from_S;
        HTTP_client(int port) {
    
            this.port = port;
            start();
    
    
    
        }
    
        public void run() {
    
    
            try   (Socket socket = new Socket(host_address,port)){
    
                PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                pw.println("stop data\r");//на всякий случай, вдруг вышли некорректно
    
                pw.println("data\r");// Greetings with SERVER
    
    
                Greetings_from_S = br.readLine();
    
                if (Greetings_from_S.equals("ready")) {
    
                    new Udp_client();
    
                }
    
    
            } catch (Exception e) {
    
               e.printStackTrace();
    
            }
    
        }
    
    
    }
    
    


    Udp_clent.java
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetAddress;
    
        public class Udp_client extends Thread
        {
    
            
            int i =0;
            byte [] data = {0};
            int udp_port=50000;
            InetAddress addr;
            DatagramSocket ds;
    
    
            public Udp_client()
    
            {
    
    
                try
                {
                    ds = new DatagramSocket();
                    addr = InetAddress.getByName(HTTP_client.host_address);
    
                }
                catch (Exception e)
                {
    
    
    
                }
    
                start();
            }
    
            public void run()  {
    
    
                while (true)
                {
    
    
                  byte temp = MainActivity.direction;
    
                
                    String s = "" + MainActivity.direction;
                    data = s.getBytes();
    
    
                    if(temp!=100 ) {
    
    
                        DatagramPacket pack = new DatagramPacket(data, data.length, addr, udp_port);
                        try {
                            ds.send(pack);
                            i=0;
    
    
                            Thread.sleep(200);
    
                        } catch (Exception e) {
    
    
                        }
                     
                    }
                    else
                    {
                        if(i==0) {
                             s = "" + 0;
                          
                            data = s.getBytes();
    
                            DatagramPacket pack = new DatagramPacket(data, data.length, addr, udp_port);
                            try {
                                ds.send(pack);
    
                                Thread.sleep(200);
    
                            } catch (Exception e) {
    
    
                            }
                        }
    
                        i=1;// перестаем отправлять нулевые пакеты
                    }
    
    
    
                }
            }
    
    
        }
    

    Суть программы осталось той же. MainActivity сначала запускает HTTP и UDP клиенты, а затем ловит нажатия и отжатия экранных кнопок, отправляя код нажатия direction на формирование UDP пакета. А оттуда уже всё — «вперёд»,«назад»,«влево», «вправо» и при отжатии «стоп» уезжают по WI-FI на телегу.

    Кроме всего этого, мы должны немного подредактировать файл так называемого манифеста, который опять же, в основном генерится сам.

    Манифест (manifest) представляет собой файл XML с метаданными, описывающими ваше приложение для ОС Android. Файл манифеста всегда называется AndroidManifest.xml и располагается в каталоге app/manifest вашего проекта.

    Правда там нам работы совсем немного.

    запрещаем повороты экрана:

    android:screenOrientation="portrait"
    

    разрешаем работу в интернет:

    <uses-permission android:name="android.permission.INTERNET"/>
    

    И вот он весь полностью.

    AndroidManifest.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
              package="mikhail_akhmetov.fourweelsrobotcontrol">
    
        <application
            android:allowBackup="true"
    
    
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme"
    
            >
    
            <activity android:name=".MainActivity"
                      android:screenOrientation="portrait"
                >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"
    
                        />
    
                    <category android:name="android.intent.category.LAUNCHER"
    
    
                        />
                </intent-filter>
    
    
            </activity>
        </application>
        <uses-permission android:name="android.permission.INTERNET"/>
    </manifest>
    
    


    При запуске мы должны увидеть что-то вроде:



    Вот в сущности и всё. Теперь тележкой можно управлять с мобильника. Приложение крайне простое, ибо демонстрационное, без каких-либо «свистелок и перделок», типа поиска телеги в домашней сети, коннекта, дисконнекта и прочих полезных фич, облегчающих жизнь, но затрудняющих понимание кода.

    С другой стороны, управление при помощи экранных кнопок это то же самое,
    «как пить просто водку, даже из горлышка — в этом нет ничего, кроме томления духа и суеты».
    И Веня Ерофеев в этом был прав, безусловно.

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

    Причём реализуется сия фича тоже крайне просто, через, так называемые интенты. Правда, создавать свои интенты сложнее, чем пользоваться готовыми. К счастью, пользоваться готовыми никто не запрещает.

    АКСЕЛЕРОМЕТР В МАССЫ



    Поэтому наш код в MainActivity (и только в нём) изменится минимально.

    Добавим переменные для акселерометра:

    private SensorManager mSensorManager;
    private Sensor mOrientation;
    
    private float xy_angle;
    private float xz_angle;
    

    Получим сам датчик от системы и зарегистрируем его как слушатель.

    mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); // Получаем менеджер сенсоров
    mOrientation = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); // Получаем датчик положения
    mSensorManager.registerListener(this, mOrientation, SensorManager.SENSOR_DELAY_NORMAL);
    

    Имплементируем сам интерфейс слушателя:

    public class MainActivity implements SensorEventListener
    
    

    И пропишем четыре обязательных метода из которых воспользуемся только последним. Методом, отрабатывающим при изменении показаний акселерометра. Вообще-то, я хотел акселерометр сам опрашивать, как обычную периферию с периодом около 100 мс, так как было подозрение (из-за названия onChanged), что метод отрабатывает слишком часто. Но там всё private и фиг за интерфейсы проберёшься.

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) { //Изменение точности показаний датчика
    }
    
    @Override
    protected void onResume() {
        super.onResume();
    }
    
    @Override
    protected void onPause() {
    
        super.onPause();
    }
    
    @Override
    public void onSensorChanged(SensorEvent event) {} //Изменение показаний датчиков
    

    В итоге MainActivity.java примет следующий вид
    import android.hardware.SensorEvent;
    import android.hardware.SensorEventListener;
    import android.hardware.SensorManager;
    import android.content.Context;
    import android.hardware.Sensor;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.widget.ImageButton;
    import android.widget.TextView;
    
    
    public class MainActivity extends AppCompatActivity implements SensorEventListener
    {
    
        private ImageButton mButtonUP;
        private ImageButton mButtonDOWN;
        private ImageButton mButtonLEFT;
        private ImageButton mButtonRIGHT;
        public static byte direction = 100;
    
        private SensorManager mSensorManager;
        private Sensor mOrientation;
    
        private float xy_angle;
        private float xz_angle;
      
    
        private int x;
        private int y;
       
    
        private TextView xyView;
        private TextView xzView;
        private TextView zyView;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new HTTP_client(40000);
            new Udp_client();
    
            mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); // Получаем менеджер сенсоров
            mOrientation = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); // Получаем датчик положения
            mSensorManager.registerListener(this, mOrientation, SensorManager.SENSOR_DELAY_NORMAL);
    
    
            xyView = (TextView) findViewById(R.id.textViewX);  //
            xzView = (TextView) findViewById(R.id.textViewY);  // Наши текстовые поля для вывода показаний
            zyView = (TextView) findViewById(R.id.textViewZ);// сюда пихнем "direction"
    
    
            mButtonUP = (ImageButton) findViewById(R.id.imageButtonUP);
            mButtonDOWN = (ImageButton) findViewById(R.id.imageButtonDown);
            mButtonLEFT = (ImageButton) findViewById(R.id.imageButtonLeft);
            mButtonRIGHT = (ImageButton) findViewById(R.id.imageButtonRight);
    		// кнопки не используются
           
    
        }
    
    
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) { //Изменение точности показаний датчика
        }
    
        @Override
        protected void onResume() {
            super.onResume();
        }
    
        @Override
        protected void onPause() {
    
            super.onPause();
        }
    
        @Override
        public void onSensorChanged(SensorEvent event) { //Изменение показаний датчиков
    
    
            xy_angle = event.values[0]*10; //Плоскость XY
            xz_angle = event.values[1]*10; //Плоскость XZ
           
    
            x=-(int) xy_angle*1;
            y=-(int) xz_angle*1;
           
    
            xyView.setText("         x =        "+ String.valueOf(x));
            xzView.setText("         y =         "+String.valueOf(y));
            zyView.setText("         direction     "+String.valueOf(direction));
    
    
            if(y>-40&&y<-20){
    
                direction=100;// скорость вперед ноль, никуда не едем, смотрим есть ли повороты
    
                        if (x>10){//скорость вправо от 10 до 30 диапазон 20 единиц
    
                            direction=(byte)(x+30);
                            if (direction>60){direction=60;}
    
                        }
                        if(x<-10){
                            direction=(byte)(-x+50);
                            if (direction>80){direction=80;}
    
                        }
    
            }
    
            else {
    
                if (y > -20) {// едем вперед, диапазон -20: 20 итого 40 единиц
    
                    direction = (byte) (y / 2 + 10);
                    if (direction > 20) {
                        direction = 20;
                    }
                    ;
                }
    
    
                if (y < -40) {// едем назад, диапазон -40: -80 итого 40 единиц
    
                    direction = (byte) (-y - 20);
                    if (direction > 40) {
                        direction = 40;
                    }
                    ;
    
    
                }
    
    
            }
        }
    }
    
    
      //  итого если direction 100 стоп
      //  1-20 вперед , угол от -20
      //  21-40 назад , угол от -40
      //  41-60 направо, угод 10
      //  61-80 налево, угол от -10
    
    


    Программа выводит показания датчиков по двум осям на экран смартфона. Вместо третьей оси, которая не используется, выводится переменная направления «direction». И эти же данные бегут в виде байтового потока на тележку. Правда, из-за того, что поток чисто байтовый, определить, где команда «вперед» или «стоп» было бы затруднительно. Поэтому я поступил просто: каждому направлению и углу наклона соответствует свой диапазон чисел. Грубо говоря 1-20 это «вперёд» с соответствующей скоростью, 21-40 это «назад» и так далее. Конечно, можно было бы передавать по UDP чисто данные, а сами управляющие команды задавать через TCP протокол и это было бы безусловно правильнее. Но для это надо редактировать программу на самой ESP8266, чего мне пока не хочется.

    Итак, телега катается по квартире, чутко реагируя на наклоны моего GalaxyS7, но и это как говаривал небезызвестный Веня ещё не то.

    «Теперь же я предлагаю вам последнее и наилучшее. «Венец трудов, превыше всех наград», как сказал поэт. Короче, я предлагаю вам коктейль «Сучий потрох», напиток, затмевающий все. Это уже не напиток — это музыка сфер. „

    В наш век Сири и Алексы, чего-то там вертеть руками? Пусть слушается голосового управления!

    А ТЕПЕРЬ СЛУШАЙ МЕНЯ!



    Цитирую:

    На самом деле работать с распознаванием и синтезом речи в Android очень просто. Все сложные вычисления скрыты от нас в довольно элегантную библиотеку с простым API. Вы сможете осилить этот урок, даже если имеете весьма поверхностные знания о программировании для Android.

    На самом деле же, у меня всё получилось довольно коряво, но скорее всего потому, что я использовал самый простой вариант, а как следует в данном API не копался. Делается всё тоже через интенты, при помощи которых мы обращаемся к голосовому движку Гугл, а именно к его функции распознавания речи. Поэтому потребуется рабочий Интернет.

    Опять меняется лишь MainActivity.java, хотя я еще немного изменил и сам макет (четыре кнопки теперь там вовсе ни к чему, хватит и одной).

    В MainActivity.java добавилось следующее:

    Тот самый интент:

    Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
    intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Ну, скажи, куда ехать-то??? ");
    startActivityForResult(intent, Print_Words);
    
    

    И то, что он возвращает:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    
        //Проверяем успешность получения обратного ответа:
        if (requestCode==Print_Words && resultCode==RESULT_OK) {
            //Как результат получаем строковый массив слов, похожих на произнесенное:
            ArrayList<String>result=data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
            //и отображаем их в элементе TextView:
    
            stroka_otveta = result.toString();
    }
    
    

    А возвращает он массив похожих слов, причем в довольно “мусорной форме», со всякими скобочками и запятыми. И вам лишь остается выбрать слово похожее на то, которое вы сказали. Ну и соответственно, если попалось слово «вперёд», то едем вперёд, если «направо» то направо и так далее. Конечно, надо учитывать, где через «Ё», где запятая лишняя прицепится (скобочки-то я отрезал, а вот на запятые сил уже не хватило).

    В итого сам текст MainActivity.java стал таким
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.ImageButton;
    import android.widget.TextView;
    import java.util.ArrayList;
    import android.content.Intent;
    import android.speech.RecognizerIntent;
    
    public class MainActivity extends AppCompatActivity {
    
        private ImageButton mButtonUP;
        public static byte direction = 100;
        public String stroka_otveta;
    
        private static final int Print_Words = 100;
        private TextView EnteredText1;
        private TextView EnteredText2;
        public static TextView EnteredText3;
        private boolean slovo_raspoznano =false;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Udp_client();
            new HTTP_client(40000);
    
            EnteredText1 = (TextView) findViewById(R.id.textViewX);  //
            EnteredText2 = (TextView) findViewById(R.id.textViewY);  //
            EnteredText3 = (TextView) findViewById(R.id.textViewZ);  //
    
            mButtonUP = (ImageButton) findViewById(R.id.imageButtonUP);
    
            mButtonUP.setOnTouchListener(new View.OnTouchListener() {
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_DOWN){
    
                        if(HTTP_client.ok){ //Вызываем RecognizerIntent для голосового ввода и преобразования голоса в текст:
                            EnteredText3.setText("             http клиент подсоединен");
                            Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                            intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                            intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Ну, скажи, куда ехать-то??? ");
                            startActivityForResult(intent, Print_Words);
    
                        }
                        else {
                            EnteredText3.setText("            нетути никого");
    
                        }
    
    		          }
                    if (event.getAction() == MotionEvent.ACTION_UP ){
    
                    }
    
                    return false;
                }
    
    
            });
    
    
        }
    
    
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    
            //Проверяем успешность получения обратного ответа:
            if (requestCode==Print_Words && resultCode==RESULT_OK) {
                //Как результат получаем строковый массив слов, похожих на произнесенное:
                ArrayList<String>result=data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
                //и отображаем их в элементе TextView:
    
                stroka_otveta = result.toString();
        }
            StringBuffer sb = new StringBuffer(stroka_otveta);
            sb.deleteCharAt(stroka_otveta.length()-1);
            sb.deleteCharAt(0);
    
    
            stroka_otveta=sb.toString();
            String[] words = stroka_otveta.split("\\s"); // Разбиение строки на слова с помощью разграничителя (пробел)
            // Вывод на экран
    
            for(int i = 0; i< words.length;i++) {
    
                    if(words[i].equals("налево,")||words[i].equals("налево")) {
    
                        direction = 1;
                        slovo_raspoznano=true;
                        stroka_otveta=words[i];
                    }
    
                    if(words[i].equals("направо,")||words[i].equals("направо")){
                       direction=2;
                        slovo_raspoznano=true;
                        stroka_otveta=words[i];
                    }
    
                    if(words[i].equals("назад,")|| words[i].equals("назад")){
                        direction=4;
                        slovo_raspoznano=true;
                        stroka_otveta=words[i];
                    }
    
                    if(words[i].equals("вперед,")|| words[i].equals("вперёд,")|| words[i].equals("вперед") || words[i].equals("вперёд")){
                        direction=3;
                        slovo_raspoznano=true;
                        stroka_otveta=words[i];
                    }
    
                if(words[i].equals("стоп,")|| words[i].equals("стой,")|| words[i].equals("стоп") || words[i].equals("стой")){
                    direction=100;
                    slovo_raspoznano=true;
                    stroka_otveta=words[i];
                }
    
    
            }
    
    
                if(!slovo_raspoznano){
                    direction=100;
                   stroka_otveta="говори внятно, а то них... непонятно";
                }
    
            EnteredText1.setText("          "+direction+"  " +stroka_otveta);
            slovo_raspoznano=false;
    
    
            super.onActivityResult(requestCode, resultCode, data);
        }
    
    }
    
    

    Ну и до кучи макет c одной кнопкой

    activity_main.xml.
    	<?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
    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"
    tools:context="mikhail_akhmetov.fourwheelsrobotandroidvoice.MainActivity">
    
        <ImageButton
            android:id="@+id/imageButtonUP"
            style="@android:style/Widget.ImageButton"
            android:layout_width="160dp"
            android:layout_height="146dp"
            android:layout_marginTop="8dp"
            android:background="@android:color/holo_orange_dark"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@android:drawable/arrow_up_float"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"/>
    
        <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="368dp"
        android:layout_height="227dp"
        android:layout_marginBottom="11dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:orientation="vertical"
        android:weightSum="1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent">
    
        <TextView
            android:id="@+id/textViewTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="                          ТЕЛЕМЕТРИЯ   С  ТЕЛЕГИ"
            android:textColor="@android:color/holo_red_dark"/>
    
        <TextView
            android:id="@+id/textViewX"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0.28"
            />
    
        <TextView
            android:id="@+id/textViewY"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0.28"
            />
    
        <TextView
            android:id="@+id/textViewZ"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0.23"
            />
    
    </LinearLayout>
    
    </android.support.constraint.ConstraintLayout>
    
    

    Самое удивительное, что действительно ездиет и слушается голоса (как правило). Но, конечно, интерфейс тормозной; пока скажете, пока распознается и вернётся; короче скорость движения выбирайте заранее небольшую или помещение наоборот поширше.

    На этом на сегодня всё, буду рад, если понравилось.

    Поделиться публикацией
    Никаких подозрительных скриптов, только релевантные баннеры. Не релевантные? Пиши на: adv@tmtm.ru с темой «Полундра»

    Зачем оно вам?
    Реклама
    Комментарии 23
    • 0
      А как реализована обработка кейса когда связь разрывается? Останавливается ли телега при реальном разрыве связи и как это реализовано?
      • 0
        ну эт, от самой тележки зависит. в данном случае остановится, т.к. в буфере нули будут, а нули для неё, это «стоп».
        • 0
          А реализацию на голых сокетах не пробовали? Я, например, похожую телегу делаю на RPi, и решил что лучшим вариантом для контролирования разрыва будет сокет — ведь он кидает исключение при разрыве связи.
          • 0
            Да там есть TCP и UDP сокеты на ESP8266 (см. предыдущую статью). Реализацию можно любую сделать. А самой ардуине все равно, есть данные -едем, нет — стоим.
            • +1
              Это не лучший вариант. Так можно контролировать только прямое соединение, а соединение через роутер уже имеет ньюансы — разрыв словится только когда пропадёт соединение клиент-роутер, а не смарфон-роутер. А потом когда смарт переподключится и пошлёт следующий пакет, факт разрыва связи будет установлен только по тому что нумерация пакетов уже другая будет. Поэтому в таких случаях самый надёжный вариант — это жёсткие таймауты(например 1000мс) и UDP.
              • 0
                Когда разорвется связь смартфон-рутер, esp8266 будет по барабану, так как UDP сервер поднятый на ней уже будет работать и будет знать IP адрес смартфона. Поэтому двусторонний UDP канал поднимается снова без проблем. A TCP там используется только для команд управления, поэтому если номер пакета будет другой — вообще не страшно. Команда управления гарантированно в пакет влазит.
                • 0
                  Проблема не в IP-адресе, а в том что не будет некоторое время команд и приёмник никак не узнает причину — либо источник и правда не даёт команд, или потеряна связь.
                  • 0
                    ну у каждого свои ТЗ. В данном случае это не важно. При потере данных всё здесь просто останавливается, пока не установится новое соединение.
                    • 0
                      А как оно поймёт что соединение пропало? будет выполнять последнюю команду… пока не поступит новая, а она не поступает.
                      • 0
                        здесь наоборот, каждые 200 мс по udp отправляется команда, допустим "«вперед». Пока телега их получает — едет вперед. Обрыв, в буфере UART ноль — остановка.
                        • 0
                          Команда принимается считанные микросекунды, остальные 200мс с точки зрения приёмника НИЧЕГО не происходит, но тележка почему-то едет, хотя не должна? В целом, у вас получается работа с таймаутами — 200мс нет новых команд, срабатывает таймаут и останов. И, судя по всему, это происходит именно в WiFi-чипе но суть остаётся такая. И команды на самом деле у вас имеют смысл не «вперёд» а «вперёд следующие 200мс».
                          • 0
                            не, не так. это я забыл нэмножкэ старый код (там разные варианты были). при обрыве так и едет исполняя текущую команду (на телеге эхолокаторы стоят, чтобы об стену не убиться, но не суть). А как только восстановится канал (сервер на телеге слушает постоянно) работает дальше. 200 мс таймаут, я вспомнил, был у меня для ручного управления, так сказать время реакции человека. Чаше нет смысла слать пакеты.
                            • 0
                              Смотри, эхолокаторы не видят котов — будут их таранить :-D

                              Время реакции человека — 100мс, у вас ещё +200 в наихудшем случае — эта задержка уже будет существенной. Я бы снизил до 20-50мс или величину времени за которую тележка проезжает не более 5мм, чтобы это не стало узким местом.
          • –1

            Для esp8266 в качестве сетевого интерфейса ардуины есть прошивка esp-link, которая умеет всё тоже само только лучше. Плюс можно по воздуху прошивать, не все ардуины, но Nano точно.

            • 0

              Автор объяснил свое отношение к esp-link в прошлой публикации...

            • 0
              ездиет


              мои глаза…
              • 0
                В Москве многие так говорят — ходиют, ездиют. Я не знаю в чем причина такого феномена. Вроде столица, кому как не москвичам знать русский. Говорю просто потому, что столкнулся с этим года три назад.
              • 0
                объясните, зачем тут ардуино, когда используется ESP8266?? С его скоростью, I2C и SPI и парой сдфиговых регистров (не обязательно) можно и так управлять машинкой.
                • +1
                  Можно, но это классические костыли. Здесь же разделение сущностей — ESP8266 занимается коммуникацией, а ардуина — логикой управления, что даёт возможность НЕЗАВИСИМО развивать оба модуля.
                • 0
                  Пишете волшебные строчки:
                  Serial.begin(9600);

                  … и с ходу получаете проблемы. Пока у ESP работает serial — все остальное висит и это подлагивание хорошо заметно. Поэтому всегда стоит использовать максимальную скорость передачи данных.
                  • 0
                    Проблема в том что часто на ардуинке переносят serial на другой порт и используется программная реализация, а там огромные проблемы с большими скоростями, поэтому 9600 работает в любом случае и пока хватает этой скорости — нет причин менять.
                    • 0
                      да, ладно… чему там висеть на 9600. вообще ни разу проблем не было. Только если действительно программно реализовывать, да и то я сильно сомневаюсь.
                    • 0
                      по акселерометру в андроиде — добавьте lowpass фильтр и движения станут намного более гладкими

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