Arduinoで回分式水処理装置の監視と制御

# はじめに
私の製作する水処理実験装置では、一定の時間ごとに何かの操作をするということがしばしばあります。例えば、最初の5分に下水の供給、その後55分攪拌、120分曝気、55分沈澱、5分で処理水の排水、といったようなパターンです。また、この間、水中の酸素濃度等をモニタリングします。こうした操作をArduinoを用いて実施するスケッチを考えました。

# 主な必要なもの
+ Arduino UNO
+ ４チャンネルリレーボード
+ 溶存酸素濃度センサーでアナログ出力を持つもの（A0に0-1Vで入力）
+ LCD(16x2程度、I2C通信)
+ データロガーシールド（RTCおよびSDカードスロットが付属。）、あるいはSDカードのみでもよい。
+ できればPC
+ ポンプ（流入、放流、空気）、攪拌機 （各Pin 5, 6, 7, 8）
以上、物理的な接続は済んでいるものとする。

# 条件
+ 最初の5分に下水の供給、その後55分攪拌、120分曝気、55分沈澱、5分で処理水の排水
+ 1秒ごとに酸素濃度の取得とLCDへの表示、1分ごとに運転状況（リレーのオンオフ）＋酸素濃度をSDカードに保存およびパソコンとシリアル通信。
+ 曝気中、溶存酸素濃度が1mg/L〜1.5mg/Lとなるようにする。

# 事前の準備
1. ライブラリマネージャで以下のライブラリをインストール
    + Time（by Michael Margolis)
    + DS1307RTC (by Michael Margolis)
    + LiquidCrystal I2C (by Frank de Brabander)
    + I2CScanner (by Luis Liamus)
2. LCDの設定と動作確認
    + I2CScannerを用いてLCDのI2Cアドレスを調べる。
        + ファイル > スケッチ例 > I2C Scanner > Scanner
        + データロガーシールドにRTCが存在する場合、そのI2Cアドレスも表示される。まずはLCDを接続せずにスキャンして、RTCのI2Cを確認しよう。
        + LCDを正しく接続すると、LCDのI2Cアドレスがリストされるようになる。
    + LiquidCrystal I2CのHello World等を動作させ、LCDが動作することを確認する。
3. SDカードの設定と動作確認
    + ファイル>スケッチ例 > SD > CardInfo　等を実行し、動作を確認する。
    + うまく動作しない場合は、SD.begin()で用いられている入力値を変更する（CardInfoの場合はchipSelectの値）。chipSelectの値はデフォルトでは4だが、データロガーシールドによっては10（あるいは8）に変更する必要があるものもある。
4. RTCの設定
    + 不要。しかし、存在していても良い。

# 注意点
+ 100Vの機器をリレーで制御するのは、電気工作についてある程度の知識がないと危険。
+ モーター等からのサージによる誤作動
    + リレーで100Vの駆動機器（モーターやポンプ）を動作させると、給電を停止した時に発生するサージによってArduinoが誤作動することがある。それを避けるためにはいくつかの方法が考えられるが、うちの実験室では以下で対応している。
    + リレーとArduinoの距離を離す。
    + リレーとArduinoを繋ぐ各ケーブル（ただしGNDは除く）について、200Ωくらいの抵抗とコンデンサ（10μF程度の）を直列に介してGNDに接続させる。
    + 十分な時間
+ モーター等からのノイズのアナログ入力への影響
    + どうしても気になるという場合は計測用のArduinoは別に用意したほうが良いかもしれない。

# スケッチ

```
//リレーは信号がLOWでポンプ等に給電されるように配線しておく。

// ライブラリの読み込み
#include <LiquidCrystal_I2C.h> 
#include <SPI.h>            
#include <SD.h>
#include <Wire.h>           
#include <TimeLib.h>        
#include <DS1307RTC.h>      

// LCDとSDカードの設定
LiquidCrystal_I2C lcd(0x27, 16, 2);  //LCDのI2Cアドレス0x27の場合
const int chipSelect = 10; // chipSelectの値10の場合
const String filename = "saved.txt";

// リレーのピンの設定
int infPumpPin = 4;
int effPumpPin = 5;
int airPumpPin = 6;
int mixerPin = 8;

// DO計からの電位信号を取得するピンの設定
int DOPin = A0;

// 運転に関するパラメータ
int feedTime = 5;  //流入ポンプは5分まで動作
int mixingTime = 60; //5分〜60分は攪拌
int aerationTime = 180; //60分〜180分は曝気
int settlingTime = 235; //180分〜235分は沈殿
int dischargeTime = 240; // 235分〜240分は放流
float minDO = 1.0;  //曝気中の溶存酸素濃度の下限
float maxDO = 1.5; //曝気中の溶存酸素濃度の上限
float DO;  // 溶存酸素濃度

// 運転制御に関する変数
time_t startTime;  //運転開始時刻
time_t currentTime;  //現在時刻
int elapsedTime;   //経過時間（分）

// 運転状態に関する変数
boolean flagInfPump = LOW; // flagはスイッチオンのときLOW、スイッチオフの時HIGHとする。
boolean flagEffPump = LOW;
boolean flagAirPump = LOW;
boolean flagMixer = LOW;

// テスト用
int timeFactor = 30; // １で通常速度で実行。N=2, 3, 4, 5, 6, 10, 12, 15, 30だとN倍速。

void setup() {
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  analogReference(INTERNAL);  // AD変換は通常0-5Vを10bitだが、この設定で0-1.1Vを10bitで変換する。
  pinMode(infPumpPin, OUTPUT);
  pinMode(effPumpPin, OUTPUT);
  pinMode(airPumpPin, OUTPUT);
  pinMode(mixerPin, OUTPUT);

  //SDカードの確認
  Serial.print("Initializing SD card...");
  // see if the card is present and can be initialized:
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    lcd.print("SD ER");
  }
  else {
    Serial.println("card initialized.");
    lcd.print("SD OK");
  }

  //RTCの確認
  setSyncProvider(RTC.get);   // the function to get the time from the RTC
  if (timeStatus() != timeSet) {
    Serial.println("Unable to sync with the RTC");
    setTime(0, 0, 0, 1, 1, 2021);   // RTCのないときは0時0分0秒に設定
  }
  else {
    Serial.println("RTC has set the system time");
  }

  //運転開始時刻を記録
  startTime = now();
}

void loop() {
  /*  loop()は概ね500mSごとに実行。
      以下の順で作業する。
      1. 現在の時刻を取得。運転開始時刻からの経過時間（elapsedTime)を分で算出
      2. 酸素濃度を得て、LCDに出力する。
      3. elapsedTimeをdischargeTimeで割った余りを算出する。その値に応じてリレーのON/OFFを設定する。
         なお、曝気時間のエアポンプの運転はDO制御する。
      4. 経過時間の秒の値が0ならSDカードに時刻、酸素濃度、リレーの状況を記録する。
      5. SDカードへの書き込みが不調の場合、その旨LCDに出力する。

  */
  currentTime = now();  // 1.の作業
  elapsedTime = ((currentTime - startTime) * timeFactor) / 60 ;
  DO = measureDO();  // 2.の作業。measureDO()の中身は下の方を参照。
  lcd.setCursor(0, 1);  //LCDの２行目の先頭に移動
  lcd.print("    ");   //LCDの２行目のDO値を書く部分の情報を消す
  lcd.setCursor(0, 1);  //LCDの２行目の先頭に移動
  lcd.print(DO); //LCDに現在のDOを表示
  relays(); // 3.の作業。relays()の中身は下の方を参照
  if (currentTime % (60/timeFactor) == 0) {
    record(); // 4., 5.の作業
    delay(1000);
  }
  delay(500);
}

float measureDO() {
  int readValue = 0;
  for (int i = 0; i < 8; i++) { //8回測定してその平均を取る。
    readValue += analogRead(A0);
    delay(10);
  }
  readValue = readValue >> 3;  // readValue >> 3は8=2^3で割るのと同じ。
  return readValue / 1023 /1.1 * 10; // DO値はA0からの読み値を1.1で割り10倍したものとする。必要に応じて換算式を変更
}


void relays() {
  if (elapsedTime % dischargeTime < feedTime) {  //流入時
    digitalWrite(infPumpPin, LOW);
    digitalWrite(effPumpPin, HIGH);
    digitalWrite(airPumpPin, HIGH);
    digitalWrite(mixerPin, LOW);     //攪拌機は沈殿・放流時以外稼働。
    flagInfPump = LOW;
    flagEffPump = HIGH;
    flagAirPump = HIGH;
    flagMixer = LOW;
  }
  else if (elapsedTime % dischargeTime < mixingTime) { //混合時
    digitalWrite(infPumpPin, HIGH);
    digitalWrite(effPumpPin, HIGH);
    digitalWrite(airPumpPin, HIGH);
    digitalWrite(mixerPin, LOW);     //攪拌機は沈殿・放流時以外稼働。
    flagInfPump = HIGH;
    flagEffPump = HIGH;
    flagAirPump = HIGH;
    flagMixer = LOW;
  }
  else if (elapsedTime % dischargeTime < aerationTime) {  //曝気時
    digitalWrite(infPumpPin, HIGH);
    digitalWrite(effPumpPin, HIGH);
    flagInfPump = LOW;
    flagEffPump = HIGH;
    if (flagAirPump == HIGH) {  // 値が等しいことを調べるためには=ではなく==を用いる。
      if (DO > maxDO) {  // flagAirPumpがHIGHのときはDOがmaxDOを超えたらエアポンプを停止
        digitalWrite(airPumpPin, HIGH);
        flagAirPump = LOW;
      }
    }
    else {               //flagAirPumpがLOWのときはDOがminDOを下回ったらエアポンプを運転
      if (DO < minDO) {
        digitalWrite(airPumpPin, LOW);
        flagAirPump = HIGH;
      }
    }
    digitalWrite(mixerPin, LOW);     //攪拌機は沈殿・放流時以外稼働。
    flagMixer = LOW;
  }
  else if (elapsedTime % dischargeTime < settlingTime) { //沈殿時
    digitalWrite(infPumpPin, HIGH);
    digitalWrite(effPumpPin, HIGH);
    digitalWrite(airPumpPin, HIGH);
    digitalWrite(mixerPin, HIGH);     //攪拌機は沈殿・放流時以外稼働。
    flagInfPump = HIGH;
    flagEffPump = HIGH;
    flagAirPump = HIGH;
    flagMixer = HIGH;
  }
  else if (elapsedTime % dischargeTime < dischargeTime) { //放流時
    digitalWrite(infPumpPin, HIGH);
    digitalWrite(effPumpPin, LOW);
    digitalWrite(airPumpPin, HIGH);
    digitalWrite(mixerPin, HIGH);     //攪拌機は沈殿・放流時以外稼働。
    flagInfPump = HIGH;
    flagEffPump = LOW;
    flagAirPump = HIGH;
    flagMixer = HIGH;
  }
}

void record() {
  String text = "";
  text += (currentTime - startTime) * timeFactor / 60 / 60 / 24; // 運転日数を書き加える
  text += "日";
  text += (currentTime - startTime) * timeFactor / 60 / 60 % 24; // 時間を書き加える
  text += "時間";
  text += (currentTime - startTime) * timeFactor / 60 % 60; // 分を書き加える
  text += "分\t";  // \tはタブ記号
  text += !flagInfPump;  // flagが1のときは!flagは0。すなわち、!flagが1のとき、リレーはオン。
  text += !flagEffPump;
  text += !flagAirPump;
  text += !flagMixer;
  text += "\t";  // \tはタブ記号
  text += DO;
  Serial.println(text);
  File dataFile = SD.open(filename, FILE_WRITE);
  if (dataFile) {
    dataFile.println(text);
    dataFile.close();
    lcd.setCursor(0, 0);
    lcd.print("SD OK");
    // print to the serial port too:
  }
  else {
    lcd.setCursor(0, 0);
    lcd.print("SD ER");
  }
}
```