M5Stackで始めるモノ作り・土壌水分モニターデバイス実装編

# 今回の概要
[前回](https://qiita.com/Reiji-Nishiyama/items/c4b991c7a26405dde0fa)はM5Stack開発環境の構築を行いました。今回は[設計編](https://qiita.com/Reiji-Nishiyama/items/d06621c15402cd9cb0f8)でプロットした土壌水分モニターデバイスを実装して行きたいと思います。

**実装した土壌水分モニターデバイス**
葉の数で水分量レベルを表します。

![IMG_4544.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/eb95968a-b2bf-e415-18fc-b5ce4afa922f.jpeg)![IMG_4546.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/18296a70-8c30-c2b0-3877-6585e55838e6.jpeg)


# 土壌水分モニターデバイス実装
軽く要件をおさらいしておきます。

**要件**
* 土壌水分量を測定し、表示する
１０段階で数値化します。バッテリー駆動であることを考慮し、間欠動作にします。間隔は調整可能とします。
* センサーをキャリブレーションする 
センサーの個体差を吸収し、測定値が同じ基準になるようにします
* Alexaと連携する（後に変更を迫られます）

**ハードウェア構成**
* M5StickC plus
 バッテリー及びLCD搭載しており、単体だけで水分量の確認が可能
* 静電容量方式の土壌水分センサー
 腐食耐性と精度を考慮

では実装して行きたいと思います。

## 土壌水分センサーの読み取り機能
使用したセンサーは3ピン端子（VCC/GND/アナログ出力）で、3.3〜5.0Vの電源を供給すると、最大3.3Vのアナログ出力を行う仕様になっています。
ちょうどM5Stickc plus下部にはVout、GND、アナログ入力可能なGPIO32ピンコネクタを備えているため、ここを利用します。
ちなみにCapacitive Soil Moisture Sensor v1.2と呼ばれるセンサーを購入しました。


#### ESP32のADCの注意点
ESP32のアナログ入力電圧は最大1Vまでになっていますが、センサーは3.3V出力のためそのままでは接続できません。しかし幸いにもESP32は入力信号を減衰する機能を備えており、11dBの減衰を設定することで最大3.6Vまでのアナログ入力を可能とします。
※なお、ADCの仕様はESP32の種類によって異なる可能性があり、ご使用のチップの仕様を参照する必要があります。

#### GPIOポートのアナログ入力設定
GPIO32をアナログ入力に設定するコードは以下になります。ATTN_11DBが11dbの減衰を設定している箇所になります。分解能については、それほど細かい粒度を必要とするものではないため9bitとしています。値の読み取りは`self.adc0.read()`で行います。

```python
self.adc0 = machine.ADC(32)
self.adc0.width(machine.ADC.WIDTH_9BIT)
self.adc0.atten(machine.ADC.ATTN_11DB)
```

#### A/D変換の値がなかなか下がらない！
設定が終われば`self.adc0.read()`でセンサーのアナログ信号をA/D変換したデジタル値を読み取ればいいだけです。
コードを書いて無限ループでセンサー値を出力するようにして、センサーを空気中と水につけた状態を交互に繰り返したときにセンサー値がどのように変化するのか観察してみました。すると、水から出した時は数値が急上昇して行くのですが、空気中から水につけた場合はなかなか数値が下がりません。下がり切るまで、15秒程掛かります。今回の土壌水分監視位であれば、これでも特に支障はないのですが、変化が早い物理量をモニタする場合には支障があります。ちょっと調べてみることにしました。

設定の問題なのか切り分けるため、0dBに変更して同じように観察してみました。ただし、入力は最大1Vまでとなるため、センサーと本体の間で信号出力を分圧して入力させます。すると、センサーを空気中と水につけた状態を交互に繰り返したときそれに素早く追従して変化するようになりました。どうやら減衰させる仕組みに原因があるようです。
A/D変換数値が下がらないということは入力電圧が下がらないということだと考え、設定を11dBに戻してセンサー出力電圧の変化を簡易的なオシロでモニタしてみることにしました。すると今度は、素早く追従するようになりました。どうもプローブを当てている時だけ追従するようです。

ググってプローブの基本的な構造を調べてみると大きな抵抗とキャパシタが入っているようです。プローブを当てることで信号線とGNDの間に並列に抵抗が入ることになりますが、抵抗が非常に大きいため普通は対象に影響は与えないはずなのですが。私のポンコツなアナログ回路の知識では到底理解が及ばないので、とりあえず同じように並列に大きな抵抗を入れてみることにします。手持ちの大きな抵抗は100kΩしか無かったのでこれを２つ直列につないで200kΩにします。すると同じように素早く追従するようになりました。テキトーな想像なのですが、ADC側にキャパシタ成分があってそこに電荷を溜めて量子化するが、信号電圧が下がっても溜まった電荷がなかなか抜けず高い変換数値となっていた。そこに並列に抵抗を入れたことで抵抗を通じて電荷が抜けていった、そんな感じでしょうか。

取り敢えず、問題は解消されたのでよしとします。

#### 10段階で水分量の状態を表す
100%乾燥状態（センサーが空気中にある）のセンサー値と、水分100%な状態（センサーが水中にある）のセンサー値は分かるものの、センサー値と乾燥状態を定量的に調べにはどうしたらいいでしょうか。一定量の土と水を用意して均等に混ぜた状態でセンサー値を読み取り、水の量を徐々に増やしながそれぞれの値をプロットしていけば出来そうですが非常に手間です。ということで湿らせたタオルにセンサーの差し込み量を変えながらプロットし、ここからここまでの範囲は、水分量レベル1などと割り振ることにしました。その結果センサー値と水分量レベルの対応関係は３次曲線でマップすることにしました。エクセルで各次数の係数を算出し、3次の近似式をPythonで実装します。

![センサー値と水分量のレベル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/3962d97b-dd75-8c8f-d0d3-28f3f813fb5d.png)


#### キャリブレーションの実装
別のセンサー個体を使用した場合、特性が変わるため先ほどと同じやり方でセンサー値と乾燥状態の近似式を求める必要がありますがいちいち面倒ですし、何より前述の手順は再現性がありません。ということで、100%乾燥状態のセンサー値と、水分100%な状態のセンサー値を測定するだけでキャリブレーションを行えるようにします。
キャリブレーションは、近似式の算出に用いた最初のセンサーを基準としてその最大最小値レンジに別の個体の最大最小レンジを線型写像することで行います。
まとめると、個体差を考慮したセンサー値の水分量レベルへの変換は、まず別の個体のセンサー値を線型写像にて基準個体の値に変換し、近似曲線で水分量レベルを取得します。

```python
# 基準センサーのレンジ：SENSOR_VALUE_WATER、SENSOR_VALUE_AIR
# 個々のセンサーのレンジ：__in_water、__in_air
mapped_value = map_value(value, self.__in_water, self.__in_air, self.SENSOR_VALUE_WATER, self.SENSOR_VALUE_AIR)
moisture_level = int(self.A3 * (mapped_value**3) + self.A2 *
                   (mapped_value**2) + self.A1 * mapped_value + self.B)
```

キャリブレーションに必要な個々のセンサーのレンジは不揮発メモリに保存しておき、起動時に設定を読み込むようにします。

これで水分量の測定に関する部分の実装は以上になります。次はUIの実装を行います。

## UIの実装

#### 水分量レベルの表示
水分量をただ数字て表したのでは面白くないため、レベルに応じた絵で表現したいと思います。植物なので葉っぱの数で水分量レベルを表現することにしました。画像についてはM5Stack提供のlcdモジュールのsprite機能が使用できず重ね合わせができなかったので、レベル毎に絵を用意して切り替えながら表示を行います。重ね合わせが使えれば葉だけを描画すれば済むので画像サイズを小さくできたのですが、jpeg圧縮率を調整して上限の25kbに収めました。

![L9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/5e440855-a215-a860-9e90-077ec822f4e2.jpeg)![L8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/ec873866-2ac1-1288-5e4a-37ff8781b504.jpeg)![L5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/6b5c88e7-cdde-d329-2f71-2a89739d0ef2.jpeg)![L0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/38bfca54-a306-402a-e813-b7faaab0709e.jpeg)

#### キャリブレーションUI
PCからシリアル接続しなくてもいいように、Webサーバーを立ててスマホからキャリブレーションの設定が出来るようにします。
幸いM5StackのFWにはWebサーバーのPythonモジュールが搭載されているためこれを利用します。実装はURL毎に処理するハンドラを実装してサーバーに登録するだけの、非常に扱いやすい作りになっています。

```python
# 一部抜粋

from wifiCfg import wlan_sta
from wifiWebCfg import MicroWebSrv
import network

def _httpHandlerRESTApi(httpClient, httpResponse):
    request = httpClient.GetRequestPath()
    print(request)
    if request == '/get/config':
        content = configReaderWriter.jsonstr
    elif request == '/get/ssids':
        content = json.dumps(scanlist)
    elif request == '/get/soilmoisture':
        content = str(moistureSensor.readRawData())

    httpResponse.WriteResponseOk(
        headers=None,
        contentType="application/json",
        contentCharset="UTF-8",
        content=content
    )


routeHandlers = [
    ("/",	        "GET",	_httpHandlerHome),
    ("/index.html", "GET", _httpHandlerHome),
    ("/pure-min.css", "GET", _httpHandlerResource),
    ("/grids-responsive-min.css", "GET", _httpHandlerResource),
    ("/style.min.css", "GET", _httpHandlerResource),
    ("/get/config", "GET", _httpHandlerRESTApi),
    ("/get/ssids", "GET", _httpHandlerRESTApi),
    ("/set/wifi", "POST", _httpHandlerConfig),
    ("/set/in-air", "POST", _httpHandlerConfig),
    ("/set/in-water", "POST", _httpHandlerConfig),
    ("/get/soilmoisture", "GET", _httpHandlerRESTApi)
]

```


テンプレートエンジンが使えれば良かったのですが軽量で良さそうなものが見当たりませんでした。コンテンツを差し込む簡単なものなら文字列置換で済むのでそれでもよかったのですが、Javascriptでクライアント側で、REST APIでデバイスから必要な情報を取得して、DOM操作してHTMLを生成するようにします。少しでも見栄えがするようにCSSフレームワークは軽量なPure CSSを使用しました。この画面を通じて、100%乾燥状態のセンサー値と、水分100%な状態のセンサー値を計測し、不揮発メモリに設定値として保存するようにします。
![UI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481340/77b0807f-fc7e-147d-e605-242161a778a1.png)

# 次回
次回は、Dashboard連携とAlexa連携を進めて行きたいと思います。しかし、ここまで作ってきた一部のものが遂に水泡に帰する時がやってきます。

## 参考情報
Pythonからesp32を制御する際に必要となるAPI仕様については以下を参考にどうぞ。
* micropython
micropythonを搭載したターゲットで利用可能なAPI仕様になります。
https://micropython-docs-ja.readthedocs.io/ja/latest/index.html
* M5Stack提供のPython API仕様
M5Stack固有のHWを操作する場合のAPI仕様については、こちらのAPI情報を参照する必要があります。
https://github.com/m5stack/UIFlow-Code/wiki

今回実装したソースは、githubへの掲載しようかとも思ったのですが、ここまで作ったソースが無意味になったのでどうしようか思案中です。