news 2026/10/16 5:06:18

ESP32本地OTA升级实战:用网页上传固件,不拆机不改线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32本地OTA升级实战:用网页上传固件,不拆机不改线

简介:ESP32 HttpServer模式下的本地OTA例程是一份基于ESP-IDF实现的完整参考实现,类似Arduino环境下的OTAWebUpdater,面向需要为设备添加局域网无线升级能力的嵌入式开发者,支持AP与STA两种工作模式,无需额外OTA服务器。压缩包共1116个文件,约35.37MB,包含c/cpp/h等源码文件、cmake构建脚本、html网页、sh脚本,以及大量编译中间产物(obj、a、elf、map等),可完整还原ESP-IDF工程构建过程。目前已有3814人学习使用。例程提供完整的WiFi连接初始化(AP/STA)、运行于89端口的OTA服务器(含固件上传页面、POST接收与固件信息查询URI);固件上传页面由原生JS实现,支持上传进度、速度与错误显示;内置固件诊断功能通过将GPIO2拉高验证固件运行状态,失败自动回滚;附带BuildVer.sh脚本,可根据编译时间自动生成版本号文件,便于版本管理。

1. 本地 OTA 不是伪需求:给 ESP32 留一个网页后门

做嵌入式开发的人早晚会碰到这个场景:设备已经焊在产线上、装在现场机柜里,或者给了客户做 demo,固件要升级但手里没有 USB 线,更不可能拆壳。云端 OTA 要搭服务器、要处理证书和密钥,很多项目根本等不到那一步,需要的是一个局域网内打开浏览器就能升级的方案。ESP32 的 HttpServer 模式本地 OTA 例程解决的正是这个问题——设备启动一个 HTTP 服务,用网页上传新固件,写入另一个 app 分区,重启后切到新固件运行,全程不需要串口线、不需要额外工具。这篇笔记适合两类人:一类是刚接触 ESP-IDF、想把手上的例程改造成可用的 OTA 升级;另一类是已经在产品里用 Arduino 的 OTAWebUpdater,想迁移到 IDF 平台,但不想从零踩 partition、流程和校验的坑。

2. OTA 前置知识:分区表布局与固件 bin 结构

2.1 为什么 IDF 没有直接给现成的网页上传例程

Arduino 生态里的 OTAWebUpdater 是一段调用现成库的代码,背后把 HTTP 服务、固件接收、flash 写入都封装好了。ESP-IDF 没有打包一个一模一样的例程,原因是 IDF 把 OTA 流程拆成了两层:底层是esp_ota_ops这套分区读写接口,上层是 HTTP server 组件。你的任务是把两者拼起来,中间还得自己处理接收缓冲、写入对齐、失败回滚这些细节。这个拆分本身是合理的,因为产品场景差异太大——有的要对接自己的后台鉴权,有的要从 SD 卡升级,有的要支持多设备同时升级,统一成一个网页上传反而是限制。

理解这个分层之后,你再看例程代码就不会觉得它“缺东西”,而是能看到每一层各自的职责边界。HTTP 层只负责收字节流,OTA 层只负责把字节流写到正确的 flash 地址,中间的粘合逻辑——比如写上 4KB 再 flush、写完校验、boot 分区切换——才是例程最值得读的部分。

2.2 分区表:OTA 能不能跑起来,全看这里

ESP32 的 flash 启动依赖分区表,OTA 的原理是“当前从 A 分区启动,把新固件写到 B 分区,重启后 bootloader 根据 OTA 数据分区里的记录切换到 B”。所以分区表里必须有至少两个 app 分区,且类型写成ota_0、ota_1,而不是factory。一个常见的误区是:项目默认的 partitions.csv 只分配了factory一个 app 分区,烧录和运行都正常,但esp_ota_get_next_update_partition()返回 NULL,日志里报No OTA partition found。

我一般会用下面这个精简分区表,适合 4MB flash 的芯片:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, ota_0, app, ota_0, 0x10000, 0x1F0000, ota_1, app, ota_1, 0x200000, 0x1F0000,

两个 app 分区各约 1.9MB,对绝大多数应用够用了。说明几点:nvs保持默认位置和大小,因为 Wi-Fi 校准数据、系统参数都存在里面,偏移变化可能引发意外行为;ota_0的偏移必须从0x10000开始,这是 bootloader 之后第一个对齐的位置;两个分区之间不需要留空隙,ota_1紧跟在ota_0后面即可。如果你的固件超过 1.9MB,就把 flash 换成 8MB 型号,或者砍掉nvs里用不到的空间,但不要压缩ota_0/ota_1的对齐边界。

配置分区表有两种方式:在 menuconfig 里把Partition Table选为Custom partition table CSV,指定你放 CSV 文件的路径;或者直接用idf.py menuconfig里的默认分区表,只动 app 分区大小。我更推荐前者,因为 CSV 是文本文件,方便放进 git 管理,别人拿到项目不会因为 menuconfig 的配置文件没导出而编译出错误布局。

2.3 固件 bin 里到底有什么

很多人直接烧录生成的project.bin,其实 bootloader 启动时对每个 app 分区都会做校验,校验依据是 bin 文件头部的一个结构体esp_image_header_t。它包含魔数、芯片型号、分段信息、入口地址等。这是 ESP-IDF 的启动机制决定的,不是 OTA 特有的——即便是第一次烧录,烧录工具也要先擦除再写整个 flash 区域,而 OTA 只把 app 分区的空间当作“空盘”来写。

理解这个头部结构的意义在于:OTA 写入完成后,esp_ota_end()会做一次完整的镜像校验,包括头部魔数、段加载地址、CRC。这一步不能跳,跳过的结果是重启后 bootloader 检查失败,设备起不来。实际编码里,你可以读取esp_app_desc_t拿到固件版本号和项目名,这是编译时由 CMake 自动写入 bin 的描述信息,在网页端可以把版本号显示出来,避免升级后不确定到底跑没跑新固件。

const esp_app_desc_t *app_desc = esp_app_get_description(); ESP_LOGI("OTA", "Current version: %s", app_desc->version);

这段代码放到启动初始化里,日志会打印当前固件版本。第一次升级前先跑一遍,确认旧版本号;升级后再跑一遍,看到版本变化,整个链路就闭环了。要注意esp_app_get_description()返回的是当前正在运行的分区里的描述信息,不是 flash 里任意分区的,所以它能真实反映 boot 状态。

3. 把 OTAWebUpdater 的思路搬到 ESP-IDF:核心代码拆解

3.1 HTTP Server 初始化:单线程还是多线程

ESP-IDF 的 HTTP server 组件底层用 esp_http_server 实现,例程里创建 server 时几个参数值得先想清楚。max_uri_handlers决定能注册多少个处理函数,本地 OTA 场景两个就够:一个响应 GET 返回网页,一个响应 POST 接收固件。stack_size直接影响接收大文件时能不能撑住,固件动辄几百 KB,接收循环里要反复调用httpd_req_recv,栈太小会溢出导致重启。我一般设为 8192,配合一个 1KB 的临时接收缓冲,实测整包固件接收不会爆栈。

httpd_handle_t server = NULL; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 80; config.max_uri_handlers = 4; config.stack_size = 8192; config.uri_match_fn = httpd_uri_match_simple; httpd_start(&server, &config);

uri_match_fn这里用默认的简单匹配即可,不需要正则。server_port用 80 方便浏览器直接访问设备 IP,不用输端口。如果项目里已经有其他服务占了 80,把这里改成 8080 并在网页脚本里同步修改 fetch 地址。启动后把server句柄保存成全局变量,OTA handler 里要往同一个 server 注册其他 URI 时能用上。

3.2 处理固件上传:不要解析 multipart,直接收原始二进制流

Arduino 的 OTAWebUpdater 用的是 form 表单上传,表现为multipart/form-data格式。IDF 例程里可以做同样的事,但你要自己解析 multipart 边界、处理Content-Disposition、跳过前面一段文本,这个工作量没有意义。更干净的做法是前端用 JavaScript 直接fetch发送 ArrayBuffer,后端按application/octet-stream接收原始字节流。这样前后端代码都简单,出错排查路径也短。

static esp_err_t ota_post_handler(httpd_req_t *req) { esp_ota_handle_t ota_handle; const esp_partition_t *update_part = esp_ota_get_next_update_partition(NULL); if (update_part == NULL) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No OTA partition found"); return ESP_FAIL; } esp_err_t err = esp_ota_begin(update_part, OTA_SIZE_UNKNOWN, &ota_handle); if (err != ESP_OK) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "esp_ota_begin failed"); return ESP_FAIL; } char buf[1024]; int remaining = req->content_len; size_t written = 0; while (remaining > 0) { int received = httpd_req_recv(req, buf, MIN(remaining, sizeof(buf))); if (received <= 0) { esp_ota_abort(ota_handle); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Receive error"); return ESP_FAIL; } esp_ota_write(ota_handle, buf, received); remaining -= received; written += received; } err = esp_ota_end(ota_handle); if (err != ESP_OK) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "esp_ota_end verify failed"); return ESP_FAIL; } err = esp_ota_set_boot_partition(update_part); if (err != ESP_OK) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Set boot partition failed"); return ESP_FAIL; } httpd_resp_sendstr(req, "OTA done, rebooting..."); vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); return ESP_OK; }

这段代码有几个关键点。esp_ota_get_next_update_partition(NULL)返回的是未在运行的那个app 分区,不需要手动指定 ota_0 还是 ota_1,这是 IDF 自动管理的。esp_ota_begin的第一个参数是目标分区,第二个参数填OTA_SIZE_UNKNOWN表示不知道固件大小,它会按实际写入的字节数来算,代价是会多占用一些 flash 做擦除;如果你在网页上传前就知道文件大小,可以用req->content_len传进去,能精确预分配。httpd_req_recv的返回值是实际收到的字节数,可能小于请求的大小,所以用remaining计数而不是假设一次收完。

esp_ota_write是整包写入,内部每 4KB 对齐就会触发一次 flash 擦写。这个动作是阻塞的,如果固件有 1.5MB,循环要持续几百毫秒到一秒多。期间任务会被占用,esp_task_wdt可能触发看门狗——这个问题放在避坑章节细说。

3.3 网页前端:一个按钮完成选择文件和上传

服务端拿原始二进制流之后,网页端就不需要multipart了。HTML 里放一个<input type="file">加一个按钮,JavaScript 读取文件内容后直接 POST。这里有几个细节值得注意:file输入框要加accept=".bin"限制文件类型;上传期间要把按钮禁用,防止用户重复点击导致两个请求并发写同一分区,后一个会覆盖前一个,结果不可控。

static esp_err_t root_get_handler(httpd_req_t *req) { const char* html = "<!DOCTYPE html>" "<html><body>" "<h2>ESP32 Local OTA</h2>" "<input type='file' id='fw' accept='.bin'>" "<button onclick='upload()'>Upload</button>" "<script>" "async function upload(){" "const f=document.getElementById('fw').files[0];" "if(!f)return;" "const buf=await f.arrayBuffer();" "const resp=await fetch('/ota',{method:'POST',body:buf});" "alert(await resp.text());" "}" "</script>" "</body></html>"; httpd_resp_set_type(req, "text/html"); httpd_resp_sendstr(req, html); return ESP_OK; }

f.arrayBuffer()会把整个固件读进浏览器内存,固件 1MB 级别没有任何压力。fetch默认的Content-Type是text/plain;charset=UTF-8,服务端不校验该字段,只关心 body 原始字节。如果将来要加鉴权,可以在 header 里带 token,服务端在ota_post_handler里检查httpd_req_get_hdr_value_len对应的字段。不建议用 URL 参数带 token,浏览器历史记录和日志里会留下痕迹。

3.4 注册 URI 和启动时序

GET/和 POST/ota两个 handler 要在app_main里注册,顺序无所谓。注册之后要确认启动时没有其他任务抢占 httpd 句柄,尤其如果你的项目用了 Wi-Fi 事件循环,不要在初始化完成前就调用httpd_start。我一般把整个 httpd 初始化放进一个独立任务里,或者位于wifi_connect成功之后,避免 AP 模式没起来时网页请求打到空 socket 上。

httpd_uri_t root_uri = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &root_uri); httpd_uri_t ota_uri = { .uri = "/ota", .method = HTTP_POST, .handler = ota_post_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &ota_uri);

这里有个不容易发现的点:httpd_register_uri_handler如果注册失败,比如同名 URI 已存在,会返回错误码。如果项目代码里之前已经注册过/ota,重复注册会导致第二次 OTA 请求直接命中旧 handler。排查时看日志里有没有httpd_register_uri_handler failed,有的话立刻检查是不是有两处初始化代码。

4. 编译、烧录与网页升级:三步跑通本地 OTA 链路

4.1 改分区表之后的编译流程

拿到例程第一步不是急着写代码,而是先把分区表换成 2.2 节里那个版本。在项目根目录建一个partitions_ota.csv,然后执行:

idf.py set-target esp32 idf.py menuconfig

在 menuconfig 里进入Partition Table→Partition Table→ 选Custom partition table CSV,然后填partitions_ota.csv的路径。保存退出后再确认一下CONFIG_PARTITION_TABLE_CUSTOM_FILENAME的值是不是你写的文件名。常见翻车点是路径写错导致编译阶段找不到文件,报错信息是Failed to parse partition table CSV file。路径用相对项目根目录的写法,别写绝对路径,否则换台电脑就编译不过。

编译命令照常:

idf.py build

编译产物里会多出build/ota_data_initial.bin。这个文件的作用:第一次烧录时把 OTA 数据分区初始化成“当前从 ota_0 启动”的状态。只烧 app 分区而不烧这个文件,第一次启动时 OTA 数据是脏的,esp_ota_get_boot_partition()可能返回错误。所以首次烧录我建议直接:

idf.py -p /dev/ttyUSB0 flash monitor

这条命令会依次烧 bootloader、分区表、ota_data_initial、两个 app 分区中的当前编译产物,一次性把基础状态铺好。之后每次开发迭代只需要烧 app 分区,可以直接:

idf.py -p /dev/ttyUSB0 app-flash

flash monitor会把串口输出接到终端,Ctrl+]退出,不要用Ctrl+C硬断,容易让串口驱动状态卡住。

4.2 网页升级操作的完整路径

设备上电后串口日志会打印 IP 地址。在浏览器地址栏输入http://<设备IP>,看到上传页面后,先点选择文件选编译好的build/project.bin,注意不要选成bootloader.bin或partition-table.bin。点击上传后观察网页返回OTA done, rebooting...,然后等设备重启。重启完成后再次打开网页,或者看串口日志里打印的新版本号。

网页升级期间设备是独占的,不能有多个浏览器标签同时操作,也不要刷新页面。页面刷新会中断 TCP 连接,服务端httpd_req_recv会返回 0 或负值,进入 abort 分支。这不算故障,但会白等一次。我一般是上传前把浏览器调成飞行模式隔离外部网络,避免页面里其他资源请求抢占设备的带宽——虽然 HTTPServer 本身处理并发请求没有问题,但 Wi-Fi 模块同一时间只能服务有限的吞吐,固件传一半被杀掉重来不划算。

4.3 验证升级成功:不只看版本号

重启后用以下命令确认运行状态:

idf.py -p /dev/ttyUSB0 monitor

日志里会看到类似Booting partition 'ota_1'或者Running partition 'ota_1'(取决于 IDF 版本),这比看版本号更可靠。版本号可能因为编译配置没更新而没变,但启动分区一定不会撒谎。如果日志里显示Booting partition 'factory',说明 OTA 数据分区可能没写对,或者 bootloader 没有按预期找 OTA 分区。

const esp_partition_t *running = esp_ota_get_running_partition(); ESP_LOGI("OTA", "Running from %s", running->label);

这段代码放在app_main最前面,每次启动都打印当前运行分区,可以省掉至少一半的“到底升没升成功”的猜疑。如果你在网页上传后重启发现还是旧分区,优先查esp_ota_set_boot_partition的返回值,它失败的唯一常见原因是传入的 partition 不是有效的 OTA 分区类型。

5. 避坑:本地 OTA 最容易翻车的五个细节

5.1 上传大固件时任务看门狗重启

现象:固件 1MB 以上,传输到一半设备突然重启,重启后还在旧固件上,浏览器报连接失败。

原因:esp_ota_write内部擦写 flash 期间阻塞任务执行,单次写入超过几百 ms,esp_task_wdt判定任务卡死触发重启。HTTP server 组件的工作任务同样受看门狗管理。

解决:在接收循环里定期调用esp_task_wdt_reset()喂狗。最稳妥的做法是不要把整个接收循环放在一个任务里跑,而是每收到一包数据就vTaskDelay(1)让出 CPU,同时喂一次狗。我项目里的写法是:

esp_task_wdt_reset(); vTaskDelay(pdMS_TO_TICKS(1));

5.2 第二次升级时esp_ota_begin返回 ESP_ERR_INVALID_SIZE

现象:第一次 OTA 成功,第二次上传时日志报esp_ota_begin failed,错误码0x1602或类似值,网页卡在esp_ota_begin failed。

原因:上一次升级中断,比如传输时断电、浏览器刷新,esp_ota_abort没来得及执行,目标分区里残留了半截固件数据。esp_ota_begin发现分区里已有数据且大小不匹配,拒绝继续写。

解决:在esp_ota_begin之前调用esp_partition_erase_range把目标分区整个擦掉。不过更保险的方式是:处理esp_ota_begin返回ESP_ERR_INVALID_SIZE时,执行一次擦除再重试。代码里要小心——擦除 2MB 分区需要几百 ms,这个动作同样会触发看门狗,记得也喂狗。

err = esp_ota_begin(update_part, OTA_SIZE_UNKNOWN, &ota_handle); if (err == ESP_ERR_INVALID_SIZE) { esp_partition_erase_range(update_part, 0, update_part->size); err = esp_ota_begin(update_part, OTA_SIZE_UNKNOWN, &ota_handle); }

5.3 上传完成并重启,但esp_ota_set_boot_partition没生效

现象:上传页面显示OTA done, rebooting...,重启后日志显示仍然从ota_0启动,新固件没跑起来。

原因:esp_ota_set_boot_partition调用成功了,但随后esp_restart()执行得不够晚,或者说你调用了它但返回到 handler 后没给 flash 写入留出完成时间就重启了。另外一个隐藏因素:如果 OTA 数据分区本身空间不足或者页对齐有问题,bootloader 会在启动时忽略该记录。

解决:esp_ota_set_boot_partition之后增加一个 100ms 左右的延时,让 flash 写完。同时检查 OTA 数据分区的 size 设置,nvs后面的空余空间必须能容纳 OTA 数据(通常是 0x2000 字节)。我碰到过一次把nvs分区设置到 0x6000 导致 OTA data 无处安放,改了分区表恢复正常。

5.4 网页请求超时,接收循环卡死

现象:上传固件过程中浏览器提示连接超时,设备串口日志停在某个esp_ota_write调用上没有后续输出。

原因:httpd_req_recv是阻塞调用,浏览器端因某种原因断开连接后,服务端不会立即感知,要等 TCP 超时。如果接收循环里没有加超时控制,这个任务会一直挂着。

解决:给循环加一个整体超时判断。remaining在减少,但长时间没有数据进来时记录当前 tick,超过 30 秒主动 abort。代码里判断received == 0时直接退出循环走失败分支,避免无限等待。

if (received == 0) { ESP_LOGW("OTA", "Connection closed by peer"); esp_ota_abort(ota_handle); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Connection closed"); return ESP_FAIL; }

5.5 分区表变更后直接烧 app,没有重烧 bootloader

现象:改完分区表重新编译,烧录后设备不断重启,日志显示Invalid partition table。

原因:flash 里的旧 bootloader 和分区表还在,新的 app 代码运行地址和旧分区表不一致。分区表改了偏移,但 bootloader 校验 app 时读的是旧位置。

解决:改分区表之后第一次烧录一定要全量烧,不能只app-flash。执行一次idf.py flash把 bootloader、分区表、OTA 数据、app 全部重写。后续不改分区表的情况下才可以用app-flash快速迭代。这个坑我踩过一次之后,凡是改分区表,第一反应就是idf.py flash,不再动脑子想“到底烧哪些”。

6. 收尾技巧:升级失败自动回滚与版本强制校验

先说你最多 20 分钟就能加上的一个保护:让设备“自己知道自己跑得好不好”。ESP-IDF 的 OTA 机制有一个回滚标志位,启动后esp_ota_mark_app_valid_cancel_rollback()会把当前固件标记为有效,如果应用没有调用这个函数就崩溃重启,bootloader 会自动切回上一个分区。这个机制叫APP_ROLLBACK_ENABLE,在 menuconfig 里默认是关闭的,打开方式:Component config → ESP32-specific → Enable app rollback support。

打开这个选项后,启动流程变成:新固件从 ota_1 启动,先不标记有效,初始化完 Wi-Fi、连上网络、外设检查通过,再调用esp_ota_mark_app_valid_cancel_rollback()。如果新固件在初始化阶段崩溃,重启后自动回到 ota_0,不会卡死在坏固件上。把这个函数放在app_main靠后的位置,而不是最开头,给系统留出自检时间。

另一个值得做的是版本号校验。例程里没做,但实际产品里经常会遇到“传错了 bin”这种低级错误。方法是在ota_post_handler里读esp_app_desc_t结构。.bin文件开头就带这个结构,你可以在接收前先读req->content_len,如果不足一个 app_desc 的长度直接拒绝:

if (req->content_len < sizeof(esp_app_desc_t)) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Too small to be a firmware"); return ESP_FAIL; }

更进一步的校验是解析 bin 里的 project name 和 version,跟当前运行版本比较,防止同事把另一款产品的固件传上来。不过解析要等接收完才能做,因为esp_ota_write已经写进 flash 了,如果版本不匹配要esp_ota_abort再返回错误。我在项目里加过一段:接收完通过esp_ota_get_running_partition和读到的版本打印对比日志,人工确认比自动拦截更省事,因为同一芯片的多款变体固件经常共享版本号,机械比较反而误报。

电脑上存一份idf.py erase-flash的快捷命令,当固件回滚也失败、设备完全没法启动时,全擦重来是最后的后悔药。全擦之后别忘了一件事:重新烧ota_data_initial.bin,否则第一次启动会读不到合法的启动分区记录,又得折腾一轮。从那以后我每次改 OTA 相关代码,都强制走一遍“编译 → 全量烧录 → 网页升级一次 → 再降级一次 → 确认回滚”,整个链路跑通才算验收。希望帮到你。

本文还有配套的精品资源,点击获取

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

PyTorch核心设计与工程实战:从动态图到分布式训练

做深度学习这一行&#xff0c;绕不开的一个名字就是PyTorch。无论是刚入门的同学写自己的第一个图像分类Demo&#xff0c;还是工业界跑大规模分布式训练&#xff0c;这两年基本都被它统一了。我最初是从别的框架转过来的&#xff0c;心里其实有点抵触&#xff1a;又要学一套API…

作者头像 李华
网站建设 2026/10/16 5:04:57

Windows本地部署Sam3图像分割模型:从环境配置到Demo实战

最近在Windows电脑上折腾本地部署Sam3&#xff0c;从装环境到跑通demo&#xff0c;踩了不少坑。这个模型在图像分割任务上确实好用&#xff0c;输入一张图片&#xff0c;不管你是想抠人像还是分割某个杂物&#xff0c;都能在不需要额外训练的情况下直接给出目标区域的像素级掩码…

作者头像 李华
网站建设 2026/10/16 5:03:34

AI日报写作方法论:从信息筛选到价值输出的完整指南

1. 一份“AI 日报”到底在记录什么每天早上九点前&#xff0c;我会把过去二十四小时里散落在各个信息源里的 AI 动态过一遍&#xff0c;筛掉噪音&#xff0c;留下真正值得关注的那几条&#xff0c;然后写成一份不超过三千字的日报。这个习惯从两年前开始&#xff0c;到现在已经…

作者头像 李华
网站建设 2026/10/16 5:03:30

Claude Opus 5.5不直接生成视频?代码讲解视频的五步AI流水线实战

很多人听到“Claude Opus 5.5 并不会‘生成’视频”这个说法&#xff0c;第一反应是不信——毕竟这名字看起来就跟“多模态生成”沾边。但做过技术视频的人都知道一个事实&#xff1a;这类模型擅长的是写代码、写文案、理思路&#xff0c;它根本不会直接输出一段 MP4 给你。真正…

作者头像 李华
网站建设 2026/10/16 5:03:28

MP2642GR-Z+ATmega1280双电池主动均衡设计实战

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

作者头像 李华
网站建设 2026/10/16 5:03:24

闲置Mac mini变身7×24小时AI助手:OpenClaw+API网关全落地

家里那台 Mac mini 吃灰大半年之后&#xff0c;我终于给它找到一个全职岗位&#xff1a;724 小时不关机的 AI 助手。整套方案说简单也简单&#xff1a;一台基础款 Mac mini 常开&#xff0c;跑一个叫 OpenClaw 的开源助手框架&#xff0c;模型请求统一走一层自己搭的 API 网关/…

作者头像 李华