水温を定期的に取得するシステムの構築

## はじめに
センサーで定期的に水温を取得・サーバーに保存するシステムを構築しました。今回の開発の目的は、①実家で飼育しているカメの水槽環境をリモートでモニタリングできるようする②開発に関する技術や知識を習得することです。
目的②により使用技術を先に決め打ちしたことと、筆者がシステム開発初心者なので、一部回りくどい実装をしてしまっているかもしれません。あしからず了承ください。

## 設計
システムの全体図はこちらです。

![20240812_component_diagram_phase1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932185/2f1fed6c-79a4-0908-ec1a-25d08739b107.jpeg)

今回は**IoTセンサーで取得した水温をサーバーを経由してデータベースに保存する**までの流れ（赤線の部分）の実装およびテストをおこないました。

## 測定値の送信について
センサー側で取得した水温をサーバー側に送る方法として、
①HTTPを使用し、センサー側が水温をサーバー側にPOSTする
②MQTTを使用し、センサー側が水温をサーバー側にPublishする
が考えられます。MQTTの方が一回あたりの通信量が少ない（=消費電力が少ない）ことから、今回は**①を採用しました**。ただし、バックエンドサーバーをRESTfulにしたかったため、測定値を一旦別のサーバーで受け取り、バックエンドサーバーにHTTPでPOSTする形にしました。

### MQTTとは
通信プロトコルの一種です。HTTPよりも軽量であり、デバイスのメモリや通信帯域が制限されている状況に向いています。
MQTTはPub/Subと呼ばれる通信方式を採用しています。具体的には、ブローカーと呼ばれる処理サーバーを介してメッセージをやり取りします。送信側は"トピック"を指定し、メッセージをブローカーに送信します（Publish）。受信側も同様に"トピック"を指定し、ブローカーをメッセージから受信します（Subscribe）。このようにして、同じトピックを指定した送信側と受信側が非同期にメッセージをやり取りすることが可能となります。

## 各コンポーネントの説明
### IoTセンサー
水温を定期的に測定し、ブローカーにPublishします。
水温センサーとして、以下のリンク先のものを用意しました。
https://wiki.seeedstudio.com/One-Wire-Temperature-Sensor-DS18B20/
また、センサーの制御や測定結果を送信するためのセンサーコントローラーとして、M5Stack AtomS3を使用しました。
https://docs.m5stack.com/ja/core/AtomS3
実家の水槽は2つあるため、それぞれ2個ずつ購入しています。

センサーコントローラーには、5秒ごとに水温を取得しブローカーサーバーの1883ポートにPublishする処理を書き込みました。

#### ソースコード
```
#include <WiFi.h>
#include <PubSubClient.h>
#include <M5Unified.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <RTClib.h>


// --設定値--

const char* ssid = "xxxx";     // Wi-FiのSSID
const char* password = "xxxx";        // Wi-Fiのパスワード

const char* mqtt_server = "xxx.xxx.xxx.xxx"; // MQTTブローカーのIPアドレス
const int mqtt_port = 1883;               // MQTTブローカーのデフォルトポート

const int sensor_id = 1;                    //センサーID

const unsigned long sleepTime = 5000;     // データ取得間隔[ms]

// -------


#define MQTT_CLIENT_NAME  "abms_sensor"

#define ONE_WIRE_BUS 1 // for M5ATOMS3 port number G1 and Pull_up
#define TEMPERATURE_PRECISION 12 // sensor resolution: 9 - 12bit

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
DeviceAddress tempDeviceAddress;

WiFiClient espClient;
PubSubClient client(espClient);

RTC_DS3231 rtc;

unsigned long sleepStartTime = 0;


void setup() {
  Serial.begin(38400);
  
  sensors.getAddress(tempDeviceAddress, 0); // get device0 address
  sensors.setResolution(tempDeviceAddress, TEMPERATURE_PRECISION); // set resolution

  sensors.begin();
  pinMode(ONE_WIRE_BUS, INPUT_PULLUP); // Wired-OR input mode Pull-up (to Vcc), otherwise input level is always the minimum value (e.g. -127)

  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  sleepStartTime = millis();

  rtc.begin();
  if(!rtc.lostPower()){
    rtc.adjust(DateTime(F(__DATE__),F(__TIME__)));
  }
}


void setup_wifi() {
  delay(10);
  // WiFi接続を開始
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}


void loop(void) {
  client.loop();

  while(!client.connected() ){
    Serial.println("Mqtt Reconnecting");
    if( client.connect(MQTT_CLIENT_NAME) ){
      Serial.println("Mqtt Connected");
      break;
    } else {
      Serial.println("Fail");
    }
    delay(1000);
  }

  // String sensor_id_str = String(sensor_id);

  if(millis() - sleepStartTime >= sleepTime){

    // 水温の取得
    sensors.requestTemperatures();

    const float water_temperatures = sensors.getTempCByIndex(0);

    // メッセージの作成
    String message_water_temperatures = "";
    message_water_temperatures += sensor_id;
    message_water_temperatures += "/";
    message_water_temperatures += water_temperatures;

    // brokerへ送信
    const char* message_water_temperatures_pointer = message_water_temperatures.c_str();
    client.publish("water_temperatures", message_water_temperatures_pointer);

    // ログを表示
    Serial.print(millis());
    Serial.print(water_temperatures);
    Serial.println(F("°C"));

    sleepStartTime = millis();

  }
}
```


### ブローカー
センサーから受け取った測定値をセンサーサブスクライバー（下記参照）に流す役割を担います。
以下のDockerイメージからコンテナを立ち上げ、コンテナ内にconfigファイル（mosquitto.conf）を配置しました。

#### 使用したDockerイメージ
https://hub.docker.com/_/eclipse-mosquitto

#### mosquitto.confの中身
```
# 使用するポートを指定
listener 1883

# 認証なしでPub/Subできるようにする。将来的には認証されたセンサーからのみPublishできるように変更。
allow_anonymous true
```

コンテナ内で以下のコマンドを実行し、mosquittoサーバーを立ち上げました。
```
$ mosquitto -c mosquitto.conf
```

### センサーサブスクライバー
ブローカーから受け取った測定値をバックエンドサーバーにHTTP/POSTするコンポーネントです。一連の処理はpythonで書きました。特に、MQTTの処理はpaho-mqttというライブラリを使用しました。

#### ソースコード
```
import paho.mqtt.client as mqtt
import requests
from logging import getLogger, basicConfig, DEBUG

broker_ip = "xxx.xxx.xxx.xxx"
broker_port = 1883
backend_ip = "backend"
backend_port = 8000
measured_value_list = ["water_temperatures"]
sensor_id = 1

basicConfig(level=DEBUG)
logger = getLogger(__name__)
logger.setLevel(DEBUG)

def on_connect(client, userdata, flags, respons_code):
    print("status {0}".format(respons_code))
    for measured_value in measured_value_list:
        client.subscribe(measured_value)
        logger.info(f"Start subscribing -- topic:{measured_value}")


def on_message(client, userdata, msg):
    url = f"http://{backend_ip}:{backend_port}/api/{msg.topic}/"    # POST先のURLを指定
    tank_id, measured_value = str(msg.payload)[2:-1].split('/')     # msg.payload = b"1/20" => tank_id = 1, measured_value = 20
    logger.debug(f"Received message -- topic: {msg.topic}, tank id:{tank_id}, value: {measured_value}")
    data = {"sensor_id": sensor_id, "water_temperature": measured_value}
    response = requests.post(url, data=data)


if __name__ == "__main__":
    client = mqtt.Client(protocol=mqtt.MQTTv311)
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect(broker_ip, port=broker_port, keepalive=60)
    client.loop_forever()
```

### バックエンド
センサーサブスクライバーからPOSTされた測定値をデータベースに保存します。今回はDjango REST Frameworkを使用し、処理を実装しました。ソースコードは割愛しますが、近々Github等で公開する予定です。


### データベース
測定値を蓄積するコンポーネントです。暫定措置として、バックエンドサーバーにインメモリデータベースを立てる形で対応します。将来的にはバックエンドとは別に用意するつもりです。

## 水温の取得実験

### 取得の様子
![IMG_4052.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932185/f9235e5f-68ee-172e-d293-e07db01468cc.jpeg)

### 測定結果
![20241214_backend_screenshot.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932185/cf547702-72f3-bf1b-4038-889eb415bdcf.jpeg)

水温を定期的に取得できていることが確認できました。センサーとPCをケーブルで繋いでいますが、これはセンサーに給電するためです。通信は有線ではなくWi-Fiでおこなっています。


# 今後やりたいこと
- フロントエンド部分の開発
- システムをクラウドに移行
- 水温が大きく変化した場合にアラートする機能の開発

# おわりに
開発当初は他にも機能を追加する予定でしたが、色々と詰め込みすぎて途中で力尽きてしまいました。今後はスコープを狭めに取るようにし、ステップバイステップで開発を進めていこうと思います。まだまだ完成には程遠いですが、追々進めていこうと思います。![20240812_component_diagram_phase1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932185/a6d5ec2c-b7f0-72b7-3539-945f6bac9a8a.jpeg)