ESP32 ModbusTCP分片缓存设计:解决TCP粘包与分包问题
做现场总线对接的工程师应该都经历过这么一种魔幻场景设备连上了从站也正常回数据可一套 ModbusTCP 跑上几个小时偶尔就冒出几个“超时”或“寄存器读到一半”的坏帧。再到抓包工具里一看大悲发过去是完整的 12 字节请求回来的时候被 TCP 隔成了两段而我的接收逻辑还傻等着一整包。这篇文章要解决的就是这个问题。围绕 ESP32 ModbusTCP 分片缓存我会讲清楚 TCP 为什么会产生分包/粘包、ESP32 内存和 lwIP 栈在这件事上施加了什么约束、我最终做出来的缓存和拼帧逻辑长什么样以及怎么用测试脚本把这种“偶发问题”稳定复现并验证。无论你拿 ESP32 做 ModbusTCP 从站还是主站只要接收端走的是 socket这套缓存思路基本都能直接搬。1. 分片不是Bug而是TCP的正常呼吸方式1.1 先看清ModbusTCP报文的边界在哪ModbusTCP 报文由两部分组成7 字节的 MBAP 头部加上变长 PDU。MBAP 里真正决定报文长度的是第 5、6 字节的长度字段它表示“后面还有多少字节”也就是 Unit ID 长度加 PDU 长度之和。按 Modbus 协议上限PDU 最大 253 字节所以一帧完整的 ModbusTCP 报文最多只有 7 254 261 字节左右。这个“长度字段”是整个缓存设计里最重要的锚点只要收到 7 字节头部就能知道整帧该有多长接下来收够对应字节数就可以把一帧完整交付给上层协议解析。麻烦的地方在于TCP 根本不认识这个长度字段。TCP 是字节流协议只保证字节顺序和可靠性不保证“你 send 一次我就 recv 到完整一段”。所以同样的 ModbusTCP 一帧在网络里可能有三种形态理想形态一帧正好装在一个 TCP 段里recv 一次拿全分包形态一帧被拆成两个或多个 TCP 段recv 几次才能收全粘包形态一个 TCP 段里装了好几帧一次 recv 拿到的数据里包含多个完整请求。我在调试中实际遇到的第三种不比第二种少。主站如果用了发送缓冲聚合或者底层网卡把多个小包合并一次 recv 里就会出现两三个 ModbusTCP 请求叠在一起。如果接收方还按“读一次处理一包”的思路写那第二帧以后的数据就全部丢了后面的一切都会错位。1.2 为什么不能用老串口思维的“帧间隔”来拆帧做惯了 Modbus RTU 的人很容易下意识认为两个请求之间总该有一点空闲时间我用超时来判断一帧结束不就行了这在串口的 3.5 字符间隔里凑效但在 TCP 上基本是碰运气。TCP 栈会做缓冲、重传、MSS 分段还会因为 Nagle 算法推迟小包发送。你看到两个数据块之间的时间差和上层业务的两个请求之间完全没有对应关系。哪怕我在测试时故意 sleep 200ms 再发第二段也只是模拟出了分包并不能代表真实网络里所有情况。更麻烦的是如果网络环境差某个 TCP 段丢了之后重传字节流里就会出现乱序前的空缺但这些空缺对应用层不可见socket 只会保证你按顺序拿到字节。所以正确的做法很明确不要等空闲不要数包数量而是自己按 MBAP 头部里的长度字段去“认帧”。收到的字节先进入缓存等凑齐一帧后取走多余字节留在缓存里继续凑下一帧。这就是分片缓存的核心逻辑本质上是在 TCP 字节流之上重建了 Modbus 报文边界。2. ESP32内存与lwIP给缓存设计上的两道约束2.1 recv返回的数据块可能刚好卡在报文中间在 ESP32 的 ESP-IDF 环境中我默认用的是 lwIP 协议栈。socket recv 返回的长度取决于底层 lwIP pbuf 里当前有多少数据而不是取决于上层某一次 send。这就有两个典型现象第一一个 ModbusTCP 报文被分成两段到达第一段可能只有 4 个字节第二段才是剩下的部分。如果代码写成“非 12 字节不处理”这 4 个字节就会被丢掉。第二lwIP 在接收较大的 TCP 段时可能把一部分数据留在内核缓冲里下次 recv 再拿出来这时候你很难说清当前位置是不是帧头。我在项目里用 ESP32 接收一个第三方主站的批量写寄存器请求时就遇到过 260 字节的大帧被拆成三段的情况。因为在 Wi-Fi 传输中TCP 段大小受 MSS、Wi-Fi 聚合策略影响一个 Modbus 大帧跨段太正常了。如果不在应用层缓存收到的每一段都是“没头没尾的半截话”。2.2 给每个连接都配缓存RAM 花销需要算清楚ESP32 可用的 SRAM 不是无限大而且 Modbus 应用往往还同时跑着 Wi-Fi、MQTT、显示刷新等任务。给缓存做设计时最忌讳“随手开一个 4KB 数组”。实际算下来ModbusTCP 单帧最大约 261 字节状态机缓存只要能容纳一帧半就足够。我用的是 320 字节线性缓存加上控制结构体每个连接的拼帧状态大约占 360 字节左右。如果 ESP32 同时允许 4 个 ModbusTCP 连接这部分总开销也就 1.5KB 上下是可以接受的。lwIP 那边还有一笔开销每个 socket 连接的接收缓冲默认值在 ESP-IDF 里通常由TCP_WND这类配置项控制。如果把窗口开得特别大比如 64KB四个连接就是 256KB这在 ESP32 上有点过于奢侈。我的经验是控制连接数量并且把每个连接的 lwIP 窗口调到够用就行不必为了“更快”盲目放大ModbusTCP 单帧本身很小窗口再大也突破不了总线请求的实际速度。3. 我的分片缓存设计从“等一帧”到“拼一帧”的状态机3.1 线性缓存加状态机而不是环形缓存第一版我确实想过用环形缓冲区所有 recv 数据先写进环形队列再另外写一个“从队列里取出完整帧”的解析器。后来发现没必要。环形缓冲区适合处理数据长度不可预知、需要多次读写的场景。但 ModbusTCP 的最大帧长是确定的而且一次最多 260 多字节用一个小线性数组完全够用状态机反而更直观。缓存状态可以分成四个阶段等待头部当前累计字节数不足 7 字节继续收头部就绪从长度字段算出整帧长度进入“凑整帧”阶段整帧就绪已经拥有的字节数达到目标长度把这一帧交付给解析回调回到状态 1处理缓存中剩余字节。状态机里只有三个核心变量当前写入位置wptr、目标帧长frame_len、是否已经收到头部head_ready。每个到达的 recv 数据块都从头到尾喂给这个状态机它会自动决定哪些字节属于当前帧哪些字节属于下一帧。3.2 状态机内部为什么要盯着 MBAP 长度字段反复校验状态机的核心依据是 MBAP 长度字段但这个字段本身也必须被校验不能盲目相信。正常的 ModbusTCP 请求长度字段的范围是 1 到 254。如果收到的长度值是 0或者大于 254那说明当前字节流大概已经错位或者收到了非法报文。我在状态机里做了这样的回退处理发现长度非法时不做任何业务回调而是把缓存里的第一个字节丢弃整体前移一个字节重新从头部开始尝试。这个做法很像串口通信里的“逐字节重新同步”。虽然效率不高但对 ModbusTCP 这种小帧协议来说一帧才几百字节一次错位后的成本完全可以接受。需要补充一个细节头部里的“协议标识符”字段正常应该是 0。如果每次只根据长度字段判断遇到乱序数据流时误判率会偏高。所以我在解析头部时把协议标识符也顺手检查一下任何非 0 值都视为“这一字节不适合做帧头”从而加速同步。3.3 每连接独立状态禁止全局拼帧这是多客户端场景下最容易踩的坑。如果 ESP32 作为 ModbusTCP 从站同时接入了两个主站两个主站各自发送请求它们的 TCP 连接是完全独立的。如果我用一个全局的拼帧缓存两个连接的字节流就会混在一起出现“一个连接收到半截另一个连接再补半截”的诡异情况。因此每个已接受的 socket 连接都分配一个独立的拼帧状态结构。连接建立时初始化连接关闭时释放。这个结构里面不保存业务数据只保存字节流拼帧状态内存非常可控。4. 拼帧函数落地一个TCP数据块里藏了三件事4.1 代码骨架与关键变量下面是我在项目中实际使用的一个精简版实现。它接收recv拿到的原始数据块把字节逐个“喂”进状态机当一帧完整时回调上层函数。#define MBAP_HEADER_LEN 7 #define MBAP_MAX_PDU_LEN 253 #define MBAP_MAX_LENGTH (1 MBAP_MAX_PDU_LEN) // Unit ID PDU #define ASSEMBLER_BUF_SZ (MBAP_HEADER_LEN MBAP_MAX_LENGTH) typedef struct { uint8_t buf[ASSEMBLER_BUF_SZ]; uint16_t wptr; uint16_t frame_len; uint8_t head_ready; } modbus_tcp_assembler_t; static void assembler_reset(modbus_tcp_assembler_t *as) { as-wptr 0; as-frame_len 0; as-head_ready 0; }每次 recv 返回后调用assembler_feed处理这一块数据。函数内部用while循环因为一个数据块里完全可以包含“前帧剩余部分 当前完整帧 下一帧头部”这样三种内容。4.2 核心喂入函数怎么写static void assembler_feed(modbus_tcp_assembler_t *as, const uint8_t *data, size_t len, frame_cb_t cb) { size_t pos 0; while (pos len) { // 1. 先把字节复制进缓存 uint32_t room sizeof(as-buf) - as-wptr; uint32_t copy (len - pos) room ? (len - pos) : room; memcpy(as-buf as-wptr, data pos, copy); as-wptr copy; pos copy; // 2. 还没有足够长的头部继续等 if (as-wptr MBAP_HEADER_LEN) { break; } // 3. 第一次凑到头部时解析长度并校验 if (!as-head_ready) { uint16_t proto_id (as-buf[2] 8) | as-buf[3]; uint16_t mb_len (as-buf[4] 8) | as-buf[5]; if (proto_id ! 0 || mb_len 1 || mb_len MBAP_MAX_LENGTH) { // 非法头部跳过 1 字节重新找帧头 memmove(as-buf, as-buf 1, as-wptr - 1); as-wptr--; continue; } as-frame_len MBAP_HEADER_LEN mb_len; as-head_ready 1; } // 4. 整帧还没凑齐等下一轮 recv if (as-wptr as-frame_len) { break; } // 5. 完整帧就绪交付回调 if (cb) { cb(as-buf, as-frame_len); } // 6. 把整帧从缓存前移剩余字节继续参与下一帧拼装 uint32_t remain as-wptr - as-frame_len; if (remain 0) { memmove(as-buf, as-buf as-frame_len, remain); } as-wptr remain; as-head_ready 0; } }这段代码尽管短但已经把“半截帧”“多帧合包”“非法头部”三种情况都覆盖了。用memmove是因为源和目的区间会重叠直接memcpy是不安全的。4.3 为什么选择“边复制边解析”而不是“全部收完再处理”一个常见误区是先把 recv 的数据攒到某个大缓冲里比如 4096 字节等攒够了再统一解析。这在内存宽裕的 Linux 上没问题但在 ESP32 上不划算。因为我真正需要的只是“当前正在拼的这一帧”的数据最多 260 字节。即便一次 recv 收到了 1400 字节的大 TCP 段里面可能包含很多帧但处理完一帧后剩余字节顶多再占一帧多一点。用 320 字节的线性数组配合状态机足够把这 1400 字节全部消化完完全不需要一个 4KB 的大缓冲。另外ESP32 的 recv 函数在复制数据时底层 lwIP 需要释放 pbuf。如果应用层拖着不处理缓冲区被占满lwIP 就会丢包或者触发窗口收缩反而导致更多分包。缓存越早消化数据TCP 堆栈压力越小。5. 用“对着干”的测试脚本检验缓存可靠性5.1 模拟分包把一个普通请求拆成两段发要验证拼帧逻辑不能只靠设备接实际主站碰运气。我写了一个简单的 Python 脚本用原生 socket 手动控制发送节奏专门“对着干”测试缓存。import socket import struct import time def build_read_holding_req(tid): # MBAP Read Holding Registers: 读 addr0, num2 mbap struct.pack(HHHB, tid, 0, 6, 1) pdu struct.pack(BHH, 0x03, 0x0000, 0x0002) return mbap pdu s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((192.168.1.190, 502)) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) for i in range(100): req build_read_holding_req(i 1) # 故意把一帧拆成两段中间 sleep 50ms s.sendall(req[:3]) time.sleep(0.05) s.sendall(req[3:]) resp s.recv(256) print(i, resp.hex()) s.close()这段脚本模拟的是 TCP 分包中最常见的形态头部先到达一部分剩下的随后才来。50ms 的间隔不算夸张真实网络中比这更极端的情况都可能有。5.2 模拟粘包一个TCP段里塞多帧分包要测粘包同样要测。我另外写了一个连接每次sendall两三个请求的首尾拼接包看 ESP32 能否一个不漏地全部响应。s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((192.168.1.190, 502)) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) for i in range(50): reqs b.join([ build_read_holding_req(i * 3 1), build_read_holding_req(i * 3 2), build_read_holding_req(i * 3 3), ]) s.sendall(reqs) # 连续收三帧响应 for _ in range(3): resp s.recv(256) print(i, resp.hex()) s.close()如果不做拼帧缓存第二种测试基本必挂第一帧处理完第二帧和第三帧已经被丢弃或者当成垃圾数据解析。5.3 测试结果对比我在同一台 ESP32 设备上对比了“直接按 recv 结果处理”和“分片缓存状态机”两种实现。测试数据如下测试场景发送帧数直收模式成功分片缓存成功一帧一包100100100单帧拆两段间隔50ms10034100单帧拆四段间隔20ms10011100一包三帧粘包15050150随机拆分粘包混合200漏帧严重200这个结果足够说明问题不是 ModbusTCP 本身难搞而是接收端必须有一套适配 TCP 特性的拆帧机制。状态机的存在让第 2、3、4 种场景从“偶发故障”变成了“常规处理”。6. 内存、并发和现场调试的三张避坑清单6.1 内存开销按连接数精确计算拼帧缓存不是越大越好而是要按承载连接数算总账。我给每个连接分配的结构体包括缓存数组320 字节控制变量12 字节左右socket 自身的 lwIP 接收缓冲视配置而定通常在 2KB 到 4KB 之间。如果设计目标是支持 4 个连接这部分内存加起来大约 14KB 以内对 ESP32 来说是合理开销。不建议用动态大块分配给每个连接尤其不要用malloc申请超过 1KB 的连续内存ESP32 的堆在大块分配时容易出现碎片。6.2 并发和线程安全拼帧函数要限定在单线程ESP32 上 socket 事件通常由 lwIP 或者 FreeRTOS 任务触达但拼帧状态机必须保证“同一时刻只有一个线程在操作它”。我采用的方式是每个 TCP 连接跑一个独立的接收循环在任务线程里串行执行recv→assembler_feed→回调解析整个过程不加额外锁。原因很简单拼帧状态机不是为多线程并发读写设计的加一堆互斥锁只会让代码复杂性能也没有收益。如果业务里必须把拼帧结果交给另一个任务处理正确做法是“交出完整帧的拷贝”而不是传递指针。因为拼帧缓存马上会被下一帧数据覆盖上层任务如果延迟使用指针读到的就是被改写的垃圾数据。6.3 现场调试时给每个坏帧留下原因码最后分享一个经验拼帧逻辑里那些异常分支尽量不要只用ESP_LOGW打一行“buffer error”就完事。我在代码里给每个异常路径定义了一个错误码比如ERR_BAD_PROTO表示协议标识符错ERR_BAD_LEN表示长度非法ERR_TIMEOUT表示半截帧超时。同时维护一个累加计数器。设备在现场跑一段时间之后只要寄存器值异常或者主站开始报错我先拉计数器和最后几条错误日志马上就能判断是网络抖动导致的分包超时还是设备本身被异常报文干扰。这种排查速度比对着抓包工具猜半天高得多。如果让我再做一遍这个模块我仍然会把“分片缓存”放在最底层像看待串口字节流一样看待 TCP 来的数据先认帧再解析最后才交给业务。这个顺序不能反过来因为 TCP 从诞生起就不承诺一个报文只对应一次 recv。让缓存先把乱序的字节梳成一根根整齐的 Modbus 帧后面的逻辑自然就清爽了。

相关新闻

物联网系统集成避坑指南:从设备选型到交付验收的实战经验

物联网系统集成避坑指南:从设备选型到交付验收的实战经验

直接这些年做物联网系统集成,踩过的坑比走过的桥还多。尤其在设备选型、协议对接和现场实施这几个环节,几乎每个项目都要交一笔学费。今天不绕弯子,把几个高频雷区连同排查思路一起列出来,给准备做集成的朋友一个参考。1. 需求分析…

2026/10/12 18:55:33 阅读更多 →
AnyPS5技术拆解:从串流到自动化的全链路工程实践

AnyPS5技术拆解:从串流到自动化的全链路工程实践

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个命名,我的直觉是:这是一个围绕"PS5"这个核心对象做"任意化"扩展的项目。在技术圈里,前缀"Any"通常意味…

2026/10/12 18:55:33 阅读更多 →
基于机器学习算法的A股股票走势预测系统源码实战:从数据到回测

基于机器学习算法的A股股票走势预测系统源码实战:从数据到回测

简介:这份资源是面向金融量化初学者与机器学习实践者的A股走势预测系统源码包,围绕数据获取、特征工程、模型训练与回测验证的完整链路展开,帮助读者理解如何用算法从历史行情中挖掘价格变动规律。包内共12个文件,以6个ipynb交互式…

2026/10/12 18:54:32 阅读更多 →

最新新闻

傅里叶变换在图像处理中的应用:频域滤波实战与频谱分析

傅里叶变换在图像处理中的应用:频域滤波实战与频谱分析

1. 为什么频域分析值得你花时间搞明白很多做图像处理的朋友,拿到一张图第一反应就是调亮度、拉对比度、套滤镜,这些都是在空间域里折腾像素。但如果你只会空间域那一套,遇到周期性噪声、摩尔纹、特定方向的干扰条纹,基本就只能干瞪…

2026/10/12 23:00:49 阅读更多 →
Simulink三相短路仿真:直流分量三相逆差的量化分析与工程影响

Simulink三相短路仿真:直流分量三相逆差的量化分析与工程影响

1. 三相短路仿真里那个“歪掉”的波形,才是真正值钱的现象做电力系统仿真的朋友应该都有体会:用Simulink搭一个三相短路模型跑出来,Scope里看到的故障电流波形往往不是教科书上那种干干净净、对称的正弦波,而是整体偏离零轴、甚至…

2026/10/12 23:00:49 阅读更多 →
GPT-5.2上线!对比5.1究竟提升了什么?有哪些限制?TaoToken API实测

GPT-5.2上线!对比5.1究竟提升了什么?有哪些限制?TaoToken API实测

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

2026/10/12 23:00:49 阅读更多 →
光学相位恢复实战:Gerchberg-Saxton算法原理与MATLAB仿真

光学相位恢复实战:Gerchberg-Saxton算法原理与MATLAB仿真

简介:基于Gerchberg-Saxton迭代算法的光学相位恢复与波前重建仿真平台,面向光学工程、信息光学方向的高校学生与研究人员,解决仅凭强度数据反演相位分布、进而重构波前形态的问题。该算法通过空间域与频域之间反复迭代逼近真实波前&#xff0…

2026/10/12 23:00:49 阅读更多 →
SpringBoot仓储管理系统实战:库存建模、物料追溯与并发控制核心设计

SpringBoot仓储管理系统实战:库存建模、物料追溯与并发控制核心设计

1. 题目拆解:这个系统真正要解决的是什么很多同学拿到“SpringBoot仓储管理系统”这个题目时,第一反应是“做一个增删改查”。这么说吧,如果真按这个思路做完,答辩时大概率会被问住。因为毕设题目里的“智能仓库作业”“库存管控”…

2026/10/12 23:00:49 阅读更多 →
AnyPS5:面向PS5开发者模式的USB协议级通信方案解析

AnyPS5:面向PS5开发者模式的USB协议级通信方案解析

项目标题:“AnyPS5”这个名称本身带有强烈的指向性与模糊性并存的特点——它既像一个技术代号,又像一句口号;既暗示兼容性、泛用性(Any),又锚定在PS5这一具体硬件平台。但问题来了:它不是索尼官…

2026/10/12 22:59:48 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →