О первый части
В первой части я описал физическую часть конструкции и лишь небольшой кусок кода. Теперь рассмотрим программную составляющую — приложение для 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, не стоит, иначе не сможете ехать прямо. Да, возможно сразу не стоило отправлять с андроида команду на поворот, если угол слишком мал, но это как-то коряво на мой взгляд.
Итоги скетча
У скетча всего три важных этапа: считывание команды, обработка ограничений поворота и подача тока на двигатели. Все, звучит просто, да и в исполнении легче чем легко, хотя создавалось долго и с затупами. Полная версия скетча — github.com/IDolgopolov/AgroArduinoF.
В конце концов
Полноценная опись нескольких месяцев работы окончена. Физическая часть разобрана, программная тем более. Принцип остается тот же — обращайтесь по непонятным явлениям, будем разбираться вместе.
А комментарии под первой частью интересны, насоветовали гору полезнейших советов, спасибо каждому.
Видео результата
ссылка на оригинал статьи https://habr.com/post/424813/
Добавить комментарий