0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第1回】M5Stack AtomS3R と Claude Code で灌水施肥システムをつくってみた — まず畑に無線を張る

0
Last updated at Posted at 2026-08-03

この記事のコードは、ほぼ 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 モード時代の値が残っていたことでした。うまくいっていなかった頃の状態がそのまま保存されていて、再起動のたびにそれを復元し、同じ状態に戻る。モードを変えたのに、古い設定に引き戻され続けていたわけです。

なので、起動時に保存済みの状態を消して、毎回ゼロから選び直させるようにしました。復帰は少し遅くなりますが、確実に選出が走ります。

これでようやく、ゲートウェイに近い noderoot になり、遠い 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 につなぐ

ソフトウェア

参考になった公式ドキュメント

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?