前回 ESP-WROOM-02 DIP化キットに接続しているサーミスターの温度をアナログコンバーターで取得する方法を紹介しました。
ESP-WROOM-02 サーミスタの温度をアナログコンバーターで取得する (Qiita@pipito-yukio)
今回は下記のようなESP気象センサーに搭載された2つのセンサーの観測データをWi-Fi経由で定期的(約10分間隔)にUDPパケットで内部ネットワークにブロードキャストするプログラムについて解説します。
搭載する機能
- (1) 充電池駆動 (ニッケル水素電池 x 4: 4.8vをレギュレータで 3.3vに降圧)
- (2) サーミスタによる外気温測定 (アナログコンバータMCP3002使用)
※実装方法の詳細については前回Qiita投稿記事をご覧ください。 - (3) BME280 センサーによる室内気温・湿度・気圧の測定 (Adafruit ライブラリ使用)
- (4) 2つのセンサーの測定値をUDPバケットとしてWi-Fi経由でブロードキャストする
- (5) 指定時間ディープスリープする (10分 - 処理時間)
実装の元になった書籍の出典箇所
搭載する機能(4)と(5) の実装については、ほとんどが①の出典箇所からの流用になります。
ただし(5)の機能については ②の出典箇所から処理手順のヒントをいただきました。
※②ESP32の本なので使用する関数はESP02のものに置き換えしています。
- ① 超特急Web接続! ESPマイコン・プログラム全集 [CD-ROM付き]
2019年4月1日 第2版発行 CQ出版株式会社- 第4章 [2] Wi-Fiスイッチャ
Wi-Fiネットワークの設定、UDPパケットのブロードキャスト - 第4章 Appendix 2 いつまでも動く無人IoTを作る ESPマイコンのケチケチ運転術
ディープスリープによる間欠運転
- 第4章 [2] Wi-Fiスイッチャ
- ② IoT 開発スタートブック ESP32でクラウドにつなげる電子工作をはじめよう!
2019年8月24日 初 版 第1刷発行 株式会社技術評論社- 第4章 より実用的なセンサ端末を作るー消費電力を下げバッテリーで動かしてみよう
ディープスリープによる間欠運転
- 第4章 より実用的なセンサ端末を作るー消費電力を下げバッテリーで動かしてみよう
開発環境
- PC: Ubuntu 22.04
- ソース作成とビルド: PlatformIO IDE for VSCode
- USBシリアルポートドライバーがインストールされていること
- ユーザーにシリアルポートのアクセス権(dialou)が付与されていること
UDPパケット送出I/F
- インターネットプロトコル: UDP (ボート番号: 2222)
- 指定された内部ネットワークにブロードキャスト: 192.168.0.255
受信するIPアドレスの機器 (192.168.0.1〜192.168.0.244) - 送出データ
- 約10分間隔でパケットを送出
- [データ形式] カンマ区切りのバイトデータ
- [送信内容] デバイス名, 外気温, 室内気温, 室内湿度, 室内気圧
- サーミスタ気温測定エラーの場合は、外気温に NaNを送出
使用ライブラリ
- BME280センサー: Adafruit BME280 Library
自作ライブラリ
- MCP3002アナログコンバーター処理クラス
1. プロジェクト作成
[プロジェクト名] udpsrv_weather_sensors
1-0. PlatformIO プロジェクトファイル
[env:esp_wroom_02]
platform = espressif8266
board = esp_wroom_02
framework = arduino
monitor_port = /dev/ttyUSB0
monitor_speed = 9600
upload_protocol = esptool
lib_deps = adafruit/Adafruit BME280 Library@^2.2.0
PlatformIO Homeからライブラリを追加すると .pio直下にライブラリのソースとサンプルがインストールされます。
~/udpsrv_weather_sensors/.pio/libdeps/esp_wroom_02/Adafruit BME280 Library$ tree
.
├── Adafruit_BME280.cpp
├── Adafruit_BME280.h
├── LICENSE
├── README.md
├── assets
│ └── board.jpg
├── examples
│ ├── advancedsettings
│ │ └── advancedsettings.ino
│ ├── bme280_unified
│ │ └── bme280_unified.ino
│ └── bme280test
│ └── bme280test.ino
└── library.properties
BME280ライブラリを使った処理は上記サンプル(bme280test.ino)をそのまま流用します。
※サンプルのコメントを除いたコードの抜粋を以下に示します
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define BME_SCK 13
#define BME_MISO 12
#define BME_MOSI 11
#define BME_CS 10
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BME280 bme; // I2C
//Adafruit_BME280 bme(BME_CS); // hardware SPI
//Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK); // software SPI
unsigned long delayTime;
void setup() {
Serial.begin(9600);
while(!Serial); // time to get serial running
Serial.println(F("BME280 test"));
unsigned status;
// default settings
status = bme.begin();
// You can also pass in a Wire library object like &Wire2
// status = bme.begin(0x76, &Wire2)
if (!status) {
Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
Serial.print("SensorID was: 0x"); Serial.println(bme.sensorID(),16);
Serial.print(" ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
Serial.print(" ID of 0x56-0x58 represents a BMP 280,\n");
Serial.print(" ID of 0x60 represents a BME 280.\n");
Serial.print(" ID of 0x61 represents a BME 680.\n");
while (1) delay(10);
}
Serial.println("-- Default Test --");
delayTime = 1000;
Serial.println();
}
void loop() {
printValues();
delay(delayTime);
}
void printValues() {
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" °C");
Serial.print("Pressure = ");
Serial.print(bme.readPressure() / 100.0F);
Serial.println(" hPa");
Serial.print("Approx. Altitude = ");
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.print("Humidity = ");
Serial.print(bme.readHumidity());
Serial.println(" %");
Serial.println();
}
1-1. ヘッダー定義
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include "SimpleMCP3002.h"
1-2. 定数定義
- データ送出間隔: 600秒 (10分)
- ネットワーク情報
- Wi-FiルータのSSIDとパスワード
- ESP気象センサーに内部ネットワークの固定アドレスを付与 (例) 192.168.0.31
- ゲートウェイ、ネットマスク、ブロードキャストアドレス
- ESP気象センサーのデバイス名: "esp8266_1" ※気象テーブルの主キーを構成
- アナログコンバーター用の各種定数
- サーミスタの温度計算用各種定数
// Loop total delay time
const int32_t SLEEP_T = 600 * 1000000; // micro second, 600 sec (10 minute)
// Wi-Fi section
const char *SSID = "mywifi-router";
const char *PASS = "0123456789abc";
const IPAddress IP_DEVICE = IPAddress(192, 168, 0, 31);
const IPAddress IP_GATEWAY = IPAddress(192, 168, 0, 1);
const IPAddress IP_NETMASK = IPAddress(255, 255, 255, 0);
// UDP section
const char *SENDTO = "192.168.0.255"; // BroadCast
const uint16_t PORT = 2222;
// DEVICE NAME
const char *DEVICE = "esp8266_1,";
// MCP3002: channel=0,1
const uint8_t ADC_SAMPLES = 10;
const uint8_t CHANNEL_THERM = 0;
const float ADC_VREF = 3.3;
// Thermister section
const float THERM_V = 3.3;
const float THERM_B = 3435.0;
const float THERM_R0 = 10000.0;
const float THERM_R1 = 10000.0; // Divide register 10k
const float THERM_READ_INVALID = -9999.0;
1-3. オブジェクト生成
- アナログコンバーター処理オブジェクト
- BME280センサー処理オブジェクト
SimpleMCP3002 adc(ADC_VREF);
Adafruit_BME280 bme;
1-4. デバック用のADC取得配列出力関数
※ DEBUG ビルドの場合有効になる
void debugSerialOut(ReadData (&datas)[ADC_SAMPLES]) {
int len = sizeof(datas) / sizeof(datas[0]);
Serial.print("ReadData: [");
for (int i = 0; i < len; i++) {
ReadData rd = datas[i];
char chrbuf[5];
Serial.print("[0x");
sprintf(chrbuf, "%x", rd.highByte);
Serial.print(chrbuf);
Serial.print(",0x");
sprintf(chrbuf, "%x", rd.lowByte);
Serial.print(chrbuf);
Serial.print("]");
if (i < ADC_SAMPLES) {
Serial.print(",");
}
}
Serial.print("]");
}
1-5. ADCサンプリング関数: 10回の平均値取得
float getSamplingAdcValue(uint8_t ch, SimpleMCP3002 &mcp) {
int adcSamples[ADC_SAMPLES];
#if defined(DEBUG)
ReadData datas[ADC_SAMPLES];
#endif
int i;
Serial.print("CH<");
Serial.print(ch);
Serial.print(">: [");
for (i = 0; i < ADC_SAMPLES; i++) {
adcSamples[i] = mcp.analogRead(ch);
#if defined(DEBUG)
datas[i] = mcp.getReadData();
#endif
Serial.print(adcSamples[i]);
if (i < (ADC_SAMPLES - 1)) {
Serial.print(",");
}
delay(10);
}
Serial.print("], mean adc: ");
uint16_t adcTotal = 0;
for (i = 0; i < ADC_SAMPLES; i++) {
adcTotal += adcSamples[i];
}
uint16_t meanAdc = round(1.0 * adcTotal / ADC_SAMPLES);
Serial.println(meanAdc);
#if defined(DEBUG)
debugSerialOut(datas)
#endif
return meanAdc;
}
1-6. サーミスタ気温取得関数
float getThermTemp() {
double rx, xa, temp;
uint16_t adcVal = getSamplingAdcValue(CHANNEL_THERM, adc);
float outVolt = adc.getVolt(adcVal);
Serial.print("Therm.outVolt: ");
Serial.print(outVolt, 3);
Serial.println(" V ");
if (adcVal == 0 || outVolt < 0.01) {
Serial.println("Thermister read invalid!");
return THERM_READ_INVALID;
}
rx = THERM_R1 * ((THERM_V - outVolt) / outVolt);
xa = log(rx / THERM_R0) / THERM_B;
temp = (1 / (xa + 0.00336)) - 273;
Serial.print("rx: ");
Serial.print(rx);
Serial.print(", xa: ");
Serial.println(xa, 6);
return (float)temp;
}
1-7. センサー測定値取得処理とUDPパケット生成・送出関数
- サーミスタ気温取得
※取得処理の詳細と計算式については前回Qiita投稿をご覧ください。 - BME280センサー測定値取得
※各測定値取得は上記サンプルコードそのまま流用 - UDPパケットの生成と送出
- WiFi用UDPオブジェクト生成
- ポート番号設定
- UDPパケットの開始設定
- UDPパケット送出I/Fに沿って各測定値をカンマ区切りで設定
- UDPパケットの送出
void measureSensors() {
// Measure Thermister (Outdoor temparature)
float thermTemp = getThermTemp();
Serial.print("Therm Temp:");
Serial.println(thermTemp, 1);
// Wait BME280
delay(50);
// Measure BME280 senser (Room)
float bmeTemp = bme.readTemperature();
float humid = bme.readHumidity(); // %
float pressure = bme.readPressure() / 100.0F; // hPa
Serial.print("BME Temp:");
Serial.println(bmeTemp, 1);
// UDP Broadcast
WiFiUDP udp;
udp.begin(PORT);
delay(100);
udp.beginPacket(SENDTO, PORT);
udp.print(DEVICE);
if (thermTemp != THERM_READ_INVALID) {
udp.print(thermTemp, 1);
} else {
udp.print(NAN);
}
udp.print(",");
udp.print(bmeTemp, 1);
udp.print(",");
udp.print(humid, 1);
udp.print(",");
udp.print(pressure, 1);
udp.endPacket();
Serial.println("UDP End Packet.");
}
1-8. セットアップ関数
各種オブジェクトの初期化、測定値取得・UDPパケット送出、ディープスリープ迄の一連の処理を関数内で処理を完結させます。
1-8-1. 各種オブジェクトの初期化処理
- アナログコンバーター処理オブジェクトの初期化
- BME280センサー処理オブジェクトの初期化
※上記サンプルの初期化処理をそのまま流用 - Wi-Fiネットワーク接続設定
- アナログコンバーターの立ち上がりを待つ (50ミリ秒)
void setup() {
unsigned long starttime = millis();
Serial.begin(9600);
Serial.println("Start.");
adc.begin();
bool status;
status = bme.begin(0x76);
if (!status) {
Serial.println("Could not find a valid BME280 sensor!");
while (1)
;
}
WiFi.mode(WIFI_STA); // Wi-Fi Station
WiFi.config(IP_DEVICE, IP_GATEWAY, IP_NETMASK); //, IP_PRIMARY_DNS);
WiFi.begin(SSID, PASS);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
Serial.println(WiFi.localIP());
// Wait MCP3002
delay(50);
Serial.print("Setup complete: ");
Serial.print((millis() - starttime) / 1000);
Serial.println(" milli sec");
1-8-2. 測定値の送出とディープスリーブ
- センサーから取得した測定値からUDPパケットを生成しネットワークにブロードキャスト
- Deep Sleep モードに遷移する
DeepSleep時間 = 10分 - (開始時間 + 処理時間)
// Mesure Sensors
measureSensors();
delay(200); // UDP sending wait
// Calc Deep sleep time
uint64_t sleeptime = SLEEP_T - (millis() - starttime) * 1000;
Serial.print("Deep sleep time: ");
Serial.print(sleeptime / 1000);
Serial.println(" milli sec");
ESP.deepSleep(sleeptime, WAKE_RF_DEFAULT);
while (1) {
delay(100); // 100ms Deep sleep wait
}
}
1-9. ループ関数は空 ※実装無し
void loop() {
}
2. ESP気象センサーの動作確認
Raspberry Pi Zero から UART接続で ESP気象センサーの動作を確認する
下記は、センサーデータの取得と間欠運転 (Deep Sleep) が正常に動作していることを確認しているところ
2-1.シリアルモニタープログラム
python 仮想環境に pyserial ライブラリをインストールします
(py37_pigpio) $ pip install pyserial
import argparse
import time
# Adafruit-Blinkaをインストールするとpyserialもインストールされる
# Adafruitライブラリを入れていない場合は単独でインストールする (pip install pyserial)
import serial
from log import logsetting
"""
ESP気象測定モジュールのシリアル出力をモニタする
【条件】
raspi-config
(1)シリアル通信: 有効
(2)シリアルコンソール: 無効
【接続】 UART (Universal Asynchronous Receiver Transmitter)
[ラズパイ側] [ESP-WROOM-02]
pin pin
GPIO14(TxD) 10 <--> RXD 11
GPIO15(RxD) 12 <--> TXD 12
【シリアルデバイス】
OK: serial0: /dev/ttyAMA0 # Blutooth
(NG: serial1: /dev/ttyS0)
"""
logger = logsetting.create_logger("main_app")
SERIAL_DEV = "/dev/ttyAMA0"
DEFAULT_BAUD_RATE = 9600 # ESP側のレートに合わせる
def monitor(ser):
while 1:
line = ser.readline() # bytes
line = line.decode('ascii', errors='ignore')
line = line.replace("\r\n", "")
line.rstrip()
logger.debug(line)
time.sleep(1.0)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--baudrate", type=int, default=9600, choices=[9600, 115200], help="Baudrate (bps).")
args = parser.parse_args()
logger.info(args)
ser = serial.Serial(SERIAL_DEV, args.baudrate) #, timeout=None)
try:
monitor(ser)
except KeyboardInterrupt:
ser.close()
こちらは ESP気象センサーのリリース時にラズパイで採取したログ出力 ※2021年09月12日
2021-09-12 16:24:46 INFO SerialMonitor.py(44)[<module>] Namespace(baudrate=9600)
2021-09-12 16:25:10 DEBUG SerialMonitor.py(36)[monitor] @i4dhdStart.
2021-09-12 16:25:14 DEBUG SerialMonitor.py(36)[monitor] 192.168.0.31
2021-09-12 16:25:15 DEBUG SerialMonitor.py(36)[monitor] Setup complete: 4 milli sec
2021-09-12 16:25:16 DEBUG SerialMonitor.py(36)[monitor] CH<0>: [512,512,512,513,512,513,513,512,512,513], mean adc: 512
2021-09-12 16:25:17 DEBUG SerialMonitor.py(36)[monitor] Therm.outVolt: 1.652 V
2021-09-12 16:25:18 DEBUG SerialMonitor.py(36)[monitor] rx: 9980.47, xa: -0.000001
2021-09-12 16:25:19 DEBUG SerialMonitor.py(36)[monitor] Therm Temp:25.4
2021-09-12 16:25:20 DEBUG SerialMonitor.py(36)[monitor] BME Temp:25.9
2021-09-12 16:25:21 DEBUG SerialMonitor.py(36)[monitor] UDP End Packet.
2021-09-12 16:25:22 DEBUG SerialMonitor.py(36)[monitor] Deep sleep time: 595497 milli sec
2021-09-12 16:34:54 DEBUG SerialMonitor.py(36)[monitor] @ihIhdhStart.
2021-09-12 16:34:58 DEBUG SerialMonitor.py(36)[monitor] 192.168.0.31
2021-09-12 16:34:59 DEBUG SerialMonitor.py(36)[monitor] Setup complete: 4 milli sec
2021-09-12 16:35:00 DEBUG SerialMonitor.py(36)[monitor] CH<0>: [449,449,449,449,447,448,447,448,449,449], mean adc: 448
2021-09-12 16:35:01 DEBUG SerialMonitor.py(36)[monitor] Therm.outVolt: 1.445 V
2021-09-12 16:35:02 DEBUG SerialMonitor.py(36)[monitor] rx: 12834.82, xa: 0.000073
2021-09-12 16:35:03 DEBUG SerialMonitor.py(36)[monitor] Therm Temp:19.0
2021-09-12 16:35:04 DEBUG SerialMonitor.py(36)[monitor] BME Temp:26.0
2021-09-12 16:35:05 DEBUG SerialMonitor.py(36)[monitor] UDP End Packet.
2021-09-12 16:35:06 DEBUG SerialMonitor.py(36)[monitor] Deep sleep time: 595494 milli sec
リリース後のメンテナンス状況
バッテリー駆動の連続稼働日数 (充電式EVOLTA4本)
-
初回(2021年9月から): 約45日間動作
※試験運用なので電池切れでパケットがこなくなるまで稼働させた
※UDPパケットが15分以上遅延するとラズパイ側のシステムがGmailで遅延報告メールを送信するようにしている。 - 2022年01月から本番運用開始
※2022年から2023年は約40日で交換 (2年間で約 22回交換) - 2024年02月現在: 35日で交換
本番運用時のトラブル対応
ラズパイからUART経由で ESP気象センサーに接続しシリアルログを確認する
※Wi-Fiが原因のトラブルがありうるので UART経由で接続できるようにしておくことが重要
本番運用開始からのトラブル
- 電池を交換してから数日しか立っていないのにUDPパケットが届かない
ラズパイからUART接続しシリアルログを確認したところ正常に動作していた
[原因] Wi-Fiルータの障害 ※ルータの再起動で解決
今回紹介した ESP気象センサーのソースコードは下記GitHubで公開しています。
GitHub pipito-yuko: home_weather_sensors/esp_wroom_02
platformio.ini
src/
SimpleMCP3002.cpp
SimpleMCP3002.h
main.cpp