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

/*
* 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;
}