ESP32で NTP時計+温湿度モニター+ZabbixSender を作った (AHT10, SSD1306 OLED)

概要

秒針付きNTP時計と、AHT10センサーで取得した温湿度をOLED SSD1306 ディスプレイに表示するやつを作りました。

また、取得した温湿度はZabbixにも送りつけます。

接続

OLEDディスプレイの裏にあるチップ抵抗のジャンパの位置を変えると、I2Cのアドレスをデフォルトの0x3Cから0x3Dに変えることができます。小さくて地味に難しかった。

ESP32は21番がSDA, 22番がSCLです。OLEDもAHT10も3.3Vに繋ぎました。

全て並列に繋げばOKです

Zabbixサーバーの設定

ホストの作成

Configuration -> Hosts から 右上の Create Host をクリック

Host name は ESP32_clock_and_temp で、Groups は Templates/Modules にしました。

アイテムの作成

作成したホストのitemsをクリック

こんな感じに温度と湿度のアイテムを作ります

あとはZabbixのダッシュボードでグラフを表示させればOK

プログラム

汚いプログラム。半分ぐらいコピペとGPT-4oです。

使うライブラリはincludeのとこ見てください。ESP32ZabbixSender以外はPIOのLibrariesから検索すると出てきます。

ESP32ZabbixSenderはこれ。ただしなぜか名前がESP8266のままなので、ESP8266ZabbixSender.cpp, ESP8266ZabbixSender.hをESP32ZabbixSender.cpp, ESP32ZabbixSender.hにリネームする必要がありました。
PIOなら、それらをlibフォルダに投げれば設置完了です。

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <Adafruit_AHTX0.h>
#include <ESP32ZabbixSender.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1

#define SERVERADDR 192, 168, 100, 11 // Zabbix server Address
#define ZABBIXPORT 10051			// Zabbix erver Port
#define ZABBIXAGHOST "ESP32_clock_and_temp"  // Zabbix item's host name

// Create display object
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_SSD1306 display2(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

//Create AHT10 object
Adafruit_AHTX0 aht;

// WiFi credentials
const char* ssid = "SSID";  // Replace with your SSID
const char* password = "Pass";  // Replace with your Password

// NTP Client setup
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 32400, 1000); // JST offset 9時間. 最小1000msで更新, ただしtimeClient.update()で呼び出さないと更新はされない
unsigned long lastSyncTime = 0;
const unsigned long SyncTimeInterval = 1024000; // 1024s = 17m4s
int NTP_disable_sleep = 0;
int NTP_error_count = 0;

// Temp && Hum get interval
unsigned long lastGetTemp = 0;
const unsigned long GetTempInterval = 30000; //30秒が前提になってるので変えないで

// Zabbix
ESP32ZabbixSender zSender;
int tempCount = 0;
int Zabbix_disable_sleep = 0;
int Zabbix_error_count = 0;

void setup() {
    // SSD1306 init
    Serial.begin(115200);
    display.begin(SSD1306_SWITCHCAPVCC, 0x3C); 
    display.clearDisplay();
    Serial.println("Display1 initialized");
    display2.begin(SSD1306_SWITCHCAPVCC, 0x3D); 
    display2.clearDisplay();
    Serial.println("Display2 initialized");

    // Connect to WiFi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }
    Serial.println("Connected to WiFi");

    // Start NTP client
    timeClient.begin();
    timeClient.update();
    Serial.println("NTP client started");

    // AHT10 init
    if (!aht.begin()) {
      Serial.println("Failed to initialize AHT10 sensor!");
      // esp_restart(); // 初期化失敗したらリスタートすれば行けるかと思ったけど, USBを物理的に繋ぎ直さないとダメだった
    }

    // Zabbix init
    zSender.Init(IPAddress(SERVERADDR), ZABBIXPORT, ZABBIXAGHOST);
}


void loop() {
    unsigned long currentTime = millis();


    // ここから NTP同期

    if (currentTime - lastSyncTime >= SyncTimeInterval - 10000){ // Wifi使う10秒前からスリープ解除(しないと失敗する)
      NTP_disable_sleep = 1;
    }

    if (currentTime - lastSyncTime >= SyncTimeInterval){
      if(timeClient.update()){
        Serial.println("Success to sync");
        lastSyncTime = millis();
        NTP_disable_sleep = 0;

        NTP_error_count = 0 ;
      }
      else{
        Serial.println("failed to sync"); // よく失敗する. Wifi再接続するとたぶん成功する

        WiFi.disconnect(); // 同期処理らしい
        WiFi.begin(ssid, password); // 非同期だけど30秒待てば多分大丈夫

        lastSyncTime = currentTime - SyncTimeInterval + 30000; // 30秒後にリトライ

        NTP_error_count++ ;
      }
    }

    // ここまで NTP同期


    // ここから 時刻表示

    unsigned long epochTime = timeClient.getEpochTime();

    setTime(epochTime);  // Set the TimeLib time to NTP time

    // Fetch and print current time details
    int yearValue = year();  
    int monthValue = month();  
    int dayValue = day();  
    int hourValue = hour();  
    int minuteValue = minute();  
    int secondValue = second();  
    int dayOfWeek = weekday();  // 1=Sunday, 2=Monday, ..., 7=Saturday

    // Create an array for the abbreviated day names
    const char* days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
    String dayString = days[dayOfWeek - 1];  // Get the correct day string (adjust index)

    int reiwa_year = yearValue - 2018;  

    // Clear display before writing new data
    display.clearDisplay();

    // Set text color to white
    display.setTextColor(SSD1306_WHITE);  // Set text color to white

    // First line
    display.setTextSize(2);
    display.setCursor(0, 0);
    display.printf("%04d/%02d/%02d", yearValue, monthValue, dayValue);

    // Second line
    display.setTextSize(4);
    display.setCursor(0, 20);
    display.printf("%02d:%02d", hourValue, minuteValue);

    // Third line
    display.setTextSize(2);
    display.setCursor(0, 50);
    display.printf("%02d %s R%d", secondValue, dayString, reiwa_year);

    // Update display
    display.display(); 

    // ここまで 時刻表示


    // ここから 温湿度表示

    if (currentTime - lastGetTemp >= GetTempInterval) {
      // Get value by sensor
      sensors_event_t humidity, temp;
      aht.getEvent(&humidity, &temp); 

      float temperature = temp.temperature;
      float humidity_value = humidity.relative_humidity;

      display2.clearDisplay();
      display2.setTextColor(SSD1306_WHITE);
      
      display2.setTextSize(4);
      display2.setCursor(0, 0);
      display2.printf("%.1f", temperature);
      display2.setTextSize(2);
      display2.printf(" ");
      display2.setTextSize(3);
      display2.printf("C");

      display2.setTextSize(4);
      display2.setCursor(0, 32);
      display2.printf("%.1f", humidity_value);
      display2.setTextSize(2);
      display2.printf(" ");
      display2.setTextSize(3);
      display2.printf("%%");

      // NTPとZabbixのエラーカウントを右下にちっちゃく表示
      display2.setTextSize(1);
      display2.setCursor(96, 56);
      display2.printf("%d %d", NTP_error_count, Zabbix_error_count);

      display2.display();

      lastGetTemp = currentTime;
      tempCount++;
      
      // ここまで 温湿度表示


      // ここから Zabbixにデータ送信

      if (tempCount == 3){ // 気温湿度センサーから値を取得して画面更新するループが4回周るごとにZabbixにデータを送りつける. つまり2分に1回
        zSender.ClearItem();
        zSender.AddItem("temp", temperature);
        zSender.AddItem("hum", humidity_value);
        if (zSender.Send() == EXIT_SUCCESS) {
          tempCount = 0;
          Zabbix_disable_sleep = 0;
          Zabbix_error_count = 0 ;
        }else{
          WiFi.disconnect();
          WiFi.begin(ssid, password); // 例によって、失敗したらWifi再接続
          tempCount = 2; // カウントを2に戻すことで30秒後に再試行
          Zabbix_error_count++ ;
        }
      }

      // ここまで Zabbixにデータ送信

    }

    if (tempCount == 2){
      Zabbix_disable_sleep++ ; // データ送信10秒前にスリープ解除するためのやつ 後に処理書いてる
    }

    delay(50); //sleepするにしても最低限のdelayを入れないとウオッチドッグにリセットされる

    // やけに消費電力が大きかった(常時0.5Wぐらい)のでdelayではなくライトスリープ使う
    // 何も考えずに1秒スリープすると秒針が飛ぶので、処理時間を考慮してスリープ時間を決定する
    long sleepDuration = 1000 - (millis() - currentTime);

    // スリープ時間が負になったらやばいので
    if (sleepDuration > 0) {
      if (NTP_disable_sleep == 1 || Zabbix_disable_sleep >= 20){ // 10秒前にスリープ解除
        delay(sleepDuration);
        // Serial.println("delay"); // デバッグ用
      }
      else{
        esp_sleep_enable_timer_wakeup(sleepDuration * 1000); // micro sec 単位らしい
        esp_light_sleep_start();
        // Serial.println("sleep"); // デバッグ用
      }
    }
    else{
      // Serial.println("no sleep"); // デバッグ用
    }
}

・時計のモニターは1秒で更新 (秒針動かすため)
・NTPサーバーと同期する時間は1024秒 (この秒数は変えてOK)
・温湿度は30秒ごとに取得 & モニター更新 (この秒数は変えないで)
・Zabbixサーバーに2分に1回送信
・delayじゃなくてlight sleepさせる(消費電力がかなり減った)

・AHT10の初期化はよく失敗する。リセットコマンド入れても無駄で、USBを物理的に差しなおさないとダメだった

・Wifi使う直前にsleep入ってるとこういう変なエラーが出て失敗するので、10秒前のループからsleepじゃなくてdelayに変更。

[WiFiUdp.cpp:172] beginPacket(): could not get host from dns: 11
[WiFiUdp.cpp:185] endPacket(): could not send data: 118

更に失敗したときはWifiを再接続して30秒後に再試行します。

・loop()内のどこかで1回はdelay(50); (最低限は1らしいけど効かなかったので50にした) を呼び出さないとウォッチドッグタイマ(WDT)にリセットされる

こんな出力でリセットがかかります

rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)

参考
https://lang-ship.com/blog/work/esp32-freertos-l03-multitask

ESP32には2コアあって、コア名PRO_CPUは無線の処理担当でWDT有効、APP_CPUはloop()関数担当でWDT無効らしい。
ウォッチドッグタイマのリセット条件はdelay()関数を1以上で呼び出すこと。

なぜWDT無効なはずのloop()関数側でdelay呼び出すとリセットされなくなるのかは謎です。

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Index