Initial commit: CCU621_M firmware project with BLE debug link support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-08 17:28:36 +08:00
commit 9ceb218f80
1597 changed files with 724159 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
/**
* @file lwip_ftp_upgrade.h
* @brief 基于 LwIP socket 的 FTP 固件升级状态机(网线远程升级)
*
* 说明:
* - 参考 ec200a_4g_ftp.c 的“状态机 + 周期调用”风格;
* - 通过 LwIP socket 实现 FTP 控制/数据连接,按 1024 字节分包写入升级区域;
* - 每次调用只推进一小步,不阻塞 v_Plat_Comm 线程。
*/
#ifndef LWIP_FTP_UPGRADE_H
#define LWIP_FTP_UPGRADE_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 初始化 LwIP FTP 升级状态机
*
* 说明:
* - 直接读取全局 t_ftp_update_info 中的 FTP 参数(server/port/user/pass/path/file_name);
* - 清空内部静态状态结构体,关闭残留 socket,准备进入 CONNECT 步骤。
*/
void lwip_ftp_upgrade_init_from_tftp(void);
/**
* @brief 周期推进 LwIP FTP 升级状态机(单步)
*
* 返回值:
* - 0:升级进行中(下次调用继续推进);
* - 1:升级成功完成(文件全部下载且升级头标记为 NEED_UP);
* - <0:升级失败(FTP/网络/Flash 错误等,具体原因在日志中打印)。
*/
int lwip_ftp_upgrade_step(void);
#ifdef __cplusplus
}
#endif
#endif /* LWIP_FTP_UPGRADE_H */
@@ -0,0 +1,215 @@
# 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 <path>` 切目录。
7. `LWIP_FTP_STEP_SEND_SIZE / WAIT_SIZE`
- 发送 `SIZE <file>`,并从 `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 <file>`,等待 `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` 线程的主循环。
+146
View File
@@ -0,0 +1,146 @@
#include "plat_comm/lwip_module/lwip_module.h"
#include "lwip/sockets.h" /* LwIP socket APIsocket/connect/send/recv/close */
#include "lwip/netdb.h" /* 域名解析 lwip_gethostbyname */
#include <string.h>
#include <errno.h>
/**
* ************************************************************************
* @brief 将主机名或 IP 字符串解析为 in_addr
* @param[in] host - 服务器地址,可为 "192.168.1.1" 或 "www.xxx.com"
* @param[out] out_addr - 解析得到的 IPv4 地址
* @return 0 成功,-1 失败(非 IP 且 DNS 解析失败)
* ************************************************************************
*/
int lwip_resolve_host(const char *host, struct in_addr *out_addr)
{
if (host == NULL || out_addr == NULL)
return -1;
/* 先按 IP 字符串解析(如 "134.17.16.109" */
unsigned long addr = inet_addr(host);
if (addr != INADDR_NONE) {
out_addr->s_addr = addr;
return 0;
}
/* 非 IP 则按域名解析(依赖 LwIP DNS,仅 LWIP_DNS=1 时 netdb.h 才声明 lwip_gethostbyname */
#if LWIP_DNS
{
struct hostent *he = lwip_gethostbyname(host);
if (he != NULL && he->h_addr_list[0] != NULL) {
memcpy(&out_addr->s_addr, he->h_addr_list[0], (size_t)he->h_length);
return 0;
}
}
#endif
return -1;
}
/**
* ************************************************************************
* @brief 设置 socket 为非阻塞模式
* @param[in] sock - socket 描述符
* @return 0 成功,-1 失败
* ************************************************************************
*/
int lwip_set_nonblock(int sock)
{
int flags = lwip_fcntl(sock, F_GETFL, 0);
if (flags >= 0) {
if (lwip_fcntl(sock, F_SETFL, flags | O_NONBLOCK) >= 0)
return 0;
}
return -1;
}
/**
* ************************************************************************
* @brief 创建 TCP socket 并连接到指定地址
* @param[in] host - 服务器地址(IP 或域名)
* @param[in] port - 服务器端口
* @return >=0 socket 描述符,<0 失败
* ************************************************************************
*/
int lwip_tcp_connect(const char *host, uint16_t port)
{
struct sockaddr_in server_addr;
struct in_addr ip_addr;
int sock;
if (host == NULL || host[0] == '\0' || port == 0)
return -1;
/* 创建 TCP 流式 socket */
sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
return -1;
/* 解析服务器 hostIP 或域名)为 in_addr */
if (lwip_resolve_host(host, &ip_addr) != 0) {
lwip_close(sock);
return -1;
}
/* 填充服务器地址并发起连接 */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = ip_addr.s_addr;
server_addr.sin_port = htons(port);
if (lwip_connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
lwip_close(sock);
return -1;
}
/* 设为非阻塞,避免 recv 阻塞任务 */
if (lwip_set_nonblock(sock) != 0) {
lwip_close(sock);
return -1;
}
return sock;
}
/**
* ************************************************************************
* @brief 关闭 socket
* @param[in] sock - socket 描述符
* ************************************************************************
*/
void lwip_tcp_close(int sock)
{
if (sock >= 0)
lwip_close(sock);
}
/**
* ************************************************************************
* @brief 发送数据
* @param[in] sock - socket 描述符
* @param[in] buf - 发送缓冲区
* @param[in] len - 发送长度
* @return >0 实际发送字节数,<=0 失败或对端关闭
* ************************************************************************
*/
int lwip_tcp_send(int sock, const void *buf, size_t len)
{
if (sock < 0 || buf == NULL || len == 0)
return -1;
return lwip_send(sock, buf, len, 0);
}
/**
* ************************************************************************
* @brief 接收数据(非阻塞)
* @param[in] sock - socket 描述符
* @param[out] buf - 接收缓冲区
* @param[in] len - 缓冲区长度
* @return >0 实际接收字节数,0 对端关闭,<0 错误(errno 需检查)
* ************************************************************************
*/
int lwip_tcp_recv(int sock, void *buf, size_t len)
{
if (sock < 0 || buf == NULL || len == 0)
return -1;
return lwip_recv(sock, buf, len, 0);
}
+82
View File
@@ -0,0 +1,82 @@
#ifndef LWIP_MODULE_H
#define LWIP_MODULE_H
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
#include <stdint.h>
#include <stddef.h> /* size_t */
#include "lwip/sockets.h" /* LwIP socket API 和结构体定义 */
#include "lwip/inet.h" /* LwIP inet_addr 和 INADDR_NONE 等宏 */
/* LwIP socket 相关宏定义(部分环境在 lwip/sockets.h 中未定义时在此兜底) */
#ifndef F_GETFL
#define F_GETFL 3 /* fcntl 获取文件描述符标志 */
#endif
#ifndef F_SETFL
#define F_SETFL 4 /* fcntl 设置文件描述符标志 */
#endif
#ifndef O_NONBLOCK
#define O_NONBLOCK 0x0004 /* 非阻塞模式,recv 无数据时立即返回不阻塞任务 */
#endif
#ifndef EWOULDBLOCK
#define EWOULDBLOCK 11 /* 非阻塞 recv 无数据时的 errno */
#endif
#ifndef EAGAIN
#define EAGAIN 11 /* 同上,与 EWOULDBLOCK 等价 */
#endif
/**
* @brief 将主机名或 IP 字符串解析为 in_addr
* @param[in] host 服务器地址,可为 "192.168.1.1" 或 "www.xxx.com"
* @param[out] out_addr 解析得到的 IPv4 地址
* @return 0 成功,-1 失败(非 IP 且 DNS 解析失败)
*/
int lwip_resolve_host(const char *host, struct in_addr *out_addr);
/**
* @brief 创建 TCP socket 并连接到指定地址
* @param[in] host 服务器地址(IP 或域名)
* @param[in] port 服务器端口
* @return >=0 socket 描述符,<0 失败
*/
int lwip_tcp_connect(const char *host, uint16_t port);
/**
* @brief 关闭 socket
* @param[in] sock socket 描述符
*/
void lwip_tcp_close(int sock);
/**
* @brief 发送数据
* @param[in] sock socket 描述符
* @param[in] buf 发送缓冲区
* @param[in] len 发送长度
* @return >0 实际发送字节数,<=0 失败或对端关闭
*/
int lwip_tcp_send(int sock, const void *buf, size_t len);
/**
* @brief 接收数据(非阻塞)
* @param[in] sock socket 描述符
* @param[out] buf 接收缓冲区
* @param[in] len 缓冲区长度
* @return >0 实际接收字节数,0 对端关闭,<0 错误(errno 需检查)
*/
int lwip_tcp_recv(int sock, void *buf, size_t len);
/**
* @brief 设置 socket 为非阻塞模式
* @param[in] sock socket 描述符
* @return 0 成功,-1 失败
*/
int lwip_set_nonblock(int sock);
#ifdef __cplusplus
}
#endif
#endif /* LWIP_MODULE_H */