この記事のコードは、ほぼ Claude(Claude Code)に書いてもらいました。人間がやったのは設計の判断と、畑での検証です。
お断り:個人の趣味として自作したシステムの記録です。可用性・保守・サポートは保証しません。業務として正しく運用したい場合は、市販の灌水・施肥管理システムをおすすめします。
トマトの水やりは、少しずつ何回にも分けたい
トマト農家をやっていて、ハウスが46棟あります。灌水はそれを6つの区画にまとめて、区画ごとに制御しています。
トマトの水やりは、一度にたっぷりあげればいいというものではありません。少量を何回にも分けてやるほうが、根が一気に水を吸わずに済み、土の水分も安定します。うちは今、1日5回に分けています。
肥料のほうは、水やりと同時に液肥(水に溶かした肥料)として流し込みます。専用のポンプで注入していて、入る量は タンクに溶かした濃度 × ポンプの流量 × 稼働時間 で決まります。濃度はタンクを調合したときに、流量はポンプごとに決まっているので、日々の調整は稼働時間でやります。
つまり、毎日こういう作業が発生します。
- 6区画それぞれについて、1日5回の水やりを時間どおりに開けて閉める
- 天気や生育ステージで、水の量を変える
- 狙った窒素量になるように、液肥ポンプを回す時間を合わせる
1日5回 × 6区画で、30回の開け閉めです。しかもバルブは畑のあちこちに散っています。手でやる作業ではありません。
だから自動化したい。ここまでは、よくある話です。
ところが、ハウスにはネットが来ていない
自動化しようとすると、最初にこれにぶつかります。ハウスは母屋から離れていて、Wi-Fi が届きません。とはいえ、モバイル回線を引いてしまえば普段は安定しています。
ただ、通信が止まっている間に水やりの時間が来たらどうなるか、そこだけは気になりました。めったに起きないとはいえ、起きたときに気づけるとも限りません。要は念のためという話です。
なので「クラウドから指示を出して制御する」という素直な作り方はやめておきました。通信が止まっていても、水やりだけは動いてほしい。 この一点が、設計の方向を決めています。
つくったもの
Field Link という名前にしました。といっても、考えたのは私ではなく Claude Code です。「圃場(field)をつなぐ(link)」で分かりやすいので、そのまま採用しました。似た響きの Space Link とはたぶん関係ありません。
やりたいことは、実はすごく単純です。
「6:00 から 12分間、1区画の水を出す」を、毎日きちんと実行する
これを、何かあった日でも落とさずにやってくれるものが欲しい。最終的にこうなりました。
ハウス46棟 / 6区画 水源
AtomS3R × 6 Raspberry Pi
電磁弁を開け閉め 流量を測る / 液肥ポンプを回す
│ │
│ ESP-MESH-Lite │
│ (AtomS3R 同士が数珠つなぎ) │
▼ │
メッシュゲートウェイ(Raspberry Pi) │
圃場内に Wi-Fi を出す │
│ │
Soracom Onyx Soracom Onyx
(メッシュ全体の出口) (メッシュ圏外なので直結)
└────────────┬─────────────────────┘
│
サーバー(VPS)
「予定表」を作って配る
各区画に置いたのが M5Stack AtomS3R です。手のひらに乗る小さな ESP32 のモジュールで、これにミニリレーユニットをつないで電磁弁を開け閉めします。水源側は流量計と液肥ポンプがあるので Raspberry Pi にしました。
そしてサーバーは、リアルタイムに弁を操作しません。「今日はこの時間にこれだけ出してね」という予定表を配るだけです。実際に弁を開けるのは、AtomS3R が自分の時計を見て自分で判断します。
畑という環境に合わせるために決めたのは、この2つです。
| 困ったこと | こうした |
|---|---|
| ハウスに Wi-Fi が届かない。かといって6台に回線を持たせると通信費が高い | ESP-MESH-Lite で機器同士を数珠つなぎにして、外への出口を1つにした |
| 通信が止まった日でも、水やりの時間は来る | リモート制御をやめて、予定表を配る方式にした |
前者は土台の話、後者が本題です。この第1回では土台のほう、「畑に無線を張る」までを書きます。予定表の話は次回です。
畑に無線を張る
選択肢は3つあった
| 案 | どうなるか |
|---|---|
| 各デバイスに SIM を挿す | 台数×月額。区画を増やすたびに固定費が増える |
| 母屋から Wi-Fi を飛ばす | 距離とハウスの構造で届かない。中継器を足しても一直線にしか伸びない |
| メッシュ | 機器同士が中継し合ってつながる。届かなければ1台足せばいい |
46棟が一直線に並んでいるわけではないので、3番目が素直でした。どこか1箇所から全部に電波を届かせるという形に、どうしてもなりません。
そこで使ったのが ESP-MESH-Lite です。Espressif(ESP32 の開発元)が出している、ESP32 同士が数珠つなぎで中継し合うライブラリです。ESP32 の Wi-Fi をそのまま使うので、追加のハードは要りません。
1台でも外につながっていれば、残りはそれを経由してインターネットに出られます。 結果、モバイル回線の契約は2つだけで済みました。
外に出る口は Raspberry Pi が担当していて、どちらも Soracom Onyx(LTE USB ドングル)に SORACOM の SIM を挿してつないでいます。USB に挿すだけなので、Raspberry Pi 側で特別な工作は要りません。IoT 向けなので、使ったぶんだけの従量課金で済みます。
| 外への回線 | 役割 | |
|---|---|---|
| メッシュゲートウェイ(RPi) | Soracom Onyx ×1 | 圃場内に Wi-Fi を出し、メッシュ全体の外への出口になる |
| 水源ユニット(RPi) | Soracom Onyx ×1 | メッシュの電波が届かない場所なので直接接続 |
| AtomS3R × 6 | なし | メッシュで数珠つなぎ |
デバイスを1台増やしても通信費は増えません。 区画を増やしたくなったときに費用を気にせず増やせるのは、あとから効いてきました。
補足:ESP-MESH-Lite とは
仕組みそのものは単純で、1台1台が「Wi-Fi につなぐ側(Station)」と「Wi-Fi を出す側(SoftAP)」を同時にやるというものです。上流につながりつつ、自分も電波を出して下流を受け入れる。これを繰り返すと、遠くまで芋づる式に届きます。
ここから先はメッシュの用語として、参加する1台1台を node と呼びます。
名前が似たものに ESP-MESH(ESP-WIFI-MESH) があって、最初はどちらを使うか迷いました。決め手は、MQTT がそのまま繋がるかどうかでした。
ESP-MESH-Lite では、各 node が親から IP アドレスをもらいます。親は受け取ったデータをネットワーク層で転送するだけで、中身には関与しません。node から見ると「普通に Wi-Fi ルータにつながっている1台の機器」と変わらない状態になります。
| ESP-WIFI-MESH | ESP-MESH-Lite | |
|---|---|---|
node の見え方 |
メッシュの一員。専用 API でやりとりする | IP アドレスを持つ普通の機器 |
| 親の役割 | 中継をアプリ側が意識する | ネットワーク層で転送するだけ |
| MQTT を使うには | メッシュ用の橋渡しを自分で書く | 無改造でそのまま動く |
公式ドキュメントにもはっきり書かれています。
Each node in ESP-Mesh-Lite enables the LWIP stack and can be treated as a device directly connected to the router, which can independently invoke network interfaces such as Socket, MQTT, HTTP, etc., on the application layer.
やりたいのは「各 node が MQTT でサーバーとやりとりする」ことだけです。ESP-IDF の MQTT クライアントをいつもどおり呼べば繋がるなら、それ以上のことは要りません。メッシュのことを何も知らないコードがそのまま動く。 ここが決め手でした。
作りたいのは灌水システムであって通信基盤ではないので、メッシュ内外の橋渡しに手をかけたくありませんでした。
自動での復旧(self-healing)も入っていて、親が居なくなれば別の親を探しにいきます。うまくいけば、アプリ側は何も書かなくていい——建前としては。実際にはこのあと4回つまずきます。
なお使っているのは v1.0.2 です。v1.0.1 には root 選出まわりの不具合があり、依存を更新すると戻ってしまうのでバージョンを固定しています。
つまずき1:素直に組んだら、メッシュが張れなかった
ESP-MESH-Lite には動作モードがいくつかあります。最初は名前のとおり MESH モードを選びました。メッシュを作りたいのだから、これだろう、と。
結果、メッシュが張れませんでした。
メッシュは「網」という名前ですが、実際は木のような形でつながります。どれか1台が root(木でいう根っこ)になってゲートウェイに直接つながり、残りの node はその下にぶら下がる。root がいなければ、誰も外に出られません。
ところが実際に動かすと、全 node が「自分は誰かの下にぶら下がろう」として待ち続けていました。root になる node がいつまでも現れない。親のいない子だけが並んでいる状態です。
誰が root になるかを node 同士のやりとりに任せた結果、いつまでも決まらなかったわけです。
解決その1:「誰が root になるか」を電波の強さで決めさせた
そこで ROUTER モードに切り替えました。役割が条件で決まるモードです。
ゲートウェイの電波が強く届く node
→ ゲートウェイに直接つなぐ(この node が root になる)
ゲートウェイの電波が弱い node
→ 近くの node につなぐ(その node の下にぶら下がる)
判定は電波の強さ(RSSI)で、閾値は -85dBm にしました。遠くの node は近くの node を経由して外に出る — これが数珠つなぎの正体です。
「話し合いで決まる」より「条件で決まる」ほうが、現場では扱いやすい。 電波が届く場所に1台置けば、そこが必ず出口になると分かっているほうが、設置するときに考えることが減ります。
解決その2:前のモードの残骸が、再起動のたびに復元されていた
ところが、モードを変えても直りきりませんでした。相変わらず root にならない node が残ります。
ログを見ると、起動時に「前回つながっていた親につなぎにいきます」という動きをしていました。ESP-MESH-Lite は、予期しない再起動に備えて前回のメッシュ状態(自分の階層や親)を RTC メモリに保存しておき、起動時に復元します。本来は復帰を速くするための機能です。
問題は、そこにMESH モード時代の値が残っていたことでした。うまくいっていなかった頃の状態がそのまま保存されていて、再起動のたびにそれを復元し、同じ状態に戻る。モードを変えたのに、古い設定に引き戻され続けていたわけです。
なので、起動時に保存済みの状態を消して、毎回ゼロから選び直させるようにしました。復帰は少し遅くなりますが、確実に選出が走ります。
これでようやく、ゲートウェイに近い node が root になり、遠い node がその下にぶら下がる形が安定して作られるようになりました。
古い設定が思わぬところに残っていることがある。 モードを変えたのに挙動が変わらないときは、設定そのものより「前の状態がどこかに残っていないか」を疑ったほうが早いです。
つまずき2:畑の無線に、誰でも入れた
メッシュが張れたので、試しにスマホで Wi-Fi を検索してみました。すると、畑の機器が出しているアクセスポイントに、パスワードなしで繋がってしまいました。
補足で書いたとおり、各 node は下に別の node をぶら下げるために、自分でもアクセスポイントを出しています。そして ESP-MESH-Lite は、それを認証なし(オープン)で立ち上げます。メッシュ内部のやりとりはライブラリが守ってくれますが、電波そのものは誰にでも見えて、誰でも入れる状態でした。畑は道路に面しているので、車を停めれば繋がります。
対処として、メッシュを起動したあとに、アクセスポイントの設定を WPA2 に上書きしました。ライブラリが決めた SSID や最大接続数はそのまま活かして、認証方式とパスワードだけ差し替えています。
動いたところで満足せず、それが外にどんな電波を出しているか一度確認したほうがいい。 気づかないまま運用に入るところでした。
つまずき3:一度切れると、諦めが早い
ESP-MESH-Lite は、親を見失ったときの再接続を2回試して諦めます。
普段は問題ありません。ただ、ハウスの中は電波環境が良いとは言えず、たまに一時的な切断が起きます。そこで2回で諦められると、繋がらないまま放置される node が出ます。誰も見ていない場所なので、気づくのは翌日です。
なので、もっと粘るようにしました。親への再接続は5回まで5秒間隔で試し、それでもだめなら10秒おきに新しい親を探しにいきます。
つまずき4:電源を入れ直した直後だけ、繋がっては切れる
停電明けなどで全 node が一斉に立ち上がると、繋がっては切れてを繰り返す時間がありました。
原因は、起動直後は複数の node が同時に「自分が root だ」と名乗ってしまうことでした。まだ周りの状況が見えていないので、それぞれが自分の判断で root になろうとします。
ライブラリはこれを見つけると「root が多いので整理しよう」と親子関係を組み替えます。ところが全 node がまだ起動の途中なので、組み替えた先の状況もすぐ変わる。整理するそばから前提が崩れていく状態でした。
そこで、整理を始めるのを起動から30秒待たせるようにしました。全 node が落ち着いてから手をつけさせる、という発想です。以降は5分おきに様子を見ます。
畑に置くものは「切れないこと」より「切れても戻ること」を優先したほうがいい。 これは次回の本題にもそのままつながる話です。
組み上がったコード
ここまでの対処を全部入れると、メッシュの初期化はこうなりました。ログ出力とエラー処理は省いています。どの行がどのつまずきに対応するかをコメントに入れました。
まず前段として、Wi-Fi とネットワークインターフェースを用意します。
esp_netif_init();
esp_event_loop_create_default();
esp_bridge_create_all_netif(); // メッシュ用のネットワークインターフェースを作る
wifi_init();
そのうえで、メッシュ本体です。
esp_err_t initialize_mesh_network(void)
{
// メッシュ本体の初期化。leaf_node = false で「自分も下に node をぶら下げる側になる」
esp_mesh_lite_config_t mesh_config = ESP_MESH_LITE_DEFAULT_INIT();
mesh_config.leaf_node = false;
esp_mesh_lite_init(&mesh_config);
// 【つまずき1】前回の状態が RTC に残っていると復元されて固着するので、毎回消す
esp_mesh_lite_erase_rtc_store();
esp_event_handler_register(ESP_MESH_LITE_EVENT, ESP_EVENT_ANY_ID,
&mesh_event_handler, NULL);
// 【つまずき1】役割を電波の強さで決めさせる(-85dBm 以上なら自分が root になる)
esp_mesh_lite_set_networking_mode(ESP_MESH_LITE_ROUTER, -85);
// 【つまずき3】再接続を粘らせる(親へ5回/5秒、新しい親のスキャンは10秒おき)
esp_mesh_lite_set_wifi_reconnect_interval(5, 5, 10);
// 【つまずき4】起動30秒は親子関係の整理をさせない。以降は5分おきに評価
esp_mesh_lite_fusion_config_t fusion_cfg = {
.fusion_rssi_threshold = -85,
.fusion_start_time_sec = 30,
.fusion_frequency_sec = 300,
};
esp_mesh_lite_set_fusion_config(&fusion_cfg);
// 起動前に接続先ゲートウェイを渡しておく。これが無いと自力で root になれない
mesh_lite_sta_config_t router = {0};
strlcpy((char *)router.ssid, CONFIG_ROUTER_SSID, sizeof(router.ssid));
strlcpy((char *)router.password, CONFIG_ROUTER_PASSWD, sizeof(router.password));
esp_mesh_lite_set_router_config(&router);
esp_mesh_lite_start();
// 【つまずき2】ライブラリはアクセスポイントを認証なしで立てるので WPA2 に上書き。
// SSID と最大接続数はライブラリが決めたものをそのまま使う
wifi_config_t ap_cfg = {0};
esp_wifi_get_config(WIFI_IF_AP, &ap_cfg);
ap_cfg.ap.authmode = WIFI_AUTH_WPA2_PSK;
strlcpy((char *)ap_cfg.ap.password, CONFIG_BRIDGE_SOFTAP_PASSWORD,
sizeof(ap_cfg.ap.password));
esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
return ESP_OK;
}
公式のサンプルどおりなら、必要なのは esp_mesh_lite_init() と esp_mesh_lite_start() の2つだけです。あいだに挟まっているものは全部、畑で動かすうちに足していきました。
なお esp_mesh_lite_set_router_config() でゲートウェイの SSID を渡しているのは、これが無いと自力で接続して root になれないためです。root になる node は結局ゲートウェイに直接つなぐので、その接続先を知っている必要があります。
数値はすべて実際に使っている値です。CONFIG_ で残した3つだけが認証情報で、これらは git 管理外のファイル(sdkconfig.secrets)に置いています。
できたもの
- 母屋から離れた46棟/6区画に散らばった機器が、SORACOM の SIM 2枚で全部つながった
- AtomS3R は何台増やしても通信費は増えない
- 電波が届かない場所ができても、間に1台足せば伸ばせる
そしてこれは、灌水のためだけの配線ではありません。
各 node は IP アドレスを持った普通の機器として振る舞うので、ESP32 を1台足せば、それがそのままネットワークに参加できます。気温・湿度・土壌水分・CO2 など、測りたいものが出てきたときに、通信のことを考え直さずに済みます。SIM も増えません。
つまりセンサー類を後から足していける基盤ができた、というのが第1回のいちばんの成果でした。灌水を自動化したかっただけなのに、気づけば畑がまるごとネットワークにつながっていた、という感じです。
次回:通信が止まっても、水やりは止まらない
土台ができたので、次回が本題です。
無線を張ったとはいえ、外に出る口は結局モバイル回線1本です。ここが止まれば、ハウスはインターネットから孤立します。めったに起きないとしても、起きた日にも水やりの時間はやってきます。
次回は、リモート制御をやめて「予定表」を配るという設計と、そこに空いていた3つの穴の埋め方を書きます。
- 再起動したら、配った予定表はどこへ行く?
- デバイスの時計が狂ったら?
- 投げっぱなしで、本当に届いたのか?
使ったもの
| もの | 数 | 用途 |
|---|---|---|
| M5Stack AtomS3R | 6 | 各灌水区画のコントローラ |
| M5Stack ミニリレーユニット | 6 | 電磁弁の開け閉め |
| M5Stack Unit RTC | 6 | 通信が止まっても正しい時刻を保つ(第2回で登場) |
| Raspberry Pi 3 | 2 | メッシュゲートウェイ/水源ユニット |
| Soracom Onyx(LTE USB ドングル) | 2 | Raspberry Pi を LTE につなぐ |
ソフトウェア
- ESP-IDF v5.5 系
- ESP-MESH-Lite v1.0.2(User Guide)
- Claude Code
参考になった公式ドキュメント
- Soracom Onyx を Raspberry Pi でセットアップする — USB に挿してからの設定手順