Arduinoの基礎 - Bluetooth

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要



Bluetoothモジュール HC-06

このセクションでは、Bluetoothの使用前に実施するBluetoothモジュールHC-06の設定方法について記載する。

HC-06は、Arduinoとシリアルインターフェイスで接続する。
ホストPCとHC-06の接続にはFT232RLを使用して、ATコマンドを送信することにより、デバイス名やボーレート等の設定ができる。
WindowsでHC-06の設定を行う場合は、RealTermを使用する。
LinuxでHC-06の設定を行う場合は、GNOME Bluetooth、Bluedevil(KDE)、Blueberry、Bluemanを使用する。

まず、HC-06のジャンパを3.3[V]にする。

COMポートの設定は、RealTermの[Port]タブ -[Port]プルダウンから、<数字> = \VCP0を選択する。(Linuxの場合、/dev/ttyUSB0等)
ボーレートの設定は、RealTermの[Port]タブ -[Baud]プルダウンから選択する。
ここでは、ボーレートを38400[bps]に設定しているが、既定の設定では9600[bps]である。

下図左のように、ATという文字を送信した後、HC-06からOKを受信すれば正常に接続されている。
下図右のように、HC-06のバージョンを確認する場合は、AT+VERSIONを送信する。
このように、AT+<コマンド>という形式のデータを送信することで、モジュールの設定を行うことができる。

Arduino Bluetooth 1.jpg


まず、デバイス名をMYBTに設定するため、以下のコマンドを送信する。
HC-06からOKsetnameを受信すれば正常に設定されている。

AT+NAMEMYBT


次に、ボーレートを38400[bps]に変更するため、以下のコマンドを送信する。(6は38400を表す)

AT+BAUD6


これらの設定は、HC-06の再起動後に有効になる。
また、Bluetoothの既定の接続PINは1234である。


Bluetoothモジュール HC-06と温度センサ

このセクションでは、Arduino、BluetoothモジュールHC-06、温度センサTMP36を使用して、温度データをBluetoothで送信する。

TMP36は、アナログ出力温度測定ICである。
詳細は、Arduinoの基礎 - 温度センサを参照すること。

ここでは、HC-06のデバイス名をMYBT、ボーレートを38400[bps]に設定している。
HC-06の設定については、上記のセクションを参照すること。

下図に、回路図および配線図を示す。
下図右では、ArduinoからのTXを分圧している抵抗器が接続されている。

Arduino Bluetooth 2.jpg


以下に、Arduinoのサンプルコードを記述する。
HC-06がGET:TEMPという文字を受信した時、温度データを送信している。
ここでは簡易的な通信のみを行っているが、クライアント側とサーバ側でデータフォーマットの取り決めをする方がよい。

このままでは、データを受信するクライアントが無いため、
次のセクションでは、スマートフォンとHC-06を接続して、遠隔通信で温度データを受信する。

 #include <SoftwareSerial.h>
 
 const int PIN_TMP36 = 1;
 SoftwareSerial bt = SoftwareSerial(2,3);
 boolean bt_found = false;
 
 void setup()
 {
    Serial.begin(9600);
    bt.begin(38400);
    delay(3000);
 
    String s = bt_sendCommand("AT");
    Serial.println("AT --> " + s);
    if( s == "OK" )
    {
       bt_found = true;
    }
    else
    {
       return;
    }
 }
 
 void loop()
 {
    if(!bt_found)
    {
       return;
    }
 
    if(bt.available() == 0)
    {
       return;
    }
 
    String r = "";
    while(bt.available() > 0)
    {
       char ch = bt.read();
       r += ch;
    }
 
    Serial.println("App Command Received: " + r);
    process_command( r );
 }
 
 float get_temperature()
 {
    int i = analogRead( PIN_TMP36 ); 
    float f = i * 5.0 / 1023.0;
 
    // TMP36 
    // C = 100V - 50
    return 100 * f - 50;
 }
 
 void process_command(String r)
 {
    if( r == "GET:TEMP" )
    {
       float temp = get_temperature();
       String s = String(temp, 1);
 
       bt.print(s);
       bt.flush();
    }
    else
    {
       Serial.println("Unknown command.");
    }
 }
 
 String bt_sendCommand(String cmd)
 {
    bt.print(cmd);
    bt.flush();
 
    Serial.println("Waiting [" + cmd + "]");
    while(bt.available() == 0)
    {
       delay(300);
    }
 
    Serial.println("OK [" + cmd + "]");
 
    String r = "";
    while(bt.available() > 0)
    {
       char ch = bt.read();
       r += ch;
    }
 
    Serial.println("Response [" + r + "]");
 
    return r;
 }



HC-06とクライアント

上記のセクションに記載したArduinoで温度データを送信するデバイス・ソフトウェアにおいて、
Bluetoothを使用してPCまたはスマートフォンと接続して、温度データを受信するPCまたはスマートフォンのソフトウェアの作成手順を記載する。

PCのソフトウェア

ソフトウェアの開発は、Visual Studio 2019のコンソールを用いている。
予め、HC-06とペアリングする必要がある。PINは、0000または1234のいずれかである。

標準ライブラリにあるBluetoothのAPIはUWP専用であるため、NuGetからコンソールでも使用できるUwpDesktopをインストールする。
Visual Studioの[プロジェクト]メニューバー - [NuGet パッケージの管理]を選択して、UwpDesktopと入力してインストールする。

以下に、Bluetooth通信の手順を大まかに記載する。

  • 周囲のBluetoothデバイスをスキャンする。
  • 見つかったデバイスから、接続・通信を行う端末かどうかを確認して、データの受信を行う。


まず、周囲のBluetoothデバイスをスキャンする。
Bluetoothデバイスは、アドバタイズパケットという自身の場所を伝えるためのデータを発信するため、
そのパケットをスキャンすることでBluetoothデバイスを認識して接続することができる。

以下の例では、BluetoothLEAdvertisementWatcherクラスのプロパティの値を設定した後、Startメソッドを実行してスキャンしている。

 using System;
 using System.Threading;
 using Windows.Devices.Bluetooth.Advertisement;
 
 class Program
 {
    private static BluetoothLEAdvertisementWatcher advWatcher;
 
    static async void Main(string[] args)
    {
       Console.WriteLine("Start");
       advWatcher = new BluetoothLEAdvertisementWatcher();
       advWatcher.SignalStrengthFilter.SamplingInterval = TimeSpan.FromMilliseconds(1000);
       advWatcher.ScanningMode = BluetoothLEScanningMode.Passive;
       advWatcher.Received += WatcherReceived;
       advWatcher.Start();
 
       await Task.Delay(60000);
 
       advWatcher.Stop();
       Console.WriteLine("Stop");
    }
 }


次に、スキャンして見つかったデバイスにおいて、接続・通信を行うデバイスかどうか確認する。
WatcherReceivedメソッドからスキャンされたデバイスの情報を受信するので、その情報を元に確認する。

  1. スキャンして見つかったデバイスが持つUUIDと、接続・通信を行うデバイスのUUIDを比較する。
  2. 同じなら、スキャンを停止してそのデバイスと接続する。
  3. 接続したデバイスから、サービスのUUIDとキャラクタリスティックのUUIDを用いて目的のデータにアクセスする。
  4. アクセスに成功した後、コールバックを設定して、目的のデータの受信する。
    以下のサンプルコードにおいて、CharacteristicChangedVroomControllerメソッド(ユーザ自身が作成したメソッド)は、
    受信したデータ(バイト列)に対して、任意の処理を行うメソッドである。


UUIDを確認する理由は、Bluetooth通信で使用されるデータフォーマットGATTがあるためである。
GATTは、サービスキャラクタリスティックの2つから構成されており、各々がUUIDで設定されている。
サービスがフォルダ、キャラクタリスティックがファイルに相当する。

GATTに則っているBluetoothデバイスは、必ずこの形式でデータを送受信するため、UUIDを確認することで目的のデバイスを判別できる。
UUIDを知る方法は、販売元が公開している場合や自身で全パケットを確認する方法がある。

 private GattDeviceService gattService;
 private GattCharacteristic vroomService;
 private readonly String serviceGuid = "00001101-0000-1000-8000-00805f9b34fb";
 
 private async Task WatcherReceived(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
 {
    bool find = false;
 
    var bleServiceUUIDs = args.Advertisement.ServiceUuids;
    foreach (var uuidone in bleServiceUUIDs)
    {
       // serviceGuid : デバイス固有のGUID
       if (uuidone == serviceGuid)
       {
          // 発見
          find = true;
          break;
       }
    }
 
    if (find)
    {
       try
       {
          // スキャンの停止
          advWatcher.Stop();
 
          // デバイスと接続
          BluetoothLEDevice device = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
 
          // UUIDを用いて目的のデバイスを取得
          gattService = device.GetGattService(serviceGuid);
 
          // キャラクタリスティックUUIDを用いて目的のキャラクタリスティックを取得
          var characteristics = gattService.GetCharacteristics(new Guid("キャラクタリスティックのGuid"));
 
          // 戻り値の配列が空かどうか確認
          if (characteristics.Count > 0)
          {
             vroomService = characteristics.First();
             if (vroomService == null)
             {
                throw new Exception("Not Exist Service");
             }
 
             // 読み取り可能な場合、キャラクタリスティックにアクセスしてコールバックを設定
             if (vroomService.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Read))
             {
                vroomService.ValueChanged += CharacteristicChangedVroomController;
                Console.WriteLine("Connect");
                await vroomService.ReadClientCharacteristicConfigurationDescriptorAsync();
             }
          }
       }
       catch (Exception ex)
       {
          Console.WriteLine($"Exception...{ex.Message})");
       }
    }
 }


スマートフォン(Android)のソフトウェア

クライアントからサーバに接続する時、RFCOMMチャネルを作成してBluetoothSocketを作成する必要があるが、
この時に指定するUUIDは、SPP(シリアルポートプロファイル)のUUIDである00001101-0000-1000-8000-00805f9b34fbを指定する。

また、デバイス名はMYBTとしているため、BondedリストからBluetoothDeviceを探す際の名前も変更する。

Bluetooth通信を使用するパーミッションを設定する。
ディスカバリは行わないため、LOCATIONのパーミッションは設定しない。

 <!-- AndroidManifest.xmlファイル -->
 
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.keicode.android.testapps.bttest1c">
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH"/>
 
    <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:supportsRtl="true"
       android:theme="@style/AppTheme"
    >
       <activity android:name=".MainActivity" android:configChanges="orientation">
            <intent-filter>
               <action android:name="android.intent.action.MAIN" />
               <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
       </activity>
    </application>
 </manifest>


状態を表示するためのTextViewとArduinoおよびHC-06から送信される文字を表示するためのTextViewの計2つのTextViewを並べている。

 <!-- res/layout/activity_main.xmlファイル -->
 
 <?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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.keicode.android.testapps.bttest1c.MainActivity">
 
    <TextView
        android:id="@+id/btStatusTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Not connected" />
 
    <TextView
        android:id="@+id/tempTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="(unknown)"
        android:layout_below="@id/btStatusTextView"
        android:textSize="24sp"/>
 </RelativeLayout>


以下のサンプルコードでは、ワーカースレッド用に1つのクラスを定義しているだけである。
例外が発生した場合、その状況をHandlerを介してUI側にフィードバックしている。

 // MainActivity.java
 
 package com.keicode.android.testapps.bttest1c;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothSocket;
 import android.os.Handler;
 import android.os.Message;
 import android.support.v7.app.AppCompatActivity;
 import android.os.Bundle;
 import android.util.Log;
 import android.widget.TextView;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Set;
 
 public class MainActivity extends AppCompatActivity
 { 
    static final String TAG = "BTTEST1";
    BluetoothAdapter bluetoothAdapter;
 
    TextView btStatusTextView;
    TextView tempTextView;
 
    BTClientThread btClientThread;
 
    final Handler handler = new Handler()
    {
       @Override
       public void handleMessage(Message msg)
       {
          String s;
 
          switch(msg.what)
          {
             case Constants.MESSAGE_BT:
                s = (String) msg.obj;
                if(s != null)
                {
                   btStatusTextView.setText(s);
                }
                break;
             case Constants.MESSAGE_TEMP:
                s = (String) msg.obj;
                if(s != null)
                {
                   tempTextView.setText(s);
                }
                break;
          }
       }
    };
 
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
       Log.d(TAG, "onCreate");
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
 
       // Find Views
       btStatusTextView = (TextView) findViewById(R.id.btStatusTextView);
       tempTextView = (TextView) findViewById(R.id.tempTextView);
 
       if(savedInstanceState != null)
       {
          String temp = savedInstanceState.getString(Constants.STATE_TEMP);
          tempTextView.setText(temp);
       }
 
       // Initialize Bluetooth
       bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
       if( bluetoothAdapter == null )
       {
          Log.d(TAG, "This device doesn't support Bluetooth.");
       }
    }
 
    @Override
    protected void onResume()
    {
       super.onResume();
       btClientThread = new BTClientThread();
       btClientThread.start();
    }
 
    @Override
    protected void onPause()
    {
       super.onPause();
       if(btClientThread != null)
       {
          btClientThread.interrupt();
          btClientThread = null;
       }
    }
 
    @Override
    protected void onSaveInstanceState(Bundle outState)
    {
       super.onSaveInstanceState(outState);
       outState.putString(Constants.STATE_TEMP, tempTextView.getText().toString());
    }
 
    public class BTClientThread extends Thread
    {
       InputStream inputStream;
       OutputStream outputStrem;
       BluetoothSocket bluetoothSocket;
 
       public void run()
       {
          byte[] incomingBuff = new byte[64];
 
          BluetoothDevice bluetoothDevice = null;
          Set<BluetoothDevice> devices = bluetoothAdapter.getBondedDevices();
          for(BluetoothDevice device : devices)
          {
             if(device.getName().equals(Constants.BT_DEVICE))
             {
                bluetoothDevice = device;
                break;
             }
          }
 
          if(bluetoothDevice == null)
          {
             Log.d(TAG, "No device found.");
             return;
          }
 
          try
          {
             bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(Constants.BT_UUID);
 
             while(true)
             {
                if(Thread.interrupted())
                {
                   break;
                }
 
                try
                {
                   bluetoothSocket.connect();
 
                   handler.obtainMessage(Constants.MESSAGE_BT, "CONNECTED " + bluetoothDevice.getName()).sendToTarget();
 
                   inputStream = bluetoothSocket.getInputStream();
                   outputStrem = bluetoothSocket.getOutputStream();
 
                   while(true)
                   {
                      if(Thread.interrupted())
                      {
                         break;
                      }
 
                      // Send Command
                      String command = "GET:TEMP";
                      outputStrem.write(command.getBytes());
 
                      // Read Response
                      int incomingBytes = inputStream.read(incomingBuff);
                      byte[] buff = new byte[incomingBytes];
                      System.arraycopy(incomingBuff, 0, buff, 0, incomingBytes);
                      String s = new String(buff, StandardCharsets.UTF_8);
 
                      // Show Result to UI
                      handler.obtainMessage(Constants.MESSAGE_TEMP, s).sendToTarget();
 
                      // Update again in a few seconds
                      Thread.sleep(3000);
                   }
                }
                catch(IOException e)
                {
                   // connect will throw IOException immediately
                   // when it's disconnected.
                   Log.d(TAG, e.getMessage());
                }
 
                handler.obtainMessage(Constants.MESSAGE_BT, "DISCONNECTED").sendToTarget();
 
                // Re-try after 3 sec
                Thread.sleep(3 * 1000);
             }
          }
          catch(InterruptedException e)
          {
             e.printStackTrace();
          }
          catch(IOException e)
          {
             e.printStackTrace();
          }
 
          if(bluetoothSocket != null)
          {
             try
             {
                bluetoothSocket.close();
             }
             catch(IOException e)
             {
             }
             bluetoothSocket = null;
          }
 
          handler.obtainMessage(Constants.MESSAGE_BT, "DISCONNECTED - Exit BTClientThread").sendToTarget();
       }
    }
 }


 // Constants.java
 package com.keicode.android.testapps.bttest1c;
 
 import java.util.UUID;
 
 public interface Constants
 {
    public static final String BT_DEVICE = "MYBT";
    public static final UUID BT_UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");
 
    public static final String STATE_TEMP = "STATE_TEMP";
 
    public static final int MESSAGE_BT = 0;
    public static final int MESSAGE_TEMP = 2;
 }


次に、環境設定を行う。
ここでは、デバイス名はMYBTとしているが、これをあらかじめペアリングする。
以上の変更を行い、Arduinoとスマートフォンの両方を起動することで自動的にBluetooth接続して、スマートフォンの画面に温度が表示される。

Arduino Bluetooth 3.jpg