RaspberryPiでのTDSメーターの使い方と温度補正の方法

# はじめに
水槽の水質管理にTDSメーターを使ってみたいと思ったのですが、
調べてみるとRaspberryPiでのTDSメーターの使い方や
温度によるTDS値への影響に関する検証記事があまりありませんでした。

備忘録もかねてこの記事でRaspberryPiでTDSメーターを使ってTDS値を測定する方法について紹介します。

TDSメーターは水の導電率を利用して測定しますが、
温度の影響を受けるため正確な値を測定するためには温度補正が必要と言われています。

今回はTDSメーターと温度センサーから取得した値をもとに、
補正を行ったTDS値と補正を行わないTDS値を計算し、比較を行いました。

温度の異なる精製水、水道水、紅茶のTDS値を測定することで、
TDS値の測定には温度補正が必要であることを確認しました。

### 開発環境
RaspberryPi：Raspberry Pi 4 Model B
OS：Raspbian GNU/Linux11
Python：3.9.2

# TDSメーター
### TDSとは
TDSとは"水の中にどれくらいの物質が溶け込んでいるかを表す数値"です。[[1]](https://mizu-cool.jp/tds/)

よく用いられる単位はppmです。
1ppmは1Lの水に1mgの物質が溶けているということを示しています。

飲料水の硬度やコーヒーの濃度、熱帯魚の水槽の水質の管理に用いられます。

### 測定原理
TDSメーターは"無機イオンが多量に溶け込んだ水溶液は電気を通しやすく、
不純物のない純水は電気を通さない"という仕組みを利用して数値を測定します。[[1]](https://mizu-cool.jp/tds/)

<img width="300" alt="測定原理" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/8d789388-afd2-5ce2-0958-3f941f496a9c.png">

測定方法は2個の電極を水溶液に浸け、測定した電気抵抗から算出します。

また水溶液の電気の通しやすさは温度の影響も受けるため、
正確な値を測定するためには温度による補正が必要です。[[2]](https://www.jaima.or.jp/jp/analytical/basic/electrochem/ec/)

電気伝導率の補正式
κ25 ＝κｔ／［1 ＋α（ｔ－ 25）］
κ25：25 ℃の電気伝導率
κｔ：ｔ ℃の電気伝導率
α：温度係数


# 電子回路
### 回路
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/169ae6f1-181f-9c35-5a89-a6b22a297d82.png)

<img width="600" alt="回路図写真" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/7d0aaf6c-d992-112d-5653-2767a1b26ba0.jpeg">

### TDSメーター

<img width="300" alt="TDSメーター" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/75918c82-8e5d-9b0b-514a-4b018f8e335b.jpeg">


今回使用したTDSメーターはKEYESTUDIOのKS0429です。[[3]](https://wiki.keyestudio.com/KS0429_keyestudio_TDS_Meter_V1.0)

TDSメーターの+の線(赤線)を参照電圧につなぎ、-の線(黒線)をGNDにつなぐことで
Aの線(黄線)から現在のTDSに応じた電圧が返ってきます。
この電圧値をもとにTDSを算出します。


### MPC3008
RaspberryPiは直接アナログ入力(ポートの電圧値の取得)ができません。

そのため、A/DコンバーターとしてMCP3008を使用しました。[[4]](https://sites.google.com/site/memorandumjavaandalgorithm/raspberry-pi-jiang-zuo12-adkonbata-mcp3208)

### 温度計

<img width="300" alt="温度計" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/70fc4b7e-4c89-bcfb-84e0-b4f7bb93f2ad.jpeg">

今回使用した温度計はDS18B20です。[[5]](https://www.amazon.co.jp/gp/product/B01DCY9G0K/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1)

DS18B20のデータ線はプルアップする必要があるため、4.7kΩのプルアップ抵抗をつないでいます。[[6]](https://101010.fun/iot/raspi-water-temp.html)

# ソフトウェア
### 前準備
DS18B20を使用するためには1-Wireを有効にする必要があります。
手順は[参考サイト](https://101010.fun/iot/raspi-water-temp.html#header-4-1)の通りにすればよいのですが、迷った箇所もあったので再度手順を記載します。

下記コマンドを実行してください。
```
$ sudo vi /boot/config.txt
```

その後、下記内容を追記してください。
```
$ sudo vi /boot/config.txt
```

追記場所はどこでもいいと思いますが、今回は下記の場所に追記しました。
(ここで迷いました笑)

<img width="300" alt="1-Wire追記" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/c5b43496-4314-3b2b-e33e-09f3838aee59.png">

その後、RaspberryPiを再起動します。

下記コマンドを実行してください。
```
$ lsmod | grep w1
```

次のような表示が出れば1-Wireは有効になっています。

<img width="300" alt="1-Wire確認" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/e277e8e4-4440-8e74-8c11-968b0facd253.png">

最後に下記コマンドでw1thermsensorをインストールします。
```
$ pip install w1thermsensor==2.0.0a2
```

### 概要
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/058b8d12-d138-3359-7e5c-1db822b41fe1.png)


ざっくりとやることを記載します。
①MPC3008からTDSメーターの電圧を、DS18B20から温度を取得
②取得した値をもとに温度による補正を行ったTDSと補正を行わないTDSを算出
③10回の測定値の平均値を取得

今回はオブジェクト指向の練習をかねてクラスを作っています。

クラスの概要
main：メイン
MCP3008：mcp3008から電圧値を取得
TdsMeter：tds値を算出
ThermoMeter：温度を取得
DataStore:データをためる

### コード
```main.py
import tdsmeter
import mcp3008
import thermometer
import datastore
import time

DATA_NUM = 10           #取得するデータ数
WAIT_TIME = 10          #測定間隔
NOT_CORRECT = 25.0      #補正しないtds値を算出

#インスタンスの生成
tds = tdsmeter.TdsMeter()
tds_adc = mcp3008.MCP3008(channel = 0, clockpin = 11, mosipin = 10, misopin = 9, cspin = 8, reference_voltage = 5)
thermo = thermometer.ThermoMeter()
datastore = datastore.DataStore()

#データ測定
for i in range(DATA_NUM):  
    tds_voltage = round(tds_adc.now_voltage(), 2)
    temperature = round(thermo.now_temperature(), 2)
    raw_tds       = round(tds.calculate_tds(tds_voltage, NOT_CORRECT), 2)
    corrected_tds = round(tds.calculate_tds(tds_voltage, temperature), 2)
    
    datastore.stock_data(raw_tds, corrected_tds, temperature, tds_voltage)
    
    time.sleep(WAIT_TIME)

#結果の取得
result_data = datastore.calculate_result_data(DATA_NUM)
print(result_data)
```

```themometer.py
from w1thermsensor import W1ThermSensor

class ThermoMeter:
    def __init__(self):
        self.sensor = W1ThermSensor()
        
    def now_temperature(self):
        temperature = self.sensor.get_temperature()
        return temperature
```
[参考サイト：ラズパイとDS18B20で水温測定](https://101010.fun/iot/raspi-water-temp.html#header-4-1)

```mcp3008.py
import RPi.GPIO as GPIO

class MCP3008:
    def __init__(self, channel, clockpin, mosipin, misopin, cspin, reference_voltage):
        self.__channel           = channel
        self.__clockpin          = clockpin
        self.__mosipin           = mosipin
        self.__misopin           = misopin
        self.__cspin             = cspin
        self.__reference_voltage = reference_voltage
        
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.__clockpin, GPIO.OUT)
        GPIO.setup(self.__mosipin, GPIO.OUT)
        GPIO.setup(self.__misopin, GPIO.IN)
        GPIO.setup(self.__cspin, GPIO.OUT)
        
    #MCP3008から取得した電圧値を算出する
    def now_voltage(self):
        GPIO.output(self.__cspin, GPIO.HIGH)
        GPIO.output(self.__clockpin, GPIO.LOW)
        GPIO.output(self.__cspin, GPIO.LOW)
        
        commandout = self.__channel
        commandout |= 0x18
        commandout <<= 3
        
        for i in range(5):
            if commandout & 0x80:
                GPIO.output(self.__mosipin, GPIO.HIGH)
            else:
                GPIO.output(self.__mosipin, GPIO.LOW)
            commandout <<= 1
            GPIO.output(self.__clockpin, GPIO.HIGH)
            GPIO.output(self.__clockpin, GPIO.LOW)
        
        adcout = 0
        
        for i in range(11):
            GPIO.output(self.__clockpin, GPIO.HIGH)
            GPIO.output(self.__clockpin, GPIO.LOW)
            adcout <<= 1
            if i > 0 and GPIO.input(self.__misopin) ==  GPIO.HIGH:
                adcout |= 0x1
        GPIO.output(self.__cspin, GPIO.HIGH)
        
        voltage = self.__reference_voltage * adcout / 1023.0
        
        return voltage
```
MCP3008は10ビットなので、12ビットのMCP3208のコードを一部変更しています。[[7]](https://qiita.com/Allovertom/items/225c0cfab8ec79b40aa5)

```tdsmeter.py
class TdsMeter:
    def __init__(self):
        pass
    
    #TDS値を算出する
    def calculate_tds(self, voltage, temperature):
        compensationCoefficient = 1.0 + 0.02 * (temperature - 25.0)
        compensationVolatge = voltage / compensationCoefficient
        tds_value = (133.42 * compensationVolatge * compensationVolatge * compensationVolatge - 255.86 * compensationVolatge * compensationVolatge + 857.39 * compensationVolatge) * 0.5
        return tds_value
```
TDS値算出式は公式HPを参考にしました。[[3]](https://wiki.keyestudio.com/KS0429_keyestudio_TDS_Meter_V1.0)

```datastore.py
class DataStore:
    def __init__(self):
        self.__sum_raw_tds       = 0
        self.__sum_corrected_tds = 0
        self.__sum_temperature   = 0
        self.__sum_voltage       = 0
    
    #測定結果をためる
    def stock_data(self, raw_tds, corrected_tds, temperature, voltage):
        self.__sum_raw_tds       += raw_tds
        self.__sum_corrected_tds += corrected_tds
        self.__sum_temperature   += temperature
        self.__sum_voltage       += voltage
        
    #平均値を算出する
    def calculate_result_data(self, data_num):
        average_raw_tds       = round(self.__sum_raw_tds / data_num, 2)
        average_corrected_tds = round(self.__sum_corrected_tds / data_num, 2)
        average_temperature   = round(self.__sum_temperature / data_num, 2)
        average_voltage       = round(self.__sum_voltage / data_num, 2)
        
        result = dict(topic         = "result",
                      raw_tds       = average_raw_tds,
                      corrected_tds = average_corrected_tds,
                      temperature   = average_temperature,
                      voltage       = average_voltage)
        
        return result
```

# テスト
濃度がわかっているTDS計用校正液は高くて今回測定できませんでした。
そのため基本的に結果は参考値になります。

### テスト方法
精製水[[8]](https://www.kenei-pharm.com/general/products/%e7%b2%be%e8%a3%bd%e6%b0%b4/)、水道水、午後の紅茶(無糖)[[9]](https://products.kirin.co.jp/softdrink/softdrink/detail.html?id=6505#_ga=2.100526622.1262054966.1683026134-1519296383.1683026134)の3種類を測定しました。

また、温度による補正の影響を確認するために
常温(約20℃)、冷やしたもの(約10℃)、温めたもの(約60℃)の測定も行いました。

### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/e3da54d0-9b6d-df73-d16d-6c3918acfe8a.png)

### 考察
常温の水道水のTDS値が約75ppmでした。
一般的な水道水のTDS値が50～160ppmとのことだったので、妥当な値だと思います。[[1]](https://mizu-cool.jp/tds/)[[10]](http://www.authentec-at.jp/images/dl/data/quality.pdf)

精製水のTDS値が15ppmもあったことには驚きました。
調べてみると精製水といってもわずかながら不純物を含んでいるようです。
精製水をさらに生成した超純水というものがあるそうですが、
それでもわずかながらの不純物を含むそうです。[[11]](https://www.water-magazine.jp/article/3718/)

温度による補正を行うことで正確な値が測定できることがわかりました。
温度による補正を行っていないと温度が違うと同じ水溶液でもTDS値が大きく異なります。
しかし、補正を行うと温度が違っていてもある程度近いTDS値が算出されました。

補正後の値にばらつきがみられた理由として
電気伝導率の補正式の温度計数が最適でないことが考えられます。
温度係数はイオン成分や濃度によって異なるからです。[[2]](https://www.jaima.or.jp/jp/analytical/basic/electrochem/ec/)
今回は[参考サイト](https://wiki.keyestudio.com/KS0429_keyestudio_TDS_Meter_V1.0)をもとに0.2に設定しましたが、
測定する水溶液がわかっている場合は温度係数の最適化を行うとより正確な値が算出できると思います。
    κ25 ＝κｔ／［1 ＋α（ｔ－ 25）］
    α：温度係数

紅茶のようにもとのTDS値が大きいと温度による影響をより大きく受けています。
閾値による制御を行う際は温度による影響を考慮する必要があります。

これらのことから、TDS値がそれなりに大きく、水温の変化が大きい水溶液を測定する際は
温度による補正が必要であることがわかりました。

<img width="500" alt="測定ログ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/e0fdc702-7c51-186f-1d4c-afe2edbdf336.png">

結果として平均値のみを記載していますが、毎回の値も確認しました。
ほとんど平均値に近い値が算出されていましたが、
たまに平均値から大きくずれた値が算出されていました。
測定する際は数回値を算出してその平均値をとる方が間違いないと思います。

# 参考
[1]TDSとは? https://mizu-cool.jp/tds/
[2]電気伝導率計の原理と応用 https://www.jaima.or.jp/jp/analytical/basic/electrochem/ec/
[3]KSO0429 keyestudio TDS Meter V1.0 https://wiki.keyestudio.com/KS0429_keyestudio_TDS_Meter_V1.0
[4]Raspberry Pi講座 ADコンバータ(mcp3208) https://sites.google.com/site/memorandumjavaandalgorithm/raspberry-pi-jiang-zuo12-adkonbata-mcp3208
[5]DS18B20 https://www.amazon.co.jp/gp/product/B01DCY9G0K/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1
[6]ラズパイとDS18B20で水温測定 https://101010.fun/iot/raspi-water-temp.html
[7]ラズパイ3b＋でAD変換からのLED点灯＠最新 Raspberry Piで学ぶ電子工作(ブルーバックス) https://qiita.com/Allovertom/items/225c0cfab8ec79b40aa5
[8]精製水 https://www.kenei-pharm.com/general/products/%e7%b2%be%e8%a3%bd%e6%b0%b4/
[9]午後の紅茶(無糖) https://products.kirin.co.jp/softdrink/softdrink/detail.html?id=6505#_ga=2.100526622.1262054966.1683026134-1519296383.1683026134
[10]水道法に基づく水質基準 http://www.authentec-at.jp/images/dl/data/quality.pdf
[11]蒸留水と精製水の違い https://www.water-magazine.jp/article/3718/