techrmc’s blog

ICT大好きな中小企業診断士のブログです

Raspberry Pi 3BとESP-WROOM-02を用いた自作の環境ログシステム(その1)

全体概要

Raspberry Pi 3BとESP-WROOM-02をベースとした自作の環境ログシステムです。特徴は次の通りです。

  • 屋外に設置される計測センサは単三乾電池3本で長時間駆動可能
  • 将来のデータ分析に備えてセンサデータをデータベースへ蓄積
  • ファイルシステムのハードディスク実装によりRaspberry Piを長寿命化

システム構成図は次図の通りです。

システム構成図


環境ログシステムは、気温・湿度・気圧・土壌湿度を計測する測定クライアントと、測定クライアントから送信されたセンサデータをMySQLデータベースに記録するデータ蓄積サーバから構成されます。
測定クライアントはESP-WROOM-02をベースとしており、屋外のウォールボックスに設置して、屋内設置のデータ蓄積サーバとWiFi接続します。センサデータの通信には、MQTTを用いています。
一方、データ蓄積サーバはRaspberry Pi 3Bをベースとしており、測定クライアントからMQTTで受信したセンサデータをMySQLデータベースに記録します。MySQLのセンサデータはNode-REDによって可視化し、パソコンやスマホWebブラウザに表示します。

データ蓄積サーバは常時起動状態となるため、このままインターネットに晒すと外部から攻撃を受ける可能性が高くなります。そこで、データ蓄積サーバはインターネットから切り離したクローズドなネットワーク(システム構成図の点線内)に配置することで、セキュリティリスクを軽減しています。このクローズドなネットワークにアクセスするためのアクセスポイントがデータアップロード用アクセスポイントです。測定クライアントがセンサーデータを送信する際は、このデータアップロード用アクセスポイントに接続し、データ蓄積サーバへセンサデータを送信します。
一方、インターネット用アクセスポイントは、測定クライアントがデータアップロード用アクセスポイントにWiFi接続できない場合など異常検知した時に用いられ、測定クライアントはインターネット上の異常監視スマホに対してLINE Notifyで異常を通知します。LINE Notifyが呼ばれると、スマホのLINEアプリに受信メッセージが表示されます。
インターネット用アクセスポイントは、測定クライアントの初期化時にインターネット上のNTPサーバと時刻同期する際にも用いられます。

測定クライアント

測定クライアントは、

で構成します。BME-280は気温・湿度・気圧を測定するセンサであり、ESP-WROOM-02にI2Cで接続します。土壌湿度センサは土壌の湿り気を測定するセンサであり、ESP-WROOM-02のアナログ入力端子に接続します。リアルタイムクロックはESP-WROOM-02とI2Cで接続し、NTPサーバと同期が取れた時刻の保持と、ESP-WROOM-02の起動制御を行います。
測定クライアントの回路図と、ユニバーサル基板に実装した写真は次の通りです。
なお、回路図には記載していませんが、電源は単3乾電池3本で、3端子レギュレータで3.3Vまで降圧しています。ESP-WROOM-02WiFi接続時に大きな電流を必要とするので、それに耐え得る3端子レギュレータを選定します。

測定クライアント回路図
測定クライアント実装写真

写真では、測定クライアントのユニバーサル基板の下に、別記事
techrmc.hatenablog.com
で紹介したWiFiドアセンサのESP-WROOM-02が写っていますが、環境ログシステムとは無関係です。

電力消費量を削減するため、データ測定時以外は、ESP-WROOM-02をスリープ状態にしておきます。スリープ状態のESP-WROOM-02は非常に小さな電力消費となるため、乾電池駆動の長期化に役立ちます。
スリープ状態のESP-WROOM-02は、リアルタイムクロックRTC-8564NBのアラーム割り込みによって起動トリガをかけます。ESP-WROOM-02は測定が終わるとスリープ状態に入っていますが、所定のアラーム時刻にRTC-8564NBからトリガがかかると、ESP-WROOM-02はスリープ状態から復帰し、BME280と土壌湿度センサからデータを読み取ります。読み取ったセンサデータはMQTTでpublishし、次の起動時刻をRTC-8564NBのアラーム時刻としてセットしたのち再びスリープ状態に入ります。測定クライアントの初期化時には、RTC-8564NBをインターネット上のNTPサーバと時刻同期しておきます。
なお、土壌湿度センサをVCCとGNDに直接接続すると、土壌湿度測定時以外も常時、土壌湿度センサに電流が流れるため、乾電池の寿命が短くなリます。そこで、測定時以外は消費電流を抑制できるよう、土壌湿度センサへの電流供給をトランジスタ2SC1815によってスイッチングしています。

Arduinoで作成したESP-WROOM-02のプログラムを次に示します。なお、BME280用のプログラムは、スイッチサイエンスのサンプルプログラム
BME280 – スイッチサイエンス
をほぼそのまま利用させて頂きましたので、そちらをご覧ください。

メインプログラムmain.ino

/*
 * 測定クライアントのメインプログラム
 * 
 * 動作モード:DIP-SW(SW1)で切り替え
 *    キャリブレーションモード:WiFi接続テスト・NTP同期・土壌湿度センサを水に浸して半固定抵抗を調整
 *    通常モード:定期的にBME280と土壌湿度センサで温度・湿度・気圧・土壌湿度を測定し、MQTTブローカへpublish
 * 異常時の挙動: 
 *    動作モードがキャリブレーションモードの時:
 *      WiFiエラー(インターネットアクセス用):キャリブレーション無限ループ
 *      WiFiエラー(データアップロード用):キャリブレーション無限ループ
 *      NTPエラー:キャリブレーション無限ループ
 *    動作モードが通常モードの時:
 *      BME280エラー:ディープスリープ
 *      WiFiエラー(インターネットアクセス用):ディープスリープ
 *      WiFiエラー(データアップロード用):LINE通知&ディープスリープ
 *      MQTTエラー:LINE通知&ディープスリープ
 *    
 */
#include <Wire.h>
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <time.h> 
extern "C" {
#include "user_interface.h"         // ESP8266用の拡張IFライブラリ
}
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define ESP8266_PLATFORM

// IOピン番号
#define PIN_MODE 12                 // 動作モード(HIGH:通常, LOW:キャリブレーション)
#define PIN_LED 13                  // LED点灯制御(HIGH:点灯, LOW:消灯)
#define PIN_SPWR 14                 // 土壌湿度センサ電源制御(HIGH:ON, LOW:OFF)

// LED点灯回数
#define NOERR 3                     // 1秒間隔で3回点灯:NTP同期した後(同期成功・不成功には無関係)
#define WIFIERRM 5                  // 1秒間隔で5回点灯:WiFi接続エラー(MQTTデータアップロード用アクセスポイント)
#define NTPERR 7                    // 1秒間隔で7回点灯:NTP同期エラー(年が2020未満)
#define WIFIERRU 9                  // 1秒間隔で9回点灯:WiFi接続エラー(インターネット用アクセスポイント)

// 測定定数
#define SLEEP_P 0                   // スリープ周期(0は無期限)
#define CYCLE_M 60                  // 測定周期(分単位)

// キャリブレーションモードでは、土壌湿度センサを水に浸し、
// 上限値と下限値の間に設定する。
#define STH_H 930                   // 土壌湿度センサの上限値
#define STH_M 900                   // 土壌湿度センサの標的値
#define STH_L 870                   // 土壌湿度センサの下限値

// 動作モード(HIGH:通常, LOW:キャリブレーション)
int mode;

// WiFi定数
const char ssid4mqtt[] = "xxxx";         // MQTTデータアップロード用アクセスポイントのSSID
const char pass4mqtt[] = "xxxx";         // MQTTデータアップロード用アクセスポイントのパスワード
const char ssid4util[] = "xxxx";         // インターネット用アクセスポイント(NTP同期、LINEエラー通知)のSSID
const char pass4util[] = "xxxx";         // インターネット用アクセスポイント(NTP同期、LINEエラー通知)のパスワード

// MQTT定数
#define MQTTCONNECTWAIT 10000       // MQTTブローカへの接続リトライ待ち時間(msec)
#define MQTTMAXRETRY 2              // MQTT接続リトライの最大回数
const char mqttAddress[] = "xxx.xx.xx.xx";
const char mqttTopic[] = "xxxx";
const char mqttClientid[] = "xxxx";
const int mqttPort = 1883;

// LINE
const char* host = "notify-api.line.me";
const int httpPort = 443;
const char* token = "xxxx";

// Use WiFiClient class for MQTT connections
WiFiClient client;
PubSubClient mqttClient(client);

// Use WiFiClientSecure class for LINE connections
WiFiClientSecure utilclient;

// タイムスタンプ(ISO8601形式)
#define ATLEN 26      // timestampの文字列長+1(末尾のNULLをカウント) 定義変更時はrtc8564nb.inoの定義も要変更
char at[ATLEN];       // timestamp ISO8601形式

// 測定値をMQTTブローカへpublishする際のJSONバッファ
// https://arduinojson.org/v6/assistant/でのバッファサイズの計算式は、
// const size_t capacity = JSON_OBJECT_SIZE(5) + 78;
StaticJsonDocument<200> jsondoc;
char msg[200];

////////////////////////////////////////
//メインsetup
////////////////////////////////////////
void setup() {

  ////////////////////////////////////////
  // モード共通初期処理
  ////////////////////////////////////////
  Serial.begin(115200);
   
  //IO PIN 設定
  pinMode(PIN_LED,OUTPUT);        // PIN_LEDを出力用にセットして、LEDを消灯しておく
  digitalWrite(PIN_LED,LOW);
  pinMode(PIN_SPWR,OUTPUT);       // PIN_SPWRを出力用にセットして、土壌湿度センサを電源OFFにしておく
  digitalWrite(PIN_SPWR,LOW);
  pinMode(PIN_MODE,INPUT_PULLUP); // PIN_MODEのデフォルトは、HIGH(動作モード:通常)

  Wire.begin();                   // マスタとしてI2Cバスに接続

  mode = digitalRead(PIN_MODE);           // モードSWを読み取り

  if (mode==LOW) {  // 土壌センサキャリブレーションモード(WiFi接続テスト、NTP時刻同期を兼ねる)
    if (connectWiFi(ssid4mqtt, pass4mqtt)==false) {  // MQTTデータアップロード用アクセスポイントへの接続がエラーの場合
      longLightMulti(WIFIERRM);
      digitalWrite(PIN_SPWR,HIGH);
      delay(5000);                      // 土壌センサの安定を待つ
      return;                           // setup()を抜けて、loop()へ入る
                                        // WiFiエラー時は、NTP同期せず、土壌センサのキャリブレーションのみ実行 
    } else if (connectWiFi(ssid4util, pass4util)==false) { // インターネット用アクセスポイントがエラーの場合
      longLightMulti(WIFIERRU);
      digitalWrite(PIN_SPWR,HIGH);
      delay(5000);                      // 土壌センサの安定を待つ
      return;                           // setup()を抜けて、loop()へ入る
                                        // WiFiエラー時は、NTP同期せず、土壌センサのキャリブレーションのみ実行       
    }

    rtc_init();                
    
    //キャリブレーションモードでは、rtc_init()での低電圧検知に関係なくNTP同期に入る
    if (rtc_sync_ntp() < 2020) { // 現在の西暦2020より小さいとき、間違いなくNTP同期エラー
      longLightMulti(NTPERR);
    } else { // 2020以上の場合、NTP同期エラーの可能性は低い。ただし、NTP同期成功とは限らない。
      longLightMulti(NOERR);
    }  

    // 現在のRTCレジスタを取得    
    rtc_read_regtbl();  
    
    digitalWrite(PIN_SPWR,HIGH);
    delay(5000);                          // 土壌センサの安定を待つ

  } else {  //キャリブレーションモードはここまで。ここから通常モード
    // RTC初期設定
    if (rtc_init()==true) { // 低電圧検知時
      if (connectWiFi(ssid4util, pass4util) == false) { // インターネット用アクセスポイントへの接続がエラーの場合
        // 現在のRTCレジスタを取得    
        rtc_read_regtbl();
        
        // WiFi接続エラー時はRTCアラームをCYCLE_M分後にセット
        // RTC低電圧が起きているので、不定時刻のCYCLE_M分後となる
        rtc_set_alarm_min(CYCLE_M);  
        sleep();                          
      }

      // WiFi接続成功
       
      // NTP同期処理
      rtc_sync_ntp();
      
      // 低電圧検知時の処理終了
      
    }
    
    // 現在のRTCレジスタを取得
    rtc_read_regtbl();
  
    // CYCLE_M分後に新アラームをセット
    // RTC低電圧が起きていると、不定時刻のCYCLE_M分後となる
    rtc_set_alarm_min(CYCLE_M);  
    rtc_read_regtbl();
    
    digitalWrite(PIN_SPWR,HIGH);
    delay(5000);                          // 土壌センサの安定を待つ
 
    bme280_Setup();    
  
    // 土壌センサ・BME280 読み取り・送信
    soilbme280_mqtt();

    sleep();   
  }
}

////////////////////////////////////////
//メインloop
////////////////////////////////////////
void loop() {
  int value;
  
  if (mode==LOW) {
    value = analogRead(A0);
    if (value > STH_H) {  // 上限閾値超えの場合
      intLight(100);
    } else if (value > STH_M) {   // 標的閾値と上限閾値の中間の場合
      intLight(70);
    } else if (value > STH_L) {   // 下限閾値と標的閾値の中間の場合
      intLight(40);
    } else {                      // 下限閾値以下の場合
      intLight(10);
    }   
  } else {    //ダミー。mode=HIGH(通常モード)の時はloop()は呼ばれないはず。
    sleep();
  }
}

////////////////////////////////////////
//ディープスリープに入る
// ・bme280をスリープ
// ・土壌センサを電源OFF
// ・LEDを消灯
////////////////////////////////////////
void sleep(){
  bme280_Sleep();
  digitalWrite(PIN_SPWR,LOW);
  digitalWrite(PIN_LED,LOW);
  delay(100);
  ESP.deepSleep(SLEEP_P,WAKE_RF_DEFAULT);  // スリープモードへ移行する
  delay(1000);                             // スリープモードに入るのに時間を要すためダミーのdelayを入れる
}

////////////////////////////////////////
//長く点灯して消灯
////////////////////////////////////////
void longLight(){
  digitalWrite(PIN_LED,HIGH);
  delay(1000);  // light on for 1 sec
  digitalWrite(PIN_LED,LOW);
  delay(100);  // light off for 100 msec
}

////////////////////////////////////////
//長く点灯して消灯を、n回繰り返す
////////////////////////////////////////
void longLightMulti(int n) {
  for(int i=0;i<n;i++) longLight();
}

////////////////////////////////////////
//duty比率で点灯&消灯
// duty=100 1秒ずっと点灯
// duty=50 0.5秒点灯して0.5秒消灯
// duty=20 0.2秒点灯して0.8秒消灯
////////////////////////////////////////
void intLight(int duty){
  digitalWrite(PIN_LED,HIGH);
  delay(10*duty);             // light on
  digitalWrite(PIN_LED,LOW);
  delay(10*(100-duty)+10);    // light off
}

////////////////////////////////////////
// WiFiアクセスポイントに接続
// 戻り値
//  true:接続成功
//  false:接続エラー
////////////////////////////////////////
bool connectWiFi(const char *ssid, const char *pass){
  int waiting=0;

  if (WiFi.status() == WL_CONNECTED) {  // 接続済みの時、一旦、接続を切る
    WiFi.disconnect();
  }

  digitalWrite(PIN_LED,LOW);
  WiFi.mode(WIFI_STA);                  // STA mode
  WiFi.begin(ssid, pass);            
  while(WiFi.status() != WL_CONNECTED){ // wait until WiFi connected
    delay(100);                         // wait 100msec
    waiting++;                          // counter increment
    if(waiting > 300) {                 // max waiting counter. approx.300*100 msec= 30sec
      digitalWrite(PIN_LED,LOW);
      return false;                         
    }
  }
  digitalWrite(PIN_LED,LOW);
  return true;
}

////////////////////////////////////////
// LINEへメッセージ送信
// 戻り値
//  true: LINEメッセージ送信完了
//  false: WiFi接続エラー or LINE接続エラー or LINEレスポンス無し
////////////////////////////////////////
bool send_to_LINE(char *value1, char *value2, char *value3) {
  utilclient.setInsecure();
  if (connectWiFi(ssid4util, pass4util) == false) { // インターネットアクセス用アクセスポイントへの接続がエラーの場合
    return false;   
  }

  if (!utilclient.connect(host, httpPort)) {
    return false;
  }
    
  // LINEサーバへメッセージ送信
  String message = String("value1=") + String(value1)
                 + String(", value2=") + String(value2)
                 + String(", value3=") + String(value3);

  String content = String("message=") + message;
  utilclient.print(String("POST /api/notify HTTP/1.1\r\n")
                 + "Host: " + host + "\r\n"
                 + "Authorization: Bearer " + token + "\r\n"
                 + "Content-Length: " + String(content.length()) + "\r\n"
                 + "Content-Type: application/x-www-form-urlencoded\r\n\r\n"
                 + content + "\r\n");   
  unsigned long timeout = millis();
  while (utilclient.available() == 0) {
    if (millis() - timeout > 5000) {
      utilclient.stop();
      return false;
    }
  }
  
  // Read all the lines of the reply from server and print them to Serial
  while(utilclient.available()){
    String line = utilclient.readStringUntil('\r');
  }
  
  return true;
}

////////////////////////////////////////
//測定してmqtt送信
//測定値5回のうち最大・最小以外の3回の平均を取る
////////////////////////////////////////
void soilbme280_mqtt(){        
  float temperature[5], max_temperature, min_temperature;
  float humidity[5], max_humidity, min_humidity;
  float pressure[5], max_pressure, min_pressure;
  int moisture[5], max_moisture, min_moisture;

  readData();   
  temperature[0] = readTemperature();
  humidity[0] = readHumidity();
  pressure[0] = readPressure();
  moisture[0] = analogRead(A0);
  // Check if any reads failed and exit early (to try again).
  if (isnan(temperature[0]) || isnan(humidity[0]) || isnan(pressure[0])) {
    sleep();
  }
  delay(100);

  max_temperature = temperature[0];
  min_temperature = temperature[0];
  max_humidity = humidity[0];
  min_humidity = humidity[0];
  max_pressure = pressure[0];
  min_pressure = pressure[0];
  max_moisture = moisture[0];
  min_moisture = moisture[0];

  for(int i=1;i<5;i++) {  
    readData();      
    temperature[i] = readTemperature();
    humidity[i] = readHumidity();
    pressure[i] = readPressure();
    moisture[i] = analogRead(A0);
    // Check if any reads failed and exit early (to try again).
    if (isnan(temperature[i]) || isnan(humidity[i]) || isnan(pressure[i])) {
      sleep();
    }

    if (temperature[i] > max_temperature) 
      max_temperature = temperature[i];
    if (temperature[i] < min_temperature) 
      min_temperature = temperature[i];
    if (humidity[i] > max_humidity) 
      max_humidity = humidity[i];
    if (humidity[i] < min_humidity) 
      min_humidity = humidity[i];
    if (pressure[i] > max_pressure) 
      max_pressure = pressure[i];
    if (pressure[i] < min_pressure) 
      min_pressure = pressure[i];
    if (moisture[i] > max_moisture) 
      max_moisture = moisture[i];
    if (moisture[i] < min_moisture) 
      min_moisture = moisture[i];
    
    delay(100);
  }

  float ave_temperature=0;
  float ave_humidity=0;
  float ave_pressure=0;
  int ave_moisture=0;

  for(int j=0;j<5;j++) {
    ave_temperature += temperature[j];
    ave_humidity += humidity[j];
    ave_pressure += pressure[j];
    ave_moisture += moisture[j];
  }
  // remove abnormal (min, max) values
  ave_temperature = (ave_temperature-max_temperature-min_temperature)/3;
  ave_humidity = (ave_humidity-max_humidity-min_humidity)/3;
  ave_pressure = (ave_pressure-max_pressure-min_pressure)/3;
  ave_moisture = (ave_moisture-max_moisture-min_moisture)/3;

  // タイムスタンプ取得
  rtc_get_timestamp(at);

  if (connectWiFi(ssid4mqtt, pass4mqtt) == false) { // データアップロード用アクセスポイントへの接続がエラーの場合    
    send_to_LINE((char *)mqttClientid,"error","WiFi_connection");   
    sleep(); // WiFi接続エラー時はsetup()でアラームセットした時刻(CYCLE_M分後)に再起動      
  }

  // データアップロード用アクセスポイントに接続成功するとここまで来る

  //
  //時刻・温度・湿度・大気圧・土壌湿度のMQTT送信
  //

  mqttClient.setServer(mqttAddress, mqttPort);
  int cnt = 0;
  while (!mqttClient.connected()) {
    if (mqttClient.connect(mqttClientid)){
      jsondoc["datetime"] = at;
      jsondoc["temperature"] = ave_temperature;
      jsondoc["humidity"] = ave_humidity;
      jsondoc["pressure"] = ave_pressure;
      jsondoc["soil_moisture"] = ave_moisture;
      serializeJson(jsondoc,msg);
      mqttClient.publish(mqttTopic, msg);
      jsondoc.clear();    // jsondocのメモリリーク防止のためclearする
    } else {
      cnt++;
      if (cnt <= MQTTMAXRETRY ) { // 最大リトライ回数以下のとき
        delay(MQTTCONNECTWAIT);   // wait for MQTTCONNECTWAIT msec before retrying
      } else {                    // 最大リトライ回数を超えた時
        send_to_LINE((char *)mqttClientid,"error", "MQTT_connection");
        sleep();
      }
    }
  }
  mqttClient.disconnect();

}

RTC-8564NB周りのプログラムrtc8564nb.ino

#include <Wire.h>
#include <time.h> 
#include <TZ.h>
/*
 * bool rtc_init() -- RTC初期化
 * void rtc_sync_ntp() --- NTP時刻をRTCに設定
 * void rtc_read_regtbl() --- RTC内のレジスタからレジスタテーブルRegTblに読み込み
 * void rtc_set_alarm_min(int m) --- RTC内のレジスタにm分後のアラーム時分を書き込み
 * void rtc_set_alarm(int h, int m) --- RTC内のレジスタにアラーム時分を書き込み
 * int rtc_get_hour() --- レジスタテーブルRegTblから現在のhourを返す
 * int rtc_get_min() --- レジスタテーブルRegTblから現在のminを返す
 * void rtc_get_timestamp(char *at) --- レジスタテーブルからtimestampを生成 
 * 
 * byte BCDtoDec(byte value) --- BCDを10進数に変換
 * byte DectoBCD(int value) --- 10進数をBCDに変換
 */

#define RTC_ADDRESS 0x51                  // RTCのI2Cアドレス

#define NTP_SERVER1   "xxx.xxx.xxx"       // NTPサーバー
#define NTP_SERVER2   "xxx.xxx.xxx"       // NTPサーバー
#define NTP_SERVER3   "ntp.nict.jp"       // NTPサーバー
#define NTPMAXREPEAT  60                  // NTP同期ウェイト(1000ms)の最大リピート数

#define RTCREGLEN 16
int RegTbl[RTCREGLEN];  // RTCのレジスタテーブル(16byte)
static const char *pszWDay[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};  // 曜日表現

#define ATLEN 26      // timestampの文字列長+1(末尾のNULLをカウント)

/*
 * bool rtc_init() -- RTC初期化。
 * 低電圧が起きたかチェック(check VL bit of Seconds register)し、アラームINTをリセット。
 * アラームはダミー設定し(ダミー値:0時・0分・1日・日曜)、全てアラーム対象外ビットを立てる。
 * 
 * Return:
 *    true: 低電圧検知
 *    false: 低電圧非検知
 */
bool rtc_init(){ 
  bool isVL;  // 低電圧検知フラグ
      
  // VL bitのあるSeconds registerを取得
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x02);   // [02]Seconds レジスタを指定
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADDRESS,1);
  while (Wire.available() == 0 ){}
  RegTbl[2] = Wire.read();

  // 低電圧検知ビットVLをチェック
  if((RegTbl[2] & 0x80) != 0){ //低電圧検知
    isVL = true;
  } else { // 低電圧非検知
    isVL = false;
  }

  // アラーム発生時に1となっているAFビット(bit3)をリセット
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x01);   // [01]Control2 レジスタを指定  
  Wire.write(0x02);   // [01]Control2 0x02:アラーム割り込み時に3ピン[INT]をLOWにする&アラームリセット
  Wire.endTransmission();

  // アラームはダミー値を設定。先頭bitに1がある場合(0x80)はアラーム対象外
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x09);   // [09]Minutes Alarm レジスタを指定  
  Wire.write(0x00 | 0x80);  // [09]Minutes Alarm, ダミーで0分
  Wire.write(0x00 | 0x80);  // [0A]Hours Alarm, ダミーで0時
  Wire.write(0x01 | 0x80);  // [0B]Days Alarm, ダミーで1日
  Wire.write(0x00 | 0x80);  // [0C]Weekdays Alarm, ダミーで日曜
                              // 0:日 1:月 2:火 3:水 4:木 5:金 6:土
  //  クロック出力レジスタ
  // [0D]CLKOUT Frequency
  // ・クロック出力機能を有効にする。
  //   ※有効:0x80 無効 :0x00
  // ・クロック周波数は1Hz(1秒間に1回)とする
  //   ※0x00:32768Hz, 0x01:1024Hz, 0x02:32Hz, 0x03:1Hz
  // 次の設定だと2ピン[CLKOUT]から1秒に1回、クロック出力される     
  //    Wire.write(0x80 | 0x03);
  Wire.write(0x00 | 0x03);    // クロック周波数1Hz(0x03)だが、クロック出力無効(0x00)
  Wire.endTransmission();

  return(isVL);
}

/*
 * int rtc_sync_ntp() --- NTP時刻をRTCに設定。
 * 同期エラー時は不正値に設定される。
 * 
 * Return
 *    西暦年
 */
int rtc_sync_ntp(){
  time_t timeNow;
  struct tm* tmNow;
  int i=0;

  configTzTime(TZ_Asia_Tokyo, NTP_SERVER1, NTP_SERVER2, NTP_SERVER3);   

  // NTP同期を待つ。同期されていないと1970となる様子。2020未満は未同期と判断。   
  do {
    delay(1000);                            // 1秒ごとに繰り返し
    timeNow = time(NULL);
    tmNow = localtime(&timeNow); 
  } while(((tmNow->tm_year+1900) < 2020)&&(i<=NTPMAXREPEAT));    // 繰り返しがNTPMAXREPEATを超えたら同期を諦める

  // NTP取得情報をRTCにセット
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x00);
  Wire.write(0x00);  // [00]Control1
  Wire.write(0x02);  // [01]Control2 0x02:アラーム割り込み時に3ピン[INT]をLOWにする
  Wire.write(DectoBCD(tmNow->tm_sec));  // [02]Seconds
  Wire.write(DectoBCD(tmNow->tm_min));  // [03]Minutes
  Wire.write(DectoBCD(tmNow->tm_hour));  // [04]Hours
  Wire.write(DectoBCD(tmNow->tm_mday));  // [05]Days
  Wire.write(DectoBCD(tmNow->tm_wday));  // [06]Weekdays(月) 
  Wire.write(DectoBCD(tmNow->tm_mon+1)|0x80);   // [07]Month/Century(21世紀の12月)
                                  // ・Month(01-12) BCD形式
                                  // ・Century(先頭bit 0:20世紀[19xx年代],1(0x80):21世紀[20xx年代])
  Wire.write(DectoBCD(tmNow->tm_year-100));  // [08]Years

  // I2Cスレーブへの送信完了
  Wire.endTransmission(); 

  return(tmNow->tm_year+1900);
}

/*
 * void rtc_set_alarm_min(int m) --- RTC内のレジスタにm分後のアラーム時分を書き込み
 */
void rtc_set_alarm_min(int m) {
  int now_hour, now_min;      //現在時分
  int alarm_hour, alarm_min;  //アラーム時分
  int wh, wm;                 //作業用時分
        
  // アラーム時分をm分後に設定
  wh = m / 60;
  wm = m % 60;
  now_min = rtc_get_min();
  now_hour = rtc_get_hour();
  alarm_min = (now_min+wm)%60;
  if (((now_min+wm)/60)==0) { //分桁上がり無し
    alarm_hour = (now_hour + wh)%24;
  } else { //分桁上がり有り
    alarm_hour = (now_hour + wh + 1)%24;
  }
  rtc_set_alarm(alarm_hour, alarm_min);
}

/*
 * void rtc_set_alarm(int h, int m) --- RTC内のレジスタにアラーム時分を書き込み
 * DaysとWDaysはダミー値を書き込み、アラーム対象外に設定
 */
void rtc_set_alarm(int h, int m){ 
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x09);
  Wire.write(DectoBCD(m));  // [09]Minutes 
  Wire.write(DectoBCD(h));  // [0A]Hours Alarm
  Wire.write(0x01 | 0x80);  // [0B]Days Alarm(1日), 先頭bitに1がある場合(0x80)はアラーム対象外
  Wire.write(0x00 | 0x80);  // [0C]Weekdays Alarm(日曜),先頭bitに1がある場合(0x80)はアラーム対象外
                            // 0:日 1:月 2:火 3:水 4:木 5:金 6:土
  Wire.endTransmission(); 
}

/* 
 * int rtc_get_hour() --- レジスタテーブルRegTblから現在のhourを返す
 * RTCのレジスタから読み出すわけではないので要注意。
 */
int rtc_get_hour(){
  return(BCDtoDec(RegTbl[4] & 0x3F));
}

/*
 * int rtc_get_min() --- レジスタテーブルRegTblから現在のminを返す
 * RTCのレジスタから読み出すわけではないので要注意。
 */
int rtc_get_min(){
  return(BCDtoDec(RegTbl[3] & 0x7F));
}

/*
 * void rtc_get_timestamp(char *at) --- レジスタテーブルからtimestampを生成 
 */
void rtc_get_timestamp(char *at) {
  String wts = String(BCDtoDec(RegTbl[8] & 0xFF)+2000); // year
  wts += '-';
  wts += ((RegTbl[7] >> 4) & 0x01);   // 月の10の位 (0,1)
  wts += (RegTbl[7] & 0x0F);          // 月の1の位 (0,,,,,9)
  wts += '-';
  wts += ((RegTbl[5] >> 4) & 0x03);   // 日の10の位 (0,,3)
  wts += (RegTbl[5] & 0x0F);          // 日の1の位 (0,,,,,9)
  wts += 'T';
  wts += ((RegTbl[4] >> 4) & 0x03);   // 時の10の位 (0,,2)
  wts += (RegTbl[4] & 0x0F);          // 時の1の位 (0,,,,,9)
  wts += ':';
  wts += ((RegTbl[3] >> 4) & 0x07);   // 分の10の位 (0,,,5)
  wts += (RegTbl[3] & 0x0F);          // 分の1の位 (0,,,,,9)
  wts += ':';
  wts += ((RegTbl[2] >> 4) & 0x07);   // 秒の10の位 (0,,,5)
  wts += (RegTbl[2] & 0x0F);          // 秒の1の位 (0,,,,,9)
  wts += "+09:00";                    // JST

  //Serial.println(wts);
  wts.toCharArray(at, ATLEN);
}

/*
 * void rtc_read_regtbl() --- RTC内のレジスタからレジスタテーブルRegTblに読み込み
 */
void rtc_read_regtbl(){
   
  // レジスタのアドレスを先頭にする
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x00);
  Wire.endTransmission(); 
 
  // I2Cスレーブに16byteのレジスタデータを要求する
  Wire.requestFrom(RTC_ADDRESS,16);
                
  // RTCREGLEN(16byte)のデータを取得する
  for (int i=0; i<RTCREGLEN; i++){
    while (Wire.available() == 0 ){}
    RegTbl[i] = Wire.read();
  }
}

////////////////////////////////////////
// 2進化10進数(BCD)を10進数に変換
////////////////////////////////////////
byte BCDtoDec(byte value){
  return ((value >> 4) * 10) + (value & 0x0F) ;
}

////////////////////////////////////////
// 10進数をBCDに変換
////////////////////////////////////////
byte DectoBCD(int value) {
  return((value/10*0x10) | (value%10));
}

そろそろ記事が長くなってきたので、続きは別記事とします。
続きはこちらへ。
techrmc.hatenablog.com