【Android × Raspberry Pi 】ハンズフリーで後続車にメッセージを送ってみた

# 目次

* はじめに
* この記事をおすすめする人
* システム構成
* SpeechRecognizerとは？・使い方
* BLEのMTU制限問題・解決策
* Pythonを用いてラズパイ画面に表示するやり方
* 実装編・UI設計
* 最後に


こちらの記事は修士1年の研究の合間に頑張って作ったものになります..
是非読んでください..!!

# 1. はじめに




### 後ろの車にメッセージを送りたくないですか...?
緊急事態で後続車に停止している旨を伝えたり...
感謝のメッセージを伝えたり...
(実はサンキューハザードは正しい使い方ではありません...)

しかし、スマホで文字を打つとなると、運転中に携帯を触ってはいけないため、
中々メッセージを伝えられません...

### 音声入力でメッセージを伝えよう！！！

本記事では、android端末に音声入力したものが、後ろに設置してあるラズパイにBLE送信され、ラズパイはモニターに表示し、音声入力の文字列が後続車に伝えることができます。
[Android端末] --BLE--> [Raspberry Pi] --HDMI--> [モニター]



ゴールは以下の動画のように、音声入力だけで後続車にメッセージを送ることができます。



https://twitter.com/paupau_engineer/status/1673543471809052672?s=46&t=j3w6NdCebqL8ss67UL7ETg







使用する際は以下を厳守でお願いします。

:::note alert
絶対に煽り運転に使用しないようにしてください。
:::



### 使用端末
android端末: Google Pixel 6
Raspberry Pi : Raspberry Pi 3 model B




# 2. この記事をおすすめする人
- Android端末のBLE通信を実装したい人
- ラズパイのBLE通信を実装したい人
- 後続車にメッセージを送ってみたい人
- Pythonを使ってラズパイGUIで画面表示したみたい人



# 3. システム構成
![全体像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/5e4588ec-a834-216e-9846-0bab67bbf5ea.png)


① Android端末はアドバタイズ、ラズパイはスキャンし、接続します。
② AndroidのSpeechRecognizerクラスを使用し、「メッセージ入力」をまちます。
③ 「メッセージ入力」が音声入力されると、伝えたいメッセージを受け付けます。
④ 伝えたいメッセージが入力されると、ラズパイにBLE送信されます。
⑤ モニターに出力させ、後続車にメッセージを送ります。
(⑥ モニターを後続車が見ることで、メッセージが伝わります。)


# 4. SpeechRecognizerとは？
SpeechRecognizerは、Androidプラットフォームで音声認識を実現するためのクラスです。音声入力を受け付け、それをテキストに変換する機能を提供します。

SpeechRecognizerは、マイクからの音声入力を受け取り、音声を解析してテキストに変換します。これにより、ユーザーが音声を使ってアプリケーションと対話したり、音声コマンドを使用して操作したりすることが可能になります。

SpeechRecognizerは、AndroidのSpeech-to-Text（音声認識）APIを使用して実装されています。このAPIは、デバイスの内部音声認識エンジンを使用して音声を解析し、テキストに変換します。音声認識エンジンはデバイスによって異なる場合がありますが、通常はGoogleの音声認識エンジンが使用されます。

実装は結構簡単で、こちらの方を参考にさせていただきました。


https://qiita.com/kaleidot725/items/8fe6b6f137a9a8710d21

# 5. BLEのMTU制限問題・解決策
使用しているRaspberry Pi 3はBluetoothのバージョンにより、MTU(Maximum Transmission Unit)は23バイトです。

よって23バイト以上の場合はパケットを区切って送る必要があります。



Speech-to-Textでは日本語に設定しているので、
6文字で区切るように設定し、なくなるまで繰り返し送るように実装しました。
そして、送りたいメッセージが送り切ると、endの文字列を送ります。
以下はandroid側の実装です。


```java
private void sendMessageOverBLE(String message) {
    int index = 0;
    int maxLength = 6;

    // メッセージを指定された長さで分割して送信
    while (index < message.length()) {
        String subMessage;
        if (index + maxLength <= message.length()) {
            subMessage = message.substring(index, index + maxLength);
        } else {
            subMessage = message.substring(index);
        }
        mCharacteristic.setValue(subMessage);

        // Bluetooth接続のパーミッションが許可されているかチェック
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        // 接続中のデバイスにキャラクタリスティック変更の通知を送信
        if (mDevice != null) {
            mBluetoothGattServer.notifyCharacteristicChanged(mDevice, mCharacteristic, false);
        } else {
            Log.w(TAG, "No connected device to notify.");
        }

        index += maxLength;
    }

    // メッセージの送信完了を示す"end"を設定して通知
    mCharacteristic.setValue("end");
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        return;
    }
    if (mDevice != null) {
        mBluetoothGattServer.notifyCharacteristicChanged(mDevice, mCharacteristic, false);
    } else {
        Log.w(TAG, "No connected device to notify.");
    }

    // UIスレッド上でメッセージの表示と保持を更新
    runOnUiThread(() -> {
        messageTextView.setText(message);
        MainActivity.this.message = message;
    });
}

```



以下はラズパイ側の実装です。

messageが「end」ではない場合は、受信したメッセージが途中のデータであるため、バッファに追加して結合を待ちます。

messageが「end」である場合、バッファに格納されているメッセージを結合し、完全なメッセージを作成します。完全なメッセージが「プログラム終了」と等しい場合、プログラムを終了します。それ以外の場合は、完全なメッセージを表示します。




```python
def handleNotification(self, cHandle, data):
    #区切られた文字を表示
    print('Received notification (raw): ', data)

    try:
        message = data.decode('utf-8')
    except UnicodeDecodeError:
        message = data.decode('utf-8', 'replace')  

    # 受信したデータが"end"である場合、完全なメッセージを結合し処理を行う
    if message == 'end':
        complete_message = ''.join(self.message_buffer)
        print('Received complete message: ' + complete_message)
        # 完全なメッセージが"プログラム終了"である場合、プログラムを終了する
        if complete_message == 'プログラム終了':
            sys.exit()
        # 完全なメッセージを表示する
        self._display_message(complete_message)
        self.message_buffer = []  
    else:  
        # メッセージが途中のデータである場合、バッファに追加して結合を待つ
        self.message_buffer.append(message)

    print('Received notification: ' + message)
```

# 6. Pythonを用いてラズパイ画面に表示するやり方

ラズパイの画面上に文字を表示する方法を説明します。
実装は簡単で、先ほど完成したmessageを使い、
以下のように実装しています。
文字サイズ的に、5文字目で改行を挟むようにしています。


```python

def _display_message(self, message):
    def _blink():
        for _ in range(3):  # 3回点滅する
            root = tk.Tk()
            root.attributes('-fullscreen', True)  # フルスクリーンウィンドウ
            # メッセージを5文字ごとに改行してフォーマットする
            formatted_message = "\n".join([message[i:i+5] for i in range(0, len(message), 5)])
            label = tk.Label(root, text=formatted_message, font=("Helvetica", 200))  # 大きなフォントサイズ
            label.pack(expand=True)
            root.after(1000, lambda: root.destroy())  # 1秒後にウィンドウを閉じる
            root.mainloop()
            time.sleep(3)  # 点滅の間に一時停止
    threading.Thread(target=_blink).start()

```
以下のようにモニターに表示されるようになっています。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/e1559ac9-b775-7921-09dc-b5f023ee6306.jpeg" width=45%>




# 7. 実装編・UI設計

### 実装編

プログラムはGitに公開しているので、そちらをご覧ください。


https://github.com/paupau0227/car-message-system



### AndroidのUI設計
UIと言っても、最低限しか作っていません(⌒-⌒; )

- 初期画面
    - 初期状態は"Message not entered"と表示されており、接続対象のデバイスにBLE接続するまでこの状態です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/d0cb7d57-a24c-c031-53cf-2cf3a0fc13dc.png" width=35%>



- BLE接続完了後
    - 対象デバイスにBLEに接続後、右上に音声入力が出ているように、「メッセージ入力」を待つ画面になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/3fe4cd42-3d7f-fe04-3ec2-0c7f2f3c0cda.png" width=35%>

- メッセージ入力フェーズ
    - 「メッセージ入力」が入力されると画面が黄色に遷移し、音声入力を待つフェーズになります。
    - ここで音声入力された文字列がBLEで送信されます。
    - 何も話さなければ、3秒ほどでこのフェーズは閉じます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/09df33cb-40c4-69d9-d665-4a7758b3c3ec.png" width=35%>


- メッセージ送信後
    - メッセージが送信されると、どんな文字が送られたのかが表示されています。
    - 次のメッセージが入力されるまで、文字列は表示され続けます。
 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/a3fcbbfd-c719-4c4b-d8c2-bba8456ddda9.png" width=35%>



# 8. 最後に

今回はハンズフリーで後続車にメッセージを送るシステムを作成しました！
時間があれば、後続車との連携ができるアドホックネットワークのようなシステムも作ってみたいですね〜！！
良ければ、いいねをお願いいたします！
ありがとうございました！