Hardware engineer が ChatGPT に頼りながら Bluetooth 通信のための Python コードを書く

この記事は[株式会社ビットキー Advent Calendar 2024](https://qiita.com/advent-calendar/2024/bitkey) 9日目の記事です．
Hardware Devlopment で回路/基板設計をしている安達 (@lllo_olll) が担当します．



## はじめに

今回は

- プログラマー職ではないハードウェアエンジニア (電気設計) の私が
- ChatGPT ものすごい頼りながら
- Macbook 上で Python のコードを書いて iPhone に Bluetooth 接続しにいく

という記事になります！

Bluetooth については 5日目の記事に概要がありますので気になる方はチェックしてください

[**Bluetooth 6.0が実現する高精度な距離測定 - Channel Soundingの仕組みと可能性**](https://qiita.com/hu11911/items/2f1f4ea271d97f6ae674)

## 環境

特に下記の通りである必要はないものばかりです．お使いの環境に合わせてください

- Device: Macbook, iPhone 15 Pro
- Language: Python
- Editor: Jupyter Notebook
- AI chatbot: ChatGPT 4o　　（こちらだけは，有料版が好ましいですが）
- Apps: LightBlue (iPhone側．ver 5.1.9)

## 1. ChatGPTとの対話の時間

さて，では，ウソップのようにエルバフの戦士(巨人=ChatGPT) の肩の上に立って
対話をしながらコードを創っていきます！

※ 全てを真面目に読むと大変なので，まずさっと読むことを推奨します
※ こんな雰囲気で会話するんだな，が伝わっていただければOKの章です！

初動はこんな質問を
個人的なこだわりとしては，自身の普段の言葉遣いに合わせて会話して

![01_chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/07539606-61b6-d4d8-a517-ade50a50997b.png)

手順を丁寧に説明してくれていて，説明しながら間にコードを入れてくれます

![02_chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/b9ba871e-c270-bb21-f413-69dc6cd9f39b.png)

注意点とかも教えてくれる

![03_chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/119545cf-aff8-8517-52a3-6e7e4243e22b.png)



初動の質問のあとは，いろいろと対話を重ねながらコードを生成させていきます

本記事では，コードの紹介は後にまわしますが，実際にはこの対話をしながら
Jupyter Notebook などでコードを実行しながら，Try & Error で進めていきます

では，その後，私が ChatGPT に投げた会話をさらっと紹介します

まず，説明が抜けていたものがあったので追加依頼
![04_chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/de2a706d-03dc-a00e-4415-20493cc36638.png)


コードを実行したら，スタバにいたので大量の BLE device が見つかり大変だったので
RSSI をしきい値にして絞って，とお願い

![05_chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/2553f5dd-a618-0832-d273-158882773450.png)


エラー出てるやん！　直して！　という雑なお願い
なぜこのエラーに至ったかはしっかり解説してくれるので
それをしっかり把握しながら進めればかなり知見が蓄積されていくので Good

![06_chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/ec7beb96-1100-97ff-87a7-4df0220d48bc.png)



だいぶ Try & error してプログラムがたくさんできたので
一旦まとめたものを書いてもらった




![07_chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/d07a6dac-fa67-0b8d-a629-d62dffe5503d.png)





こんな感じの会話で，良い感じのコードくんでくれました！　God ですね

## 2. 実行しよう

### 2.1. iPhone 側設定 (アプリ LightBlue で設定)

まずは iPhone 側で設定しておきます（こちらも ChatGPT 指示) 
LightBlue で行います



<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/901f5413-e6f3-489e-70f2-f943f47c6585.png">

1. 画面下部のタブにある Virtual Devices をタップ
2. 画面右上の + をタップ
3. New Virtual Peripheral という選択画面が出てくるので，今回は Alert Notification を選択
ここは，どのメッセージを BLE のメッセージとして送るかを聞かれている
4. 作られた Alert Notification という項目をタップ
5. Name のところを，自分の iPhone だとわかるように名前を編集
今回は |||o_o||| という私の Qiita のアカウント名にしてみる

### 2.2. ChatGPT と対話の末に完成したコードの実行

こんな感じになりました
ChatGPT は勝手にコメントも記入してくれるので有難いですね

```python
from bleak import BleakScanner
from bleak import BleakClient
import asyncio
import nest_asyncio
nest_asyncio.apply()

# 設定パラメータ
rssi_threshold = -60  # RSSIの閾値
target_name = "lllo_olll" # ターゲットデバイス名

# 周辺 BLE module をスキャンして，RSSI の値が強いものだけを抽出
async def scan_devices_with_rssi():
    print(f"Scanning for devices with RSSI > {rssi_threshold}dBm...")
    devices_found = {}

    # コールバック関数
    def detection_callback(device, advertisement_data):
        if advertisement_data.rssi > rssi_threshold:
            # 名前とアドレスをキーにして重複排除
            devices_found[(device.name, device.address)] = advertisement_data.rssi

    # BleakScannerのコンストラクタでコールバックを設定
    scanner = BleakScanner(detection_callback=detection_callback)

    await scanner.start()
    await asyncio.sleep(10)  # 10秒スキャン
    await scanner.stop()

    # 一覧表示
    print("\nDetected Devices:")
    for (name, address), rssi in devices_found.items():
        print(f"Name: {name}, Address: {address}, RSSI: {rssi}")

    return devices_found

# 抽出された Device の中から target_name と同じ名前のものに接続しにいく
async def connect_to_device_by_name(devices, target_name):
    for (name, address), rssi in devices.items():
        if name == target_name:
            print(f"Target device found! Connecting to {name} at {address}...")
            client = BleakClient(address)
            try:
                await client.connect()
                if client.is_connected:
                    print(f"Successfully connected to {name} at {address}")
                    return client  # 接続成功時にBleakClientを返す
            except Exception as e:
                print(f"Failed to connect to {name} at {address}: {e}")
            return None

    print(f"Target device '{target_name}' not found in the scanned devices.")
    return None

# LightBlue で Alert Notification (UUID=0x1811) を設定したので，その内容を確認しにいく
async def read_alert_notification(client):
    try:
        # サービスUUID
        service_uuid = "00001811-0000-1000-8000-00805f9b34fb"  # 0x1811 in full format

        # 接続デバイスのサービス一覧を取得
        services = await client.get_services()
        print("Available Services:")
        for service in services:
            print(f"- {service.uuid} ({service.description})")

        # 該当サービスを探索
        for service in services:
            if service.uuid == service_uuid:
                print(f"Found Alert Notification Service: {service.uuid}")
                # キャラクタリスティック一覧を表示
                for char in service.characteristics:
                    print(f"  - Characteristic: {char.uuid} ({char.properties})")
                    # 読み取り可能なキャラクタリスティックを探索
                    if "read" in char.properties:
                        value = await client.read_gatt_char(char.uuid)
                        print(f"Value from {char.uuid}: {value}")
                return

        print("Alert Notification Service not found.")

    except Exception as e:
        print(f"Failed to read from Alert Notification Service: {e}")

# Main 関数
async def main():
    
    # スキャンしてデバイス一覧を取得
    devices = await scan_devices_with_rssi()

    # ターゲットデバイスに接続
    client = await connect_to_device_by_name(devices, target_name)
    if client is None:
        return

    # 接続したデバイスからAlert Notificationを読み取る
    await read_alert_notification(client)

    # 接続解除
    await client.disconnect()
    print("Disconnected.")

# 実行
asyncio.run(main())
```

さて，実行結果です (一部マスクかけさせてもらいました)

Macbook と iPhone は真横においたので，RSSI 値はかなり良い値になっているのがわかりますね
ただ，さすが Starbacksで，他の -60dBm の範囲内だとかなり他のデバイスが見えてくる．．．

接続も問題なく成功してますね．Successfully connected となっていて，
Alert Notification に入れられた Battery などの情報が読み取れていることが分かります

![09_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3953115/e2374a9e-16fc-06b4-7adb-0ad264f0aad6.png)


## 締めの言葉

いかがでしたか？　

簡単そうだけど，わりと長めのプログラムできあがっているな，と思っています

私の経歴としては，機械工学専攻の出身で，実験時に Python でデータの処理をしてみる，
というところからプログラミングに入ったのですが，当初は何か始めるにしてもいくつか
記事を Qiita なりで探して調べてから実行して，エラーが出たらまたいくつか記事を見て
実行して，と大変でした．その時代から，情報は Chatbot などの Chatbot “１つ” に集約
されていて，そかも自身の望む形で情報が手に入るようになりましたね．それも簡単に．
本当に良い時代にエンジニアになれてよかったです

この記事を読んで，プログラミングに手を伸ばしたかった方の背中がおせると幸いです！

---

明日 (10日目) の[株式会社ビットキー Advent Calendar 2024](https://qiita.com/advent-calendar/2024/bitkey) は
Androidアプリ開発を担当している@crakaCが担当します，お楽しみに！