BLEセンサー（Peripheral）を作成して、GATT仕様書と読み比べる - ESP32 MicroPython

# ESP32用のMicroPython

ESP32用のMicroPythonもBluetooth Low Energy（BLE）をサポートしています。
[NimBLEというスタック](https://github.com/micropython/micropython/tree/master/extmod/nimble)を組み込んでいるようです。
Windows10 上の Ubuntu でビルドし、ESP32本体で動作確認を行うことができます。

https://micropython-docs-ja.readthedocs.io/ja/latest/library/ubluetooth.html

# Windows10でUbuntuをインストール

## Ubuntuインストールの準備とインストール

Windows PowerShell を「管理者として実行する」で起動し、次のコマンドで Windows Subsystem for Linux オプション機能を有効にします。

```
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
```

Windows10 の Microsoft Store で、Ubuntuを検索し、Ubuntuをインストールします。
今回、[Ubuntu 20.04 LTS (GNU/Linux 4.4.0-18362-Microsoft x86_64)](https://www.microsoft.com/store/apps/9n6svws3rx71)をインストールしました。

**参照：**https://aka.ms/wslinstall

## Ubuntuの初期化
インストール後、Ubuntuを起動し、Linuxユーザーアカウントの設定を行います。
設定後、自動的にログインするので、ディストリビューションのパッケージの更新とアップグレードを行います。

```
sudo apt update && sudo apt upgrade
```

**参照：** https://docs.microsoft.com/ja-jp/windows/wsl/initialize-distro

# 必要なツールのインストール

```
sudo apt update
sudo apt install make wget gcc

wget https://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
pip install virtualenv
```

# ディレクトリ構成
Windowsからソースコードにアクセスしやすいようにする為、Cドライブの直下に、`esp32`ディレクトリを作成し、最終的に次のようなディレクトリ構成とします。

```
/mnt/c/esp32
 + /micropython
 + /esp-idf
```

# セットアップとインストール

## ディレクトリの作成

Cドライブの直下に`esp32`ディレクトリを作成します。

```
cd /mnt/c
mkdir esp32
```

## MicroPythonの取得（gitクローンとブランチの切り替え）

`/mnt/c/esp32/micropython`ディレクトリにMicroPythonのソースコードを取得します。

```
cd /mnt/c/esp32
git clone https://github.com/jp-96/micropython-esp32nimble.git micropython
```

`bleperipheral`というブランチに切り替えます(`checkout`)。

```
cd /mnt/c/esp32/micropython
git branch -a
git checkout origin/bleperipheral
git checkout bleperipheral
git branch -a
```


## ESP-IDFの取得（gitクローンとリビジョン）

`/mnt/c/esp32/esp-idf`ディレクトリにESP-IDFのソースコードを取得（gitクローン）します。
ただし、MicroPythonは特定のESP-IDFのリビジョンに依存しますので、そのリビジョンを取得します(`checkout`)。

`make ESPIDF=`を実行すると依存するリビジョンが表示されますので、そのリビジョンを`git checkout`のパラメーターに与えます。
※ 2020/06/18 現在、`Supported git hash (v4.0) (experimental): 4c81978a3e2220674a432a588292a4c860eef27b`と表示されました。

```
cd /mnt/c/esp32/micropython/ports/esp32
make ESPIDF=

cd /mnt/c/esp32
git clone https://github.com/espressif/esp-idf.git esp-idf

cd /mnt/c/esp32/esp-idf
git checkout 4c81978a3e2220674a432a588292a4c860eef27b
git submodule update --init --recursive
```

## esp-idfツールのインストール

`esp-idf`に付属する`install.sh`を実行し、クロスコンパイラ(`Toolchain`)等のツールをインストールします。
ただし、今回の`Ubuntu 20.04 LTS`には、`python3`しかインストールされていないので、`install.sh`を実行するとエラーが発生して実行に失敗します（`/usr/bin/env: ‘python’: No such file or directory`）。
そこで、まずは、`python`という`python3`へのシンポリックリンクを作成します。

```
sudo ln -s /usr/bin/python3 /usr/bin/python
```

その後、インストールを実行します。

```
cd /mnt/c/esp32/esp-idf
./install.sh
```

クロスコンパイラ(Toolchain) とPython仮想環境が `~/.espressif`ディレクトリに展開されます。

# ビルド

## Python仮想環境の有効化

次のコマンドで、インストールされたPython仮想環境を有効化します。

```
source ~/.espressif/python_env/idf4.0_py3.8_env/bin/activate
```

## ビルド環境の設定

次のコマンドで、ビルド環境を設定します。

```
. /mnt/c/esp32/esp-idf/export.sh
```

## 組み込みスクリプトのプリコンパイル
MicroPythonの組み込みスクリプトの一部をバイトコードにプリコンパイルします。

```
cd /mnt/c/esp32/micropython/mpy-cross
make mpy-cross
```

## berkeley-db-1.xxモジュールのアップデート
`berkeley-db-1.xx`モジュールをアックデートします。

```
cd /mnt/c/esp32/micropython
git submodule init lib/berkeley-db-1.xx
git submodule update
```

## ubleperipheralモジュールのアップデート
`ubleperipheral`モジュールをアックデートします。

```
cd /mnt/c/esp32/micropython
git submodule init extmod/ubleperipheral
git submodule update
```

## ESP32用のMicroPythonのビルド

ESP32用のMicroPythonのファームウェアをビルドします。
ファームウェアのイメージファイルは、`build`ディレクトリに作成されます。
（`bootloader.bin`, `partitions.bin`, `application.bin` の3つ）

```
cd /mnt/c/esp32/micropython/ports/esp32
make submodules
make PYTHON2=python FROZEN_MANIFEST=boards/manifest_bleperipheral.py
```

※ python2が見つからないというエラーが出てしまうので、`make`に`PYTHON2=python`パラメーターを渡しています。

# ファームウェアの書き込み

## USBシリアルポートの設定
ESP32とは、USBシリアルポートで通信しますが、例えば、 /dev/ttyS1 (COM1)にアクセスしようとするとエラーが発生することがあります。
次のように、ユーザーを dialout グループに追加しておく必要があります。

```
sudo usermod -a -G dialout $USER
```

## ESP32の接続
ESP32 をUSBケーブルでパソコンに接続し、デバイスマネージャで、ポートを確認します。
例えば、Windows10で、`COM14` として認識されている場合、Ubuntuでは、`/dev/ttyS14` として認識されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/0ae641aa-39d6-e8f2-e701-3896cf4137f9.png)

## 消去と書込

ファームウェアをESP32マイコンに書き込む前に、`make erase`でフラッシュメモリの消去を行います。その後、`make deploy`で書き込みます。
デフォルトでは、通信速度（`BAUD`）が`460800`に、通信ポート（`PORT`）が`/dev/ttyUSB0`に、それぞれ設定されていますので、パラメーターで明示的に指定しています。`PORT`は、デバイスマネージャーで確認したポートに合わせてください。

```消去.
cd /mnt/c/esp32/micropython/ports/esp32
make erase BAUD=115200 PORT=/dev/ttyS14
```

```書込.
cd /mnt/c/esp32/micropython/ports/esp32
make deploy BAUD=115200 PORT=/dev/ttyS14
```

# サンプルプログラムの動作確認

## プログラム書き込みツールのインストール

プログラム書き込みツールとして、`ampy`をインストールします。

```
source ~/.espressif/python_env/idf4.0_py3.8_env/bin/activate
pip install adafruit-ampy
```

## サンプルプログラムの転送

`/mnt/c/esp32/micropython/extmod/ubleperipheral/examples`ディレクトリにBLEのサンプルプログラムがありますので、全てのファイルをESP32へ転送します(`put`)。

```
cd /mnt/c/esp32/micropython/extmod/ubleperipheral/examples

ampy --port=/dev/ttyS14 put ble_temperature.py
ampy --port=/dev/ttyS14 put ble_uart_peripheral.py
ampy --port=/dev/ttyS14 put ble_uart_repl.py
ampy --port=/dev/ttyS14 put main.py
```
## サンプルプログラムの動作確認

ESP32を再起動するとサンプルプログラムが`import`されますので、ESP32とシリアル通信して、`temp()`関数を実行します。BLEが接続可能状態となりますので、スマホアプリの`nRF Connect`等を使って温度計の動作確認できます。


# スニペットの説明

`ble_temperature.py`で、GATT仕様書と読み比べてみましょう。

## 環境センシングプロファイル

まずは、`ble_temperature.py`で使用している、環境センシングプロファイル（ESP-Environmental Sensing Profile）の概要図を確認します。
[参照: ESP, p.8, 2.2 Role/Service Relationships](https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=294796)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/87678181-3b0b-00dd-c6f4-b1fe2cf62201.png)

環境センシングは、３つのサービスからなっていて、１つ目の環境センシングサービスは、実装が必須で、残りの２つは、任意です。
`BLEPeripheral.build`の第１引数のタプルで、これらのサービスを与えます。サンプルプログラムでは、`_ENV_SENSE_SERVICE`として定義された環境センシングサービスのみのタプルを指定しています。

```ble_temperature.py
class BLETemperature:
    def __init__(self, connected, disconnected, multi_connections):
            :
            :
        ((self._handleTempChar,),) = self._bleperipheral.build(
            (_ENV_SENSE_SERVICE, ),
            adv_services=[_ENV_SENSE_UUID],
            adv_name="upy-temp",
            adv_appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER
        )
            :
```

残りの引数は、アドバタイジングで示される情報です。

| 引数           | 内容                     | 
|----------------|-------------------------|
| adv_services   | BLEサービスのUUID配列     |
| adv_name       | 表示するローカル名        |
| adv_appearance | [アイコンなどで示される番号](https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.gap.appearance.xml) |

## 環境センシングサービス

環境センシングプロファイルで示された環境センシングサービスの詳細は、[環境センシングサービス仕様書](https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=294797)で確認できます。
しかし、仕様書には、実装に必要なUUIDの値は記載されていませんので、[GATTサービスのページ](https://www.bluetooth.com/ja-jp/specifications/gatt/services/)を参照します。ここで、環境センシングのUUID(割当番号)が、`0x181A`であることがわかります。

| 名称                   | Uniform Type Identifier                    | 割当番号 | 仕様コード |
|-----------------------|---------------------------------------------|--------|-----|
| [Environmental Sensing](https://www.bluetooth.com/xml-viewer/?src=https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.environmental_sensing.xml) | org.bluetooth.service.environmental_sensing | 0x181A | GSS |

```ble_temperature.py
# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
```

## 温度特性

さらに、[環境センシング (Environmental Sensing)](https://www.bluetooth.com/xml-viewer/?src=https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.environmental_sensing.xml)のリンク先を開くと、詳細な仕様を確認できます。
環境センシングには、たくさんの特性(characteristic)が定義されていますが、その中の`@type:org.bluetooth.characteristic.temperature`が、温度特性です。

ここで、プロパティを確認しておきます。`ble_temperature.py`では、`read`と`notify`を実装しています。
※ 'read'や'write'というのは、Collectorから見た操作を示しています。

| プロパティ | 要否 | 適用 |
|------|-----------|---|
| read | Mandatory | x |
| write | Excluded |  |
| writewithoutresponse | Excluded |  |
| signedwrite | Excluded |  |
| reliablewrite | Excluded |  |
| notify | Optional | x |
| indicate | Excluded |  |
| writableauxiliaries | Optional |  |
| broadcast | Excluded |  |
| extendedproperties | Optional |  |

この温度特性の詳細な情報は、[GATTの特徴のページ](https://www.bluetooth.com/ja-jp/specifications/gatt/descriptors/)を参照します。特性のUUID(割当番号)が`0x2A6e`であることがわかります。

| 名称        | Uniform Type Identifier                  | 割当番号 | 仕様コード |
|-------------|------------------------------------------|--------|-----|
| [Temperature](https://www.bluetooth.com/xml-viewer/?src=https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.temperature.xml) | org.bluetooth.characteristic.temperature | 0x2A6E | GSS |

これらの仕様をもとに、特性が定義されています。

```ble_temperature.py
_TEMP_CHAR = (
    bluetooth.UUID(0x2A6E),
    bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,
)
```

さらに、続けて、環境センシングサービスが、定義されています。

```ble_temperature.py
_ENV_SENSE_SERVICE = (
    _ENV_SENSE_UUID,
    (_TEMP_CHAR,),
)
```

# 特性のハンドル

プロファイル仕様書とサービス仕様書で、環境センシングサービスをタプルの階層構造で定義しましたが、センサー（peripheral）から、各特性に対して操作をする際に、ハンドルを指定して操作します。
その特性のハンドルは、`BLEPeripheral.build()`の戻り値で取得します。これも、サービスで定義したタプルの階層構造（順序）でハンドルが戻りますので、そのハンドルを保持しています(`self._handleTempChar`)。

```ble_temperature.py
class BLETemperature:
    def __init__(self, connected, disconnected, multi_connections):
            :
            :
        ((self._handleTempChar,),) = self._bleperipheral.build(
            (_ENV_SENSE_SERVICE, ),
            adv_services=[_ENV_SENSE_UUID],
            adv_name="upy-temp",
            adv_appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER
        )
            :
```


# 温度（値）の送信

```ble_temperature.py
class BLETemperature:
        :
        :
    def set_temperature(self, temp_deg_c, notify=False):
        # Data is sint16 in degrees Celsius with a resolution of 0.01 degrees Celsius.
        # Write the local value, ready for a central to read.
        self._bleperipheral.write(self._handleTempChar, struct.pack("<h", int(temp_deg_c * 100)), notify)
```

[Temperature](https://www.bluetooth.com/xml-viewer/?src=https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.temperature.xml)のリンク先を開くと、温度特性の詳細な仕様を確認できます。


| 項目 | 説明 |
|-----------------|----------------------------------------------------------------------|
| informativetext | Unit is in degrees Celsius with a resolution of 0.01 degrees Celsius |
| requirement     | Mandatory |
| format | sint16 |
| unit | org.bluetooth.unit.thermodynamic_temperature.degree_celsius |
| decimalexponent | -2 |

温度特性ハンドルに対して、16ビット符号付き整数に変換して送信しています(1は、摂氏0.01度)。


# 最後に

ESP32用のMicroPythonで、サンプルプログラムのBLE温度計の動作確認を行いました。
GATT仕様書が読めるようになれば、サービスと特性をタプル階層構造で定義するだけで、BLE peripheralを簡単に実装できます。