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