ESP32 WROVER CAMとBME280 > 画像と気温をブラウザで見る

ESP32 WROVERのカメラ付きで部屋の様子を監視しその画像をレンタルサーバーにFTPで投げます。同時にBME280で計測した気温などをMQTTで公衆MQTTブローカーに投げます。そしてブラウザでレンタルサーバーの画像を表示し、その下に公衆ブローカーからのWEBソケット通信(WSS://)で気温などを表示します。ランニングコストはほぼ0円。応用次第で低コストでIOTシステムを構築できます。

![dog.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806156/bb4f289f-e0a6-86dc-8ad1-e6dbb47a6c88.png)

# ESP32(micropython)
ME280の温度、湿度、気圧をMQTTで送信,画像FTPでUP(ESP32 WROVER DEV CAM)
<details><summary>mqtt_bme280_pub_ftp_spi.py</summary>

```python
'''
 @file mqtt_bme280_pub_ftp_spi.py
 @brief BME280の温度、湿度、気圧をMQTTで送信,画像FTPでUP(ESP32 WROVER DEV CAM)
 @details MQTT over SSL/TLS（ポート8883）を使用、SPI通信
'''
import network
import utime
import camera
import gc
import time
import io
from ftplib import FTP
import _thread
from machine import Timer, SPI, Pin
import ussl
import usocket
from umqtt.robust import MQTTClient

# Wi-Fiの設定
SSID = "XXXXXXXXXXX"
PASSWORD = "XXXXXXXXXXX"

# FTPサーバーの設定
FTP_SERVER = 'XXXXXXXXXXX'
FTP_USERNAME = 'XXXXXXXXXXX'
FTP_PASSWORD = 'XXXXXXXXXXX'
FTP_PATH = '/XXXXXXXXXXX/'

# ブローカーの設定
host = 'XXXXXXXXXXX'
port = 8883  # SSLポート
topics = {
    'temperature': b'sensor/temperature',
    'humidity': b'sensor/humidity',
    'pressure': b'sensor/pressure'
}

# Wi-Fiに接続
def wifi_connect():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        print('connecting to network...')
        wlan.connect(SSID, PASSWORD)
    start = utime.time()
    while not wlan.isconnected():
        utime.sleep(1)
        if utime.time() - start > 10:
            print("connect timeout!")
            return False
    if wlan.isconnected():
        print('network config:', wlan.ifconfig())
        return True
    return False

if not wifi_connect():
    raise Exception("Failed to connect to Wi-Fi")

# SPI通信の設定
spi = SPI(1, baudrate=100000, polarity=0, phase=0, sck=Pin(14), mosi=Pin(13), miso=Pin(12))
cs = Pin(15, Pin.OUT)
cs.value(1)  # デフォルトは非選択状態

# BME280の初期化と補正データの読み込み
# グローバル変数の初期化
t_fine = 0.0
digT = []
digP = []
digH = []

# BME280のレジスタへの書き込み
def write_reg(reg, value):
    cs.value(0)
    spi.write(bytearray([reg & 0x7F, value]))
    cs.value(1)

# BME280のレジスタからの読み込み
def read_reg(reg, length):
    cs.value(0)
    spi.write(bytearray([reg | 0x80]))
    data = spi.read(length)
    cs.value(1)
    return data

# BME280の初期設定を行う関数
def init_bme280():
    write_reg(0xF2, 0x01)
    write_reg(0xF4, 0x27)
    write_reg(0xF5, 0xA0)

# BME280から補正データを読み込む関数
def read_compensate():
    global digT, digP, digH
    dat_t = read_reg(0x88, 6)
    digT = [
        (dat_t[1] << 8) | dat_t[0],
        (dat_t[3] << 8) | dat_t[2],
        (dat_t[5] << 8) | dat_t[4]
    ]
    for i in range(1, 3):
        if digT[i] >= 32768:
            digT[i] -= 65536

    dat_p = read_reg(0x8E, 18)
    digP = [
        (dat_p[1] << 8) | dat_p[0],
        (dat_p[3] << 8) | dat_p[2],
        (dat_p[5] << 8) | dat_p[4],
        (dat_p[7] << 8) | dat_p[6],
        (dat_p[9] << 8) | dat_p[8],
        (dat_p[11] << 8) | dat_p[10],
        (dat_p[13] << 8) | dat_p[12],
        (dat_p[15] << 8) | dat_p[14],
        (dat_p[17] << 8) | dat_p[16]
    ]
    for i in range(1, 9):
        if digP[i] >= 32768:
            digP[i] -= 65536

    digH = [read_reg(0xA1, 1)[0]]
    dat_h = read_reg(0xE1, 7)
    digH.extend([
        (dat_h[1] << 8) | dat_h[0],
        dat_h[2],
        (dat_h[3] << 4) | (0x0F & dat_h[4]),
        (dat_h[5] << 4) | ((dat_h[4] >> 4) & 0x0F),
        dat_h[6]
    ])
    if digH[1] >= 32768:
        digH[1] -= 65536
    for i in range(3, 5):
        if digH[i] >= 32768:
            digH[i] -= 65536
    if digH[5] >= 128:
        digH[5] -= 256

# 測定データを読み込み、補正する関数
def read_data():
    try:
        dat = read_reg(0xF7, 8)
        dat_p = (dat[0] << 12) | (dat[1] << 4) | (dat[2] >> 4)
        dat_t = (dat[3] << 12) | (dat[4] << 4) | (dat[5] >> 4)
        dat_h = (dat[6] << 8) | dat[7]

        tmp = bme280_compensate_t(dat_t)
        prs = bme280_compensate_p(dat_p)
        hum = bme280_compensate_h(dat_h)

        return tmp, prs, hum
    except Exception as e:
        print("Error reading BME280 data:", e)
        return None, None, None

# 温度データを補正する関数
def bme280_compensate_t(adc_T):
    global t_fine
    var1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1]
    var2 = ((adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0)) * digT[2]
    t_fine = var1 + var2
    T = t_fine / 5120.0
    return T

# 気圧データを補正する関数
def bme280_compensate_p(adc_P):
    global t_fine
    var1 = t_fine / 2.0 - 64000.0
    var2 = var1 * var1 * digP[5] / 32768.0
    var2 = var2 + var1 * digP[4] * 2.0
    var2 = var2 / 4.0 + digP[3] * 65536.0
    var1 = (digP[2] * var1 * var1 / 524288.0 + digP[1] * var1) / 524288.0
    var1 = (1.0 + var1 / 32768.0) * digP[0]
    if var1 == 0:
        return 0
    P = 1048576.0 - adc_P
    P = (P - var2 / 4096.0) * 6250.0 / var1
    var1 = digP[8] * P * P / 2147483648.0
    var2 = P * digP[7] / 32768.0
    P = P + (var1 + var2 + digP[6]) / 16.0
    return P / 100

# 湿度データを補正する関数
def bme280_compensate_h(adc_H):
    global t_fine
    var_H = t_fine - 76800.0
    var_H = (adc_H - (digH[3] * 64.0 + digH[4] / 16384.0 * var_H)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_H * (1.0 + digH[2] / 67108864.0 * var_H)))
    var_H = var_H * (1.0 - digH[0] * var_H / 524288.0)
    if var_H > 100.0:
        var_H = 100.0
    elif var_H < 0.0:
        var_H = 0.0
    return var_H

# カメラの初期化
def camera_init():
    camera.deinit()
    gc.collect()  # メモリの最適化
    camera.init(0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
                format=camera.JPEG, framesize=camera.FRAME_VGA,  # 解像度をVGAに設定
                xclk_freq=camera.XCLK_10MHz,
                href=23, vsync=25, reset=-1, pwdn=-1,
                sioc=27, siod=26, xclk=21, pclk=22, fb_location=camera.PSRAM)
    camera.framesize(camera.FRAME_VGA)  # 解像度をVGAに設定
    camera.flip(1)
    camera.mirror(1)
    camera.saturation(0)
    camera.brightness(0)
    camera.contrast(0)
    camera.quality(20)  # 画質を低めに設定
    camera.speffect(camera.EFFECT_NONE)
    camera.whitebalance(camera.WB_NONE)
    gc.collect()  # メモリの最適化

# FTPアップロード
def upload_to_ftp(file_data, file_name):
    try:
        ftp = FTP(FTP_SERVER)
        ftp.login(FTP_USERNAME, FTP_PASSWORD)
        ftp.cwd(FTP_PATH)
        
        # FTPにバイトストリームとして送信
        ftp.storbinary('STOR ' + file_name, file_data)
        
        ftp.quit()
        print('Image uploaded successfully')
    except Exception as e:
        print('FTP upload failed:', e)
    gc.collect()

def capture_and_upload():
    gc.collect()  # メモリの最適化
    try:
        # 画像をキャプチャ
        buf = camera.capture()

        # バイトストリームに変換
        if buf:
            file_data = io.BytesIO(buf)
            # FTPでアップロードを実行
            upload_to_ftp(file_data, 'image.jpg')

        del buf
        gc.collect()  # メモリの最適化
    except Exception as e:
        print('Error capturing or uploading image:', e)

# SSL/TLS設定（証明書検証を無効にする）
def ssl_wrap_socket(sock, server_hostname):
    return ussl.wrap_socket(sock, server_hostname=server_hostname)

# MQTTクライアントの設定
client_id = "picow"
mqtt = MQTTClient(client_id, host, port=port, ssl=True, ssl_params={'server_hostname': host})

# MQTT接続の試行
try:
    mqtt.connect()
except Exception as e:
    print("MQTT connection failed:", e)
    raise

# BME280の初期化
init_bme280()
read_compensate()

# カメラの初期化
camera_init()

# タスクを交互に実行するタイマーの設定
def alternate_tasks(timer):
    global last_task
    gc.collect()  # メモリの最適化
    if last_task == "capture":
        # 次のタスクはデータの読み取りと送信
        last_task = "read"
        try:
            temperature, pressure, humidity = read_data()
            if temperature is not None and pressure is not None and humidity is not None:
                mqtt.publish(topics['temperature'], str(temperature).encode())
                mqtt.publish(topics['humidity'], str(humidity).encode())
                mqtt.publish(topics['pressure'], str(pressure).encode())
        except Exception as e:
            print('Error reading or publishing data:', e)
    else:
        # 次のタスクは画像のキャプチャとアップロード
        last_task = "capture"
        capture_and_upload()

# 最後に実行したタスクを記録する変数
last_task = "capture"

# 10秒ごとにタスクを交互に実行するタイマーを設定
task_timer = Timer(-1)
task_timer.init(period=10000, mode=Timer.PERIODIC, callback=alternate_tasks)

# メインループ（何もしない）
while True:
    time.sleep(1)
    gc.collect()

```

</details>


# ブラウザ(HTML JS)
<details><summary>index.html</summary>

```html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 Camera and Sensor Data</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin-top: 50px;
        }
        img {
            border: 2px solid #000;
            max-width: 100%;
            height: auto;
        }
        .sensor-data {
            margin: 20px auto;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            max-width: 600px;
        }
        .sensor-data h2 {
            margin-top: 0;
        }
    </style>
    <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
    <script>
        function refreshImage() {
            var image = document.getElementById("cameraImage");
            var timestamp = new Date().getTime();
            image.src = "image.jpg?t=" + timestamp;
        }

        setInterval(refreshImage, 30000); // 10秒ごとに画像を更新

        // MQTT接続情報
        const options = {
            connectTimeout: 4000,
            clientId: 'mqtt_js_' + Math.random().toString(16).substr(2, 8),
            keepalive: 60,
            clean: true,
        };

        const host = 'wss://XXXXXXXXXXXX'; // セキュアWebSocket MQTTブローカー
        const topics = ['sensor/temperature', 'sensor/humidity', 'sensor/pressure'];

        const client = mqtt.connect(host, options);

        client.on('connect', () => {
            console.log('Connected to MQTT broker');
            client.subscribe(topics, (err) => {
                if (!err) {
                    console.log(`Subscribed to topics: ${topics.join(', ')}`);
                } else {
                    console.error(`Failed to subscribe: ${err}`);
                }
            });
        });

        client.on('message', (topic, message) => {
            const value = parseFloat(message.toString());
            if (topic === 'sensor/temperature') {
                document.getElementById('temperature').innerText = value.toFixed(2);
            } else if (topic === 'sensor/humidity') {
                document.getElementById('humidity').innerText = value.toFixed(2);
            } else if (topic === 'sensor/pressure') {
                document.getElementById('pressure').innerText = value.toFixed(2);
            }
        });

        client.on('error', (err) => {
            console.error('Connection error: ', err);
        });

        client.on('reconnect', () => {
            console.log('Reconnecting...');
        });

        client.on('offline', () => {
            console.log('Client is offline');
        });
    </script>
</head>
<body>
    <h1>ESP32 Camera Stream</h1>
    <img id="cameraImage" src="image.jpg" alt="ESP32 Camera Stream">
    <div class="sensor-data">
        <h2>Sensor Data</h2>
        <p>Temperature: <span id="temperature">N/A</span> °C</p>
        <p>Humidity: <span id="humidity">N/A</span> %</p>
        <p>Pressure: <span id="pressure">N/A</span> hPa</p>
    </div>
</body>
</html>

```
</details>

# 結果

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806156/53b34080-701f-e623-e512-e63011278033.jpeg">
<BR><BR>

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806156/355b2cbd-0cc3-a8a9-8c61-af7bdb41a9b9.jpeg">