init
This commit is contained in:
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user