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

    Итоги скетча


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

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


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

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

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


    Поделиться публикацией

    Комментарии 10

      –3
      Страна производит электричество, паровозы, миллионы тонн чугуна. Люди напрягают все силы, люди буквально падают от напряжения, люди начинают даже заикаться от напряжения, покрываются морщинами на крайнем Севере и вынуждены вставлять себе золотые зубы. А как же иначе?! Нужно! Но в это же самое время находятся люди, которые из всех достижений человечества облюбовали себе печку! Вот как! Славно, славно!
        0
        Сникерс?
          0
          вы сомневаетесь?
            0

            в чем суть, господа?

        0
        Очень здорово!
        Хочешь развивать проект в дальнейшем?
        Будешь дорабатывать приложение в плане дизайна и т. д.?
          0
          Изначально проект был куда шире, кроме езды машинка должна была обзавестись различной периферией. Возможно, все-таки доделаю эти штуки.
          Приложение конечно можно доделать, это и не столь сложно, но освещать это уже не вижу смысла, слишком мелко выйдет. Но если кому-то интересно, готов рассказать в личном беседе, какие моменты стоит доделать и куда чего дописать.
            0
            Было бы интересно увидеть машинку с периферией. Буду ждать.
            От разработки приложений я далек, но именно сам интерфейс и способ управления мне понравился. Чуток допилить и будет конфетка.
          0
          Автор, Вы — молодец :) Первая часть была крутой, но и во второй Вы с поставленными задачами справились :) Какие у Вас дальнейшие планы?
          — Блютуз, подойди ко мне!
          — Синий зуб, и что?! (с)
            0
            Вы бы ссылку на первую часть для приличия где-то оставили. А то искать приходится.
              0
              вы наверно уже нашли, а кому-нибудь пригодится — habr.com/post/424087 (часть первая)

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.