DIY精密農業:Raspberry Pi Picoを使った肥料モニター 第3部

Justin Paul は Claude Code で追肥コントローラを書き直した。WiFi を Bluetooth に、Web ページをネイティブアプリに、データシートの数字をセンサーごとの校正に置き換えた。

日本語
コピー
FlowMeter 应用截图:一次 6 月 13 日的施肥记录,顶部显示平均施用率 341.3 lbs N/ac(已校正)、总量 400.0 加仑、3699 个读数、时长 82 分钟;下方是卫星地图,作业轨迹用绿点画出,转弯处的红橙点被过滤器剔除

第2回は2025年4月28日、ちょうどトウモロコシの播種が終わったあとに公開した。最後に残った課題を並べ、第3回では施肥の話をすると約束した。それが「数か月以内」より少し延びたのは、このプロジェクトが年に2回しか動かないからだ(播種と追肥)。

ただ今シーズンが始まる前に、Claude に既存のコードを見せて、前シーズンにやり残した部分を片付けてもらうことにした。かなりがっかりしたと思う……ともかく、何を変えたかを書く。

これがプロジェクトの追肥側だ。28% UAN、7行、条間30インチ。それから今春ほとんどゼロから作り直したモニターで、コードの大半は Claude Code が書いた。ハードウェアはほぼ第1回の箱のままだ。周りは全部変わった。

第2回を書いた時点で存在しなかったものが5つある。

  • Bluetooth。WiFi アクセスポイントをやめたので、流量を見ながらでもスマホはネットにつながったままになる。
  • iPhone と Android のネイティブアプリ。TestFlight と Play からインストールする。Pico 上で動く Web ページではない。
  • homelab で動くポータル。アプリが作業終了のたびに自動でアップロードし、圃場の衛星画像の上に各パスの軌跡を描く。
  • まともなキャリブレーション手順。センサーごとに、作業場でスマホから実行できる。データシートの数字を信じる必要はもうない。
  • Bluetooth 経由のファームウェア更新。圃場で問題に気づいても、ノート PC を抱えてトラクターまで歩かなくていい。

まずは去年の4月に約束したことから、これらがどうやって生まれたかを書く。

第2回の宿題を採点する

前回の最後に5つの改善点を挙げた。それぞれの実際の結果がこれだ。

GPS の速度計測、まったく動いていなかった。 直った。Pico でやるのをやめたのが理由だ。速度の取得元はスマホの GPS になり、Waveshare L76X 拡張ボードはバックアップ。アプリには Auto / Phone / Device の手動セレクタがあって、使いたい方を強制できる。途中で回り道もあった。別のアプリに切り替えて天気を見ると速度が固まる、という問題で、原因はフォアグラウンドの購読ではなく、ちゃんとしたバックグラウンド位置情報タスクが必要だったことだった。

地図分析用の GPS 座標付きデータログ。 完了。しかも一番よく使っている部分だ。詳細は後で。

ノズルが開いているかを検知するセンサー。 解決したが、はっきり言っておきたい。センサーを足すと言って、実際に書いたのはフィルタだ。総流量が 0.05 GPM を下回る読みはすべて閉じているとみなす。ハードウェアは増えず、第2回で「旋回中のゼロ点が意味不明」だった問題もついでに片付いた。あの旋回中の読みこそ、フィルタが捨てていたものだ。

スマホやスマートウォッチへの通知。 半分。アプリは行が詰まると警告を出し、キャビンで振動する。自分が気にしていたケースはこれでカバーできている。ウォッチアプリはない。

10,000〜15,000 mAh の内蔵バッテリー。 やっていない。今も USB モバイルバッテリーで動かしている。TODO リストには残っている。

旧バージョンにできなかったこと

第1回の装置は動く。けなすつもりはない。使い始めた初日に、2行目で結び目のあるホースを、4行目で詰まった防サイフォン装置を見つけた。これを作った理由はまさにそこにある。

限界は作業の端のほうで出る。main.py は521行あり、Microdot の Web サーバーを抱えていた。Pico 自身が WiFi ホットスポットを立てて、スマホにダッシュボードのページを配っていたからだ。そのホットスポットにつながっている間はスマホがネットから切れる。つまり流量を見ている間は地図もレーダーも通話も使えない。読みは Pico のフラッシュ上の flow_data.csv に書かれ、ノート PC を抱えて作業場に行くまでそこに留まる。キャリブレーションはデータシートから取った pulses_per_litre = 450 の1つの値だけで、それを6個のセンサー全部に適用していた。安物のタービンセンサー6個が揃って同じ値を出すかのように。何か変更するたびにノート PC をトラクターまで運び、mpremote を走らせる必要があった。

今シーズン変わったこと

今年の3月28日から6月14日の間に3つのリポジトリが動き、変更の大半は6月の4日に集中している。

WiFi を Bluetooth に置き換えた。 4月22日、Web サーバーを外した。このコミットは5,689行を削除し、1,892行を追加している。削除分の大半は vendored の Microdot ライブラリと、それに付随する HTML と JavaScript だ。代わりに BLE サービスを置き、流量、GPS、ステータス、設定、累積パルス数のそれぞれにキャラクタリスティックを用意した。キャリブレーションは1行ごとの7つの値になり、1つではなくなった。パルスカウンタは毎秒のレートから累積総量に変えた。これで作業の途中で Bluetooth が切れても、失うのは1秒分の分解能だけで、切断中に流れた分のガロンではない。

Web ページではなく、本物のアプリ。 Expo と TypeScript で、スマホ上では react-native-ble-plx と SQLite が動く。行ごとのメーター、詰まった行の警告、作業記録、CSV エクスポート、そして流量・対地速度・条間からリアルタイムに計算する1エーカーあたりの窒素ポンド数(28% UAN、1ガロンあたり10.67ポンド、窒素28%)。iOS 版は TestFlight で自分のスマホに入れ、Android 版は署名済み AAB で Play の内部テストに出す。どちらも自宅地下室の自分の Gitea Actions runner でビルドしていて、ホスト型のビルドサービスは使っていない。2プラットフォーム、どちらもネイティブ。ブラウザのタブがマイコンにつながっているものは1つもない。

本当に使える校正。 第 1 部ではどのセンサーも 1 リットルあたり 450 パルスだった。データシートにそう書いてあったからだ。今はアプリに接液テストがある。行を選び、実際に受けたい容積を入力して Start を押し、液体を計量カップに入れて Stop を押す。Pico が報告するのはレートだけでなく累積パルス数なので、アプリは Start から Stop までのカウントを差分し、実際に受け取った容積で割り、その行自身の数値を Bluetooth 経由でデバイスに書き戻す。センサーが 7 つ、数値も 7 つ、ノート PC は要らない。

7 行目。 追肥に使っている機具は 7 行なので、6 月 11 日にボックスへ GPIO 10 をもう 1 本追加し、Bluetooth のメッセージも 6 個から 7 個の値に広げた。これは覚えておいてほしい。

ポータルが 1 つ、これが Part 2 で本当に気にしていた約束だ。 この部分が一番意外だった。小規模な SaaS アプリなのに、完全に自分のハードウェア上で動いている。バックエンドは Postgres 16 と PostGIS、フロントエンドは Next.js に MapLibre で地図、さらにタイルサーバー。全部が地下室のミニ PC 上のコンテナで動いている。ログイン、ファームの組織、作業記録があり、圃場を他人と共有もできる。

データを流し込むのにこちら側でやることは何もない。ある作業中に Stop を押すと、アプリがバックグラウンドでアップロードする。電波がなければスマホのキューに残り、次に電波が入ったときに送られる。以下は 6 月 13 日の実際の作業だ。

6 月 13 日の 1 作業。各点は 1 秒分の流量を、それが計測された位置に、合計 GPM で色分けして示す。

どの点も、流量が発生した位置の GPS 座標に落とされた 1 秒を表し、合計 GPM で色分けされている。82 分間で 3,699 点。センサーは毎秒サンプリングするので、この 2 つの数字は本来一致しない。差の分が、その 1 時間半のうち旋回や補給、あるいは停止していた 20 数分だ。これらの読み取り値はデフォルトでは非表示になっている。軌跡全体を見たいときはチェックボックスを 1 つオンにすれば戻ってくる。

この色のグラデーションは CSV からは絶対に得られない。下端のオレンジと赤は詰まった行ではなく、各パスの終わりにアプリケータが地面から離れ、GPS はまだ記録している区間だ。縮小すると、作業 1 回分の形が見えてくる。

圃場における作業全体の軌跡。各枕地と各コーナーが赤い読み取り値になっている

同じ作業を縮小した図。赤い塊はどれも枕地かコーナーで、詰まった行ではない。

比較のために……こちらが前四半期、AI の助けが入る前の読み取り値だ。

枕地もコーナーも全部赤い。これを見た後では、「アイドル読み取り値を隠す」はもはやあると嬉しい機能ではなく、地図を読めるようにする唯一の方法になる。Part 2 で私が挙げた理由より、ずっと説得力がある。

ページ上部の 2 つの数字には CORRECTED の印が付いている。つまり、この作業について後からポータルに何かを申告したということだ。容積をタンクの実際の減少量に合わせて再スケールする、あるいは死んだ行の流量を隣接行で補う。どちらも生の読み取り値には触らない。修正は作業をキーにした別テーブルに保存され、ページを読むときに適用されるので、元データは下に残ったままで、スマホが再同期しても消えない。

Bluetooth 経由のファームウェア更新。 これは 1 人では絶対にやらなかった部分で、一番の難所でもあった。6 月 11 日午後 6 時 16 分に最初のコミット、6 月 12 日の夜には実機で端から端まで通った。CI が署名済みパッケージをビルドして公開し、スマホが電波のあるうちにダウンロードし、電波のない圃場で Bluetooth 経由で転送する。Pico はアプリのコピーを 2 つ保持し、決して変わらない小さなブートローダーで起動する。新しくインストールされたコピーは起動して動作し、自分を healthy とマークしなければならない。3 回連続でクラッシュしたコピーは動くバージョンにロールバックされる。マニフェストは Ed25519 で署名され、Pico 上で検証される。純粋な MicroPython 実装だ。MicroPython は SHA-512 も base64 モジュールも持っていないからだ。検証にはおよそ 3 秒かかる。この 2 日間でファームウェアのビルドは合計 8 回出た。

もうキャビンにノート PC を持ち込む必要はない。

うまくいかなかったところ

施用量が 6 倍多く計算されていた。 これが一番致命的なので最初に書く。ガロン毎エーカルの計算が、総流量を1 行分の行間で割っていた。機具全体の作業幅ではなく。6 行、1 GPM、5 mph、30 インチ行間で、アプリは 237.6 ガロン毎エーカー、709 ポンド窒素毎エーカーと表示した。正しい答えは 39.6 と 118。6 行分、6 倍多い。しかもユニットテストは 237.6 をそのままアサートしていた。バグを書いたのと同じセッションが、それに同意するテストも書いていたからだ。コンパイルは通り、テストも通り、画面には自信満々の数字が出る。これを捕まえられたのは、追肥の施用量がおよそどれくらいか知っている人だけだった。

iOS が特定の Bluetooth キャラクタリスティックをどうしても見つけられなかった。 ファームウェアの一括データ転送用に作ったチャネル、0007 で終わるやつが、サービスディスカバリの段階でまったく現れなかった。診断バージョンがついに、スマホから見えるものを出力した。0001、0002、0003、0004、0005、0006、0008。それだけが欠けていた。読み取り可能にしてみた。その後ろにパディング用のキャラクタリスティックを足して最後尾にならないようにしてみた。Bluetooth アドレスをローテートしてスマホがキャッシュ済みのコピーを使えないようにしてみた(Pico W では無線がオンでもオフでもエラーが出て、これはできない)。最終的な解決策は正面突破をやめることだった。ファームウェアのデータは今、コントロールチャネルを通り、先頭にゼロバイトを付けて JSON コマンドと区別している。あのチャネルは毎回見つかる。

110 バイトのメッセージが届くころには、20 バイトしか残っていない。 デバイスのログには OTA ctrl raw: len=20 b'{"op":"begin","versi' と表示されていた。iOS はネゴシエート済みのパケットサイズを超える書き込みを、分割せずに黙って切り捨てる。しかもそのネゴシエート済みサイズは、Pico がどう要求しようと 23 バイトで固定されていた。解決策は、各コマンドをスマホが実際にネゴシエートしたサイズで分割し、Pico 側で貼り戻して JSON がパースできるまで待つことだった。

カンマを出力する base64 エンコーダ。 分割のコードはバッファを subarray() で切っていた。React Native の buffer ライブラリはこの呼び出しから普通のバイト配列を返すが、そちらの toString('base64') が返すのは base64 ではなくカンマ区切りの数字だ。Bluetooth ライブラリはスマホから出ていく前にそれを弾いた。ラッパー関数ひとつで直った。

Android は GPS を取得した瞬間に落ちる。 実機のみ、屋外のみ、本物の測位があるときのみ——デバッグにとっては最高の組み合わせだ。バックグラウンドの位置情報タスクは、再起動後も job を永続化するよう Android に要求する。この要求を出しながら起動権限を宣言していないアプリは、Android に殺される。設定ファイルに一行足せば直った。その一行を見つけられたのは、ローカルで署名ビルドして、クラッシュログを USB 経由でスマホから引き抜いたからだ。

7 行目が落ちていた。 Pico は 7 つの流量値を送ったのに、アプリは 6 つしか保存していなかった。7 行目のデータはデータベースに書き込まれる途中で黙って捨てられ、それが 1 日ほど続いた。この手のバグは、地図を眺めてなぜいつまでも 6 行しか描かれないのかと首をひねって、初めて見つかる。

AI が得意なこと、苦手なこと

面を広げるのは得意だ。このプロジェクトは MicroPython のスクリプトひとつから、Go と PostGIS の API、地図のフロントエンド、自動ロールバック付きの署名済みファームウェア更新経路、ゼロから書いた Ed25519 検証器(プラットフォームに既成のものがなかった)、そしてアプリストアのリリースパイプライン 2 本まで育った。すべてを一つの春にやり切り、種まきにも間に合った。これだけのことを一人で広げるのは無理だ。そこは取り繕わない。

苦手なのは、その数がいくつであるべきかを知ることだ。上に挙げたバグはどれも、実行時にエラーを出さなかった。施肥量の計算式が一番きれいな例だ。コードは正しく見え、テストは通り、画面に出る数字ももっともらしい。それでいて 6 倍ずれていた。どんなツールもこれには文句をつけない。ここが、同じやり方でやろうとしている人に伝えたい境界線だ。書くのは速い。だが何が正しいかは、依然として自分で判断するしかない——そして施肥量で一度間違える代償は、どんな Web アプリにもないものだ。

これから

検証用のテストを書いたが、一度も走らせていない。7 行のデータはいまだにデータシートの 1 リットルあたり 450 パルスのままだ。つまり地図上のガロンの数字はすべて、PDF から書き写したあの数と同じだけしか信用できない。これがリストの一番目で、計量カップを持ってセンサーごとに測っていく。この地図が見栄えのするものになるか、本当のものになるかを決めるからだ。その次は Part 2 で約束したバッテリー、さらにその先は、もしかすると行ごとの施肥データを、今進めている他の農場向けツールにつなぐことかもしれない。

前回と同じだ。農家である必要も、農業を知っている必要もない。俺の設計判断が馬鹿げていると思う箇所や、もっといいやり方がある部分があれば、コメントで教えてほしい。

Loading

読み込み中…

出典: Justin's IT Blog← ホームへ戻る