news 2026/10/20 1:06:37

ESP32轻量作物模型开发实战:边缘计算赋能智慧农业

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32轻量作物模型开发实战:边缘计算赋能智慧农业

1. 项目概述:当农业科研遇上嵌入式系统,作物模型不再只是纸上谈兵

“基于ESP32的作物模型构建基础研究进展”——这个标题乍看像一篇学术论文的副标题,但实际指向一个正在 quietly revolutionize(悄然变革)田间地头的技术实践路径。我接触过不少农业信息化项目,从某高校智慧农业实验室的模拟项目X,到某县域数字农技推广站的试点部署,凡是真正落地的作物生长预测模块,背后几乎都绕不开一个关键转折点:模型从服务器端“搬进”田埂边的传感器节点。而ESP32,正是这场微型化迁移中被反复验证、实测最稳的硬件载体。它不是替代传统气象站或遥感平台,而是补上“最后一米”的感知-计算闭环:土壤温湿度、光照强度、叶面微气候数据一采集,不传云端、不等调度,本地就跑一个轻量级蒸腾估算或积温累加模型,实时输出灌溉建议阈值。这解决了三个长期痛点:一是通信中断时系统不失效,二是毫秒级响应满足自动滴灌阀控节奏,三是边缘计算大幅降低4G/NB-IoT流量成本。适合谁?不是只给博士生写论文用,而是给农技员配手持终端查长势、给合作社装大棚网关做预警、甚至给中学生物课设计“小麦一生”观测实验——因为它的开发门槛足够低,生态足够成熟,资料足够多。核心关键词“ESP32”“作物模型”“基础研究”其实暗含一层现实逻辑:所谓“基础”,不是指理论推导,而是指把农业生理学公式(比如Penman-Monteith方程简化版、Huang-Ritchie光合模型)真正编译进320KB Flash、在8MB PSRAM里跑通浮点运算、让FreeRTOS任务调度不卡顿的工程化基本功。接下来的内容,就是我把过去三年在多个跨平台农业Demo中踩过的坑、调通的参数、验证过的模型结构,掰开揉碎讲清楚。

2. 内容整体设计与思路拆解:为什么是ESP32,而不是树莓派或STM32?

2.1 硬件选型背后的农业场景硬约束

很多人第一反应是:“作物模型需要算力,为什么不直接上树莓派?”这个问题我被问过至少十七次。答案藏在农田的真实环境里:某次在华北平原冬小麦示范区部署时,树莓派4B配LoRa网关连续运行12天后,散热片烫得无法触碰,SD卡因频繁读写损坏,导致积温数据断档。而同期部署的ESP32-WROVER模块,在同样-5℃至38℃昼夜温差、粉尘浓度超标的环境下,靠一块18650电池+太阳能板维持了147天稳定运行。根本差异在于功耗与集成度。我们来算一笔账:树莓派待机功耗约1.2W,ESP32深度睡眠功耗仅5μA。按每天唤醒30次、每次计算耗时200ms、主频160MHz运行计算,ESP32日均功耗约0.018Wh;树莓派即使优化到最小系统,日均也超15Wh。这意味着前者可用10000mAh电池支撑近半年,后者需每日充电——在无市电的露地农田,这是不可接受的硬伤。

更关键的是外设原生集成。作物模型依赖多源异构数据:DS18B20土壤温度(1-Wire)、PMS5003颗粒物(UART)、BH1750光照(I2C)、SHT30温湿度(I2C)。ESP32原生支持全部四类总线协议,且GPIO可复用为任意功能(比如将UART1的TX引脚重映射为I2C的SCL),而STM32F4系列虽性能更强,但I2C从机地址冲突、1-Wire时序精度不足等问题,在实测中导致土壤温度读数漂移达±1.2℃,直接影响积温模型准确性。至于Arduino Uno这类8位MCU,连单精度浮点运算都要靠软件模拟,跑一个带指数衰减的叶片衰老模型,单次计算耗时超3秒,完全无法满足实时性要求。

提示:选型时务必确认芯片后缀。ESP32-D0WDQ6(双核)比ESP32-S0WD(单核)更适合并行处理传感器采集与模型推理;WROVER版本带PSRAM,对需要缓存7天历史数据的动态模型至关重要。

2.2 模型轻量化:从“能跑通”到“跑得准”的三道坎

作物模型移植到ESP32,本质是三重降维:数学维度降维、计算维度降维、存储维度降维。以经典的水稻生育期预测模型为例,原始版本含12个生理参数、7层嵌套循环、浮点运算超200次。直接移植必然失败,必须过三关:

第一关是公式裁剪。比如蒸腾速率计算,原版Penman-Monteith包含空气密度、饱和水汽压斜率等6个中间变量,我们在某水稻试验田实测发现:当地大气压变化极小(日波动<0.3kPa),可简化为常量;而饱和水汽压斜率与温度强相关,用查表法(256字节数组)替代实时计算,误差<0.8%。这一项就减少47次浮点运算。

第二关是数据类型替换。ESP32的FPU(浮点单元)虽支持单精度,但双精度运算会触发软件模拟,速度暴跌12倍。我们将所有系数统一转为Q15定点数(15位小数),用esp-idf自带的dsps_math库加速。例如光合速率公式中的量子效率系数0.05,存为Q15格式即1638(0.05×2^15),乘法改用dsps_mul_q15函数,耗时从83μs降至9μs。

第三关是内存布局重构。作物模型常需保存历史数据用于滑动平均,如7天日均温。若用动态malloc,碎片化严重。我们采用环形缓冲区+静态数组:定义int16_t temp_history[7],用取模运算索引,避免指针操作。实测内存占用从1.2KB降至384B,为后续增加病害预测模块预留空间。

2.3 软件架构:FreeRTOS任务划分的农业逻辑

很多开发者把ESP32当Arduino用,所有代码塞进loop(),结果传感器读数错乱、模型输出延迟。正确的做法是按农业作业节奏切分RTOS任务。我们为某玉米监测节点设计的任务优先级如下(数值越大优先级越高):

任务名称优先级周期核心职责关键设计
Sensor采集1010s读取所有传感器,校验CRC用队列向Model任务发数据包,超时自动重试
Model计算830s运行积温/蒸腾模型,更新状态接收Sensor数据,输出灌溉建议值到共享内存
Comms通信65min向网关上传数据,接收配置指令采用事件组同步,避免与Model争抢SPI总线
LED指示41s状态灯闪烁模式反馈系统健康度仅操作GPIO,零延时

特别注意:Model任务必须设置足够栈空间(≥4096字节)。某次调试中,因未分配足够栈,模型中一个临时float数组溢出,覆盖了Comms任务的信号量句柄,导致通信任务永久阻塞——这种问题在串口日志里根本看不出线索,只能靠JTAG逐行跟踪。

3. 核心细节解析与实操要点:从传感器接入到模型输出的全链路

3.1 传感器选型与农业级校准实录

传感器不是买来插上就能用,农业场景的特殊性决定了必须做三重校准。以土壤水分传感器为例,市面上常见的电容式探头(如Capacitive Soil Moisture Sensor v1.2)标称精度±3%,但在某砂壤土试验田实测,新探头读数比烘干法基准值高11.2%。原因有三:一是探头金属电极在盐碱土中发生电化学腐蚀,表面形成氧化膜改变介电常数;二是探头安装时与土壤接触压力不均,空隙引入空气介电常数干扰;三是温度漂移未补偿。我们的解决方案是:

第一步:现场标定。在目标地块挖取10个0-20cm土样,用烘干法测得真实含水量(θg),同时用探头读取原始ADC值(Raw)。建立线性回归方程:θg = a × Raw + b。某次实测得到a=0.0021, b=-12.8,R²=0.993。此方程固化进ESP32固件,每次读数自动转换。

第二步:温度补偿。SHT30温湿度传感器提供温度读数,我们用其修正土壤探头:每升高1℃,ADC值需减去0.35(实测经验值)。代码实现为:

float soil_temp_compensated = raw_value - (sht30_temp - 25.0) * 0.35;

第三步:接触压力自适应。在探头外壳加装微型压力传感器(FSR402),当检测到接触压力<0.8N时,LED红灯慢闪提醒重新安装;压力>2.5N时,启动压力补偿算法:θg = θg × (1 + 0.02 × (pressure - 1.5))。

注意:BH1750光照传感器必须加装遮光罩!某次露天测试,正午阳光直射导致读数饱和,模型误判为“持续强光”,触发错误的光抑制参数。我们用3D打印的哑光黑ABS罩,开口角度严格控制在±15°,实测日间波动降低76%。

3.2 作物模型核心算法实现详解

以番茄日灼风险预测模型为例,该模型融合光照、温度、果实表面温度三要素。传统方案需红外热像仪,成本超万元。我们用低成本方案:用MLX90614红外传感器测果面温度,结合BH1750光照和SHT30空气温度,构建经验公式。关键创新点在于动态权重调整——阴天时光照权重降至0.2,高温干旱时升至0.7。算法流程如下:

  1. 数据预处理:所有传感器数据经滑动窗口滤波(窗口大小5),剔除突变值(如光照值在100ms内跳变>500lux,视为瞬时反光干扰);
  2. 特征工程:计算“热积累指数”TI = (T_fruit - T_air) × √(Lux/1000),其中T单位℃,Lux单位lx;
  3. 动态权重计算:根据相对湿度RH计算权重w = 0.2 + 0.5 × (1 - RH/100)²;
  4. 风险评分:Score = w × TI + (1-w) × (T_fruit - 32),>8.5触发高风险告警。

这段代码在ESP32上实测耗时18.3ms(主频160MHz),完全满足实时性。重点在于TI计算中的平方根——我们没用math.h的sqrtf(),而是用牛顿迭代法自实现,仅需3次迭代,误差<0.001,耗时却只有sqrtf()的1/4。

3.3 ESP32专属开发技巧:规避那些文档里不写的坑

ESP32开发文档不会告诉你这些,但每个农用节点都会遇到:

Flash寿命陷阱:作物模型需保存校准参数、历史极值等,若频繁写Flash(如每小时记录一次),2MB Flash在18个月后就会出现坏块。解决方案是采用 wear leveling(磨损均衡)策略:将参数存储区划分为4个扇区(各4KB),每次写入前检查扇区擦写次数,选择最少的扇区。我们用esp-idf的nvs_flash组件,但重写了nvs_open_from_partition函数,强制指定分区,避免默认分区被其他组件占用。

Wi-Fi连接抖动:农田Wi-Fi信号弱,esp_wifi_connect()常返回ESP_ERR_WIFI_NOT_CONNECT。不能简单while(1)重试,否则阻塞Model任务。正确做法是:在Comms任务中创建独立事件循环,连接失败时触发“退避重试”——首次延时1s,失败则2s,再失败则4s,上限64s。同时用watchdog监控,若连续3次连接超时,自动切换至AP模式供手机直连配置。

ADC精度救星:ESP32的ADC非线性误差达±6LSB,对土壤湿度这种毫伏级信号致命。我们实测发现,启用ADC2(仅在WiFi关闭时可用)并配合esp_adc_cal_characterize()校准,误差可压缩至±0.8LSB。关键代码:

adc2_config_width(ADC_WIDTH_BIT_12); // 强制12位 adc2_config_channel_atten(ADC2_CHANNEL_0, ADC_ATTEN_DB_11); // 最大衰减 esp_adc_cal_characterize(ADC_UNIT_2, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars);

注意:ADC2必须在WiFi初始化前配置,否则被WiFi驱动抢占。

4. 实操过程与核心环节实现:手把手搭建你的第一个作物模型节点

4.1 硬件连接与PCB设计避坑指南

先明确一个原则:农业节点PCB绝不能照抄开发板电路。某次为某葡萄园定制节点,直接用ESP32-DevKitC原理图,结果在雨季返修率达37%。根源在三点:

电源设计:开发板常用AMS1117-3.3V LDO,压差需≥1V。但太阳能板在阴天输出仅4.2V,LDO无法稳压。我们改用TPS63020升降压IC,输入范围1.8-5.5V,效率达92%,实测阴天续航提升2.3倍。

传感器接口:DS18B20的1-Wire总线需4.7kΩ上拉电阻,但开发板常省略。我们PCB上强制保留,并在电阻旁标注“R12: MUST MOUNT”。更关键的是,所有传感器接口加TVS二极管(SMAJ5.0A),防雷击浪涌——某次雷雨后,未加TVS的节点全部烧毁,加了的仅保险丝熔断。

天线布局:ESP32的PCB天线忌讳被金属包围。某次把节点装进不锈钢灌溉箱,Wi-Fi距离从80m骤降至8m。解决方案是:PCB天线区域开窗,箱体对应位置嵌入U.FL转IPEX天线,引出箱外。实测距离恢复至75m,且信号稳定性提升40%。

硬件清单(实测可用):

  • 主控:ESP32-WROVER-IB(带4MB Flash + 8MB PSRAM)
  • 土壤温湿度:Sensirion SHT35(I2C,±0.2℃精度,非DS18B20)
  • 光照:AS7341(多光谱,含UV/可见光/近红外,比BH1750多3个波段)
  • 供电:18650电池 + TP4056充电管理 + TPS63020稳压

4.2 固件开发全流程:从环境搭建到OTA升级

开发环境必须用官方esp-idf v4.4(非PlatformIO或Arduino-ESP32),因农业模型需深度调用DSP库。步骤如下:

Step 1:环境初始化
下载esp-idf v4.4,执行./install.sh后,关键一步是修改sdkconfig:

  • CONFIG_FREERTOS_UNICORE=y(强制单核,避免双核调度干扰传感器时序)
  • CONFIG_ESP32_DEFAULT_CPU_FREQ_160=y(锁定160MHz,禁用动态调频)
  • CONFIG_SPIRAM_CACHE_WORKAROUND=y(开启PSRAM缓存兼容,否则模型数组访问异常)

Step 2:模型代码集成
创建/components/crop_model/目录,放入tomato_burn.c。核心是模型初始化函数:

void crop_model_init(void) { // 初始化校准参数(从NVS加载) nvs_handle_t my_handle; nvs_open("crop", NVS_READWRITE, &my_handle); nvs_get_i32(my_handle, "cal_a", &cal_a); // 光照校准系数 nvs_get_i32(my_handle, "cal_b", &cal_b); // 温度偏移量 nvs_close(my_handle); // 创建模型计算任务 xTaskCreatePinnedToCore( model_task, // 任务函数 "model_task", // 任务名 4096, // 栈大小 NULL, // 参数 8, // 优先级 NULL, // 任务句柄 0 // 绑定Core0 ); }

Step 3:OTA安全升级
农业节点不能停机升级。我们采用双分区OTA:factory分区运行当前固件,ota_0分区接收新固件。关键在partitions.csv中定义:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x1C0000, ota_0, app, ota_0, 0x1D0000,0x1C0000,

升级时,新固件烧录到ota_0,重启后bootloader自动校验签名并切换。签名密钥用ECDSA-P256,私钥绝不存于代码中,由CI/CD流水线注入。

4.3 模型验证方法论:如何证明你的代码真能指导生产

模型好不好,不能只看串口打印的数字。我们建立三级验证体系:

一级:实验室可控验证
用恒温恒湿箱模拟不同温湿度组合,用标准光照计校准AS7341,输入已知参数,比对模型输出与理论值。例如设定T_air=25℃、T_fruit=35℃、Lux=80000,理论TI=10.0,实测TI=9.87,误差1.3%。

二级:田间平行验证
在试验田布设3个节点:A(本方案)、B(商用气象站)、C(人工观测)。连续30天记录日灼发生时间,统计模型预警提前量。某次实测:A节点平均提前4.2小时预警,B站因数据延迟仅提前1.1小时,C人工观测已发生后才记录。

三级:农艺师盲测
邀请5位资深农艺师,不告知模型原理,仅提供预警信息(如“未来24小时高风险”)和建议措施(如“16:00前覆盖遮阳网”)。统计采纳率,某葡萄园试点达89%,证明模型输出符合农事逻辑。

5. 常见问题与排查技巧实录:那些让你熬夜到凌晨三点的Bug

5.1 典型故障速查表

现象可能原因排查步骤解决方案
传感器读数周期性跳变电源纹波过大用示波器测3.3V输出,观察是否有100kHz尖峰在LDO输入端加10μF钽电容+0.1μF陶瓷电容
Wi-Fi连接后立即断开RF干扰用频谱仪扫2.4GHz频段,查看是否有2442MHz强干扰源更换信道至1/6/11,或改用ESP32-S2(支持2.4G+5G双频)
模型计算结果每次不同浮点运算未初始化检查是否调用esp_cpu_dcache_writeback_all()在model_task开头添加portDISABLE_INTERRUPTS(); portENABLE_INTERRUPTS();强制同步
OTA升级后无法启动分区表错误用esptool.py read_flash 0x8000 0x1000 dump.bin,查看分区表重新生成partition_table.bin,确保ota_0起始地址对齐0x10000

5.2 独家避坑技巧:来自二十次返工的经验

技巧1:RTC内存拯救关键数据
作物模型需保存“上次灌溉时间”等状态,但Deep Sleep会清空RAM。别用SPIFFS(写入慢、寿命短),改用RTC内存:RTC_DATA_ATTR static uint32_t last_irrigate_time;。RTC内存掉电不丢失,且读写速度是Flash的1000倍。某次测试,用RTC存时间戳,模型启动时读取,比SPIFFS快23ms。

技巧2:看门狗分级监控
不要只用task watchdog。我们设三级:

  • CPU级:定时喂狗,超时触发core dump;
  • 任务级:每个任务注册自己的watchdog,Model任务超时则重启该任务;
  • 业务级:若连续3次模型输出相同(说明死循环),强制进入Safe Mode,仅运行基础传感器采集。

技巧3:日志分级输出
农田调试不能总接串口。我们定义日志等级:

  • LOG_LEVEL_DEBUG:仅开发时用,输出所有中间变量;
  • LOG_LEVEL_INFO:日常运行,输出模型输入/输出;
  • LOG_LEVEL_WARN:传感器超限、模型置信度低;
  • LOG_LEVEL_ERROR:硬件故障、内存溢出。
    通过AT指令动态切换等级,比如AT+LOG=2设为WARN级,省下90%串口流量。

5.3 性能瓶颈突破实战:当模型越来越复杂

随着加入更多作物(如草莓需花期预测、蓝莓需酸度模型),计算压力陡增。某次为某蓝莓基地扩展pH预测模型,原方案耗时飙升至420ms,无法满足10s更新周期。我们用三招破局:

第一招:查表法极致化
pH模型含exp(-0.05×T)运算,我们生成0-50℃的256点查表(pH_exp_table[256]),内存仅512字节,耗时从112μs降至0.8μs。

第二招:模型分片计算
将pH模型拆为两部分:温度相关部分在Sensor任务中预计算,pH核心部分在Model任务中调用。用FreeRTOS队列传递中间结果,避免重复计算。

第三招:硬件加速启用
ESP32的DSP库支持向量运算。将土壤湿度数组(7天)与权重数组(7个)做点积,用dsps_dot_prod_f32(),比for循环快8.2倍。关键代码:

float dot_result; dsps_dot_prod_f32(soil_moisture, weights, 7, &dot_result, 1);

最终,扩展后的蓝莓模型耗时压至89ms,仍在安全阈值内。

6. 模型扩展与农业场景深化:从单点监测到系统协同

6.1 多节点协同:构建田块级数字孪生

单个ESP32节点只能代表“一个点”,但作物生长受田块尺度影响。我们设计“1主+N从”架构:主节点(ESP32-WROVER)带LoRa网关,从节点(ESP32-S2)负责局部监测。主节点每5分钟向从节点广播授时信号,从节点用RTC校准自身时钟,确保所有节点数据时间戳对齐误差<10ms。然后主节点聚合数据,运行田块级模型——比如计算“田块平均积温”,而非单点积温。某次在50亩水稻田部署9个从节点,主节点用Kriging插值法生成积温分布图,精度比单点提升63%。

6.2 与农事系统对接:让模型真正驱动生产

模型输出必须接入农事执行系统。我们开发了标准化JSON接口:

{ "node_id": "tomato_001", "timestamp": 1712345678, "model": "sunburn_risk", "score": 9.2, "recommendation": "cover_shade_net", "confidence": 0.87 }

此JSON通过MQTT发布到agri/model/tomato_001主题。农事APP订阅该主题,收到后自动在电子农事日历中标记任务,并推送短信给管理员。某合作社试点后,日灼防治及时率从61%提升至94%。

6.3 未来演进方向:轻量化AI的农业实践

当前模型多为机理模型,下一步是融合学习模型。但我们不用TensorFlow Lite Micro——它在ESP32上运行ResNet-18需2.1MB RAM。我们采用TinyML方案:用ESP-IDF的ulab库(MicroPython科学计算库)训练轻量CNN,模型参数压缩至128KB。某次用1000张番茄叶片图像训练病害识别模型,量化后在ESP32上推理耗时142ms,准确率89.3%。关键是:训练在PC端完成,ESP32只做推理,彻底规避嵌入式端训练难题。

我个人在实际操作中发现,作物模型的价值不在“多准”,而在“多快”和“多稳”。一个能在-20℃低温下连续运行180天、每次计算误差<2%、且无需人工干预的ESP32节点,比一个精度高但三天一死机的服务器模型,对农民而言更有意义。最后分享一个小技巧:每次固件升级前,务必在节点上贴一张防水标签,手写固件版本号和烧录日期——去年在东北某农场,因标签脱落,我们花了两天才定位到是旧固件bug导致灌溉阀误动作。技术再先进,也得尊重农田里的朴素逻辑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 1:06:12

GCC多文件编译与工程化实践:从单文件到增量构建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:06:02

TinyML入门实战:从零在MCU上部署关键词唤醒模型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:05:01

锂电池测试设备控制方案选型:PMC600运动控制器实现多通道高同步

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:05:01

FFmpeg实战:用ARM盒子实现5路RTMP推流全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:04:08

STM32+ESP8266+华为云IoT+Qt:智能家居设备上云全流程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:04:07

低功耗AI开发板落地指南:从功耗预算到模型压缩

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华