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