0
0

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

Last updated at Posted at 2024-08-28

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

dog.drawio.png

ESP32(micropython)

ME280の温度、湿度、気圧をMQTTで送信,画像FTPでUP(ESP32 WROVER DEV CAM)

mqtt_bme280_pub_ftp_spi.py
'''
 @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()

ブラウザ(HTML JS)

index.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>

結果



0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0