
目次
- システム概要
- システムの特徴
- 使用機材
- 回路構成
- プログラムスケッチ
- ※MACアドレス取得について
- 実機構成
- 動作フロー
- 設定画面
- Discord異常通知
- Ambientデータ送信
- 専用基板
- 基板&配線サポート台座
- まとめ
- 参考動画
- 参考URL
この記事では、市販品に頼らず、自分で作れる温度見守り装置の作り方を、
農家目線・初心者向けに分かりやすく解説しています。
ビニールハウスや育苗室、保管庫などの温度を自動で測定し、
異常があればスマホに通知してくれる仕組みです。
想定は、
育苗期間等の短期間の測定
安価で部品点数少なくコスパ良く
特徴は、
18650リチウムイオン電池1本だけで動作
特別な電源工事は不要で、
電池を入れるだけですぐ使える
温度は自動でクラウド保存
設定温度を超えたらスマホにお知らせ
WiFi設定や温度変更はスマホ画面から簡単操作
といった、「現場で本当に使える」実用重視の構成になっています。
電子工作が初めての方でも取り組めるように、
配線図・プログラム・設定画面・実際の動作例まで順番に説明していますので、
「ハウスの温度を遠隔で見たい」
「夜中に見回りに行きたくない」
「市販品は高いし融通がきかない」
そんな方に特におすすめの内容です。
発展形として、
XIAO ESP32C3はバッテリー充電チップ内蔵のため
リチウムイオン電池の充放電をサポート
USB-C端子にソーラーパネルを接続すれば、
長期の運用が可能
220kΩ抵抗を2本追加する事で電圧を監視する事が出来、
電池切れを予想出来るようになります。
システム概要
(DS18B20 + Discord通知 + Ambient + Web設定)
システムの特徴
- DS18B20温度センサーによる温度測定
- 測定時のみセンサーへ給電(省電力設計)
- 設定温度超過時にDiscordへ自動通知
- Ambientへ温度データを自動送信
- DeepSleepによる長時間バッテリー駆動
- スマホから設定できるWeb設定画面
- 個体識別用MACアドレス管理
地温・気温測定が出来る温度センサーDS18B20

こちらで使用している温度センサーの特徴として
💧 防水・防湿(屋外向き)
- センサー本体がステンレス管に封入
- ケーブル出口も樹脂でモールドされているタイプが多い
- 水・湿気・結露OK
→ 育苗ハウス、貯蔵庫、屋外設置に強い
🧲 物理的に強い
- ステンレスプローブなので
- 土に挿せる
- 配管に密着させられる
- 氷水・水中でも使える
- 樹脂センサーより断然タフ
🆔 個体識別ができる
- 各センサーに64bit固有ID
- 1個のESP32C3に複数本接続可能
- 基板にはセンサー2本接続まで接続可能なので、
- プログラムコードを工夫すれば、地温気温の同時送信可能
👉 例
- ハウス内複数点
- 地温+気温をまとめて取得
🌡 高精度・デジタル出力
- 測定範囲:-55〜+125℃
- 精度:±0.5℃(-10〜+85℃)
- デジタル通信(1-Wire)なのでノイズに強い
- → ハウス内、モーター周り、長配線でも安定
まとめると
| 用 途 | 相性 |
| 育苗ハウス温度 | ◎ |
| 地温測定 | ◎ |
| 水温(タンク・用水) | ◎ |
| 貯蔵庫 | ◎ |
| 精密実験(0.1℃単位) | △ |
ひとことで言うと
- 「農家が自作IoT温度センサーを作るなら、まずこれ」
- 設置場所を選ばない使いやすさでイチオシのセンサーです😃
使用機材
- マイコン:Seeeduino XIAO ESP32C3 https://www.switch-science.com/products/8348
- 温度センサー:DS18B20 防水タイプ https://www.digikey.jp/ja/products/detail/evvo/DS18B20A20/27691899
- 電源:18650 リチウムイオン電池 https://x.gd/VSTqY
- 抵抗:4.7kΩ(プルアップ)、220Ω(LED)https://amzn.to/3M5taig
- ケース:防水ケース https://www.monotaro.com/p/8821/7175/
- ケーブルグランド https://amzn.to/4bs8viM
- 18650電池BOX https://amzn.to/4teedeo
- その他:LED、設定ボタン https://amzn.to/3M1Zcf4 https://amzn.to/3YVfKbB
- 基板:KiCadにて設計 JLCPCBに発注
回路構成
回路図
- R1:DS18B20の4.7kΩプルアップ抵抗
- R4:LED用抵抗220Ω
- J1,J2:DS18B20端子
- SW1:設定ボタン
- D1:設定モードLED
- J1,J2:DS18B20温度センサー端子
- J3:18650電池端子
- R2,R3:電圧測定用抵抗(今回は使用しません)

プログラムスケッチ
開発環境 Arduino IDE 2.3.7 追加インストール ESP32 by Espressif Systems(ボードマネージャ) ライブラリ OneWire DallasTemperature Ambient Discord_WebHook
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Discord_WebHook.h>
#include <Ambient.h>
#include <esp_sleep.h>
#include <esp_system.h>
// ===== Discord Webhook URL を (channel_id, token) に分解(チェック無し)=====
// URL形式: https://discord.com/api/webhooks/<id>/<token>
// 戻り値: 分解できたら true、できなければ false
bool splitDiscordWebhook(const String& url, String& outId, String& outToken) {
outId = "";
outToken = "";
int p = url.indexOf("/webhooks/");
if (p < 0) return false;
int start = p + String("/webhooks/").length();
int slash = url.indexOf('/', start);
if (slash < 0) return false;
outId = url.substring(start, slash);
outToken = url.substring(slash + 1);
outId.trim();
outToken.trim();
return (outId.length() > 0 && outToken.length() > 0);
}
#define CONFIG_BUTTON_PIN 10
#define LED_PIN 5
#define SENSOR_POWER_PIN 20
#define ONE_WIRE_PIN 4
Preferences preferences;
WebServer server(80);
WiFiClient client;
Discord_Webhook discord;
Ambient ambient;
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature sensors(&oneWire);
// ===== 設定値 =====
String kotaiID = "5";
float thresholdTemp = 28.0;
unsigned long sleepSec = 300;
String WIFI_SSID = "MySSID";
String WIFI_PASSWORD = "MyPASS";
String ambientUserKey = "MyUserKey";
String DISCORD_WEBHOOK = "https://discord.com/api/webhooks/1234567890/AbcdefghijklmnopQrstuvwxyZ";
bool ENABLE_DISCORD = true;
// 表示&devKey用(コロン無しMAC)
String MAC_ADDR;
void loadSettings() {
preferences.begin("config", true);
kotaiID = preferences.getString("kotaiID", kotaiID);
thresholdTemp = preferences.getFloat("threshold", thresholdTemp);
sleepSec = preferences.getULong("sleep", sleepSec);
WIFI_SSID = preferences.getString("ssid", WIFI_SSID);
WIFI_PASSWORD = preferences.getString("pass", WIFI_PASSWORD);
ambientUserKey = preferences.getString("ambient", ambientUserKey);
DISCORD_WEBHOOK = preferences.getString("discord", DISCORD_WEBHOOK);
ENABLE_DISCORD = preferences.getBool("enable_discord", ENABLE_DISCORD);
preferences.end();
}
void saveSettings() {
preferences.begin("config", false);
preferences.putString("kotaiID", kotaiID);
preferences.putFloat("threshold", thresholdTemp);
preferences.putULong("sleep", sleepSec);
preferences.putString("ssid", WIFI_SSID);
preferences.putString("pass", WIFI_PASSWORD);
preferences.putString("ambient", ambientUserKey);
preferences.putString("discord", DISCORD_WEBHOOK);
preferences.putBool("enable_discord", ENABLE_DISCORD);
preferences.end();
}
float measureTempC() {
digitalWrite(SENSOR_POWER_PIN, HIGH);
delay(80);
sensors.begin();
sensors.requestTemperatures();
delay(750);
float t = sensors.getTempCByIndex(0);
digitalWrite(SENSOR_POWER_PIN, LOW);
return t;
}
bool connectWiFi(uint32_t timeoutMs = 10000) {
// まれな不安定対策:一度OFF→STA
WiFi.mode(WIFI_OFF);
delay(50);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID.c_str(), WIFI_PASSWORD.c_str());
uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) {
delay(100);
}
return (WiFi.status() == WL_CONNECTED);
}
void wifiOff() {
WiFi.disconnect(false);
WiFi.mode(WIFI_OFF);
}
void goSleep() {
esp_sleep_enable_timer_wakeup(sleepSec * 1000000ULL);
esp_deep_sleep_start();
}
// ===== 設定画面(MACもコロン無し表示) =====
void handleRoot() {
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
"<style>body{font-family:sans-serif;padding:20px;background:#f9f9f9}"
"form{background:#fff;padding:20px;border-radius:10px;max-width:600px}"
".row{display:flex;align-items:center;margin-bottom:12px}"
".row label{width:180px;font-weight:bold}"
".row input{width:240px;padding:6px}"
".row input[type=checkbox]{width:auto;margin-left:0;transform:scale(1.4)}"
".disabled{background:#eee;color:#999}"
"</style>"
"<script>"
"function toggleDiscord(){"
" var cb = document.querySelector(\"input[name='enable_discord']\");"
" var w = document.getElementById('discord');"
" if(cb && w){"
" if(cb.checked){"
" w.disabled = false;"
" w.classList.remove('disabled');"
" }else{"
" w.disabled = true;"
" w.classList.add('disabled');"
" }"
" }"
"}"
"</script>"
"</head><body onload='toggleDiscord()'>";
html += "<h2>ESP32温度センサー設定</h2><form method='POST' action='/save'>";
html += "<div class='row'><label>MAC</label><div>" + MAC_ADDR + "</div></div>";
html += "<div class='row'><label>個体ID</label><input name='id' value='" + kotaiID + "'></div>";
html += "<div class='row'><label>通知閾値(℃)</label><input name='threshold' type='number' step='0.1' value='" + String(thresholdTemp) + "'></div>";
html += "<div class='row'><label>測定間隔(秒)</label><input name='sleep' type='number' value='" + String(sleepSec) + "'></div>";
html += "<div class='row'><label>WiFi SSID</label><input name='ssid' value='" + WIFI_SSID + "'></div>";
html += "<div class='row'><label>WiFi PASS</label><input name='pass' type='password' value='" + WIFI_PASSWORD + "'></div>";
html += "<div class='row'><label>Ambient UserKey</label><input name='ambient' value='" + ambientUserKey + "'></div>";
// Discord有効時は編集可、無効時はグレー&編集不可(初期状態)
html += "<div class='row'><label>Discord Webhook</label>"
"<input id='discord' name='discord' value='" + DISCORD_WEBHOOK + "' "
+ String(ENABLE_DISCORD ? "" : "disabled class='disabled'") +
"></div>";
html += "<div class='row'><label>Discord有効</label>"
"<input type='checkbox' name='enable_discord' value='1' onchange='toggleDiscord()'";
if (ENABLE_DISCORD) html += " checked";
html += "></div>";
html += "<input type='submit' value='保存'></form></body></html>";
server.send(200, "text/html", html);
}
void handleSave() {
kotaiID = server.arg("id");
thresholdTemp = server.arg("threshold").toFloat();
sleepSec = server.arg("sleep").toInt();
WIFI_SSID = server.arg("ssid");
WIFI_PASSWORD = server.arg("pass");
ambientUserKey = server.arg("ambient");
// disabled のときはブラウザが送らない → hasArgで保持
if (server.hasArg("discord")) {
DISCORD_WEBHOOK = server.arg("discord");
}
ENABLE_DISCORD = server.hasArg("enable_discord");
saveSettings();
server.send(200, "text/html; charset=UTF-8",
"<!DOCTYPE html><html><head><meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
"<style>"
"body{font-family:sans-serif;text-align:center;padding:40px;}"
".msg{font-size:32px;font-weight:bold;}"
"</style>"
"</head><body>"
"<div class='msg'>保存しました。<br>再起動します...</div>"
"</body></html>");
delay(500);
ESP.restart();
}
void startConfigPortal() {
digitalWrite(LED_PIN, HIGH);
// 複数台でも混乱しないAP名(MAC末尾)
String apSsid = "ESP32_Config_" + MAC_ADDR.substring(8); // 例: AABBCCDD
WiFi.mode(WIFI_AP);
WiFi.softAP(apSsid.c_str());
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.begin();
Serial.print("設定モード開始 AP SSID: ");
Serial.println(apSsid);
Serial.print("IP: ");
Serial.println(WiFi.softAPIP());
while (true) {
server.handleClient();
delay(10);
}
}
// ===== メイン =====
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
pinMode(SENSOR_POWER_PIN, OUTPUT);
digitalWrite(SENSOR_POWER_PIN, LOW);
// ★ MAC(WiFi.macAddress() / コロン無し)
WiFi.mode(WIFI_STA);
MAC_ADDR = WiFi.macAddress(); // "AA:BB:CC:DD:EE:FF"
MAC_ADDR.replace(":", ""); // "AABBCCDDEEFF"
Serial.print("MAC: ");
Serial.println(MAC_ADDR);
loadSettings();
if (digitalRead(CONFIG_BUTTON_PIN) == LOW) {
Serial.println("設定ボタン押下 → 設定モードへ");
startConfigPortal();
}
// 温度計測(省電力:センサー電源ONはこの瞬間だけ)
float temperature = measureTempC();
Serial.print("計測温度: ");
Serial.print(temperature);
Serial.println(" ℃");
bool sendToDiscord = false;
String msg;
if (temperature == -127.0) {
msg = "⚠️ DS18B20 read failed. Temp: -127.0°C";
sendToDiscord = true;
} else if (temperature > thresholdTemp) {
msg = "🔥 異常温度: " + String(temperature) + "°C (ID:" + kotaiID + ")";
sendToDiscord = true;
}
// Wi-Fi接続(必要なときだけON)
Serial.print("WiFi接続: ");
bool wifiOK = connectWiFi(10000);
Serial.println(wifiOK ? "OK" : "NG");
// ★ WiFi詳細表示
if (wifiOK) {
Serial.print("IP: ");
Serial.println(WiFi.localIP());
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
}
if (wifiOK) {
// Discord(異常時のみ)
if (ENABLE_DISCORD && sendToDiscord && DISCORD_WEBHOOK.length() > 0) {
Serial.print("Discord送信: ");
Serial.println(msg);
String cid, tok;
if (splitDiscordWebhook(DISCORD_WEBHOOK, cid, tok)) {
discord.begin(cid, tok);
discord.send(msg);
} else {
Serial.println("Discord送信: Webhook URL を分解できません");
}
} else {
Serial.println("Discord送信: なし");
}
// Ambient
if (ambientUserKey.length() > 0) {
char devKey[32], writeKey[20];
strncpy(devKey, MAC_ADDR.c_str(), sizeof(devKey) - 1);
devKey[sizeof(devKey) - 1] = '\0';
unsigned int channelId;
Serial.print("Ambient送信: ");
if (!ambient.getchannel(ambientUserKey.c_str(), devKey, channelId, writeKey, sizeof(writeKey), &client)) {
Serial.println("NG(チャンネル情報取得失敗)");
Serial.print("devKey=");
Serial.println(devKey);
} else {
// writeKey掃除(NULL終端保証 → sizeofベース)
writeKey[sizeof(writeKey) - 1] = '\0';
for (int i = 0; i < (int)sizeof(writeKey); i++) {
if (writeKey[i] == '}' || writeKey[i] == '"') writeKey[i] = '\0';
}
ambient.begin(channelId, writeKey, &client);
ambient.set(1, temperature);
Serial.print("channelId=");
Serial.println(channelId);
Serial.print("writeKey=");
Serial.println(writeKey);
Serial.println(ambient.send() ? "OK" : "NG(送信失敗)");
}
} else {
Serial.println("Ambient送信: なし(UserKey未設定)");
}
}
wifiOff();
digitalWrite(LED_PIN, LOW);
goSleep();
}
void loop() {}※MACアドレス取得について
Ambient公式サイト 複数台のIoT端末を同一プログラムで動かすhttps://ambidata.io/docs/getchannel/
こちらに書かれているコード、環境(Arduino-ESP32 core の版)によって
esp_read_mac() と ESP_MAC_WIFI_STA が存在しないことがあって、
そこでコケます。
👉 Arduino環境やPCが変わると
👉 「未定義エラー」になることがあります。
実際に、
'esp_read_mac' was not declared in this scope
のようなエラーが出る場合があります。
一番ラクで安定なのは、ESP-IDF依存のAPIを使わず
WiFi.macAddress() で取得する方法です。コロン無しにも簡単にできます。
✅ 基本形(コロン付き)
まずは通常のMAC取得:
WiFi.mode(WIFI_STA);
String mac = WiFi.macAddress();
これで、
AA:BB:CC:DD:EE:FF
という形式でMACアドレスが取得できます。
✅ コロン無しに変換する方法
AmbientのdevKeyや識別用には
AABBCCDDEEFF
のようなコロン無し形式が便利です。
これは replace() を使うだけで簡単にできます。
こちらの部分↓
WiFi.mode(WIFI_STA); // 先にSTAにしておくと安定
MAC_ADDR = WiFi.macAddress(); // 例 "AA:BB:CC:DD:EE:FF"
MAC_ADDR.replace(":", ""); // 例 "AABBCCDDEEFF"AmbientでのMAC アドレス登録画面
ESP32C3のMACアドレスは、Ambientでデバイスキーとして使用されて、
それぞれのチャンネルに対応付けされます。

実機構成
防水ケース内部構成
- ESP32基板固定
- 18650バッテリー内蔵
- センサーケーブル引き出し
- 動作LED確認窓
- 防水DS18B20温度プローブ
- ハウス内や倉庫へ設置可能

動作フロー
起動後、以下の流れで自動動作します。

DeepSleep後は、設定した測定間隔で起動して温度測定を繰り返す
設定画面
設定ボタンを押しながら電源を入れると
ESP32がアクセスポイントとして起動します。
SSID例: ESP32_Config_XXXXXX
スマホから接続し、以下の設定が可能です。

⚫︎MACアドレス(個体識別)
測定データをグラフ表示するために必要な項目
⚫︎個体ID
通知メッセージに付加してどの温度センサーか識別します
⚫︎温度閾値
通知する温度設定
⚫︎測定間隔
測定間隔を秒で指定
⚫︎WiFi設定
SSIDとパスワードを設定
⚫︎Ambient UserKey
Ambientユーザーキー
⚫︎Discord Webhook
温度通知に使用するURL
⚫︎Discord通知ON/OFF
温度通知ON OFFのチェック
チェックを外すと通知しなくなります
保存完了画面

保存後、自動的に再起動します。
Discord異常通知
以下の場合に自動通知されます。
- 温度が通知閾値を超過した場合
- センサーエラー発生時
- -127℃エラー 「-127 °C」 が出る場合は、ほぼ確実に
👉 センサーと通信できていない(検出失敗) という意味です。 - 断線、もしくはセンサーの故障が考えられます。
- -127℃エラー 「-127 °C」 が出る場合は、ほぼ確実に
通知例:

Ambientデータ送信
- MACアドレスをdevKeyとして使用
- 個体ごとに自動チャンネル作成
- 温度データを時系列で記録

専用基板
今回の装置では、ブレッドボード配線ではなく**専用の基板(プリント基板)**を作っています。
専用基板にすることで、
配線ミスがなくなる
センサー用電源を測定時だけONにできる
ケースに収まり、現場設置が簡単になる
といったメリットがあります。
※R2、R3は電圧測定用抵抗
今回は使用しません


基板&配線サポート台座
台座を3DCADにて設計
3Dプリンターで作成
温度センサーやバッテリーケースのリード線は
細くて断線しやすいので、
台座にサポートブロックを設けています


まとめ
このシステムにより
- 完全自動温度監視
- 超省電力運用
- 遠隔通知
- データ記録
- Web設定
をすべて実現できます。
農業・設備管理・防災用途など幅広く応用可能なIoT温度監視システムです。
参考動画
ESP32C3のリチウムイオンバッテリー端子へのハンダ付け解説
参考URL
Ambient IoTデータ可視化サービス
https://ambidata.io
Seeed Studio XIAO ESP32C3 を使い始める
https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/
KiCad クロスプラットフォームでオープンソースのPCB設計スイート
https://www.kicad.org/
JLCPCB プリント基板発注
https://jlcpcb.com/
Discord 外部サーバー連携
https://discord.com/
コメント
この記事へのトラックバックはありません。









この記事へのコメントはありません。