You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
372 lines
14 KiB
372 lines
14 KiB
/*
|
|
* Copyright (c) 2019 Fuzhou Rockchip Electronics Co., Ltd
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Radkil adapted in 2026: 移除 RT-Thread 依赖, 对接 rd_stdio + TComCtrl
|
|
*/
|
|
|
|
#include "ymodem.h"
|
|
#include "rd_stdio.h"
|
|
#include "ringbuffer.h"
|
|
#include <stdlib.h>
|
|
|
|
#ifndef YMODEM_PATH_MAX
|
|
#define YMODEM_PATH_MAX 256
|
|
#endif
|
|
|
|
struct custom_ctx
|
|
{
|
|
struct rym_ctx parent;
|
|
void *fp; /* Rd_Fopen 返回的文件句柄 */
|
|
int flen; /* 剩余待接收字节数, <0 表示对端未给长度 */
|
|
int expect_index; /* 下一包应有的序号, 0 表示还没收到数据包 */
|
|
char fpath[YMODEM_PATH_MAX];
|
|
};
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════ *
|
|
* TComCtrl → rym_device_t 桥接 *
|
|
* 读: 直接掏接收环形缓冲区 (见 serial_rym_read, 不走 rd_ComRead) *
|
|
* 写: rd_ComSend (底层回调必须按"实际字节数"返回) *
|
|
* 无需额外维护信号量 *
|
|
* ═══════════════════════════════════════════════════════════════════════ */
|
|
|
|
/*
|
|
* YMODEM 是原始二进制流, 不能借用 rd_ComRead:
|
|
* 1) m_pfCheck == NULL 时 rd_ComRead 内部 iPackLen 恒为 1, 每次只返回 1 字节,
|
|
* 再叠加 ymodem 层的 Rd_Delay(1)=1ms, 吞吐被压到 ~1KB/s (115200 下慢 11 倍);
|
|
* 2) rd_ComRead 在环空时执行 Rd_SemWait (osWaitForever) 永久阻塞,
|
|
* 使 ymodem 层所有基于 Rd_GetTime() 的超时变成死代码, 对端一停发就永久卡死.
|
|
* 这里一次取走环里已到达的全部字节; 无数据返回 0, 由上层用
|
|
* Rd_GetTime()/Rd_Delay(1) 轮询控制节奏与超时退出.
|
|
*/
|
|
static size_t serial_rym_read(void *opaque, uint8_t *buf, size_t len)
|
|
{
|
|
TComCtrl *ptCom = (TComCtrl *)opaque;
|
|
size_t got = 0;
|
|
|
|
if (ptCom == NULL || ptCom->m_stComBuf.m_ptRecv == NULL || buf == NULL || len == 0)
|
|
return 0;
|
|
|
|
while (got < len)
|
|
{
|
|
char ch;
|
|
if (rd_RingbufferGetchar(ptCom->m_stComBuf.m_ptRecv, &ch) != 1)
|
|
break;
|
|
buf[got++] = (uint8_t)ch;
|
|
}
|
|
|
|
return got;
|
|
}
|
|
|
|
static size_t serial_rym_write(void *opaque, const uint8_t *buf, size_t len)
|
|
{
|
|
TComCtrl *ptCom = (TComCtrl *)opaque;
|
|
int ret;
|
|
|
|
if (ptCom == NULL || buf == NULL || len == 0) return 0;
|
|
|
|
/* 返回实际写出的字节数; 失败返回 0 交给 ymodem 层超时处理,
|
|
* 不能把负数负值强转成 size_t 大数 */
|
|
ret = rd_ComSend(ptCom, (char *)buf, (uint32_t)len);
|
|
if (ret <= 0)
|
|
{
|
|
/* 发送失败在 ymodem 层只会表现为"对端不回话", 很难定位, 所以直接报出来:
|
|
* ret=-2(RD_NULL): TComCtrl 为空或 m_pfSend 未接
|
|
* ret=-1(RD_FAILURE): 底层写失败 (上位机侧常见于 COM 口没打开成功/
|
|
* 被别的程序占用, 例如 Xshell 还开着)
|
|
* ret=0: 一个字莂也没写出去 */
|
|
log_e("ymodem TX: rd_ComSend failed (ret=%d, len=%u) -> 字节没发出去",
|
|
ret, (unsigned)len);
|
|
return 0;
|
|
}
|
|
return (size_t)ret;
|
|
}
|
|
|
|
static void ymodem_build_device(TComCtrl *ptCom, rym_device_t *dev)
|
|
{
|
|
RD_MEMSET(dev, 0, sizeof(*dev));
|
|
dev->read = serial_rym_read;
|
|
dev->write = serial_rym_write;
|
|
dev->opaque = ptCom;
|
|
}
|
|
|
|
/*
|
|
* 会话开始前把接收环里的残留字节丢掉.
|
|
* 否则上一条命令/上一次会话遗留的 'C' (接收端催包) 或 ACK 会顶到前面,
|
|
* 使 ymodem 的"发送一个包 → 读一个应答"序列整体错位:
|
|
* 表现就是传输跑了一半报 -118 (RYM_ERR_ACK), 或者会话结束时收不到正确应答.
|
|
*
|
|
* 同时必须把 m_sem_Recv 的计数也清零:
|
|
* 接收路径 (rd_ComRecvProc) 每收一批数据就 Rd_SemPost(m_sem_Recv),
|
|
* 而 rd_ComRead 的约定是"先等信号量、再读环".
|
|
* serial_rym_read 绕过 rd_ComRead 直接掏环, 信号量只进不出, 计数会顶到上限;
|
|
* 不清零的话, 会话结束后 c:read() 会"信号量立刻等到、环里却没数据",
|
|
* 阻塞语义被破坏 (空读/假就绪).
|
|
*/
|
|
static void ymodem_flush_rx(TComCtrl *ptCom)
|
|
{
|
|
char ch;
|
|
int guard;
|
|
|
|
if (ptCom == NULL)
|
|
return;
|
|
|
|
if (ptCom->m_stComBuf.m_ptRecv != NULL)
|
|
{
|
|
while (rd_RingbufferGetchar(ptCom->m_stComBuf.m_ptRecv, &ch) == 1)
|
|
{
|
|
/* 丢弃 */
|
|
}
|
|
}
|
|
|
|
/* 把残留的"有数据"信号也吃掉, 恢复"sem>0 ⇒ 环里有数据"的约定.
|
|
* 计数有上限(init+1), 正常情况下几次就空; guard 只防异常实现死循环.
|
|
*
|
|
* 用 0 ms = 纯试探, 不等. 前提是 Rd_SemWaitTimeout 真的遵守超时:
|
|
* FreeRTOS: osSemaphoreAcquire(sem, 0) ✓ 非阻塞
|
|
* Win32 : pthread_cond_timedwait 到期时间=当前 ✓ 立即返回
|
|
* Linux : rd_thread.c 的 WEAK 兜底恒返回 -1 ✓ 不会阻, 但也清不掉计数
|
|
* (Win32 曾把超时丢掉转成阻塞等待, 导致上位机 ry 在这里挂死, 已修) */
|
|
for (guard = 0; guard < 1024; guard++)
|
|
{
|
|
if (Rd_SemWaitTimeout(&ptCom->m_stComBuf.m_sem_Recv, 0) != 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════ *
|
|
* YMODEM 接收回调 (文件写入) *
|
|
* ═══════════════════════════════════════════════════════════════════════ */
|
|
|
|
static enum rym_code _rym_recv_begin(struct rym_ctx *ctx, uint8_t *buf, size_t len)
|
|
{
|
|
struct custom_ctx *cctx = (struct custom_ctx *)ctx;
|
|
char *ret;
|
|
|
|
ret = strchr(cctx->fpath, '\0');
|
|
if (ret == NULL)
|
|
{
|
|
log_w("No end character");
|
|
return RYM_ERR_ACK;
|
|
}
|
|
*ret = '/';
|
|
RD_STRNCPY(ret + 1, (const char *)buf, len - 1);
|
|
ret[1 + len - 1] = '\0';
|
|
|
|
cctx->fp = Rd_Fopen(cctx->fpath, "wb");
|
|
if (cctx->fp == NULL)
|
|
{
|
|
log_e("error creating file: %s", cctx->fpath);
|
|
return RYM_CODE_CAN;
|
|
}
|
|
|
|
cctx->flen = atoi(1 + (const char *)buf + strnlen((const char *)buf, len - 1));
|
|
if (cctx->flen == 0)
|
|
cctx->flen = -1;
|
|
|
|
cctx->expect_index = 0; /* 新文件, 序号重新开始 */
|
|
log_i("ymodem RX: file '%s', declared size=%d", cctx->fpath, cctx->flen);
|
|
|
|
return RYM_CODE_ACK;
|
|
}
|
|
|
|
static enum rym_code _rym_recv_data(struct rym_ctx *ctx, uint8_t *buf, size_t len)
|
|
{
|
|
struct custom_ctx *cctx = (struct custom_ctx *)ctx;
|
|
uint8_t index = ctx->buf[1]; /* 本包序号 */
|
|
ssize_t ret;
|
|
|
|
if (cctx->fp == NULL)
|
|
return RYM_CODE_CAN;
|
|
|
|
/* 序号去重: 对端重传的包只能回 ACK, 绝不能再写一次.
|
|
* 否则每次重传都会把同一段数据追加到文件里, 文件会变成
|
|
* "原内容重复 N 遍" (实测一个 5 字节的文件被撑到了 1490 字节) */
|
|
if (cctx->expect_index > 0 && cctx->expect_index <= 0xFF)
|
|
{
|
|
if (index == (uint8_t)(cctx->expect_index - 1))
|
|
return RYM_CODE_ACK; /* 重复包: 已经写过, 只确认 */
|
|
if (index != (uint8_t)cctx->expect_index)
|
|
return RYM_CODE_NAK; /* 跳包/乱序: 不写也不确认, 等对端重传 */
|
|
}
|
|
|
|
if (cctx->flen < 0)
|
|
{
|
|
ret = Rd_Mwrite(buf, len, cctx->fp); /* 对端没给长度, 来多少写多少 */
|
|
}
|
|
else
|
|
{
|
|
/* 严格按包头声明的长度截断, flen 不会变成负数后被当成"无限写" */
|
|
int wlen = ((int)len > cctx->flen) ? cctx->flen : (int)len;
|
|
cctx->flen -= wlen;
|
|
ret = Rd_Mwrite(buf, (size_t)wlen, cctx->fp);
|
|
}
|
|
|
|
if (ret < 0)
|
|
{
|
|
log_e("ymodem RX data: write failed (ret=%ld), aborting", (long)ret);
|
|
return RYM_CODE_CAN; /* 写入失败: 中止会话 */
|
|
}
|
|
|
|
cctx->expect_index = (int)index + 1; /* 记录下一包应有的序号 */
|
|
|
|
return RYM_CODE_ACK;
|
|
}
|
|
|
|
static enum rym_code _rym_recv_end(struct rym_ctx *ctx, uint8_t *buf, size_t len)
|
|
{
|
|
struct custom_ctx *cctx = (struct custom_ctx *)ctx;
|
|
(void)buf;
|
|
(void)len;
|
|
|
|
if (cctx->fp != NULL)
|
|
{
|
|
Rd_Mclose(cctx->fp);
|
|
cctx->fp = NULL;
|
|
}
|
|
return RYM_CODE_ACK;
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════ *
|
|
* YMODEM 发送回调 (文件读取) *
|
|
* ═══════════════════════════════════════════════════════════════════════ */
|
|
|
|
static const char *_get_path_lastname(const char *path)
|
|
{
|
|
char *ptr = strrchr(path, '/');
|
|
if (ptr == NULL) return path;
|
|
return ++ptr;
|
|
}
|
|
|
|
static enum rym_code _rym_send_begin(struct rym_ctx *ctx, uint8_t *buf, size_t len)
|
|
{
|
|
struct custom_ctx *cctx = (struct custom_ctx *)ctx;
|
|
|
|
cctx->fp = Rd_Fopen(cctx->fpath, "rb");
|
|
if (cctx->fp == NULL)
|
|
{
|
|
log_e("error open file: %s", cctx->fpath);
|
|
return RYM_ERR_FILE;
|
|
}
|
|
|
|
Rd_Mseek(cctx->fp, 0, SEEK_END);
|
|
long file_size = Rd_Mtell(cctx->fp);
|
|
Rd_Mseek(cctx->fp, 0, SEEK_SET);
|
|
|
|
RD_MEMSET(buf, 0, len);
|
|
const char *fname = _get_path_lastname(cctx->fpath);
|
|
/* YMODEM 包头第二字段必须是十进制文件长度字符串 (CRC/Checksum 模式由握手字节 C/NAK 协商);
|
|
* 这里写 "STX" 之类的标识符是非法的, 接收端会把文件长度解析成 0 */
|
|
RD_SPRINTF((char *)buf, "%s%c%ld", fname, '\0', file_size);
|
|
log_i("ymodem TX: file '%s' declared size=%ld byte(s)", fname, file_size);
|
|
|
|
return RYM_CODE_SOH;
|
|
}
|
|
|
|
static enum rym_code _rym_send_data(struct rym_ctx *ctx, uint8_t *buf, size_t len)
|
|
{
|
|
struct custom_ctx *cctx = (struct custom_ctx *)ctx;
|
|
size_t read_size = 0;
|
|
int retry;
|
|
|
|
for (retry = 0; retry < 10; retry++)
|
|
{
|
|
ssize_t n = Rd_Mread(buf + read_size, len - read_size, cctx->fp);
|
|
if (n > 0) read_size += (size_t)n;
|
|
if (read_size == len) break;
|
|
}
|
|
|
|
if (read_size < len)
|
|
{
|
|
RD_MEMSET(buf + read_size, 0x1A, len - read_size);
|
|
ctx->stage = RYM_STAGE_FINISHING;
|
|
}
|
|
|
|
#if RYM_TX_BLOCK_1K
|
|
/* 数据阶段一律用 1K(STX) 块 + 0x1A 填满, 这是标准 YMODEM 发送端的行为
|
|
* (lrzsz 的 sz 默认就是 1K 块). 部分接收端(含 Xshell)只接受 STX 数据块,
|
|
* 收到 SOH 数据块会一直回 NAK; 文件尾不足 1K 时靠包头里的长度截断. */
|
|
return RYM_CODE_STX;
|
|
#else
|
|
/* 按本块实际字节数选择 128B/1K 块 (旧行为) */
|
|
return (read_size > 128) ? RYM_CODE_STX : RYM_CODE_SOH;
|
|
#endif
|
|
}
|
|
|
|
static enum rym_code _rym_send_end(struct rym_ctx *ctx, uint8_t *buf, size_t len)
|
|
{
|
|
struct custom_ctx *cctx = (struct custom_ctx *)ctx;
|
|
RD_MEMSET(buf, 0, len);
|
|
|
|
if (cctx->fp != NULL)
|
|
{
|
|
Rd_Mclose(cctx->fp);
|
|
cctx->fp = NULL;
|
|
}
|
|
return RYM_CODE_SOH;
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════════ *
|
|
* 公开 API *
|
|
* ═══════════════════════════════════════════════════════════════════════ */
|
|
|
|
int rym_download_file(TComCtrl *ptCom, const char *file_path)
|
|
{
|
|
int res;
|
|
rym_device_t dev;
|
|
|
|
struct custom_ctx *ctx = RD_CALLOC(1, sizeof(*ctx));
|
|
if (!ctx)
|
|
{
|
|
log_e("RD_MALLOC failed");
|
|
return RD_FAILURE;
|
|
}
|
|
ctx->fp = NULL;
|
|
RD_STRNCPY(ctx->fpath, file_path, YMODEM_PATH_MAX - 1);
|
|
|
|
ymodem_build_device(ptCom, &dev);
|
|
ymodem_flush_rx(ptCom);
|
|
res = rym_recv_on_device(&ctx->parent, &dev,
|
|
_rym_recv_begin, _rym_recv_data, _rym_recv_end,
|
|
10);
|
|
|
|
/* 会话期间 ISR 一直在 post m_sem_Recv 而没人 acquire,
|
|
* 结束时再清一次, 保证后续 rd_ComRead 的阻塞语义正常 */
|
|
ymodem_flush_rx(ptCom);
|
|
|
|
if (ctx->fp != NULL)
|
|
Rd_Mclose(ctx->fp);
|
|
RD_FREE(ctx);
|
|
return res;
|
|
}
|
|
|
|
int rym_upload_file(TComCtrl *ptCom, const char *file_path)
|
|
{
|
|
int res;
|
|
rym_device_t dev;
|
|
|
|
struct custom_ctx *ctx = RD_CALLOC(1, sizeof(*ctx));
|
|
if (!ctx)
|
|
{
|
|
log_e("RD_MALLOC failed");
|
|
return RD_FAILURE;
|
|
}
|
|
ctx->fp = NULL;
|
|
RD_STRNCPY(ctx->fpath, file_path, YMODEM_PATH_MAX - 1);
|
|
|
|
ymodem_build_device(ptCom, &dev);
|
|
ymodem_flush_rx(ptCom);
|
|
res = rym_send_on_device(&ctx->parent, &dev,
|
|
_rym_send_begin, _rym_send_data, _rym_send_end,
|
|
10);
|
|
|
|
/* 会话期间 ISR 一直在 post m_sem_Recv 而没人 acquire,
|
|
* 结束时再清一次, 保证后续 rd_ComRead 的阻塞语义正常 */
|
|
ymodem_flush_rx(ptCom);
|
|
|
|
if (ctx->fp != NULL)
|
|
Rd_Mclose(ctx->fp);
|
|
RD_FREE(ctx);
|
|
return res;
|
|
}
|
|
|