📜 ⬆️ ⬇️

The machine on the Arduino, controlled by an Android device via Bluetooth, is the application code and mic (part 2)

About the first part


In the first part I described the physical part of the construction and only a small piece of code. Now consider the software component - an application for Android and an Arduino sketch.

First I will give a detailed description of each moment, and at the end I will leave links to the projects as a whole + a video of the result, which should disappoint you to encourage.

Android application


The program for android is divided into two parts: the first is the connection of the device via Bluetooth, the second is the control joystick.

I warn you - the design of the application was not worked out at all and was done in any way, just to work. Adaptability and UX do not wait, but should not get out of the screen.
')

Layout


Starting activity is kept on the layout, the elements: buttons and layout for the list of devices. The button starts the process of finding devices with active Bluetooth. The ListView displays the devices found.

<?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> 

The control screen is based on the layout, in which there is only a button that in the future will become a joystick. To the button, through the background attribute, a style is attached that makes it round.
TextView is not used in the final version, but initially it was added for debugging: digits were sent via Bluetooth. At the initial stage I advise you to use. But then the numbers will begin to be calculated in a separate stream, from which it is difficult to access the 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> 

File button_control_circle.xml (style), it must be placed in the folder 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> 

You also need to create an item_device.xml file, it is needed for each item in the list:

 <?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> 

Manifesto


Just in case, give the full code manifest. It is necessary to get full access to bluetooth through uses-permission and not to forget to designate the second activity through the activity tag.

 <?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> 

Main activity, pairing Arduino and Android


Inherit the class from AppCompatActivity and declare variables:

 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; } 

I will describe the onCreate () method line by line:

 @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); } }); } 

The following functions check whether permission to use bluetooth is obtained (without the user's permission, we will not be able to transmit data) and whether bluetooth is enabled:

 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; } } 

If all checks are passed, the device search begins. If one of the conditions is not met, the notification will be displayed, they say, “allow \ enable?”, And this will be repeated until the check is passed.

Device search is divided into three parts: preparing the list, adding to the list of found devices, establishing a connection with the selected device.

 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(); } } }); } 

When the Bluetooth module hung on the Arduino (more on this later) is found, it will appear in the list. By clicking on it, you will start creating a socket (perhaps after a click you will have to wait 3-5 seconds or click again). You will understand that the connection is established, by the LEDs on the Bluetooth module: without a connection, they flash quickly, and if there is a connection, the frequency decreases noticeably.


Manage and send commands


After the connection is established, you can proceed to the second activity - ActivityControl. On the screen there will be only a blue circle - a joystick. It is made from the usual Button, the markup is shown above.

 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(); } 

In the onCreate () method, all the main actions take place:

 //        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(); } 

Pay attention (!) - we will find out how many pixels the button takes. Due to this, we get adaptability: the size of the button will depend on the screen resolution, but the rest of the code is easy to adjust to it, because we do not fix the dimensions in advance. Later we will teach the application to find out where the touch was, and then translate it into clear values ​​for Arduink from 0 to 255 (after all, the touch may be 456 pixels from the center, and the MK will not work with that number).

The following is the code ControlDriveInputListener (), this class is located in the class of the activity itself, after the onCreate () method. Being in the ActivityControl file, the ControlDriveInputListener class becomes a child, which means it has access to all variables of the main class.

Do not pay attention to the functions called by pressing for now. Now we are interested in the process of catching touches: at what point a person has put a finger and what data we will get about it.

Notice, I use the java.util.Timer class: it allows you to create a new stream, which can be delayed and repeated an infinite number of times in each number of seconds. It should be used for the following situation: the person put his finger, the ACTION_DOWN method worked, the information went to Arduinka, and after that the person decided not to move his finger, because he is satisfied with the speed. The second time the ACTION_DOWN method does not work, because you first need to call ACTION_UP (pull your finger off the screen).

Well, we start the Timer () class loop and start sending the same data every 10 milliseconds. When the finger is moved (ACTION_MOVE will work) or raised (ACTION_UP), the Timer cycle must be killed so that the data from the old click does not start to be sent again.

 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; } } 

Note again: the onTouch () method uses the x and y count from the upper left corner of the View. In our case, the point (0; 0) is at the Button here:



Now, when we learned how to get the current location of a finger on the buttons, let's figure out how to convert pixels (after all, x and y are exactly the distance in pixels) into working values. To do this, I use the calculateAndSendCommand (x, y) method, which must be placed in the ControlDriveInputListener class. You will also need some auxiliary methods; we write them to the same class after 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; } 

When the data is calculated and transferred, a second stream enters the game. He is responsible for sending information. You cannot do without it, otherwise the socket transmitting the data will slow down the catching of touches, a queue will be created and the whole end will be shorter.

The ConnectedThread class is also located in the ActivityControl class.

 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) {} } } 

Summing up the Android application


Briefly summarize all the bulky above.

  1. In ActivityMain we set up bluetooth, establish a connection.
  2. In ActivityControl we bind a button and get data about it.
  3. We hang on the OnTouchListener button, it catches touch, movement and lifting of a finger.
  4. The obtained data (a point with x and y coordinates) is converted into a rotation angle and speed
  5. We send data, separating them with special characters.

A final understanding will come to you when you look at the entire code - github.com/IDolgopolov/BluetoothWorkAPP.git . There is a code without comments, so it looks much cleaner, smaller and simpler.

Sketch Arduino


The Android application is parsed, written, understood ... and here it will be easier. I will try to consider everything step by step, and then I will give a link to the full file.

Variables


To begin, consider the constants and variables that will be needed.

 #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 () method


In the setup () method, we set the parameters of the pins: they will work as input or output. Also set the speed of communication with arduinka computer, bluetooth with arduinka.

 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); } 

The loop () method and additional functions


In the constantly repeating loop () method, data is read. First, consider the main algorithm, and then the functions involved in it.

 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); } } } 

We get the result: from the phone we send bytes in the style "@ speed # angle #" (for example, a typical command "@ 200 # 60 #". This cycle repeats every 100 milliseconds, since we set this interval for sending commands on the android. In short, do it makes no sense, since they will start to get in line, and if you make it longer, the wheels will start to move in spurts.

All delays through the delay () command, which you will see below, are chosen not through physical and mathematical calculations, but empirically. Thanks to all the set ramps, the machine goes smoothly, and all teams have time to work out (currents have time to run).

, .

 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, , . , , , - .


: , . , , , . .


. , . — , .

, , .


Source: https://habr.com/ru/post/424813/


All Articles