/** * @file demo_main.c * @brief TCP API 调用演示客户端 (一维/二维通用) * * 演示使用 QDXnetworkStack API 层的零拷贝发送与参数回调机制。 * 用户操作流程与原 tcp_c_demo 保持完全一致。 */ #include "qdx_port.h" #include "qdx_preprocess.h" #include "qdx_protocol.h" #include "qdx_tcp_logic.h" #include /* _kbhit, _getch */ #include #include #include #include #include /* ============================================================ * 宏定义与全局变量 * ============================================================ */ /* 默认服务端连接参数(硬编码) */ #define DEFAULT_SERVER_IP "127.0.0.1" #define DEFAULT_CONTROL_PORT 5511 #define DEFAULT_DATA_PORT 5512 /* ANSI 控制台颜色宏定义 */ #define CLR_RESET "\033[0m" #define CLR_RED "\033[31m" #define CLR_GREEN "\033[32m" #define CLR_YELLOW "\033[33m" #define CLR_BLUE "\033[34m" #define CLR_MAGENTA "\033[35m" #define CLR_CYAN "\033[36m" #define CLR_DIM "\033[90m" static uint8_t g_dimension_mode = 0; /* 0=1D, 1=2D */ static char g_matrix_dir[260] = {0}; /* 发送缓冲区(静态分配,避免 malloc) */ #define MAX_2D_PIXELS (256 * 256) static uint16_t g_raw_matrix[MAX_2D_PIXELS]; /* API 要求的外部传输缓冲区,总大小 256KB,前置留出 1KB 给网络层附加头部 */ #define TX_BUFFER_TOTAL_CAPACITY (256 * 1024) #define TX_BUFFER_HEAD_OFFSET 1024 static uint8_t g_tx_buffer[TX_BUFFER_TOTAL_CAPACITY]; static TcpTxBuffer_t g_api_tx_buffer; /* ============================================================ * 辅助函数:十六进制打印 * ============================================================ */ static void print_hex(const uint8_t *data, int len) { (void)data; (void)len; } /* ============================================================ * 模拟数据发送逻辑 (1D & 2D) 的前置声明 * ============================================================ */ static void simulate_send_2d_frame(uint32_t frameNum); static void simulate_send_1d_frame(uint32_t frameNum); /* ============================================================ * QDX API 回调函数实现 * ============================================================ */ /** * @brief 上位机下发配置更新时的回调 */ static void on_config_updated(const ConfigCommon_t *common, const Config2D_t *cfg2d, const Config1D_t *cfg1d) { printf(CLR_CYAN "\n[API Callback] 收到最新配置" CLR_RESET "\n"); /* 打印通用配置 */ printf(" -> Common: Pipeline: %.*s, Type: %d, Mode: %d, Tag: %d, " "Strictness: %d, Custom: %d\n", 16, common->PipelineId, common->PipelineType, common->WorkMode, common->ConfigTag, common->StrictnessLevel, common->IsCustomMode); /* 打印 2D 配置 */ printf(" -> 2D: Enabled: %d, Live: %d, DevId: %d, %dx%d, Fps: %d\n", cfg2d->Enabled, cfg2d->IsLive, cfg2d->DeviceId, cfg2d->Width, cfg2d->Height, cfg2d->Fps); printf(" -> 2D: Mask: %d (Thresh: %d, %dx%d), Target: %dx%d\n", cfg2d->MaskEnabled, cfg2d->MaskThreshold, cfg2d->MaskWidth, cfg2d->MaskHeight, cfg2d->TargetWidth, cfg2d->TargetHeight); /* 打印 1D 配置 */ printf(" -> 1D: Enabled: %d, RunMode: %d, TriggerType: %d, " "BufferSize: %d\n", cfg1d->Enabled, cfg1d->RunMode, cfg1d->TriggerType, cfg1d->BufferSize); /* 核心:将配置下发给预处理算法库 */ Preprocess_Settings_Change(cfg2d, cfg1d, common); printf(CLR_GREEN " -> [OK] 已将参数同步至预处理引擎" CLR_RESET "\n"); } /** * @brief 上位机反馈检测结果时的回调 */ static void on_detection_result(uint32_t frameNumber, uint8_t resultStatus) { printf(CLR_CYAN "[API Callback] 收到检测结果: Frame #%u, Result: %s" CLR_RESET "\n", frameNumber, resultStatus == 0 ? "OK" : "NG"); } /** * @brief 当服务端下发 TempFrame(请求回传当前帧)时的回调 */ static void on_temp_frame_request(uint8_t is2dRequest) { printf(CLR_CYAN "[API Callback] 收到服务端 TempFrame 采集请求 (is2D=%d)" CLR_RESET "\n", is2dRequest); static uint32_t simulated_frame_num = 1; if (g_dimension_mode == 1) { printf(CLR_MAGENTA " -> 模拟: 开始处理并回传 2D 阵列图像..." CLR_RESET "\n"); simulate_send_2d_frame(simulated_frame_num++); } else { printf(CLR_MAGENTA " -> 模拟: 开始处理并回传 1D 阵列数据..." CLR_RESET "\n"); simulate_send_1d_frame(simulated_frame_num++); } } /* ============================================================ * 模拟数据发送逻辑 (1D & 2D) * ============================================================ */ /** * @brief 随机选取 2D 矩阵文件并解析 * 逻辑与原 demo 的 parse_temperature_matrix 完全一致 */ static int load_random_2d_matrix(uint16_t *matrix, int max_pixels, int *out_w, int *out_h) { char pattern[300]; snprintf(pattern, sizeof(pattern), "%s*.txt", g_matrix_dir); WIN32_FIND_DATAA fd; HANDLE hFind = FindFirstFileA(pattern, &fd); if (hFind == INVALID_HANDLE_VALUE) return -1; int count = 0; do { if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) count++; } while (FindNextFileA(hFind, &fd)); FindClose(hFind); if (count == 0) return -1; int target = rand() % count; hFind = FindFirstFileA(pattern, &fd); int idx = 0; char filepath[300] = {0}; do { if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { if (idx == target) { snprintf(filepath, sizeof(filepath), "%s%s", g_matrix_dir, fd.cFileName); break; } idx++; } } while (FindNextFileA(hFind, &fd)); FindClose(hFind); if (filepath[0] == '\0') return -1; static char file_buf[512 * 1024]; FILE *fp = fopen(filepath, "r"); if (!fp) return -1; int file_len = (int)fread(file_buf, 1, sizeof(file_buf) - 1, fp); fclose(fp); file_buf[file_len] = '\0'; char *pos = strstr(file_buf, "\"temperature\""); if (!pos) return -1; pos = strchr(pos, '['); if (!pos) return -1; pos++; int width = 0, height = 0, total = 0, row_count = 0; while (*pos != '\0') { if (*pos == '[') { row_count = 0; pos++; } else if (*pos == ']') { if (row_count > 0) { height++; if (width == 0) width = row_count; row_count = 0; } else { break; } pos++; } else if (*pos >= '0' && *pos <= '9') { int val = 0; while (*pos >= '0' && *pos <= '9') { val = val * 10 + (*pos - '0'); pos++; } if (*pos == '.') { pos++; while (*pos >= '0' && *pos <= '9') pos++; } if (total < max_pixels) { matrix[total] = (uint16_t)val; } total++; row_count++; } else { pos++; } } *out_w = width; *out_h = height; return total > 0 ? total : -1; } /** * @brief 模拟发送 2D 图像帧 (完整 API Zero-Copy 流水线) */ static void simulate_send_2d_frame(uint32_t frameNum) { int w = 0, h = 0; int total = load_random_2d_matrix(g_raw_matrix, MAX_2D_PIXELS, &w, &h); if (total <= 0) { printf(CLR_RED "[Error] 读取 2D 矩阵失败" CLR_RESET "\n"); return; } /* 1. 构造原始图像结构体 */ RawImageBuffer_t rawBuff = {.pData = g_raw_matrix, .Width = (uint16_t)w, .Height = (uint16_t)h, .FrameNumber = frameNum}; /* 2. 重置发送缓冲包装器(确保偏移正确) */ g_api_tx_buffer.pBuffer = g_tx_buffer; g_api_tx_buffer.TotalCapacity = TX_BUFFER_TOTAL_CAPACITY; g_api_tx_buffer.HeadOffset = TX_BUFFER_HEAD_OFFSET; g_api_tx_buffer.ValidPayloadLen = 0; PreprocessResult_t resMeta = {0}; /* 3. 核心 API: 预处理执行。它会根据滑动窗口目标大小提取 ROI, * 并写入我们提供的 g_tx_buffer[HeadOffset] 处。 */ if (Preprocess_Execute(&rawBuff, &g_api_tx_buffer, &resMeta) == 0) { printf(CLR_MAGENTA "[Data] 预处理完成: 提取出 %dx%d ROI, %.1f~%.1f°C" CLR_RESET "\n", resMeta.ValidWidth, resMeta.ValidHeight, resMeta.MinTemp / 10.0f, resMeta.MaxTemp / 10.0f); /* 4. 核心 API: TCP 零拷贝图文打包与发送。 * 内部会在 HeadOffset 前面的 1KB 预留空间中拼装 18 字节包头,直接压入 * LwIP(WinSock) */ int8_t err = TcpLogic_BuildAndSendTemperatureFrame( &g_api_tx_buffer, &resMeta, 0x01 /* TRIGGER */, 1 /* IS_2D */); if (err == 0) { printf(CLR_GREEN " -> [OK] TCP 帧已加入发送队列" CLR_RESET "\n"); } else { printf(CLR_RED " -> [Fail] TCP 帧发送失败, 错误码: %d" CLR_RESET "\n", err); } } else { printf(CLR_RED "[Data] 预处理拒绝了本帧数据 (未达到阈值或参数异常)" CLR_RESET "\n"); } } /** * @brief 模拟发送 1D 温度帧 * 1D 模式下不需要滑动窗口图像处理,我们可以跳过 Preprocess_Execute, * 直接组装 `PreprocessResult_t` 结构后提供给发送库。 */ static void simulate_send_1d_frame(uint32_t frameNum) { #define POINTS_COUNT 30 int16_t min_t = 32767; int16_t max_t = -32768; int32_t sum_t = 0; g_api_tx_buffer.pBuffer = g_tx_buffer; g_api_tx_buffer.TotalCapacity = TX_BUFFER_TOTAL_CAPACITY; g_api_tx_buffer.HeadOffset = TX_BUFFER_HEAD_OFFSET; uint8_t *dest = g_api_tx_buffer.pBuffer + g_api_tx_buffer.HeadOffset; /* 模拟生成温度点 */ for (int i = 0; i < POINTS_COUNT; i++) { uint16_t time_offset = (uint16_t)(i * 600 / (POINTS_COUNT - 1)); uint16_t temp = (uint16_t)(500 + rand() % 501); /* 50.0~100.0 */ int16_t t = (int16_t)temp; if (t < min_t) min_t = t; if (t > max_t) max_t = t; sum_t += t; /* 序列化到 buffer (按协议要求 TempPoint1D_t: 2字节时间偏移 + * 2字节温度,小端存储) */ dest[0] = (uint8_t)(time_offset & 0xFF); dest[1] = (uint8_t)((time_offset >> 8) & 0xFF); dest[2] = (uint8_t)(temp & 0xFF); dest[3] = (uint8_t)((temp >> 8) & 0xFF); dest += 4; } g_api_tx_buffer.ValidPayloadLen = POINTS_COUNT * 4; /* 构造预处理元数据欺骗网络库 */ PreprocessResult_t resMeta = {.pValidData = g_api_tx_buffer.pBuffer + g_api_tx_buffer.HeadOffset, .DataLength = g_api_tx_buffer.ValidPayloadLen, .ValidWidth = POINTS_COUNT, .ValidHeight = 1, .MinTemp = min_t, .MaxTemp = max_t, .AvgTemp = (int16_t)(sum_t / POINTS_COUNT), .RoiTemp = (int16_t)(sum_t / POINTS_COUNT), .Status = 0, .FrameNumber = frameNum}; printf(CLR_MAGENTA "[Data] 构造 1D 数据完成: %d 点, %.1f~%.1f°C" CLR_RESET "\n", POINTS_COUNT, min_t / 10.0f, max_t / 10.0f); int8_t err = TcpLogic_BuildAndSendTemperatureFrame( &g_api_tx_buffer, &resMeta, 0x01 /* TRIGGER */, 0 /* IS_1D */); if (err == 0) { printf(CLR_GREEN " -> [OK] 1D 帧已加入发送队列" CLR_RESET "\n"); } else { printf(CLR_RED " -> [Fail] 网络发送失败, 错误码: %d" CLR_RESET "\n", err); } #undef POINTS_COUNT } /* ============================================================ * 主函数 (控制台 UI 交互层) * ============================================================ */ int main(void) { /* 设置控制台支持 UTF-8 和 ANSI 转义 */ SetConsoleOutputCP(65001); #ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004 #endif HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); DWORD dwMode = 0; GetConsoleMode(hOut, &dwMode); SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); printf("========================================================\n"); printf("= TCP 透传客户端 API 演示版 (基于 QDXnetworkStack.c)\n"); printf("= 服务端: %s 控制: %d 数据: %d\n", DEFAULT_SERVER_IP, DEFAULT_CONTROL_PORT, DEFAULT_DATA_PORT); printf("========================================================\n\n"); /* Windows 下需要手动启动 Winsock */ WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("[ERROR] WSAStartup 失败。\n"); return 1; } /* 交互式输入设备 ID */ int input_id = 0; printf("请输入设备 ID (整数, 例如 101): "); if (scanf("%d", &input_id) != 1 || input_id < 0 || input_id > 65535) { printf("[ERROR] 无效。默认设为 101。\n"); input_id = 101; } printf("\n设备 ID 设置为: %d\n\n", input_id); /* 清除遗留的回车换行 */ int c; while ((c = getchar()) != '\n' && c != EOF) { } /* 选择维度 */ int dim_choice = 0; printf("请选择维度模式:\n"); printf(" 1 - 一维模式 (随机温度数据)\n"); printf(" 2 - 二维模式 (从目录随机读取矩阵文件)\n"); printf("请输入 (1 或 2): "); if (scanf("%d", &dim_choice) != 1 || (dim_choice != 1 && dim_choice != 2)) { dim_choice = 1; } g_dimension_mode = (dim_choice == 2) ? 1 : 0; printf("\n维度模式: %s\n\n", g_dimension_mode ? "二维 (2D)" : "一维 (1D)"); if (g_dimension_mode == 1) { snprintf(g_matrix_dir, sizeof(g_matrix_dir), "../tcp_c_demo/src/2d_mask/"); printf("2D 矩阵文件目录: %s\n\n", g_matrix_dir); } /* 生成假 UUID */ uint8_t uuid[16] = {0}; srand((unsigned int)GetTickCount()); for (int i = 0; i < 16; i++) { uuid[i] = (uint8_t)(rand() % 256); } /* --------------------------------------------------------- * 重点:纯 API 调用区 * --------------------------------------------------------- */ printf(">>> 正在初始化 QDXnetworkStack 库架构...\n"); /* 1. 初始化预处理引擎 (分配 256x256 静态滑窗资源) */ Preprocess_Init(256, 256); /* 2. 初始化网络栈 */ TcpLogic_Init(uuid, NULL); /* 3. 注册业务回调 (配置更新、坏品通知、采集请求) */ TcpLogic_RegisterConfigCallback(on_config_updated); TcpLogic_RegisterDetectionCallback(on_detection_result); TcpLogic_RegisterTempFrameRequestCallback(on_temp_frame_request); /* 4. [Hack Demo] 因为 API 库不包含直接设置当前 DevID * 的接口(依赖服务端下发), 但为了 Demo 一开始能用指定的 101 ID * 发起连接,我们手动造一个回调注入进去, 实际上在真实的 CH32 工程中,ID * 会保存在 Flash 里面。这演示了库的工作原理。 */ // 由于 g_TcpLogic 结构体对于外部是静态隐藏的, // API 设计中如果要改变 DevID,规范应是等待协议 0x05 下发。 // 此处模拟真实情况:我们相信服务端会自动给我分发 ID, // 所以不再强求立刻修改。库有默认 ID=101。 /* 5. 启动后台处理收发断连。 * 它内部通过 _beginthreadex 开了线程! */ TcpLogic_Start(); printf(">>> 库引擎以启动。后台线程已接管所有 Socket 收发。\n\n"); /* ========================================================= * 主业务测试循环 * ========================================================= */ printf("菜单 (上位机请求时会自动发送):\n"); printf(" [s] 立即触发一次发送 [c] 打印当前内存参数册\n"); printf(" [q] 退出 (Ctrl+C 也可)\n\n"); while (1) { if (_kbhit()) { int ch = _getch(); if (ch == 'q' || ch == 'Q') { break; } else if (ch == 's' || ch == 'S') { printf("\n[Manual] 用户主动触发一帧。\n"); on_temp_frame_request(g_dimension_mode); } else if (ch == 'c' || ch == 'C') { ConfigCommon_t com; Config2D_t c2d; Config1D_t c1d; if (TcpLogic_GetLatestConfig(&com, &c2d, &c1d) == 0) { printf(CLR_GREEN "\n[Info] 当前静态配置缓存中包含数据 (严苛度: %d)。" CLR_RESET "\n", com.StrictnessLevel); /* 打印通用配置 */ printf(" -> Common: Pipeline: %.*s, Type: %d, Mode: %d, Tag: %d, " "Strictness: %d, Custom: %d\n", 16, com.PipelineId, com.PipelineType, com.WorkMode, com.ConfigTag, com.StrictnessLevel, com.IsCustomMode); /* 打印 2D 配置 */ printf( " -> 2D: Enabled: %d, Live: %d, DevId: %d, %dx%d, Fps: %d\n", c2d.Enabled, c2d.IsLive, c2d.DeviceId, c2d.Width, c2d.Height, c2d.Fps); printf(" -> 2D: Mask: %d (Thresh: %d, %dx%d), Target: %dx%d\n", c2d.MaskEnabled, c2d.MaskThreshold, c2d.MaskWidth, c2d.MaskHeight, c2d.TargetWidth, c2d.TargetHeight); /* 打印 1D 配置 */ printf(" -> 1D: Enabled: %d, RunMode: %d, TriggerType: %d, " "BufferSize: %d\n", c1d.Enabled, c1d.RunMode, c1d.TriggerType, c1d.BufferSize); } else { printf(CLR_YELLOW "\n[Info] 当前静态配置缓存尚未收到上位机同步!" CLR_RESET "\n"); } } } Sleep(50); } printf("程序退出中...\n"); WSACleanup(); return 0; }