ESP-WROOM-02 気象センサーの観測データをUDPパケットでブロードキャストする

　前回 ESP-WROOM-02 DIP化キットに接続しているサーミスターの温度をアナログコンバーターで取得する方法を紹介しました。
[ESP-WROOM-02 サーミスタの温度をアナログコンバーターで取得する (Qiita@pipito-yukio)](https://qiita.com/pipito-yukio/items/9a1bff2c9333d7351f5a)
　今回は下記のようなESP気象センサーに搭載された2つのセンサーの観測データをWi-Fi経由で定期的(約10分間隔)にUDPパケットで内部ネットワークにブロードキャストするプログラムについて解説します。

![ESP_sensorModuleOnly.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/cfe02e85-17ce-85bf-0ee2-af7042fdf7f8.jpeg)

### 搭載する機能

+ (1) 充電池駆動 (ニッケル水素電池 x 4: 4.8vをレギュレータで 3.3vに降圧)
+ (2) サーミスタによる外気温測定 (アナログコンバータMCP3002使用)
※実装方法の詳細については前回Qiita投稿記事をご覧ください。
+ (3) BME280 センサーによる室内気温・湿度・気圧の測定 (Adafruit ライブラリ使用)
+ (4) 2つのセンサーの測定値をUDPバケットとしてWi-Fi経由でブロードキャストする
+ (5) 指定時間ディープスリープする (10分 - 処理時間)


### 実装の元になった書籍の出典箇所

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/a53d865e-6e11-6914-6a56-1534f1f0acd5.jpeg">

搭載する機能(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マイコンのケチケチ運転術
    **ディープスリープによる間欠運転** 
+ ② IoT 開発スタートブック ESP32でクラウドにつなげる電子工作をはじめよう！
2019年8月24日 初　版　第1刷発行　株式会社技術評論社
    + 第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 プロジェクトファイル

```platformio.ini
[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**)をそのまま流用します。
※サンプルのコメントを除いたコードの抜粋を以下に示します
```cpp
#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. ヘッダー定義

```main.cpp
#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" ※気象テーブルの主キーを構成
+ アナログコンバーター用の各種定数
+ サーミスタの温度計算用各種定数

```cpp
// 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センサー処理オブジェクト


```cpp
SimpleMCP3002 adc(ADC_VREF);
Adafruit_BME280 bme;
```

#### 1-4. デバック用のADC取得配列出力関数

※ DEBUG ビルドの場合有効になる

```cpp
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回の平均値取得

```cpp
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. サーミスタ気温取得関数

```cpp
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パケットの送出

```cpp
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ミリ秒)

```cpp
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分 - (開始時間 + 処理時間) 

```cpp
  // 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. ループ関数は空 ※実装無し

```cpp
void loop() {
}
```

#### 2. ESP気象センサーの動作確認

Raspberry Pi Zero から UART接続で ESP気象センサーの動作を確認する

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/90edf244-246a-0354-3d79-9ee6d428c79e.jpeg">

　下記は、センサーデータの取得と間欠運転 (Deep Sleep) が正常に動作していることを確認しているところ 

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/4b4e197d-9514-364a-dda7-b019de3622ec.jpeg">


#### 2-1.シリアルモニタープログラム

python 仮想環境に pyserial ライブラリをインストールします

```bash
(py37_pigpio) $ pip install pyserial
```


```SerialMonitor.py
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日

```bash
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](https://github.com/pipito-yukio/home_weather_sensors/tree/master/esp_wroom_02/udpsrv_sensor_bme_thermister)

```
platformio.ini
src/
  SimpleMCP3002.cpp
  SimpleMCP3002.h
  main.cpp
```