2016年12月4日 星期日

Android + Arduino 小車的手機藍牙遙控計劃

對於手機的Android Apps今次是作為初學者的第一個項目。但不枉我在這次印度公幹期間,電腦之外,我都執了幾件零件和書。在印度試好了如何在Android上用藍牙連接Arduino,簡單的按鍵去傳送方向指令(數字),和預備日後作其他用途的顯示和輸入介面。詳細的檔案己經放到下面的dropbox link。
Arduino Sketch: https://dl.dropboxusercontent.com/u/71621110/Blogger/Sketch_CarBT.ino
Android Project: https://dl.dropboxusercontent.com/u/71621110/Blogger/myFirstBluetoothApp.zip

 Arduino

首先,Arduino 的Sketch中,令小車移動的指令是連接L298D的4個腳位控制個馬達的正轉反轉和速度。先把向前向後轉左轉右等指令包裝成forward()、backward()、left()、right()、brake()等指令:
int Left_motor_go=8;     //左馬達前進(IN1)
int Left_motor_back=9;     //左馬達後退(IN2)
int Right_motor_go=10;    // 右馬達前進(IN3)
int Right_motor_back=11;    // 右馬達後退(IN4)

void forward()     // 前進
{
  digitalWrite(Right_motor_go,HIGH);  // 右馬達前進
  digitalWrite(Right_motor_back,LOW);     
  digitalWrite(Left_motor_go,LOW);  // 左馬達前進
  digitalWrite(Left_motor_back,HIGH);
  
  analogWrite(Right_motor_go,200);
  analogWrite(Right_motor_back,0);  
  analogWrite(Left_motor_go,0);
  analogWrite(Left_motor_back,200);
}

連接藍牙到Arduino時要像以下般連接,藍牙模組會直播接連到板子上的TX,RX 腳位。當Arduino開始"setup()"時除了設定相關腳位的pinMode外,也需設定Serial的通訊。之後的程式上就可以像Serial 通訊般互傳指令。而Serial Rate 要跟自己藍牙模組的Baud Rate 相同,才可以在同一頻率下理解訊息。之前在電腦上搜尋這藍牙時,確定自己用的是HC-05的藍牙組件。HC-05出廠設定是rate=9600,配對密碼1234。
Bluetooth's TX  <-->  Arduino's RX
Bluetooth's RX  <-->  Arduino's TX
Bluetooth's GND  <-->  Arduino's GND
Bluetooth's VCC  <-->  Arduino's 5V



void setup()
{
  pinMode(Left_motor_go,OUTPUT); // PIN 8 (PWM)
  pinMode(Left_motor_back,OUTPUT); // PIN 9 (PWM)
  pinMode(Right_motor_go,OUTPUT);// PIN 10 (PWM) 
  pinMode(Right_motor_back,OUTPUT);// PIN 11 (PWM)
  Serial.begin(9600);  
}

當有可讀的內容,它就會讀取信息並存放到字元變數cmd當中,然後視乎這個變數的內容而決定動作。(另外有回傳信息方便有手機上確認小車收到的信號和正在運行的指令)。原來Arduino在藍牙方面的使用這樣容易。
void loop()
{
    if (Serial.available() > 0 ) {
      char cmd = Serial.read();
      switch(cmd) {
        ...
        case '1':
          left_back();
          break;
        case '2':
          back();
          break;
        ...
        case '8':
          forward();
          break;
        case '9':
          right();
          break;
      }
    }
}

Android

另一方面是Android上的App。之前也嘗試過自學Apps但放棄了,今次的改變是從原來的SDK轉到Eclipse作為開發環境,和非常重要的是放棄那個慢得過份的原生模擬器AVD,改用Genymotion和USB連接到手機時的偵錯模式。

這個我一開始是參考《Make: Arduino 機器人及小裝置專題製作》這本書中的方法,它提供了一個類別 TBlue.class 去處理Android中如何控制手機上的藍牙,背後調動的是BluetoothAdaptor、BluetoothDevice、BluetoothSocket這些類別。書的檔案連結在這裡(http://www.botbook.com/)。本身對Apps一竅不通的我,是透過跟著這本書和這篇CodeData上的教學來學習的(http://www.codedata.com.tw/mobile/android-tutorial-the-1st-class-1-sunwukong/),只要懂得一個App的基本規劃、Activity、按鍵的動作,就大概足夠寫出以下App的主要功能,App中其他如Menu的使用只是為了下一步的自學和改良而準備的。

我沿用以上教學的做法:把畫面規劃獨立寫在res\layout\ 的 .xml檔中。主畫面規劃成3部分:藍牙連結、信息顯示、指令輸入。
  • 最上用作連結藍牙的兩行,當EditText輸入藍牙組件的位址再按連接,TextView會顯示連接的結果是成功或失敗。目標藍牙的位址可以在其他工具查到 (預備了一個放大鏡的圖示,打算將來加入搜尋附近設備的功能)。
  • 中間的顯示台會顯示和記錄手機發出的指令和Arduino傳回的信息。
  • 輸入部份有兩種輸入方式,一種是提供前後左右等方向和暫停鍵的按鈕;另一種是最下面提供一行輸入用的EditText,用家可以自行輸入指令代碼。
要用手機的藍牙,記得AndroidManifest.xml 加入兩行Permission:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

程式部份的.java 檔:
package com.example.myfirstbluetoothapp;

import com.example.myfirstbluetoothapp.TBlue;
import com.example.myfirstbluetoothapp.AboutActivity;
import com.example.myfirstbluetoothapp.R;

import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.WindowManager;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.EditText;
import android.widget.Button;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;

public class MyFirstBluetoothApp extends Activity {
    private TBlue tBlue;
    private int skipped;
    private String btAddress = "20:16:05:25:48:45";
    private String sCommand;

    private Button addressBn, disconnectBn, commandBn;
    private Button num1Bn, num2Bn, num3Bn, num4Bn, num5Bn, num6Bn, num7Bn, num8Bn, num9Bn;
    private EditText addressEt, commandEt;
    private TextView messagesTv, terminalTv; 
    
    private MenuItem search_item, info_item, version_item;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initGUI();
        try { actionController(); } catch(Exception e) {msgMessage("Action Error!\n");}
        
    }
    
    @Override
    public void onResume() {
        super.onResume();
        //timerHandler.postDelayed(sendToArduino, 1000); 
        //skipped=9999; // force Bluetooth reconnection 
    }
    
    // 載入選單資源
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater menuInflater = getMenuInflater();
        menuInflater.inflate(R.menu.main_menu, menu);
        search_item = menu.findItem(R.id.search_item);
        info_item = menu.findItem(R.id.info_item);
        version_item = menu.findItem(R.id.version_item);
        return true;
    }
   
    public void initGUI() {
        setContentView(R.layout.activity_my_first_bluetooth_app);
        
        addressBn = (Button) findViewById(R.id.bn_address);
        disconnectBn = (Button) findViewById(R.id.bn_disconnect);
        commandBn = (Button) findViewById(R.id.bn_command);
        num1Bn = (Button) findViewById(R.id.bn_num1);
        num2Bn = (Button) findViewById(R.id.bn_num2);
        ......
        num9Bn = (Button) findViewById(R.id.bn_num9);
        addressEt = (EditText) findViewById(R.id.et_address);
        commandEt = (EditText) findViewById(R.id.et_command);
        messagesTv = (TextView) findViewById(R.id.tv_messages); 
        terminalTv = (TextView) findViewById(R.id.tv_terminal); 
        
        messagesTv.setText(""); terminalTv.setText("");
        commandEt.setText(""); addressEt.setText(btAddress);
    }
    
    public void actionController() throws Exception {
        // 建立 Connect Button 的點擊監聽物件
        addressBn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) { 
                msgMessage("Bluetooth initialization... \n");
                try {initBluetooth(); } catch(Exception e) {}
            }
        });
        
        // 建立 DisConnect Button 的點擊監聽物件
        disconnectBn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) { 
                try {closeBluetooth();} catch(Exception e) {};
            }
        });
        
        // 建立 Send Button 的點擊監聽物件
        commandBn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) { sendBT(commandEt.getText().toString());}
        });
        
        // 建立 Number Button 的點擊監聽物件
        num1Bn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) { sendBT("1");}
        }); 
        num2Bn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) { sendBT("2");}
        });
        ......
        num9Bn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) { sendBT("9"); }
        }); 
    }
    
    // 建立 Menu Item 的點擊動作,已註冊在main_menu.xml中
    public void clickMenuItem(MenuItem item) { 
        int itemId = item.getItemId();
        switch (itemId) {
            case R.id.search_item:
                break;
            case R.id.info_item:
                break;
            case R.id.version_item:
                Intent intent = new Intent(this, AboutActivity.class);
                startActivity(intent);
                break;
        }       
    }
    
    
    /*** Bluetooth ***/

    void initBluetooth() throws Exception {
        skipped=0; 
        
        btAddress = addressEt.getText().toString();
        
        tBlue=new TBlue(btAddress);
        if (tBlue.streaming()) {
            msgMessage("Bluetooth Connected. \n");
        } else {
            msgMessage("Error: Bluetooth connection failed. \n");
            closeBluetooth();
        }
    }

    void closeBluetooth()
    {
        msgMessage("Bluetooth Disconnect...");
        tBlue.close();
    }
    
    void receiveBT() {
        String inString=tBlue.read(); 
        msgTerminal("->: " + inString + "\n");    //receive signal from Arduino
    }
    
    void sendBT(String s)
    {
        if (skipped>=10) tBlue.connect(); 
        if (tBlue.streaming()) {    //send signal to Arduino
            Log.i("fb", "Clear to send, sending... ");
            tBlue.write(s); 
            msgTerminal("<-: " + s + "\n");
            skipped=0;
        } else {    //skip
            Log.i("fb", "Not ready, skipping send. in: \""+ s +"\"");
            skipped++; 
            msgTerminal(".\n");
            return;
        }
        new CountDownTimer(1000,1000){
            public void onTick(long ms){}
            public void onFinish(){receiveBT();}    //receive feedback from Arduino
        }.start();

    }
    
    public void msgMessage(String s)
    {
        Log.i("FB", s);
        if (3<=messagesTv.getLineCount()) messagesTv.setText("");
        messagesTv.append(s);
    }
    
    public void msgTerminal(String s)
    {
        Log.i("FB", s);
        if (10<terminalTv.getLineCount()) terminalTv.setText("");
        terminalTv.append(s);
    }
}


1 則留言: