Создаем Android-приложение для управления домашним роботом через Bluetooth
В этой статье представлена пошаговая инструкция, которая поможет вам самостоятельно создать приложение для Android-смартфона, предназначенное для управления чем-либо через Bluetooth. Для демонстрации мы подробно разберем пример мигания светодиодом на Arduino по командам с телефона или планшета. В результате выполнения наших инструкций вы научитесь делать вот так:
Для управления домашним роботом достаточно добавить кнопок и обработать их команды на стороне Arduino.
Что для этого потребуется
- Любая Arduino-совместимая плата
- Bluetooth-модуль
- Устройство на котором установлена ОС Android
В качестве Bluetooth-модуля лучше всего использовать HC-05. Его легко купить в китайском интернет магазине или на eBay. Модуль питается от 3.3 В, но его линии I/O могут работать и с 5-вольтовой логикой, что позволяет подключать его UART к Arduino.
Подключение Bluetooth-модуля к Arduino
Так теперь нам нужно подключить нашу Arduino с Bluetooth. Если на Arduino нет вывода с 3.3В , а только 5В то нужен будет поставить стабилизатор чтобы снизить питание. Назначение выводов HC-05 легко найти в интернете. Для использования рекомендуем вам сделать плату с выведенными линиями питания, Rx и Tx. Подключение к Arduino необходимо производить в следующем порядке:
- вывод Arduino 3.3В или (5В через стабилизатор!) — к 12 пину модуля Bluetooth
- вывод Arduino GND — к 13 пину модуля Bluetooth
- вывод Arduino TX — к 2 пину модуля RX Bluetooth
- вывод Arduino RX — к 1 пину модуля TX Bluetooth
После подключения необходимо проверить работоспособность Bluetooth модуля. Подключим Светодиод к 12 выводу Arduino и загрузим на плату следующий скетч:
char incomingByte; // входящие данные int LED = 12; // LED подключен к 12 пину void setup() < Serial.begin(9600); // инициализация порта pinMode(LED, OUTPUT); //Устанавливаем 12 вывод как выход Serial.println("Press 1 to LED ON or 0 to LED OFF. "); >void loop() < if (Serial.available() >0) < //если пришли данные incomingByte = Serial.read(); // считываем байт if(incomingByte == '0') < digitalWrite(LED, LOW); // если 1, то выключаем LED Serial.println("LED OFF. Press 1 to LED ON!"); // и выводим обратно сообщение >if(incomingByte == '1') < digitalWrite(LED, HIGH); // если 0, то включаем LED Serial.println("LED ON. Press 0 to LED OFF!"); >> >
Теперь скачиваем из Play Market программу Bluetooth-терминал и устанавливаем его. Включаем нашу Arduino. В приложении Нажимаем кнопку меню->Connect a device-Secure.
Тем самым ваше устройство начнём искать Bluetooth поблизости. Наш модуль должен называться HC-05. Вам потребуется выписать его MAC-адрес, так он понадобится в дальнейшем. Как только он обнаружит устройство HC-05 выберите его. Пароль, если потребуется: 1234 (это стандартный код). После того как вы подключились к нему у вас должно вывести сообщение которое пришло в Bluetooth терминал от Arduino: “Press 1 to LED ON or 0 to LED OFF..” Далее введите 1 и нажмите отправить. Тем самым вы посылаете цифру 1 через Bluetooth на Arduino. Как только он примет цифру 1 должен загореться светодиод подключенный к 12 выводу Arduino. После введите цифру 0 и светодиод должен погаснуть. Если всё получилось переходим дальше.
Установка Android SDK
Скачиваем с официального сайта программу для создания приложений для android любых моделей. Распаковываем архив, запускаем SDK Manager.exe и устанавливаем программу. Вам предложат установить API, и версию android для которой вы в дальнейшем будете устанавливать приложения.
Android SDK Manager
После нажимаем кнопку Install, ждём когда завершиться установка и закрываем окно.
Заходим в саму программу, она находиться в папке eclipse/eclipse.exe. После открытия программы в диалоговом окне необходимо указать директорию для хранения будущих проектов. Лучше создавать папку на локальном диске, используя при этом только латинские буквы.
Создание приложения
Выбираем File->New->Project.
Так как мы создаём приложение для android, выбираем Android-> Android Application Project, и нажимаем Next
Следующее диалоговое окно:
Application Name -> пишем имя приложение,
Project Name -> пишем имя проекта,
Package Name -> Ни чего не пишем он создается автоматически!
Minimum Required SDK -> это минимальные требование указываем нашу версию Android у меня 4.1 её я и выбираю.
Target SDK -> выбираем вашу версию Android
Compile with -> выбираем вашу версию Android
Theme: для начала я бы советовал выбрать None.
Нажимаем Next.
В следующем окне ничего менять не нужно. Просто жмем Next.
Далее нам предлагают создать свою иконку для приложения, можете изменить стандартный ярлык загрузив свою картинку, я же для начала предлагаю просто нажать Next.
В следующем необходимо выбрать пункт меню Blank Activity и нажимать Next.
Жмем Finish и через несколько секунд открывается главное окно нашей программы. Выбираем вкладку Activity_main.xml и видим наш редактор:
- Файлы нашего проекта.
- Run Запуск эмулятора для проверки программы на наличие ошибок
- Панель кнопок текста и многое другое от сюда вы будите выбирать что вам нужно и добавлять в качестве элементов приложения
- Для выбора размера дисплея вашего телефона или планшета
- Выбор ориентации. Два вида: горизонтальный и вертикальный
- API уровень (лучше не трогать)
- Тут будет отображаться всё то что вы добавили в приложение, так же тут можно переименовать ваши добавленные элементы или удалять их.
- Показывает свойства элемента, его размер цвет и т.д., так же тут можно редактировать элемент
- Показывает наличие ошибок.
- Выбор редактирования (графический либо текстовой). Для начинающих конечно лучше пользоваться графическим режимом
- Окно вашего приложения , можно видеть интерфейс будущего приложения
Теперь добавим две кнопки в интерфейс приложения. Выбираем элемент Button и переносим его на форму.
Справа вверху мы видим объекты которые мы добавили. Так же важно, какой из объектов выбран в данный момент. Справа внизу можно редактировать кнопку, давайте изменим текст подписи кнопки и его цвет.
Для этого в поле свойств элемента «Text» введите, вместо button1, значение «ВКЛ», а у button2 — “ВЫКЛ”. Должно получиться вот так:
Мы можем запустить только что созданное приложение на эмуляторе. Идем в настройки запуска «Run» → Run Configurations», в левой части нажимаем на «Android Application». Появляется новая конфигурация «New_configuration». В правой части окна выбираем вкладку «Target» и выбираем опцию «Launch on all compatible devices/AVD» и добавляем устройство. Проверяем что кнопки появились и их можно нажимать. Если всё хорошо — продолжаем дальше.
Теперь в файлах проекта выбираем bin->AndroidManifest.hml
Теперь нажмём снизу на AndroudManifest.hml
В этот файл нам нужно будет добавить две строки:
Они будут запрашивать включение Bluetooth при старте приложение, если он будет выключен приложение попросит пользователя его включить. Добавить его нужно сюда:
Далее откроем другой файл: src->com.example(name)
В этом файле и будет наш основной код. Все его содержимое нужно удалить и вставить вот этот код:
package com.example.NAME;//Вместо NAME введите имя вашего проекта import java.io.IOException; import java.io.OutputStream; import java.util.UUID; import com.example.NAME.R; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.Toast; public class MainActivity extends Activity < private static final String TAG = "bluetooth1"; Button button1, button2;//Указываем id наших кнопок private static final int REQUEST_ENABLE_BT = 1; private BluetoothAdapter btAdapter = null; private BluetoothSocket btSocket = null; private OutputStream outStream = null; // SPP UUID сервиса private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); // MAC-адрес Bluetooth модуля private static String address = "00:00:00:00:00"; //Вместо “00:00” Нужно нудет ввести MAC нашего bluetooth /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button1 = (Button) findViewById(R.id.button1); //Добавляем сюда имена наших кнопок button2 = (Button) findViewById(R.id.button2); btAdapter = BluetoothAdapter.getDefaultAdapter(); checkBTState(); button1.setOnClickListener(new OnClickListener() //Если будет нажата кнопка 1 то < public void onClick(View v) < sendData("1"); // Посылаем цифру 1 по bluetooth Toast.makeText(getBaseContext(), "Включаем LED", Toast.LENGTH_SHORT).show(); //выводим на устройстве сообщение >>); button2.setOnClickListener(new OnClickListener() < public void onClick(View v) < sendData("0"); // Посылаем цифру 1 по bluetooth Toast.makeText(getBaseContext(), "Выключаем LED", Toast.LENGTH_SHORT).show(); >>); > @Override public void onResume() < super.onResume(); Log.d(TAG, ". onResume - попытка соединения. "); // Set up a pointer to the remote node using it's address. BluetoothDevice device = btAdapter.getRemoteDevice(address); // Two things are needed to make a connection: // A MAC address, which we got above. // A Service ID or UUID. In this case we are using the // UUID for SPP. try < btSocket = device.createRfcommSocketToServiceRecord(MY_UUID); >catch (IOException e) < errorExit("Fatal Error", "In onResume() and socket create failed: " + e.getMessage() + "."); >// Discovery is resource intensive. Make sure it isn't going on // when you attempt to connect and pass your message. btAdapter.cancelDiscovery(); // Establish the connection. This will block until it connects. Log.d(TAG, ". Соединяемся. "); try < btSocket.connect(); Log.d(TAG, ". Соединение установлено и готово к передачи данных. "); >catch (IOException e) < try < btSocket.close(); >catch (IOException e2) < errorExit("Fatal Error", "In onResume() and unable to close socket during connection failure" + e2.getMessage() + "."); >> // Create a data stream so we can talk to server. Log.d(TAG, ". Создание Socket. "); try < outStream = btSocket.getOutputStream(); >catch (IOException e) < errorExit("Fatal Error", "In onResume() and output stream creation failed:" + e.getMessage() + "."); >> @Override public void onPause() < super.onPause(); Log.d(TAG, ". In onPause(). "); if (outStream != null) < try < outStream.flush(); >catch (IOException e) < errorExit("Fatal Error", "In onPause() and failed to flush output stream: " + e.getMessage() + "."); >> try < btSocket.close(); >catch (IOException e2) < errorExit("Fatal Error", "In onPause() and failed to close socket." + e2.getMessage() + "."); >> private void checkBTState() < // Check for Bluetooth support and then check to make sure it is turned on // Emulator doesn't support Bluetooth and will return null if(btAdapter==null) < errorExit("Fatal Error", "Bluetooth не поддерживается"); >else < if (btAdapter.isEnabled()) < Log.d(TAG, ". Bluetooth включен. "); >else < //Prompt user to turn on Bluetooth Intent enableBtIntent = new Intent(btAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); >> > private void errorExit(String title, String message) < Toast.makeText(getBaseContext(), title + " - " + message, Toast.LENGTH_LONG).show(); finish(); >private void sendData(String message) < byte[] msgBuffer = message.getBytes(); Log.d(TAG, ". Посылаем данные: " + message + ". "); try < outStream.write(msgBuffer); >catch (IOException e) < String msg = "In onResume() and an exception occurred during write: " + e.getMessage(); if (address.equals("00:00:00:00:00:00")) msg = msg + ".\n\nВ переменной address у вас прописан 00:00:00:00:00:00, вам необходимо прописать реальный MAC-адрес Bluetooth модуля"; msg = msg + ".\n\nПроверьте поддержку SPP UUID: " + MY_UUID.toString() + " на Bluetooth модуле, к которому вы подключаетесь.\n\n"; errorExit("Fatal Error", msg); >> >
ОБЯЗАТЕЛЬНО! Введите вместо 00:00:00:00:00 МАС вашего Bluetooth модуля, который можно узнать через Bluetooth терминал.
Ваше приложение готово. Теперь нам нужно проверить, как оно поведёт себя на устройстве. Запустите для этого симулятор. Если он запустился нормально без ошибок, то в папке, где вы создавали свой проект, будет создан файл с вашей программой. Его необходимо скопировать и установить на свое устройство.
Работа приложения
При нажатии на кнопку “Вкл” ваше Android-устройство передаст через Bluetooth цифру 1 и, как только Arduino примет цифру 1, светодиод загорится. При нажатии на кнопку “Выкл” передается цифра 2 и светодиод выключится, как показано на видео в начале статьи. Всё просто))
Мы будем очень рады, если вы поддержите наш ресурс и посетите магазин наших товаров shop.customelectronics.ru.
Машинка на Arduino, управляемая Android-устройством по Bluetooth, — код приложения и мк (часть 2)
В первой части я описал физическую часть конструкции и лишь небольшой кусок кода. Теперь рассмотрим программную составляющую — приложение для Android и скетч Arduino.
Вначале приведу подробное описание каждого момента, а в конце оставлю ссылки на проекты целиком + видео результата, которое должно вас разочаровать ободрить.
Android-приложение
Программа для андроида разбита на две части: первая — подключение устройства по Bluetooth, вторая — джойстик управления.
Предупреждаю — дизайн приложения совсем не прорабатывался и делался на тяп-ляп, лишь бы работало. Адаптивности и UX не ждите, но вылезать за пределы экрана не должно.
Верстка
Стартовая активность держится на верстке, элементы: кнопки и layout для списка устройств. Кнопка запускает процесс нахождения устройств с активным Bluetooth. В ListView отображаются найденные устройства.
Экран управления опирается на верстку, в которой есть только кнопка, которая в будущем станет джойстиком. К кнопки, через атрибут background, прикреплен стиль, делающий ее круглой.
TextView в финальной версии не используется, но изначально он был добавлен для отладки: выводились цифры, отправляемые по блютузу. На начальном этапе советую использовать. Но потом цифры начнут высчитываться в отдельном потоке, из которого сложно получить доступ к TextView.
Файл button_control_circle.xml (стиль), его нужно поместить в папку drawable:
Также нужно создать файл item_device.xml, он нужен для каждого элемента списка:
Манифест
На всякий случай приведу полный код манифеста. Нужно получить полный доступ к блютузу через uses-permission и не забыть обозначить вторую активность через тег activity.
Основная активность, сопряжение Arduino и Android
Наследуем класс от AppCompatActivity и объявляем переменные:
public class MainActivity extends AppCompatActivity < private BluetoothAdapter bluetoothAdapter; private ListView listView; private ArrayListpairedDeviceArrayList; private ArrayAdapter pairedDeviceAdapter; public static BluetoothSocket clientSocket; private Button buttonStartControl; >
Метод onCreate() опишу построчно:
@Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); //обязательная строчка //прикрепляем ранее созданную разметку setContentView(R.layout.activity_main); //цепляем кнопку из разметки Button buttonStartFind = (Button) findViewById(R.id.button_start_find); //цепляем layout, в котором будут отображаться найденные устройства listView = (ListView) findViewById(R.id.list_device); //устанавливаем действие на клик buttonStartFind.setOnClickListener(new View.OnClickListener() < @Override public void onClick(View v) < //если разрешения получены (функция ниже) if(permissionGranted()) < //адаптер для управления блютузом bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if(bluetoothEnabled()) < //если блютуз включен (функция ниже) findArduino(); //начать поиск устройства (функция ниже) >> > >); //цепляем кнопку для перехода к управлению buttonStartControl = (Button) findViewById(R.id.button_start_control); buttonStartControl.setOnClickListener(new View.OnClickListener() < @Override public void onClick(View v) < //объект для запуска новых активностей Intent intent = new Intent(); //связываем с активностью управления intent.setClass(getApplicationContext(), ActivityControl.class); //закрыть эту активность, открыть экран управления startActivity(intent); >>); >
Нижеприведенные функции проверяют, получено ли разрешение на использование блютуза (без разрешение пользователя мы не сможем передавать данные) и включен ли блютуз:
private boolean permissionGranted() < //если оба разрешения получены, вернуть true if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH) == PermissionChecker.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_ADMIN) == PermissionChecker.PERMISSION_GRANTED) < return true; >else < ActivityCompat.requestPermissions(this, new String[] , 0); return false; > > private boolean bluetoothEnabled() < //если блютуз включен, вернуть true, если нет, вежливо попросить пользователя его включить if(bluetoothAdapter.isEnabled()) < return true; >else < Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, 0); return false; >>
Если все проверки пройдены, начинается поиск устройства. Если одно из условий не выполнено, то высветится уведомление, мол, «разрешите\включите?», и это будет повторяться, пока проверка не будет пройдена.
Поиск устройства делится на три части: подготовка списка, добавление в список найденных устройств, установка соединения с выбранным устройством.
private void findArduino() < //получить список доступных устройств SetpairedDevice = bluetoothAdapter.getBondedDevices(); if (pairedDevice.size() > 0) < //если есть хоть одно устройство pairedDeviceArrayList = new ArrayList<>(); //создать список for(BluetoothDevice device: pairedDevice) < //добавляем в список все найденные устройства //формат: "уникальный адрес/имя" pairedDeviceArrayList.add(device.getAddress() + "/" + device.getName()); >> //передаем список адаптеру, пригождается созданный ранее item_device.xml pairedDeviceAdapter = new ArrayAdapter(getApplicationContext(), R.layout.item_device, R.id.item_device_textView, pairedDeviceArrayList); listView.setAdapter(pairedDeviceAdapter); //на каждый элемент списка вешаем слушатель listView.setOnItemClickListener(new AdapterView.OnItemClickListener() < @Override public void onItemClick(AdapterView>adapterView, View view, int i, long l) < //через костыль получаем адрес String itemMAC = listView.getItemAtPosition(i).toString().split("/", 2)[0]; //получаем класс с информацией об устройстве BluetoothDevice connectDevice = bluetoothAdapter.getRemoteDevice(itemMAC); try < //генерируем socket - поток, через который будут посылаться данные Method m = connectDevice.getClass().getMethod( "createRfcommSocket", new Class[]); clientSocket = (BluetoothSocket) m.invoke(connectDevice, 1); clientSocket.connect(); if(clientSocket.isConnected()) < //если соединение установлено, завершаем поиск bluetoothAdapter.cancelDiscovery(); >> catch(Exception e) < e.getStackTrace(); >> >); >
Когда Bluetooth-модуль, повешенный на Arduino (подробнее об этом далее), будет найден, он появится в списке. Нажав на него, вы начнете создание socket (возможно, после клика придется подождать 3-5 секунд или нажать еще раз). Вы поймете, что соединение установлено, по светодиодам на Bluetooth-модуле: без соединения они мигают быстро, при наличии соединения заметно частота уменьшается.
Управление и отправка команд
После того как соединение установлено, можно переходить ко второй активности — ActivityControl. На экране будет только синий кружок — джойстик. Сделан он из обычной Button, разметка приведена выше.
public class ActivityControl extends AppCompatActivity < //переменные, которые понадобятся private Button buttonDriveControl; private float BDCheight, BDCwidth; private float centerBDCheight, centerBDCwidth; private String angle = "90"; //0, 30, 60, 90, 120, 150, 180 private ConnectedThread threadCommand; private long lastTimeSendCommand = System.currentTimeMillis(); >
В методе onCreate() происходит все основное действо:
//без этой строки студия потребует вручную переопределить метод performClick() //нам оно не недо @SuppressLint("ClickableViewAccessibility") @Override protected void onCreate(Bundle savedInstanceState) < //обязательная строка super.onCreate(savedInstanceState); //устанавливаем разметку, ее код выше setContentView(R.layout.activity_control); //привязываем кнопку buttonDriveControl = (Button) findViewById(R.id.button_drive_control); //получаем информацию о кнопке final ViewTreeObserver vto = buttonDriveControl.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() < @Override public void onGlobalLayout() < //получаем высоту и ширину кнопки в пикселях(!) BDCheight = buttonDriveControl.getHeight(); BDCwidth = buttonDriveControl.getWidth(); //находим центр кнопки в пикселях(!) centerBDCheight = BDCheight/2; centerBDCwidth = BDCwidth/2; //отключаем GlobalListener, он больше не понадобится buttonDriveControl.getViewTreeObserver().removeOnGlobalLayoutListener(this); >>); //устанавливаем листенер, который будет отлавливать прикосновения //его код представлен ниже buttonDriveControl.setOnTouchListener(new ControlDriveInputListener()); //создаем новый поток, он будет занят отправкой данных //в качестве параметра передаем сокет, созданный в первой активности //код потока представлен ниже threadCommand = new ConnectedThread(MainActivity.clientSocket); threadCommand.run(); >
Обратите внимание (!) — мы узнаем, сколько пикселей занимает кнопка. Благодаря этому получаем адаптивность: размер кнопки будет зависеть от разрешения экрана, но весь остальной код легко под это подстроится, потому что мы не фиксируем размеры заранее. Позже научим приложение узнавать, в каком месте было касание, а после переводить это в понятные для ардуинки значения от 0 до 255 (ведь касание может быть в 456 пикселях от центра, а МК с таким числом работать не будет).
Далее приведен код ControlDriveInputListener(), данный класс располагается в классе самой активности, после метода onCreate(). Находясь в файле ActivityControl, класс ControlDriveInputListener становится дочерним, а значит имеет доступ ко всем переменным основного класса.
Не обращайте пока что внимание на функции, вызываемые при нажатии. Сейчас нас интересует сам процесс отлавливания касаний: в какую точку человек поставил палец и какие данные мы об этом получим.
Обратите внимание, использую класс java.util.Timer: он позволяет создать новый поток, который может иметь задержку и повторятся бесконечное число раз через каждое энное число секунд. Его нужно использовать для следующей ситуации: человек поставил палец, сработал метод ACTION_DOWN, информация пошла на ардуинку, а после этого человек решил не сдвигать палец, потому что скорость его устраивает. Второй раз метод ACTION_DOWN не сработает, так как сначала нужно вызвать ACTION_UP (отодрать палец от экрана).
Чтож, мы запускаем цикл класса Timer() и начинаем каждые 10 миллисекунд отправлять те же самые данные. Когда же палец будет сдвинут (сработает ACTION_MOVE) или поднят (ACTION_UP), цикл Timer надо убить, чтобы данные от старого нажатия не начали отправляться снова.
public class ControlDriveInputListener implements View.OnTouchListener < private Timer timer; @Override public boolean onTouch(View view, MotionEvent motionEvent) < //получаем точки касания в пикселях //отсчет ведется от верхнего левого угла (!) final float x = motionEvent.getX(); final float y = motionEvent.getY(); //узнаем, какое действие было сделано switch(motionEvent.getAction()) < //если нажатие //оно сработает всегда, когда вы дотронетесь до кнопки case MotionEvent.ACTION_DOWN: //создаем таймер timer = new Timer(); //запускаем цикл //аргументы указывают: задержка между повторами 0, //повторять каждые 10 миллисекунд timer.schedule(new TimerTask() < @Override public void run() < //функцию рассмотрим ниже calculateAndSendCommand(x, y); >>, 0, 10); break; //если палец был сдвинут (сработает после ACTION_DOWN) case MotionEvent.ACTION_MOVE: //обязательно (!) //если ранее был запущен цикл Timer(), завершаем его if(timer != null) < timer.cancel(); timer = null; >//создаем новый цикл timer = new Timer(); //отправляем данные с той же частотой, пока не сработает ACTION_UP timer.schedule(new TimerTask() < @Override public void run() < calculateAndSendCommand(x, y); >>, 0, 10); break; //если палец убрали с экрана case MotionEvent.ACTION_UP: //убиваем цикл if(timer != null) < timer.cancel(); timer = null; >break; > return false; > >
Обратите еще раз внимание: отсчет x и y метод onTouch() ведет от верхнего левого угла View. В нашем случае точка (0; 0) находится у Button тут:

Теперь, когда мы узнали, как получить актуальное расположение пальца на кнопки, разберемся, как преобразовать пиксели (ведь x и y — именно расстояние в пикселях) в рабочие значения. Для этого использую метод calculateAndSendCommand(x, y), который нужно разместить в классе ControlDriveInputListener. Также понадобятся некоторые вспомогательные методы, их пишем в этот же класс после calculateAndSendCommand(x, y).
private void calculateAndSendCommand(float x, float y) < //все методы описаны ниже //получаем нужные значения //четверть - 1, 2, 3, 4 //чтобы понять, о чем я, проведите через середину кнопки координаты //и да, дальше оно использоваться не будет, но для отладки пригождалось int quarter = identifyQuarter(x, y); //функция переводит отклонение от центра в скорость //вычитаем y, чтобы получить количество пикселей от центра кнопки int speed = speedCalculation(centerBDCheight - y); //определяет угол поворота //вспомните первую часть статьи, у нас есть 7 вариантов угла String angle = angleCalculation(x); //если хотите вывести информацию на экран, то используйте этот способ //но в финальной версии он не сработает, так как затрагивает отдельный поток /*String resultDown = "x: "+ Float.toString(x) + " y: " + Float.toString(y) + " qr: " + Integer.toString(quarter) + "\n" + "height: " + centerBDCheight + " width: " + centerBDCwidth + "\n" + "speed: " + Integer.toString(speed) + " angle: " + angle; */ //viewResultTouch.setText(resultDown); //все данные полученные, можно их отправлять //но делать это стоить не чаще (и не реже), чем в 100 миллисекунд if((System.currentTimeMillis() - lastTimeSendCommand) >100) < //функцию рассмотрим дальше threadCommand.sendCommand(Integer.toString(speed), angle); //перезаписываем время последней отправки данных lastTimeSendCommand = System.currentTimeMillis(); >> private int identifyQuarter(float x, float y) < //смотрим, как расположена точка относительно центра //возвращаем угол if(x >centerBDCwidth && y > centerBDCheight) < return 4; >else if (x < centerBDCwidth && y >centerBDCheight) < return 3; >else if (x < centerBDCwidth && y < centerBDCheight) < return 2; >else if (x > centerBDCwidth && y < centerBDCheight) < return 1; >return 0; > private int speedCalculation(float deviation) < //получаем коэффициент //он позволит превратить пиксели в скорость float coefficient = 255/(BDCheight/2); //высчитываем скорость по коэффициенту //округляем в целое int speed = Math.round(deviation * coefficient); //если скорость отклонение меньше 70, ставим скорость ноль //это понадобится, когда вы захотите повернуть, но не ехать if(speed >0 && speed < 70) speed = 0; if(speed < 0 && speed >- 70) speed = 0; //нет смысла отсылать скорость ниже 120 //слишком мало, колеса не начнут крутиться if(speed < 120 && speed >70) speed = 120; if(speed > -120 && speed < -70) speed = -120; //если вы унесете палец за кнопку, ACTION_MOVE продолжит считывание //вы сможете получить отклонение больше, чем пикселей в кнопке //на этот случай нужно ограничить скорость if(speed >255 ) speed = 255; if(speed < - 255) speed = -255; //пометка: скорость >0 - движемся вперед, < 0 - назад return speed; >private String angleCalculation(float x) < //разделяем ширину кнопки на 7 частей //0 - максимально влево, 180 - вправо //90 - это когда прямо if(x < BDCwidth/6) < angle = "0"; >else if (x > BDCwidth/6 && x < BDCwidth/3) < angle = "30"; >else if (x > BDCwidth/3 && x < BDCwidth/2) < angle = "60"; >else if (x > BDCwidth/2 && x < BDCwidth/3*2) < angle = "120"; >else if (x > BDCwidth/3*2 && x < BDCwidth/6*5) < angle = "150"; >else if (x > BDCwidth/6*5 && x < BDCwidth) < angle = "180"; >else < angle = "90"; >return angle; >
Когда данные посчитаны и переведены, в игру вступает второй поток. Он отвечает именно за отправку информации. Нельзя обойтись без него, иначе сокет, передающий данные, будет тормозить отлавливание касаний, создастся очередь и все конец всему короче.
Класс ConnectedThread также располагаем в классе ActivityControl.
private class ConnectedThread extends Thread < private final BluetoothSocket socket; private final OutputStream outputStream; public ConnectedThread(BluetoothSocket btSocket) < //получаем сокет this.socket = btSocket; //создаем стрим - нить для отправки данных на ардуино OutputStream os = null; try < os = socket.getOutputStream(); >catch(Exception e) <> outputStream = os; > public void run() < >public void sendCommand(String speed, String angle) < //блютуз умеет отправлять только байты, поэтому переводим byte[] speedArray = speed.getBytes(); byte[] angleArray = angle.getBytes(); //символы используются для разделения //как это работает, вы поймете, когда посмотрите принимающий код скетча ардуино String a = "#"; String b = "@"; String c = "*"; try < outputStream.write(b.getBytes()); outputStream.write(speedArray); outputStream.write(a.getBytes()); outputStream.write(c.getBytes()); outputStream.write(angleArray); outputStream.write(a.getBytes()); >catch(Exception e) <> > >
Подводим итоги Андроид-приложения
Коротко обобщу все громоздкое вышеописанное.
- В ActivityMain настраиваем блютуз, устанавливаем соединение.
- В ActivityControl привязываем кнопку и получаем данные о ней.
- Вешаем на кнопку OnTouchListener, он отлавливает касание, передвижение и подъем пальца.
- Полученные данные (точку с координатами x и y) преобразуем в угол поворота и скорость
- Отправляем данные, разделяя их специальными знаками
Скетч Arduino
Андроид-приложение разобрано, написано, понято… а тут уже и попроще будет. Постараюсь поэтапно все рассмотреть, а потом дам ссылку на полный файл.
Переменные
Для начала рассмотрим константы и переменные, которые понадобятся.
#include //переназначаем пины входа\вывода блютуза //не придется вынимать его во время заливки скетча на плату SoftwareSerial BTSerial(8, 9); //пины поворота и скорости int speedRight = 6; int dirLeft = 3; int speedLeft = 11; int dirRight = 7; //пины двигателя, поворачивающего колеса int angleDirection = 4; int angleSpeed = 5; //пин, к которому подключен плюс штуки, определяющей поворот //подробная технология описана в первой части int pinAngleStop = 12; //сюда будем писать значения String val; //скорость поворота int speedTurn = 180; //пины, которые определяют поворот //таблица и описания системы в первой статье int pinRed = A0; int pinWhite = A1; int pinBlack = A2; //переменная для времени long lastTakeInformation; //переменные, показывающие, что сейчас будет считываться boolean readAngle = false; boolean readSpeed = false;
Метод setup()
В методе setup() мы устанавливаем параметры пинов: будут работать они на вход или выход. Также установим скорость общения компьютера с ардуинкой, блютуза с ардуинкой.
void setup() < pinMode(dirLeft, OUTPUT); pinMode(speedLeft, OUTPUT); pinMode(dirRight, OUTPUT); pinMode(speedRight, OUTPUT); pinMode(pinRed, INPUT); pinMode(pinBlack, INPUT); pinMode(pinWhite, INPUT); pinMode(pinAngleStop, OUTPUT); pinMode(angleDirection, OUTPUT); pinMode(angleSpeed, OUTPUT); //данная скорость актуальна только для модели HC-05 //если у вас модуль другой версии, смотрите документацию BTSerial.begin(38400); //эта скорость постоянна Serial.begin(9600); >
Метод loop() и дополнительные функции
В постоянно повторяющемся методе loop() происходит считывание данных. Сначала рассмотрим основной алгоритм, а потом функции, задействованные в нем.
void loop() < //если хоть несчитанные байты if(BTSerial.available() >0) < //считываем последний несчитанный байт char a = BTSerial.read(); if (a == '@') < //если он равен @ (случайно выбранный мною символ) //обнуляем переменную val val = ""; //указываем, что сейчас считаем скорость readSpeed = true; >else if (readSpeed) < //если пора считывать скорость и байт не равен решетке //добавляем байт к val if(a == '#') < //если байт равен решетке, данные о скорости кончились //выводим в монитор порта для отладки Serial.println(val); //указываем, что скорость больше не считываем readSpeed = false; //передаем полученную скорость в функцию езды go(val.toInt()); //обнуляем val val = ""; //выходим из цикла, чтобы считать следующий байт return; >val+=a; > else if (a == '*') < //начинаем считывать угол поворота readAngle = true; >else if (readAngle) < //если решетка, то заканчиваем считывать угол //пока не решетка, добавляем значение к val if(a == '#') < Serial.println(val); Serial.println("-----"); readAngle = false; //передаем значение в функцию поворота turn(val.toInt()); val= ""; return; >val+=a; > //получаем время последнего приема данных lastTakeInformation = millis(); > else < //если несчитанных байтов нет, и их не было больше 150 миллисекунд //глушим двигатели if(millis() - lastTakeInformation >150) < lastTakeInformation = 0; analogWrite(angleSpeed, 0); analogWrite(speedRight, 0); analogWrite(speedLeft, 0); >> >
Получаем результат: с телефона отправляем байты в стиле «@скорость#угол#» (например, типичная команда «@200#60#». Данный цикл повторяется каждый 100 миллисекунд, так как на андроиде мы установили именно этот промежуток отправки команд. Короче делать нет смысла, так как они начнут становится в очередь, а если сделать длиннее, то колеса начнут двигаться рывками.
Все задержки через команду delay(), которые вы увидите далее, подобраны не через физико-математические вычисления, а опытным путем. Благодаря всем выставленным задрежам, машинка едет плавно, и у всех команд есть время на отработку (токи успевают пробежаться).
В цикле используются две побочные функции, они принимают полученные данные и заставляют машинку ехать и крутится.
void go(int mySpeed) < //если скорость больше 0 if(mySpeed >0) < //едем вперед digitalWrite(dirRight, HIGH); analogWrite(speedRight, mySpeed); digitalWrite(dirLeft, HIGH); analogWrite(speedLeft, mySpeed); >else < //а если меньше 0, то назад digitalWrite(dirRight, LOW); analogWrite(speedRight, abs(mySpeed) + 30); digitalWrite(dirLeft, LOW); analogWrite(speedLeft, abs(mySpeed) + 30); >delay(10); > void turn(int angle) < //подаем ток на плюс определителя угла digitalWrite(pinAngleStop, HIGH); //даем задержку, чтобы ток успел установиться delay(5); //если угол 150 и больше, поворачиваем вправо //если 30 и меньше, то влево //промежуток от 31 до 149 оставляем для движения прямо if(angle >149) < //если замкнут белый, но разомкнуты черный и красный //значит достигнуто крайнее положение, дальше крутить нельзя //выходим из функции через return if( digitalRead(pinWhite) == HIGH && digitalRead(pinBlack) == LOW && digitalRead(pinRed) == LOW) < return; >//если проверка на максимальный угол пройдена //крутим колеса digitalWrite(angleDirection, HIGH); analogWrite(angleSpeed, speedTurn); > else if (angle < 31) < if(digitalRead(pinRed) == HIGH && digitalRead(pinBlack) == HIGH && digitalRead(pinWhite) == HIGH) < return; >digitalWrite(angleDirection, LOW); analogWrite(angleSpeed, speedTurn); > //убираем питание digitalWrite(pinAngleStop, LOW); delay(5); >
Поворачивать, когда андроид отправляет данные о том, что пользователь зажал угол 60, 90, 120, не стоит, иначе не сможете ехать прямо. Да, возможно сразу не стоило отправлять с андроида команду на поворот, если угол слишком мал, но это как-то коряво на мой взгляд.
Итоги скетча
У скетча всего три важных этапа: считывание команды, обработка ограничений поворота и подача тока на двигатели. Все, звучит просто, да и в исполнении легче чем легко, хотя создавалось долго и с затупами. Полная версия скетча.
В конце концов
Полноценная опись нескольких месяцев работы окончена. Физическая часть разобрана, программная тем более. Принцип остается тот же — обращайтесь по непонятным явлениям, будем разбираться вместе.
А комментарии под первой частью интересны, насоветовали гору полезнейших советов, спасибо каждому.
Как сделать приложение для ардуино
ARDUINO Андроид-приложение для управление ардуино по НС-06.
- Автор темы Юрий Бронников
- Дата начала 22 Авг 2020
Вы используете устаревший браузер. Этот и другие сайты могут отображаться в нём некорректно.
Вам необходимо обновить браузер или попробовать использовать другой.
Юрий Бронников
25 Май 2020 3 0
Все андроид-приложения, которые я нашел, отправляют команду ардуино по кликанию на кнопку (например «1»).
Мне же важно чтобы команда на ардуино поступала только в момент удержания кнопки. После, ардуино переходила обратно в ждущий режим.
То есть, держим кнопку на телефоне — на ардуино приходит «1», отпускаем кнопку на телефоне — приходит «0».
Дайте, пожалуйста, ссылку на подобное приложение.
Wan-Derer
Команда форума
31 Июл 2018 1,905 402 Москва wan-derer.ru
Un_ka
13 Июл 2020 241 71 других форумов.
Sketchware — можете создать приложение на телефоне и для телефона. Thunkable — браузерный пакет для создания приложений, про него у Alex’a на сайте есть гайд, правда устаревший. MIT app inventor — подобное Thunkable, только старее.
Если что-нибудь у вас получится, обязательно покажите.
PiratFox
13 Фев 2020 1,672 467
@Юрий Бронников, можете попробовать ещё также браузерную среду RemoteXY. Там всякие кнопки есть, и не только кнопки.
Изменено: 25 Авг 2020
Юрий Бронников
25 Май 2020 3 0
Sketchware — можете создать приложение на телефоне и для телефона. Thunkable — браузерный пакет для создания приложений, про него у Alex’a на сайте есть гайд, правда устаревший. MIT app inventor — подобное Thunkable, только старее.
Если что-нибудь у вас получится, обязательно покажите.
Решение своей задачи я нашел в MIT app inventor. Thunkable не осилил, не разобрался.
Во вложении:
Perekl.Rele.ino Скетч для ардуино
Snaryd.rar в архиве проект расширения .aia
И фотки проекта MIT app inventor (скрин не смог сделать — комп контузило)
Создаем мобильное приложение для управления «Умной теплицей» на Arduino

На 5 уроке про Умную теплицу на Ардуино, мы создадим свое собственное мобильное приложение для устройств на Андроиде. Для этого мы используем редактор визуального программирования Android App Invertor 2
В следующей статье про перенос « функции мониторинга и управления теплицей на телефон или планшет с ОС Androi d» мы установили связь нашей системы с телефоном (или планшетом) с операционной системой Android по Bluetooth, что позволило нам отправлять данные мониторига данных нашей теплицы на телефон и получать команды управления с телефона. Но для связи теплицы с телефоном мы использовали на телефоне приложение Bluetooth Terminal, что совсем неудобно. Нам нужно полноценное приложение. В этом уроке мы и займемся его созданием.
Глубоко внимать в вопросы программирования для операционной системы Android не входит в наши планы, поэтому нам нужна простая и понятная система создания кода для Android, наподобие системы Sctratch для Arduino, которую мы рассматривали на этом уроке – Программируем с Arduino… К счастью подобный визуальный редактор есть. Это онлайн визуальный редактор для визуального программирования для Android App Invertor 2. Страница проекта – http://ai2.appinventor.mit.edu.

Рисунок 1. Страница онлайн-редактора App Invertor 2.

После авторизации (можно использовать профиль google) или регистрации попадаем в свой профиль программы, где можем создать новый проект.
Рисунок 2. Ваш профиль программы. Создание проекта.

Сначала в панети Design создаем интерфейс нашего приложения, перетаскивая на экран необходимые компоненнты. Кроме визуальных компонентов необходимо добавить 3 невизуальных:
Bluetooth client из раздела Connectivity;
Clock из раздела Sensors (для получения данных из Bluetooth c периодичностью, установленной в Clock);
Notifer из UserInterface.
Рисунок 3. Создание интерфейса в Design.

Теперь создаем код. Переходим в раздел Block. Сначала создаем код для инициализации Bluetooth соединения и создания Bluetooth клиента (рисунок 4).
Рисунок 4. Код для инициализации Bluetooth соединения и создания Bluetooth клиента.
Затем код для отправки сообщений при изменениии состояний chexckbox-ов для насоса, вентилятора и лампы (рисунок 5).

Рисунок 5. Код для отправки сообщений при изменениии состояний chexckbox-ов.
И код получения по таймеру сообщений, поступающих по Bluetooth из Arduino (рисунок 6).

Рисунок 6. Код получения по таймеру сообщений, поступающих из Arduino
Создаем app приложение (рисунок 7) и загружаем его на телефон.

Рисунок 7. Генерация app приложения
Нам надо внести самые маленькие изменения в наш предыдущий скетч, заменив разделитель с пробела на символ ‘*’ при отправке данных с Arduino на Android.
Создадим в Arduino IDE новый скетч, занесем в него код из листинга 1 и загрузим скетч на на плату Arduino. Напоминаем, что в настройках Arduino IDE необходимо выбрать тип платы (Arduino UNO) и порт подключения платы.
// подключение библиотеки SoftwareSerial #include // подключение библиотеки DHT #include "DHT.h" // тип датчика DHT #define DHTTYPE DHT11 // контакты подключения bluetooth-модуля HC-05 int pinBlRx=17; int pinBlTx=18; // контакт подключения входа данных модуля DHT11 int pinDHT11=9; // контакт подключения аналогового выхода модуля влажности почвы int pinSoilMoisture=A0; // контакт подключения аналогового выхода датчика температуры TMP36 int pinTMP36=A1; // контакт подключения аналогового выхода фоторезистора int pinPhotoresistor=A2; // пины светодиодов индикации #define LED_TEMP 5 #define LED_MOISTURE 6 #define LED_LIGHT 7 // значения для условий #define TEMP_DETECT 30 #define MOISTURE_DETECT 500 #define LIGHT_DETECT 250 // реле int pinRelays[]=; // статусы полива, освещения, вентилятора boolean statusRelays[]=; // создание экземпляра объекта SoftwareSerial SoftwareSerial HC05Serial(pinBlRx,pinBlTx); // создание экземпляра объекта DHT DHT dht(pinDHT11, DHTTYPE); unsigned long millisupdate=0; // для получения данных из SoftwareSerial String inputString0 = ""; // признак конца полученной строки boolean stringComplete0 = false; void setup() < // запуск последовательного порта Serial.begin(9600); // pinMode(LED_TEMP,OUTPUT);digitalWrite(LED_TEMP,LOW); pinMode(LED_MOISTURE,OUTPUT);digitalWrite(LED_MOISTURE,LOW); pinMode(LED_LIGHT,OUTPUT);digitalWrite(LED_LIGHT,LOW); // инициализация dht dht.begin(); // запуск SoftwareSerial HC05Serial.begin(9600); // резервирование 50 bytes для the inputString: inputString0.reserve(50); >void loop() < // ожидание конца строки для анализа поступившего запроса: serialEvent0(); if (stringComplete0) < Serial.println(inputString0); parse_string0(inputString0); // очистить : inputString0 = ""; stringComplete0 = false; >// каждые 5 сек - получение показаний датчиков // и вывод на дисплей if(millis()-millisupdate>5000) < millisupdate=millis(); // получение данных с DHT11 float h = dht.readHumidity(); if (isnan(h)) < Serial.println("Failed to read from DHT"); HC05Serial.println("H1=101"); delay(10); >else < Serial.print("HumidityDHT11= "); Serial.print(h);Serial.println(" %"); HC05Serial.print("aH=");HC05Serial.print(h);HC05Serial.print("*"); delay(10); >// получение значения с аналогового вывода модуля влажности почвы int val0=analogRead(pinSoilMoisture); Serial.print("SoilMoisture= "); Serial.println(val0); HC05Serial.print("SM=");HC05Serial.print(h);HC05Serial.print("*"); delay(10); // получение значения с аналогового вывода датчика температуры TMP36 int val1=analogRead(pinTMP36); // перевод в мВ int mV= val1*1000/1024; // перевод в градусы цельсия int t=(mV-500)/10+75;//t=23; Serial.print("TempTMP36= "); Serial.print(t);Serial.println(" C"); HC05Serial.print("aT=");HC05Serial.print(t);HC05Serial.print("*"); delay(10); // получение значения с аналогового вывода фоторезистора int val2=analogRead(pinPhotoresistor); Serial.print("Light= "); Serial.println(val2); HC05Serial.print("Ph=");HC05Serial.print(val2);HC05Serial.print("*"); delay(10); // обновить // вывод состояние полива, лампы, вентилятора Serial.print("pump - "); Serial.println(statusRelays[2]); Serial.print("fun - "); Serial.println(statusRelays[1]); Serial.print("lamp - "); Serial.println(statusRelays[0]); HC05Serial.print("PM=");HC05Serial.print(statusRelays[2]);HC05Serial.print("*"); delay(10); HC05Serial.print("FN=");HC05Serial.print(statusRelays[1]);HC05Serial.print("*"); delay(10); HC05Serial.print("LM=");HC05Serial.print(statusRelays[0]); delay(10); //// проверка условий // увлажненность почвы if(val0 > MOISTURE_DETECT) digitalWrite(LED_MOISTURE,HIGH); else digitalWrite(LED_MOISTURE,LOW); // температура воздуха if(t > TEMP_DETECT) digitalWrite(LED_TEMP,HIGH); else digitalWrite(LED_TEMP,LOW); // освещенность if(val2 < LIGHT_DETECT) digitalWrite(LED_LIGHT,HIGH); else digitalWrite(LED_LIGHT,LOW); // пауза 5 секунд Serial.println(); >> // SerialEvent для HC05 void serialEvent0() < while (HC05Serial.available()) < // получить очередной байт: char inChar = (char)HC05Serial.read(); // добавить в строку inputString0 += inChar; // /n - конец передачи if (inChar == '#') < stringComplete0 = true; >> > // парсинг строки из android void parse_string0(String inputString) < // длина строки int length1=inputString.length(); if(length1!=5) if(inputString.charAt(2)!='=') if(inputString.charAt(4)!='#') String param1=inputString.substring(0,2); int param2=inputString.substring(3,4).toInt(); Serial.print("param1=");Serial.println(param1); Serial.print("param2=");Serial.println(param2); if(param1=="PM") doCommand(2,min(param2,1)); else if(param1=="FN") doCommand(1,min(param2,1)); else if(param1=="LM") doCommand(0,min(param2,1)); > // исполнение команды от смартфона void doCommand(int relay, int status1) < // изменить статус statusRelays[relay]=status1; // изменить состояние реле digitalWrite(pinRelays[relay],statusRelays[relay]); >
Загружаем скетч на Arduino, на телефоне запускаем приложение.


Рисунок 8, 9, 10. Приложение в работе.
На следующем уроке рассмотрим вопрос превращения нашей теплицы в объект IoT (Интернет вещей).