背景
- 温湿度や大気センサーのデータをBLEで送信し、それを収集してWebサーバーへWi-Fi経由で送っています。
その際、収集したデータをOLEDに表示していましたが、約2ヶ月で画面焼けが発生しました。

- そこで、表示画面を電子ペーパーに変更してみました。
回路図
Windows 10/11 で動作する回路図エディタである水魚堂のBSch3V(フリーソフト)を使いました。
- マイコンは、Seeed xiao ESP32C3を使いました
- 技術資料
https://wiki.seeedstudio.com/ja/XIAO_ESP32C3_Getting_Started/

- ePaperは、WeAct 1.54 インチ 1.54 インチ電子ペーパー モジュール電子ペーパー E インク E インク表示画面 SPI ブラック ホワイト - 技術資料 https://github.com/WeActStudio/WeActStudio.EpaperModule
ハード作成

部品
- マイコンと電子ペーパはアリエクで購入しました。アマゾンの同等品は、下記になります
- 備品は LEDは抵抗入りにしました
リポバッテの電圧測定のため220kΩの抵抗を使いました
基板は
ソフト作成
開発環境 Arduino IDE 2.3.4 ソース
/* XIAO_ESP32C3_LED_Receive_Ant.ino Seeed XIAO BLE nRF52840から受信したBLEのアドバタイズデータを WiFiでWebサーバーにPHPに送信する ボードマネージャ ESP32 by Espressif Syatem: Version 2.0.17 ライブラリーマネージャー GxEPD2 by Jean-Marc Zingg 1.68 ツール Patiton Scheme: Huge App(3MB On OTA/1MB SPIFFS) PHPの位置 /innar/temp_humi_press.php /outer/temp_humi_press.php */ // ------------GxEPD2------------------------------------ // base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code // enable or disable GxEPD2_GFX base class #define ENABLE_GxEPD2_GFX 0 #include <Arduino.h> #include <GxEPD2_BW.h> #include <GxEPD2_3C.h> #include <Fonts/FreeMonoBold12pt7b.h> #include <Fonts/FreeMonoBold18pt7b.h> // ESP32-C3 CS(SS)=7, SCL(SCK)=4, SDA(MOSI)=6, BUSY=3, RES(RST)=2, DC=1 // #define SDA_PIN (MOSI) // #define CSK_PIN (SCK) #define CS_PIN D6 //ChipSelect #define BUSY_PIN D3 //Busy #define RES_PIN D2 //Reset #define DC_PIN D1 //Data/Command control // 1.54'' EPD Module GxEPD2_BW<GxEPD2_154_D67, GxEPD2_154_D67::HEIGHT> display(GxEPD2_154_D67(/*CS=5*/ CS_PIN, /*DC=*/DC_PIN, /*RES=*/RES_PIN, /*BUSY=*/BUSY_PIN)); // GDEH0154D67 200x200, SSD1681 // ------------WiFi------------------------------------ #include <WiFi.h> #include "time.h" #include "esp_sntp.h" #include <HTTPClient.h> const String WEB_API_URL_i = "http://192.168.10.117/innar/temp_humi_press.php"; // 送信先URL const String WEB_API_URL_o = "http://192.168.10.117/outer/temp_humi_press.php"; // 送信先URL const char* ssid = "????????????????"; const char* password = "????????????????"; const char* ntpServer1 = "ntp.nict.jp"; const char* ntpServer2 = "time.google.com"; const char* ntpServer3 = "ntp.jst.mfeed.ad.jp"; const long gmtOffset_sec = 9 * 3600; const int daylightOffset_sec = 0; // Callback function (get's called when time adjusts via NTP)-------------------- void timeavailable(struct timeval* t) { Serial.print("Got time adjustment from NTP! "); } void printLocalTime() { char str[256]; unsigned long m; // timeとlocaltimeを使用して現在時刻取得 time_t t; struct tm* tm; m = millis(); t = time(NULL); tm = localtime(&t); sprintf(str, "\nday:%04d/%02d/%02d, time:%02d:%02d:%02d, ", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); Serial.print(str); } //------------BLE関連------------------------------ #include <BLEDevice.h> BLEScan* pBLEScan; // 設定 const size_t RCV_CNT_MAX = 10; // 同時に受信するデバイスの最大数 const int BLE_SCAN_TIME = 5; // BLEのスキャンを行う時間(s) const char XIAO[4] = { 0xFF, 0xFF, 0x12, 0x35 }; // XIAO nRF52840識別値(FFFFは固定) // シーケンス番号の記憶 #include <map> std::map<uint16_t, uint16_t> seqHist; // その他定義 struct RcvData { uint16_t id; // 子機ID uint8_t type; // 子機種別 uint8_t ttl; // TTL (time to live) uint16_t seq; // シーケンス番号 float pres; // 気圧データ float temp; // 気温データ float humi; // 湿度データ float vbat; // 電圧データ int16_t rssi; // 電波強度 RSSI -50非常に強い -80弱い }; // 初期化 void setup() { Serial.begin(115200); delay(1000); Serial.print(" System Start! "); // voltage CHK ---------------------------------------- pinMode(A0, INPUT); // ADC // LED ------------------------------------------------- pinMode(D7, OUTPUT); // LED connected to digital pin 7 delay(100); // set to Display(GxEPD2)----------------------------- //is GxEPD2 pinMode(CS_PIN, OUTPUT); //ChipSelect pinMode(BUSY_PIN, OUTPUT); //Busy pinMode(RES_PIN, OUTPUT); //Reset pinMode(DC_PIN, OUTPUT); //Data/Command control display.init(115200, true, 50, false); delay(100); Gxprint(0.0, 0.0, 0.0); //外気温 内気温 リポバッテリ // Conect to Wi-Fi------------------------------------------- Serial.printf("Connecting to %s ", ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.print(" CONNECTED "); //init and get the time sntp_set_time_sync_notification_cb(timeavailable); configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3); // BLEの設定----------------------------------------- Serial.print("init BLE device..."); BLEDevice::init(""); pBLEScan = BLEDevice::getScan(); pBLEScan->setActiveScan(false); // パッシブスキャンにする } // Main loop int BleTimer = 0; float inVal, outVal, inVolt; void loop() { RcvData rcvdatas[RCV_CNT_MAX]; int rcvcnt = 0; int loopCunt; float pres, temp, humi; uint16_t rssi, InOutVal; char buff[64]; // Perform a BLE scan BLEScanResults foundDevices = pBLEScan->start(BLE_SCAN_TIME); int hit = foundDevices.getCount(); // Extract the target device from the received data for (int i = 0; i < hit; i++) { BLEAdvertisedDevice dev = foundDevices.getDevice(i); std::string data = dev.getManufacturerData(); if (data.length() < 18) continue; // If the data is from the XIAO nRF52840 then store the value if (data[0] == XIAO[0] && data[1] == XIAO[1] && data[2] == XIAO[2] && data[3] == XIAO[3]) { onoffLED(); rcvdatas[rcvcnt].id = data[5] << 8 | data[4]; InOutVal = rcvdatas[rcvcnt].id; rcvdatas[rcvcnt].type = data[6]; rcvdatas[rcvcnt].ttl = data[7]; rcvdatas[rcvcnt].seq = data[9] << 8 | data[8]; if (seqHist[rcvdatas[rcvcnt].id] == rcvdatas[rcvcnt].seq) continue; // 同じデータは無視 seqHist[rcvdatas[rcvcnt].id] = rcvdatas[rcvcnt].seq; if (rcvdatas[rcvcnt].type == 10 || rcvdatas[rcvcnt].type == 11) { rcvdatas[rcvcnt].pres = (float)(data[11] << 8 | data[10]) / 10.00; rcvdatas[rcvcnt].temp = (float)(data[13] << 8 | data[12]) / 10.00; rcvdatas[rcvcnt].humi = (float)(data[15] << 8 | data[14]) / 10.00; rcvdatas[rcvcnt].vbat = (float)(data[17] << 8 | data[16]) / 100; rcvdatas[rcvcnt].rssi = (uint16_t)dev.getRSSI(); // Signal strength //マイナス温度補正 if ((rcvdatas[rcvcnt].temp * 10.0) > 60000.0) { rcvdatas[rcvcnt].temp = rcvdatas[rcvcnt].temp - 6553.5; } rcvcnt++; } if (rcvcnt >= RCV_CNT_MAX) break; } } // ディスプレイに表示 if (rcvcnt > 0) { rcvcnt--; if (BleTimer > 5) { //シリアル出力 printLocalTime(); // Serial.printf("Pres:%.1f,temp:%.1f,humi:%.1f,vbat:%.1f,rssi:%d, ID:%d,type:%d", // rcvdatas[rcvcnt].pres - 1000, rcvdatas[rcvcnt].temp, rcvdatas[rcvcnt].humi, rcvdatas[rcvcnt].vbat, abs(rcvdatas[rcvcnt].rssi), rcvdatas[rcvcnt].id, rcvdatas[rcvcnt].type); // //液晶出力 if (InOutVal == 101) { outVal = rcvdatas[rcvcnt].temp; } else if (InOutVal == 102) { inVal = rcvdatas[rcvcnt].temp; } inVolt = voltChk(); //e-Paper出力 Gxprint(outVal, inVal, inVolt); // Wifi出力------------------------------- postServer(rcvdatas, 1, InOutVal); } // Serial.printf("\nPres:%.1f,temp:%.1f,humi:%.1f,rssi:%d", // rcvdatas[rcvcnt].pres, rcvdatas[rcvcnt].temp, rcvdatas[rcvcnt].humi,-1 * rcvdatas[rcvcnt].rssi ); BleTimer = 0; } else { BleTimer++; // if ((BleTimer % 2) == 0) digitalWrite(D7, 0); // else digitalWrite(D7, 1); for ( loopCunt = 0; loopCunt < 270 ; loopCunt ++){ onoffLED(); } if ((BleTimer % 5) == 0) { Serial.printf(" %d", BleTimer / 5); } } } // Webサーバーに送信 void postServer(struct RcvData* td, int cnt, uint16_t InOutVal) { HTTPClient http; Serial.print(" Send to Web Server "); // POSTデータ作成 String jsonData = "{\"data\":["; char buff[128]; for (int i = 0; i < cnt; i++) { if (InOutVal == 101) { sprintf(buff, "{\"id\":%d,", 101); } else if (InOutVal == 102) { sprintf(buff, "{\"id\":%d,", 102); } jsonData += buff; sprintf(buff, "\"pres\":%.1f,", td[i].pres); jsonData += buff; sprintf(buff, "\"volt\":%.1f,", td[i].vbat); jsonData += buff; sprintf(buff, "\"temp\":%.1f,", td[i].temp); jsonData += buff; sprintf(buff, "\"humi\":%.1f,", td[i].humi); jsonData += buff; sprintf(buff, "\"rssi\":%d,", td[i].rssi); jsonData += buff; sprintf(buff, "\"seq\":%d}", td[i].seq); jsonData += buff; if (i < cnt - 1) jsonData += ","; } jsonData += "],\"count\":" + String(cnt) + "}"; Serial.print(jsonData); // 送信 if (InOutVal == 101) { http.begin(WEB_API_URL_o); //HTTP outer } else if (InOutVal == 102) { http.begin(WEB_API_URL_i); //HTTP innar } http.addHeader("Content-Type", "application/x-www-form-urlencoded"); http.addHeader("Content-Length", String(jsonData.length())); http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); int httpCode = http.POST(jsonData); // printf("[HTTP] POST... code: %d ", httpCode); // 送信後の処理 if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); printf(" OK %s ", payload); } else { printf("[HTTP] POST... failed, error: %s ", httpCode); } http.end(); } void Gxprint(float outFloat, float inFloat, float voltFloat) { int16_t tbx, tby; uint16_t tbw, tbh; char inString[8], outString[8], voltString[8]; //floatを文字列に変換 sprintf(inString, "%+4.1f", inFloat); sprintf(outString, "%+4.1f", outFloat); sprintf(voltString, "%4.1f", voltFloat); //初期値設定 display.setRotation(1); display.setFont(&FreeMonoBold12pt7b); display.setTextColor(GxEPD_BLACK); display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); //外気温 display.setFont(&FreeMonoBold12pt7b); display.setCursor(2, 30); display.print("Outside\ntemp."); display.setFont(&FreeMonoBold18pt7b); int16_t tbx, tby; uint16_t tbw, tbh; display.getTextBounds(outString, 0, 0, &tbx, &tby, &tbw, &tbh); display.setCursor(display.width() - tbw - 3, 55); display.print(outString); display.fillRect(0, 65, 199, 2, GxEPD_BLACK); //内気温 display.setFont(&FreeMonoBold12pt7b); display.setCursor(2, 90); display.print("Indoor\ntemp."); display.setFont(&FreeMonoBold18pt7b); display.getTextBounds(inString, 0, 0, &tbx, &tby, &tbw, &tbh); display.setCursor(display.width() - tbw - 3, 115); display.print(inString); display.fillRect(0, 125, 199, 2, GxEPD_BLACK); //大気圧 display.setFont(&FreeMonoBold12pt7b); display.setCursor(2, 150); display.print("LiPo battery\nvoltage"); display.setFont(&FreeMonoBold18pt7b); display.getTextBounds(voltString, 0, 0, &tbx, &tby, &tbw, &tbh); display.setCursor(display.width() - tbw - 3, 176); display.print(voltString); } while (display.nextPage()); } float voltChk() { uint32_t Vbatt = 0; for (int i = 0; i < 16; i++) { Vbatt = Vbatt + analogReadMilliVolts(A0); // ADC with correction } return (2 * Vbatt / 16 / 1000.0); // attenuation ratio 1/2, mV --> V } void onoffLED(){ delay(13); digitalWrite(D7, 1); delay(3); digitalWrite(D7, 0); }




