This commit is contained in:
2026-03-13 22:22:45 +08:00
parent 2cdb65309f
commit ea4368bf15
19 changed files with 28899 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
================================================================================
END USER LICENSE AGREEMENT
================================================================================
Copyright (c) QDX. All rights reserved.
This End User License Agreement ("Agreement") is a legal agreement between you
(either an individual or a single entity) and QDX regarding the use of the
source code and accompanying documentation (hereinafter referred to as the
"Software Source Code").
By obtaining, using, or modifying the Software Source Code, you agree to be
bound by the terms and conditions of this Agreement. If you do not agree, do
not use the Software Source Code.
--------------------------------------------------------------------------------
1. COPYRIGHT AND OWNERSHIP
--------------------------------------------------------------------------------
All title, copyrights, and other intellectual property rights in and to the
Software Source Code (including but not limited to its design concepts,
algorithms, data structures, implementation, and related documentation) are
owned by QDX. The Software Source Code is protected by copyright laws and
international copyright treaties.
QDX reserves the right of final interpretation regarding the terms of this
Agreement and the scope of use of the Software Source Code.
--------------------------------------------------------------------------------
2. GRANT OF LICENSE AND RESTRICTIONS
--------------------------------------------------------------------------------
Subject to your compliance with this Agreement, QDX grants you a limited,
non-transferable, and non-exclusive right to use and modify the Software Source
Code under specific business contexts.
* Authorized Scope:
The open use of this Software Source Code is strictly limited to the specific,
formally authorized commercial project and its directly related program or
feature development (including debugging, compiling, and system integration).
* Strict Prohibitions:
Without the express written permission of QDX, it is strictly prohibited to
use this Software Source Code for any unauthorized commercial projects,
products, open-source releases, or to disclose it to any unauthorized third
parties. You may not package this code independently for sale, rental, or
distribution for competitive commercial purposes.
--------------------------------------------------------------------------------
3. DISCLAIMER OF WARRANTIES ("AS IS")
--------------------------------------------------------------------------------
* No Warranties:
The Software Source Code and its associated documentation are provided "AS IS",
without warranty of any kind. QDX makes no express or implied warranties,
including, but not limited to, the implied warranties of merchantability,
fitness for a particular purpose, or non-infringement.
* No Liability for Variants and Performance:
QDX shall not be responsible for the performance, quality, security,
commercial consequences, or any software failures resulting from any variants
(including new features, refactoring, or ported code snippets) created by you
during your use or modification of the Software Source Code.
* Limitation of Liability:
In no event shall QDX be liable for any direct, indirect, incidental, special,
or punitive damages (including, but not limited to, loss of profits, business
interruption, loss of data, or loss of business information) arising out of
the use or inability to use the Software Source Code, even if QDX has been
advised of the possibility of such damages.
--------------------------------------------------------------------------------
4. LEGAL EFFECT
--------------------------------------------------------------------------------
QDX holds the final authority and right of interpretation concerning any
disputes arising from or related to this Agreement according to the law. If you
fail to comply with any terms of this Agreement, QDX reserves the right to
terminate your use authorization immediately and pursue legal responsibility for
any economic damages.
By acquiring and utilizing this Software Source Code, you acknowledge that you
have read, understood, and agreed to be strictly bound by all the authorization
and disclaimer clauses set forth above.
+125
View File
@@ -0,0 +1,125 @@
/**
* @file qdx_port.h
* @brief Hardware/OS Abstraction Layer (HAL) for QDX Network Stack
*
* Provides platform-independent interfaces for network socket operations,
* timing, mutexes, and threading. Users must implement these functions
* based on their specific MCU OS (e.g., FreeRTOS, LwIP, RT-Thread).
*/
#ifndef QDX_PORT_H
#define QDX_PORT_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================
* Time & Delay
* ============================================================ */
/**
* @brief Get absolute system uptime/ticks in milliseconds.
* @return Milliseconds since system boot.
*/
uint32_t qdx_port_get_tick_ms(void);
/**
* @brief Blocking delay in milliseconds.
* @param ms Delay time
*/
void qdx_port_delay_ms(uint32_t ms);
/* ============================================================
* Network (TCP Socket)
* ============================================================ */
/* Opaque handle for sockets dependent on underlying IP stack */
typedef void *qdx_socket_t;
/**
* @brief Create and connect a TCP socket to a remote host.
* @param ip Str IP address of the server (e.g., "192.168.1.10")
* @param port Remote port
* @return Valid socket handle on success, or NULL on failure.
*/
qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port);
/**
* @brief Close a TCP socket.
* @param sock Socket handle
*/
void qdx_port_tcp_close(qdx_socket_t sock);
/**
* @brief Send data over TCP socket.
* @param sock Socket handle
* @param data Data buffer to send
* @param len Length of data in bytes
* @return Number of bytes sent, or < 0 for error.
*/
int32_t qdx_port_tcp_send(qdx_socket_t sock, const uint8_t *data, uint32_t len);
/**
* @brief Receive data from TCP socket (Non-blocking or specific timeout).
* @param sock Socket handle
* @param buf Buffer to store received data
* @param max_len Maximum buffer size
* @return Number of bytes received. Return 0 if timeout/empty. Return < 0 for
* connection closed/error.
*/
int32_t qdx_port_tcp_recv(qdx_socket_t sock, uint8_t *buf, uint32_t max_len);
/* ============================================================
* Mutex & Threading
* ============================================================ */
/* Opaque handle for mutex */
typedef void *qdx_mutex_t;
/**
* @brief Create a recursive or standard mutex.
* @return Mutex handle, or NULL on failure.
*/
qdx_mutex_t qdx_port_mutex_create(void);
/**
* @brief Lock a mutex.
* @param mutex Mutex handle
*/
void qdx_port_mutex_lock(qdx_mutex_t mutex);
/**
* @brief Unlock a mutex.
* @param mutex Mutex handle
*/
void qdx_port_mutex_unlock(qdx_mutex_t mutex);
/**
* @brief Delete a mutex.
* @param mutex Mutex handle
*/
void qdx_port_mutex_delete(qdx_mutex_t mutex);
/* Thread entry callback definition */
typedef void (*qdx_thread_entry_t)(void *arg);
/**
* @brief Create a background thread/task.
* @param name Task name
* @param entry Task entry function
* @param arg Task argument
* @param stack_size Requested stack size in bytes
* @param priority Task priority
* @return 0 on success, < 0 on failure.
*/
int8_t qdx_port_thread_create(const char *name, qdx_thread_entry_t entry,
void *arg, uint32_t stack_size, uint8_t priority);
#ifdef __cplusplus
}
#endif
#endif /* QDX_PORT_H */
@@ -0,0 +1,143 @@
/**
* @file qdx_port_template.c
* @brief Template Implementation of the HAL Port for QDX Network Stack
*
* Instructions:
* 1. Rename this file to qdx_port.c in your MCU project.
* 2. Implement these functions using your specific OS/Network APIs
* (e.g., LwIP, FreeRTOS, RT-Thread, CMSIS-OS).
* 3. Ensure qdx_port_tcp_recv is non-blocking or uses a short timeout,
* so the background thread can periodically check connection status.
*/
#include "qdx_port.h"
#include <stddef.h> /* For NULL */
/* Include your system headers here
* #include "FreeRTOS.h"
* #include "task.h"
* #include "lwip/sockets.h"
* #include "lwip/sys.h"
*/
/* ============================================================
* Time & Delay
* ============================================================ */
uint32_t qdx_port_get_tick_ms(void) {
/* TODO: Return current system uptime in milliseconds.
* Example (FreeRTOS): return (uint32_t)(xTaskGetTickCount() *
* portTICK_PERIOD_MS); Example (HAL): return HAL_GetTick();
*/
return 0;
}
void qdx_port_delay_ms(uint32_t ms) {
/* TODO: Block current thread for ms milliseconds.
* Example (FreeRTOS): vTaskDelay(pdMS_TO_TICKS(ms));
* Example (HAL): HAL_Delay(ms);
*/
}
/* ============================================================
* Network (TCP Socket)
* ============================================================ */
qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port) {
/* TODO: Create socket and connect to IP:PORT.
* Example (LwIP/BSD Sockets):
* int sock = socket(AF_INET, SOCK_STREAM, 0);
* if (sock < 0) return NULL;
* struct sockaddr_in dest_addr;
* dest_addr.sin_addr.s_addr = inet_addr(ip);
* dest_addr.sin_family = AF_INET;
* dest_addr.sin_port = htons(port);
* if (connect(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) == 0)
* {
* // Optional: Set receive timeout
* return (qdx_socket_t)(intptr_t)sock;
* }
* close(sock);
*/
return NULL;
}
void qdx_port_tcp_close(qdx_socket_t sock) {
/* TODO: Close the socket gracefully.
* Example:
* if (sock) {
* close((int)(intptr_t)sock);
* }
*/
}
int32_t qdx_port_tcp_send(qdx_socket_t sock, const uint8_t *data,
uint32_t len) {
/* TODO: Send data through the socket.
* Example:
* if (!sock) return -1;
* return send((int)(intptr_t)sock, data, len, 0);
*/
return -1;
}
int32_t qdx_port_tcp_recv(qdx_socket_t sock, uint8_t *buf, uint32_t max_len) {
/* TODO: Receive data from the socket.
* Should return actual bytes read. If no bytes are available (timeout),
* return 0. If socket is closed or an error occurs, return < 0. Example: if
* (!sock) return -1;
* // Assume socket was set with SO_RCVTIMEO to prevent hanging forever
* int bytes = recv((int)(intptr_t)sock, buf, max_len, 0);
* if (bytes < 0) {
* if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; // Timeout
* return -1; // Actual error
* }
* return bytes; // Includes bytes == 0 (graceful close) -> might want to
* map to -1
*/
return -1;
}
/* ============================================================
* Mutex & Threading
* ============================================================ */
qdx_mutex_t qdx_port_mutex_create(void) {
/* TODO: Create and return a mutex handle.
* Example (FreeRTOS):
* SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
* return (qdx_mutex_t)mutex;
*/
return NULL;
}
void qdx_port_mutex_lock(qdx_mutex_t mutex) {
/* TODO: Lock the given mutex.
* Example (FreeRTOS):
* if (mutex) xSemaphoreTake((SemaphoreHandle_t)mutex, portMAX_DELAY);
*/
}
void qdx_port_mutex_unlock(qdx_mutex_t mutex) {
/* TODO: Unlock the given mutex.
* Example (FreeRTOS):
* if (mutex) xSemaphoreGive((SemaphoreHandle_t)mutex);
*/
}
void qdx_port_mutex_delete(qdx_mutex_t mutex) {
/* TODO: Delete the given mutex.
* Example (FreeRTOS):
* if (mutex) vSemaphoreDelete((SemaphoreHandle_t)mutex);
*/
}
int8_t qdx_port_thread_create(const char *name, qdx_thread_entry_t entry,
void *arg, uint32_t stack_size,
uint8_t priority) {
/* TODO: Create a background thread/task to run the entry function.
* Example (FreeRTOS):
* BaseType_t res = xTaskCreate(entry, name, stack_size /
* sizeof(StackType_t), arg, priority, NULL); return (res == pdPASS) ? 0 : -1;
*/
return -1;
}
+325
View File
@@ -0,0 +1,325 @@
/**
* @file qdx_preprocess.c
* @brief Zero-Copy Image Preprocessing Implementation
*/
#include "qdx_preprocess.h"
#include "qdx_port.h"
#include <string.h>
/* ============================================================
* Internal State & Configuration Cache
* ============================================================ */
/* Static allocation for column sums to avoid malloc */
#define PREPROCESS_MAX_WIDTH 256
static uint32_t g_col_sums[PREPROCESS_MAX_WIDTH];
static uint8_t g_is_initialized = 0;
/* 配置读写互斥锁,防止 Execute 与 Settings_Change 并发冲突 */
static qdx_mutex_t g_preprocess_mutex = NULL;
static struct {
Config2D_t cfg2d;
Config1D_t cfg1d;
ConfigCommon_t common;
} g_PreprocessCfg;
/* ============================================================
* API Implementation
* ============================================================ */
int8_t Preprocess_Init(uint16_t maxWidth, uint16_t maxHeight) {
(void)maxHeight; /* 列累加仅依赖宽度 */
if (maxWidth > PREPROCESS_MAX_WIDTH) {
return -1; /* 超出静态分配缓冲区上限 */
}
memset(g_col_sums, 0, sizeof(g_col_sums));
memset(&g_PreprocessCfg, 0, sizeof(g_PreprocessCfg));
/* 创建配置互斥锁 */
g_preprocess_mutex = qdx_port_mutex_create();
if (g_preprocess_mutex == NULL)
return -1;
/* 最小默认配置,防止除零或无限循环 */
g_PreprocessCfg.cfg2d.TargetWidth = 1;
g_PreprocessCfg.cfg2d.TargetHeight = 1;
g_is_initialized = 1;
return 0;
}
int8_t Preprocess_Settings_Change(const Config2D_t *newConfig2D,
const Config1D_t *newConfig1D,
const ConfigCommon_t *newCommon) {
if (!g_is_initialized)
return -1;
/* 加锁保护配置更新,防止与 Execute 读取产生竞态 */
qdx_port_mutex_lock(g_preprocess_mutex);
if (newConfig2D)
memcpy(&g_PreprocessCfg.cfg2d, newConfig2D, sizeof(Config2D_t));
if (newConfig1D)
memcpy(&g_PreprocessCfg.cfg1d, newConfig1D, sizeof(Config1D_t));
if (newCommon)
memcpy(&g_PreprocessCfg.common, newCommon, sizeof(ConfigCommon_t));
/* 安全检查 */
if (g_PreprocessCfg.cfg2d.TargetWidth == 0)
g_PreprocessCfg.cfg2d.TargetWidth = 1;
if (g_PreprocessCfg.cfg2d.TargetHeight == 0)
g_PreprocessCfg.cfg2d.TargetHeight = 1;
qdx_port_mutex_unlock(g_preprocess_mutex);
return 0;
}
int8_t Preprocess_Execute(const RawImageBuffer_t *input,
TcpTxBuffer_t *out_buffer,
PreprocessResult_t *output_meta) {
if (!g_is_initialized || !input || !input->pData || !out_buffer ||
!out_buffer->pBuffer || !output_meta)
return -1;
/* 加锁快照当前配置,最小化持锁时间 */
qdx_port_mutex_lock(g_preprocess_mutex);
uint16_t tgt_w = g_PreprocessCfg.cfg2d.TargetWidth;
uint16_t tgt_h = g_PreprocessCfg.cfg2d.TargetHeight;
int16_t thresh = g_PreprocessCfg.cfg2d.TriggerTemperatureThreshold;
qdx_port_mutex_unlock(g_preprocess_mutex);
uint16_t w = input->Width;
uint16_t h = input->Height;
/* 目标超过输入时回退到整幅图像 */
if (tgt_w > w)
tgt_w = w;
if (tgt_h > h)
tgt_h = h;
/* ---- 缓冲区容量越界检查 ---- */
uint32_t required_bytes = (uint32_t)tgt_w * tgt_h * 2u;
if (out_buffer->HeadOffset + required_bytes > out_buffer->TotalCapacity) {
return -3; /* 输出缓冲区空间不足 */
}
/* 判断是否需要滑窗计算,或直接导出全图 */
if (tgt_w == w && tgt_h == h) {
/* 无需滑窗,仅做温度过滤与整体统计 */
int16_t min_t = 32767;
int16_t max_t = -32768;
uint32_t total_sum = 0;
uint32_t pixels = w * h;
/* Write directly to out_buffer starting at HeadOffset */
uint8_t *dest_ptr = out_buffer->pBuffer + out_buffer->HeadOffset;
for (uint32_t i = 0; i < pixels; i++) {
int16_t raww_val = (int16_t)input->pData[i];
if (raww_val < min_t)
min_t = raww_val;
if (raww_val > max_t)
max_t = raww_val;
total_sum += raww_val;
/* Encode Original Raw Value (Little-Endian sequence into uint8_t) */
*dest_ptr++ = (uint8_t)(raww_val & 0xFF);
*dest_ptr++ = (uint8_t)((raww_val >> 8) & 0xFF);
}
output_meta->pValidData = out_buffer->pBuffer + out_buffer->HeadOffset;
output_meta->DataLength = pixels * 2;
output_meta->ValidWidth = w;
output_meta->ValidHeight = h;
output_meta->MinTemp = min_t;
output_meta->MaxTemp = max_t;
output_meta->AvgTemp = (int16_t)(total_sum / pixels);
output_meta->RoiTemp = output_meta->AvgTemp;
output_meta->Status = 0;
output_meta->FrameNumber = input->FrameNumber;
out_buffer->ValidPayloadLen = output_meta->DataLength;
return 0;
}
/* ------------------------------------------------------------------------
Perform Sliding Window Search for Average Temperature Target Region
------------------------------------------------------------------------ */
memset(g_col_sums, 0, w * sizeof(uint32_t));
uint32_t max_region_sum = 0;
uint16_t best_x = 0;
uint16_t best_y = 0;
for (uint16_t y = 0; y <= h - tgt_h; y++) {
/* Step 1: Initialize column sums for this row strip
If y == 0, we calculate the entire strip.
Otherwise, we subtract the top row that left, and add the bottom row that
entered. */
if (y == 0) {
for (uint16_t c = 0; c < w; c++) {
uint32_t col_total = 0;
for (uint16_t r = 0; r < tgt_h; r++) {
int16_t val = (int16_t)input->pData[r * w + c];
if (val < thresh)
val = 90;
col_total += val;
}
g_col_sums[c] = col_total;
}
} else {
/* Slide down by 1 row */
for (uint16_t c = 0; c < w; c++) {
int16_t top_val = (int16_t)input->pData[(y - 1) * w + c];
int16_t bot_val = (int16_t)input->pData[(y + tgt_h - 1) * w + c];
if (top_val < thresh)
top_val = 90;
if (bot_val < thresh)
bot_val = 90;
g_col_sums[c] = g_col_sums[c] - top_val + bot_val;
}
}
/* Step 2: Slide Across the Columns (Left to Right) */
uint32_t current_window_sum = 0;
/* Initialize first window */
for (uint16_t c = 0; c < tgt_w; c++) {
current_window_sum += g_col_sums[c];
}
if (current_window_sum > max_region_sum) {
max_region_sum = current_window_sum;
best_x = 0;
best_y = y;
}
/* Slide Right */
for (uint16_t x = 1; x <= w - tgt_w; x++) {
current_window_sum =
current_window_sum - g_col_sums[x - 1] + g_col_sums[x + tgt_w - 1];
if (current_window_sum > max_region_sum) {
max_region_sum = current_window_sum;
best_x = x;
best_y = y;
}
}
}
/* ------------------------------------------------------------------------
Extract the Data
------------------------------------------------------------------------ */
int16_t min_t = 32767;
int16_t max_t = -32768;
uint32_t total_sum = 0;
uint32_t pixels = tgt_w * tgt_h;
uint8_t *dest_ptr = out_buffer->pBuffer + out_buffer->HeadOffset;
for (uint16_t r = 0; r < tgt_h; r++) {
for (uint16_t c = 0; c < tgt_w; c++) {
/* Raw offset into original image */
uint32_t src_idx = (best_y + r) * w + (best_x + c);
int16_t raww_val = (int16_t)input->pData[src_idx];
if (raww_val < min_t)
min_t = raww_val;
if (raww_val > max_t)
max_t = raww_val;
total_sum += raww_val;
/* Output Original Raw Values */
*dest_ptr++ = (uint8_t)(raww_val & 0xFF);
*dest_ptr++ = (uint8_t)((raww_val >> 8) & 0xFF);
}
}
output_meta->pValidData = out_buffer->pBuffer + out_buffer->HeadOffset;
output_meta->DataLength = pixels * 2;
output_meta->ValidWidth = tgt_w;
output_meta->ValidHeight = tgt_h;
output_meta->MinTemp = min_t;
output_meta->MaxTemp = max_t;
output_meta->AvgTemp = (int16_t)(total_sum / pixels);
output_meta->RoiTemp = output_meta->AvgTemp;
output_meta->Status = 0;
output_meta->FrameNumber = input->FrameNumber;
out_buffer->ValidPayloadLen = output_meta->DataLength;
return 0;
}
/* ============================================================
* Internal Trigger Check
* ============================================================ */
int8_t Preprocess_CheckInternalTrigger2D(const RawImageBuffer_t *input) {
if (!g_is_initialized || !input || !input->pData)
return -1;
qdx_port_mutex_lock(g_preprocess_mutex);
uint16_t roi_x = g_PreprocessCfg.cfg2d.TriggerRoiX;
uint16_t roi_y = g_PreprocessCfg.cfg2d.TriggerRoiY;
uint16_t roi_w = g_PreprocessCfg.cfg2d.TriggerRoiW;
uint16_t roi_h = g_PreprocessCfg.cfg2d.TriggerRoiH;
uint8_t condition = g_PreprocessCfg.cfg2d.TriggerCondition;
int16_t thresh = g_PreprocessCfg.cfg2d.TriggerTemperatureThreshold;
qdx_port_mutex_unlock(g_preprocess_mutex);
uint16_t w = input->Width;
uint16_t h = input->Height;
/* Boundary Check & Clipping */
if (roi_w == 0 || roi_h == 0)
return 0;
if (roi_x >= w || roi_y >= h)
return 0;
if (roi_x + roi_w > w)
roi_w = w - roi_x;
if (roi_y + roi_h > h)
roi_h = h - roi_y;
int16_t max_temp = -32768;
int64_t sum_temp = 0;
uint32_t count = roi_w * roi_h;
for (uint16_t r = 0; r < roi_h; r++) {
for (uint16_t c = 0; c < roi_w; c++) {
int16_t val = (int16_t)input->pData[(roi_y + r) * w + (roi_x + c)];
/* Temperature Filtration Preprocessing */
if (val < thresh) {
val = 90; /* Treat as 9.0C */
}
if (val > max_temp)
max_temp = val;
sum_temp += val;
}
}
int16_t calc_val = 0;
if (condition == 1) {
/* 1: Max */
calc_val = max_temp;
} else {
/* 0: Average */
calc_val = (int16_t)(sum_temp / count);
}
if (calc_val >= thresh) {
return 1; /* Triggered */
}
return 0; /* Not triggered */
}
@@ -0,0 +1,74 @@
/**
* @file qdx_preprocess.h
* @brief Zero-Copy Image Preprocessing for Thermal Imaging
*
* Includes Sliding Window Maximum/Average ROI search,
* Temperature filtering, and Data extraction.
*/
#ifndef QDX_PREPROCESS_H
#define QDX_PREPROCESS_H
#include "qdx_protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize static memory resources for sliding window calculations.
*
* Called once during system boot. Pre-allocates column accumulation arrays
* to prevent dynamic allocation during runtime.
*
* @param maxWidth The maximum supported width of the 2D input matrix
* @param maxHeight The maximum supported height of the 2D input matrix
* @return 0 on success, < 0 on failure.
*/
int8_t Preprocess_Init(uint16_t maxWidth, uint16_t maxHeight);
/**
* @brief Execute the preprocessing pipeline on a raw frame.
*
* High-performance processing logic that reads `input`, applies thresholds,
* finds the best `TargetWidth x TargetHeight` ROI using sliding window arrays,
* and extracts that region into `out_buffer` starting precisely at
* `HeadOffset`.
*
* @param input Raw thermal image array
* @param out_buffer Pre-allocated transmission buffer wrapper
* @param output_meta Struct to fill with process results/metadata
* @return 0 if successful, < 0 if aborted (e.g., config invalid)
*/
int8_t Preprocess_Execute(const RawImageBuffer_t *input,
TcpTxBuffer_t *out_buffer,
PreprocessResult_t *output_meta);
/**
* @brief Safely update internal preprocessing shadow parameters.
*
* @param newConfig2D Partial Configuration Update
* @param newConfig1D Partial Configuration Update
* @param newCommon General System Setup
* @return 0 on success.
*/
int8_t Preprocess_Settings_Change(const Config2D_t *newConfig2D,
const Config1D_t *newConfig1D,
const ConfigCommon_t *newCommon);
/**
* @brief Check if the current frame triggers the internal 2D capture condition
*
* Evaluates the specified TriggerRoi region using either Max or Average
* temperature against the TriggerTemperatureThreshold.
*
* @param input Raw thermal image array
* @return 1 if trigger condition is met, 0 if not met, < 0 on error.
*/
int8_t Preprocess_CheckInternalTrigger2D(const RawImageBuffer_t *input);
#ifdef __cplusplus
}
#endif
#endif /* QDX_PREPROCESS_H */
+156
View File
@@ -0,0 +1,156 @@
/**
* @file qdx_protocol.c
* @brief Communication Protocol 2.0 Utility Implementations
*/
#include "qdx_protocol.h"
#include <string.h>
/* ============================================================
* Shift-Safe Serialization (Little-Endian)
* ============================================================ */
void qdx_write_u16_le(uint8_t *buf, uint16_t val) {
buf[0] = (uint8_t)(val & 0xFF);
buf[1] = (uint8_t)((val >> 8) & 0xFF);
}
void qdx_write_u32_le(uint8_t *buf, uint32_t val) {
buf[0] = (uint8_t)(val & 0xFF);
buf[1] = (uint8_t)((val >> 8) & 0xFF);
buf[2] = (uint8_t)((val >> 16) & 0xFF);
buf[3] = (uint8_t)((val >> 24) & 0xFF);
}
uint16_t qdx_read_u16_le(const uint8_t *buf) {
return (uint16_t)(buf[0] | (buf[1] << 8));
}
uint32_t qdx_read_u32_le(const uint8_t *buf) {
/* 显式转型防止高位字节的符号位扩展 */
return ((uint32_t)buf[0]) | ((uint32_t)buf[1] << 8) |
((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24);
}
/* ============================================================
* CRC16-MODBUS Checksum (Polynomial: 0xA001, Init: 0xFFFF)
* ============================================================ */
uint16_t qdx_crc16_modbus(const uint8_t *data, int len) {
uint16_t crc = 0xFFFF;
for (int i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
/* ============================================================
* Build Frame (Header + TLV + Payload + CRC16)
* Uses strict byte shifting to prevent unaligned struct access on MCU.
* ============================================================ */
int qdx_build_frame(uint8_t *buf, uint8_t msg_class, uint8_t tlv_type,
const uint8_t *payload, uint16_t payload_len,
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
uint8_t flags) {
flags |= FLAG_LAST_FRAGMENT;
uint16_t total_len = HEADER_SIZE + TLV_HEADER_SIZE + payload_len + CRC_SIZE;
/* 1. Fill FrameHeader (16 bytes) safely */
qdx_write_u16_le(buf + 0, PROTO_MAGIC); /* Magic */
buf[2] = PROTO_VERSION; /* Version */
qdx_write_u16_le(buf + 3, total_len); /* Length */
qdx_write_u16_le(buf + 5, seq); /* Sequence */
qdx_write_u32_le(buf + 7, timestamp); /* Timestamp */
buf[11] = 0x01; /* Source = MCU (0x01) */
qdx_write_u16_le(buf + 12, dev_id); /* DevID */
buf[14] = msg_class; /* Class */
buf[15] = flags; /* Flags */
/* 2. Fill TLV Header (3 bytes) safely */
buf[16] = tlv_type; /* TLV Type */
qdx_write_u16_le(buf + 17, payload_len); /* TLV Length */
/* 3. Copy Payload(使用 memmove 防止 payload 与 buf 重叠时的未定义行为) */
if (payload && payload_len > 0) {
memmove(buf + HEADER_SIZE + TLV_HEADER_SIZE, payload, payload_len);
}
/* 4. Append CRC16 */
uint16_t crc = qdx_crc16_modbus(buf, total_len - CRC_SIZE);
qdx_write_u16_le(buf + total_len - CRC_SIZE, crc);
return total_len;
}
/* ============================================================
* In-Place Frame Build (Header + CRC only, payload already in place)
* 用于零拷贝场景:payload(含 TLV + 数据)已填充在 buf[HEADER_SIZE] 处,
* 本函数仅写入 16 字节帧头并在尾部追加 CRC16。
* ============================================================ */
int qdx_build_frame_inplace(uint8_t *buf, uint8_t msg_class,
uint16_t payload_len, uint16_t dev_id, uint16_t seq,
uint32_t timestamp, uint8_t flags) {
flags |= FLAG_LAST_FRAGMENT;
uint16_t total_len = HEADER_SIZE + payload_len + CRC_SIZE;
/* 填写 16 字节帧头 */
qdx_write_u16_le(buf + 0, PROTO_MAGIC);
buf[2] = PROTO_VERSION;
qdx_write_u16_le(buf + 3, total_len);
qdx_write_u16_le(buf + 5, seq);
qdx_write_u32_le(buf + 7, timestamp);
buf[11] = 0x01; /* Source = MCU */
qdx_write_u16_le(buf + 12, dev_id);
buf[14] = msg_class;
buf[15] = flags;
/* payload 已在 buf[16..16+payload_len-1],无需拷贝 */
/* 追加 CRC16 */
uint16_t crc = qdx_crc16_modbus(buf, total_len - CRC_SIZE);
qdx_write_u16_le(buf + total_len - CRC_SIZE, crc);
return total_len;
}
/* ============================================================
* Build Fragmented Frame (Header + Chunk + CRC16, NO TLV)
* ============================================================ */
int qdx_build_fragment_frame(uint8_t *buf, uint8_t msg_class,
const uint8_t *chunk, uint16_t chunk_len,
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
uint8_t flags) {
uint16_t total_len = HEADER_SIZE + chunk_len + CRC_SIZE;
/* 1. Fill FrameHeader (16 bytes) safely */
qdx_write_u16_le(buf + 0, PROTO_MAGIC);
buf[2] = PROTO_VERSION;
qdx_write_u16_le(buf + 3, total_len);
qdx_write_u16_le(buf + 5, seq);
qdx_write_u32_le(buf + 7, timestamp);
buf[11] = 0x01; /* Source MCU */
qdx_write_u16_le(buf + 12, dev_id);
buf[14] = msg_class;
buf[15] = flags;
/* 2. Copy Payload Chunk(使用 memmove 应对潜在重叠) */
if (chunk && chunk_len > 0) {
memmove(buf + HEADER_SIZE, chunk, chunk_len);
}
/* 3. Append CRC16 */
uint16_t crc = qdx_crc16_modbus(buf, total_len - CRC_SIZE);
qdx_write_u16_le(buf + total_len - CRC_SIZE, crc);
return total_len;
}
+336
View File
@@ -0,0 +1,336 @@
/**
* @file qdx_protocol.h
* @brief Communication Protocol 2.0 Structures and Constants
*
* Platform-independent header for CH32 MCU and other systems.
* All structures use 1-byte alignment. Focuses on safe uint8_t
* serialization and avoiding hardware faults from unaligned access.
*/
#ifndef QDX_PROTOCOL_H
#define QDX_PROTOCOL_H
#include <stdint.h>
/* ============================================================
* Protocol Constants
* ============================================================ */
#define PROTO_MAGIC 0x55AA /* Frame Header Magic */
#define PROTO_VERSION 0x20 /* Protocol Version v2.0 */
/* Message Classes (Class) */
#define CLASS_CONTROL 0x01 /* Configuration / Control */
#define CLASS_DATA 0x02 /* Real-time Data Report */
#define CLASS_RESPONSE 0x03 /* ACK / NACK / Error */
#define CLASS_SYSTEM 0x04 /* Handshake / Heartbeat / Sync */
/* TLV Types */
#define TYPE_HANDSHAKE 0x01
#define TYPE_HEARTBEAT 0x02
#define TYPE_SYNC_TIME 0x03
#define TYPE_DEVID_ASSIGN 0x05
#define TYPE_TEMP_FRAME 0x10
#define TYPE_RAW_FRAME 0x11
#define TYPE_CONFIG_COMMON 0x20
#define TYPE_CONFIG_2D 0x22
#define TYPE_CONFIG_1D 0x23
#define TYPE_ACK_PAYLOAD 0x30
#define TYPE_DETECTION_RESULT 0x40
/* Flags */
#define FLAG_PRIORITY_MASK 0x03
#define FLAG_COMPRESSED 0x04
#define FLAG_ENCRYPTED 0x08
#define FLAG_ACK_REQ 0x10
#define FLAG_LAST_FRAGMENT 0x20
/* Frame Structure Sizes */
#define HEADER_SIZE 16 /* FrameHeader_t size */
#define TLV_HEADER_SIZE 3 /* TLV_t size (Type + Length) */
#define CRC_SIZE 2 /* CRC16 size */
/* Fragmentation limit */
#define MAX_FRAGMENT_PAYLOAD 1400
/* Error Codes */
#define ERR_NONE 0x0000
#define ERR_CRC 0x1001
#define ERR_VERSION 0x1002
#define ERR_LENGTH 0x1003
#define ERR_AUTH 0x2001
#define ERR_BUSY 0x2002
#define ERR_DEV_ID_CONFLICT 0x2003
#define ERR_PARAM 0x3001
/* ============================================================
* Internal Protocol Structures (Packed to 1 byte)
* ============================================================ */
#pragma pack(push, 1)
/**
* @brief Frame Header (16 bytes)
*/
typedef struct {
uint16_t Magic;
uint8_t Version;
uint16_t Length;
uint16_t Sequence;
uint32_t Timestamp;
uint8_t Source;
uint16_t DevID;
uint8_t Class;
uint8_t Flags;
} FrameHeader_t;
/**
* @brief TLV Header (3 bytes)
*/
typedef struct {
uint8_t Type;
uint16_t Length;
} TLV_t;
/**
* @brief Handshake (46 bytes)
*/
typedef struct {
uint16_t ProtocolVersion;
uint8_t DeviceUUID[16];
uint8_t AuthToken[16];
char HardwareVersion[8];
char FirmwareVersion[8];
uint32_t Capabilities;
} Handshake_t;
/**
* @brief Heartbeat (6 bytes)
*/
typedef struct {
uint32_t UpTime;
uint8_t CpuLoad;
uint8_t MemUsage;
} Heartbeat_t;
/**
* @brief ACK Payload (5 bytes)
*/
typedef struct {
uint16_t AckSeq;
uint8_t Status;
uint16_t ErrorCode;
} Ack_t;
/**
* @brief Device ID Assignment (4 bytes)
*/
typedef struct {
uint16_t NewDevID;
uint16_t Reserved;
} DevIDAssignment_t;
/**
* @brief Temperature Frame Header (18 bytes)
*/
typedef struct {
uint32_t FrameNumber;
uint16_t Width;
uint16_t Height;
int16_t MinTemp;
int16_t MaxTemp;
int16_t AvgTemp;
int16_t RoiTemp;
uint8_t FrameType;
uint8_t Status;
uint8_t Is2D;
uint8_t Reserved;
} TemperatureFrameHeader_t;
/**
* @brief 1D Temperature Point (4 bytes)
*/
typedef struct {
uint16_t TimeOffset;
uint16_t Temperature;
} TempPoint1D_t;
/**
* @brief Detection Result (8 bytes)
*/
typedef struct {
uint32_t FrameNumber;
uint8_t Result;
uint8_t Reserved[3];
} DetectionResult_t;
/**
* @brief Common Configuration
*/
typedef struct {
char PipelineId[16];
uint8_t PipelineType;
uint8_t WorkMode;
uint8_t ConfigTag;
uint8_t StrictnessLevel;
uint8_t IsCustomMode;
uint8_t Reserved[2];
} ConfigCommon_t;
/**
* @brief 2D Configuration
*/
typedef struct {
uint8_t Enabled;
uint8_t IsLive;
uint16_t DeviceId;
uint16_t Width;
uint16_t Height;
uint8_t Fps;
uint32_t Exposure;
uint8_t AutoExposure;
uint8_t MaskEnabled;
int16_t MaskThreshold;
uint16_t MaskWidth;
uint16_t MaskHeight;
int16_t Angle;
uint16_t TargetWidth;
uint16_t TargetHeight;
uint8_t TriggerMode;
uint8_t TriggerGpioLine;
uint16_t TriggerDelayMs;
uint8_t TriggerBurstCount;
uint16_t TriggerInternalIntervalMs;
int16_t TriggerTemperatureThreshold;
uint16_t TriggerDebounceIntervalMs;
uint8_t TriggerCondition;
uint16_t TriggerRoiX;
uint16_t TriggerRoiY;
uint16_t TriggerRoiW;
uint16_t TriggerRoiH;
uint16_t NGioDelay;
uint8_t OutputGpioLine;
uint8_t AlarmGpioLine;
uint16_t AlarmHoldMs;
uint8_t StoreNgImagesOnly;
uint8_t TrainingEnabled;
uint16_t TrainingSampleThreshold;
uint16_t ProcessingTimeoutMs;
uint8_t MaxProcessingQueueSize;
uint8_t Reserved;
} Config2D_t;
/**
* @brief 1D Configuration
*/
typedef struct {
uint8_t Enabled;
uint8_t RunMode;
uint8_t TriggerType;
uint16_t BufferSize;
int16_t TriggerTempLimit;
uint16_t StartPointsToRemove;
uint16_t ReferenceLength;
uint16_t HighTimerLimit;
uint16_t TimerCLimit;
uint8_t NgCountLimit;
uint16_t LSizeStart;
uint16_t RSizeStart;
uint16_t NGioDelay;
uint8_t OutputGpioLine;
uint8_t AlarmGpioLine;
uint16_t AlarmHoldMs;
} Config1D_t;
#pragma pack(pop)
/* ============================================================
* Application Level Business Structures
* ============================================================ */
/**
* @brief Raw Image Buffer provided by capture module
*/
typedef struct {
uint16_t *pData;
uint16_t Width;
uint16_t Height;
uint32_t FrameNumber;
} RawImageBuffer_t;
/**
* @brief Preprocessing result metadata
*/
typedef struct {
uint8_t *pValidData;
uint32_t DataLength;
uint16_t ValidWidth;
uint16_t ValidHeight;
int16_t MinTemp;
int16_t MaxTemp;
int16_t AvgTemp;
int16_t RoiTemp;
uint8_t Status;
uint32_t FrameNumber;
} PreprocessResult_t;
/**
* @brief Buffer wrapper for Zero-Copy TCP packet building
*/
typedef struct {
uint8_t *pBuffer;
uint32_t TotalCapacity;
uint32_t HeadOffset; /* Reserved header space for envelope */
uint32_t ValidPayloadLen;
} TcpTxBuffer_t;
/* System global config hook types */
typedef void (*ConfigUpdateCallback_t)(const ConfigCommon_t *common,
const Config2D_t *cfg2d,
const Config1D_t *cfg1d);
typedef void (*DetectionResultCallback_t)(uint32_t frameNumber,
uint8_t resultStatus);
/* ============================================================
* Protocol Utility Function Declarations
* ============================================================ */
/* Calculate Modbus CRC16 */
uint16_t qdx_crc16_modbus(const uint8_t *data, int len);
/* Shift-safe write: 16-bit little-endian */
void qdx_write_u16_le(uint8_t *buf, uint16_t val);
/* Shift-safe write: 32-bit little-endian */
void qdx_write_u32_le(uint8_t *buf, uint32_t val);
/* Shift-safe read: 16-bit little-endian */
uint16_t qdx_read_u16_le(const uint8_t *buf);
/* Shift-safe read: 32-bit little-endian */
uint32_t qdx_read_u32_le(const uint8_t *buf);
/**
* @brief Build a complete protocol frame using safe byte shifts.
*/
int qdx_build_frame(uint8_t *buf, uint8_t msg_class, uint8_t tlv_type,
const uint8_t *payload, uint16_t payload_len,
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
uint8_t flags);
/**
* @brief In-place frame build: 仅填写帧头并追加 CRC,不拷贝 payload。
* 用于零拷贝场景,payload(含 TLV)已在 buf[HEADER_SIZE] 处就位。
*/
int qdx_build_frame_inplace(uint8_t *buf, uint8_t msg_class,
uint16_t payload_len, uint16_t dev_id, uint16_t seq,
uint32_t timestamp, uint8_t flags);
/**
* @brief Build a fragmented payload frame (No TLV wrapper).
*/
int qdx_build_fragment_frame(uint8_t *buf, uint8_t msg_class,
const uint8_t *chunk, uint16_t chunk_len,
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
uint8_t flags);
#endif /* QDX_PROTOCOL_H */
+694
View File
@@ -0,0 +1,694 @@
/**
* @file qdx_tcp_logic.c
* @brief TCP Network Logic Implementation for MCU
*/
#include "qdx_tcp_logic.h"
#include "qdx_port.h"
#include <stddef.h>
#include <stdio.h>
#include <string.h>
/* ============================================================
* Internal State & Configuration Cache
* ============================================================ */
#define RECV_BUF_SIZE 2048
#define MAX_FRAGMENT_PAYLOAD 1400
#define HEARTBEAT_INTERVAL_MS 2000
#define SERVER_TIMEOUT_MS 6000
#define RECONNECT_DELAY_MS 3000
typedef struct {
qdx_socket_t sock;
uint32_t last_activity_ms;
uint32_t last_heartbeat_ms;
uint32_t last_reconnect_ms;
uint16_t sequence;
uint8_t is_connected;
uint8_t is_running;
const char *label;
uint8_t recv_buffer[RECV_BUF_SIZE * 2];
uint32_t recv_len;
} TcpStreamCtx_t;
static struct {
uint8_t uuid[16];
uint8_t auth_token[16];
uint16_t dev_id;
int32_t pending_new_dev_id;
uint32_t frame_count;
TcpStreamCtx_t control_stream;
TcpStreamCtx_t data_stream;
qdx_mutex_t config_mutex;
uint8_t has_valid_config;
ConfigCommon_t cached_common;
Config2D_t cached_cfg2d;
Config1D_t cached_cfg1d;
ConfigUpdateCallback_t config_cb;
DetectionResultCallback_t detect_cb;
TempFrameRequestCallback_t temp_req_cb;
} g_TcpLogic;
/* Server endpoint prototype - user would configure these, but we map to demo
* defaults */
static const char *SERVER_IP = "127.0.0.1";
static const uint16_t CONTROL_PORT = 5511;
static const uint16_t DATA_PORT = 5512;
/* ============================================================
* Internal Helpers
* ============================================================ */
static void tcp_stream_init(TcpStreamCtx_t *ctx, const char *label) {
memset(ctx, 0, sizeof(TcpStreamCtx_t));
ctx->label = label;
}
static void tcp_stream_disconnect(TcpStreamCtx_t *ctx) {
ctx->is_connected = 0;
if (ctx->sock) {
qdx_port_tcp_close(ctx->sock);
ctx->sock = NULL;
}
}
static int8_t tcp_stream_connect(TcpStreamCtx_t *ctx, const char *ip,
uint16_t port) {
ctx->sock = qdx_port_tcp_connect(ip, port);
if (ctx->sock == NULL)
return -1;
ctx->is_connected = 1;
ctx->last_activity_ms = qdx_port_get_tick_ms();
ctx->last_heartbeat_ms = ctx->last_activity_ms;
ctx->recv_len = 0;
return 0;
}
static int32_t tcp_send_frame(TcpStreamCtx_t *ctx, uint8_t msg_class,
uint8_t tlv_type, const uint8_t *payload,
uint16_t payload_len, uint8_t flags) {
if (!ctx->is_connected || ctx->sock == NULL)
return -1;
/* Max frame buffer for generic small control/heartbeat messages.
Note: Images use BuildAndSendTemperatureFrame instead of this. */
uint8_t buffer[1024];
if (HEADER_SIZE + TLV_HEADER_SIZE + payload_len + CRC_SIZE > sizeof(buffer)) {
return -1; /* Too large for generic send */
}
uint16_t seq = ctx->sequence++;
int frame_len =
qdx_build_frame(buffer, msg_class, tlv_type, payload, payload_len,
g_TcpLogic.dev_id, seq, qdx_port_get_tick_ms(), flags);
int32_t sent = qdx_port_tcp_send(ctx->sock, buffer, frame_len);
if (sent < 0) {
tcp_stream_disconnect(ctx);
return -1;
}
return sent;
}
static void tcp_send_handshake(TcpStreamCtx_t *ctx) {
uint8_t payload[54];
memset(payload, 0, sizeof(payload));
qdx_write_u16_le(payload + 0, 0x0200);
memcpy(payload + 2, g_TcpLogic.uuid, 16);
memcpy(payload + 18, g_TcpLogic.auth_token, 16);
/* Safe string copy without relying on strncpy platform behavior */
const char *hw = "V1.0";
const char *fw = "V2.0";
for (int i = 0; i < 8 && hw[i]; i++)
payload[34 + i] = hw[i];
for (int i = 0; i < 8 && fw[i]; i++)
payload[42 + i] = fw[i];
qdx_write_u32_le(payload + 50, 0x07);
tcp_send_frame(ctx, CLASS_SYSTEM, TYPE_HANDSHAKE, payload, sizeof(payload),
FLAG_ACK_REQ);
}
static void tcp_send_heartbeat(TcpStreamCtx_t *ctx) {
uint8_t payload[6];
qdx_write_u32_le(payload + 0, qdx_port_get_tick_ms());
payload[4] = 10; /* Placeholder CpuLoad */
payload[5] = 20; /* Placeholder MemUsage */
tcp_send_frame(ctx, CLASS_SYSTEM, TYPE_HEARTBEAT, payload, sizeof(payload),
0);
}
static void tcp_send_ack(TcpStreamCtx_t *ctx, uint16_t ack_seq, uint8_t status,
uint16_t error_code) {
uint8_t payload[5];
qdx_write_u16_le(payload + 0, ack_seq);
payload[2] = status;
qdx_write_u16_le(payload + 3, error_code);
tcp_send_frame(ctx, CLASS_RESPONSE, TYPE_ACK_PAYLOAD, payload,
sizeof(payload), 0);
}
/* ============================================================
* Receiving and Parsing
* ============================================================ */
static void qdx_deserialize_config_common(ConfigCommon_t *cfg,
const uint8_t *val) {
/* PipelineId: 16 字节字符数组,逐字节拷贝 */
for (int i = 0; i < 16; i++)
cfg->PipelineId[i] = (char)val[i];
cfg->PipelineType = val[16];
cfg->WorkMode = val[17];
cfg->ConfigTag = val[18];
cfg->StrictnessLevel = val[19];
cfg->IsCustomMode = val[20];
cfg->Reserved[0] = val[21];
cfg->Reserved[1] = val[22];
}
static void qdx_deserialize_config2d(Config2D_t *cfg, const uint8_t *val) {
cfg->Enabled = val[0];
cfg->IsLive = val[1];
cfg->DeviceId = qdx_read_u16_le(val + 2);
cfg->Width = qdx_read_u16_le(val + 4);
cfg->Height = qdx_read_u16_le(val + 6);
cfg->Fps = val[8];
cfg->Exposure = qdx_read_u32_le(val + 9);
cfg->AutoExposure = val[13];
cfg->MaskEnabled = val[14];
cfg->MaskThreshold = (int16_t)qdx_read_u16_le(val + 15);
cfg->MaskWidth = qdx_read_u16_le(val + 17);
cfg->MaskHeight = qdx_read_u16_le(val + 19);
cfg->Angle = (int16_t)qdx_read_u16_le(val + 21);
cfg->TargetWidth = qdx_read_u16_le(val + 23);
cfg->TargetHeight = qdx_read_u16_le(val + 25);
cfg->TriggerMode = val[27];
cfg->TriggerGpioLine = val[28];
cfg->TriggerDelayMs = qdx_read_u16_le(val + 29);
cfg->TriggerBurstCount = val[31];
cfg->TriggerInternalIntervalMs = qdx_read_u16_le(val + 32);
cfg->TriggerTemperatureThreshold = (int16_t)qdx_read_u16_le(val + 34);
cfg->TriggerDebounceIntervalMs = qdx_read_u16_le(val + 36);
cfg->TriggerCondition = val[38];
cfg->TriggerRoiX = qdx_read_u16_le(val + 39);
cfg->TriggerRoiY = qdx_read_u16_le(val + 41);
cfg->TriggerRoiW = qdx_read_u16_le(val + 43);
cfg->TriggerRoiH = qdx_read_u16_le(val + 45);
cfg->NGioDelay = qdx_read_u16_le(val + 47);
cfg->OutputGpioLine = val[49];
cfg->AlarmGpioLine = val[50];
cfg->AlarmHoldMs = qdx_read_u16_le(val + 51);
cfg->StoreNgImagesOnly = val[53];
cfg->TrainingEnabled = val[54];
cfg->TrainingSampleThreshold = qdx_read_u16_le(val + 55);
cfg->ProcessingTimeoutMs = qdx_read_u16_le(val + 57);
cfg->MaxProcessingQueueSize = val[59];
cfg->Reserved = val[60];
}
static void qdx_deserialize_config1d(Config1D_t *cfg, const uint8_t *val) {
cfg->Enabled = val[0];
cfg->RunMode = val[1];
cfg->TriggerType = val[2];
cfg->BufferSize = qdx_read_u16_le(val + 3);
cfg->TriggerTempLimit = (int16_t)qdx_read_u16_le(val + 5);
cfg->StartPointsToRemove = qdx_read_u16_le(val + 7);
cfg->ReferenceLength = qdx_read_u16_le(val + 9);
cfg->HighTimerLimit = qdx_read_u16_le(val + 11);
cfg->TimerCLimit = qdx_read_u16_le(val + 13);
cfg->NgCountLimit = val[15];
cfg->LSizeStart = qdx_read_u16_le(val + 16);
cfg->RSizeStart = qdx_read_u16_le(val + 18);
cfg->NGioDelay = qdx_read_u16_le(val + 20);
cfg->OutputGpioLine = val[22];
cfg->AlarmGpioLine = val[23];
cfg->AlarmHoldMs = qdx_read_u16_le(val + 24);
}
static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
uint16_t pkt_len) {
uint16_t hdr_seq = qdx_read_u16_le(packet + 5);
uint8_t hdr_flags = packet[15];
/* TLV Data starts after header (16 bytes) */
int offset = HEADER_SIZE;
int payload_len = pkt_len - HEADER_SIZE - CRC_SIZE;
int parsed_len = 0;
uint8_t cfg_updated = 0;
//printf("\n[DEBUG][%s] 收到 TLV 包: Seq=%d, PayloadLen=%d\n", ctx->label,
// hdr_seq, payload_len);
while (parsed_len <= payload_len - 3) {
uint8_t type = packet[offset];
uint16_t len = qdx_read_u16_le(packet + offset + 1);
//printf("[DEBUG][%s] -> 解析 TLV: Type=0x%02X, Len=%d\n", ctx->label, type,
//len);
if (parsed_len + 3 + len > payload_len) {
//printf("[DEBUG][%s] ! 结构错误: 剩余长度不足 (需 %d, 剩 %d)\n",
//ctx->label, len, payload_len - parsed_len - 3);
break; /* Malformed */
}
const uint8_t *value = packet + offset + 3;
switch (type) {
case TYPE_DEVID_ASSIGN: {
if (len >= sizeof(DevIDAssignment_t)) {
uint16_t new_id = qdx_read_u16_le(value);
g_TcpLogic.pending_new_dev_id = new_id;
tcp_send_ack(ctx, hdr_seq, 0, 0);
}
break;
}
case TYPE_CONFIG_COMMON: {
if (len >= sizeof(ConfigCommon_t)) {
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
qdx_deserialize_config_common(&g_TcpLogic.cached_common, value);
g_TcpLogic.has_valid_config = 1;
cfg_updated = 1;
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
}
break;
}
case TYPE_CONFIG_2D: {
if (len >= sizeof(Config2D_t)) {
//printf("[DEBUG][%s] * 解析 Config2D 成功\n", ctx->label);
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
qdx_deserialize_config2d(&g_TcpLogic.cached_cfg2d, value);
g_TcpLogic.has_valid_config = 1; /* 任意配置到达即标记有效 */
cfg_updated = 1;
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
} else {
//printf("[DEBUG][%s] ! Config2D 长度不对: len=%d, sizeof=%d\n",
//ctx->label, len, (int)sizeof(Config2D_t));
}
break;
}
case TYPE_CONFIG_1D: {
if (len >= sizeof(Config1D_t)) {
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
qdx_deserialize_config1d(&g_TcpLogic.cached_cfg1d, value);
g_TcpLogic.has_valid_config = 1; /* 任意配置到达即标记有效 */
cfg_updated = 1;
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
}
break;
}
case TYPE_TEMP_FRAME: {
if (g_TcpLogic.temp_req_cb) {
/* If payload length is >= 18 (TemperatureFrameHeader_t), we can peek
at Is2D. Otherwise we pass 0 or a default value. For now let's pass
an indicator if Is2D is set. */
uint8_t is2d = 0;
if (len >= 18) {
is2d = value[18]; /* index 18 in TemperatureFrameHeader_t is Is2D */
}
g_TcpLogic.temp_req_cb(is2d);
}
break;
}
case TYPE_DETECTION_RESULT: {
if (len >= sizeof(DetectionResult_t) && g_TcpLogic.detect_cb) {
uint32_t frame_num = qdx_read_u32_le(value);
uint8_t result_status = value[4];
g_TcpLogic.detect_cb(frame_num, result_status);
}
break;
}
default:
break;
}
offset += (3 + len);
parsed_len += (3 + len);
}
if (cfg_updated && g_TcpLogic.config_cb && g_TcpLogic.has_valid_config) {
/* Safely trigger callback. Passing pointers to cached config is ok
during the context of this thread, but user must copy if they
dispatch to another task. */
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
g_TcpLogic.config_cb(&g_TcpLogic.cached_common, &g_TcpLogic.cached_cfg2d,
&g_TcpLogic.cached_cfg1d);
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
}
if (hdr_flags & FLAG_ACK_REQ) {
tcp_send_ack(ctx, hdr_seq, 0, 0);
}
}
static void tcp_process_rx_buffer(TcpStreamCtx_t *ctx) {
while (ctx->recv_len >= HEADER_SIZE) {
/* 1. Search for Magic 0x55AA */
int start_idx = -1;
for (uint32_t i = 0; i <= ctx->recv_len - 2; i++) {
if (ctx->recv_buffer[i] == 0xAA && ctx->recv_buffer[i + 1] == 0x55) {
start_idx = i;
break;
}
}
if (start_idx == -1) {
ctx->recv_buffer[0] = ctx->recv_buffer[ctx->recv_len - 1];
ctx->recv_len = 1;
break;
}
if (start_idx > 0) {
/* 缓冲区内部左移,源与目标重叠,必须使用 memmove */
memmove(ctx->recv_buffer, ctx->recv_buffer + start_idx,
ctx->recv_len - start_idx);
ctx->recv_len -= start_idx;
if (ctx->recv_len < HEADER_SIZE)
break;
}
uint8_t version = ctx->recv_buffer[2];
uint16_t length = qdx_read_u16_le(ctx->recv_buffer + 3);
if (version != PROTO_VERSION || length < HEADER_SIZE + CRC_SIZE) {
/* 丢弃 Magic 标记,缓冲区内部左移 2 字节 */
//printf("\n[DEBUG][%s] 错误: Header 验证失败! Version=0x%02X "
//"(Expected=0x%02X), Length=%d\n",
//ctx->label, version, PROTO_VERSION, length);
memmove(ctx->recv_buffer, ctx->recv_buffer + 2, ctx->recv_len - 2);
ctx->recv_len -= 2;
continue;
}
if (length > sizeof(ctx->recv_buffer)) {
/* Frame too large, drop entirely */
//printf("\n[DEBUG][%s] 错误: 帧长度超限 (length=%d, max=%d)\n", ctx->label,
//length, (int)sizeof(ctx->recv_buffer));
ctx->recv_len = 0;
break;
}
if (ctx->recv_len < length) {
break; /* Need more data */
}
/* 2. Validate CRC */
uint16_t received_crc = qdx_read_u16_le(ctx->recv_buffer + length - 2);
uint16_t calculated_crc = qdx_crc16_modbus(ctx->recv_buffer, length - 2);
if (received_crc == calculated_crc) {
/* 3. Dispatch */
parse_and_dispatch_tlv(ctx, ctx->recv_buffer, length);
} else {
//printf("\n[DEBUG][%s] 错误: CRC 校验失败! Calc=0x%04X, Recv=0x%04X "
//"(Len=%d)\n",
//ctx->label, calculated_crc, received_crc, length);
}
/* 4. 移除已处理帧(缓冲区内部左移,必须 memmove) */
memmove(ctx->recv_buffer, ctx->recv_buffer + length,
ctx->recv_len - length);
ctx->recv_len -= length;
}
}
static void recv_thread_entry(void *arg) {
TcpStreamCtx_t *ctx = (TcpStreamCtx_t *)arg;
while (ctx->is_running) {
if (!ctx->is_connected) {
qdx_port_delay_ms(100);
continue;
}
/* Leave space for maximum TCP MTU read */
if (sizeof(ctx->recv_buffer) - ctx->recv_len > 0) {
int32_t bytes =
qdx_port_tcp_recv(ctx->sock, ctx->recv_buffer + ctx->recv_len,
sizeof(ctx->recv_buffer) - ctx->recv_len);
if (bytes > 0) {
ctx->recv_len += bytes;
ctx->last_activity_ms = qdx_port_get_tick_ms();
tcp_process_rx_buffer(ctx);
} else if (bytes < 0) {
/* Disconnected / Error */
tcp_stream_disconnect(ctx);
}
} else {
/* Buffer completely full but no valid packet found. Prevent overflow
* lock. */
ctx->recv_len = 0;
}
qdx_port_delay_ms(10);
}
}
/* ============================================================
* Main Background Manager
* ============================================================ */
static void manager_thread_entry(void *arg) {
while (1) {
uint32_t now = qdx_port_get_tick_ms();
/* DevID Reassignment Handling */
if (g_TcpLogic.pending_new_dev_id >= 0) {
g_TcpLogic.dev_id = (uint16_t)g_TcpLogic.pending_new_dev_id;
g_TcpLogic.pending_new_dev_id = -1;
tcp_stream_disconnect(&g_TcpLogic.control_stream);
tcp_stream_disconnect(&g_TcpLogic.data_stream);
g_TcpLogic.control_stream.last_reconnect_ms = 0;
g_TcpLogic.data_stream.last_reconnect_ms = 0;
qdx_port_delay_ms(500);
continue;
}
/* Connection Management: Control Stream */
if (!g_TcpLogic.control_stream.is_connected) {
if (now - g_TcpLogic.control_stream.last_reconnect_ms >
RECONNECT_DELAY_MS) {
if (tcp_stream_connect(&g_TcpLogic.control_stream, SERVER_IP,
CONTROL_PORT) == 0) {
tcp_send_handshake(&g_TcpLogic.control_stream);
}
g_TcpLogic.control_stream.last_reconnect_ms = qdx_port_get_tick_ms();
}
}
/* Connection Management: Data Stream */
if (!g_TcpLogic.data_stream.is_connected) {
if (now - g_TcpLogic.data_stream.last_reconnect_ms > RECONNECT_DELAY_MS) {
if (tcp_stream_connect(&g_TcpLogic.data_stream, SERVER_IP, DATA_PORT) ==
0) {
tcp_send_handshake(&g_TcpLogic.data_stream);
}
g_TcpLogic.data_stream.last_reconnect_ms = qdx_port_get_tick_ms();
}
}
/* Heartbeat & Timeout checks */
if (g_TcpLogic.control_stream.is_connected) {
if (now - g_TcpLogic.control_stream.last_heartbeat_ms >
HEARTBEAT_INTERVAL_MS) {
tcp_send_heartbeat(&g_TcpLogic.control_stream);
g_TcpLogic.control_stream.last_heartbeat_ms = now;
}
}
if (g_TcpLogic.data_stream.is_connected) {
if (now - g_TcpLogic.data_stream.last_heartbeat_ms >
HEARTBEAT_INTERVAL_MS) {
tcp_send_heartbeat(&g_TcpLogic.data_stream);
g_TcpLogic.data_stream.last_heartbeat_ms = now;
}
}
qdx_port_delay_ms(100);
}
}
/* ============================================================
* Public API Implementations
* ============================================================ */
int8_t TcpLogic_Init(const uint8_t *deviceUUID, const uint8_t *authToken) {
memset(&g_TcpLogic, 0, sizeof(g_TcpLogic));
if (deviceUUID)
memcpy(g_TcpLogic.uuid, deviceUUID, 16);
if (authToken)
memcpy(g_TcpLogic.auth_token, authToken, 16);
/* Default DevID = 101 */
g_TcpLogic.dev_id = 101;
g_TcpLogic.pending_new_dev_id = -1;
g_TcpLogic.config_mutex = qdx_port_mutex_create();
if (g_TcpLogic.config_mutex == NULL)
return -1;
tcp_stream_init(&g_TcpLogic.control_stream, "Control");
tcp_stream_init(&g_TcpLogic.data_stream, "Data");
return 0;
}
void TcpLogic_Start(void) {
g_TcpLogic.control_stream.is_running = 1;
g_TcpLogic.data_stream.is_running = 1;
qdx_port_thread_create("tcp_mgr", manager_thread_entry, NULL, 2048, 3);
qdx_port_thread_create("tcp_rx_c", recv_thread_entry,
&g_TcpLogic.control_stream, 2048, 4);
qdx_port_thread_create("tcp_rx_d", recv_thread_entry, &g_TcpLogic.data_stream,
2048, 4);
}
int8_t TcpLogic_GetLatestConfig(ConfigCommon_t *out_common,
Config2D_t *out_cfg2d, Config1D_t *out_cfg1d) {
if (!out_common || !out_cfg2d || !out_cfg1d)
return -2;
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
if (!g_TcpLogic.has_valid_config) {
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
return -1;
}
memcpy(out_common, &g_TcpLogic.cached_common, sizeof(ConfigCommon_t));
memcpy(out_cfg2d, &g_TcpLogic.cached_cfg2d, sizeof(Config2D_t));
memcpy(out_cfg1d, &g_TcpLogic.cached_cfg1d, sizeof(Config1D_t));
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
return 0;
}
void TcpLogic_RegisterConfigCallback(ConfigUpdateCallback_t cb) {
g_TcpLogic.config_cb = cb;
}
void TcpLogic_RegisterDetectionCallback(DetectionResultCallback_t cb) {
g_TcpLogic.detect_cb = cb;
}
void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb) {
g_TcpLogic.temp_req_cb = cb;
}
/* ============================================================
* Zero-Copy Frame Building & Fragmentation for Temperature Data
* ============================================================ */
int8_t
TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t *io_buffer,
const PreprocessResult_t *processMeta,
uint8_t frameType, uint8_t is2D) {
if (!g_TcpLogic.data_stream.is_connected || !io_buffer || !processMeta)
return -1;
if (io_buffer->ValidPayloadLen == 0)
return -2;
g_TcpLogic.frame_count++;
/* We need to prepend: TLV Header (3) + TemperatureFrameHeader_t (18) */
uint32_t tlv_wrapper_len = TLV_HEADER_SIZE + sizeof(TemperatureFrameHeader_t);
/* Ensure application left enough head room */
if (io_buffer->HeadOffset < HEADER_SIZE + tlv_wrapper_len) {
return -3; /* Not enough offset space allocated by user memory pool */
}
/* Start writing right before the application payload */
uint8_t *tlv_start =
io_buffer->pBuffer + io_buffer->HeadOffset - tlv_wrapper_len;
/* 1. Fill TLV Header manually via shift */
uint32_t tlv_value_len =
sizeof(TemperatureFrameHeader_t) + io_buffer->ValidPayloadLen;
tlv_start[0] = TYPE_TEMP_FRAME;
qdx_write_u16_le(tlv_start + 1, (uint16_t)tlv_value_len);
/* 2. Fill TemperatureFrameHeader manually via shift to avoid alignment
* faults
*/
uint8_t *temp_hdr = tlv_start + TLV_HEADER_SIZE;
qdx_write_u32_le(temp_hdr + 0, processMeta->FrameNumber);
qdx_write_u16_le(temp_hdr + 4, processMeta->ValidWidth);
qdx_write_u16_le(temp_hdr + 6, processMeta->ValidHeight);
qdx_write_u16_le(temp_hdr + 8, (uint16_t)processMeta->MinTemp);
qdx_write_u16_le(temp_hdr + 10, (uint16_t)processMeta->MaxTemp);
qdx_write_u16_le(temp_hdr + 12, (uint16_t)processMeta->AvgTemp);
qdx_write_u16_le(temp_hdr + 14, (uint16_t)processMeta->RoiTemp);
temp_hdr[16] = frameType;
temp_hdr[17] = processMeta->Status;
temp_hdr[18] = is2D;
temp_hdr[19] = 0; /* Reserved */
/* Total payload length is the entire TLV block */
uint32_t total_tlv_len = TLV_HEADER_SIZE + tlv_value_len;
/* 3. Handle Fragmentation if necessary */
if (total_tlv_len <= MAX_FRAGMENT_PAYLOAD) {
/* No fragmentation needed, build frame in place at the front */
uint8_t *frame_start = tlv_start - HEADER_SIZE;
uint16_t seq = g_TcpLogic.data_stream.sequence++;
int final_len = qdx_build_frame_inplace(
frame_start, CLASS_DATA, (uint16_t)total_tlv_len, g_TcpLogic.dev_id,
seq, qdx_port_get_tick_ms(), 0);
int32_t sent =
qdx_port_tcp_send(g_TcpLogic.data_stream.sock, frame_start, final_len);
return (sent >= 0) ? 0 : -1;
}
/* Fragmentation required.
Note: For zero-copy fragmentation, we send piece by piece.
We need an external buffer for each piece's frame header + CRC.
We can't easily prepend headers to later fragments inline. */
uint32_t offset = 0;
uint8_t frag_buf[HEADER_SIZE + MAX_FRAGMENT_PAYLOAD + CRC_SIZE];
uint32_t frag_count =
(total_tlv_len + MAX_FRAGMENT_PAYLOAD - 1) / MAX_FRAGMENT_PAYLOAD;
for (uint32_t i = 0; i < frag_count; i++) {
uint32_t chunk_len = total_tlv_len - offset;
if (chunk_len > MAX_FRAGMENT_PAYLOAD)
chunk_len = MAX_FRAGMENT_PAYLOAD;
uint8_t flags = (i == frag_count - 1) ? FLAG_LAST_FRAGMENT : 0;
uint16_t seq = g_TcpLogic.data_stream.sequence++;
/* We copy the chunk into frag_buf to append Header/CRC.
This involves ONE copy of the chunk, but it's small (1400 bytes at a
time), and ensures we don't need additional memory pools. */
int frame_len = qdx_build_fragment_frame(
frag_buf, CLASS_DATA, tlv_start + offset, (uint16_t)chunk_len,
g_TcpLogic.dev_id, seq, qdx_port_get_tick_ms(), flags);
int32_t sent =
qdx_port_tcp_send(g_TcpLogic.data_stream.sock, frag_buf, frame_len);
if (sent < 0) {
tcp_stream_disconnect(&g_TcpLogic.data_stream);
return -1;
}
offset += chunk_len;
}
return 0;
}
+112
View File
@@ -0,0 +1,112 @@
/**
* @file qdx_tcp_logic.h
* @brief Zero-Copy TCP Network Logic and State Machine tailored for MCU
*
* Implements connection management (Dual Stream 5511/5512),
* packet handling, heartbeating, config caching, and callbacks.
*/
#ifndef QDX_TCP_LOGIC_H
#define QDX_TCP_LOGIC_H
#include "qdx_protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize the TCP logic module
*
* @param deviceUUID Provide standard UUID for the device.
* @param authToken Optional auth token for connections.
* @return 0 on success, < 0 on failure.
*/
int8_t TcpLogic_Init(const uint8_t *deviceUUID, const uint8_t *authToken);
/**
* @brief Wait for and start non-blocking network connection attempts.
*
* It will spawn background threads for Control and Data streams
* connecting to the server.
*/
void TcpLogic_Start(void);
/**
* @brief Encapsulate and send a temperature frame without generic memory copy.
*
* Uses the pre-filled `TcpTxBuffer_t` (containing image array) to prepend
* the required nested headers directly into the buffer offset.
*
* @param io_buffer The user-allocated buffer containing Image payload starting
* at HeadOffset
* @param processMeta Real-time analysis metadata for the image data
* @param frameType Frame type: 0=LIVE, 1=TRIGGER, 2=MASKED
* @param is2D 1 for 2D matrix, 0 for 1D array
* @return 0 successfully sent to transmission queue, < 0 if failed
*/
int8_t
TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t *io_buffer,
const PreprocessResult_t *processMeta,
uint8_t frameType, uint8_t is2D);
/**
* @brief Retrieves a distinct copy of the latest active configuration
* structure.
*
* Recommended for safe reads of configuration during offline or fallback
* scenarios. Requires pointers to pre-allocated Config* structures.
*
* @param out_common Required pointer to common config structure.
* @param out_cfg2d Required pointer to 2d config structure.
* @param out_cfg1d Required pointer to 1d config structure.
* @return 0 on success, -1 if no configuration has been stored or received.
*/
int8_t TcpLogic_GetLatestConfig(ConfigCommon_t *out_common,
Config2D_t *out_cfg2d, Config1D_t *out_cfg1d);
/**
* @brief Register configuration parsing event callback.
*
* Fired immediately after the host sends a Configuration payload and it is
* safely cached to the internal registers.
*
* @param cb Callable function matching the interface
*/
void TcpLogic_RegisterConfigCallback(ConfigUpdateCallback_t cb);
/**
* @brief Register remote decision consequence callback (e.g. Reject / Ok
* processing result).
*
* Fired shortly after the host finishes processing a sent 2D or 1D target
* frame.
*
* @param cb Callable function matching the interface
*/
void TcpLogic_RegisterDetectionCallback(DetectionResultCallback_t cb);
/**
* @brief Callback for when host requests a temperature frame.
*
* Fired when the host sends a TYPE_TEMP_FRAME request (typically empty or
* carrying trigger context). The device should capture an image and reply by
* calling TcpLogic_BuildAndSendTemperatureFrame.
*
* @param is2dRequest Non-zero if request is specifically for 2D frame, zero if
* for 1D.
*/
typedef void (*TempFrameRequestCallback_t)(uint8_t is2dRequest);
/**
* @brief Register host temperature frame request callback.
*
* @param cb Callable function matching the interface
*/
void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb);
#ifdef __cplusplus
}
#endif
#endif /* QDX_TCP_LOGIC_H */