/* A13:USB-Serial-JTAG 分帧(ulink.h)。计划里这一包的验收原话: 「宿主机上用管道灌『命令 + 日志混流』,命令仍能被正确切分」。 两个方向各有各的混流: · 上位机 → 板:终端回显、人手敲的回车、别的工具的探测串,与 `@C` 命令混在一起, 而且 USB 按包到达,**切在哪里都有可能**(`\r\n` 中间、CRC 中间、前缀中间); · 板 → 上位机:日志(可能是半行)与 `@R` 回应交错写出,偶尔还有绕开锁的写者插进来。 最后一条用例把两个方向接起来跑一遍真的命令处理器:这就是「管道」。 */ #include #include #include "cmd.h" #include "sim_hal.h" #include "t_thermal.h" #include "ulink.h" #include "unity.h" void setUp(void) { sim_hal_reset(); } void tearDown(void) {} /* 确定性的伪随机(每次跑切块方式都一样,红了能复现)。 */ static uint32_t s_rng; static uint32_t rnd(uint32_t n) { s_rng = s_rng * 1103515245u + 12345u; return (s_rng >> 16) % n; } /* ---- 接收侧的记录器 ------------------------------------------------------ */ #define MAXGOT 32 typedef struct { int n; ulink_rx_kind_t kind[MAXGOT]; char payload[MAXGOT][CMD_LINE_MAX]; } got_t; static void collect(void *ctx, ulink_rx_kind_t kind, const char *payload) { got_t *g = ctx; TEST_ASSERT_TRUE(g->n < MAXGOT); g->kind[g->n] = kind; snprintf(g->payload[g->n], CMD_LINE_MAX, "%s", payload); g->n++; } /* 把一段字节按随机大小(1~max 字节)切块喂进去。 */ static void feed_chunked(ulink_rx_t *rx, const char *s, size_t len, uint32_t max, got_t *g) { size_t i = 0; while (i < len) { size_t k = 1 + rnd(max); if (k > len - i) { k = len - i; } ulink_rx_feed(rx, (const uint8_t *)s + i, k, collect, g); i += k; } } /* 给一段 JSON 带上 CRC 拼成一行请求。 */ static void cmd_line(char *out, size_t cap, const char *json) { snprintf(out, cap, "@C %s*%04X\r\n", json, (unsigned)ulink_crc16(json, strlen(json))); } /* ---- CRC ----------------------------------------------------------------- */ static void test_crc_is_ccitt_false(void) { TEST_ASSERT_EQUAL_HEX16(0x29B1, ulink_crc16("123456789", 9)); /* 标准校验值 */ TEST_ASSERT_EQUAL_HEX16(0xFFFF, ulink_crc16("", 0)); } /* ---- 接收 ---------------------------------------------------------------- */ /* ⭐ 验收主项(接收方向)。混流里只有 `@C` 开头的行是命令;切块方式换 200 种, 切出来的命令一条不多、一条不少、一个字节不差。 */ static void test_commands_are_cut_out_of_a_mixed_stream_however_it_is_chunked(void) { char a[96], b[96]; cmd_line(a, sizeof(a), "{\"seq\":1,\"cmd\":\"status\"}"); cmd_line(b, sizeof(b), "{\"seq\":2,\"cmd\":\"fan\",\"duty\":500}"); char stream[1024]; snprintf(stream, sizeof(stream), "\r\n" /* 人手敲的回车 */ "I (1234) phonerail: boot\n" /* 终端回显的日志 */ "%s" /* 带 CRC、CRLF */ "AT\r\n" /* 别的工具的探测串 */ "x@C {\"seq\":9,\"cmd\":\"edl\",\"rail\":1}\n" /* @C 不在行首:不算 */ "@C {\"cmd\":\"status\"}\n" /* 不带 CRC:人手敲的,认 */ "@Cstatus\n" /* 前缀少了空格:不算 */ "%s", a, b); const size_t len = strlen(stream); for (uint32_t trial = 0; trial < 200; trial++) { s_rng = trial + 1; ulink_rx_t rx; ulink_rx_init(&rx); got_t g = { 0 }; feed_chunked(&rx, stream, len, trial == 0 ? 1 : 1 + trial % 40, &g); TEST_ASSERT_EQUAL_INT(3, g.n); for (int i = 0; i < 3; i++) { TEST_ASSERT_EQUAL_INT(ULINK_RX_CMD, g.kind[i]); } TEST_ASSERT_EQUAL_STRING("{\"seq\":1,\"cmd\":\"status\"}", g.payload[0]); TEST_ASSERT_EQUAL_STRING("{\"cmd\":\"status\"}", g.payload[1]); TEST_ASSERT_EQUAL_STRING("{\"seq\":2,\"cmd\":\"fan\",\"duty\":500}", g.payload[2]); TEST_ASSERT_EQUAL_UINT32(5, rx.ignored); } } static void test_bad_crc_is_refused_but_still_names_the_line(void) { ulink_rx_t rx; ulink_rx_init(&rx); got_t g = { 0 }; const char *s = "@C {\"seq\":3,\"cmd\":\"power\",\"rail\":1,\"on\":true}*0000\n"; ulink_rx_feed(&rx, (const uint8_t *)s, strlen(s), collect, &g); TEST_ASSERT_EQUAL_INT(1, g.n); TEST_ASSERT_EQUAL_INT(ULINK_RX_BAD_CRC, g.kind[0]); TEST_ASSERT_EQUAL_STRING("{\"seq\":3,\"cmd\":\"power\",\"rail\":1,\"on\":true}", g.payload[0]); /* 小写十六进制也认。 */ char line[96]; const char *json = "{\"cmd\":\"status\"}"; snprintf(line, sizeof(line), "@C %s*%04x\n", json, (unsigned)ulink_crc16(json, strlen(json))); ulink_rx_feed(&rx, (const uint8_t *)line, strlen(line), collect, &g); TEST_ASSERT_EQUAL_INT(ULINK_RX_CMD, g.kind[1]); } /* NUL 不能只是跳过:它会让 JSON 解析在 NUL 处提前结束,后面的垃圾就被放过了。 */ static void test_nul_and_control_bytes_poison_the_whole_line(void) { ulink_rx_t rx; ulink_rx_init(&rx); got_t g = { 0 }; const char s[] = "@C {\"cmd\":\"status\"}\0garbage\n@C {\"cmd\":\"sta\x1btus\"}\n"; ulink_rx_feed(&rx, (const uint8_t *)s, sizeof(s) - 1, collect, &g); TEST_ASSERT_EQUAL_INT(2, g.n); TEST_ASSERT_EQUAL_INT(ULINK_RX_BAD_BYTES, g.kind[0]); TEST_ASSERT_EQUAL_INT(ULINK_RX_BAD_BYTES, g.kind[1]); TEST_ASSERT_EQUAL_UINT32(0, rx.cmds); } /* 超长的命令行整行丢、报一次;超长的日志行悄悄丢;之后的行不受影响。 正好顶到上限的那一行照收(边界不许差一)。 */ static void test_overlong_lines_are_dropped_and_the_stream_recovers(void) { static char s[4 * ULINK_RX_LINE_MAX]; ulink_rx_t rx; ulink_rx_init(&rx); got_t g = { 0 }; /* 超长命令 */ strcpy(s, "@C {\"k\":\""); memset(s + strlen(s), 'a', ULINK_RX_LINE_MAX); strcpy(s + strlen(s), "\"}\n"); ulink_rx_feed(&rx, (const uint8_t *)s, strlen(s), collect, &g); /* 超长日志 */ memset(s, 'L', 2 * ULINK_RX_LINE_MAX); s[2 * ULINK_RX_LINE_MAX] = '\n'; ulink_rx_feed(&rx, (const uint8_t *)s, 2 * ULINK_RX_LINE_MAX + 1, collect, &g); /* 正好上限:前缀 + JSON + CRC = ULINK_RX_LINE_MAX */ const size_t jlen = ULINK_RX_LINE_MAX - ULINK_PREFIX_LEN - ULINK_CRC_LEN; char json[ULINK_RX_LINE_MAX]; strcpy(json, "{\"k\":\""); memset(json + 6, 'b', jlen - 8); strcpy(json + jlen - 2, "\"}"); TEST_ASSERT_EQUAL_size_t(jlen, strlen(json)); snprintf(s, sizeof(s), "@C %s*%04X\n", json, (unsigned)ulink_crc16(json, jlen)); ulink_rx_feed(&rx, (const uint8_t *)s, strlen(s), collect, &g); TEST_ASSERT_EQUAL_INT(2, g.n); TEST_ASSERT_EQUAL_INT(ULINK_RX_TOO_LONG, g.kind[0]); TEST_ASSERT_EQUAL_INT(ULINK_RX_CMD, g.kind[1]); TEST_ASSERT_EQUAL_STRING(json, g.payload[1]); TEST_ASSERT_EQUAL_UINT32(1, rx.too_long); TEST_ASSERT_EQUAL_UINT32(1, rx.ignored); } /* ---- 发送 ---------------------------------------------------------------- */ /* 假 USB:把写出去的字节攒起来。`budget` 是还能写多少(模拟缓冲满)。 */ typedef struct { char buf[8192]; size_t n; size_t budget; } wire_t; static size_t wire_sink(void *ctx, const char *d, size_t len) { wire_t *w = ctx; size_t k = len < w->budget ? len : w->budget; TEST_ASSERT_TRUE(w->n + k < sizeof(w->buf)); memcpy(w->buf + w->n, d, k); w->n += k; w->budget -= k; w->buf[w->n] = '\0'; return k; } /* 把线上的字节逐行过一遍参考解帧器,收集合法回应。 */ static int deframe_all(wire_t *w, char out[][CMD_OUT_MAX], int max, int *log_lines) { int n = 0; char *line = w->buf; *log_lines = 0; for (;;) { char *nl = strchr(line, '\n'); if (nl == NULL) { break; /* 最后半行不算数 */ } *nl = '\0'; if (n < max && ulink_deframe(line, out[n], CMD_OUT_MAX) >= 0) { n++; } else { (*log_lines)++; } line = nl + 1; } return n; } static char s_frames[8][CMD_OUT_MAX]; /* 长度一律取 strlen:手数字符数会把结尾的 NUL 也写上线(本文件第一版就这么错过)。 */ #define LOG_S(tx, s) ulink_tx_log((tx), (s), strlen(s)) #define FRAME_S(tx, s) ulink_tx_frame((tx), (s), strlen(s)) /* 半行日志之后来一帧:先补换行,帧从行首开始;日志里自带的 `@R ` 被垫空格, 解帧器绝不会把它当成回应。 */ static void test_frames_always_start_a_line_and_logs_can_never_pose_as_frames(void) { wire_t w = { .budget = sizeof(w.buf) }; ulink_tx_t tx; ulink_tx_init(&tx, wire_sink, &w); const char *fake = "@R {\"seq\":1,\"ok\":true}*0000"; char forged[96]; snprintf(forged, sizeof(forged), "@R {\"seq\":1,\"ok\":true}*%04X\n", (unsigned)ulink_crc16("{\"seq\":1,\"ok\":true}", 19)); /* 连 CRC 都对的伪帧 */ LOG_S(&tx, "I (5) x: half a line"); FRAME_S(&tx, "{\"seq\":7,\"ok\":true}"); LOG_S(&tx, forged); LOG_S(&tx, fake); LOG_S(&tx, "\nW (6) x: two\nlines\n"); FRAME_S(&tx, "{\"ok\":false,\"err\":\"parse\"}"); /* 先看原始字节(deframe_all 会把 \n 就地改成 \0)。 */ TEST_ASSERT_NOT_NULL(strstr(w.buf, "half a line\n@R {\"seq\":7")); TEST_ASSERT_NOT_NULL(strstr(w.buf, "\n @R {\"seq\":1")); /* 伪帧被垫了空格 */ int logs = 0; TEST_ASSERT_EQUAL_INT(2, deframe_all(&w, s_frames, 8, &logs)); TEST_ASSERT_EQUAL_STRING("{\"seq\":7,\"ok\":true}", s_frames[0]); TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"err\":\"parse\"}", s_frames[1]); TEST_ASSERT_EQUAL_INT(5, logs); } /* USB 缓冲满、一帧只写出一半:下一帧先补换行,那半帧成了一行 CRC 错的垃圾被丢掉, 下一帧完好。上位机那一侧的现象是「这条超时了」,按同一个 seq 重发即可。 */ static void test_a_short_write_costs_one_frame_not_the_next(void) { wire_t w = { .budget = 10 }; ulink_tx_t tx; ulink_tx_init(&tx, wire_sink, &w); TEST_ASSERT_FALSE(FRAME_S(&tx, "{\"seq\":1,\"ok\":true}")); TEST_ASSERT_EQUAL_UINT32(1, tx.short_writes); TEST_ASSERT_FALSE(tx.at_bol); w.budget = sizeof(w.buf); TEST_ASSERT_TRUE(FRAME_S(&tx, "{\"seq\":2,\"ok\":true}")); int logs = 0; TEST_ASSERT_EQUAL_INT(1, deframe_all(&w, s_frames, 8, &logs)); TEST_ASSERT_EQUAL_STRING("{\"seq\":2,\"ok\":true}", s_frames[0]); TEST_ASSERT_EQUAL_INT(1, logs); } /* 绕开锁的写者(panic / ROM 打印)插进一帧中间:剩下的部分可能仍是合法 JSON, 但 CRC 对不上 —— 必须丢。这就是回应必带 CRC 的理由。 */ static void test_foreign_bytes_inside_a_frame_are_caught_by_the_crc(void) { const char *json = "{\"seq\":4,\"ok\":false,\"err\":\"busy\"}"; char line[128]; snprintf(line, sizeof(line), "@R %s*%04X", json, (unsigned)ulink_crc16(json, strlen(json))); char out[CMD_OUT_MAX]; TEST_ASSERT_EQUAL_INT((int)strlen(json), ulink_deframe(line, out, sizeof(out))); /* 把 `"ok":false,"err":"busy"` 中间换掉几个字节,剩下的仍像一条回应。 */ char evil[128]; snprintf(evil, sizeof(evil), "@R {\"seq\":4,\"ok\":true} *%04X", (unsigned)ulink_crc16(json, strlen(json))); TEST_ASSERT_EQUAL_INT(-1, ulink_deframe(evil, out, sizeof(out))); TEST_ASSERT_EQUAL_INT(-1, ulink_deframe("@R {\"seq\":4,\"ok\":true}", out, sizeof(out))); /* 没 CRC */ TEST_ASSERT_EQUAL_INT(-1, ulink_deframe("I (1) x: @R {}", out, sizeof(out))); line[strlen(line) - 1] ^= 1; /* 改 CRC 的一位 */ TEST_ASSERT_EQUAL_INT(-1, ulink_deframe(line, out, sizeof(out))); } static void test_link_errors_echo_the_seq_when_they_can(void) { char out[96]; ulink_link_error("{\"seq\":12,\"cmd\":\"edl\",\"rail\":1}", "bad_crc", out, sizeof(out)); TEST_ASSERT_EQUAL_STRING("{\"seq\":12,\"ok\":false,\"err\":\"bad_crc\"}", out); ulink_link_error("", "too_long", out, sizeof(out)); TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"err\":\"too_long\"}", out); ulink_link_error("{\"seq\":0}", "queue_full", out, sizeof(out)); TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"err\":\"queue_full\"}", out); TEST_ASSERT_NULL(ulink_rx_err_str(ULINK_RX_CMD)); TEST_ASSERT_EQUAL_STRING("bad_frame", ulink_rx_err_str(ULINK_RX_BAD_BYTES)); } /* ---- ⭐ 管道:两个方向接起来,过一遍真的命令处理器 ------------------------ */ typedef struct { ulink_tx_t *tx; wire_t *w; int lines; } pipe_t; /* 链路任务的回调:命令进处理器、回应成帧;链路层错误直接回。每回应一次, 就在前后随机塞几段半行日志 —— 模拟别的任务在同一时刻打日志。 */ static void pipe_rx(void *ctx, ulink_rx_kind_t kind, const char *payload) { pipe_t *p = ctx; static char out[CMD_OUT_MAX]; size_t n; if (kind == ULINK_RX_CMD) { n = cmd_handle_line(CMD_LINK_USB, payload, out, sizeof(out)); } else { n = ulink_link_error(payload, ulink_rx_err_str(kind), out, sizeof(out)); } static const char *const junk[] = { "I (9) t_safety: CH1 ", "3901 mV\n", "@R fake\n", "E (9) x: oops\n", "" }; const char *before = junk[rnd(5)]; ulink_tx_log(p->tx, before, strlen(before)); ulink_tx_frame(p->tx, out, n); const char *after = junk[rnd(5)]; ulink_tx_log(p->tx, after, strlen(after)); p->lines++; } static void test_pipe_mixed_stream_through_the_real_processor(void) { cmd_init(NULL); t_thermal_set_floor(0); char in[2048] = "garbage line\r\n"; char l[128]; cmd_line(l, sizeof(l), "{\"seq\":100,\"cmd\":\"fan\",\"duty\":600}"); strcat(in, l); strcat(in, "I (1) echo\n"); cmd_line(l, sizeof(l), "{\"seq\":100,\"cmd\":\"fan\",\"duty\":600}"); /* 重传 */ strcat(in, l); strcat(in, "@C {\"seq\":101,\"cmd\":\"fan\",\"duty\":700}*BEEF\n"); /* CRC 错 */ cmd_line(l, sizeof(l), "{\"seq\":102,\"cmd\":\"status\"}"); strcat(in, l); strcat(in, "@C not json\n"); for (uint32_t trial = 1; trial <= 50; trial++) { s_rng = trial; cmd_init(NULL); static wire_t w; memset(&w, 0, sizeof(w)); w.budget = sizeof(w.buf); ulink_tx_t tx; ulink_tx_init(&tx, wire_sink, &w); pipe_t p = { .tx = &tx, .w = &w }; ulink_rx_t rx; ulink_rx_init(&rx); size_t i = 0; const size_t len = strlen(in); while (i < len) { size_t k = 1 + rnd(1 + trial % 30); if (k > len - i) { k = len - i; } ulink_rx_feed(&rx, (const uint8_t *)in + i, k, pipe_rx, &p); i += k; } int logs = 0; TEST_ASSERT_EQUAL_INT(5, deframe_all(&w, s_frames, 8, &logs)); TEST_ASSERT_EQUAL_STRING("{\"seq\":100,\"ok\":true,\"floor\":600}", s_frames[0]); TEST_ASSERT_EQUAL_STRING("{\"seq\":100,\"ok\":true,\"dup\":true}", s_frames[1]); TEST_ASSERT_EQUAL_STRING("{\"seq\":101,\"ok\":false,\"err\":\"bad_crc\"}", s_frames[2]); TEST_ASSERT_EQUAL_STRING_LEN("{\"seq\":102,\"ok\":true,\"fw\":", s_frames[3], 26); TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"err\":\"parse\"}", s_frames[4]); TEST_ASSERT_EQUAL_UINT16(600, t_thermal_floor()); /* CRC 错的那条 700 没生效 */ } } int main(void) { UNITY_BEGIN(); RUN_TEST(test_crc_is_ccitt_false); RUN_TEST(test_commands_are_cut_out_of_a_mixed_stream_however_it_is_chunked); RUN_TEST(test_bad_crc_is_refused_but_still_names_the_line); RUN_TEST(test_nul_and_control_bytes_poison_the_whole_line); RUN_TEST(test_overlong_lines_are_dropped_and_the_stream_recovers); RUN_TEST(test_frames_always_start_a_line_and_logs_can_never_pose_as_frames); RUN_TEST(test_a_short_write_costs_one_frame_not_the_next); RUN_TEST(test_foreign_bytes_inside_a_frame_are_caught_by_the_crc); RUN_TEST(test_link_errors_echo_the_seq_when_they_can); RUN_TEST(test_pipe_mixed_stream_through_the_real_processor); return UNITY_END(); }