О первый части
В первой части я описал физическую часть конструкции и лишь небольшой кусок кода. Теперь рассмотрим программную составляющую — приложение для Android и скетч Arduino.
Вначале приведу подробное описание каждого момента, а в конце оставлю ссылки на проекты целиком + видео результата, которое должно вас разочаровать ободрить.
Android-приложение
Программа для андроида разбита на две части: первая — подключение устройства по Bluetooth, вторая — джойстик управления.
Предупреждаю — дизайн приложения совсем не прорабатывался и делался на тяп-ляп, лишь бы работало. Адаптивности и UX не ждите, но вылезать за пределы экрана не должно.
Верстка
Стартовая активность держится на верстке, элементы: кнопки и layout для списка устройств. Кнопка запускает процесс нахождения устройств с активным Bluetooth. В ListView отображаются найденные устройства.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="40dp"
android:layout_marginTop="50dp"
android:text="@string/start_search"
android:id="@+id/button_start_find"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:id="@+id/button_start_control"
android:text="@string/start_control"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"/>
<ListView
android:id="@+id/list_device"
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
/>
</RelativeLayout>
Экран управления опирается на верстку, в которой есть только кнопка, которая в будущем станет джойстиком. К кнопки, через атрибут background, прикреплен стиль, делающий ее круглой.
TextView в финальной версии не используется, но изначально он был добавлен для отладки: выводились цифры, отправляемые по блютузу. На начальном этапе советую использовать. Но потом цифры начнут высчитываться в отдельном потоке, из которого сложно получить доступ к TextView.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="25dp"
android:layout_marginStart="15dp"
android:id="@+id/button_drive_control"
android:background="@drawable/button_control_circle" />
<TextView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:minWidth="70dp"
android:id="@+id/view_result_touch"
android:layout_marginEnd="90dp"
/>
</RelativeLayout>
Файл button_control_circle.xml (стиль), его нужно поместить в папку drawable:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#00F" />
<corners android:bottomRightRadius="100dp"
android:bottomLeftRadius="100dp"
android:topRightRadius="100dp"
android:topLeftRadius="100dp"/>
</shape>
Также нужно создать файл item_device.xml, он нужен для каждого элемента списка:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="150dp"
android:layout_height="40dp"
android:id="@+id/item_device_textView"/>
</LinearLayout>
Манифест
На всякий случай приведу полный код манифеста. Нужно получить полный доступ к блютузу через uses-permission и не забыть обозначить вторую активность через тег activity.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.bluetoothapp">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<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="com.arproject.bluetoothworkapp.MainActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.arproject.bluetoothworkapp.ActivityControl"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:screenOrientation="landscape"/>
</application>
</manifest>
Основная активность, сопряжение Arduino и Android
Наследуем класс от AppCompatActivity и объявляем переменные:
public class MainActivity extends AppCompatActivity {
private BluetoothAdapter bluetoothAdapter;
private ListView listView;
private ArrayList<String> pairedDeviceArrayList;
private ArrayAdapter<String> 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[] {Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN}, 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() {
//получить список доступных устройств
Set<BluetoothDevice> pairedDevice = 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<String>(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[]{int.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) преобразуем в угол поворота и скорость
- Отправляем данные, разделяя их специальными знаками
А окончательное понимание к вам придет, когда вы посмотрите весь код целиком — github.com/IDolgopolov/BluetoothWorkAPP.git. Там код без комментариев, поэтому смотрится куда чище, меньше и проще.
Скетч Arduino
Андроид-приложение разобрано, написано, понято… а тут уже и попроще будет. Постараюсь поэтапно все рассмотреть, а потом дам ссылку на полный файл.
Переменные
Для начала рассмотрим константы и переменные, которые понадобятся.
#include <SoftwareSerial.h>
//переназначаем пины входавывода блютуза
//не придется вынимать его во время заливки скетча на плату
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, не стоит, иначе не сможете ехать прямо. Да, возможно сразу не стоило отправлять с андроида команду на поворот, если угол слишком мал, но это как-то коряво на мой взгляд.
Итоги скетча
У скетча всего три важных этапа: считывание команды, обработка ограничений поворота и подача тока на двигатели. Все, звучит просто, да и в исполнении легче чем легко, хотя создавалось долго и с затупами. Полная версия скетча.
В конце концов
Полноценная опись нескольких месяцев работы окончена. Физическая часть разобрана, программная тем более. Принцип остается тот же — обращайтесь по непонятным явлениям, будем разбираться вместе.
А комментарии под первой частью интересны, насоветовали гору полезнейших советов, спасибо каждому.
Видео результата
Автор: DolgopolovDenis