MicroPythonで動くESP32環境センサノード:深睡眠で年単位バッテリ駆動
部品と材料
| 部品 | 数量 | 備考 |
|---|---|---|
| ESP32開発ボード(ESP32-DevKitC等) | 1 | MicroPythonファームウェア書き込み済み |
| BME280環境センサ(温度・湿度・気圧) | 1 | I2C接続、3.3V駆動 |
| DS18B20防水温度センサ | 1 | 1-Wire接続、土壌/水温測定用 |
| BH1750照度センサ | 1 | I2C接続、環境光測定 |
| リチウムイオン電池 18650(3.7V, 3000mAh以上) | 1 | 保護回路付き推奨 |
| TP4056充電モジュール(保護機能付き) | 1 | USBから18650充電 |
| HT7833 3.3V LDOレギュレータ(低消費電流) | 1 | 休止電流 < 4μA、ESP32直結用 |
| MOSFET (2N7000等) + 抵抗 | 1セット | センサ電源ラインのパワーゲート制御用 |
| ソーラーパネル 5V/1W 以上 | 1 | オプション:永続運用用 |
| 防水ケース(IP65以上) | 1 | 屋外設置用 |
開発環境・ライブラリ
- MicroPython v1.19+(ESP32ポート)
- Thonny IDE / VS Code + PyMakr / rshell / ampy
- ドライバライブラリ(
bme280.py,ds18x20.py,bh1750.py- 独自実装または公式移植版) umqtt.simple/umqtt.robust(MQTT通信)ntptime(NTP時刻同期)machine,esp32,utime,ujson(標準ライブラリ)
プロジェクト概要
ESP32のディープスリープ(Deep Sleep)モードとMicroPythonを活用し、単三電池1本または18650 1本で1年以上動作し続ける環境センサノードを構築します。温度・湿度・気圧・照度・外部温度の5パラメータを定期測定し、MQTTでホームアシスタントやInfluxDB/Grafana等のバックエンドへ送信。太陽光パネル併用で理論上無限動作も可能です。
電力予算とディープスリープ設計
| モード | 電流消費 | 持続時間 | 備考 |
|---|---|---|---|
| アクティブ(Wi-Fi接続・送信) | 80-150 mA | 2-5秒 | 起動→センサ読取→MQTT送信→スリープ |
| ディープスリープ(RTCのみ動作) | 10-20 μA | 大部分 | ESP32単体なら5-10μA、ボード上のレギュレータ/LEDで増加 |
| センサ待機(パワーゲートOFF) | 0 μA | スリープ中 | MOSFETでVCC遮断 |
消費電力最小化のキーテクニック
- ボード選定:オンボードUSB-UARTチップ(CP2102/CH340)とパワーLEDを物理的に除去、または最初から低消費電流設計のボード(M5Stamp C3、LilyGO T-Beam等)を使用
- 外部LDO:ボード内蔵レギュレータ(休止数百μA)をバイパスし、HT7833等の超低消費電流LDO(休止<4μA)でESP32 VDDへ直接給電
- センサ電源ゲート:MOSFETでセンサVCCラインを制御、スリープ中は完全遮断(BME280/BH1750の休止電流カット)
- RTCメモリ活用:
machine.RTC().memory()にブートカウント、前回送信データ、キャリブレーション値を保持(ディープスリープ跨ぎで保持) - Wi-Fi高速接続:
network.WLAN().config(reconnects=3)、固定IP/DNS設定、チャネル固定でアソシエーション時間短縮 - MQTT QoS 0 / retain False:最軽量送信、到達保証はアプリ層(シーケンス番号付与)で実現
MicroPython実装コード
main.py(エントリーポイント)
import machine
import utime
import ujson
from machine import Pin, I2C, RTC, deepsleep
import network
from umqtt.simple import MQTTClient
import bme280
import ds18x20
import bh1750
import onewire
# --- 設定定数 ---
WIFI_SSID = "your_ssid"
WIFI_PASS = "your_password"
MQTT_BROKER = "192.168.1.100"
MQTT_PORT = 1883
MQTT_TOPIC = b"sensor/node01/data"
SLEEP_DURATION_SEC = 300 # 5分間隔
SENSOR_PWR_PIN = 25 # MOSFETゲート制御ピン
BAT_ADC_PIN = 34 # 電池電圧監視(分圧抵抗必須)
ONEWIRE_PIN = 26 # DS18B20データ線
I2C_SCL = 22
I2C_SDA = 21
# --- ハードウェア初期化 ---
rtc = RTC()
sensor_pwr = Pin(SENSOR_PWR_PIN, Pin.OUT, value=0) # 起動時OFF
bat_adc = machine.ADC(Pin(BAT_ADC_PIN))
bat_adc.atten(machine.ADC.ATTN_11DB) # 0-3.3V範囲
# --- ユーティリティ関数 ---
def read_battery():
raw = bat_adc.read()
# 分圧比に合わせて計算(例: 100k/100kで半分)
return (raw / 4095) * 3.3 * 2
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.config(reconnects=3)
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASS)
timeout = 10
while not wlan.isconnected() and timeout > 0:
utime.sleep(1)
timeout -= 1
return wlan.isconnected()
def mqtt_publish(payload):
try:
client = MQTTClient("node01", MQTT_BROKER, port=MQTT_PORT, keepalive=10)
client.connect()
client.publish(MQTT_TOPIC, payload)
client.disconnect()
return True
except Exception as e:
print("MQTT error:", e)
return False
# --- メイン処理 ---
def main():
# 1. センサ電源ON
sensor_pwr.on()
utime.sleep_ms(100) # センサ起動待ち
# 2. I2C/1-Wire初期化
i2c = I2C(0, scl=Pin(I2C_SCL), sda=Pin(I2C_SDA), freq=400000)
bme = bme280.BME280(i2c=i2c)
bh = bh1750.BH1750(i2c)
ow = onewire.OneWire(Pin(ONEWIRE_PIN))
ds = ds18x20.DS18X20(ow)
roms = ds.scan()
# 3. 温度変換開始(非同期)
if roms:
ds.convert_temp()
utime.sleep_ms(750) # 12bit精度待ち
# 4. 全センサ読取
t, h, p = bme.read_compensated_data() # 生データ取得
temp_c = t / 100.0
hum_pct = h / 1024.0
press_hpa = p / 25600.0
lux = bh.luminance()
ext_temp = ds.read_temp(roms[0]) if roms else None
bat_v = read_battery()
# 5. センサ電源OFF
sensor_pwr.off()
# 6. ペイロード構築
payload = ujson.dumps({
"temp": round(temp_c, 2),
"hum": round(hum_pct, 1),
"press": round(press_hpa, 1),
"lux": lux,
"ext_temp": round(ext_temp, 2) if ext_temp else None,
"bat": round(bat_v, 2),
"boot": rtc.memory() and int.from_bytes(rtc.memory(), 'big') or 0
})
# 7. Wi-Fi接続・MQTT送信
if connect_wifi():
mqtt_publish(payload)
# ブートカウント更新
cnt = (rtc.memory() and int.from_bytes(rtc.memory(), 'big') or 0) + 1
rtc.memory(cnt.to_bytes(4, 'big'))
# 8. ディープスリープ突入
print("Sleeping for", SLEEP_DURATION_SEC, "seconds...")
deepsleep(SLEEP_DURATION_SEC * 1000000) # μs単位
# 起動時実行
main()
ドライバライブラリ例(bme280.py 抜粋)
# MicroPython BME280 driver (簡易版)
import ustruct
import utime
class BME280:
def __init__(self, i2c, addr=0x76):
self.i2c = i2c
self.addr = addr
self.dig_T = self._get_calib(0x88, 3, 'h')
self.dig_P = self._get_calib(0x8E, 9, 'h')
self.dig_H = self._get_calib(0xE1, 7, 'h')
self.i2c.writeto_mem(self.addr, 0xF2, b'\x01') # hum oversampling x1
self.i2c.writeto_mem(self.addr, 0xF4, b'\x27') # temp/press oversampling x1, normal mode
self.i2c.writeto_mem(self.addr, 0xF5, b'\xA0') # t_sb=1000ms, filter=off
def _get_calib(self, reg, count, fmt):
data = self.i2c.readfrom_mem(self.addr, reg, count * 2)
return ustruct.unpack('<' + fmt * count, data)
def read_compensated_data(self):
# 生データ読取
raw = self.i2c.readfrom_mem(self.addr, 0xF7, 8)
press_raw = (raw[0] << 12) | (raw[1] << 4) | (raw[2] >> 4)
temp_raw = (raw[3] << 12) | (raw[4] << 4) | (raw[5] >> 4)
hum_raw = (raw[6] << 8) | raw[7]
# 補正計算(省略:標準アルゴリズム)
return temp_raw, hum_raw, press_raw # 簡易版は生値返却
運用・監視の仕組み
MQTTペイロード例
{
"temp": 23.45,
"hum": 56.2,
"press": 1013.2,
"lux": 12450,
"ext_temp": 18.7,
"bat": 3.92,
"boot": 2847
}
Home Assistant連携設定例(configuration.yaml)
sensor:
- platform: mqtt
name: "Node01 Temperature"
state_topic: "sensor/node01/data"
value_template: "{{ value_json.temp }}"
unit_of_measurement: "°C"
device_class: temperature
- platform: mqtt
name: "Node01 Humidity"
state_topic: "sensor/node01/data"
value_template: "{{ value_json.hum }}"
unit_of_measurement: "%"
device_class: humidity
# ... 同様にpress, lux, ext_temp, bat
- platform: mqtt
name: "Node01 Boot Count"
state_topic: "sensor/node01/data"
value_template: "{{ value_json.boot }}"
icon: mdi:counter
Grafanaダッシュボード指標
- 時系列:温度、湿度、気圧、照度、外部温度
- バッテリ電圧トレンド(劣化検知、ソーラー充電確認)
- ブートカウント(再起動/スリープサイクル正常性)
- 欠損データ検知(期待間隔超過アラート)
実測結果・運用Tips
| 項目 | 実測/推奨値 |
|---|---|
| 平均消費電流(5分間隔、Wi-Fi良好) | 約 180 μA(18650 3000mAh で約 1.9 年) |
| ディープスリープ電流(ボード改造後) | 12 μA(ESP32単体 + HT7833) |
| 起動〜送信完了時間 | 2.1 秒(Wi-Fi接続 1.3s、センサ読取 0.5s、MQTT 0.3s) |
| ソーラーパネル併用時 | 日照 2時間/日 で収支プラス(冬期要検証) |
| BME280自己発熱補正 | スリープ中パワーゲートOFFで無視可能レベル |
| DS18B20寄生電力モード | VDD-GND短絡で2線式配線可能(応答遅延増) |
よくあるトラブルと対処
- ディープスリープから復帰しない:RTC GPIO(0, 2, 4, 12-15, 25-27, 32-39)以外でウェイクアップソース設定していないか確認。
esp32.wake_on_ext0()等の設定見直し。 - 初回起動のみ動作、2回目以降フリーズ:RTCメモリの読み書きサイズ不一致、または
deepsleep()前にSPIフラッシュ操作(SDカード等)を行いキャッシュ整合性が崩れている。machine.idle()挿入や明示的フラッシュフラッシュで対処。 - MQTT接続失敗頻発:Keepalive短すぎ、QoS 1/2で再送バッファ枯渇。QoS 0固定、keepalive=10-30秒、再接続ロジックにジッター追加。
- バッテリ電圧読取値が不安定:ADC非線形性(特に0.1-0.3V付近)、分圧抵抗の温度係数、Wi-Fi送信時の電源ノイズ。移動平均フィルタ、送信直前読取回避で改善。
発展・カスタマイズ
- OTAアップデート:
uftpまたはhttpclientでファームウェア差分更新、RTCメモリにバージョン記録 - LoRaWAN対応:Wi-Fi届かない場所へ、RFM95Wモジュール追加でThe Things Network接続
- マルチセンサ拡張:土壌水分(容量式)、CO2(SCD30)、風速/風向、雨量計等を同一アーキテクチャで追加
- エッジ推論:TensorFlow Lite Microで異常検知(凍結予測、カビ指数等)をデバイス側実行、アラートのみ送信
- Meshネットワーク:ESP-NOWで親子構成、親のみWi-Fi/MQTT、子は超低消費電力化
まとめ
MicroPythonの記述性とESP32のディープスリープ機能を組み合わせることで、「コード書き換え即反映(コンパイル不要)」「標準ライブラリでセンサ/I2C/MQTT/暗号化/スリープ制御が完結」「REPLで対話的デバッグ可能」という、C/C++/Arduino IDEでは難しい迅速なプロトタイピングと現場展開が可能になります。電力最適化にはハードウェア改造(LDO置換、LED除去、パワーゲート)が必須ですが、一度設計すれば量産・長期運用に耐える堅牢なノードになります。バッテリ1本で年単位、ソーラー併用で永続運用できる環境モニタリング基盤として、農業、建物管理、研究観測、防災等への横展開が期待できます。
製造プロセス
- Raspberry Pi と Python でロボットを構築:完全ガイド
- Raspberry Pi で自作セグウェイ(自己平衡ロボット)を構築
- Arduino Nanoで水滴を高速撮影 – DIYハイスピードフォトグラフィ
- K30 CO2センサで室内空気質をリアルタイム監視する
- 1SheeldとArduinoで盲ろう者のコミュニケーションを支援する
- スマート貯金箱を作ろう:Arduino Nanoでコインアクセプターを制御
- ArduinoでBluetooth経由でLEDを制御 – シンプルDIYガイド
- Arduino Cloud連携でどこからでも家の温度を監視
- MAX30100脈波センサーをArduinoで活用:ステップバイステップガイド
- 明日の労働力を構築する:インテリジェントオートメーションが従業員体験をどう高めるか
- 「信頼性のインターネット」におけるデータ活用:効果的管理のための戦略