# LwIP FTP 升级功能说明(最终版) ## 1. 设计目标 - 在 **不依赖 4G 模块 AT 指令** 的情况下,通过 **以太网 (LwIP socket)** 实现 FTP 固件下载。 - 复用已有的 `flash_upgrade.h` 升级区管理: - `upgrade_erase_program_area()` 擦除升级区域; - `upgrade_write_packet()` 按 `PACKET_SIZE`(1024 字节)分包写入; - `upgrade_success_update_header()` 更新升级头(`UPGRADE_FLAG_IDLE / UPGRADE_FLAG_NEED_UP`)。 - 与 4G FTP 升级整体思路保持一致,但实现形式为: **非阻塞状态机 + 周期调用 `v_ftp_update_action()`**。 ## 2. 模块文件结构 位于 `app/plat_comm/lwip_module/`: - `lwip_module.[ch]` - 对 LwIP socket 做简单封装:`lwip_resolve_host / lwip_set_nonblock / lwip_tcp_send / lwip_tcp_recv / lwip_tcp_close` 等。 - `lwip_ftp_upgrade.[ch]` - **网线 FTP 程序远程升级的完整状态机实现**,仅保留这一对文件: - 控制连接(21 端口); - `USER/PASS` 登录; - `TYPE I` 二进制模式; - `CWD`(可选目录切换); - `SIZE` 查询文件大小; - `PASV` 被动模式解析数据通道; - `RETR` 下载文件并按 1024 字节分包写 Flash; - `QUIT` 结束会话并写入 NEED_UP 升级头。 - 对外只暴露两个接口: - `void lwip_ftp_upgrade_init_from_tftp(void);` - `int lwip_ftp_upgrade_step(void);` > 说明:旧版的 `lwip_ftp_fw_upgrade_api.[ch]`、`lwip_ftp_upgrade_flash_adapter.[ch]`、`lwip_ftp_fw_upgrade_sm.[ch]` 等文件已删除,不再使用。 ## 3. 参数来源与触发方式 ### 3.1 参数结构:`T_FTP_UPDATE_INFO` 在 `bs_public_impl.h` 中定义,关键字段如下: - `u8_update_server_addr`:FTP 服务器域名 / IP; - `u16_update_server_port`:FTP 端口(通常 21); - `u8_user / u8_pwd`:用户名、密码; - `u8_path`:远程目录(可为空); - `u8_file_name`:文件名; - `u8_netType`:网络类型(`0=4G`,`1=网线`); - `u32_file_size`:文件大小(由 FTP SIZE 结果回填); - `u8_update_result`:升级结果(0=成功,1=失败)。 这些字段由 OCPP `UpdateFirmware` 命令解析函数 `parse_ftp_url()` 填充。 ### 3.2 触发流程 1. OCPP 平台下发 `UpdateFirmware`,URL 形如: `ftp://user:pass@host:port/path/file.bin` → 解析到全局 `t_ftp_update_info`。 2. 上层把对应平台的 `u8_updata_flag` 置位。 3. `plat_comm_task.c` 中的 `v_Plat_Comm()` 周期调用 `v_ftp_update_action()`。 4. `v_ftp_update_action()` 内部根据: - `s_bs_task_ctrl.ocpp_data.u8_log_file_upload`:`1=日志上传,0=程序升级`; - `t_ftp_update_info.u8_netType`:`0=4G`(走 4G 模块),`1=网线`(走 LwIP FTP)。 ## 4. 程序升级流程(网线 / LwIP) 当满足: - `s_bs_task_ctrl.ocpp_data.u8_log_file_upload == 0`(程序升级); - `t_ftp_update_info.u8_netType != 0`(网线/LwIP); `v_ftp_update_action()` 中流程为: ```c if (t_ftp_update_info.u8_init_flag == 0) { if (t_ftp_update_info.u8_netType == 0) { v_ftp_ctrl_init(); // 4G 模块初始化 } else { lwip_ftp_upgrade_init_from_tftp(); // 网线 FTP 状态机初始化 } t_ftp_update_info.u8_init_flag = 1; } if (t_ftp_update_info.u8_netType == 0) { u8_ec200a_4g_ftp_update(); // 4G FTP 升级 } else { int ret = lwip_ftp_upgrade_step(); // LwIP FTP 升级状态机单步 if (ret == 1) { // 升级成功 s_bs_task_ctrl.s_bs_ctrl[0].u8_updata_flag = 0; s_bs_task_ctrl.s_bs_ctrl[1].u8_updata_flag = 0; t_ftp_update_info.u8_update_result = 0; } else if (ret < 0) { // 升级失败 s_bs_task_ctrl.s_bs_ctrl[0].u8_updata_flag = 0; s_bs_task_ctrl.s_bs_ctrl[1].u8_updata_flag = 0; t_ftp_update_info.u8_update_result = 1; } } ``` ### 4.1 状态机步骤(`E_LWIP_FTP_STEP`) `lwip_ftp_upgrade.c` 内部定义的状态机大致流程: 1. `LWIP_FTP_STEP_CONNECT` - 建立 FTP 控制连接(21 端口),并设置为非阻塞。 2. `LWIP_FTP_STEP_WAIT_WELCOME` - 等待 `220` 欢迎语。 3. `LWIP_FTP_STEP_SEND_USER / WAIT_USER` - 发送 `USER xxx`,等待响应; - 若返回 `331`,继续发送 `PASS`;否则直接进入 `TYPE`。 4. `LWIP_FTP_STEP_SEND_PASS / WAIT_PASS` - 发送 `PASS xxx`,等待登录结果。 5. `LWIP_FTP_STEP_SEND_TYPE / WAIT_TYPE` - 发送 `TYPE I`,切换为二进制模式。 6. `LWIP_FTP_STEP_SEND_CWD / WAIT_CWD`(可选) - 如果 `u8_path` 非空,发送 `CWD ` 切目录。 7. `LWIP_FTP_STEP_SEND_SIZE / WAIT_SIZE` - 发送 `SIZE `,并从 `213` 响应中解析文件大小; - 回填到 `s_ctx.file_size` 和 `t_ftp_update_info.u32_file_size`。 8. `LWIP_FTP_STEP_PREPARE_FLASH` - 调用 `upgrade_erase_program_area()` 擦除升级区; - 构造 `upgrade_header_t`,写入 `UPGRADE_FLAG_IDLE / file_size / packet_count` 等信息。 9. `LWIP_FTP_STEP_SEND_PASV / WAIT_PASV` - 发送 `PASV`,解析 `227` 响应得到数据通道 IP/端口。 10. `LWIP_FTP_STEP_CONNECT_DATA` - 按 `data_ip / data_port` 建立数据连接 socket。 11. `LWIP_FTP_STEP_SEND_RETR / WAIT_RETR` - 发送 `RETR `,等待 `150/125` 等开始传输响应。 12. `LWIP_FTP_STEP_RECV_DATA` - 循环接收数据,按 1024 字节分包写入 Flash(见 4.2)。 13. `LWIP_FTP_STEP_SEND_QUIT` - 发送 `QUIT`,关闭控制连接; - 写入 `UPGRADE_FLAG_NEED_UP` 头部,标记升级就绪。 14. `LWIP_FTP_STEP_DONE / FAIL` - `DONE`:整个流程成功,`lwip_ftp_upgrade_step()` 返回 `1`; - `FAIL`:任意步骤出错,返回 `<0`。 ### 4.2 分包写 Flash 与进度打印 - 内部使用一个 **动态申请的** 1024 字节缓存 `packet_buf`(不是全局大数组): - 在 `lwip_ftp_upgrade_init_from_tftp()` 中第一次调用 `pvPortMalloc(PACKET_SIZE)`(FreeRTOS 堆),后续重复使用; - 失败会直接打印 `"pvPortMalloc packet_buf fail"` 并进入 `FAIL`。 - 数据通道每次 `lwip_tcp_recv()` 得到的数据都会写入 `packet_buf`: - 每累计满 1024 字节调用一次 `upgrade_write_packet(packet_index, packet_buf, len)`; - 处理最后不足一包的数据时,也会单独调用一次。 - **接收进度打印**: - 每次收到数据后,都会根据 `downloaded / file_size` 计算百分比: ```c Plat_Comm_LOG("lwip ftp: recv %lu/%lu (%u%%)\r\n", ...); ``` - 方便在串口日志中实时观察下载进度。 ### 4.3 控制通道响应等待(最长 1 秒) - 所有 FTP 控制命令的响应(`220 / 331 / 230 / 213 / 227 / 150 ...`)统一通过: ```c static int ftp_read_reply_block(int sock, uint32_t timeout_ms, int *out_code, char *out_msg, size_t out_msg_sz); ``` - 关键特性: - **内部使用局部缓冲区**: - `char rx_buf[FTP_CTRL_RX_BUF_SIZE];` - `char line[FTP_LINE_MAX];` - 不再在全局结构体中定义大数组。 - 每次调用最长阻塞 `timeout_ms` 毫秒(本工程中统一传入 `1000`,即 1 秒): - 在 1 秒内尽可能收满一条完整响应行; - 超时、对端关闭或错误均返回非 0,状态机进入 `FAIL`。 - 满足需求: - **“发送命令后可最长等待 1 秒接收数据,这样可以保证大部分交互在一个周期内完成”**。 ## 5. 日志上传(4G / 预留 LwIP) - 当 `s_bs_task_ctrl.ocpp_data.u8_log_file_upload == 1` 时,`v_ftp_update_action()` 走“日志上传”分支: - 当前仅在 `t_ftp_update_info.u8_netType == 0`(4G)时调用 `u8_ec200a_4g_ftp_log_upload()`; - 网线场景下会打印提示,尚未提供 LwIP 日志上传实现。 后续如需支持 **LwIP FTP 日志上传**,建议在 `lwip_ftp_upgrade.[ch]` 基础上扩展 `STOR/APPE` 等命令。 ## 6. 注意事项 - **非阻塞总框架 + 局部 1 秒等待**: - 整体采用状态机 + 周期调用的方式集成在 `v_Plat_Comm` 中,不新建线程; - 单条 FTP 命令的响应等待时间 **最多 1 秒**,保证大部分交互能在一次 `v_ftp_update_action()` 周期内完成。 - **内存使用**: - 不再在全局结构体中定义大数组: - 控制通道解析使用的缓冲区全部为函数内局部变量; - 数据分包缓冲 `packet_buf[PACKET_SIZE]` 采用 FreeRTOS `pvPortMalloc` 动态申请,仅 1KB。 - **文件大小依赖 SIZE**: - 必须保证 FTP 服务器支持 `SIZE` 命令,否则无法写入正确的 `file_size`,升级头将不可靠。 - **错误处理与重试**: - 任意步骤出错,`lwip_ftp_upgrade_step()` 返回 `<0`; - `v_ftp_update_action()` 会清除 `u8_updata_flag` 并设置 `u8_update_result = 1`; - 上层可根据日志与结果决定是否重新触发升级。 ## 7. 与 4G FTP 的对比 - **4G FTP**: - 文件:`ec200a_4g_ftp.c`; - 通过 AT 命令驱动 4G 模块内置 FTP 客户端; - 状态机在 `u8_ec200a_4g_ftp_update()` 内实现。 - **LwIP FTP(网线)**: - 文件:`lwip_module.c` + `lwip_ftp_upgrade.c`; - 使用 LwIP socket 直接实现 FTP 协议; - 与 4G 共用 `flash_upgrade` 升级区与头部结构; - 集成方式完全嵌入 `v_ftp_update_action()`,不影响 `v_Plat_Comm` 线程的主循环。