Pull to refresh

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

Reading time 18 min
Views 19K

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


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

Итоги скетча


У скетча всего три важных этапа: считывание команды, обработка ограничений поворота и подача тока на двигатели. Все, звучит просто, да и в исполнении легче чем легко, хотя создавалось долго и с затупами. Полная версия скетча.

В конце концов


Полноценная опись нескольких месяцев работы окончена. Физическая часть разобрана, программная тем более. Принцип остается тот же — обращайтесь по непонятным явлениям, будем разбираться вместе.

А комментарии под первой частью интересны, насоветовали гору полезнейших советов, спасибо каждому.

Видео результата


Tags:
Hubs:
+13
Comments 10
Comments Comments 10

Articles