Машинка на Arduino, управляемая Android-устройством по Bluetooth, — код приложения и мк (часть 2)

от автора

О первый части

В первой части я описал физическую часть конструкции и лишь небольшой кусок кода. Теперь рассмотрим программную составляющую — приложение для 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) {}         }      } 

Подводим итоги Андроид-приложения

Коротко обобщу все громоздкое вышеописанное.

  1. В ActivityMain настраиваем блютуз, устанавливаем соединение.
  2. В ActivityControl привязываем кнопку и получаем данные о ней.
  3. Вешаем на кнопку OnTouchListener, он отлавливает касание, передвижение и подъем пальца.
  4. Полученные данные (точку с координатами x и y) преобразуем в угол поворота и скорость
  5. Отправляем данные, разделяя их специальными знаками

А окончательное понимание к вам придет, когда вы посмотрите весь код целиком — 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *