105
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,473 @@
|
||||
/**
|
||||
* @file qdx_port.c
|
||||
* @brief FreeRTOS + WCHNET implementation of the QDX HAL (qdx_port.h)
|
||||
*
|
||||
* Bridges the WCHNET proprietary TCP/IP stack with the blocking/thread-based
|
||||
* interface expected by qdx_tcp_logic. Uses binary semaphores and ring buffers
|
||||
* to convert WCHNET async callbacks into blocking recv semantics.
|
||||
*/
|
||||
|
||||
#include "qdx_port.h"
|
||||
#include "wchnet.h"
|
||||
#include "eth_driver.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#include "semphr.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ============================================================
|
||||
* Ring-Buffer for receive data
|
||||
* ============================================================ */
|
||||
|
||||
#define RX_RING_SIZE 2920 /* == WCHNET_TCP_MSS * 2 */
|
||||
|
||||
typedef struct {
|
||||
uint8_t buf[RX_RING_SIZE];
|
||||
uint16_t head; /* write index */
|
||||
uint16_t tail; /* read index */
|
||||
uint16_t count; /* bytes stored */
|
||||
} RxRingBuf_t;
|
||||
|
||||
static void ring_init(RxRingBuf_t *r)
|
||||
{
|
||||
r->head = 0;
|
||||
r->tail = 0;
|
||||
r->count = 0;
|
||||
}
|
||||
|
||||
static uint16_t ring_available(const RxRingBuf_t *r)
|
||||
{
|
||||
return r->count;
|
||||
}
|
||||
|
||||
static uint16_t ring_write(RxRingBuf_t *r, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
uint16_t free = RX_RING_SIZE - r->count;
|
||||
if (len > free) len = free;
|
||||
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
r->buf[r->head] = data[i];
|
||||
r->head = (r->head + 1) % RX_RING_SIZE;
|
||||
}
|
||||
r->count += len;
|
||||
return len;
|
||||
}
|
||||
|
||||
static uint16_t ring_read(RxRingBuf_t *r, uint8_t *data, uint16_t max_len)
|
||||
{
|
||||
uint16_t to_read = (r->count < max_len) ? r->count : max_len;
|
||||
|
||||
for (uint16_t i = 0; i < to_read; i++) {
|
||||
data[i] = r->buf[r->tail];
|
||||
r->tail = (r->tail + 1) % RX_RING_SIZE;
|
||||
}
|
||||
r->count -= to_read;
|
||||
return to_read;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Socket Context — maps WCHNET socket ID to qdx_socket_t
|
||||
* ============================================================ */
|
||||
|
||||
#define MAX_SOCKETS 2
|
||||
|
||||
typedef struct {
|
||||
uint8_t in_use;
|
||||
uint8_t wchnet_sock_id;
|
||||
volatile uint8_t connected;
|
||||
RxRingBuf_t rx_ring;
|
||||
SemaphoreHandle_t rx_sem; /* binary sem: data available */
|
||||
SemaphoreHandle_t connect_sem; /* binary sem: connect complete */
|
||||
} SocketCtx_t;
|
||||
|
||||
static SocketCtx_t g_sock_ctx[MAX_SOCKETS];
|
||||
|
||||
/* WCHNET per-socket receive buffers (required by WCHNET_ModifyRecvBuf) */
|
||||
extern u8 SocketRecvBuf[WCHNET_MAX_SOCKET_NUM][RECE_BUF_LEN];
|
||||
|
||||
/* WCHNET mutex for thread-safe access to WCHNET APIs */
|
||||
static SemaphoreHandle_t g_wchnet_mutex;
|
||||
|
||||
/* ============================================================
|
||||
* Internal helpers
|
||||
* ============================================================ */
|
||||
|
||||
static SocketCtx_t *alloc_sock_ctx(void)
|
||||
{
|
||||
for (int i = 0; i < MAX_SOCKETS; i++) {
|
||||
if (!g_sock_ctx[i].in_use) {
|
||||
memset(&g_sock_ctx[i], 0, sizeof(SocketCtx_t));
|
||||
g_sock_ctx[i].in_use = 1;
|
||||
ring_init(&g_sock_ctx[i].rx_ring);
|
||||
g_sock_ctx[i].rx_sem = xSemaphoreCreateBinary();
|
||||
g_sock_ctx[i].connect_sem = xSemaphoreCreateBinary();
|
||||
return &g_sock_ctx[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void free_sock_ctx(SocketCtx_t *ctx)
|
||||
{
|
||||
if (!ctx) return;
|
||||
if (ctx->rx_sem) { vSemaphoreDelete(ctx->rx_sem); ctx->rx_sem = NULL; }
|
||||
if (ctx->connect_sem) { vSemaphoreDelete(ctx->connect_sem); ctx->connect_sem = NULL; }
|
||||
ctx->in_use = 0;
|
||||
ctx->connected = 0;
|
||||
}
|
||||
|
||||
static SocketCtx_t *find_ctx_by_wchnet_id(uint8_t sockid)
|
||||
{
|
||||
for (int i = 0; i < MAX_SOCKETS; i++) {
|
||||
if (g_sock_ctx[i].in_use && g_sock_ctx[i].wchnet_sock_id == sockid)
|
||||
return &g_sock_ctx[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/** Parse dotted-decimal IP string to 4-byte array */
|
||||
static int parse_ip(const char *ip_str, uint8_t ip[4])
|
||||
{
|
||||
unsigned int a, b, c, d;
|
||||
int n = 0, idx = 0;
|
||||
const char *p = ip_str;
|
||||
|
||||
a = b = c = d = 0;
|
||||
uint32_t vals[4] = {0};
|
||||
while (*p && idx < 4) {
|
||||
if (*p >= '0' && *p <= '9') {
|
||||
vals[idx] = vals[idx] * 10 + (*p - '0');
|
||||
} else if (*p == '.') {
|
||||
idx++;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
if (idx != 3) return -1;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (vals[i] > 255) return -1;
|
||||
ip[i] = (uint8_t)vals[i];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public: called from WCHNET_HandleSockInt (task context)
|
||||
* ============================================================ */
|
||||
|
||||
void qdx_port_sock_recv_notify(uint8_t sockid)
|
||||
{
|
||||
SocketCtx_t *ctx = find_ctx_by_wchnet_id(sockid);
|
||||
if (!ctx) return;
|
||||
|
||||
/* Read data from WCHNET into ring buffer */
|
||||
uint8_t tmp[512];
|
||||
uint32_t len;
|
||||
uint32_t total = 0;
|
||||
while (1) {
|
||||
len = sizeof(tmp);
|
||||
uint8_t err = WCHNET_SocketRecv(sockid, tmp, &len);
|
||||
if (len == 0) break;
|
||||
ring_write(&ctx->rx_ring, tmp, (uint16_t)len);
|
||||
total += len;
|
||||
if (err != WCHNET_ERR_SUCCESS) break;
|
||||
}
|
||||
DBG_HB("recv_notify sock%d: %u bytes, ring=%u\r\n",
|
||||
sockid, (unsigned)total, (unsigned)ring_available(&ctx->rx_ring));
|
||||
/* Wake blocking recv thread */
|
||||
xSemaphoreGive(ctx->rx_sem);
|
||||
}
|
||||
|
||||
void qdx_port_sock_connect_notify(uint8_t sockid)
|
||||
{
|
||||
SocketCtx_t *ctx = find_ctx_by_wchnet_id(sockid);
|
||||
if (!ctx) return;
|
||||
ctx->connected = 1;
|
||||
DBG_NET("connect_notify sock%d\r\n", sockid);
|
||||
xSemaphoreGive(ctx->connect_sem);
|
||||
}
|
||||
|
||||
void qdx_port_sock_disconnect_notify(uint8_t sockid)
|
||||
{
|
||||
SocketCtx_t *ctx = find_ctx_by_wchnet_id(sockid);
|
||||
if (!ctx) return;
|
||||
ctx->connected = 0;
|
||||
DBG_NET("disconnect_notify sock%d\r\n", sockid);
|
||||
/* Wake recv thread so it can detect disconnect */
|
||||
xSemaphoreGive(ctx->rx_sem);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Init — call once before scheduler starts
|
||||
* ============================================================ */
|
||||
|
||||
void qdx_port_init(void)
|
||||
{
|
||||
memset(g_sock_ctx, 0, sizeof(g_sock_ctx));
|
||||
g_wchnet_mutex = xSemaphoreCreateMutex();
|
||||
DBG_INIT("qdx_port init done, mutex=%p\r\n", g_wchnet_mutex);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Time & Delay
|
||||
* ============================================================ */
|
||||
|
||||
uint32_t qdx_port_get_tick_ms(void)
|
||||
{
|
||||
return xTaskGetTickCount() * portTICK_PERIOD_MS;
|
||||
}
|
||||
|
||||
void qdx_port_delay_ms(uint32_t ms)
|
||||
{
|
||||
vTaskDelay(pdMS_TO_TICKS(ms));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Mutex
|
||||
* ============================================================ */
|
||||
|
||||
qdx_mutex_t qdx_port_mutex_create(void)
|
||||
{
|
||||
return (qdx_mutex_t)xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
void qdx_port_mutex_lock(qdx_mutex_t mutex)
|
||||
{
|
||||
if (mutex)
|
||||
xSemaphoreTake((SemaphoreHandle_t)mutex, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void qdx_port_mutex_unlock(qdx_mutex_t mutex)
|
||||
{
|
||||
if (mutex)
|
||||
xSemaphoreGive((SemaphoreHandle_t)mutex);
|
||||
}
|
||||
|
||||
void qdx_port_mutex_delete(qdx_mutex_t mutex)
|
||||
{
|
||||
if (mutex)
|
||||
vSemaphoreDelete((SemaphoreHandle_t)mutex);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Thread
|
||||
* ============================================================ */
|
||||
|
||||
int8_t qdx_port_thread_create(const char *name, qdx_thread_entry_t entry,
|
||||
void *arg, uint32_t stack_size, uint8_t priority)
|
||||
{
|
||||
/* stack_size from caller is in bytes; xTaskCreate expects words */
|
||||
uint32_t stack_words = stack_size / sizeof(StackType_t);
|
||||
if (stack_words < configMINIMAL_STACK_SIZE)
|
||||
stack_words = configMINIMAL_STACK_SIZE;
|
||||
|
||||
BaseType_t ret = xTaskCreate((TaskFunction_t)entry, name,
|
||||
(uint16_t)stack_words, arg,
|
||||
(UBaseType_t)priority, NULL);
|
||||
DBG_INIT("thread_create \"%s\" stack=%d pri=%d -> %s\r\n",
|
||||
name, (int)stack_words, (int)priority, (ret == pdPASS) ? "OK" : "FAIL");
|
||||
return (ret == pdPASS) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* TCP Socket — connect / send / recv / close
|
||||
* ============================================================ */
|
||||
|
||||
qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port)
|
||||
{
|
||||
uint8_t dest_ip[4];
|
||||
if (parse_ip(ip, dest_ip) != 0) {
|
||||
DBG_ERR("bad IP \"%s\"\r\n", ip);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DBG_NET("connecting to %s:%d\r\n", ip, port);
|
||||
|
||||
SocketCtx_t *ctx = alloc_sock_ctx();
|
||||
if (!ctx) {
|
||||
DBG_ERR("no free SocketCtx\r\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SOCK_INF sock_inf;
|
||||
memset(&sock_inf, 0, sizeof(sock_inf));
|
||||
sock_inf.ProtoType = PROTO_TYPE_TCP;
|
||||
sock_inf.SourPort = 0; /* let WCHNET assign ephemeral port */
|
||||
sock_inf.DesPort = port;
|
||||
memcpy(sock_inf.IPAddr, dest_ip, 4);
|
||||
|
||||
DBG_NET("SOCK_INF: proto=%d dst=%d.%d.%d.%d:%d\r\n",
|
||||
sock_inf.ProtoType,
|
||||
sock_inf.IPAddr[0], sock_inf.IPAddr[1],
|
||||
sock_inf.IPAddr[2], sock_inf.IPAddr[3],
|
||||
sock_inf.DesPort);
|
||||
|
||||
uint8_t wchnet_id = 0;
|
||||
|
||||
xSemaphoreTake(g_wchnet_mutex, portMAX_DELAY);
|
||||
uint8_t err = WCHNET_SocketCreat(&wchnet_id, &sock_inf);
|
||||
xSemaphoreGive(g_wchnet_mutex);
|
||||
|
||||
if (err != WCHNET_ERR_SUCCESS) {
|
||||
DBG_ERR("SocketCreat fail %02X\r\n", err);
|
||||
free_sock_ctx(ctx);
|
||||
return NULL;
|
||||
}
|
||||
DBG_NET("SocketCreat OK, wchnet_id=%d\r\n", wchnet_id);
|
||||
|
||||
ctx->wchnet_sock_id = wchnet_id;
|
||||
|
||||
/* Assign WCHNET receive buffer */
|
||||
WCHNET_ModifyRecvBuf(wchnet_id,
|
||||
(uint32_t)SocketRecvBuf[wchnet_id], RECE_BUF_LEN);
|
||||
|
||||
xSemaphoreTake(g_wchnet_mutex, portMAX_DELAY);
|
||||
err = WCHNET_SocketConnect(wchnet_id);
|
||||
xSemaphoreGive(g_wchnet_mutex);
|
||||
|
||||
if (err != WCHNET_ERR_SUCCESS) {
|
||||
DBG_ERR("SocketConnect fail %02X\r\n", err);
|
||||
WCHNET_SocketClose(wchnet_id, TCP_CLOSE_RST);
|
||||
free_sock_ctx(ctx);
|
||||
return NULL;
|
||||
}
|
||||
DBG_NET("SocketConnect OK (err=0x%02X), waiting connect_sem (5s)...\r\n", err);
|
||||
|
||||
/* Block until SINT_STAT_CONNECT or 5s timeout */
|
||||
uint32_t t0 = xTaskGetTickCount();
|
||||
BaseType_t sem_ret = xSemaphoreTake(ctx->connect_sem, pdMS_TO_TICKS(5000));
|
||||
uint32_t elapsed = (xTaskGetTickCount() - t0) * portTICK_PERIOD_MS;
|
||||
|
||||
if (sem_ret != pdTRUE) {
|
||||
DBG_ERR("connect_sem TIMEOUT after %d ms -> %d.%d.%d.%d:%d\r\n",
|
||||
(int)elapsed, dest_ip[0], dest_ip[1], dest_ip[2], dest_ip[3], port);
|
||||
DBG_ERR(" ctx->connected=%d wchnet_id=%d\r\n", ctx->connected, wchnet_id);
|
||||
WCHNET_SocketClose(wchnet_id, TCP_CLOSE_RST);
|
||||
free_sock_ctx(ctx);
|
||||
return NULL;
|
||||
}
|
||||
DBG_NET("connect_sem got after %d ms, connected=%d\r\n", (int)elapsed, ctx->connected);
|
||||
|
||||
if (!ctx->connected) {
|
||||
WCHNET_SocketClose(wchnet_id, TCP_CLOSE_RST);
|
||||
free_sock_ctx(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#if KEEPALIVE_ENABLE
|
||||
WCHNET_SocketSetKeepLive(wchnet_id, ENABLE);
|
||||
#endif
|
||||
|
||||
DBG_NET("connected sock %d -> %d.%d.%d.%d:%d\r\n",
|
||||
wchnet_id, dest_ip[0], dest_ip[1], dest_ip[2], dest_ip[3], port);
|
||||
return (qdx_socket_t)ctx;
|
||||
}
|
||||
|
||||
int32_t qdx_port_tcp_send(qdx_socket_t sock, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
SocketCtx_t *ctx = (SocketCtx_t *)sock;
|
||||
if (!ctx || !ctx->connected)
|
||||
return -1;
|
||||
|
||||
uint32_t total_sent = 0;
|
||||
const uint8_t *ptr = data;
|
||||
uint32_t remaining = len;
|
||||
uint8_t retries = 0;
|
||||
|
||||
while (remaining > 0) {
|
||||
uint32_t send_len = remaining;
|
||||
|
||||
xSemaphoreTake(g_wchnet_mutex, portMAX_DELAY);
|
||||
uint8_t err = WCHNET_SocketSend(ctx->wchnet_sock_id, (uint8_t *)ptr, &send_len);
|
||||
xSemaphoreGive(g_wchnet_mutex);
|
||||
|
||||
if (err != WCHNET_ERR_SUCCESS && send_len == 0) {
|
||||
/* WCHNET send buffer full — yield so wchnet task can flush */
|
||||
if (++retries > 50) {
|
||||
DBG_ERR("send fail after retries, err=0x%02X\r\n", err);
|
||||
return -1;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
continue;
|
||||
}
|
||||
retries = 0;
|
||||
ptr += send_len;
|
||||
remaining -= send_len;
|
||||
total_sent += send_len;
|
||||
|
||||
if (remaining > 0) {
|
||||
/* Partial send — yield to let wchnet task drain the buffer */
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
}
|
||||
}
|
||||
|
||||
return (int32_t)total_sent;
|
||||
}
|
||||
|
||||
int32_t qdx_port_tcp_recv(qdx_socket_t sock, uint8_t *buf, uint32_t max_len)
|
||||
{
|
||||
SocketCtx_t *ctx = (SocketCtx_t *)sock;
|
||||
if (!ctx)
|
||||
return -1;
|
||||
|
||||
/* Check ring buffer first */
|
||||
uint16_t got = ring_read(&ctx->rx_ring, buf, (uint16_t)max_len);
|
||||
if (got > 0)
|
||||
return (int32_t)got;
|
||||
|
||||
/* No data — check if disconnected */
|
||||
if (!ctx->connected)
|
||||
return -1;
|
||||
|
||||
/* Block on semaphore up to 100ms */
|
||||
xSemaphoreTake(ctx->rx_sem, pdMS_TO_TICKS(100));
|
||||
|
||||
/* Try again after wake */
|
||||
got = ring_read(&ctx->rx_ring, buf, (uint16_t)max_len);
|
||||
if (got > 0)
|
||||
return (int32_t)got;
|
||||
|
||||
/* Still no data — disconnected? */
|
||||
if (!ctx->connected)
|
||||
return -1;
|
||||
|
||||
return 0; /* timeout, no data */
|
||||
}
|
||||
|
||||
void qdx_port_tcp_close(qdx_socket_t sock)
|
||||
{
|
||||
SocketCtx_t *ctx = (SocketCtx_t *)sock;
|
||||
if (!ctx) return;
|
||||
|
||||
if (ctx->in_use) {
|
||||
xSemaphoreTake(g_wchnet_mutex, portMAX_DELAY);
|
||||
/* Use TCP_CLOSE_RST to immediately release the WCHNET socket slot.
|
||||
* TCP_CLOSE_NORMAL (0) causes TIME_WAIT which holds the slot for
|
||||
* an extended period, preventing reconnection with only 2 sockets. */
|
||||
WCHNET_SocketClose(ctx->wchnet_sock_id, TCP_CLOSE_RST);
|
||||
xSemaphoreGive(g_wchnet_mutex);
|
||||
}
|
||||
ring_init(&ctx->rx_ring);
|
||||
free_sock_ctx(ctx);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* WCHNET global lock — exposed for task_wchnet_entry in main.c
|
||||
* All WCHNET API calls must be serialized through this mutex.
|
||||
* ============================================================ */
|
||||
|
||||
void qdx_port_net_lock(void)
|
||||
{
|
||||
if (g_wchnet_mutex)
|
||||
xSemaphoreTake(g_wchnet_mutex, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void qdx_port_net_unlock(void)
|
||||
{
|
||||
if (g_wchnet_mutex)
|
||||
xSemaphoreGive(g_wchnet_mutex);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @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>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Multi-Level Debug Print System
|
||||
*
|
||||
* Verbosity levels (set DBG_LEVEL to control output volume):
|
||||
* 0 = NONE — all debug prints disabled
|
||||
* 1 = ERR — errors only
|
||||
* 2 = BRIEF — + server config / network events (recommended)
|
||||
* 3 = NORMAL — + trigger / data / init events (default)
|
||||
* 4 = VERBOSE — + heartbeat / detailed internals
|
||||
*
|
||||
* Category macros — each prints when DBG_LEVEL >= its threshold:
|
||||
* DBG_ERR (>=1) Errors, HardFault, CRC fail, bad frames
|
||||
* DBG_CFG (>=2) Server-received config (Config2D/1D/Common, DevID, ACK, DetResult)
|
||||
* DBG_NET (>=2) TCP connect/disconnect, PHY change, socket events
|
||||
* DBG_TRIG (>=3) Trigger events (ext/int), burst start/complete
|
||||
* DBG_DATA (>=3) Frame send results (2D/1D/TEMP_REQ)
|
||||
* DBG_INIT (>=3) Boot-time init messages (sensor, DVP, WCHNET)
|
||||
* DBG_HB (>=4) Heartbeat prints (high-frequency, debug only)
|
||||
* ============================================================ */
|
||||
#define DBG_LEVEL_NONE 0
|
||||
#define DBG_LEVEL_ERR 1
|
||||
#define DBG_LEVEL_BRIEF 2
|
||||
#define DBG_LEVEL_NORMAL 3
|
||||
#define DBG_LEVEL_VERBOSE 4
|
||||
|
||||
/* >>> Change this single value to control output volume <<< */
|
||||
#define DBG_LEVEL DBG_LEVEL_NORMAL
|
||||
|
||||
#define DBG_PRINT_(threshold, tag, fmt, ...) \
|
||||
do { if (DBG_LEVEL >= (threshold)) printf(tag fmt, ##__VA_ARGS__); } while(0)
|
||||
|
||||
#define DBG_ERR(fmt, ...) DBG_PRINT_(DBG_LEVEL_ERR, "[ERR] ", fmt, ##__VA_ARGS__)
|
||||
#define DBG_CFG(fmt, ...) DBG_PRINT_(DBG_LEVEL_BRIEF, "[CFG] ", fmt, ##__VA_ARGS__)
|
||||
#define DBG_NET(fmt, ...) DBG_PRINT_(DBG_LEVEL_BRIEF, "[NET] ", fmt, ##__VA_ARGS__)
|
||||
#define DBG_TRIG(fmt, ...) DBG_PRINT_(DBG_LEVEL_NORMAL, "[TRIG] ", fmt, ##__VA_ARGS__)
|
||||
#define DBG_DATA(fmt, ...) DBG_PRINT_(DBG_LEVEL_NORMAL, "[DATA] ", fmt, ##__VA_ARGS__)
|
||||
#define DBG_INIT(fmt, ...) DBG_PRINT_(DBG_LEVEL_NORMAL, "[INIT] ", fmt, ##__VA_ARGS__)
|
||||
#define DBG_HB(fmt, ...) DBG_PRINT_(DBG_LEVEL_VERBOSE, "[HB] ", fmt, ##__VA_ARGS__)
|
||||
|
||||
/* Legacy compatibility aliases — avoid using in new code */
|
||||
#define DBG_PORT(fmt, ...) DBG_INIT(fmt, ##__VA_ARGS__)
|
||||
#define DBG_LOGIC(fmt, ...) DBG_CFG(fmt, ##__VA_ARGS__)
|
||||
#define DBG_APP(fmt, ...) DBG_DATA(fmt, ##__VA_ARGS__)
|
||||
|
||||
/* ============================================================
|
||||
* 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,333 @@
|
||||
/**
|
||||
* @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 max_payload = out_buffer->TotalCapacity - out_buffer->HeadOffset;
|
||||
if ((uint32_t)tgt_w * tgt_h * 2u > max_payload) {
|
||||
uint16_t orig_w = tgt_w, orig_h = tgt_h;
|
||||
while ((uint32_t)tgt_w * tgt_h * 2u > max_payload && (tgt_w > 1 || tgt_h > 1)) {
|
||||
tgt_w = (tgt_w + 1) / 2;
|
||||
tgt_h = (tgt_h + 1) / 2;
|
||||
}
|
||||
DBG_INIT("PP: target clamped %dx%d -> %dx%d (buf=%d)\r\n",
|
||||
(int)orig_w, (int)orig_h, (int)tgt_w, (int)tgt_h, (int)max_payload);
|
||||
}
|
||||
|
||||
uint32_t required_bytes = (uint32_t)tgt_w * tgt_h * 2u;
|
||||
|
||||
/* 判断是否需要滑窗计算,或直接导出全图 */
|
||||
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,771 @@
|
||||
/**
|
||||
* @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 = "192.168.7.50";
|
||||
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) {
|
||||
DBG_NET("[%s] Disconnected\r\n", ctx->label);
|
||||
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) {
|
||||
DBG_NET("[%s] connecting %s:%d...\r\n", ctx->label, ip, port);
|
||||
ctx->sock = qdx_port_tcp_connect(ip, port);
|
||||
if (ctx->sock == NULL) {
|
||||
DBG_ERR("[%s] connect FAILED\r\n", ctx->label);
|
||||
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;
|
||||
DBG_NET("[%s] Connected to %s:%d\r\n", ctx->label, ip, port);
|
||||
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) {
|
||||
DBG_NET("[%s] Handshake (DevID=%d)\r\n", ctx->label, (int)g_TcpLogic.dev_id);
|
||||
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;
|
||||
|
||||
DBG_HB("[%s] TLV pkt: Seq=%d Len=%d\r\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);
|
||||
|
||||
DBG_HB("[%s] TLV: Type=0x%02X Len=%d\r\n", ctx->label, type, len);
|
||||
|
||||
if (parsed_len + 3 + len > payload_len) {
|
||||
DBG_ERR("[%s] TLV truncated (need %d, have %d)\r\n",
|
||||
ctx->label, len, payload_len - parsed_len - 3);
|
||||
break; /* Malformed */
|
||||
}
|
||||
|
||||
const uint8_t *value = packet + offset + 3;
|
||||
|
||||
switch (type) {
|
||||
case TYPE_HEARTBEAT: {
|
||||
/* Server heartbeat response — update activity timestamp */
|
||||
ctx->last_activity_ms = qdx_port_get_tick_ms();
|
||||
break;
|
||||
}
|
||||
case TYPE_ACK_PAYLOAD: {
|
||||
if (len >= 3) {
|
||||
uint16_t ack_seq = qdx_read_u16_le(value);
|
||||
uint8_t status = value[2];
|
||||
uint16_t err_code = (len >= 5) ? qdx_read_u16_le(value + 3) : 0;
|
||||
DBG_CFG("<< ACK: seq=%d status=%d err=0x%04x\r\n",
|
||||
(int)ack_seq, (int)status, (int)err_code);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_DEVID_ASSIGN: {
|
||||
if (len >= sizeof(DevIDAssignment_t)) {
|
||||
uint16_t new_id = qdx_read_u16_le(value);
|
||||
DBG_CFG("<< DevID assigned: %d -> %d\r\n",
|
||||
(int)g_TcpLogic.dev_id, (int)new_id);
|
||||
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);
|
||||
DBG_CFG("<< ConfigCommon: Mode=%d Tag=%d Strict=%d\r\n",
|
||||
(int)g_TcpLogic.cached_common.WorkMode,
|
||||
(int)g_TcpLogic.cached_common.ConfigTag,
|
||||
(int)g_TcpLogic.cached_common.StrictnessLevel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_CONFIG_2D: {
|
||||
if (len >= sizeof(Config2D_t)) {
|
||||
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);
|
||||
DBG_CFG("<< Config2D: En=%d %dx%d Tgt=%dx%d Fps=%d "
|
||||
"Trig=%d Burst=%d Intv=%d Thresh=%d "
|
||||
"ROI(%d,%d,%d,%d) NGio=%d\r\n",
|
||||
(int)g_TcpLogic.cached_cfg2d.Enabled,
|
||||
(int)g_TcpLogic.cached_cfg2d.Width,
|
||||
(int)g_TcpLogic.cached_cfg2d.Height,
|
||||
(int)g_TcpLogic.cached_cfg2d.TargetWidth,
|
||||
(int)g_TcpLogic.cached_cfg2d.TargetHeight,
|
||||
(int)g_TcpLogic.cached_cfg2d.Fps,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerMode,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerBurstCount,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerInternalIntervalMs,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerTemperatureThreshold,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerRoiX,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerRoiY,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerRoiW,
|
||||
(int)g_TcpLogic.cached_cfg2d.TriggerRoiH,
|
||||
(int)g_TcpLogic.cached_cfg2d.NGioDelay);
|
||||
} else {
|
||||
DBG_ERR("<< Config2D bad len=%d (need %d)\r\n",
|
||||
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);
|
||||
DBG_CFG("<< Config1D: En=%d RunMode=%d TrigType=%d "
|
||||
"BufSz=%d TempLim=%d L=%d R=%d NGio=%d\r\n",
|
||||
(int)g_TcpLogic.cached_cfg1d.Enabled,
|
||||
(int)g_TcpLogic.cached_cfg1d.RunMode,
|
||||
(int)g_TcpLogic.cached_cfg1d.TriggerType,
|
||||
(int)g_TcpLogic.cached_cfg1d.BufferSize,
|
||||
(int)g_TcpLogic.cached_cfg1d.TriggerTempLimit,
|
||||
(int)g_TcpLogic.cached_cfg1d.LSizeStart,
|
||||
(int)g_TcpLogic.cached_cfg1d.RSizeStart,
|
||||
(int)g_TcpLogic.cached_cfg1d.NGioDelay);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_TEMP_FRAME: {
|
||||
DBG_CFG("<< TempFrame request (len=%d)\r\n", (int)len);
|
||||
if (g_TcpLogic.temp_req_cb) {
|
||||
uint8_t is2d = 0;
|
||||
if (len >= 18) {
|
||||
is2d = value[18];
|
||||
}
|
||||
DBG_CFG(" -> callback: is2D=%d\r\n", (int)is2d);
|
||||
g_TcpLogic.temp_req_cb(is2d);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_DETECTION_RESULT: {
|
||||
if (len >= sizeof(DetectionResult_t)) {
|
||||
uint32_t frame_num = qdx_read_u32_le(value);
|
||||
uint8_t result_status = value[4];
|
||||
DBG_CFG("<< DetectionResult: frm=%d result=%d\r\n",
|
||||
(int)frame_num, (int)result_status);
|
||||
if (g_TcpLogic.detect_cb)
|
||||
g_TcpLogic.detect_cb(frame_num, result_status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_HANDSHAKE:
|
||||
/* Server echoes handshake info — safe to ignore */
|
||||
break;
|
||||
default:
|
||||
DBG_ERR("Unknown TLV type=0x%02x len=%d\r\n", (int)type, (int)len);
|
||||
break;
|
||||
}
|
||||
|
||||
offset += (3 + len);
|
||||
parsed_len += (3 + len);
|
||||
}
|
||||
|
||||
if (cfg_updated && g_TcpLogic.config_cb && g_TcpLogic.has_valid_config) {
|
||||
DBG_CFG("Config updated -> notify app\r\n");
|
||||
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) {
|
||||
DBG_ERR("[%s] bad header: ver=0x%02X(exp 0x%02X) len=%d\r\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)) {
|
||||
DBG_ERR("[%s] frame too large: %d > %d\r\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 {
|
||||
DBG_ERR("[%s] CRC fail: calc=0x%04X recv=0x%04X len=%d\r\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;
|
||||
}
|
||||
|
||||
void TcpLogic_InjectTestConfig(const ConfigCommon_t *common,
|
||||
const Config2D_t *cfg2d,
|
||||
const Config1D_t *cfg1d) {
|
||||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||||
if (common) memcpy(&g_TcpLogic.cached_common, common, sizeof(*common));
|
||||
if (cfg2d) memcpy(&g_TcpLogic.cached_cfg2d, cfg2d, sizeof(*cfg2d));
|
||||
if (cfg1d) memcpy(&g_TcpLogic.cached_cfg1d, cfg1d, sizeof(*cfg1d));
|
||||
g_TcpLogic.has_valid_config = 1;
|
||||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||||
if (g_TcpLogic.config_cb) {
|
||||
g_TcpLogic.config_cb(&g_TcpLogic.cached_common,
|
||||
&g_TcpLogic.cached_cfg2d,
|
||||
&g_TcpLogic.cached_cfg1d);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 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++;
|
||||
|
||||
DBG_DATA(">> TempFrame #%d: %dx%d type=%d is2D=%d payload=%d\r\n",
|
||||
(int)processMeta->FrameNumber,
|
||||
(int)processMeta->ValidWidth, (int)processMeta->ValidHeight,
|
||||
(int)frameType, (int)is2D, (int)io_buffer->ValidPayloadLen);
|
||||
|
||||
/* 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;
|
||||
static 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;
|
||||
|
||||
DBG_DATA(">> Fragmented: %d frags, total=%d\r\n",
|
||||
(int)frag_count, (int)total_tlv_len);
|
||||
|
||||
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,122 @@
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @brief Inject configuration directly (for test mode without server).
|
||||
*
|
||||
* Sets the internal config cache and fires the config callback.
|
||||
* Only non-NULL parameters are updated.
|
||||
*/
|
||||
void TcpLogic_InjectTestConfig(const ConfigCommon_t *common,
|
||||
const Config2D_t *cfg2d,
|
||||
const Config1D_t *cfg1d);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* QDX_TCP_LOGIC_H */
|
||||
Reference in New Issue
Block a user