695 lines
23 KiB
C
695 lines
23 KiB
C
/**
|
||
* @file qdx_tcp_logic.c
|
||
* @brief TCP Network Logic Implementation for MCU
|
||
*/
|
||
|
||
#include "qdx_tcp_logic.h"
|
||
#include "qdx_port.h"
|
||
#include <stddef.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
/* ============================================================
|
||
* Internal State & Configuration Cache
|
||
* ============================================================ */
|
||
|
||
#define RECV_BUF_SIZE 2048
|
||
#define MAX_FRAGMENT_PAYLOAD 1400
|
||
#define HEARTBEAT_INTERVAL_MS 2000
|
||
#define SERVER_TIMEOUT_MS 6000
|
||
#define RECONNECT_DELAY_MS 3000
|
||
|
||
typedef struct {
|
||
qdx_socket_t sock;
|
||
uint32_t last_activity_ms;
|
||
uint32_t last_heartbeat_ms;
|
||
uint32_t last_reconnect_ms;
|
||
uint16_t sequence;
|
||
uint8_t is_connected;
|
||
uint8_t is_running;
|
||
const char *label;
|
||
uint8_t recv_buffer[RECV_BUF_SIZE * 2];
|
||
uint32_t recv_len;
|
||
} TcpStreamCtx_t;
|
||
|
||
static struct {
|
||
uint8_t uuid[16];
|
||
uint8_t auth_token[16];
|
||
uint16_t dev_id;
|
||
int32_t pending_new_dev_id;
|
||
uint32_t frame_count;
|
||
|
||
TcpStreamCtx_t control_stream;
|
||
TcpStreamCtx_t data_stream;
|
||
|
||
qdx_mutex_t config_mutex;
|
||
uint8_t has_valid_config;
|
||
ConfigCommon_t cached_common;
|
||
Config2D_t cached_cfg2d;
|
||
Config1D_t cached_cfg1d;
|
||
|
||
ConfigUpdateCallback_t config_cb;
|
||
DetectionResultCallback_t detect_cb;
|
||
TempFrameRequestCallback_t temp_req_cb;
|
||
} g_TcpLogic;
|
||
|
||
/* Server endpoint prototype - user would configure these, but we map to demo
|
||
* defaults */
|
||
static const char *SERVER_IP = "127.0.0.1";
|
||
static const uint16_t CONTROL_PORT = 5511;
|
||
static const uint16_t DATA_PORT = 5512;
|
||
|
||
/* ============================================================
|
||
* Internal Helpers
|
||
* ============================================================ */
|
||
|
||
static void tcp_stream_init(TcpStreamCtx_t *ctx, const char *label) {
|
||
memset(ctx, 0, sizeof(TcpStreamCtx_t));
|
||
ctx->label = label;
|
||
}
|
||
|
||
static void tcp_stream_disconnect(TcpStreamCtx_t *ctx) {
|
||
ctx->is_connected = 0;
|
||
if (ctx->sock) {
|
||
qdx_port_tcp_close(ctx->sock);
|
||
ctx->sock = NULL;
|
||
}
|
||
}
|
||
|
||
static int8_t tcp_stream_connect(TcpStreamCtx_t *ctx, const char *ip,
|
||
uint16_t port) {
|
||
ctx->sock = qdx_port_tcp_connect(ip, port);
|
||
if (ctx->sock == NULL)
|
||
return -1;
|
||
|
||
ctx->is_connected = 1;
|
||
ctx->last_activity_ms = qdx_port_get_tick_ms();
|
||
ctx->last_heartbeat_ms = ctx->last_activity_ms;
|
||
ctx->recv_len = 0;
|
||
return 0;
|
||
}
|
||
|
||
static int32_t tcp_send_frame(TcpStreamCtx_t *ctx, uint8_t msg_class,
|
||
uint8_t tlv_type, const uint8_t *payload,
|
||
uint16_t payload_len, uint8_t flags) {
|
||
if (!ctx->is_connected || ctx->sock == NULL)
|
||
return -1;
|
||
|
||
/* Max frame buffer for generic small control/heartbeat messages.
|
||
Note: Images use BuildAndSendTemperatureFrame instead of this. */
|
||
uint8_t buffer[1024];
|
||
if (HEADER_SIZE + TLV_HEADER_SIZE + payload_len + CRC_SIZE > sizeof(buffer)) {
|
||
return -1; /* Too large for generic send */
|
||
}
|
||
|
||
uint16_t seq = ctx->sequence++;
|
||
int frame_len =
|
||
qdx_build_frame(buffer, msg_class, tlv_type, payload, payload_len,
|
||
g_TcpLogic.dev_id, seq, qdx_port_get_tick_ms(), flags);
|
||
|
||
int32_t sent = qdx_port_tcp_send(ctx->sock, buffer, frame_len);
|
||
if (sent < 0) {
|
||
tcp_stream_disconnect(ctx);
|
||
return -1;
|
||
}
|
||
return sent;
|
||
}
|
||
|
||
static void tcp_send_handshake(TcpStreamCtx_t *ctx) {
|
||
uint8_t payload[54];
|
||
memset(payload, 0, sizeof(payload));
|
||
qdx_write_u16_le(payload + 0, 0x0200);
|
||
memcpy(payload + 2, g_TcpLogic.uuid, 16);
|
||
memcpy(payload + 18, g_TcpLogic.auth_token, 16);
|
||
/* Safe string copy without relying on strncpy platform behavior */
|
||
const char *hw = "V1.0";
|
||
const char *fw = "V2.0";
|
||
for (int i = 0; i < 8 && hw[i]; i++)
|
||
payload[34 + i] = hw[i];
|
||
for (int i = 0; i < 8 && fw[i]; i++)
|
||
payload[42 + i] = fw[i];
|
||
qdx_write_u32_le(payload + 50, 0x07);
|
||
|
||
tcp_send_frame(ctx, CLASS_SYSTEM, TYPE_HANDSHAKE, payload, sizeof(payload),
|
||
FLAG_ACK_REQ);
|
||
}
|
||
|
||
static void tcp_send_heartbeat(TcpStreamCtx_t *ctx) {
|
||
uint8_t payload[6];
|
||
qdx_write_u32_le(payload + 0, qdx_port_get_tick_ms());
|
||
payload[4] = 10; /* Placeholder CpuLoad */
|
||
payload[5] = 20; /* Placeholder MemUsage */
|
||
|
||
tcp_send_frame(ctx, CLASS_SYSTEM, TYPE_HEARTBEAT, payload, sizeof(payload),
|
||
0);
|
||
}
|
||
|
||
static void tcp_send_ack(TcpStreamCtx_t *ctx, uint16_t ack_seq, uint8_t status,
|
||
uint16_t error_code) {
|
||
uint8_t payload[5];
|
||
qdx_write_u16_le(payload + 0, ack_seq);
|
||
payload[2] = status;
|
||
qdx_write_u16_le(payload + 3, error_code);
|
||
|
||
tcp_send_frame(ctx, CLASS_RESPONSE, TYPE_ACK_PAYLOAD, payload,
|
||
sizeof(payload), 0);
|
||
}
|
||
|
||
/* ============================================================
|
||
* Receiving and Parsing
|
||
* ============================================================ */
|
||
|
||
static void qdx_deserialize_config_common(ConfigCommon_t *cfg,
|
||
const uint8_t *val) {
|
||
/* PipelineId: 16 字节字符数组,逐字节拷贝 */
|
||
for (int i = 0; i < 16; i++)
|
||
cfg->PipelineId[i] = (char)val[i];
|
||
cfg->PipelineType = val[16];
|
||
cfg->WorkMode = val[17];
|
||
cfg->ConfigTag = val[18];
|
||
cfg->StrictnessLevel = val[19];
|
||
cfg->IsCustomMode = val[20];
|
||
cfg->Reserved[0] = val[21];
|
||
cfg->Reserved[1] = val[22];
|
||
}
|
||
|
||
static void qdx_deserialize_config2d(Config2D_t *cfg, const uint8_t *val) {
|
||
cfg->Enabled = val[0];
|
||
cfg->IsLive = val[1];
|
||
cfg->DeviceId = qdx_read_u16_le(val + 2);
|
||
cfg->Width = qdx_read_u16_le(val + 4);
|
||
cfg->Height = qdx_read_u16_le(val + 6);
|
||
cfg->Fps = val[8];
|
||
cfg->Exposure = qdx_read_u32_le(val + 9);
|
||
cfg->AutoExposure = val[13];
|
||
cfg->MaskEnabled = val[14];
|
||
cfg->MaskThreshold = (int16_t)qdx_read_u16_le(val + 15);
|
||
cfg->MaskWidth = qdx_read_u16_le(val + 17);
|
||
cfg->MaskHeight = qdx_read_u16_le(val + 19);
|
||
cfg->Angle = (int16_t)qdx_read_u16_le(val + 21);
|
||
cfg->TargetWidth = qdx_read_u16_le(val + 23);
|
||
cfg->TargetHeight = qdx_read_u16_le(val + 25);
|
||
cfg->TriggerMode = val[27];
|
||
cfg->TriggerGpioLine = val[28];
|
||
cfg->TriggerDelayMs = qdx_read_u16_le(val + 29);
|
||
cfg->TriggerBurstCount = val[31];
|
||
cfg->TriggerInternalIntervalMs = qdx_read_u16_le(val + 32);
|
||
cfg->TriggerTemperatureThreshold = (int16_t)qdx_read_u16_le(val + 34);
|
||
cfg->TriggerDebounceIntervalMs = qdx_read_u16_le(val + 36);
|
||
cfg->TriggerCondition = val[38];
|
||
cfg->TriggerRoiX = qdx_read_u16_le(val + 39);
|
||
cfg->TriggerRoiY = qdx_read_u16_le(val + 41);
|
||
cfg->TriggerRoiW = qdx_read_u16_le(val + 43);
|
||
cfg->TriggerRoiH = qdx_read_u16_le(val + 45);
|
||
cfg->NGioDelay = qdx_read_u16_le(val + 47);
|
||
cfg->OutputGpioLine = val[49];
|
||
cfg->AlarmGpioLine = val[50];
|
||
cfg->AlarmHoldMs = qdx_read_u16_le(val + 51);
|
||
cfg->StoreNgImagesOnly = val[53];
|
||
cfg->TrainingEnabled = val[54];
|
||
cfg->TrainingSampleThreshold = qdx_read_u16_le(val + 55);
|
||
cfg->ProcessingTimeoutMs = qdx_read_u16_le(val + 57);
|
||
cfg->MaxProcessingQueueSize = val[59];
|
||
cfg->Reserved = val[60];
|
||
}
|
||
|
||
static void qdx_deserialize_config1d(Config1D_t *cfg, const uint8_t *val) {
|
||
cfg->Enabled = val[0];
|
||
cfg->RunMode = val[1];
|
||
cfg->TriggerType = val[2];
|
||
cfg->BufferSize = qdx_read_u16_le(val + 3);
|
||
cfg->TriggerTempLimit = (int16_t)qdx_read_u16_le(val + 5);
|
||
cfg->StartPointsToRemove = qdx_read_u16_le(val + 7);
|
||
cfg->ReferenceLength = qdx_read_u16_le(val + 9);
|
||
cfg->HighTimerLimit = qdx_read_u16_le(val + 11);
|
||
cfg->TimerCLimit = qdx_read_u16_le(val + 13);
|
||
cfg->NgCountLimit = val[15];
|
||
cfg->LSizeStart = qdx_read_u16_le(val + 16);
|
||
cfg->RSizeStart = qdx_read_u16_le(val + 18);
|
||
cfg->NGioDelay = qdx_read_u16_le(val + 20);
|
||
cfg->OutputGpioLine = val[22];
|
||
cfg->AlarmGpioLine = val[23];
|
||
cfg->AlarmHoldMs = qdx_read_u16_le(val + 24);
|
||
}
|
||
|
||
static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
|
||
uint16_t pkt_len) {
|
||
uint16_t hdr_seq = qdx_read_u16_le(packet + 5);
|
||
uint8_t hdr_flags = packet[15];
|
||
|
||
/* TLV Data starts after header (16 bytes) */
|
||
int offset = HEADER_SIZE;
|
||
int payload_len = pkt_len - HEADER_SIZE - CRC_SIZE;
|
||
int parsed_len = 0;
|
||
|
||
uint8_t cfg_updated = 0;
|
||
|
||
//printf("\n[DEBUG][%s] 收到 TLV 包: Seq=%d, PayloadLen=%d\n", ctx->label,
|
||
// hdr_seq, payload_len);
|
||
|
||
while (parsed_len <= payload_len - 3) {
|
||
uint8_t type = packet[offset];
|
||
uint16_t len = qdx_read_u16_le(packet + offset + 1);
|
||
|
||
//printf("[DEBUG][%s] -> 解析 TLV: Type=0x%02X, Len=%d\n", ctx->label, type,
|
||
//len);
|
||
|
||
if (parsed_len + 3 + len > payload_len) {
|
||
//printf("[DEBUG][%s] ! 结构错误: 剩余长度不足 (需 %d, 剩 %d)\n",
|
||
//ctx->label, len, payload_len - parsed_len - 3);
|
||
break; /* Malformed */
|
||
}
|
||
|
||
const uint8_t *value = packet + offset + 3;
|
||
|
||
switch (type) {
|
||
case TYPE_DEVID_ASSIGN: {
|
||
if (len >= sizeof(DevIDAssignment_t)) {
|
||
uint16_t new_id = qdx_read_u16_le(value);
|
||
g_TcpLogic.pending_new_dev_id = new_id;
|
||
tcp_send_ack(ctx, hdr_seq, 0, 0);
|
||
}
|
||
break;
|
||
}
|
||
case TYPE_CONFIG_COMMON: {
|
||
if (len >= sizeof(ConfigCommon_t)) {
|
||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||
qdx_deserialize_config_common(&g_TcpLogic.cached_common, value);
|
||
g_TcpLogic.has_valid_config = 1;
|
||
cfg_updated = 1;
|
||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||
}
|
||
break;
|
||
}
|
||
case TYPE_CONFIG_2D: {
|
||
if (len >= sizeof(Config2D_t)) {
|
||
//printf("[DEBUG][%s] * 解析 Config2D 成功\n", ctx->label);
|
||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||
qdx_deserialize_config2d(&g_TcpLogic.cached_cfg2d, value);
|
||
g_TcpLogic.has_valid_config = 1; /* 任意配置到达即标记有效 */
|
||
cfg_updated = 1;
|
||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||
} else {
|
||
//printf("[DEBUG][%s] ! Config2D 长度不对: len=%d, sizeof=%d\n",
|
||
//ctx->label, len, (int)sizeof(Config2D_t));
|
||
}
|
||
break;
|
||
}
|
||
case TYPE_CONFIG_1D: {
|
||
if (len >= sizeof(Config1D_t)) {
|
||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||
qdx_deserialize_config1d(&g_TcpLogic.cached_cfg1d, value);
|
||
g_TcpLogic.has_valid_config = 1; /* 任意配置到达即标记有效 */
|
||
cfg_updated = 1;
|
||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||
}
|
||
break;
|
||
}
|
||
case TYPE_TEMP_FRAME: {
|
||
if (g_TcpLogic.temp_req_cb) {
|
||
/* If payload length is >= 18 (TemperatureFrameHeader_t), we can peek
|
||
at Is2D. Otherwise we pass 0 or a default value. For now let's pass
|
||
an indicator if Is2D is set. */
|
||
uint8_t is2d = 0;
|
||
if (len >= 18) {
|
||
is2d = value[18]; /* index 18 in TemperatureFrameHeader_t is Is2D */
|
||
}
|
||
g_TcpLogic.temp_req_cb(is2d);
|
||
}
|
||
break;
|
||
}
|
||
case TYPE_DETECTION_RESULT: {
|
||
if (len >= sizeof(DetectionResult_t) && g_TcpLogic.detect_cb) {
|
||
uint32_t frame_num = qdx_read_u32_le(value);
|
||
uint8_t result_status = value[4];
|
||
g_TcpLogic.detect_cb(frame_num, result_status);
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
|
||
offset += (3 + len);
|
||
parsed_len += (3 + len);
|
||
}
|
||
|
||
if (cfg_updated && g_TcpLogic.config_cb && g_TcpLogic.has_valid_config) {
|
||
/* Safely trigger callback. Passing pointers to cached config is ok
|
||
during the context of this thread, but user must copy if they
|
||
dispatch to another task. */
|
||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||
g_TcpLogic.config_cb(&g_TcpLogic.cached_common, &g_TcpLogic.cached_cfg2d,
|
||
&g_TcpLogic.cached_cfg1d);
|
||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||
}
|
||
|
||
if (hdr_flags & FLAG_ACK_REQ) {
|
||
tcp_send_ack(ctx, hdr_seq, 0, 0);
|
||
}
|
||
}
|
||
|
||
static void tcp_process_rx_buffer(TcpStreamCtx_t *ctx) {
|
||
while (ctx->recv_len >= HEADER_SIZE) {
|
||
/* 1. Search for Magic 0x55AA */
|
||
int start_idx = -1;
|
||
for (uint32_t i = 0; i <= ctx->recv_len - 2; i++) {
|
||
if (ctx->recv_buffer[i] == 0xAA && ctx->recv_buffer[i + 1] == 0x55) {
|
||
start_idx = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (start_idx == -1) {
|
||
ctx->recv_buffer[0] = ctx->recv_buffer[ctx->recv_len - 1];
|
||
ctx->recv_len = 1;
|
||
break;
|
||
}
|
||
|
||
if (start_idx > 0) {
|
||
/* 缓冲区内部左移,源与目标重叠,必须使用 memmove */
|
||
memmove(ctx->recv_buffer, ctx->recv_buffer + start_idx,
|
||
ctx->recv_len - start_idx);
|
||
ctx->recv_len -= start_idx;
|
||
if (ctx->recv_len < HEADER_SIZE)
|
||
break;
|
||
}
|
||
|
||
uint8_t version = ctx->recv_buffer[2];
|
||
uint16_t length = qdx_read_u16_le(ctx->recv_buffer + 3);
|
||
|
||
if (version != PROTO_VERSION || length < HEADER_SIZE + CRC_SIZE) {
|
||
/* 丢弃 Magic 标记,缓冲区内部左移 2 字节 */
|
||
//printf("\n[DEBUG][%s] 错误: Header 验证失败! Version=0x%02X "
|
||
//"(Expected=0x%02X), Length=%d\n",
|
||
//ctx->label, version, PROTO_VERSION, length);
|
||
memmove(ctx->recv_buffer, ctx->recv_buffer + 2, ctx->recv_len - 2);
|
||
ctx->recv_len -= 2;
|
||
continue;
|
||
}
|
||
|
||
if (length > sizeof(ctx->recv_buffer)) {
|
||
/* Frame too large, drop entirely */
|
||
//printf("\n[DEBUG][%s] 错误: 帧长度超限 (length=%d, max=%d)\n", ctx->label,
|
||
//length, (int)sizeof(ctx->recv_buffer));
|
||
ctx->recv_len = 0;
|
||
break;
|
||
}
|
||
|
||
if (ctx->recv_len < length) {
|
||
break; /* Need more data */
|
||
}
|
||
|
||
/* 2. Validate CRC */
|
||
uint16_t received_crc = qdx_read_u16_le(ctx->recv_buffer + length - 2);
|
||
uint16_t calculated_crc = qdx_crc16_modbus(ctx->recv_buffer, length - 2);
|
||
|
||
if (received_crc == calculated_crc) {
|
||
/* 3. Dispatch */
|
||
parse_and_dispatch_tlv(ctx, ctx->recv_buffer, length);
|
||
} else {
|
||
//printf("\n[DEBUG][%s] 错误: CRC 校验失败! Calc=0x%04X, Recv=0x%04X "
|
||
//"(Len=%d)\n",
|
||
//ctx->label, calculated_crc, received_crc, length);
|
||
}
|
||
|
||
/* 4. 移除已处理帧(缓冲区内部左移,必须 memmove) */
|
||
memmove(ctx->recv_buffer, ctx->recv_buffer + length,
|
||
ctx->recv_len - length);
|
||
ctx->recv_len -= length;
|
||
}
|
||
}
|
||
|
||
static void recv_thread_entry(void *arg) {
|
||
TcpStreamCtx_t *ctx = (TcpStreamCtx_t *)arg;
|
||
|
||
while (ctx->is_running) {
|
||
if (!ctx->is_connected) {
|
||
qdx_port_delay_ms(100);
|
||
continue;
|
||
}
|
||
|
||
/* Leave space for maximum TCP MTU read */
|
||
if (sizeof(ctx->recv_buffer) - ctx->recv_len > 0) {
|
||
int32_t bytes =
|
||
qdx_port_tcp_recv(ctx->sock, ctx->recv_buffer + ctx->recv_len,
|
||
sizeof(ctx->recv_buffer) - ctx->recv_len);
|
||
|
||
if (bytes > 0) {
|
||
ctx->recv_len += bytes;
|
||
ctx->last_activity_ms = qdx_port_get_tick_ms();
|
||
tcp_process_rx_buffer(ctx);
|
||
} else if (bytes < 0) {
|
||
/* Disconnected / Error */
|
||
tcp_stream_disconnect(ctx);
|
||
}
|
||
} else {
|
||
/* Buffer completely full but no valid packet found. Prevent overflow
|
||
* lock. */
|
||
ctx->recv_len = 0;
|
||
}
|
||
|
||
qdx_port_delay_ms(10);
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* Main Background Manager
|
||
* ============================================================ */
|
||
|
||
static void manager_thread_entry(void *arg) {
|
||
while (1) {
|
||
uint32_t now = qdx_port_get_tick_ms();
|
||
|
||
/* DevID Reassignment Handling */
|
||
if (g_TcpLogic.pending_new_dev_id >= 0) {
|
||
g_TcpLogic.dev_id = (uint16_t)g_TcpLogic.pending_new_dev_id;
|
||
g_TcpLogic.pending_new_dev_id = -1;
|
||
|
||
tcp_stream_disconnect(&g_TcpLogic.control_stream);
|
||
tcp_stream_disconnect(&g_TcpLogic.data_stream);
|
||
|
||
g_TcpLogic.control_stream.last_reconnect_ms = 0;
|
||
g_TcpLogic.data_stream.last_reconnect_ms = 0;
|
||
qdx_port_delay_ms(500);
|
||
continue;
|
||
}
|
||
|
||
/* Connection Management: Control Stream */
|
||
if (!g_TcpLogic.control_stream.is_connected) {
|
||
if (now - g_TcpLogic.control_stream.last_reconnect_ms >
|
||
RECONNECT_DELAY_MS) {
|
||
if (tcp_stream_connect(&g_TcpLogic.control_stream, SERVER_IP,
|
||
CONTROL_PORT) == 0) {
|
||
tcp_send_handshake(&g_TcpLogic.control_stream);
|
||
}
|
||
g_TcpLogic.control_stream.last_reconnect_ms = qdx_port_get_tick_ms();
|
||
}
|
||
}
|
||
|
||
/* Connection Management: Data Stream */
|
||
if (!g_TcpLogic.data_stream.is_connected) {
|
||
if (now - g_TcpLogic.data_stream.last_reconnect_ms > RECONNECT_DELAY_MS) {
|
||
if (tcp_stream_connect(&g_TcpLogic.data_stream, SERVER_IP, DATA_PORT) ==
|
||
0) {
|
||
tcp_send_handshake(&g_TcpLogic.data_stream);
|
||
}
|
||
g_TcpLogic.data_stream.last_reconnect_ms = qdx_port_get_tick_ms();
|
||
}
|
||
}
|
||
|
||
/* Heartbeat & Timeout checks */
|
||
if (g_TcpLogic.control_stream.is_connected) {
|
||
if (now - g_TcpLogic.control_stream.last_heartbeat_ms >
|
||
HEARTBEAT_INTERVAL_MS) {
|
||
tcp_send_heartbeat(&g_TcpLogic.control_stream);
|
||
g_TcpLogic.control_stream.last_heartbeat_ms = now;
|
||
}
|
||
}
|
||
|
||
if (g_TcpLogic.data_stream.is_connected) {
|
||
if (now - g_TcpLogic.data_stream.last_heartbeat_ms >
|
||
HEARTBEAT_INTERVAL_MS) {
|
||
tcp_send_heartbeat(&g_TcpLogic.data_stream);
|
||
g_TcpLogic.data_stream.last_heartbeat_ms = now;
|
||
}
|
||
}
|
||
|
||
qdx_port_delay_ms(100);
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* Public API Implementations
|
||
* ============================================================ */
|
||
|
||
int8_t TcpLogic_Init(const uint8_t *deviceUUID, const uint8_t *authToken) {
|
||
memset(&g_TcpLogic, 0, sizeof(g_TcpLogic));
|
||
|
||
if (deviceUUID)
|
||
memcpy(g_TcpLogic.uuid, deviceUUID, 16);
|
||
if (authToken)
|
||
memcpy(g_TcpLogic.auth_token, authToken, 16);
|
||
|
||
/* Default DevID = 101 */
|
||
g_TcpLogic.dev_id = 101;
|
||
g_TcpLogic.pending_new_dev_id = -1;
|
||
|
||
g_TcpLogic.config_mutex = qdx_port_mutex_create();
|
||
if (g_TcpLogic.config_mutex == NULL)
|
||
return -1;
|
||
|
||
tcp_stream_init(&g_TcpLogic.control_stream, "Control");
|
||
tcp_stream_init(&g_TcpLogic.data_stream, "Data");
|
||
|
||
return 0;
|
||
}
|
||
|
||
void TcpLogic_Start(void) {
|
||
g_TcpLogic.control_stream.is_running = 1;
|
||
g_TcpLogic.data_stream.is_running = 1;
|
||
|
||
qdx_port_thread_create("tcp_mgr", manager_thread_entry, NULL, 2048, 3);
|
||
qdx_port_thread_create("tcp_rx_c", recv_thread_entry,
|
||
&g_TcpLogic.control_stream, 2048, 4);
|
||
qdx_port_thread_create("tcp_rx_d", recv_thread_entry, &g_TcpLogic.data_stream,
|
||
2048, 4);
|
||
}
|
||
|
||
int8_t TcpLogic_GetLatestConfig(ConfigCommon_t *out_common,
|
||
Config2D_t *out_cfg2d, Config1D_t *out_cfg1d) {
|
||
if (!out_common || !out_cfg2d || !out_cfg1d)
|
||
return -2;
|
||
|
||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||
if (!g_TcpLogic.has_valid_config) {
|
||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||
return -1;
|
||
}
|
||
|
||
memcpy(out_common, &g_TcpLogic.cached_common, sizeof(ConfigCommon_t));
|
||
memcpy(out_cfg2d, &g_TcpLogic.cached_cfg2d, sizeof(Config2D_t));
|
||
memcpy(out_cfg1d, &g_TcpLogic.cached_cfg1d, sizeof(Config1D_t));
|
||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void TcpLogic_RegisterConfigCallback(ConfigUpdateCallback_t cb) {
|
||
g_TcpLogic.config_cb = cb;
|
||
}
|
||
|
||
void TcpLogic_RegisterDetectionCallback(DetectionResultCallback_t cb) {
|
||
g_TcpLogic.detect_cb = cb;
|
||
}
|
||
|
||
void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb) {
|
||
g_TcpLogic.temp_req_cb = cb;
|
||
}
|
||
|
||
/* ============================================================
|
||
* Zero-Copy Frame Building & Fragmentation for Temperature Data
|
||
* ============================================================ */
|
||
|
||
int8_t
|
||
TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t *io_buffer,
|
||
const PreprocessResult_t *processMeta,
|
||
uint8_t frameType, uint8_t is2D) {
|
||
|
||
if (!g_TcpLogic.data_stream.is_connected || !io_buffer || !processMeta)
|
||
return -1;
|
||
if (io_buffer->ValidPayloadLen == 0)
|
||
return -2;
|
||
|
||
g_TcpLogic.frame_count++;
|
||
|
||
/* We need to prepend: TLV Header (3) + TemperatureFrameHeader_t (18) */
|
||
uint32_t tlv_wrapper_len = TLV_HEADER_SIZE + sizeof(TemperatureFrameHeader_t);
|
||
|
||
/* Ensure application left enough head room */
|
||
if (io_buffer->HeadOffset < HEADER_SIZE + tlv_wrapper_len) {
|
||
return -3; /* Not enough offset space allocated by user memory pool */
|
||
}
|
||
|
||
/* Start writing right before the application payload */
|
||
uint8_t *tlv_start =
|
||
io_buffer->pBuffer + io_buffer->HeadOffset - tlv_wrapper_len;
|
||
|
||
/* 1. Fill TLV Header manually via shift */
|
||
uint32_t tlv_value_len =
|
||
sizeof(TemperatureFrameHeader_t) + io_buffer->ValidPayloadLen;
|
||
tlv_start[0] = TYPE_TEMP_FRAME;
|
||
qdx_write_u16_le(tlv_start + 1, (uint16_t)tlv_value_len);
|
||
|
||
/* 2. Fill TemperatureFrameHeader manually via shift to avoid alignment
|
||
* faults
|
||
*/
|
||
uint8_t *temp_hdr = tlv_start + TLV_HEADER_SIZE;
|
||
qdx_write_u32_le(temp_hdr + 0, processMeta->FrameNumber);
|
||
qdx_write_u16_le(temp_hdr + 4, processMeta->ValidWidth);
|
||
qdx_write_u16_le(temp_hdr + 6, processMeta->ValidHeight);
|
||
qdx_write_u16_le(temp_hdr + 8, (uint16_t)processMeta->MinTemp);
|
||
qdx_write_u16_le(temp_hdr + 10, (uint16_t)processMeta->MaxTemp);
|
||
qdx_write_u16_le(temp_hdr + 12, (uint16_t)processMeta->AvgTemp);
|
||
qdx_write_u16_le(temp_hdr + 14, (uint16_t)processMeta->RoiTemp);
|
||
temp_hdr[16] = frameType;
|
||
temp_hdr[17] = processMeta->Status;
|
||
temp_hdr[18] = is2D;
|
||
temp_hdr[19] = 0; /* Reserved */
|
||
|
||
/* Total payload length is the entire TLV block */
|
||
uint32_t total_tlv_len = TLV_HEADER_SIZE + tlv_value_len;
|
||
|
||
/* 3. Handle Fragmentation if necessary */
|
||
if (total_tlv_len <= MAX_FRAGMENT_PAYLOAD) {
|
||
/* No fragmentation needed, build frame in place at the front */
|
||
uint8_t *frame_start = tlv_start - HEADER_SIZE;
|
||
uint16_t seq = g_TcpLogic.data_stream.sequence++;
|
||
|
||
int final_len = qdx_build_frame_inplace(
|
||
frame_start, CLASS_DATA, (uint16_t)total_tlv_len, g_TcpLogic.dev_id,
|
||
seq, qdx_port_get_tick_ms(), 0);
|
||
|
||
int32_t sent =
|
||
qdx_port_tcp_send(g_TcpLogic.data_stream.sock, frame_start, final_len);
|
||
return (sent >= 0) ? 0 : -1;
|
||
}
|
||
|
||
/* Fragmentation required.
|
||
Note: For zero-copy fragmentation, we send piece by piece.
|
||
We need an external buffer for each piece's frame header + CRC.
|
||
We can't easily prepend headers to later fragments inline. */
|
||
|
||
uint32_t offset = 0;
|
||
uint8_t frag_buf[HEADER_SIZE + MAX_FRAGMENT_PAYLOAD + CRC_SIZE];
|
||
uint32_t frag_count =
|
||
(total_tlv_len + MAX_FRAGMENT_PAYLOAD - 1) / MAX_FRAGMENT_PAYLOAD;
|
||
|
||
for (uint32_t i = 0; i < frag_count; i++) {
|
||
uint32_t chunk_len = total_tlv_len - offset;
|
||
if (chunk_len > MAX_FRAGMENT_PAYLOAD)
|
||
chunk_len = MAX_FRAGMENT_PAYLOAD;
|
||
|
||
uint8_t flags = (i == frag_count - 1) ? FLAG_LAST_FRAGMENT : 0;
|
||
uint16_t seq = g_TcpLogic.data_stream.sequence++;
|
||
|
||
/* We copy the chunk into frag_buf to append Header/CRC.
|
||
This involves ONE copy of the chunk, but it's small (1400 bytes at a
|
||
time), and ensures we don't need additional memory pools. */
|
||
int frame_len = qdx_build_fragment_frame(
|
||
frag_buf, CLASS_DATA, tlv_start + offset, (uint16_t)chunk_len,
|
||
g_TcpLogic.dev_id, seq, qdx_port_get_tick_ms(), flags);
|
||
|
||
int32_t sent =
|
||
qdx_port_tcp_send(g_TcpLogic.data_stream.sock, frag_buf, frame_len);
|
||
if (sent < 0) {
|
||
tcp_stream_disconnect(&g_TcpLogic.data_stream);
|
||
return -1;
|
||
}
|
||
|
||
offset += chunk_len;
|
||
}
|
||
|
||
return 0;
|
||
}
|