DIY精准农业:使用Raspberry Pi Pico的肥料监测器 第3部分

Justin Paul 用 Claude Code 重写了追肥控制器的代码:蓝牙取代 WiFi、原生 App 取代网页、逐传感器校准取代数据手册上的一个数字,GPS 测速改由手机提供。

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

第 2 部分发布于 2025 年 4 月 28 日,正好赶在玉米播种之后。我在结尾列了一堆想修的问题,并承诺第 3 部分会讲施肥。结果拖得比“接下来几个月”久了一点,因为这个项目一年基本只用两次(一次播种,一次追肥)。

不过这个季节开始前,我决定让 Claude 看看现有代码,把上个季节没搞完的部分试着修一修。我猜 Claude 挺失望的……总之,说说它改了什么。

这是项目里追肥的那一侧:28% UAN,七行,行距 30 英寸,还有一个我今年春天几乎从零重建的监视器,大部分代码是 Claude Code 敲的。硬件基本还是第 1 部分那个盒子。周围的一切都变了。

有五样东西是写第 2 部分时不存在的:

  • 蓝牙,取代了 WiFi 接入点,这样手机在读流量时还能保持联网。
  • iPhone 和 Android 上的原生应用,从 TestFlight 和 Play 安装,不再是 Pico 上跑的网页。
  • 一个跑在我 homelab 里的门户,应用每次作业结束后自动上传,田块的卫星图上会画出每一趟的轨迹。
  • 一套真正的校准流程,逐个传感器做,在车间里用手机就能完成,不用再信数据手册上的一个数字。
  • 通过蓝牙更新固件,在地里发现问题不用再抱着笔记本走到拖拉机那儿。

下面说说这几样是怎么来的,从去年四月我给你们许下的承诺开始。

给第 2 部分的作业打分

上一篇结尾我列了五项改进。下面是每一项的实际结果。

GPS 测速,之前完全不能用。 修好了,办法是放弃在 Pico 上做。现在速度来源是手机的 GPS,Waveshare L76X 扩展板作为备份,应用里有个 Auto / Phone / Device 手动选择器,想强制用哪个就用哪个。这中间还绕了一段路:一切换到别的应用去看天气,速度就冻住,最后发现需要的是一个正经的后台定位任务,而不是前台订阅。

带 GPS 坐标的数据记录,用于地图分析。 已完成,而且是我用得最多的一块。下面细说。

检测喷头是否接合的传感器。 解决了,但我想把话说清楚:我说要加一个传感器,结果写的是一个过滤器。总流量低于 0.05 GPM 的读数一律算作未接合。不用额外硬件,还顺带解决了第 2 部分里“转弯时零点让人看不懂”的问题——那些转弯读数正是被过滤器丢掉的。

手机或智能手表通知。 算一半。应用会在某一行堵塞时报警,并在驾驶室里震动,这覆盖了我在意的那种情况。手表应用没有。

内置 10,000 到 15,000 mAh 电池。 没做。现在还是靠 USB 充电宝供电。仍在待办清单上。

旧版本做不到的事

第 1 部分的装置是能用的,我不想贬低它。用它的第一天,它就在两行上发现了一根打结的软管,在第四行上发现了一个堵塞的防虹吸装置——做这东西的全部理由就在这里。

局限出在这项工作的边缘。main.py 有 521 行,还带了一个 Microdot Web 服务器,因为 Pico 自己起了个 WiFi 热点,向我的手机提供一个仪表盘页面。连上那个热点就意味着手机断网,所以只要我在看流量,地图、雷达和通话就都没了。读数写进 Pico flash 上的 flow_data.csv,一直待在那儿,直到我抱着笔记本走到车间。校准只有一个数 pulses_per_litre = 450,取自数据手册,然后套用到全部六个传感器上,仿佛六个廉价涡轮传感器彼此一致。任何改动都意味着把笔记本搬到拖拉机上,跑一遍 mpremote。

这一季改了什么

今年 3 月 28 日到 6 月 14 日之间,三个仓库有改动,大部分集中在 6 月的四天里。

用蓝牙取代 WiFi。 4 月 22 日,Web 服务器被拿掉了。那个提交删掉 5,689 行,新增 1,892 行;删掉的大部分是 vendored 的 Microdot 库以及配套的 HTML 和 JavaScript。取而代之的是一个 BLE 服务,为流量、GPS、状态、配置和累计脉冲计数分别提供特征值。校准改成按行进行,七个值而不是一个。脉冲计数器从每秒速率改成累计总量,所以一趟作业中间蓝牙断连,损失的是一秒的分辨率,而不是断连期间流出去的那些加仑。

真正的应用,而不是网页。 用的是 Expo 和 TypeScript,手机上跑 react-native-ble-plx 和 SQLite。逐行仪表、堵塞行告警、作业记录、CSV 导出,以及根据流量、地速和行距实时算出的每亩施氮磅数(28% UAN,每加仑 10.67 磅,含氮 28%)。iOS 端通过 TestFlight 装到我手机上,Android 端以签名 AAB 走 Play 内部测试,两边都由我地下室里自己的 Gitea Actions runner 构建,没用托管构建服务。两个平台,都是原生,没有一个是浏览器标签页连着一块单片机。

真能用的校准。 第一部分里每个传感器都是每升 450 个脉冲,因为数据手册上就是这么写的。现在应用里有了接液测试:选一行,输入你打算接的容积,点 Start,把液体放进量杯,点 Stop。Pico 上报的是累计脉冲数而不只是速率,所以应用把 Start 到 Stop 之间的计数做个差分,除以你实际接到的容积,再把这一行自己的数字通过蓝牙写回设备。七个传感器,七个数字,不用笔记本。

第七行。 我追肥用的机具是七行,所以 6 月 11 日盒子上多接了一路 GPIO 10,每条蓝牙消息也从六个值扩到七个。先记住这一点。

一个门户,这才是我真正在意的 Part 2 承诺。 这部分最让我意外,因为它是个小型 SaaS 应用,却完全跑在我自己的硬件上。后端用 Postgres 16 和 PostGIS,前端是 Next.js 加 MapLibre 做地图,再加一个瓦片服务器,全都跑在地下室一台迷你 PC 上的容器里。有登录、农场的组织、作业记录,还能把地块分享给别人。

往里灌数据不用我做任何事。我在一次作业里点下 Stop,应用就在后台上传;没信号就留在手机队列里,下次有信号再传上去。下面是 6 月 13 日的一次真实作业:

6 月 13 日的一次作业。每个点代表一秒的流量,放在它被测量到的位置,按总 GPM 着色。

每一个点都代表流量在发生位置对应的 GPS 坐标上被丢弃的一秒,按总 GPM 着色。82 分钟里共有 3,699 个点。传感器每秒采样一次,所以这两个数字本不该吻合,差出来的就是那一个半小时里用来转弯、加注或原地不动的二十多分钟。这些读数默认隐藏。想看完整轨迹时,勾选一个复选框就能把它们放回来。

这种颜色渐变是我从 CSV 里永远得不到的。底边上的橙色和红色不是堵塞行,而是每一趟结束时施药器离开地面、GPS 仍在记录。缩小视图,整趟作业的形状就出来了:

整场作业在田间的轨迹,每个地头和拐角处都是红色读数

同一趟作业缩小后的视图。每一簇红色都是地头或拐角,不是堵塞行。

作为对比……这是上个季度在 AI 帮忙之前的读数样子

每个地头、每个拐角都是红色。看过这个之后,“隐藏空闲读数”就不再是锦上添花,而是地图能看懂的唯一办法——这比我在第 2 部分里为它给的理由有说服力得多。

页面顶部那两个数字带着 CORRECTED 标记。意思是我事后就这次作业向门户交代了一些事情:把体积重新缩放到与罐里实际减少的量一致,或者用相邻行补上一个死行的流量。这些都不碰原始读数。修正存放在以作业为键的独立表里,在读取页面时应用,所以原始数据仍在下面,手机重新同步也冲不掉它。

通过蓝牙更新固件。 这块是我一个人绝不会去做的,也是最难啃的一块。6 月 11 日下午 6:16 第一次提交,6 月 12 日晚上就在真实硬件上端到端跑通。CI 构建并发布一个签名包;手机在有信号时下载,再在没信号的田里通过蓝牙转发过去。Pico 保留两份应用副本,并通过一个永不改变的小引导程序启动:新安装的副本必须能启动、运行并把自己标记为健康,连续崩溃三次的副本会被回滚到能用的版本。清单用 Ed25519 签名,并在 Pico 上验证,纯 MicroPython 实现,因为 MicroPython 既不带 SHA-512 也不带 base64 模块。验证大约需要 3 秒。那两天里一共发出了八个固件构建。

驾驶室里不再需要笔记本电脑了。

出问题的地方

施用量算高了六倍。 这条最要命,所以放在最前面。加仑每英亩的算法把总流量除以了单行的行距,而不是整个机具的幅宽。六行、1 GPM、5 mph、30 英寸行距:应用显示 237.6 加仑每英亩、709 磅氮每英亩。正确答案是 39.6 和 118。六行,高了六倍。而单元测试断言的正是 237.6,因为写 bug 的那次会话也写了那个附和它的测试。它编译通过,测试通过,在屏幕上给出一个自信满满的数字,唯一能抓住它的只有大致知道追肥用量该是多少的人。

iOS 死活看不见我的某一个蓝牙特征值。 我为固件批量数据传输建的那个通道,就是以 0007 结尾的那个,在服务发现阶段压根没出现。诊断版本终于打印出了手机能看到的东西:0001、0002、0003、0004、0005、0006、0008。唯独少了那一个。我试过把它设为可读,试过在它后面加一个填充通道让它不至于排在最后,试过轮换蓝牙地址让手机没法用缓存副本(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。蓝牙库在写入离开手机之前就把它拒了。一个包装函数就修好了。

Android 一拿到 GPS 定位就崩。 只在真机上、只在户外、只在有真实定位时出现——对调试来说,这组合简直妙不可言。后台定位任务要求 Android 把 job 持久化到重启之后,而任何应用只要提出这个要求却没声明开机权限,就会被 Android 杀掉。配置文件里加一行就修好了;找到这一行,靠的是一次本地签名构建,再用 USB 从手机上把崩溃日志拉下来。

第七行掉在了地上。 Pico 发了七个流量值,应用只存了六个。第七行的数据在写进数据库的路上被悄悄丢掉,持续了大约一天。这种 bug,只有当你盯着地图,纳闷它为什么永远只画六行的时候,才会发现。

AI 擅长什么,不擅长什么

擅长铺开面。这个项目从一个 MicroPython 脚本,长成了 Go 加 PostGIS 的 API、一个地图前端、一条带自动回滚的签名固件更新链路、一个从零写起的 Ed25519 验证器(因为平台上没有现成的),还有两条应用商店发布流水线。全在一个春天里完成,还赶上了播种。这些活我一个人是铺不开的,这点我不装。

不擅长知道那个数该是多少。上面列的每一个 bug 运行时都没报错。施肥量公式是最干净的例子:代码看着对,测试能过,屏幕上显示的数也说得通,误差却有六倍。再多的工具也不会对此有意见。这就是我想告诉任何打算这么干活的人的分界线。它写得快。但什么才算对,仍然得由你来判断——而在施肥量上,错一次的代价,是任何 Web 应用都不会有的。

接下来

我搭好了校验测试,然后一次都没跑过。七行数据至今还停在数据手册上的每升 450 个脉冲,也就是说地图上每一个加仑数字,都只跟我从 PDF 里抄出来的那个数一样可靠。这是清单上的第一件事,拿着量壶一个传感器一个传感器地测,因为它决定这张地图是好看,还是真实。之后是 Part 2 里答应你的电池,再往后,也许是把逐行施肥数据接进我正在做的其他农场工具里。

和上次一样。你不需要是农民,也不需要懂农业。如果你觉得我某个设计选择很蠢,或者某部分有更好的做法,在评论区告诉我。

Loading

Like Loading…

来源: Justin's IT Blog← 返回首页