Подробности под катом:
Для нашей работы мы воспользуемся инструментарием, который был описан в предыдущей статье — беспроводным программатором 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. Чего-то архи-сложного в этой книге я не нашел, а для наших целей вполне хватит и первого десятка глав книги, благо, что в ней все примеры кода приводятся при работе именно с вышеупомянутой Androd Studio.
Отладку приложений можно проводить, как на программном эмуляторе, так и прямо на смартфоне, переключив его в «режим разработчика».
В предыдущей статье было описано управление тележкой через оконное приложение на Windows. То есть весь код для создания HTTP и UDP соединений, а также логика управления у нас уже по идее присутствует. Поэтому взяв на вооружение слоган компании Oracle «Написано в одном месте, работает везде» мы просто перекинем эти классы в новую программу уже для Android приложения. А вот GUI — графический интерфейс пользователя, по понятным причинам придется оставить там, где он был. Но с другой стороны, на Android всё делается очень похоже и довольно быстро, поэтому в накладе мы не останемся.
ТЫКАЕМ ПАЛЬЦЕМ В ЭКРАН
Итак создаем новый проект «FourWheelRobotControl» в Android Studio.
Это простое приложение и оно будет состоять из активности (activity)
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; } }); }
и макета:
/spoiler/ activity_main.xml
Его писать ручками не надо, он генерится автоматически после ваших творений в редакторе.
Теперь просто перенесём два класса из программы приведенной в предыдущей статье:
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 datar");//на всякий случай, вдруг вышли некорректно pw.println("datar");// Greetings with SERVER Greetings_from_S = br.readLine(); if (Greetings_from_S.equals("ready")) { new Udp_client(); } } catch (Exception e) { e.printStackTrace(); } } }
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"
разрешаем работу в интернет:
И вот он весь полностью.
При запуске мы должны увидеть что-то вроде:
Вот в сущности и всё. Теперь тележкой можно управлять с мобильника. Приложение крайне простое, ибо демонстрационное, без каких-либо «свистелок и перделок», типа поиска телеги в домашней сети, коннекта, дисконнекта и прочих полезных фич, облегчающих жизнь, но затрудняющих понимание кода.
С другой стороны, управление при помощи экранных кнопок это то же самое,
«как пить просто водку, даже из горлышка — в этом нет ничего, кроме томления духа и суеты».
И Веня Ерофеев в этом был прав, безусловно.
Гораздо интереснее, к примеру, рулить телегой при помощи штатных акселерометров того же смартфона.
Причём реализуется сия фича тоже крайне просто, через, так называемые интенты. Правда, создавать свои интенты сложнее, чем пользоваться готовыми. К счастью, пользоваться готовыми никто не запрещает.
АКСЕЛЕРОМЕТР В МАССЫ
Поэтому наш код в 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) {} //Изменение показаний датчиков
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 чисто данные, а сами управляющие команды задавать через HTTP протокол и это было бы безусловно правильнее. Но для это надо редактировать программу на самой 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) { //Как результат получаем строковый массив слов, похожих на произнесенное: ArrayListresult=data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); //и отображаем их в элементе TextView: stroka_otveta = result.toString(); }
А возвращает он массив похожих слов, причем в довольно “мусорной форме», со всякими скобочками и запятыми. И вам лишь остается выбрать слово похожее на то, которое вы сказали. Ну и соответственно, если попалось слово «вперёд», то едем вперёд, если «направо» то направо и так далее. Конечно, надо учитывать, где через «Ё», где запятая лишняя прицепится (скобочки-то я отрезал, а вот на запятые сил уже не хватило).
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) { //Как результат получаем строковый массив слов, похожих на произнесенное: ArrayListresult=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 одной кнопкой
Самое удивительное, что действительно ездиет и слушается голоса (как правило). Но, конечно, интерфейс тормозной; пока скажете, пока распознается и вернётся; короче скорость движения выбирайте заранее небольшую или помещение наоборот поширше.
На этом на сегодня всё, буду рад, если понравилось.
Источник