ch32v307_camera/pc/demo_main.c
2026-03-13 22:22:45 +08:00

520 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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 <conio.h> /* _kbhit, _getch */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
/* ============================================================
* 宏定义与全局变量
* ============================================================ */
/* 默认服务端连接参数(硬编码) */
#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;
}