#14 Bluetoothを使ってPCと通信しよう【もっと!後輩たちのためのEV3rt講座】

# 目次

| 章 | タイトル | 内容 |
|:-:|:-:|:-:|
| 1 | [はじめに](#1-はじめに) | 今回の説明 |
| 2 | [通信方法](#2-通信方法) | 3つの通信方法を紹介 |
| 3 | [HRP3の機能を用いる方法](#3-hrp3の機能を用いる方法) | HRP3のシリアル通信関数を用いた方法を紹介 |
| 4 | [ログタスク機能を用いる方法](#4-ログタスクを用いる方法) | ログとして文字列を送信する方法を紹介 |
| 5 | [ファイルポインタを用いる方法](#5-ファイルポインタを用いる方法) | ファイルとしてBTを開き送受信する方法を紹介 |
| 6 | [まとめ](#6-まとめ) | 今回のまとめ |

# 1. はじめに

前回は、Bluetoothを用いて、コンパイルした `app` ファイルを無線転送する方法をご紹介しました。
今回も同じくBluetoothを使用し、プログラム実行中にBluetoothで **データのやり取り** を行う方法を説明していきます。

ロボットの実行中に、センサーの値を確認したいということは多々あるかと思います。
今回紹介するBluetooth経由のシリアル通信を用いれば、リアルタイムでセンサー値を確認しながら、ロボットを動かすことができます。
ロボット競技攻略においては必須スキルの一つと言えますので、ぜひ使いこなせるようになりましょう！

## 始める前に

前回、EV3とPCをペアリングしましたが、日を改めて(PCを終了→起動) 作業しようとすると、そのペアリングが切れている場合があります。
その場合、前回のペアリング設定を一度削除し、もう一度ペアリングの操作をやり直してみてください。

接続方法は[前回の記事](https://qiita.com/koushiro/items/6e90accc9bc1ce02b1c5#2-pc%E3%81%A8bluetooth%E3%81%A7%E6%8E%A5%E7%B6%9A)から

# 2. 通信方法

そもそも、EV3rtにおいてどのような無線通信の方法があるのかを整理しておきます。
EV3rtでBluetooth通信を行う場合、以下の3つの方法が挙げられます。

- TOPPERS/HRP3の機能を使う
- ログタスクを用いて、ログとして出力する
- ファイルポインタを用いる

## TOPPERS/HRP3の機能を使う

まずは1つ目の **「TOPPERS/HRP3の機能を使う」** 方法についてです。
TOPPERS/HRP3にはシリアル通信を行う関数が内蔵されています。
恐らく元々は有線接続でのシリアル通信を行うための関数だと思いますが、シリアル通信の伝送路としてBluetoothを指定すると、Bluetoothシリアル通信が実現できます。
EV3→PCへの **送信** 、PC→EV3への **受信** の両方に対応しています。

## ログタスクを用いて、ログとして出力する

2つ目の **「ログタスクを使う」** 方法について。
そもそもEV3rtには、ログを出す機能があり、デフォルト設定では「LCD」(EV3の液晶) となっています。
EV3rt起動時に現れる「EV3rt Console」に、ログが出力されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/6031eb01-a987-7275-bc25-0f5bcaef9b08.jpeg" width=60% />

このログを出す場所は `rc.conf.ini` ファイルから設定する事が出来、 (詳細は[こちら](https://qiita.com/koushiro/items/e3d04e3a4e266c603865#debug)) 「BT」と設定することでBluetooth接続先のコンソールにログを出すことが出来ます。
こちらはあくまで **「ログを出す機能」** であり、EV3→PCへの **送信** のみで、PC→EV3の **受信** の用途には使えません。

## ファイルポインタを用いる

最後は「ファイルポインタを用いる」方法です。
C言語には「ファイル」を扱う関数が搭載されています。 `stdio.h` というヘッダファイルにより使えるようになる、いわゆる「標準入出力ライブラリ」ですね。この中から、 `fprintf` や `fgetc` `fgets` などを使っていきます。

本来、これらの関数はファイルに対してテキストなどを読み書きする関数であり、「どのファイルを扱うか」を指定するのに **「ファイルポインタ」** というものを用います。
この「ファイルポインタ」に、専用の関数を用いて「Bluetoothシリアルポート」を指定すると、`fprintf` や `fgetc`などによりBluetoothでのシリアル通信が出来るようになるわけです。

EV3→PCへの **送信** 、PC→EV3への **受信** の両方に対応しています。
ファイル書き込み = 送信、ファイル読み込み = 受信、のような対応関係ですね。

ここまでの3つの方法をまとめると以下のようになります。

| 方法 | 送信 | 受信 |
|:-:|:-:|:-:|
| HRP3の機能 | 〇 | 〇 |
| ログタスク | 〇 | ✖ |
| ファイルポインタ | 〇 | 〇 |

では、実際に使い方を紹介していきたいと思います！

# 3. HRP3の機能を用いる方法

## EV3 → PC (送信)

まずはHRP3に搭載されている「シリアル通信を行う関数」を使い、EV3からPCへ文字列を送信します。
その際に使うのが `serial_wri_dat()` 関数です。
簡単なプログラム例を通して、使い方を説明していきます。

```c:app.c
void main_task(intptr_t unused)
{
    int i;
    char message[64]; // 最大文字数の設定 (実際には配列の数-1まで)
    for(i=0; i<10; i++){
        sprintf(message, "%d: Hello Bluetooth!!\n", i);
        serial_wri_dat(SIO_PORT_BT, message, sizeof(message));
        tslp_tsk(1*1000*1000);
    }
}
```

 `serial_wri_dat()` 関数は、シリアルポートに文字列を送信する関数で、第一引数に「シリアル通信を行うポート」、第二引数に「送信する文字列」、第三引数に「送信する文字列の長さ」を指定します。
「シリアル通信を行うポート」にはBluetoothを指定したいわけですが、 `SIO_PORT_BT` とすればその意味になります。
上記のコードで言うと、「`SIO_PORT_BT` = Bluetoothポートに」「char型配列 `message` の文字列を」「`message` の長さ分だけ」送信する、と言う意味になります。
( `sizeof()` を使うと、配列の長さが得られます。)

また、当たり前のように `sprintf()` 関数を使っていますが、これの解説は[#9でLCD上に文字列を描く](https://qiita.com/koushiro/items/2e10eeee3fdf39866a00#%E6%96%87%E5%AD%97%E5%88%97%E3%82%92%E6%8F%8F%E3%81%8F)際に簡単に紹介しているので、そちらを確認してください。

### 通信を確認

では、このコードをコンパイルしてEV3に転送し、実際にEV3とPC間でシリアル通信を試してみましょう！
前提として、EV3をPCとBluetoothペアリングしておいてください。

次に、前回 `app` ファイルを転送するのに使用した **Tera Term**を開き、シリアルポートを開いてください。
(Tera Termでの接続方法は[こちら](https://qiita.com/koushiro/private/6e90accc9bc1ce02b1c5#tera-term-%E3%81%A7%E3%81%AE%E6%8E%A5%E7%B6%9A))

シリアルポートが開けたら、先に設定をしておきます。
まずは、「設定」→「端末」を開いてください。
![スクリーンショット 2024-12-28 192415.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/4d243b53-e60f-32ac-e209-4fa479500f2d.png)

![スクリーンショット 2024-12-28 192836.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/4a57b55e-ba2b-0bda-e5c9-41f4d966b7c4.png)
デフォルトでは改行コードが「CR」になっているはずなので、これを **「LF」** に変更して下さい。
(「CR」で進めたい場合は、EV3のプログラムの改行コードを適宜 `\r\n` に変更して下さい)
又、「ローカルエコー」に☑チェックを入れておいてください。

続いて、「設定」→「シリアルポート」を開いてください。
![スクリーンショット 2024-12-28 192721.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/cea57910-4130-b375-ae61-834f6996250b.png)
スピードがデフォルトでは「9600」になっているかと思いますので、「115200」に変更して下さい。

これで、設定は完了です！

それでは、EV3に転送した `app` ファイルを実行してみましょう！
すると、Tera Term上に以下のように文字列が表示されるはずです。
![スクリーンショット 2024-12-28 212326.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/8aae6760-b612-d282-46bf-52e9536b60a9.png)
**これでEV3→PCへの文字列転送が出来ました！**

## PC → EV3 (受信)

今度は、今の逆、PCからEV3への文字列伝送を試してみましょう！
コードを以下のように書き換えていきます。

```c:app.c
void main_task(intptr_t unused)
{
    char recieve_buf;
    ev3_lcd_set_font(EV3_FONT_MEDIUM);

    while(1){
        serial_rea_dat(SIO_PORT_BT, &recieve_buf, 1);
        ev3_lcd_draw_string(&recieve_buf, 10, 10);
    }
}
```

受信には `serial_rea_dat()`関数を使います。
 `serial_rea_dat()` 関数は、シリアルポートから文字列を受信する関数で、第一引数に「シリアル通信を行うポート」、第二引数に「受信した文字列を格納するchar型配列」、第三引数に「受信する文字列の長さ」を指定します。
「シリアル通信を行うポート」には先ほど同様`SIO_PORT_BT` としてBluetoothポートを指定します。

上記のコードで言うと、「`SIO_PORT_BT` = Bluetoothポートから」「文字列を受信してchar型配列 `recieve_buf` に」「1文字分だけ」格納する、と言う意味になります。
初めてなのでとりあえず1文字ずつ受信するようにしています。

### 通信を確認

では、実際にプログラムを実行してみましょう！
コンパイルした `app` ファイルをEV3に転送し、実行してください。
実行したらTera Termに何か文字を打ってみてください。
(日本語には対応していないので英数半角で入力してください)
![スクリーンショット 2024-12-28 214510.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/df801312-b1a4-a788-1a76-8cce06494e6e.png)
すると、EV3のLCD上に入力した文字が表示されるはずです！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/713c1a40-8341-acf7-86b8-c65630971905.jpeg" width=60% />
これにて、**PC→EV3への文字列伝送も完了です!!**

ちなみに、以下のように書き換えば1文字ずつではなく、複数文字の転送も可能です。
```c:app.c
void main_task(intptr_t unused)
{
    char recieve_buf[8];
    ev3_lcd_set_font(EV3_FONT_MEDIUM);

    while(1){
        serial_rea_dat(SIO_PORT_BT, recieve_buf, 8);
        ev3_lcd_draw_string(recieve_buf, 10, 10);
    }
}
```
![スクリーンショット 2024-12-28 215301.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/fc4106c7-165b-69cf-0abf-114c670147a3.png)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/aa6cdb52-678d-b3b0-3210-79b7f3b7ac1f.jpeg" width=60% />
このプログラムでは、8文字受信するごとにLCD上の文字列を更新します。

複数文字で転送する場合は、送信側の文字数を固定するなどしておかないと、始まりや終わりの位置を見失ってしまう可能性があるので気を付けましょう。

# 4. ログタスクを用いる方法

それでは方法を変えて、 **ログタスクを用いる方法** にチャレンジしてみましょう。
この方法は前述の通り **EV3→PC (送信)** しか出来ないので、用途によって使い分けてください。

## 設定ファイル(rc.conf.ini) の変更

まずは、ログの出力先をデフォルトのLCD上から、Bluetooth接続先に変更します。
[#3 アプリケーション開発のおやくそく](https://qiita.com/koushiro/items/e3d04e3a4e266c603865#debug)で紹介した `rc.conf.ini` ファイルの編集方法に従って、以下のように変更します。

```conf:rc.conf.ini
[Debug]
DefaultPort=BT 👈(もともとはLCDになっているはず)
```

これでログの出力先がBluetoothで接続されたPC (Tera Term上) に変わりました。

## プログラム

では、ログタスクを用いて文字列を転送するコードを書いていきましょう。

```c:app.c
void main_task(intptr_t unused)
{
    int i;
    for(i=0; i<10; i++){
        syslog(LOG_NOTICE, "%d: Log via Bluetooth", i);
        tslp_tsk(1*1000*1000);
    }
}
```

使うのは `syslog()` 関数で、ログとして文字列を出力する関数です。
第一引数に「ログ情報の重要度」、第二引数に「送信したい文字列(フォーマット指定子対応)」、第三引数以降は必要に応じて「文字列に組み込む変数」を指定します。

まずログ情報の重要度は、`hrp3/include/t_syslog.h`に定義されているのですが、`LOG NOTICE`より小さい値を指定するとログとして出力されるようです。
特にこだわりが無ければ、`LOG NOTICE` を指定しておきましょう。

### 通信を確認

それでは、コンパイルした `app` ファイルをEV3に転送し、実行してみましょう。
先ほど同様、Tera Termを起動し、各種設定を行っておいて下さい。

EV3側のアプリを実行すると、以下のように表示されるはずです。
![スクリーンショット 2024-12-28 222534.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/a38a9012-27b1-6ff4-0f21-a9c060a735ef.png)

これで、**ログタスクを用いたEV3→PCの文字列転送**が出来ました！
EV3→PCへの一方向の通信で事足りる場合は、こちらの方法の方が一行でシンプルに書けて良いかと思います。
センサ値のロギングなどに重宝しそうですね。

# 5. ファイルポインタを用いる方法

最後の方法、**ファイルポインタを用いる方法**についてです。
こちらは「EV3→PC (送信)」「PC→EV3 (受信)」の双方向の通信に対応しています。
Bluetooth経由の通信を **「まるでファイルを扱う」** かのように行えるのがこの方法のポイントです。

## EV3 → PC (送信)

まずは EV3→PCへの文字列送信についてです。
実際のコードを見ながら確認していきましょう。

```c:app.c
void main_task(intptr_t unused)
{
    int i;
    FILE *bt = ev3_serial_open_file(EV3_SERIAL_BT); // Bluetooth仮想シリアルポートのファイルをオープンする
    if(bt != NULL){    
        for(i=0; i<10; i++){
            fprintf(bt, "%d: Hello Bluetooth!!\n", i);
            tslp_tsk(1*1000*1000);
        }
    }
}
```

まず4行目で、Bluetoothシリアルポートを「ファイル」として開くために、 `ev3_serial_open_file()`関数を使用しています。
この関数は第一引数に指定したポートをファイルとして開き、ファイルポインタを返します。
ここでは、ファイルポインタ `bt` が、Bluetoothシリアルポートを用いた伝送路として登録された形です。
これは**送信、受信に関わらず共通で実行する**必要があります。

第一引数に指定するシリアルポートは、`hrp3/sdk/common/ev3api/src/ev3api_fs.h`に以下のように定義されています。
```c
typedef enum {
    EV3_SERIAL_DEFAULT = 0, // デフォルトのシリアルポート（ログタスク用ポート）
    EV3_SERIAL_UART = 1, // UARTポート（センサポート1）
    EV3_SERIAL_BT = 2, // Bluetooth仮想シリアルポート
    EV3_SERIAL_SPP_MASTER = 3, // Bluetooth SPPマスタの仮想シリアルポート
} serial_port_t;
```

ファイルポインタとして開けたら、正しく開けているかの確認(5行目のif文)をした後、文字列転送をしていきます。
ここで使われるのが7行目の `fprintf()`関数です。

ここまでLCD上に文字列を表示するために`sprintf()`関数を用いてきましたが、それの**ファイル書き込み**バージョンと思っていただければ大丈夫です。
第一引数に「ファイルポインタ」、第二引数には「書き込みたい文字列」、必要に応じて第三引数以降に「文字列に組み込みたい変数」を指定します。

詳しくは以下を参照👇

https://bituse.info/c_func/46

本来この関数は、ファイルにテキストを書き込む関数ですが、今Bluetoothシリアルポートをファイルとして開いているので、この関数でEV3→PCへの文字列転送が可能となります。

### 通信を確認

それでは、コンパイルした `app` ファイルをEV3に転送し、実行してみましょう。
これまで同様、Tera Termを起動し、各種設定を行っておいて下さい。

EV3側のアプリを実行すると、以下のように表示されるはずです。
![スクリーンショット 2024-12-28 230034.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/83fb1c17-49f5-bdcd-a571-dc3dfd945378.png)

これで、 **ファイルポインタを用いたEV3→PCへの文字列送信** が出来ました！

文字列転送には`fprintf()`に限らず、`fputc()`や``fputs()``も使えるはずですので、ご自身の用途に応じて使用してみてください。

## PC → EV3 (受信)

次にEV3→PCの文字列受信についてです。
こちらも実際のコードを見ながら確認していきましょう。

### 1文字の転送

まずは、1文字ずつ受信する方法からです。

```c:app.c
void main_task(intptr_t unused)
{
    FILE *bt = ev3_serial_open_file(EV3_SERIAL_BT); // Bluetooth仮想シリアルポートのファイルをオープンする
    ev3_lcd_set_font(EV3_FONT_MEDIUM);
    if(bt != NULL){
        while(1){
            // fgetcで1文字ずつ読み込むパターン
            int c = fgetc(bt);
            ev3_lcd_draw_string(&c, 10, 10);
        }
    }
}
```

先ほど同様、3行目でBluetoothシリアルポートをファイルとして開き、ファイルポインタに指定しています。

続いて受信する部分ですが、8行目の `fgetc()` 関数がそれにあたります。
`fgetc()`関数はファイルから1文字読み込む関数で、引数にファイルポインタを指定します。

ここでは、Bluetoothシリアルポートから1文字読み出し、変数`c`へ格納するようになっています。
ここで変数`c`の型が `int` であることに違和感を覚えるかもしれませんが、これは `fgetc()` 関数の戻り値の型が`int`だからです。C言語で「1文字」を扱うときはしばしば`char`ではなく`int`が用いられるので注意しましょう。
受信した文字はLCD上に表示します。

### 通信を確認

それでは、コンパイルした `app` ファイルをEV3に転送し、実行してみましょう。
コンパイルした `app` ファイルをEV3に転送し、実行してください。
実行したらTera Termに何か文字を打ってみてください。
(日本語には対応していないので英数半角で入力してください)
![スクリーンショット 2024-12-28 231504.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/807b42ed-d4d1-5eb1-1f30-5e6c0a4ec26a.png)
すると、EV3のLCD上に入力した文字が表示されるはずです！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/5e0a9945-447c-8a07-63de-d0753d08f5f3.jpeg" width=60% />

まずはこれで、 **PC→EV3へ1文字の転送(受信)** が出来るようになりました!

### 複数文字の転送

せっかくなので、複数文字の転送も試してみましょう。

```c:app.c
void main_task(intptr_t unused)
{
    FILE *bt = ev3_serial_open_file(EV3_SERIAL_BT); // Bluetooth仮想シリアルポートのファイルをオープンする
    ev3_lcd_set_font(EV3_FONT_MEDIUM);
    if(bt != NULL){
        while(1){
            // fgetsで文字列を読み込むパターン
            char buf[8];
            memset(buf, "\0", sizeof(buf)); // バッファをクリアするにはこれを使う
            fgets(buf, sizeof(buf), bt);
            ev3_lcd_fill_rect(0, 0, EV3_LCD_WIDTH, EV3_LCD_HEIGHT, EV3_LCD_WHITE);
            ev3_lcd_draw_string(buf, 10, 10);
        }
    }
}
```

Bluetoothシリアルポートを開くところは先ほどと同様です。
受信部分について、今度は10行目の `fgets()` 関数を使用しています。
`fgets()` 関数はファイルから複数文字読み出す関数で、第一引数に「読みだした文字列を格納するchar型配列」、第二引数に「読み出す文字数」、第三引数に「読み出し元となるファイルポインタ」を指定します。

今回の場合、Bluetoothシリアルポートから文字列を読み出し、char型配列 `buf` に8文字分格納する、ということになります。
読みだした文字列はLCD上に表示されます。

ここで、9行目の `memset()`関数ですが、これは新しく作成した配列 `buf` の中身を `\0` すなわちNULL文字で埋めるという意味です。これをしておかないと、 `buf` の中に何が入っているか分からない状態でプログラムを進めることとなり、意図しないものが入っている可能性が出てくるため、それを排除しています。

では、このプログラムを実行してみましょう。

### 通信を確認

それでは、コンパイルした `app` ファイルをEV3に転送し、実行してみましょう。
コンパイルした `app` ファイルをEV3に転送し、実行してください。
実行したらTera Termに何か文字を打ってみてください。
(日本語には対応していないので英数半角で入力してください)
![スクリーンショット 2024-12-28 232709.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/aba6f75f-c876-2593-23ac-33115aefb725.png)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/80fb61e6-9bed-5fb0-e7a0-a417ee56173c.jpeg" width=60% />

あれ、配列の長さに合わせて8文字入力したはずが、LCD上に7文字しか表示されていません…
なぜでしょうか…？？

これは、`fgets()`関数の仕様で、この関数で読みだした文字列の一番最後には、 **終端文字** と呼ばれる文字列の一番最後を表す文字が入る決まりとなっています。
従って、 `fgets()` 関数で読み出すことが出来る **実質的な文字数** は「第二引数で指定した値-1」となります。
ここでは、char型配列 `buf` の長さが8ですので、**実質的な読み出し文字数** は **7** となります。

詳しくはこちら👇

https://bituse.info/c_func/28

以上で、 **PC→EV3へ複数文字の転送(受信)** が完了です!!
また、全てのPC-EV3間 Bluetooth通信をマスターしました!!

# 6. まとめ

いかがだったでしょうか？
3つの方法を紹介しましたが、どれも簡単に使えて良いかと思います。
ただ、細かなところを見ていくと、状況によって向き不向きが出てくるかと思います。
使いたい状況とこれら3機能の特性を考え、使い分けてください！

次回は、UART通信というものを用い、有線で他のデバイスと通信する方法を紹介したいと思います。
実は、今回の記事でその方法の片鱗が出てきています…

前回: [#13 Bluetooth接続をしよう](https://qiita.com/koushiro/items/6e90accc9bc1ce02b1c5)
次回: 執筆中