init
This commit is contained in:
parent
2cdb65309f
commit
ea4368bf15
BIN
doc/CH32FV2x_V3xRM.PDF
Normal file
BIN
doc/CH32FV2x_V3xRM.PDF
Normal file
Binary file not shown.
25589
doc/CH32FV2x_V3xRM.md
Normal file
25589
doc/CH32FV2x_V3xRM.md
Normal file
File diff suppressed because it is too large
Load Diff
274
doc/CH32二维运行结构概览.md
Normal file
274
doc/CH32二维运行结构概览.md
Normal file
@ -0,0 +1,274 @@
|
||||
# Page 1
|
||||
|
||||
⼆维运⾏结构概览
|
||||
CH32
|
||||
版本:
|
||||
V1.1
|
||||
更新说明 :更新系统架构图
|
||||
概述
|
||||
1.
|
||||
本⽂档旨在描述运⾏于 CH32 单⽚机上的核⼼业务逻辑和软件架构。系统屏蔽了底层的硬件驱动细节,
|
||||
主要聚焦于单⽚机端的业务流转和数据处理。整个系统由采集、处理、发送三⼤核⼼模块组成,并辅以
|
||||
TCP 协议栈库和动态参数配置功能,以确保系统在⾼性能要求下稳定⾼效地运⾏。
|
||||
系统核⼼架构
|
||||
2.
|
||||
系统整体基于数据流驱动,涵盖了从图像帧获取到最终⽹络传输的完整⽣命周期。
|
||||
|
||||
# Page 2
|
||||
|
||||
外部物理
|
||||
相机接⼝
|
||||
硬件接⼝
|
||||
单⽚机端系统架构
|
||||
业务核⼼层
|
||||
DI 传感器 采集与 DMA 连续轮询
|
||||
电平跳变缓冲 满帧就绪事件
|
||||
每满帧触发的 DMA 处理
|
||||
Loop
|
||||
外部 IO 状态缓冲 触发模式配置
|
||||
有效电平指令 外部硬触发模式
|
||||
内部软触发模式
|
||||
待下⼀帧 外部触发等待模块 内部软触发判定模块
|
||||
信号跳变读取历史 单帧掩膜嗅探达标
|
||||
锁定事件帧 ⽆跳变 未达标
|
||||
交付绝对控制权
|
||||
预处理提取模块 丢弃 动态切换_触发设定
|
||||
安全热更新_触发阈值与ROI 零拷⻉有效数据负载
|
||||
打包与发送模块
|
||||
安全热更新_
|
||||
⼨
|
||||
裁剪位置与尺
|
||||
调⽤ Send API (5512)
|
||||
⽹络与底层传输层
|
||||
调整发送策略 TCP 协议栈库
|
||||
以太⽹/Wi-Fi TCP 报⽂
|
||||
触发 Recv 回调 (5511)
|
||||
参数管理与控制模块 上位机 ConfigServer
|
||||
收到 NG 检测结果
|
||||
硬件 IO 交互边界
|
||||
定时驱动模块
|
||||
发出维持延迟⾼电平
|
||||
DO ⽓缸/报警灯
|
||||
核⼼功能模块详细设计
|
||||
3.
|
||||
采集模块
|
||||
3.1
|
||||
定位:底层硬件和信号抽象。
|
||||
职责:负责与相机进⾏物理通信。对单⽚机其他业务代码⽽⾔,它是⼀个完全解耦的⿊盒。
|
||||
业务对接机制:采集模块内部⾃⾏处理相机通信、触发延时、DMA采集等。每完成⼀帧的采集,模
|
||||
块会将数据转化为统⼀的⼆维整数矩阵(Integer Matrix),存⼊单⽚机内存的环形缓冲区,并通过
|
||||
软中断、回调函数或信号量通知预处理模块接管数据。
|
||||
|
||||
# Page 3
|
||||
|
||||
内部软触发判定模块(核⼼控制块)
|
||||
3.2
|
||||
定位:轮询模式下的事件嗅探器。
|
||||
职责:在内部触发( TriggerMode = 0 )模式下,⾼频处理由底层 DMA 源源不断抛出的完整原始测
|
||||
温满帧,判断并锁定有效⽬标进⼊画⾯的“瞬间”。
|
||||
⼯作机制:
|
||||
i. ⾼速嗅探:每收到⼀帧满帧,⽴即调⽤ Preprocess_CheckInternalTrigger2D API 快速浏览预
|
||||
设的触发感兴趣区域 (Trigger ROI)。这避免了对⽆需关注的⽆⽬标区域进⾏毫⽆意义的算⼒浪
|
||||
费。
|
||||
ii. 掩膜嗅探:运⽤设定的温度阈值将低温背景瞬间剥除,并迅速计算出该⼩格 ROI 内部的最⾼温
|
||||
度或平均温度。
|
||||
iii. 定格:当发现温度满⾜判定条件时,⽴刻向主控系统反馈命中信号(返回 1),主控随即接管该
|
||||
帧缓冲区的绝对控制权(防⽌它被后续 DMA 盲⽬轮询覆盖),将其确认为事件起点帧(第 0
|
||||
帧),推⼊下⼀级的数据裁剪与提取阶段,并开启定时连拍。
|
||||
预处理与提取模块(核⼼业务块)
|
||||
3.3
|
||||
定位:数据加⼯、过滤清洗与裁剪提炼。
|
||||
职责:接收整数矩阵,并根据当前的算法参数(如过滤阈值
|
||||
TriggerTemperatureThreshold
|
||||
、⽬标
|
||||
尺⼨
|
||||
TargetWidth * TargetHeight
|
||||
)进⾏滑动滤波和坐标锁定。
|
||||
核⼼处理逻辑(温度过滤与最⼤均温滑动计算):
|
||||
i. 温度过滤预处理:识别出低于阈值的像素,在计算中视作默认低温值(如 90,即9.0°C),保
|
||||
留⾼于等于阈值的像素原始温度参与计算。
|
||||
ii. 滑动窗⼝计算:通过列累加和向右滑动窗⼝(加右侧新列减左侧旧列),快速计算⼆维矩阵内
|
||||
连续
|
||||
TargetWidth * TargetHeight
|
||||
区域的总和(即平均温度)。
|
||||
iii. 位置锁定:在预设或受控范围(如 ROI)内记录总和最⼤值的起始坐标 (X, Y) ,这即是触发
|
||||
帧的最佳裁剪位置。
|
||||
iv. 零拷⻉极速搬运:基于锁定好的坐标 (X, Y) ,从原始环形缓冲区中原样提取出这块
|
||||
TargetWidth * TargetHeight
|
||||
的未被修改过的真实测温有效像素。直接填⼊外部提供的已经偏
|
||||
移好的⽹络打包内存中。
|
||||
执⾏流程:
|
||||
|
||||
# Page 4
|
||||
|
||||
收到新帧就绪信号
|
||||
获取当前帧⼆维数组指针
|
||||
是否开启内部触发判定?
|
||||
是
|
||||
否(外部硬触发) 计算触发ROI内的最⾼/均温
|
||||
达标 未触发
|
||||
读取匹配参数: 阈值与⽬标
|
||||
丢弃退出
|
||||
尺⼨
|
||||
温度阈值过滤计算_仅⽤于
|
||||
统计寻优
|
||||
执⾏窗⼝滑动累加与最⼤均
|
||||
温计算
|
||||
|
||||
# Page 5
|
||||
|
||||
锁定最优坐标 X, Y
|
||||
根据 X, Y 裁剪提取原汁原味
|
||||
的未处理像素矩阵
|
||||
附带正确极值统计信息⽣成
|
||||
结果
|
||||
⽆缝移交⽹络 TCP 组包零拷
|
||||
⻉发送
|
||||
打包与发送模块(核⼼业务块)
|
||||
3.4 TCP
|
||||
定位:基于 TCP Raw Stream 2.0 协议的应⽤层通信封装、链路管理与调度引擎。
|
||||
主要功能总结:
|
||||
i. 双流管理与主动连接:采⽤“采集端主动连接,上位机被动监听”模式,解决 IP 不固定的问题。
|
||||
系统维护两个独⽴的 TCP ⻓连接以实现控制与数据解耦:
|
||||
控制流 (Port 5511):⽤于配置下发、指令控制与⼼跳维护。
|
||||
数据流 (Port 5512):专属⽤于⾼频温度数据(如 TemperatureFrame_t)的实时上报。
|
||||
ii. ⾝份握⼿与动态绑定:连接建⽴后发送⾸包进⾏握⼿(Handshake),携带硬件 UUID。⽀持服
|
||||
务端对逻辑设备 ID (DevID) 的冲突检测和动态重新分配,保证多设备并发下的正确识别。
|
||||
iii. 数据封装与打包:按照 2.0 ⼩端序规范,将业务数据封装为 TLV(Type-Length-Value)结构。
|
||||
⾃动在其外侧包裹核⼼帧头(包含
|
||||
Magic标识(0x55AA)
|
||||
、序列号、时间戳、载荷属性等)以及
|
||||
尾部的
|
||||
CRC16
|
||||
校验和。
|
||||
iv. 可靠性传输机制:
|
||||
重传与确认:针对控制指令和握⼿实施 ACK 确认与重传。
|
||||
⼤包分⽚ (Fragmentation):当业务载荷超过 MTU/分⽚最⼤限制时,⾃动进⾏序列分⽚
|
||||
传输,以确保⽹络利⽤率。
|
||||
v. 链路诊断与恢复:维持周期⼼跳机制(Keep-Alive)具备超时判定功能;在遭遇断⽹或对端重
|
||||
置后,可执⾏指数退避式的⾼容忍度重连,并⾃动重启握⼿绑定流程。
|
||||
执⾏流程概览:
|
||||
|
||||
# Page 6
|
||||
|
||||
应⽤层触发通信
|
||||
消息类型分类
|
||||
业务数据上报
|
||||
按 TLV 封装并定位⾄ 5512
|
||||
⼼跳/响应/状态
|
||||
数据流
|
||||
载荷是否⼤于允许的单次发 按 TLV 封装并定位⾄ 5511
|
||||
送上限? 控制流
|
||||
否
|
||||
是
|
||||
执⾏⼤包分⽚
|
||||
⽣成完整数据包
|
||||
Fragmentation
|
||||
附加 FrameHeader 与
|
||||
CRC16 校验帧
|
||||
|
||||
# Page 7
|
||||
|
||||
CRC16 校验帧
|
||||
调⽤底层 TCP 协议栈对应
|
||||
Socket 发送接⼝
|
||||
参数更改模块(控制业务块)
|
||||
3.5
|
||||
定位:系统的“神经中枢”与配置通道。
|
||||
职责:解析基于 TCP 连接传来的上位机控制指令,动态修改单⽚机的⼯作⾏为及参数(如:预处
|
||||
理阈值,⽹络打包频率等),⽽⽆需复位单⽚机。
|
||||
执⾏流程:
|
||||
i. 解析:TCP 栈触发接收回调,提取载荷中的指令码及配置数据。
|
||||
ii. 校验:检查数据的合法性。
|
||||
iii. 应⽤:使⽤原⼦操作或影⼦寄存器(Shadow Buffer)机制更新全局参数结构体,确保正在进
|
||||
⾏图像处理的流⽔线不会因为参数中途突变⽽导致指针越界或计算崩溃。
|
||||
iv. 回执:向客⼾端回传 ACK 包,告知参数修改⽣效。
|
||||
硬件 与触发反馈模块(交互边界)
|
||||
3.6 IO
|
||||
定位:处理与外部硬件的物理数字电平交互(DI/DO),连接“触发动作”与“剔除执⾏”。
|
||||
职责与⼯作机制:负责实时响应外部脉冲输⼊或程序内部事件,并能根据⽹络判定结果执⾏动作以
|
||||
控制外部设备。
|
||||
① 触发采集机制(影响⼯作流起点)
|
||||
采集流⽔线的启动⽅式严格受触发模式配置参数的影响:
|
||||
1. 外部触发 (External Trigger):
|
||||
⾏为逻辑:系统实时监听预设的 DI 触发引脚(例如:接收光电传感器的输⼊)。当检测到有效
|
||||
电平变化,并在通过设定的消抖滤波间隔 ( TriggerDebounceIntervalMs ) 确认信号稳定后,视
|
||||
为正式触发。
|
||||
影响:系统在经历特定的触发延时后,命令采集模块连续捕捉设定的张数
|
||||
( TriggerBurstCount )。只有被这批动作捕捉到的⼆维矩阵数据才会进⼊预处理与 TCP 发送队
|
||||
列。⾮触发状态下,系统处于待机且不产⽣多余计算和⽹络流量。
|
||||
2. 内部触发 (Internal Trigger / 轮询阈值触发):
|
||||
⾏为逻辑:采集模块通过底层 DMA 持续不间断地循环向单⽚机缓冲内存区抛出测温画⾯。主
|
||||
程序每收到⼀帧,都会调⽤ Preprocess_CheckInternalTrigger2D 进⾏轻量级的 ROI 温度计算
|
||||
嗅探。
|
||||
|
||||
# Page 8
|
||||
|
||||
算法双轨制设计:(核⼼重点) ⽆论是触发嗅探还是后续的截取寻优,系统都会使⽤配置的恒
|
||||
定底噪温度(如 9.0℃)掩盖低温像素。但最终打包进⽹络的数据与统计报告(最⾼/最低/平均
|
||||
温)必须是原汁原味、从未被过滤过的测温像素。这种分离设计既保证了单⽚机极速抓取⽬标
|
||||
的抗⼲扰能⼒,⼜避免了上位机 AI 模型因收到修改过的纯⾊背景图⽽发⽣误判。
|
||||
影响:⼀旦⽬标进⼊画⾯,触发算法判定达标,系统会⽴刻“锁定”该帧所在缓冲区(防⽌被
|
||||
DMA 再次覆盖),确认其为事件起点帧(第0帧),并在提取发出后稳定连拍指定的定格张数。
|
||||
内部触发 (基于轮询) ⾏为动作图:
|
||||
硬件采集/DMA 单⽚机主循环 Preprocess模块
|
||||
loop [每⼀帧采集完成]
|
||||
推送当前帧 (Raw Buffer)
|
||||
1. Preprocess_CheckInternalTrigger2D()
|
||||
在 TriggerRoi 内执⾏(掩膜过滤 + 极值判定)
|
||||
alt [判定未触发 (< Threshold)]
|
||||
返回 0 (丢弃)
|
||||
解锁缓冲让下⼀次 DMA 覆盖
|
||||
[判定触发 (>= Threshold)]
|
||||
返回 1 (锁定⽬标)
|
||||
记录本帧为 [事件第0帧], 锁定缓冲
|
||||
2. Preprocess_Execute()
|
||||
全范围滑窗寻优 + 原样零拷⻉提取
|
||||
TcpTxBuffer 组装完毕
|
||||
开启定时连拍并推送到⽹络发送队列
|
||||
硬件采集/DMA 单⽚机主循环 Preprocess模块
|
||||
|
||||
# Page 9
|
||||
|
||||
② ⾃动剔除与报警输出机制 驱动机构
|
||||
( )
|
||||
本架构⽀持全链路防错的闭环响应机制,将上位机的智能分析结果迅速转译为实际动作:
|
||||
接收判定 (触发下发):TCP 接收任务(控制流)收到来⾃主机的特定结果回调封包(如
|
||||
DetectionResult_t 宣告当前帧检测失败/缺陷,即 Result == 1 / NG)。
|
||||
执⾏输出 (DO 直接驱动):
|
||||
i. 识别到 NG,系统内部回调会⽴即响应,拉⾼预先配置的 DO 输出引脚(对应⽣产流⽔线的废
|
||||
料剔除⽓缸、拨⽚或是声光报警装置)。
|
||||
ii. 系统内部开启定时器,维持该⾼电平动作状态 NGioDelay 毫秒时⻓,这确保了较慢的机械分拨
|
||||
机构能吃满⾏程去推落次品。
|
||||
iii. NGioDelay 时间期满后,定时中断会⾃动将对应 DO 信号电平拉低复位,⼲净利落地收回⽓
|
||||
缸,复归常态以应对下⼀次测试。
|
||||
协议栈功能库(底层⽀持)
|
||||
3.7 TCP
|
||||
定位:可靠的流式⽹络传输⽀持(如 LwIP 移植)。
|
||||
职责:
|
||||
Socket 抽象:提供标准化、极简的创建、监听、连接、发送、接收 API。
|
||||
状态机维护:⾃动处理三次握⼿、四次挥⼿、重传及断线侦测操作。
|
||||
缓冲管理:管理底层的发送缓冲和接收窗⼝(TCP Window)。
|
||||
性能保证与优化策略
|
||||
4.
|
||||
为确保“采集 -> 处理 -> 发送”这⼀重负载流⽔线在 CH32 上具备⾼帧率、低延迟的性能,需采⽤以下策
|
||||
略保障:
|
||||
1. 零拷⻉ (Zero-Copy) 内存流转:
|
||||
在整个数据链路中,严禁在不同业务逻辑之间进⾏⼤块内存的
|
||||
memcpy
|
||||
复制。应当采⽤传递内存指
|
||||
针的⽅式,让不同的模块轮流获取同⼀块内存的读写权限。
|
||||
2. Ping-Pong 双缓冲设计 (Double Buffering):
|
||||
为“矩阵输出”和“预处理”分配 A/B 两块缓冲区。当采集模块(⿊盒)向 Buffer A 写⼊第 N 帧时,预处
|
||||
理模块正在读取 Buffer B 处理第 N-1 帧。两者物理隔离,实现 CPU 和外设间的全并发,⽆缝衔
|
||||
接。
|
||||
3. 事件驱动与 RTOS 并发:
|
||||
抛弃低效的死循环轮询模式。围绕 RTOS 任务构建模型:采集结束触发信号量 -> 唤醒⾼优先级处
|
||||
理任务 -> 唤醒发送任务。其余时间 CPU 处于休眠或处理低速⽹络事件。
|
||||
4. 发包防阻塞与 TCP 合并传输:
|
||||
将不重要且碎⽚的包合并发送;针对⼤数据块发送,采⽤异步队列机制或者结合 DMA 送到⽹络
|
||||
|
||||
# Page 10
|
||||
|
||||
MAC 接⼝,防⽌ CPU ⻓时间处于等待 TCP ACK 的阻塞状态。
|
||||
|
||||
BIN
doc/CH32二维运行结构概览.pdf
Normal file
BIN
doc/CH32二维运行结构概览.pdf
Normal file
Binary file not shown.
237
doc/函数调用指南.md
Normal file
237
doc/函数调用指南.md
Normal file
@ -0,0 +1,237 @@
|
||||
# Page 1
|
||||
|
||||
语⾔版本函数调⽤指南
|
||||
C
|
||||
版本V1.0
|
||||
1. 概述
|
||||
本指南旨在为 CH32 单⽚机其他业务代码提供调⽤“图像预处理模块”和“TCP 打包与发送模块”的 C 语⾔ API 说明。这两个模块被封装为独⽴的底层库函数,
|
||||
负责将原始采集数据处理并推送⾄上位机,同时接收上位机配置。
|
||||
开发者⽆需关⼼内部的滑窗算⼒优化或是 TCP 连接维持、分⽚等细节,只需按照约定的结构体提供⼊参并调⽤相关 API 即可。
|
||||
2. 核⼼数据结构
|
||||
2.1 原始图像数据结构 ( RawImageBuffer_t )
|
||||
该结构由采集模块(⿊盒)在采集完成后构建并传⼊处理库。注意:本库所有内部计算和过滤针对的均是 16位整数矩阵 (精确到 0.1℃的定点数)。
|
||||
typedef struct {
|
||||
uint16_t* pData; // 指向二维 16位 整数矩阵的起始指针(如 275 代表 27.5℃)
|
||||
uint16_t Width; // 原始图像宽度
|
||||
uint16_t Height; // 原始图像高度
|
||||
uint32_t FrameNumber; // 当前帧号(或时间戳),用于溯源
|
||||
} RawImageBuffer_t;
|
||||
2.2 预处理结果结构 ( PreprocessResult_t )
|
||||
预处理模块运算完毕后产出的有效数据载荷结构。
|
||||
typedef struct {
|
||||
uint8_t* pValidData; // 必须是 uint8_t 类型的外部缓冲区指针,规避结构体强转导致的内存对齐陷阱
|
||||
uint32_t DataLength; // 有效数据的字节长度 (宽度 * 高度 * sizeof(元素))
|
||||
uint16_t ValidWidth; // 产出图像宽度 (对于一维,可表示点数)
|
||||
uint16_t ValidHeight; // 产出图像高度
|
||||
int16_t MinTemp; // 有效区域内的最低温度
|
||||
int16_t MaxTemp; // 有效区域内的最高温度
|
||||
int16_t AvgTemp; // 有效区域内的平均温度
|
||||
int16_t RoiTemp; // 触发点温度
|
||||
uint8_t Status; // 处理状态 (0: OK, 1: 异常)
|
||||
uint32_t FrameNumber; // 继承自原始图像的帧号
|
||||
} PreprocessResult_t;
|
||||
2.3 ⽹络封装缓冲结构 ( TcpTxBuffer_t )
|
||||
专为 TCP 零拷⻉封包设计的外部分配缓冲区。应⽤层(或专⻔的内存池)提供⾜够⼤的连续内存空间。
|
||||
typedef struct {
|
||||
uint8_t* pBuffer; // 指向由应用层分配的具体内存区 (包含物理组装全空间)
|
||||
uint32_t TotalCapacity; // 该 Buffer 总容量
|
||||
uint32_t HeadOffset; // 【核心】预留给封包用的首部偏移量。载荷将从 pBuffer + HeadOffset 开始写入
|
||||
uint32_t ValidPayloadLen; // 在调用封装函数后,由网络库回填的最终报文总长度
|
||||
} TcpTxBuffer_t;
|
||||
|
||||
# Page 2
|
||||
|
||||
2.4 系统运⾏状态与配置 ( ConfigCommon_t , Config2D_t , Config1D_t )
|
||||
系统参数配置不再使⽤单⼀的句柄封装,⽽是由通信协议中定义的三个独⽴结构体分别管理: ConfigCommon_t (通⽤参数)、 Config2D_t (⼆维专有参数)
|
||||
与
|
||||
Config1D_t
|
||||
(⼀维专有参数)。TCP 模块会通过回调动态更新这三个结构体,应⽤层需要保存最新配置以供预处理等模块使⽤(详⻅通信协议规范 2.0)。
|
||||
3. 预处理模块 API
|
||||
3.1 Preprocess_Init
|
||||
功能:初始化预处理模块,分配静态计算所需的⼯作内存(如列累加数组)。
|
||||
原型:
|
||||
int8_t Preprocess_Init(uint16_t maxWidth, uint16_t maxHeight);
|
||||
⼊参:
|
||||
maxWidth / maxHeight : 系统允许的最⼤处理分辨率,⽤于预分配内存池。
|
||||
返回值: 0 成功, <0 失败。
|
||||
3.2 Preprocess_Execute
|
||||
功能:对单帧⼆维矩阵进⾏裁剪并导出。系统会基于温度过滤启动滑动窗⼝去寻找热源,锁定⽬标区域(ROI)后,会将这段区域内未被修改过的真实
|
||||
原始像素原样导出,写⼊外部提供的 Buffer 中。
|
||||
原型: int8_t Preprocess_Execute(const RawImageBuffer_t* input, TcpTxBuffer_t* out_buffer, PreprocessResult_t* output_meta);
|
||||
⼊参:
|
||||
input : 采集模块提供的原始数据句柄。
|
||||
out_buffer : 应⽤层预先分配好的待发送缓冲区。库将直接通过 out_buffer->pBuffer + out_buffer->HeadOffset 零拷⻉写⼊原始图像测温字节。
|
||||
出参:
|
||||
output_meta : 运算完成的规范化统计结果(⻓宽、最⼤、最⼩温度、平均温度等信息,统计依据同样为未失真的原始像素)。
|
||||
返回值:
|
||||
0
|
||||
表⽰处理成功并锁定 ROI,
|
||||
<0
|
||||
表⽰内存越界或其他致命错误。
|
||||
3.3 Preprocess_CheckInternalTrigger2D
|
||||
功能:根据上位机设定的“触发 ROI 区域”、“温度触发阈值”及“判定条件(最⾼温/平均温)”,对传⼊的单帧原始图像进⾏内部热源触发判定。
|
||||
原型:
|
||||
int8_t Preprocess_CheckInternalTrigger2D(const RawImageBuffer_t* input);
|
||||
⼊参:
|
||||
input : 当前需要评判的原始图像。
|
||||
返回值: 1 表⽰触发条件满⾜(画⾯中设定的 ROI 区域发现了⾜够⾼温的⽬标), 0 表⽰未触发, <0 参数错误。
|
||||
使⽤场景:在内部触发机制( TriggerMode = 0 )下,结合相机的连续 DMA 或轮询输出使⽤。
|
||||
3.4 Preprocess_Settings_Change
|
||||
功能:安全地将通过 TCP 接收到的最新业务⼯作参数更新⾄预处理库内部。⽀持影⼦机制加锁更新策略,确保不会破坏正在进⾏处理的流⽔线帧。
|
||||
原型: int8_t Preprocess_Settings_Change(const Config2D_t* newConfig2D, const Config1D_t* newConfig1D, const ConfigCommon_t* newCommon);
|
||||
⼊参:
|
||||
newConfig2D / newConfig1D : 从上位机新下发的专⽤参数结构体(如 TargetWidth , TriggerTemperatureThreshold )。
|
||||
newCommon : 从上位机新下发的通⽤配置结构体。
|
||||
返回值: 0 成功, <0 失败。
|
||||
4. TCP 打包与发送模块 API
|
||||
4.1 TcpLogic_Init
|
||||
功能:初始化整个应⽤层 TCP 管理任务,包括底层 Socket 绑定、接收任务建⽴以及缓冲池初始化。
|
||||
原型: int8_t TcpLogic_Init(const uint8_t* deviceUUID, const uint8_t* authToken);
|
||||
⼊参:
|
||||
deviceUUID : 16 字节的设备物理识别码(如 MAC 或 UID)。
|
||||
authToken : ⾝份验证令牌。
|
||||
返回值:
|
||||
0
|
||||
成功,
|
||||
<0
|
||||
失败。
|
||||
|
||||
# Page 3
|
||||
|
||||
4.2 TcpLogic_Start
|
||||
功能:⾮阻塞启动 TCP 服务管理引擎。此后库将在后台⾃动进⾏ 5511 (控制流) 与 5512 (数据流) 的连接、握⼿(Handshake)、重连和⼼跳维持。
|
||||
原型:
|
||||
void TcpLogic_Start(void);
|
||||
4.3 TcpLogic_BuildAndSendTemperatureFrame
|
||||
功能:将之前由预处理写⼊ TcpTxBuffer_t 内的数据(连同 PreprocessResult_t 结构体中的统计数据)封装。函数不需要搬移⼤块矩阵数据,直接利
|
||||
⽤移位操作和 HeadOffset 空间从前向后组装报⽂头(包含 TLV、Magic等),最后压⼊ 5512 发送缓冲。
|
||||
原
|
||||
型:
|
||||
int8_t TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t* io_buffer, const PreprocessResult_t* processMeta, uint8_t frameType, uint8_t is2D);
|
||||
⼊参:
|
||||
io_buffer : 包含已被预处理模块填充过载荷的 Buffer 包裹器。本函数执⾏完后,其中的 ValidPayloadLen 将被更新。
|
||||
processMeta : 包含帧号与温区极值统计。
|
||||
frameType : 帧类型 (0x00 LIVE, 0x01 TRIGGER, 0x02 MASKED)。
|
||||
is2D : 1 为⼆维数组, 0 为⼀维。
|
||||
返回值: 0 已组装完毕并压⼊队列, <0 失败。
|
||||
4.4 TcpLogic_GetLatestConfig
|
||||
功能:主动查询并返回 TCP 库缓存的、由上位机最近⼀次下发的完整配置参数结构体。适⽤于应⽤层需要在⾮回调上下⽂中(如初始化后⾸次同步或
|
||||
故障恢复后重新拉取)获取当前⽣效配置的场景。
|
||||
原型:
|
||||
int8_t TcpLogic_GetLatestConfig(ConfigCommon_t* out_common, Config2D_t* out_cfg2d, Config1D_t* out_cfg1d);
|
||||
出参:
|
||||
out_common : 由调⽤者提供的通⽤配置结构体指针,库将最新缓存的通⽤配置拷⻉到此处。
|
||||
out_cfg2d : 由调⽤者提供的⼆维专有配置结构体指针。
|
||||
out_cfg1d : 由调⽤者提供的⼀维专有配置结构体指针。
|
||||
返回值:
|
||||
0
|
||||
成功(配置有效),
|
||||
-1
|
||||
尚未从上位机收到过任何配置,
|
||||
<0
|
||||
其他错误。
|
||||
4.5 接收与配置更新回调注册
|
||||
TCP 库在后台线程处理收到的指令(如参数更改或控制信号)。主业务通过注册回调函数来处理这些上位机下发的事件,确保底层安全。
|
||||
// 定义回调函数类型
|
||||
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);
|
||||
// 【注意】:此回调专门用于测试上位机主动请求帧。实际业务由硬件触发或DMA循环完成,业务代码不应依赖或实现此回调。
|
||||
typedef void (*TempFrameRequestCallback_t)(uint8_t is2dRequest);
|
||||
// 注册回调 API
|
||||
void TcpLogic_RegisterConfigCallback(ConfigUpdateCallback_t cb);
|
||||
void TcpLogic_RegisterDetectionCallback(DetectionResultCallback_t cb);
|
||||
void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb);
|
||||
应⽤⽰例:参数热更新
|
||||
当注册了 ConfigUpdateCallback_t ,TCP 发⽣控制流的参数接收时,后台库完成参数解析及 CRC 校验后,触发此回调。⽤⼾可在回调中利⽤软中断或影⼦
|
||||
积存器机制将新参数赋予当前正在使⽤的
|
||||
SystemConfig_t
|
||||
。
|
||||
5. 核⼼ API 设计准则与开发规范
|
||||
为兼顾底层性能考量与可靠性⽹络封装,本库严格遵循以下原则:
|
||||
1. “传⼊指针 + ⻓度” 与预留偏移封包 (Zero-Copy Offset) ⽅案
|
||||
物理内存分配由应⽤层(依托其内存池机制)负责,传递给库的操作句柄为 out_buffer 。预处理在填充矩阵数据时,会越过 HeadOffset (这个偏移量
|
||||
等于后续 TCP 组合包所需的 Header 及 TLV 指⽰器的⼤⼩)。后续 TCP ⽹络库封装时,仅需要往前填充封包信息,⽆需对上百 KB 的矩阵数据做任何
|
||||
memcpy
|
||||
内存搬移动作。
|
||||
|
||||
# Page 4
|
||||
|
||||
2. 防范访问陷阱 (Strict uint8_t Vectoring)
|
||||
禁⽌将⽹⼝收发缓冲区中的指针强转为 uint32_t 或是具体通讯结构体使⽤。库内部所有的数据移动与地址递增处理严格使⽤ uint8_t * 处理⽹络数据
|
||||
流,从根本上阻绝了因不同编译器或单⽚机对⻬法则不⼀导致的 HardFault 或越界访问。
|
||||
3. 内置安全⼤⼩端转化 (Bitwise Disassembly/Assembly)
|
||||
**这是针对 16位 整数矩阵数据的核⼼保障。**⽆论是将采集到的定点温度( uint16_t / int16_t )拆分成⽹络字节流(封包),还是从字节流解析成单⽚
|
||||
机状态(解包),都彻底抛弃了直接强转或结构体对⻬强塞的⽅式。库内部严格规定使⽤单字节移位操作(如 val = (buf[0]) | (buf[1]<<8); ),完美
|
||||
解决“⽹络端与主机⼩端”之间的安全切换,并在提取矩阵数据时保证 2-Byte Little-Endian 输出。
|
||||
6. 底层数据收发机制与平台移植架构 (Port 层解耦)
|
||||
(预处理及TCP封包逻辑)绝对不会直接操作硬件寄存器或特定的 Socket API。
|
||||
取⽽代之的是⼀个位于 qdx_port.h 的硬件抽象层(HAL,Port层)。
|
||||
1. 发送:硬件发送缓冲区写⼊地址 ( qdx_port_tcp_send )
|
||||
当应⽤层调⽤类似 TcpLogic_BuildAndSendTemperatureFrame 时,传递的是在外部通过内存池预先分派好、并在前⾯加上了协议头的业务缓冲数组(即
|
||||
io_buffer->pBuffer )。⽹络库内部并不会“写⼊⽹卡相关的寄存器”,⽽是调⽤ qdx_port_tcp_send 。这保证了只需将封装好的完整、连续的 RAM 地址
|
||||
指针和⻓度丢给驱动层。内部的 WCH NET 或 LwIP 会从给定的这个内存地址将其发往 DMA 或以太⽹ MAC。
|
||||
2. 接收:硬件接收缓冲区 ( qdx_port_tcp_recv )
|
||||
对应地,系统会启动后台线程监听 TCP 数据流。它每次都会从 qdx_port_tcp_recv 尝试获取数据,由底层协议栈驱动将接收到的真正⽹络⽐特流存⼊
|
||||
库提供的接收缓冲中,随后库再去解析配置命令。
|
||||
7. 常规调⽤流程图 (伪代码模式)
|
||||
// 1. 初始化
|
||||
Preprocess_Init(MAX_W, MAX_H);
|
||||
TcpLogic_Init(MyUUID, MyToken);
|
||||
TcpLogic_RegisterConfigCallback(OnConfigUpdated);
|
||||
TcpLogic_RegisterDetectionCallback(OnHardwareReject);
|
||||
// 2. 启动网络引擎线程 (RTOS环境下)
|
||||
TcpLogic_Start();
|
||||
// 3. 图像中断 / DMA 轮询回调
|
||||
void OnCameraDataReady(uint16_t* matrix, uint16_t w, uint16_t h) {
|
||||
RawImageBuffer_t rawBuff = {matrix, w, h, ++frameCnt};
|
||||
// 【核心】判断这帧图像里是否有物体达到了设定的触发温度
|
||||
if (Preprocess_CheckInternalTrigger2D(&rawBuff) == 1) {
|
||||
// 发现高温目标!分配一块发送专用的零拷贝缓冲
|
||||
TcpTxBuffer_t txBuff = MemoryPool_GetTxBuffer();
|
||||
PreprocessResult_t resMeta = {0};
|
||||
// 交由库执行滑动窗口剪裁,将最核心的高温区域内原始像素直接填入缓冲的预留偏移后
|
||||
if (0 == Preprocess_Execute(&rawBuff, &txBuff, &resMeta)) {
|
||||
// TCP封包:在头部预留好的 HeadOffset 内执行无损组包并发出
|
||||
TcpLogic_BuildAndSendTemperatureFrame(&txBuff, &resMeta, 0x01, 1);
|
||||
} else {
|
||||
MemoryPool_FreeTxBuffer(&txBuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 4. 当参数更新回调触发时
|
||||
void OnConfigUpdated(const ConfigCommon_t* common, const Config2D_t* cfg2d, const Config1D_t* cfg1d) {
|
||||
// 将获得配置输入给预处理模块,利用互斥锁安全地刷新工作参数(如 TriggerRoi)
|
||||
Preprocess_Settings_Change(cfg2d, cfg1d, common);
|
||||
}
|
||||
|
||||
# Page 5
|
||||
|
||||
6. 函数调⽤时序图 (Control Flow)
|
||||
通过下⽅时序图,可清晰展⽰从设备启动,到外部通信介⼊,再到硬件持续触发采集的数据流动与函数调⽤顺序。
|
||||
|
||||
# Page 6
|
||||
|
||||
主机/中断(Main) TcpLogic (⽹络库) Preprocess (预处理库) ConfigServer (上位机)
|
||||
1. Preprocess_Init(W, H)
|
||||
2. TcpLogic_Init(UUID, Token)
|
||||
3. TcpLogic_RegisterConfigCallback()
|
||||
4. TcpLogic_Start()
|
||||
5. ⾃动连接 5511 与 5512,完成握⼿
|
||||
6. 下发⾸批动态配置指令 (Type 0x20/0x22)
|
||||
7. 触发参数回调 OnConfigUpdated()
|
||||
8. Preprocess_Settings_Change(cfg)
|
||||
循环运转:依靠中断或任务调度持续⼯作
|
||||
9. 分配待发内存 TcpTxBuffer_t(预留 Offset)
|
||||
10. 等待相机 DMA 完成⼀帧或 IO 触发
|
||||
11. Preprocess_Execute(&rawBuff, &txBuff)
|
||||
在 Offset 位置后写⼊图⽚矩阵⻓串数据
|
||||
返回处理 meta 信息及成功的 txBuff
|
||||
12. TcpLogic_BuildAndSend(txBuff)
|
||||
在头部 Offset 内利⽤移位封包 TLV/序列号
|
||||
数据送⼊ TCP 流发送 (零拷⻉达成)
|
||||
opt [获取到剔除响应]
|
||||
TCP 数据流返回 Defect Result (NG)
|
||||
触发 DetectionResultCallback
|
||||
拉⾼ DO1 定时执⾏剔除动作
|
||||
主机/中断(Main) TcpLogic (⽹络库) Preprocess (预处理库) ConfigServer (上位机)
|
||||
|
||||
BIN
doc/函数调用指南.pdf
Normal file
BIN
doc/函数调用指南.pdf
Normal file
Binary file not shown.
BIN
pc/api_demo.exe
Normal file
BIN
pc/api_demo.exe
Normal file
Binary file not shown.
42
pc/build.bat
Normal file
42
pc/build.bat
Normal file
@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
rem === C Language TCP API Demo Client Build Script ===
|
||||
rem This script uses the installed MinGW GCC compiler.
|
||||
rem It includes the new APItcpDemo files + QDXnetworkStack sources.
|
||||
|
||||
set GCC_PATH=C:\MinGW\bin\gcc.exe
|
||||
|
||||
echo Checking for GCC compiler...
|
||||
if not exist "%GCC_PATH%" (
|
||||
echo [ERROR] GCC compiler not found at: %GCC_PATH%
|
||||
echo Please ensure MinGW is properly installed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo =========================================
|
||||
echo Building APItcpDemo (QDXnetworkStack API Client)...
|
||||
echo Target: api_demo.exe
|
||||
echo =========================================
|
||||
echo.
|
||||
|
||||
rem Compiler flags: -O2 optimization, strictly enable warnings
|
||||
set CFLAGS=-O2 -Wall -I../QDXnetworkStack
|
||||
|
||||
rem Source files: Demo local sources + the QDXnetworkStack sources
|
||||
set SRC_FILES=demo_main.c qdx_port_win32.c ../QDXnetworkStack/qdx_tcp_logic.c ../QDXnetworkStack/qdx_protocol.c ../QDXnetworkStack/qdx_preprocess.c
|
||||
|
||||
"%GCC_PATH%" %CFLAGS% %SRC_FILES% -o api_demo.exe -lws2_32
|
||||
|
||||
if %ERRORLEVEL% equ 0 (
|
||||
echo.
|
||||
echo [SUCCESS] Build completed! Output file: api_demo.exe
|
||||
echo Usage example: Run 'api_demo.exe' and interact with the console.
|
||||
) else (
|
||||
echo.
|
||||
echo [FAILED] Build error. Please check the logs above.
|
||||
)
|
||||
|
||||
pause
|
||||
519
pc/demo_main.c
Normal file
519
pc/demo_main.c
Normal file
@ -0,0 +1,519 @@
|
||||
/**
|
||||
* @file demo_main.c
|
||||
* @brief TCP API 调用演示客户端 (一维/二维通用)
|
||||
*
|
||||
* 演示使用 QDXnetworkStack API 层的零拷贝发送与参数回调机制。
|
||||
* 用户操作流程与原 tcp_c_demo 保持完全一致。
|
||||
*/
|
||||
|
||||
#include "qdx_port.h"
|
||||
#include "qdx_preprocess.h"
|
||||
#include "qdx_protocol.h"
|
||||
#include "qdx_tcp_logic.h"
|
||||
|
||||
#include <conio.h> /* _kbhit, _getch */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
/* ============================================================
|
||||
* 宏定义与全局变量
|
||||
* ============================================================ */
|
||||
|
||||
/* 默认服务端连接参数(硬编码) */
|
||||
#define DEFAULT_SERVER_IP "127.0.0.1"
|
||||
#define DEFAULT_CONTROL_PORT 5511
|
||||
#define DEFAULT_DATA_PORT 5512
|
||||
|
||||
/* ANSI 控制台颜色宏定义 */
|
||||
#define CLR_RESET "\033[0m"
|
||||
#define CLR_RED "\033[31m"
|
||||
#define CLR_GREEN "\033[32m"
|
||||
#define CLR_YELLOW "\033[33m"
|
||||
#define CLR_BLUE "\033[34m"
|
||||
#define CLR_MAGENTA "\033[35m"
|
||||
#define CLR_CYAN "\033[36m"
|
||||
#define CLR_DIM "\033[90m"
|
||||
|
||||
static uint8_t g_dimension_mode = 0; /* 0=1D, 1=2D */
|
||||
static char g_matrix_dir[260] = {0};
|
||||
|
||||
/* 发送缓冲区(静态分配,避免 malloc) */
|
||||
#define MAX_2D_PIXELS (256 * 256)
|
||||
static uint16_t g_raw_matrix[MAX_2D_PIXELS];
|
||||
|
||||
/* API 要求的外部传输缓冲区,总大小 256KB,前置留出 1KB 给网络层附加头部 */
|
||||
#define TX_BUFFER_TOTAL_CAPACITY (256 * 1024)
|
||||
#define TX_BUFFER_HEAD_OFFSET 1024
|
||||
static uint8_t g_tx_buffer[TX_BUFFER_TOTAL_CAPACITY];
|
||||
static TcpTxBuffer_t g_api_tx_buffer;
|
||||
|
||||
/* ============================================================
|
||||
* 辅助函数:十六进制打印
|
||||
* ============================================================ */
|
||||
static void print_hex(const uint8_t *data, int len) {
|
||||
(void)data;
|
||||
(void)len;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 模拟数据发送逻辑 (1D & 2D) 的前置声明
|
||||
* ============================================================ */
|
||||
static void simulate_send_2d_frame(uint32_t frameNum);
|
||||
static void simulate_send_1d_frame(uint32_t frameNum);
|
||||
|
||||
/* ============================================================
|
||||
* QDX API 回调函数实现
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* @brief 上位机下发配置更新时的回调
|
||||
*/
|
||||
static void on_config_updated(const ConfigCommon_t *common,
|
||||
const Config2D_t *cfg2d,
|
||||
const Config1D_t *cfg1d) {
|
||||
printf(CLR_CYAN "\n[API Callback] 收到最新配置" CLR_RESET "\n");
|
||||
|
||||
/* 打印通用配置 */
|
||||
printf(" -> Common: Pipeline: %.*s, Type: %d, Mode: %d, Tag: %d, "
|
||||
"Strictness: %d, Custom: %d\n",
|
||||
16, common->PipelineId, common->PipelineType, common->WorkMode,
|
||||
common->ConfigTag, common->StrictnessLevel, common->IsCustomMode);
|
||||
|
||||
/* 打印 2D 配置 */
|
||||
printf(" -> 2D: Enabled: %d, Live: %d, DevId: %d, %dx%d, Fps: %d\n",
|
||||
cfg2d->Enabled, cfg2d->IsLive, cfg2d->DeviceId, cfg2d->Width,
|
||||
cfg2d->Height, cfg2d->Fps);
|
||||
printf(" -> 2D: Mask: %d (Thresh: %d, %dx%d), Target: %dx%d\n",
|
||||
cfg2d->MaskEnabled, cfg2d->MaskThreshold, cfg2d->MaskWidth,
|
||||
cfg2d->MaskHeight, cfg2d->TargetWidth, cfg2d->TargetHeight);
|
||||
|
||||
/* 打印 1D 配置 */
|
||||
printf(" -> 1D: Enabled: %d, RunMode: %d, TriggerType: %d, "
|
||||
"BufferSize: %d\n",
|
||||
cfg1d->Enabled, cfg1d->RunMode, cfg1d->TriggerType, cfg1d->BufferSize);
|
||||
|
||||
/* 核心:将配置下发给预处理算法库 */
|
||||
Preprocess_Settings_Change(cfg2d, cfg1d, common);
|
||||
printf(CLR_GREEN " -> [OK] 已将参数同步至预处理引擎" CLR_RESET "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 上位机反馈检测结果时的回调
|
||||
*/
|
||||
static void on_detection_result(uint32_t frameNumber, uint8_t resultStatus) {
|
||||
printf(CLR_CYAN "[API Callback] 收到检测结果: Frame #%u, Result: %s" CLR_RESET
|
||||
"\n",
|
||||
frameNumber, resultStatus == 0 ? "OK" : "NG");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 当服务端下发 TempFrame(请求回传当前帧)时的回调
|
||||
*/
|
||||
static void on_temp_frame_request(uint8_t is2dRequest) {
|
||||
printf(CLR_CYAN
|
||||
"[API Callback] 收到服务端 TempFrame 采集请求 (is2D=%d)" CLR_RESET
|
||||
"\n",
|
||||
is2dRequest);
|
||||
|
||||
static uint32_t simulated_frame_num = 1;
|
||||
|
||||
if (g_dimension_mode == 1) {
|
||||
printf(CLR_MAGENTA " -> 模拟: 开始处理并回传 2D 阵列图像..." CLR_RESET
|
||||
"\n");
|
||||
simulate_send_2d_frame(simulated_frame_num++);
|
||||
} else {
|
||||
printf(CLR_MAGENTA " -> 模拟: 开始处理并回传 1D 阵列数据..." CLR_RESET
|
||||
"\n");
|
||||
simulate_send_1d_frame(simulated_frame_num++);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 模拟数据发送逻辑 (1D & 2D)
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* @brief 随机选取 2D 矩阵文件并解析
|
||||
* 逻辑与原 demo 的 parse_temperature_matrix 完全一致
|
||||
*/
|
||||
static int load_random_2d_matrix(uint16_t *matrix, int max_pixels, int *out_w,
|
||||
int *out_h) {
|
||||
char pattern[300];
|
||||
snprintf(pattern, sizeof(pattern), "%s*.txt", g_matrix_dir);
|
||||
|
||||
WIN32_FIND_DATAA fd;
|
||||
HANDLE hFind = FindFirstFileA(pattern, &fd);
|
||||
if (hFind == INVALID_HANDLE_VALUE)
|
||||
return -1;
|
||||
|
||||
int count = 0;
|
||||
do {
|
||||
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
||||
count++;
|
||||
} while (FindNextFileA(hFind, &fd));
|
||||
FindClose(hFind);
|
||||
|
||||
if (count == 0)
|
||||
return -1;
|
||||
|
||||
int target = rand() % count;
|
||||
hFind = FindFirstFileA(pattern, &fd);
|
||||
int idx = 0;
|
||||
char filepath[300] = {0};
|
||||
|
||||
do {
|
||||
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
|
||||
if (idx == target) {
|
||||
snprintf(filepath, sizeof(filepath), "%s%s", g_matrix_dir,
|
||||
fd.cFileName);
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
} while (FindNextFileA(hFind, &fd));
|
||||
FindClose(hFind);
|
||||
|
||||
if (filepath[0] == '\0')
|
||||
return -1;
|
||||
|
||||
static char file_buf[512 * 1024];
|
||||
FILE *fp = fopen(filepath, "r");
|
||||
if (!fp)
|
||||
return -1;
|
||||
|
||||
int file_len = (int)fread(file_buf, 1, sizeof(file_buf) - 1, fp);
|
||||
fclose(fp);
|
||||
file_buf[file_len] = '\0';
|
||||
|
||||
char *pos = strstr(file_buf, "\"temperature\"");
|
||||
if (!pos)
|
||||
return -1;
|
||||
pos = strchr(pos, '[');
|
||||
if (!pos)
|
||||
return -1;
|
||||
pos++;
|
||||
|
||||
int width = 0, height = 0, total = 0, row_count = 0;
|
||||
|
||||
while (*pos != '\0') {
|
||||
if (*pos == '[') {
|
||||
row_count = 0;
|
||||
pos++;
|
||||
} else if (*pos == ']') {
|
||||
if (row_count > 0) {
|
||||
height++;
|
||||
if (width == 0)
|
||||
width = row_count;
|
||||
row_count = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
pos++;
|
||||
} else if (*pos >= '0' && *pos <= '9') {
|
||||
int val = 0;
|
||||
while (*pos >= '0' && *pos <= '9') {
|
||||
val = val * 10 + (*pos - '0');
|
||||
pos++;
|
||||
}
|
||||
if (*pos == '.') {
|
||||
pos++;
|
||||
while (*pos >= '0' && *pos <= '9')
|
||||
pos++;
|
||||
}
|
||||
if (total < max_pixels) {
|
||||
matrix[total] = (uint16_t)val;
|
||||
}
|
||||
total++;
|
||||
row_count++;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
*out_w = width;
|
||||
*out_h = height;
|
||||
return total > 0 ? total : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 模拟发送 2D 图像帧 (完整 API Zero-Copy 流水线)
|
||||
*/
|
||||
static void simulate_send_2d_frame(uint32_t frameNum) {
|
||||
int w = 0, h = 0;
|
||||
int total = load_random_2d_matrix(g_raw_matrix, MAX_2D_PIXELS, &w, &h);
|
||||
if (total <= 0) {
|
||||
printf(CLR_RED "[Error] 读取 2D 矩阵失败" CLR_RESET "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* 1. 构造原始图像结构体 */
|
||||
RawImageBuffer_t rawBuff = {.pData = g_raw_matrix,
|
||||
.Width = (uint16_t)w,
|
||||
.Height = (uint16_t)h,
|
||||
.FrameNumber = frameNum};
|
||||
|
||||
/* 2. 重置发送缓冲包装器(确保偏移正确) */
|
||||
g_api_tx_buffer.pBuffer = g_tx_buffer;
|
||||
g_api_tx_buffer.TotalCapacity = TX_BUFFER_TOTAL_CAPACITY;
|
||||
g_api_tx_buffer.HeadOffset = TX_BUFFER_HEAD_OFFSET;
|
||||
g_api_tx_buffer.ValidPayloadLen = 0;
|
||||
|
||||
PreprocessResult_t resMeta = {0};
|
||||
|
||||
/* 3. 核心 API: 预处理执行。它会根据滑动窗口目标大小提取 ROI,
|
||||
* 并写入我们提供的 g_tx_buffer[HeadOffset] 处。 */
|
||||
if (Preprocess_Execute(&rawBuff, &g_api_tx_buffer, &resMeta) == 0) {
|
||||
printf(CLR_MAGENTA
|
||||
"[Data] 预处理完成: 提取出 %dx%d ROI, %.1f~%.1f°C" CLR_RESET "\n",
|
||||
resMeta.ValidWidth, resMeta.ValidHeight, resMeta.MinTemp / 10.0f,
|
||||
resMeta.MaxTemp / 10.0f);
|
||||
|
||||
/* 4. 核心 API: TCP 零拷贝图文打包与发送。
|
||||
* 内部会在 HeadOffset 前面的 1KB 预留空间中拼装 18 字节包头,直接压入
|
||||
* LwIP(WinSock) */
|
||||
int8_t err = TcpLogic_BuildAndSendTemperatureFrame(
|
||||
&g_api_tx_buffer, &resMeta, 0x01 /* TRIGGER */, 1 /* IS_2D */);
|
||||
|
||||
if (err == 0) {
|
||||
printf(CLR_GREEN " -> [OK] TCP 帧已加入发送队列" CLR_RESET "\n");
|
||||
} else {
|
||||
printf(CLR_RED " -> [Fail] TCP 帧发送失败, 错误码: %d" CLR_RESET
|
||||
"\n",
|
||||
err);
|
||||
}
|
||||
} else {
|
||||
printf(CLR_RED
|
||||
"[Data] 预处理拒绝了本帧数据 (未达到阈值或参数异常)" CLR_RESET "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 模拟发送 1D 温度帧
|
||||
* 1D 模式下不需要滑动窗口图像处理,我们可以跳过 Preprocess_Execute,
|
||||
* 直接组装 `PreprocessResult_t` 结构后提供给发送库。
|
||||
*/
|
||||
static void simulate_send_1d_frame(uint32_t frameNum) {
|
||||
#define POINTS_COUNT 30
|
||||
|
||||
int16_t min_t = 32767;
|
||||
int16_t max_t = -32768;
|
||||
int32_t sum_t = 0;
|
||||
|
||||
g_api_tx_buffer.pBuffer = g_tx_buffer;
|
||||
g_api_tx_buffer.TotalCapacity = TX_BUFFER_TOTAL_CAPACITY;
|
||||
g_api_tx_buffer.HeadOffset = TX_BUFFER_HEAD_OFFSET;
|
||||
|
||||
uint8_t *dest = g_api_tx_buffer.pBuffer + g_api_tx_buffer.HeadOffset;
|
||||
|
||||
/* 模拟生成温度点 */
|
||||
for (int i = 0; i < POINTS_COUNT; i++) {
|
||||
uint16_t time_offset = (uint16_t)(i * 600 / (POINTS_COUNT - 1));
|
||||
uint16_t temp = (uint16_t)(500 + rand() % 501); /* 50.0~100.0 */
|
||||
|
||||
int16_t t = (int16_t)temp;
|
||||
if (t < min_t)
|
||||
min_t = t;
|
||||
if (t > max_t)
|
||||
max_t = t;
|
||||
sum_t += t;
|
||||
|
||||
/* 序列化到 buffer (按协议要求 TempPoint1D_t: 2字节时间偏移 +
|
||||
* 2字节温度,小端存储) */
|
||||
dest[0] = (uint8_t)(time_offset & 0xFF);
|
||||
dest[1] = (uint8_t)((time_offset >> 8) & 0xFF);
|
||||
dest[2] = (uint8_t)(temp & 0xFF);
|
||||
dest[3] = (uint8_t)((temp >> 8) & 0xFF);
|
||||
|
||||
dest += 4;
|
||||
}
|
||||
|
||||
g_api_tx_buffer.ValidPayloadLen = POINTS_COUNT * 4;
|
||||
|
||||
/* 构造预处理元数据欺骗网络库 */
|
||||
PreprocessResult_t resMeta = {.pValidData = g_api_tx_buffer.pBuffer +
|
||||
g_api_tx_buffer.HeadOffset,
|
||||
.DataLength = g_api_tx_buffer.ValidPayloadLen,
|
||||
.ValidWidth = POINTS_COUNT,
|
||||
.ValidHeight = 1,
|
||||
.MinTemp = min_t,
|
||||
.MaxTemp = max_t,
|
||||
.AvgTemp = (int16_t)(sum_t / POINTS_COUNT),
|
||||
.RoiTemp = (int16_t)(sum_t / POINTS_COUNT),
|
||||
.Status = 0,
|
||||
.FrameNumber = frameNum};
|
||||
|
||||
printf(CLR_MAGENTA "[Data] 构造 1D 数据完成: %d 点, %.1f~%.1f°C" CLR_RESET
|
||||
"\n",
|
||||
POINTS_COUNT, min_t / 10.0f, max_t / 10.0f);
|
||||
|
||||
int8_t err = TcpLogic_BuildAndSendTemperatureFrame(
|
||||
&g_api_tx_buffer, &resMeta, 0x01 /* TRIGGER */, 0 /* IS_1D */);
|
||||
|
||||
if (err == 0) {
|
||||
printf(CLR_GREEN " -> [OK] 1D 帧已加入发送队列" CLR_RESET "\n");
|
||||
} else {
|
||||
printf(CLR_RED " -> [Fail] 网络发送失败, 错误码: %d" CLR_RESET "\n",
|
||||
err);
|
||||
}
|
||||
|
||||
#undef POINTS_COUNT
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 主函数 (控制台 UI 交互层)
|
||||
* ============================================================ */
|
||||
|
||||
int main(void) {
|
||||
/* 设置控制台支持 UTF-8 和 ANSI 转义 */
|
||||
SetConsoleOutputCP(65001);
|
||||
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
|
||||
#endif
|
||||
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
DWORD dwMode = 0;
|
||||
GetConsoleMode(hOut, &dwMode);
|
||||
SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
|
||||
printf("========================================================\n");
|
||||
printf("= TCP 透传客户端 API 演示版 (基于 QDXnetworkStack.c)\n");
|
||||
printf("= 服务端: %s 控制: %d 数据: %d\n", DEFAULT_SERVER_IP,
|
||||
DEFAULT_CONTROL_PORT, DEFAULT_DATA_PORT);
|
||||
printf("========================================================\n\n");
|
||||
|
||||
/* Windows 下需要手动启动 Winsock */
|
||||
WSADATA wsaData;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
|
||||
printf("[ERROR] WSAStartup 失败。\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 交互式输入设备 ID */
|
||||
int input_id = 0;
|
||||
printf("请输入设备 ID (整数, 例如 101): ");
|
||||
if (scanf("%d", &input_id) != 1 || input_id < 0 || input_id > 65535) {
|
||||
printf("[ERROR] 无效。默认设为 101。\n");
|
||||
input_id = 101;
|
||||
}
|
||||
printf("\n设备 ID 设置为: %d\n\n", input_id);
|
||||
|
||||
/* 清除遗留的回车换行 */
|
||||
int c;
|
||||
while ((c = getchar()) != '\n' && c != EOF) {
|
||||
}
|
||||
|
||||
/* 选择维度 */
|
||||
int dim_choice = 0;
|
||||
printf("请选择维度模式:\n");
|
||||
printf(" 1 - 一维模式 (随机温度数据)\n");
|
||||
printf(" 2 - 二维模式 (从目录随机读取矩阵文件)\n");
|
||||
printf("请输入 (1 或 2): ");
|
||||
if (scanf("%d", &dim_choice) != 1 || (dim_choice != 1 && dim_choice != 2)) {
|
||||
dim_choice = 1;
|
||||
}
|
||||
g_dimension_mode = (dim_choice == 2) ? 1 : 0;
|
||||
printf("\n维度模式: %s\n\n", g_dimension_mode ? "二维 (2D)" : "一维 (1D)");
|
||||
|
||||
if (g_dimension_mode == 1) {
|
||||
snprintf(g_matrix_dir, sizeof(g_matrix_dir), "../tcp_c_demo/src/2d_mask/");
|
||||
printf("2D 矩阵文件目录: %s\n\n", g_matrix_dir);
|
||||
}
|
||||
|
||||
/* 生成假 UUID */
|
||||
uint8_t uuid[16] = {0};
|
||||
srand((unsigned int)GetTickCount());
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uuid[i] = (uint8_t)(rand() % 256);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
* 重点:纯 API 调用区
|
||||
* --------------------------------------------------------- */
|
||||
printf(">>> 正在初始化 QDXnetworkStack 库架构...\n");
|
||||
|
||||
/* 1. 初始化预处理引擎 (分配 256x256 静态滑窗资源) */
|
||||
Preprocess_Init(256, 256);
|
||||
|
||||
/* 2. 初始化网络栈 */
|
||||
TcpLogic_Init(uuid, NULL);
|
||||
|
||||
/* 3. 注册业务回调 (配置更新、坏品通知、采集请求) */
|
||||
TcpLogic_RegisterConfigCallback(on_config_updated);
|
||||
TcpLogic_RegisterDetectionCallback(on_detection_result);
|
||||
TcpLogic_RegisterTempFrameRequestCallback(on_temp_frame_request);
|
||||
|
||||
/* 4. [Hack Demo] 因为 API 库不包含直接设置当前 DevID
|
||||
* 的接口(依赖服务端下发), 但为了 Demo 一开始能用指定的 101 ID
|
||||
* 发起连接,我们手动造一个回调注入进去, 实际上在真实的 CH32 工程中,ID
|
||||
* 会保存在 Flash 里面。这演示了库的工作原理。
|
||||
*/
|
||||
// 由于 g_TcpLogic 结构体对于外部是静态隐藏的,
|
||||
// API 设计中如果要改变 DevID,规范应是等待协议 0x05 下发。
|
||||
// 此处模拟真实情况:我们相信服务端会自动给我分发 ID,
|
||||
// 所以不再强求立刻修改。库有默认 ID=101。
|
||||
|
||||
/* 5. 启动后台处理收发断连。
|
||||
* 它内部通过 _beginthreadex 开了线程!
|
||||
*/
|
||||
TcpLogic_Start();
|
||||
printf(">>> 库引擎以启动。后台线程已接管所有 Socket 收发。\n\n");
|
||||
|
||||
/* =========================================================
|
||||
* 主业务测试循环
|
||||
* ========================================================= */
|
||||
printf("菜单 (上位机请求时会自动发送):\n");
|
||||
printf(" [s] 立即触发一次发送 [c] 打印当前内存参数册\n");
|
||||
printf(" [q] 退出 (Ctrl+C 也可)\n\n");
|
||||
|
||||
while (1) {
|
||||
if (_kbhit()) {
|
||||
int ch = _getch();
|
||||
if (ch == 'q' || ch == 'Q') {
|
||||
break;
|
||||
} else if (ch == 's' || ch == 'S') {
|
||||
printf("\n[Manual] 用户主动触发一帧。\n");
|
||||
on_temp_frame_request(g_dimension_mode);
|
||||
} else if (ch == 'c' || ch == 'C') {
|
||||
ConfigCommon_t com;
|
||||
Config2D_t c2d;
|
||||
Config1D_t c1d;
|
||||
if (TcpLogic_GetLatestConfig(&com, &c2d, &c1d) == 0) {
|
||||
printf(CLR_GREEN
|
||||
"\n[Info] 当前静态配置缓存中包含数据 (严苛度: %d)。" CLR_RESET
|
||||
"\n",
|
||||
com.StrictnessLevel);
|
||||
/* 打印通用配置 */
|
||||
printf(" -> Common: Pipeline: %.*s, Type: %d, Mode: %d, Tag: %d, "
|
||||
"Strictness: %d, Custom: %d\n",
|
||||
16, com.PipelineId, com.PipelineType, com.WorkMode,
|
||||
com.ConfigTag, com.StrictnessLevel, com.IsCustomMode);
|
||||
|
||||
/* 打印 2D 配置 */
|
||||
printf(
|
||||
" -> 2D: Enabled: %d, Live: %d, DevId: %d, %dx%d, Fps: %d\n",
|
||||
c2d.Enabled, c2d.IsLive, c2d.DeviceId, c2d.Width, c2d.Height,
|
||||
c2d.Fps);
|
||||
printf(" -> 2D: Mask: %d (Thresh: %d, %dx%d), Target: %dx%d\n",
|
||||
c2d.MaskEnabled, c2d.MaskThreshold, c2d.MaskWidth,
|
||||
c2d.MaskHeight, c2d.TargetWidth, c2d.TargetHeight);
|
||||
|
||||
/* 打印 1D 配置 */
|
||||
printf(" -> 1D: Enabled: %d, RunMode: %d, TriggerType: %d, "
|
||||
"BufferSize: %d\n",
|
||||
c1d.Enabled, c1d.RunMode, c1d.TriggerType, c1d.BufferSize);
|
||||
} else {
|
||||
printf(CLR_YELLOW
|
||||
"\n[Info] 当前静态配置缓存尚未收到上位机同步!" CLR_RESET
|
||||
"\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
Sleep(50);
|
||||
}
|
||||
|
||||
printf("程序退出中...\n");
|
||||
WSACleanup();
|
||||
return 0;
|
||||
}
|
||||
194
pc/qdx_port_win32.c
Normal file
194
pc/qdx_port_win32.c
Normal file
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* @file qdx_port_win32.c
|
||||
* @brief Windows (WinSock2 + WinAPI) 平台移植层
|
||||
*
|
||||
* 目的:将 qdx_port.h 声明的 HAL 接口映射到 Windows API,
|
||||
* 使 QDXnetworkStack 库能够在 PC 端运行。
|
||||
*
|
||||
* 注意:本文件仅用于 PC 端 Demo 模拟,不适用于 MCU 移植。
|
||||
*/
|
||||
|
||||
#include "qdx_port.h"
|
||||
|
||||
#include <process.h> /* _beginthreadex */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
#include <winsock2.h>
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* 时间与延迟
|
||||
* ============================================================ */
|
||||
|
||||
/* 获取系统启动以来的毫秒数 */
|
||||
uint32_t qdx_port_get_tick_ms(void) { return (uint32_t)GetTickCount(); }
|
||||
|
||||
/* 阻塞延迟指定毫秒 */
|
||||
void qdx_port_delay_ms(uint32_t ms) { Sleep(ms); }
|
||||
|
||||
/* ============================================================
|
||||
* TCP 网络操作 (基于 WinSock2)
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* @brief 创建 TCP 套接字并连接到远端
|
||||
*
|
||||
* 连接成功后设置 500ms 接收超时,防止接收线程永久阻塞。
|
||||
*/
|
||||
qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port) {
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct sockaddr_in addr;
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = inet_addr(ip);
|
||||
|
||||
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR) {
|
||||
closesocket(sock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 设置接收超时 500ms,使 recv 线程可定期检查连接状态 */
|
||||
DWORD timeout_ms = 500;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout_ms,
|
||||
sizeof(timeout_ms));
|
||||
|
||||
printf("[Port] TCP 已连接: %s:%d\n", ip, port);
|
||||
|
||||
/* 将 SOCKET 转为不透明指针(SOCKET 本身是 unsigned int 类型) */
|
||||
return (qdx_socket_t)(intptr_t)sock;
|
||||
}
|
||||
|
||||
/* 关闭 TCP 套接字 */
|
||||
void qdx_port_tcp_close(qdx_socket_t sock) {
|
||||
if (sock) {
|
||||
closesocket((SOCKET)(intptr_t)sock);
|
||||
}
|
||||
}
|
||||
|
||||
/* TCP 发送 */
|
||||
int32_t qdx_port_tcp_send(qdx_socket_t sock, const uint8_t *data,
|
||||
uint32_t len) {
|
||||
if (!sock)
|
||||
return -1;
|
||||
int ret = send((SOCKET)(intptr_t)sock, (const char *)data, (int)len, 0);
|
||||
if (ret == SOCKET_ERROR) {
|
||||
return -1;
|
||||
}
|
||||
return (int32_t)ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TCP 接收(非阻塞/短超时)
|
||||
*
|
||||
* 返回值约定:
|
||||
* > 0 : 实际收到的字节数
|
||||
* = 0 : 超时无数据
|
||||
* < 0 : 连接断开或错误
|
||||
*/
|
||||
int32_t qdx_port_tcp_recv(qdx_socket_t sock, uint8_t *buf, uint32_t max_len) {
|
||||
if (!sock)
|
||||
return -1;
|
||||
int ret = recv((SOCKET)(intptr_t)sock, (char *)buf, (int)max_len, 0);
|
||||
if (ret == SOCKET_ERROR) {
|
||||
int err = WSAGetLastError();
|
||||
/* WSAETIMEDOUT / WSAEWOULDBLOCK 表示超时,视为无数据可读 */
|
||||
if (err == WSAETIMEDOUT || err == WSAEWOULDBLOCK) {
|
||||
return 0;
|
||||
}
|
||||
return -1; /* 真实错误 */
|
||||
}
|
||||
if (ret == 0) {
|
||||
return -1; /* 服务端主动关闭连接 */
|
||||
}
|
||||
return (int32_t)ret;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 互斥锁 (基于 CRITICAL_SECTION)
|
||||
* ============================================================ */
|
||||
|
||||
/* 创建互斥锁 */
|
||||
qdx_mutex_t qdx_port_mutex_create(void) {
|
||||
CRITICAL_SECTION *cs = (CRITICAL_SECTION *)malloc(sizeof(CRITICAL_SECTION));
|
||||
if (!cs)
|
||||
return NULL;
|
||||
InitializeCriticalSection(cs);
|
||||
return (qdx_mutex_t)cs;
|
||||
}
|
||||
|
||||
/* 加锁 */
|
||||
void qdx_port_mutex_lock(qdx_mutex_t mutex) {
|
||||
if (mutex) {
|
||||
EnterCriticalSection((CRITICAL_SECTION *)mutex);
|
||||
}
|
||||
}
|
||||
|
||||
/* 解锁 */
|
||||
void qdx_port_mutex_unlock(qdx_mutex_t mutex) {
|
||||
if (mutex) {
|
||||
LeaveCriticalSection((CRITICAL_SECTION *)mutex);
|
||||
}
|
||||
}
|
||||
|
||||
/* 销毁互斥锁 */
|
||||
void qdx_port_mutex_delete(qdx_mutex_t mutex) {
|
||||
if (mutex) {
|
||||
DeleteCriticalSection((CRITICAL_SECTION *)mutex);
|
||||
free(mutex);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 线程 (基于 _beginthreadex)
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* @brief 线程启动包装器
|
||||
*
|
||||
* 目的:_beginthreadex 要求 unsigned __stdcall 签名,
|
||||
* 而 qdx_thread_entry_t 是 void(*)(void*),
|
||||
* 需要通过此包装器做签名适配。
|
||||
*/
|
||||
typedef struct {
|
||||
qdx_thread_entry_t entry;
|
||||
void *arg;
|
||||
} ThreadWrapper_t;
|
||||
|
||||
static unsigned __stdcall thread_wrapper(void *param) {
|
||||
ThreadWrapper_t *tw = (ThreadWrapper_t *)param;
|
||||
qdx_thread_entry_t entry = tw->entry;
|
||||
void *arg = tw->arg;
|
||||
free(tw); /* 释放包装结构,生命周期结束 */
|
||||
entry(arg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 创建后台线程 */
|
||||
int8_t qdx_port_thread_create(const char *name, qdx_thread_entry_t entry,
|
||||
void *arg, uint32_t stack_size,
|
||||
uint8_t priority) {
|
||||
(void)name; /* Windows 线程不直接支持命名 */
|
||||
(void)priority; /* 简化 Demo 不设线程优先级 */
|
||||
|
||||
ThreadWrapper_t *tw = (ThreadWrapper_t *)malloc(sizeof(ThreadWrapper_t));
|
||||
if (!tw)
|
||||
return -1;
|
||||
tw->entry = entry;
|
||||
tw->arg = arg;
|
||||
|
||||
HANDLE h =
|
||||
(HANDLE)_beginthreadex(NULL, stack_size, thread_wrapper, tw, 0, NULL);
|
||||
if (h == NULL) {
|
||||
free(tw);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 分离线程句柄,后台自动运行 */
|
||||
CloseHandle(h);
|
||||
return 0;
|
||||
}
|
||||
79
prj/middle/QDXnetworkStack/LICENSE.txt
Normal file
79
prj/middle/QDXnetworkStack/LICENSE.txt
Normal file
@ -0,0 +1,79 @@
|
||||
================================================================================
|
||||
END USER LICENSE AGREEMENT
|
||||
================================================================================
|
||||
|
||||
Copyright (c) QDX. All rights reserved.
|
||||
|
||||
This End User License Agreement ("Agreement") is a legal agreement between you
|
||||
(either an individual or a single entity) and QDX regarding the use of the
|
||||
source code and accompanying documentation (hereinafter referred to as the
|
||||
"Software Source Code").
|
||||
|
||||
By obtaining, using, or modifying the Software Source Code, you agree to be
|
||||
bound by the terms and conditions of this Agreement. If you do not agree, do
|
||||
not use the Software Source Code.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
1. COPYRIGHT AND OWNERSHIP
|
||||
--------------------------------------------------------------------------------
|
||||
All title, copyrights, and other intellectual property rights in and to the
|
||||
Software Source Code (including but not limited to its design concepts,
|
||||
algorithms, data structures, implementation, and related documentation) are
|
||||
owned by QDX. The Software Source Code is protected by copyright laws and
|
||||
international copyright treaties.
|
||||
QDX reserves the right of final interpretation regarding the terms of this
|
||||
Agreement and the scope of use of the Software Source Code.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
2. GRANT OF LICENSE AND RESTRICTIONS
|
||||
--------------------------------------------------------------------------------
|
||||
Subject to your compliance with this Agreement, QDX grants you a limited,
|
||||
non-transferable, and non-exclusive right to use and modify the Software Source
|
||||
Code under specific business contexts.
|
||||
|
||||
* Authorized Scope:
|
||||
The open use of this Software Source Code is strictly limited to the specific,
|
||||
formally authorized commercial project and its directly related program or
|
||||
feature development (including debugging, compiling, and system integration).
|
||||
|
||||
* Strict Prohibitions:
|
||||
Without the express written permission of QDX, it is strictly prohibited to
|
||||
use this Software Source Code for any unauthorized commercial projects,
|
||||
products, open-source releases, or to disclose it to any unauthorized third
|
||||
parties. You may not package this code independently for sale, rental, or
|
||||
distribution for competitive commercial purposes.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
3. DISCLAIMER OF WARRANTIES ("AS IS")
|
||||
--------------------------------------------------------------------------------
|
||||
* No Warranties:
|
||||
The Software Source Code and its associated documentation are provided "AS IS",
|
||||
without warranty of any kind. QDX makes no express or implied warranties,
|
||||
including, but not limited to, the implied warranties of merchantability,
|
||||
fitness for a particular purpose, or non-infringement.
|
||||
|
||||
* No Liability for Variants and Performance:
|
||||
QDX shall not be responsible for the performance, quality, security,
|
||||
commercial consequences, or any software failures resulting from any variants
|
||||
(including new features, refactoring, or ported code snippets) created by you
|
||||
during your use or modification of the Software Source Code.
|
||||
|
||||
* Limitation of Liability:
|
||||
In no event shall QDX be liable for any direct, indirect, incidental, special,
|
||||
or punitive damages (including, but not limited to, loss of profits, business
|
||||
interruption, loss of data, or loss of business information) arising out of
|
||||
the use or inability to use the Software Source Code, even if QDX has been
|
||||
advised of the possibility of such damages.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
4. LEGAL EFFECT
|
||||
--------------------------------------------------------------------------------
|
||||
QDX holds the final authority and right of interpretation concerning any
|
||||
disputes arising from or related to this Agreement according to the law. If you
|
||||
fail to comply with any terms of this Agreement, QDX reserves the right to
|
||||
terminate your use authorization immediately and pursue legal responsibility for
|
||||
any economic damages.
|
||||
|
||||
By acquiring and utilizing this Software Source Code, you acknowledge that you
|
||||
have read, understood, and agreed to be strictly bound by all the authorization
|
||||
and disclaimer clauses set forth above.
|
||||
125
prj/middle/QDXnetworkStack/qdx_port.h
Normal file
125
prj/middle/QDXnetworkStack/qdx_port.h
Normal file
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @file qdx_port.h
|
||||
* @brief Hardware/OS Abstraction Layer (HAL) for QDX Network Stack
|
||||
*
|
||||
* Provides platform-independent interfaces for network socket operations,
|
||||
* timing, mutexes, and threading. Users must implement these functions
|
||||
* based on their specific MCU OS (e.g., FreeRTOS, LwIP, RT-Thread).
|
||||
*/
|
||||
|
||||
#ifndef QDX_PORT_H
|
||||
#define QDX_PORT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Time & Delay
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* @brief Get absolute system uptime/ticks in milliseconds.
|
||||
* @return Milliseconds since system boot.
|
||||
*/
|
||||
uint32_t qdx_port_get_tick_ms(void);
|
||||
|
||||
/**
|
||||
* @brief Blocking delay in milliseconds.
|
||||
* @param ms Delay time
|
||||
*/
|
||||
void qdx_port_delay_ms(uint32_t ms);
|
||||
|
||||
/* ============================================================
|
||||
* Network (TCP Socket)
|
||||
* ============================================================ */
|
||||
|
||||
/* Opaque handle for sockets dependent on underlying IP stack */
|
||||
typedef void *qdx_socket_t;
|
||||
|
||||
/**
|
||||
* @brief Create and connect a TCP socket to a remote host.
|
||||
* @param ip Str IP address of the server (e.g., "192.168.1.10")
|
||||
* @param port Remote port
|
||||
* @return Valid socket handle on success, or NULL on failure.
|
||||
*/
|
||||
qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port);
|
||||
|
||||
/**
|
||||
* @brief Close a TCP socket.
|
||||
* @param sock Socket handle
|
||||
*/
|
||||
void qdx_port_tcp_close(qdx_socket_t sock);
|
||||
|
||||
/**
|
||||
* @brief Send data over TCP socket.
|
||||
* @param sock Socket handle
|
||||
* @param data Data buffer to send
|
||||
* @param len Length of data in bytes
|
||||
* @return Number of bytes sent, or < 0 for error.
|
||||
*/
|
||||
int32_t qdx_port_tcp_send(qdx_socket_t sock, const uint8_t *data, uint32_t len);
|
||||
|
||||
/**
|
||||
* @brief Receive data from TCP socket (Non-blocking or specific timeout).
|
||||
* @param sock Socket handle
|
||||
* @param buf Buffer to store received data
|
||||
* @param max_len Maximum buffer size
|
||||
* @return Number of bytes received. Return 0 if timeout/empty. Return < 0 for
|
||||
* connection closed/error.
|
||||
*/
|
||||
int32_t qdx_port_tcp_recv(qdx_socket_t sock, uint8_t *buf, uint32_t max_len);
|
||||
|
||||
/* ============================================================
|
||||
* Mutex & Threading
|
||||
* ============================================================ */
|
||||
|
||||
/* Opaque handle for mutex */
|
||||
typedef void *qdx_mutex_t;
|
||||
|
||||
/**
|
||||
* @brief Create a recursive or standard mutex.
|
||||
* @return Mutex handle, or NULL on failure.
|
||||
*/
|
||||
qdx_mutex_t qdx_port_mutex_create(void);
|
||||
|
||||
/**
|
||||
* @brief Lock a mutex.
|
||||
* @param mutex Mutex handle
|
||||
*/
|
||||
void qdx_port_mutex_lock(qdx_mutex_t mutex);
|
||||
|
||||
/**
|
||||
* @brief Unlock a mutex.
|
||||
* @param mutex Mutex handle
|
||||
*/
|
||||
void qdx_port_mutex_unlock(qdx_mutex_t mutex);
|
||||
|
||||
/**
|
||||
* @brief Delete a mutex.
|
||||
* @param mutex Mutex handle
|
||||
*/
|
||||
void qdx_port_mutex_delete(qdx_mutex_t mutex);
|
||||
|
||||
/* Thread entry callback definition */
|
||||
typedef void (*qdx_thread_entry_t)(void *arg);
|
||||
|
||||
/**
|
||||
* @brief Create a background thread/task.
|
||||
* @param name Task name
|
||||
* @param entry Task entry function
|
||||
* @param arg Task argument
|
||||
* @param stack_size Requested stack size in bytes
|
||||
* @param priority Task priority
|
||||
* @return 0 on success, < 0 on failure.
|
||||
*/
|
||||
int8_t qdx_port_thread_create(const char *name, qdx_thread_entry_t entry,
|
||||
void *arg, uint32_t stack_size, uint8_t priority);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* QDX_PORT_H */
|
||||
143
prj/middle/QDXnetworkStack/qdx_port_template.c
Normal file
143
prj/middle/QDXnetworkStack/qdx_port_template.c
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @file qdx_port_template.c
|
||||
* @brief Template Implementation of the HAL Port for QDX Network Stack
|
||||
*
|
||||
* Instructions:
|
||||
* 1. Rename this file to qdx_port.c in your MCU project.
|
||||
* 2. Implement these functions using your specific OS/Network APIs
|
||||
* (e.g., LwIP, FreeRTOS, RT-Thread, CMSIS-OS).
|
||||
* 3. Ensure qdx_port_tcp_recv is non-blocking or uses a short timeout,
|
||||
* so the background thread can periodically check connection status.
|
||||
*/
|
||||
|
||||
#include "qdx_port.h"
|
||||
#include <stddef.h> /* For NULL */
|
||||
/* Include your system headers here
|
||||
* #include "FreeRTOS.h"
|
||||
* #include "task.h"
|
||||
* #include "lwip/sockets.h"
|
||||
* #include "lwip/sys.h"
|
||||
*/
|
||||
|
||||
/* ============================================================
|
||||
* Time & Delay
|
||||
* ============================================================ */
|
||||
|
||||
uint32_t qdx_port_get_tick_ms(void) {
|
||||
/* TODO: Return current system uptime in milliseconds.
|
||||
* Example (FreeRTOS): return (uint32_t)(xTaskGetTickCount() *
|
||||
* portTICK_PERIOD_MS); Example (HAL): return HAL_GetTick();
|
||||
*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
void qdx_port_delay_ms(uint32_t ms) {
|
||||
/* TODO: Block current thread for ms milliseconds.
|
||||
* Example (FreeRTOS): vTaskDelay(pdMS_TO_TICKS(ms));
|
||||
* Example (HAL): HAL_Delay(ms);
|
||||
*/
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Network (TCP Socket)
|
||||
* ============================================================ */
|
||||
|
||||
qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port) {
|
||||
/* TODO: Create socket and connect to IP:PORT.
|
||||
* Example (LwIP/BSD Sockets):
|
||||
* int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
* if (sock < 0) return NULL;
|
||||
* struct sockaddr_in dest_addr;
|
||||
* dest_addr.sin_addr.s_addr = inet_addr(ip);
|
||||
* dest_addr.sin_family = AF_INET;
|
||||
* dest_addr.sin_port = htons(port);
|
||||
* if (connect(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) == 0)
|
||||
* {
|
||||
* // Optional: Set receive timeout
|
||||
* return (qdx_socket_t)(intptr_t)sock;
|
||||
* }
|
||||
* close(sock);
|
||||
*/
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void qdx_port_tcp_close(qdx_socket_t sock) {
|
||||
/* TODO: Close the socket gracefully.
|
||||
* Example:
|
||||
* if (sock) {
|
||||
* close((int)(intptr_t)sock);
|
||||
* }
|
||||
*/
|
||||
}
|
||||
|
||||
int32_t qdx_port_tcp_send(qdx_socket_t sock, const uint8_t *data,
|
||||
uint32_t len) {
|
||||
/* TODO: Send data through the socket.
|
||||
* Example:
|
||||
* if (!sock) return -1;
|
||||
* return send((int)(intptr_t)sock, data, len, 0);
|
||||
*/
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t qdx_port_tcp_recv(qdx_socket_t sock, uint8_t *buf, uint32_t max_len) {
|
||||
/* TODO: Receive data from the socket.
|
||||
* Should return actual bytes read. If no bytes are available (timeout),
|
||||
* return 0. If socket is closed or an error occurs, return < 0. Example: if
|
||||
* (!sock) return -1;
|
||||
* // Assume socket was set with SO_RCVTIMEO to prevent hanging forever
|
||||
* int bytes = recv((int)(intptr_t)sock, buf, max_len, 0);
|
||||
* if (bytes < 0) {
|
||||
* if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; // Timeout
|
||||
* return -1; // Actual error
|
||||
* }
|
||||
* return bytes; // Includes bytes == 0 (graceful close) -> might want to
|
||||
* map to -1
|
||||
*/
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Mutex & Threading
|
||||
* ============================================================ */
|
||||
|
||||
qdx_mutex_t qdx_port_mutex_create(void) {
|
||||
/* TODO: Create and return a mutex handle.
|
||||
* Example (FreeRTOS):
|
||||
* SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
|
||||
* return (qdx_mutex_t)mutex;
|
||||
*/
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void qdx_port_mutex_lock(qdx_mutex_t mutex) {
|
||||
/* TODO: Lock the given mutex.
|
||||
* Example (FreeRTOS):
|
||||
* if (mutex) xSemaphoreTake((SemaphoreHandle_t)mutex, portMAX_DELAY);
|
||||
*/
|
||||
}
|
||||
|
||||
void qdx_port_mutex_unlock(qdx_mutex_t mutex) {
|
||||
/* TODO: Unlock the given mutex.
|
||||
* Example (FreeRTOS):
|
||||
* if (mutex) xSemaphoreGive((SemaphoreHandle_t)mutex);
|
||||
*/
|
||||
}
|
||||
|
||||
void qdx_port_mutex_delete(qdx_mutex_t mutex) {
|
||||
/* TODO: Delete the given mutex.
|
||||
* Example (FreeRTOS):
|
||||
* if (mutex) vSemaphoreDelete((SemaphoreHandle_t)mutex);
|
||||
*/
|
||||
}
|
||||
|
||||
int8_t qdx_port_thread_create(const char *name, qdx_thread_entry_t entry,
|
||||
void *arg, uint32_t stack_size,
|
||||
uint8_t priority) {
|
||||
/* TODO: Create a background thread/task to run the entry function.
|
||||
* Example (FreeRTOS):
|
||||
* BaseType_t res = xTaskCreate(entry, name, stack_size /
|
||||
* sizeof(StackType_t), arg, priority, NULL); return (res == pdPASS) ? 0 : -1;
|
||||
*/
|
||||
return -1;
|
||||
}
|
||||
325
prj/middle/QDXnetworkStack/qdx_preprocess.c
Normal file
325
prj/middle/QDXnetworkStack/qdx_preprocess.c
Normal file
@ -0,0 +1,325 @@
|
||||
/**
|
||||
* @file qdx_preprocess.c
|
||||
* @brief Zero-Copy Image Preprocessing Implementation
|
||||
*/
|
||||
|
||||
#include "qdx_preprocess.h"
|
||||
#include "qdx_port.h"
|
||||
#include <string.h>
|
||||
|
||||
/* ============================================================
|
||||
* Internal State & Configuration Cache
|
||||
* ============================================================ */
|
||||
|
||||
/* Static allocation for column sums to avoid malloc */
|
||||
#define PREPROCESS_MAX_WIDTH 256
|
||||
static uint32_t g_col_sums[PREPROCESS_MAX_WIDTH];
|
||||
static uint8_t g_is_initialized = 0;
|
||||
|
||||
/* 配置读写互斥锁,防止 Execute 与 Settings_Change 并发冲突 */
|
||||
static qdx_mutex_t g_preprocess_mutex = NULL;
|
||||
|
||||
static struct {
|
||||
Config2D_t cfg2d;
|
||||
Config1D_t cfg1d;
|
||||
ConfigCommon_t common;
|
||||
} g_PreprocessCfg;
|
||||
|
||||
/* ============================================================
|
||||
* API Implementation
|
||||
* ============================================================ */
|
||||
|
||||
int8_t Preprocess_Init(uint16_t maxWidth, uint16_t maxHeight) {
|
||||
(void)maxHeight; /* 列累加仅依赖宽度 */
|
||||
|
||||
if (maxWidth > PREPROCESS_MAX_WIDTH) {
|
||||
return -1; /* 超出静态分配缓冲区上限 */
|
||||
}
|
||||
|
||||
memset(g_col_sums, 0, sizeof(g_col_sums));
|
||||
memset(&g_PreprocessCfg, 0, sizeof(g_PreprocessCfg));
|
||||
|
||||
/* 创建配置互斥锁 */
|
||||
g_preprocess_mutex = qdx_port_mutex_create();
|
||||
if (g_preprocess_mutex == NULL)
|
||||
return -1;
|
||||
|
||||
/* 最小默认配置,防止除零或无限循环 */
|
||||
g_PreprocessCfg.cfg2d.TargetWidth = 1;
|
||||
g_PreprocessCfg.cfg2d.TargetHeight = 1;
|
||||
|
||||
g_is_initialized = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t Preprocess_Settings_Change(const Config2D_t *newConfig2D,
|
||||
const Config1D_t *newConfig1D,
|
||||
const ConfigCommon_t *newCommon) {
|
||||
if (!g_is_initialized)
|
||||
return -1;
|
||||
|
||||
/* 加锁保护配置更新,防止与 Execute 读取产生竞态 */
|
||||
qdx_port_mutex_lock(g_preprocess_mutex);
|
||||
|
||||
if (newConfig2D)
|
||||
memcpy(&g_PreprocessCfg.cfg2d, newConfig2D, sizeof(Config2D_t));
|
||||
if (newConfig1D)
|
||||
memcpy(&g_PreprocessCfg.cfg1d, newConfig1D, sizeof(Config1D_t));
|
||||
if (newCommon)
|
||||
memcpy(&g_PreprocessCfg.common, newCommon, sizeof(ConfigCommon_t));
|
||||
|
||||
/* 安全检查 */
|
||||
if (g_PreprocessCfg.cfg2d.TargetWidth == 0)
|
||||
g_PreprocessCfg.cfg2d.TargetWidth = 1;
|
||||
if (g_PreprocessCfg.cfg2d.TargetHeight == 0)
|
||||
g_PreprocessCfg.cfg2d.TargetHeight = 1;
|
||||
|
||||
qdx_port_mutex_unlock(g_preprocess_mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t Preprocess_Execute(const RawImageBuffer_t *input,
|
||||
TcpTxBuffer_t *out_buffer,
|
||||
PreprocessResult_t *output_meta) {
|
||||
if (!g_is_initialized || !input || !input->pData || !out_buffer ||
|
||||
!out_buffer->pBuffer || !output_meta)
|
||||
return -1;
|
||||
|
||||
/* 加锁快照当前配置,最小化持锁时间 */
|
||||
qdx_port_mutex_lock(g_preprocess_mutex);
|
||||
uint16_t tgt_w = g_PreprocessCfg.cfg2d.TargetWidth;
|
||||
uint16_t tgt_h = g_PreprocessCfg.cfg2d.TargetHeight;
|
||||
int16_t thresh = g_PreprocessCfg.cfg2d.TriggerTemperatureThreshold;
|
||||
qdx_port_mutex_unlock(g_preprocess_mutex);
|
||||
|
||||
uint16_t w = input->Width;
|
||||
uint16_t h = input->Height;
|
||||
|
||||
/* 目标超过输入时回退到整幅图像 */
|
||||
if (tgt_w > w)
|
||||
tgt_w = w;
|
||||
if (tgt_h > h)
|
||||
tgt_h = h;
|
||||
|
||||
/* ---- 缓冲区容量越界检查 ---- */
|
||||
uint32_t required_bytes = (uint32_t)tgt_w * tgt_h * 2u;
|
||||
if (out_buffer->HeadOffset + required_bytes > out_buffer->TotalCapacity) {
|
||||
return -3; /* 输出缓冲区空间不足 */
|
||||
}
|
||||
|
||||
/* 判断是否需要滑窗计算,或直接导出全图 */
|
||||
if (tgt_w == w && tgt_h == h) {
|
||||
/* 无需滑窗,仅做温度过滤与整体统计 */
|
||||
int16_t min_t = 32767;
|
||||
int16_t max_t = -32768;
|
||||
uint32_t total_sum = 0;
|
||||
uint32_t pixels = w * h;
|
||||
|
||||
/* Write directly to out_buffer starting at HeadOffset */
|
||||
uint8_t *dest_ptr = out_buffer->pBuffer + out_buffer->HeadOffset;
|
||||
|
||||
for (uint32_t i = 0; i < pixels; i++) {
|
||||
int16_t raww_val = (int16_t)input->pData[i];
|
||||
|
||||
if (raww_val < min_t)
|
||||
min_t = raww_val;
|
||||
if (raww_val > max_t)
|
||||
max_t = raww_val;
|
||||
total_sum += raww_val;
|
||||
|
||||
/* Encode Original Raw Value (Little-Endian sequence into uint8_t) */
|
||||
*dest_ptr++ = (uint8_t)(raww_val & 0xFF);
|
||||
*dest_ptr++ = (uint8_t)((raww_val >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
output_meta->pValidData = out_buffer->pBuffer + out_buffer->HeadOffset;
|
||||
output_meta->DataLength = pixels * 2;
|
||||
output_meta->ValidWidth = w;
|
||||
output_meta->ValidHeight = h;
|
||||
output_meta->MinTemp = min_t;
|
||||
output_meta->MaxTemp = max_t;
|
||||
output_meta->AvgTemp = (int16_t)(total_sum / pixels);
|
||||
output_meta->RoiTemp = output_meta->AvgTemp;
|
||||
output_meta->Status = 0;
|
||||
output_meta->FrameNumber = input->FrameNumber;
|
||||
|
||||
out_buffer->ValidPayloadLen = output_meta->DataLength;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
Perform Sliding Window Search for Average Temperature Target Region
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
memset(g_col_sums, 0, w * sizeof(uint32_t));
|
||||
uint32_t max_region_sum = 0;
|
||||
uint16_t best_x = 0;
|
||||
uint16_t best_y = 0;
|
||||
|
||||
for (uint16_t y = 0; y <= h - tgt_h; y++) {
|
||||
|
||||
/* Step 1: Initialize column sums for this row strip
|
||||
If y == 0, we calculate the entire strip.
|
||||
Otherwise, we subtract the top row that left, and add the bottom row that
|
||||
entered. */
|
||||
|
||||
if (y == 0) {
|
||||
for (uint16_t c = 0; c < w; c++) {
|
||||
uint32_t col_total = 0;
|
||||
for (uint16_t r = 0; r < tgt_h; r++) {
|
||||
int16_t val = (int16_t)input->pData[r * w + c];
|
||||
if (val < thresh)
|
||||
val = 90;
|
||||
col_total += val;
|
||||
}
|
||||
g_col_sums[c] = col_total;
|
||||
}
|
||||
} else {
|
||||
/* Slide down by 1 row */
|
||||
for (uint16_t c = 0; c < w; c++) {
|
||||
int16_t top_val = (int16_t)input->pData[(y - 1) * w + c];
|
||||
int16_t bot_val = (int16_t)input->pData[(y + tgt_h - 1) * w + c];
|
||||
|
||||
if (top_val < thresh)
|
||||
top_val = 90;
|
||||
if (bot_val < thresh)
|
||||
bot_val = 90;
|
||||
|
||||
g_col_sums[c] = g_col_sums[c] - top_val + bot_val;
|
||||
}
|
||||
}
|
||||
|
||||
/* Step 2: Slide Across the Columns (Left to Right) */
|
||||
uint32_t current_window_sum = 0;
|
||||
|
||||
/* Initialize first window */
|
||||
for (uint16_t c = 0; c < tgt_w; c++) {
|
||||
current_window_sum += g_col_sums[c];
|
||||
}
|
||||
|
||||
if (current_window_sum > max_region_sum) {
|
||||
max_region_sum = current_window_sum;
|
||||
best_x = 0;
|
||||
best_y = y;
|
||||
}
|
||||
|
||||
/* Slide Right */
|
||||
for (uint16_t x = 1; x <= w - tgt_w; x++) {
|
||||
current_window_sum =
|
||||
current_window_sum - g_col_sums[x - 1] + g_col_sums[x + tgt_w - 1];
|
||||
|
||||
if (current_window_sum > max_region_sum) {
|
||||
max_region_sum = current_window_sum;
|
||||
best_x = x;
|
||||
best_y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
Extract the Data
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
int16_t min_t = 32767;
|
||||
int16_t max_t = -32768;
|
||||
uint32_t total_sum = 0;
|
||||
uint32_t pixels = tgt_w * tgt_h;
|
||||
uint8_t *dest_ptr = out_buffer->pBuffer + out_buffer->HeadOffset;
|
||||
|
||||
for (uint16_t r = 0; r < tgt_h; r++) {
|
||||
for (uint16_t c = 0; c < tgt_w; c++) {
|
||||
/* Raw offset into original image */
|
||||
uint32_t src_idx = (best_y + r) * w + (best_x + c);
|
||||
int16_t raww_val = (int16_t)input->pData[src_idx];
|
||||
|
||||
if (raww_val < min_t)
|
||||
min_t = raww_val;
|
||||
if (raww_val > max_t)
|
||||
max_t = raww_val;
|
||||
total_sum += raww_val;
|
||||
|
||||
/* Output Original Raw Values */
|
||||
*dest_ptr++ = (uint8_t)(raww_val & 0xFF);
|
||||
*dest_ptr++ = (uint8_t)((raww_val >> 8) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
output_meta->pValidData = out_buffer->pBuffer + out_buffer->HeadOffset;
|
||||
output_meta->DataLength = pixels * 2;
|
||||
output_meta->ValidWidth = tgt_w;
|
||||
output_meta->ValidHeight = tgt_h;
|
||||
output_meta->MinTemp = min_t;
|
||||
output_meta->MaxTemp = max_t;
|
||||
output_meta->AvgTemp = (int16_t)(total_sum / pixels);
|
||||
output_meta->RoiTemp = output_meta->AvgTemp;
|
||||
output_meta->Status = 0;
|
||||
output_meta->FrameNumber = input->FrameNumber;
|
||||
|
||||
out_buffer->ValidPayloadLen = output_meta->DataLength;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Internal Trigger Check
|
||||
* ============================================================ */
|
||||
int8_t Preprocess_CheckInternalTrigger2D(const RawImageBuffer_t *input) {
|
||||
if (!g_is_initialized || !input || !input->pData)
|
||||
return -1;
|
||||
|
||||
qdx_port_mutex_lock(g_preprocess_mutex);
|
||||
uint16_t roi_x = g_PreprocessCfg.cfg2d.TriggerRoiX;
|
||||
uint16_t roi_y = g_PreprocessCfg.cfg2d.TriggerRoiY;
|
||||
uint16_t roi_w = g_PreprocessCfg.cfg2d.TriggerRoiW;
|
||||
uint16_t roi_h = g_PreprocessCfg.cfg2d.TriggerRoiH;
|
||||
uint8_t condition = g_PreprocessCfg.cfg2d.TriggerCondition;
|
||||
int16_t thresh = g_PreprocessCfg.cfg2d.TriggerTemperatureThreshold;
|
||||
qdx_port_mutex_unlock(g_preprocess_mutex);
|
||||
|
||||
uint16_t w = input->Width;
|
||||
uint16_t h = input->Height;
|
||||
|
||||
/* Boundary Check & Clipping */
|
||||
if (roi_w == 0 || roi_h == 0)
|
||||
return 0;
|
||||
if (roi_x >= w || roi_y >= h)
|
||||
return 0;
|
||||
if (roi_x + roi_w > w)
|
||||
roi_w = w - roi_x;
|
||||
if (roi_y + roi_h > h)
|
||||
roi_h = h - roi_y;
|
||||
|
||||
int16_t max_temp = -32768;
|
||||
int64_t sum_temp = 0;
|
||||
uint32_t count = roi_w * roi_h;
|
||||
|
||||
for (uint16_t r = 0; r < roi_h; r++) {
|
||||
for (uint16_t c = 0; c < roi_w; c++) {
|
||||
int16_t val = (int16_t)input->pData[(roi_y + r) * w + (roi_x + c)];
|
||||
|
||||
/* Temperature Filtration Preprocessing */
|
||||
if (val < thresh) {
|
||||
val = 90; /* Treat as 9.0C */
|
||||
}
|
||||
|
||||
if (val > max_temp)
|
||||
max_temp = val;
|
||||
sum_temp += val;
|
||||
}
|
||||
}
|
||||
|
||||
int16_t calc_val = 0;
|
||||
if (condition == 1) {
|
||||
/* 1: Max */
|
||||
calc_val = max_temp;
|
||||
} else {
|
||||
/* 0: Average */
|
||||
calc_val = (int16_t)(sum_temp / count);
|
||||
}
|
||||
|
||||
if (calc_val >= thresh) {
|
||||
return 1; /* Triggered */
|
||||
}
|
||||
|
||||
return 0; /* Not triggered */
|
||||
}
|
||||
74
prj/middle/QDXnetworkStack/qdx_preprocess.h
Normal file
74
prj/middle/QDXnetworkStack/qdx_preprocess.h
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @file qdx_preprocess.h
|
||||
* @brief Zero-Copy Image Preprocessing for Thermal Imaging
|
||||
*
|
||||
* Includes Sliding Window Maximum/Average ROI search,
|
||||
* Temperature filtering, and Data extraction.
|
||||
*/
|
||||
|
||||
#ifndef QDX_PREPROCESS_H
|
||||
#define QDX_PREPROCESS_H
|
||||
|
||||
#include "qdx_protocol.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize static memory resources for sliding window calculations.
|
||||
*
|
||||
* Called once during system boot. Pre-allocates column accumulation arrays
|
||||
* to prevent dynamic allocation during runtime.
|
||||
*
|
||||
* @param maxWidth The maximum supported width of the 2D input matrix
|
||||
* @param maxHeight The maximum supported height of the 2D input matrix
|
||||
* @return 0 on success, < 0 on failure.
|
||||
*/
|
||||
int8_t Preprocess_Init(uint16_t maxWidth, uint16_t maxHeight);
|
||||
|
||||
/**
|
||||
* @brief Execute the preprocessing pipeline on a raw frame.
|
||||
*
|
||||
* High-performance processing logic that reads `input`, applies thresholds,
|
||||
* finds the best `TargetWidth x TargetHeight` ROI using sliding window arrays,
|
||||
* and extracts that region into `out_buffer` starting precisely at
|
||||
* `HeadOffset`.
|
||||
*
|
||||
* @param input Raw thermal image array
|
||||
* @param out_buffer Pre-allocated transmission buffer wrapper
|
||||
* @param output_meta Struct to fill with process results/metadata
|
||||
* @return 0 if successful, < 0 if aborted (e.g., config invalid)
|
||||
*/
|
||||
int8_t Preprocess_Execute(const RawImageBuffer_t *input,
|
||||
TcpTxBuffer_t *out_buffer,
|
||||
PreprocessResult_t *output_meta);
|
||||
|
||||
/**
|
||||
* @brief Safely update internal preprocessing shadow parameters.
|
||||
*
|
||||
* @param newConfig2D Partial Configuration Update
|
||||
* @param newConfig1D Partial Configuration Update
|
||||
* @param newCommon General System Setup
|
||||
* @return 0 on success.
|
||||
*/
|
||||
int8_t Preprocess_Settings_Change(const Config2D_t *newConfig2D,
|
||||
const Config1D_t *newConfig1D,
|
||||
const ConfigCommon_t *newCommon);
|
||||
|
||||
/**
|
||||
* @brief Check if the current frame triggers the internal 2D capture condition
|
||||
*
|
||||
* Evaluates the specified TriggerRoi region using either Max or Average
|
||||
* temperature against the TriggerTemperatureThreshold.
|
||||
*
|
||||
* @param input Raw thermal image array
|
||||
* @return 1 if trigger condition is met, 0 if not met, < 0 on error.
|
||||
*/
|
||||
int8_t Preprocess_CheckInternalTrigger2D(const RawImageBuffer_t *input);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* QDX_PREPROCESS_H */
|
||||
156
prj/middle/QDXnetworkStack/qdx_protocol.c
Normal file
156
prj/middle/QDXnetworkStack/qdx_protocol.c
Normal file
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @file qdx_protocol.c
|
||||
* @brief Communication Protocol 2.0 Utility Implementations
|
||||
*/
|
||||
|
||||
#include "qdx_protocol.h"
|
||||
#include <string.h>
|
||||
|
||||
/* ============================================================
|
||||
* Shift-Safe Serialization (Little-Endian)
|
||||
* ============================================================ */
|
||||
|
||||
void qdx_write_u16_le(uint8_t *buf, uint16_t val) {
|
||||
buf[0] = (uint8_t)(val & 0xFF);
|
||||
buf[1] = (uint8_t)((val >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
void qdx_write_u32_le(uint8_t *buf, uint32_t val) {
|
||||
buf[0] = (uint8_t)(val & 0xFF);
|
||||
buf[1] = (uint8_t)((val >> 8) & 0xFF);
|
||||
buf[2] = (uint8_t)((val >> 16) & 0xFF);
|
||||
buf[3] = (uint8_t)((val >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
uint16_t qdx_read_u16_le(const uint8_t *buf) {
|
||||
return (uint16_t)(buf[0] | (buf[1] << 8));
|
||||
}
|
||||
|
||||
uint32_t qdx_read_u32_le(const uint8_t *buf) {
|
||||
/* 显式转型防止高位字节的符号位扩展 */
|
||||
return ((uint32_t)buf[0]) | ((uint32_t)buf[1] << 8) |
|
||||
((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CRC16-MODBUS Checksum (Polynomial: 0xA001, Init: 0xFFFF)
|
||||
* ============================================================ */
|
||||
uint16_t qdx_crc16_modbus(const uint8_t *data, int len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (int i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (crc & 0x0001) {
|
||||
crc >>= 1;
|
||||
crc ^= 0xA001;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Build Frame (Header + TLV + Payload + CRC16)
|
||||
* Uses strict byte shifting to prevent unaligned struct access on MCU.
|
||||
* ============================================================ */
|
||||
int qdx_build_frame(uint8_t *buf, uint8_t msg_class, uint8_t tlv_type,
|
||||
const uint8_t *payload, uint16_t payload_len,
|
||||
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
|
||||
uint8_t flags) {
|
||||
|
||||
flags |= FLAG_LAST_FRAGMENT;
|
||||
uint16_t total_len = HEADER_SIZE + TLV_HEADER_SIZE + payload_len + CRC_SIZE;
|
||||
|
||||
/* 1. Fill FrameHeader (16 bytes) safely */
|
||||
qdx_write_u16_le(buf + 0, PROTO_MAGIC); /* Magic */
|
||||
buf[2] = PROTO_VERSION; /* Version */
|
||||
qdx_write_u16_le(buf + 3, total_len); /* Length */
|
||||
qdx_write_u16_le(buf + 5, seq); /* Sequence */
|
||||
qdx_write_u32_le(buf + 7, timestamp); /* Timestamp */
|
||||
buf[11] = 0x01; /* Source = MCU (0x01) */
|
||||
qdx_write_u16_le(buf + 12, dev_id); /* DevID */
|
||||
buf[14] = msg_class; /* Class */
|
||||
buf[15] = flags; /* Flags */
|
||||
|
||||
/* 2. Fill TLV Header (3 bytes) safely */
|
||||
buf[16] = tlv_type; /* TLV Type */
|
||||
qdx_write_u16_le(buf + 17, payload_len); /* TLV Length */
|
||||
|
||||
/* 3. Copy Payload(使用 memmove 防止 payload 与 buf 重叠时的未定义行为) */
|
||||
if (payload && payload_len > 0) {
|
||||
memmove(buf + HEADER_SIZE + TLV_HEADER_SIZE, payload, payload_len);
|
||||
}
|
||||
|
||||
/* 4. Append CRC16 */
|
||||
uint16_t crc = qdx_crc16_modbus(buf, total_len - CRC_SIZE);
|
||||
qdx_write_u16_le(buf + total_len - CRC_SIZE, crc);
|
||||
|
||||
return total_len;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* In-Place Frame Build (Header + CRC only, payload already in place)
|
||||
* 用于零拷贝场景:payload(含 TLV + 数据)已填充在 buf[HEADER_SIZE] 处,
|
||||
* 本函数仅写入 16 字节帧头并在尾部追加 CRC16。
|
||||
* ============================================================ */
|
||||
int qdx_build_frame_inplace(uint8_t *buf, uint8_t msg_class,
|
||||
uint16_t payload_len, uint16_t dev_id, uint16_t seq,
|
||||
uint32_t timestamp, uint8_t flags) {
|
||||
|
||||
flags |= FLAG_LAST_FRAGMENT;
|
||||
uint16_t total_len = HEADER_SIZE + payload_len + CRC_SIZE;
|
||||
|
||||
/* 填写 16 字节帧头 */
|
||||
qdx_write_u16_le(buf + 0, PROTO_MAGIC);
|
||||
buf[2] = PROTO_VERSION;
|
||||
qdx_write_u16_le(buf + 3, total_len);
|
||||
qdx_write_u16_le(buf + 5, seq);
|
||||
qdx_write_u32_le(buf + 7, timestamp);
|
||||
buf[11] = 0x01; /* Source = MCU */
|
||||
qdx_write_u16_le(buf + 12, dev_id);
|
||||
buf[14] = msg_class;
|
||||
buf[15] = flags;
|
||||
|
||||
/* payload 已在 buf[16..16+payload_len-1],无需拷贝 */
|
||||
|
||||
/* 追加 CRC16 */
|
||||
uint16_t crc = qdx_crc16_modbus(buf, total_len - CRC_SIZE);
|
||||
qdx_write_u16_le(buf + total_len - CRC_SIZE, crc);
|
||||
|
||||
return total_len;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Build Fragmented Frame (Header + Chunk + CRC16, NO TLV)
|
||||
* ============================================================ */
|
||||
int qdx_build_fragment_frame(uint8_t *buf, uint8_t msg_class,
|
||||
const uint8_t *chunk, uint16_t chunk_len,
|
||||
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
|
||||
uint8_t flags) {
|
||||
|
||||
uint16_t total_len = HEADER_SIZE + chunk_len + CRC_SIZE;
|
||||
|
||||
/* 1. Fill FrameHeader (16 bytes) safely */
|
||||
qdx_write_u16_le(buf + 0, PROTO_MAGIC);
|
||||
buf[2] = PROTO_VERSION;
|
||||
qdx_write_u16_le(buf + 3, total_len);
|
||||
qdx_write_u16_le(buf + 5, seq);
|
||||
qdx_write_u32_le(buf + 7, timestamp);
|
||||
buf[11] = 0x01; /* Source MCU */
|
||||
qdx_write_u16_le(buf + 12, dev_id);
|
||||
buf[14] = msg_class;
|
||||
buf[15] = flags;
|
||||
|
||||
/* 2. Copy Payload Chunk(使用 memmove 应对潜在重叠) */
|
||||
if (chunk && chunk_len > 0) {
|
||||
memmove(buf + HEADER_SIZE, chunk, chunk_len);
|
||||
}
|
||||
|
||||
/* 3. Append CRC16 */
|
||||
uint16_t crc = qdx_crc16_modbus(buf, total_len - CRC_SIZE);
|
||||
qdx_write_u16_le(buf + total_len - CRC_SIZE, crc);
|
||||
|
||||
return total_len;
|
||||
}
|
||||
336
prj/middle/QDXnetworkStack/qdx_protocol.h
Normal file
336
prj/middle/QDXnetworkStack/qdx_protocol.h
Normal file
@ -0,0 +1,336 @@
|
||||
/**
|
||||
* @file qdx_protocol.h
|
||||
* @brief Communication Protocol 2.0 Structures and Constants
|
||||
*
|
||||
* Platform-independent header for CH32 MCU and other systems.
|
||||
* All structures use 1-byte alignment. Focuses on safe uint8_t
|
||||
* serialization and avoiding hardware faults from unaligned access.
|
||||
*/
|
||||
|
||||
#ifndef QDX_PROTOCOL_H
|
||||
#define QDX_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ============================================================
|
||||
* Protocol Constants
|
||||
* ============================================================ */
|
||||
#define PROTO_MAGIC 0x55AA /* Frame Header Magic */
|
||||
#define PROTO_VERSION 0x20 /* Protocol Version v2.0 */
|
||||
|
||||
/* Message Classes (Class) */
|
||||
#define CLASS_CONTROL 0x01 /* Configuration / Control */
|
||||
#define CLASS_DATA 0x02 /* Real-time Data Report */
|
||||
#define CLASS_RESPONSE 0x03 /* ACK / NACK / Error */
|
||||
#define CLASS_SYSTEM 0x04 /* Handshake / Heartbeat / Sync */
|
||||
|
||||
/* TLV Types */
|
||||
#define TYPE_HANDSHAKE 0x01
|
||||
#define TYPE_HEARTBEAT 0x02
|
||||
#define TYPE_SYNC_TIME 0x03
|
||||
#define TYPE_DEVID_ASSIGN 0x05
|
||||
#define TYPE_TEMP_FRAME 0x10
|
||||
#define TYPE_RAW_FRAME 0x11
|
||||
#define TYPE_CONFIG_COMMON 0x20
|
||||
#define TYPE_CONFIG_2D 0x22
|
||||
#define TYPE_CONFIG_1D 0x23
|
||||
#define TYPE_ACK_PAYLOAD 0x30
|
||||
#define TYPE_DETECTION_RESULT 0x40
|
||||
|
||||
/* Flags */
|
||||
#define FLAG_PRIORITY_MASK 0x03
|
||||
#define FLAG_COMPRESSED 0x04
|
||||
#define FLAG_ENCRYPTED 0x08
|
||||
#define FLAG_ACK_REQ 0x10
|
||||
#define FLAG_LAST_FRAGMENT 0x20
|
||||
|
||||
/* Frame Structure Sizes */
|
||||
#define HEADER_SIZE 16 /* FrameHeader_t size */
|
||||
#define TLV_HEADER_SIZE 3 /* TLV_t size (Type + Length) */
|
||||
#define CRC_SIZE 2 /* CRC16 size */
|
||||
|
||||
/* Fragmentation limit */
|
||||
#define MAX_FRAGMENT_PAYLOAD 1400
|
||||
|
||||
/* Error Codes */
|
||||
#define ERR_NONE 0x0000
|
||||
#define ERR_CRC 0x1001
|
||||
#define ERR_VERSION 0x1002
|
||||
#define ERR_LENGTH 0x1003
|
||||
#define ERR_AUTH 0x2001
|
||||
#define ERR_BUSY 0x2002
|
||||
#define ERR_DEV_ID_CONFLICT 0x2003
|
||||
#define ERR_PARAM 0x3001
|
||||
|
||||
/* ============================================================
|
||||
* Internal Protocol Structures (Packed to 1 byte)
|
||||
* ============================================================ */
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
/**
|
||||
* @brief Frame Header (16 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t Magic;
|
||||
uint8_t Version;
|
||||
uint16_t Length;
|
||||
uint16_t Sequence;
|
||||
uint32_t Timestamp;
|
||||
uint8_t Source;
|
||||
uint16_t DevID;
|
||||
uint8_t Class;
|
||||
uint8_t Flags;
|
||||
} FrameHeader_t;
|
||||
|
||||
/**
|
||||
* @brief TLV Header (3 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t Type;
|
||||
uint16_t Length;
|
||||
} TLV_t;
|
||||
|
||||
/**
|
||||
* @brief Handshake (46 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t ProtocolVersion;
|
||||
uint8_t DeviceUUID[16];
|
||||
uint8_t AuthToken[16];
|
||||
char HardwareVersion[8];
|
||||
char FirmwareVersion[8];
|
||||
uint32_t Capabilities;
|
||||
} Handshake_t;
|
||||
|
||||
/**
|
||||
* @brief Heartbeat (6 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t UpTime;
|
||||
uint8_t CpuLoad;
|
||||
uint8_t MemUsage;
|
||||
} Heartbeat_t;
|
||||
|
||||
/**
|
||||
* @brief ACK Payload (5 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t AckSeq;
|
||||
uint8_t Status;
|
||||
uint16_t ErrorCode;
|
||||
} Ack_t;
|
||||
|
||||
/**
|
||||
* @brief Device ID Assignment (4 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t NewDevID;
|
||||
uint16_t Reserved;
|
||||
} DevIDAssignment_t;
|
||||
|
||||
/**
|
||||
* @brief Temperature Frame Header (18 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t FrameNumber;
|
||||
uint16_t Width;
|
||||
uint16_t Height;
|
||||
int16_t MinTemp;
|
||||
int16_t MaxTemp;
|
||||
int16_t AvgTemp;
|
||||
int16_t RoiTemp;
|
||||
uint8_t FrameType;
|
||||
uint8_t Status;
|
||||
uint8_t Is2D;
|
||||
uint8_t Reserved;
|
||||
} TemperatureFrameHeader_t;
|
||||
|
||||
/**
|
||||
* @brief 1D Temperature Point (4 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t TimeOffset;
|
||||
uint16_t Temperature;
|
||||
} TempPoint1D_t;
|
||||
|
||||
/**
|
||||
* @brief Detection Result (8 bytes)
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t FrameNumber;
|
||||
uint8_t Result;
|
||||
uint8_t Reserved[3];
|
||||
} DetectionResult_t;
|
||||
|
||||
/**
|
||||
* @brief Common Configuration
|
||||
*/
|
||||
typedef struct {
|
||||
char PipelineId[16];
|
||||
uint8_t PipelineType;
|
||||
uint8_t WorkMode;
|
||||
uint8_t ConfigTag;
|
||||
uint8_t StrictnessLevel;
|
||||
uint8_t IsCustomMode;
|
||||
uint8_t Reserved[2];
|
||||
} ConfigCommon_t;
|
||||
|
||||
/**
|
||||
* @brief 2D Configuration
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t Enabled;
|
||||
uint8_t IsLive;
|
||||
uint16_t DeviceId;
|
||||
uint16_t Width;
|
||||
uint16_t Height;
|
||||
uint8_t Fps;
|
||||
uint32_t Exposure;
|
||||
uint8_t AutoExposure;
|
||||
uint8_t MaskEnabled;
|
||||
int16_t MaskThreshold;
|
||||
uint16_t MaskWidth;
|
||||
uint16_t MaskHeight;
|
||||
int16_t Angle;
|
||||
uint16_t TargetWidth;
|
||||
uint16_t TargetHeight;
|
||||
uint8_t TriggerMode;
|
||||
uint8_t TriggerGpioLine;
|
||||
uint16_t TriggerDelayMs;
|
||||
uint8_t TriggerBurstCount;
|
||||
uint16_t TriggerInternalIntervalMs;
|
||||
int16_t TriggerTemperatureThreshold;
|
||||
uint16_t TriggerDebounceIntervalMs;
|
||||
uint8_t TriggerCondition;
|
||||
uint16_t TriggerRoiX;
|
||||
uint16_t TriggerRoiY;
|
||||
uint16_t TriggerRoiW;
|
||||
uint16_t TriggerRoiH;
|
||||
uint16_t NGioDelay;
|
||||
uint8_t OutputGpioLine;
|
||||
uint8_t AlarmGpioLine;
|
||||
uint16_t AlarmHoldMs;
|
||||
uint8_t StoreNgImagesOnly;
|
||||
uint8_t TrainingEnabled;
|
||||
uint16_t TrainingSampleThreshold;
|
||||
uint16_t ProcessingTimeoutMs;
|
||||
uint8_t MaxProcessingQueueSize;
|
||||
uint8_t Reserved;
|
||||
} Config2D_t;
|
||||
|
||||
/**
|
||||
* @brief 1D Configuration
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t Enabled;
|
||||
uint8_t RunMode;
|
||||
uint8_t TriggerType;
|
||||
uint16_t BufferSize;
|
||||
int16_t TriggerTempLimit;
|
||||
uint16_t StartPointsToRemove;
|
||||
uint16_t ReferenceLength;
|
||||
uint16_t HighTimerLimit;
|
||||
uint16_t TimerCLimit;
|
||||
uint8_t NgCountLimit;
|
||||
uint16_t LSizeStart;
|
||||
uint16_t RSizeStart;
|
||||
uint16_t NGioDelay;
|
||||
uint8_t OutputGpioLine;
|
||||
uint8_t AlarmGpioLine;
|
||||
uint16_t AlarmHoldMs;
|
||||
} Config1D_t;
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
/* ============================================================
|
||||
* Application Level Business Structures
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* @brief Raw Image Buffer provided by capture module
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t *pData;
|
||||
uint16_t Width;
|
||||
uint16_t Height;
|
||||
uint32_t FrameNumber;
|
||||
} RawImageBuffer_t;
|
||||
|
||||
/**
|
||||
* @brief Preprocessing result metadata
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t *pValidData;
|
||||
uint32_t DataLength;
|
||||
uint16_t ValidWidth;
|
||||
uint16_t ValidHeight;
|
||||
int16_t MinTemp;
|
||||
int16_t MaxTemp;
|
||||
int16_t AvgTemp;
|
||||
int16_t RoiTemp;
|
||||
uint8_t Status;
|
||||
uint32_t FrameNumber;
|
||||
} PreprocessResult_t;
|
||||
|
||||
/**
|
||||
* @brief Buffer wrapper for Zero-Copy TCP packet building
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t *pBuffer;
|
||||
uint32_t TotalCapacity;
|
||||
uint32_t HeadOffset; /* Reserved header space for envelope */
|
||||
uint32_t ValidPayloadLen;
|
||||
} TcpTxBuffer_t;
|
||||
|
||||
/* System global config hook types */
|
||||
typedef void (*ConfigUpdateCallback_t)(const ConfigCommon_t *common,
|
||||
const Config2D_t *cfg2d,
|
||||
const Config1D_t *cfg1d);
|
||||
typedef void (*DetectionResultCallback_t)(uint32_t frameNumber,
|
||||
uint8_t resultStatus);
|
||||
|
||||
/* ============================================================
|
||||
* Protocol Utility Function Declarations
|
||||
* ============================================================ */
|
||||
|
||||
/* Calculate Modbus CRC16 */
|
||||
uint16_t qdx_crc16_modbus(const uint8_t *data, int len);
|
||||
|
||||
/* Shift-safe write: 16-bit little-endian */
|
||||
void qdx_write_u16_le(uint8_t *buf, uint16_t val);
|
||||
|
||||
/* Shift-safe write: 32-bit little-endian */
|
||||
void qdx_write_u32_le(uint8_t *buf, uint32_t val);
|
||||
|
||||
/* Shift-safe read: 16-bit little-endian */
|
||||
uint16_t qdx_read_u16_le(const uint8_t *buf);
|
||||
|
||||
/* Shift-safe read: 32-bit little-endian */
|
||||
uint32_t qdx_read_u32_le(const uint8_t *buf);
|
||||
|
||||
/**
|
||||
* @brief Build a complete protocol frame using safe byte shifts.
|
||||
*/
|
||||
int qdx_build_frame(uint8_t *buf, uint8_t msg_class, uint8_t tlv_type,
|
||||
const uint8_t *payload, uint16_t payload_len,
|
||||
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
|
||||
uint8_t flags);
|
||||
|
||||
/**
|
||||
* @brief In-place frame build: 仅填写帧头并追加 CRC,不拷贝 payload。
|
||||
* 用于零拷贝场景,payload(含 TLV)已在 buf[HEADER_SIZE] 处就位。
|
||||
*/
|
||||
int qdx_build_frame_inplace(uint8_t *buf, uint8_t msg_class,
|
||||
uint16_t payload_len, uint16_t dev_id, uint16_t seq,
|
||||
uint32_t timestamp, uint8_t flags);
|
||||
|
||||
/**
|
||||
* @brief Build a fragmented payload frame (No TLV wrapper).
|
||||
*/
|
||||
int qdx_build_fragment_frame(uint8_t *buf, uint8_t msg_class,
|
||||
const uint8_t *chunk, uint16_t chunk_len,
|
||||
uint16_t dev_id, uint16_t seq, uint32_t timestamp,
|
||||
uint8_t flags);
|
||||
|
||||
#endif /* QDX_PROTOCOL_H */
|
||||
694
prj/middle/QDXnetworkStack/qdx_tcp_logic.c
Normal file
694
prj/middle/QDXnetworkStack/qdx_tcp_logic.c
Normal file
@ -0,0 +1,694 @@
|
||||
/**
|
||||
* @file qdx_tcp_logic.c
|
||||
* @brief TCP Network Logic Implementation for MCU
|
||||
*/
|
||||
|
||||
#include "qdx_tcp_logic.h"
|
||||
#include "qdx_port.h"
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ============================================================
|
||||
* Internal State & Configuration Cache
|
||||
* ============================================================ */
|
||||
|
||||
#define RECV_BUF_SIZE 2048
|
||||
#define MAX_FRAGMENT_PAYLOAD 1400
|
||||
#define HEARTBEAT_INTERVAL_MS 2000
|
||||
#define SERVER_TIMEOUT_MS 6000
|
||||
#define RECONNECT_DELAY_MS 3000
|
||||
|
||||
typedef struct {
|
||||
qdx_socket_t sock;
|
||||
uint32_t last_activity_ms;
|
||||
uint32_t last_heartbeat_ms;
|
||||
uint32_t last_reconnect_ms;
|
||||
uint16_t sequence;
|
||||
uint8_t is_connected;
|
||||
uint8_t is_running;
|
||||
const char *label;
|
||||
uint8_t recv_buffer[RECV_BUF_SIZE * 2];
|
||||
uint32_t recv_len;
|
||||
} TcpStreamCtx_t;
|
||||
|
||||
static struct {
|
||||
uint8_t uuid[16];
|
||||
uint8_t auth_token[16];
|
||||
uint16_t dev_id;
|
||||
int32_t pending_new_dev_id;
|
||||
uint32_t frame_count;
|
||||
|
||||
TcpStreamCtx_t control_stream;
|
||||
TcpStreamCtx_t data_stream;
|
||||
|
||||
qdx_mutex_t config_mutex;
|
||||
uint8_t has_valid_config;
|
||||
ConfigCommon_t cached_common;
|
||||
Config2D_t cached_cfg2d;
|
||||
Config1D_t cached_cfg1d;
|
||||
|
||||
ConfigUpdateCallback_t config_cb;
|
||||
DetectionResultCallback_t detect_cb;
|
||||
TempFrameRequestCallback_t temp_req_cb;
|
||||
} g_TcpLogic;
|
||||
|
||||
/* Server endpoint prototype - user would configure these, but we map to demo
|
||||
* defaults */
|
||||
static const char *SERVER_IP = "127.0.0.1";
|
||||
static const uint16_t CONTROL_PORT = 5511;
|
||||
static const uint16_t DATA_PORT = 5512;
|
||||
|
||||
/* ============================================================
|
||||
* Internal Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static void tcp_stream_init(TcpStreamCtx_t *ctx, const char *label) {
|
||||
memset(ctx, 0, sizeof(TcpStreamCtx_t));
|
||||
ctx->label = label;
|
||||
}
|
||||
|
||||
static void tcp_stream_disconnect(TcpStreamCtx_t *ctx) {
|
||||
ctx->is_connected = 0;
|
||||
if (ctx->sock) {
|
||||
qdx_port_tcp_close(ctx->sock);
|
||||
ctx->sock = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static int8_t tcp_stream_connect(TcpStreamCtx_t *ctx, const char *ip,
|
||||
uint16_t port) {
|
||||
ctx->sock = qdx_port_tcp_connect(ip, port);
|
||||
if (ctx->sock == NULL)
|
||||
return -1;
|
||||
|
||||
ctx->is_connected = 1;
|
||||
ctx->last_activity_ms = qdx_port_get_tick_ms();
|
||||
ctx->last_heartbeat_ms = ctx->last_activity_ms;
|
||||
ctx->recv_len = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t tcp_send_frame(TcpStreamCtx_t *ctx, uint8_t msg_class,
|
||||
uint8_t tlv_type, const uint8_t *payload,
|
||||
uint16_t payload_len, uint8_t flags) {
|
||||
if (!ctx->is_connected || ctx->sock == NULL)
|
||||
return -1;
|
||||
|
||||
/* Max frame buffer for generic small control/heartbeat messages.
|
||||
Note: Images use BuildAndSendTemperatureFrame instead of this. */
|
||||
uint8_t buffer[1024];
|
||||
if (HEADER_SIZE + TLV_HEADER_SIZE + payload_len + CRC_SIZE > sizeof(buffer)) {
|
||||
return -1; /* Too large for generic send */
|
||||
}
|
||||
|
||||
uint16_t seq = ctx->sequence++;
|
||||
int frame_len =
|
||||
qdx_build_frame(buffer, msg_class, tlv_type, payload, payload_len,
|
||||
g_TcpLogic.dev_id, seq, qdx_port_get_tick_ms(), flags);
|
||||
|
||||
int32_t sent = qdx_port_tcp_send(ctx->sock, buffer, frame_len);
|
||||
if (sent < 0) {
|
||||
tcp_stream_disconnect(ctx);
|
||||
return -1;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
static void tcp_send_handshake(TcpStreamCtx_t *ctx) {
|
||||
uint8_t payload[54];
|
||||
memset(payload, 0, sizeof(payload));
|
||||
qdx_write_u16_le(payload + 0, 0x0200);
|
||||
memcpy(payload + 2, g_TcpLogic.uuid, 16);
|
||||
memcpy(payload + 18, g_TcpLogic.auth_token, 16);
|
||||
/* Safe string copy without relying on strncpy platform behavior */
|
||||
const char *hw = "V1.0";
|
||||
const char *fw = "V2.0";
|
||||
for (int i = 0; i < 8 && hw[i]; i++)
|
||||
payload[34 + i] = hw[i];
|
||||
for (int i = 0; i < 8 && fw[i]; i++)
|
||||
payload[42 + i] = fw[i];
|
||||
qdx_write_u32_le(payload + 50, 0x07);
|
||||
|
||||
tcp_send_frame(ctx, CLASS_SYSTEM, TYPE_HANDSHAKE, payload, sizeof(payload),
|
||||
FLAG_ACK_REQ);
|
||||
}
|
||||
|
||||
static void tcp_send_heartbeat(TcpStreamCtx_t *ctx) {
|
||||
uint8_t payload[6];
|
||||
qdx_write_u32_le(payload + 0, qdx_port_get_tick_ms());
|
||||
payload[4] = 10; /* Placeholder CpuLoad */
|
||||
payload[5] = 20; /* Placeholder MemUsage */
|
||||
|
||||
tcp_send_frame(ctx, CLASS_SYSTEM, TYPE_HEARTBEAT, payload, sizeof(payload),
|
||||
0);
|
||||
}
|
||||
|
||||
static void tcp_send_ack(TcpStreamCtx_t *ctx, uint16_t ack_seq, uint8_t status,
|
||||
uint16_t error_code) {
|
||||
uint8_t payload[5];
|
||||
qdx_write_u16_le(payload + 0, ack_seq);
|
||||
payload[2] = status;
|
||||
qdx_write_u16_le(payload + 3, error_code);
|
||||
|
||||
tcp_send_frame(ctx, CLASS_RESPONSE, TYPE_ACK_PAYLOAD, payload,
|
||||
sizeof(payload), 0);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Receiving and Parsing
|
||||
* ============================================================ */
|
||||
|
||||
static void qdx_deserialize_config_common(ConfigCommon_t *cfg,
|
||||
const uint8_t *val) {
|
||||
/* PipelineId: 16 字节字符数组,逐字节拷贝 */
|
||||
for (int i = 0; i < 16; i++)
|
||||
cfg->PipelineId[i] = (char)val[i];
|
||||
cfg->PipelineType = val[16];
|
||||
cfg->WorkMode = val[17];
|
||||
cfg->ConfigTag = val[18];
|
||||
cfg->StrictnessLevel = val[19];
|
||||
cfg->IsCustomMode = val[20];
|
||||
cfg->Reserved[0] = val[21];
|
||||
cfg->Reserved[1] = val[22];
|
||||
}
|
||||
|
||||
static void qdx_deserialize_config2d(Config2D_t *cfg, const uint8_t *val) {
|
||||
cfg->Enabled = val[0];
|
||||
cfg->IsLive = val[1];
|
||||
cfg->DeviceId = qdx_read_u16_le(val + 2);
|
||||
cfg->Width = qdx_read_u16_le(val + 4);
|
||||
cfg->Height = qdx_read_u16_le(val + 6);
|
||||
cfg->Fps = val[8];
|
||||
cfg->Exposure = qdx_read_u32_le(val + 9);
|
||||
cfg->AutoExposure = val[13];
|
||||
cfg->MaskEnabled = val[14];
|
||||
cfg->MaskThreshold = (int16_t)qdx_read_u16_le(val + 15);
|
||||
cfg->MaskWidth = qdx_read_u16_le(val + 17);
|
||||
cfg->MaskHeight = qdx_read_u16_le(val + 19);
|
||||
cfg->Angle = (int16_t)qdx_read_u16_le(val + 21);
|
||||
cfg->TargetWidth = qdx_read_u16_le(val + 23);
|
||||
cfg->TargetHeight = qdx_read_u16_le(val + 25);
|
||||
cfg->TriggerMode = val[27];
|
||||
cfg->TriggerGpioLine = val[28];
|
||||
cfg->TriggerDelayMs = qdx_read_u16_le(val + 29);
|
||||
cfg->TriggerBurstCount = val[31];
|
||||
cfg->TriggerInternalIntervalMs = qdx_read_u16_le(val + 32);
|
||||
cfg->TriggerTemperatureThreshold = (int16_t)qdx_read_u16_le(val + 34);
|
||||
cfg->TriggerDebounceIntervalMs = qdx_read_u16_le(val + 36);
|
||||
cfg->TriggerCondition = val[38];
|
||||
cfg->TriggerRoiX = qdx_read_u16_le(val + 39);
|
||||
cfg->TriggerRoiY = qdx_read_u16_le(val + 41);
|
||||
cfg->TriggerRoiW = qdx_read_u16_le(val + 43);
|
||||
cfg->TriggerRoiH = qdx_read_u16_le(val + 45);
|
||||
cfg->NGioDelay = qdx_read_u16_le(val + 47);
|
||||
cfg->OutputGpioLine = val[49];
|
||||
cfg->AlarmGpioLine = val[50];
|
||||
cfg->AlarmHoldMs = qdx_read_u16_le(val + 51);
|
||||
cfg->StoreNgImagesOnly = val[53];
|
||||
cfg->TrainingEnabled = val[54];
|
||||
cfg->TrainingSampleThreshold = qdx_read_u16_le(val + 55);
|
||||
cfg->ProcessingTimeoutMs = qdx_read_u16_le(val + 57);
|
||||
cfg->MaxProcessingQueueSize = val[59];
|
||||
cfg->Reserved = val[60];
|
||||
}
|
||||
|
||||
static void qdx_deserialize_config1d(Config1D_t *cfg, const uint8_t *val) {
|
||||
cfg->Enabled = val[0];
|
||||
cfg->RunMode = val[1];
|
||||
cfg->TriggerType = val[2];
|
||||
cfg->BufferSize = qdx_read_u16_le(val + 3);
|
||||
cfg->TriggerTempLimit = (int16_t)qdx_read_u16_le(val + 5);
|
||||
cfg->StartPointsToRemove = qdx_read_u16_le(val + 7);
|
||||
cfg->ReferenceLength = qdx_read_u16_le(val + 9);
|
||||
cfg->HighTimerLimit = qdx_read_u16_le(val + 11);
|
||||
cfg->TimerCLimit = qdx_read_u16_le(val + 13);
|
||||
cfg->NgCountLimit = val[15];
|
||||
cfg->LSizeStart = qdx_read_u16_le(val + 16);
|
||||
cfg->RSizeStart = qdx_read_u16_le(val + 18);
|
||||
cfg->NGioDelay = qdx_read_u16_le(val + 20);
|
||||
cfg->OutputGpioLine = val[22];
|
||||
cfg->AlarmGpioLine = val[23];
|
||||
cfg->AlarmHoldMs = qdx_read_u16_le(val + 24);
|
||||
}
|
||||
|
||||
static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
|
||||
uint16_t pkt_len) {
|
||||
uint16_t hdr_seq = qdx_read_u16_le(packet + 5);
|
||||
uint8_t hdr_flags = packet[15];
|
||||
|
||||
/* TLV Data starts after header (16 bytes) */
|
||||
int offset = HEADER_SIZE;
|
||||
int payload_len = pkt_len - HEADER_SIZE - CRC_SIZE;
|
||||
int parsed_len = 0;
|
||||
|
||||
uint8_t cfg_updated = 0;
|
||||
|
||||
//printf("\n[DEBUG][%s] 收到 TLV 包: Seq=%d, PayloadLen=%d\n", ctx->label,
|
||||
// hdr_seq, payload_len);
|
||||
|
||||
while (parsed_len <= payload_len - 3) {
|
||||
uint8_t type = packet[offset];
|
||||
uint16_t len = qdx_read_u16_le(packet + offset + 1);
|
||||
|
||||
//printf("[DEBUG][%s] -> 解析 TLV: Type=0x%02X, Len=%d\n", ctx->label, type,
|
||||
//len);
|
||||
|
||||
if (parsed_len + 3 + len > payload_len) {
|
||||
//printf("[DEBUG][%s] ! 结构错误: 剩余长度不足 (需 %d, 剩 %d)\n",
|
||||
//ctx->label, len, payload_len - parsed_len - 3);
|
||||
break; /* Malformed */
|
||||
}
|
||||
|
||||
const uint8_t *value = packet + offset + 3;
|
||||
|
||||
switch (type) {
|
||||
case TYPE_DEVID_ASSIGN: {
|
||||
if (len >= sizeof(DevIDAssignment_t)) {
|
||||
uint16_t new_id = qdx_read_u16_le(value);
|
||||
g_TcpLogic.pending_new_dev_id = new_id;
|
||||
tcp_send_ack(ctx, hdr_seq, 0, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_CONFIG_COMMON: {
|
||||
if (len >= sizeof(ConfigCommon_t)) {
|
||||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||||
qdx_deserialize_config_common(&g_TcpLogic.cached_common, value);
|
||||
g_TcpLogic.has_valid_config = 1;
|
||||
cfg_updated = 1;
|
||||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_CONFIG_2D: {
|
||||
if (len >= sizeof(Config2D_t)) {
|
||||
//printf("[DEBUG][%s] * 解析 Config2D 成功\n", ctx->label);
|
||||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||||
qdx_deserialize_config2d(&g_TcpLogic.cached_cfg2d, value);
|
||||
g_TcpLogic.has_valid_config = 1; /* 任意配置到达即标记有效 */
|
||||
cfg_updated = 1;
|
||||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||||
} else {
|
||||
//printf("[DEBUG][%s] ! Config2D 长度不对: len=%d, sizeof=%d\n",
|
||||
//ctx->label, len, (int)sizeof(Config2D_t));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_CONFIG_1D: {
|
||||
if (len >= sizeof(Config1D_t)) {
|
||||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||||
qdx_deserialize_config1d(&g_TcpLogic.cached_cfg1d, value);
|
||||
g_TcpLogic.has_valid_config = 1; /* 任意配置到达即标记有效 */
|
||||
cfg_updated = 1;
|
||||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_TEMP_FRAME: {
|
||||
if (g_TcpLogic.temp_req_cb) {
|
||||
/* If payload length is >= 18 (TemperatureFrameHeader_t), we can peek
|
||||
at Is2D. Otherwise we pass 0 or a default value. For now let's pass
|
||||
an indicator if Is2D is set. */
|
||||
uint8_t is2d = 0;
|
||||
if (len >= 18) {
|
||||
is2d = value[18]; /* index 18 in TemperatureFrameHeader_t is Is2D */
|
||||
}
|
||||
g_TcpLogic.temp_req_cb(is2d);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TYPE_DETECTION_RESULT: {
|
||||
if (len >= sizeof(DetectionResult_t) && g_TcpLogic.detect_cb) {
|
||||
uint32_t frame_num = qdx_read_u32_le(value);
|
||||
uint8_t result_status = value[4];
|
||||
g_TcpLogic.detect_cb(frame_num, result_status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
offset += (3 + len);
|
||||
parsed_len += (3 + len);
|
||||
}
|
||||
|
||||
if (cfg_updated && g_TcpLogic.config_cb && g_TcpLogic.has_valid_config) {
|
||||
/* Safely trigger callback. Passing pointers to cached config is ok
|
||||
during the context of this thread, but user must copy if they
|
||||
dispatch to another task. */
|
||||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||||
g_TcpLogic.config_cb(&g_TcpLogic.cached_common, &g_TcpLogic.cached_cfg2d,
|
||||
&g_TcpLogic.cached_cfg1d);
|
||||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||||
}
|
||||
|
||||
if (hdr_flags & FLAG_ACK_REQ) {
|
||||
tcp_send_ack(ctx, hdr_seq, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void tcp_process_rx_buffer(TcpStreamCtx_t *ctx) {
|
||||
while (ctx->recv_len >= HEADER_SIZE) {
|
||||
/* 1. Search for Magic 0x55AA */
|
||||
int start_idx = -1;
|
||||
for (uint32_t i = 0; i <= ctx->recv_len - 2; i++) {
|
||||
if (ctx->recv_buffer[i] == 0xAA && ctx->recv_buffer[i + 1] == 0x55) {
|
||||
start_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (start_idx == -1) {
|
||||
ctx->recv_buffer[0] = ctx->recv_buffer[ctx->recv_len - 1];
|
||||
ctx->recv_len = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (start_idx > 0) {
|
||||
/* 缓冲区内部左移,源与目标重叠,必须使用 memmove */
|
||||
memmove(ctx->recv_buffer, ctx->recv_buffer + start_idx,
|
||||
ctx->recv_len - start_idx);
|
||||
ctx->recv_len -= start_idx;
|
||||
if (ctx->recv_len < HEADER_SIZE)
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t version = ctx->recv_buffer[2];
|
||||
uint16_t length = qdx_read_u16_le(ctx->recv_buffer + 3);
|
||||
|
||||
if (version != PROTO_VERSION || length < HEADER_SIZE + CRC_SIZE) {
|
||||
/* 丢弃 Magic 标记,缓冲区内部左移 2 字节 */
|
||||
//printf("\n[DEBUG][%s] 错误: Header 验证失败! Version=0x%02X "
|
||||
//"(Expected=0x%02X), Length=%d\n",
|
||||
//ctx->label, version, PROTO_VERSION, length);
|
||||
memmove(ctx->recv_buffer, ctx->recv_buffer + 2, ctx->recv_len - 2);
|
||||
ctx->recv_len -= 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (length > sizeof(ctx->recv_buffer)) {
|
||||
/* Frame too large, drop entirely */
|
||||
//printf("\n[DEBUG][%s] 错误: 帧长度超限 (length=%d, max=%d)\n", ctx->label,
|
||||
//length, (int)sizeof(ctx->recv_buffer));
|
||||
ctx->recv_len = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ctx->recv_len < length) {
|
||||
break; /* Need more data */
|
||||
}
|
||||
|
||||
/* 2. Validate CRC */
|
||||
uint16_t received_crc = qdx_read_u16_le(ctx->recv_buffer + length - 2);
|
||||
uint16_t calculated_crc = qdx_crc16_modbus(ctx->recv_buffer, length - 2);
|
||||
|
||||
if (received_crc == calculated_crc) {
|
||||
/* 3. Dispatch */
|
||||
parse_and_dispatch_tlv(ctx, ctx->recv_buffer, length);
|
||||
} else {
|
||||
//printf("\n[DEBUG][%s] 错误: CRC 校验失败! Calc=0x%04X, Recv=0x%04X "
|
||||
//"(Len=%d)\n",
|
||||
//ctx->label, calculated_crc, received_crc, length);
|
||||
}
|
||||
|
||||
/* 4. 移除已处理帧(缓冲区内部左移,必须 memmove) */
|
||||
memmove(ctx->recv_buffer, ctx->recv_buffer + length,
|
||||
ctx->recv_len - length);
|
||||
ctx->recv_len -= length;
|
||||
}
|
||||
}
|
||||
|
||||
static void recv_thread_entry(void *arg) {
|
||||
TcpStreamCtx_t *ctx = (TcpStreamCtx_t *)arg;
|
||||
|
||||
while (ctx->is_running) {
|
||||
if (!ctx->is_connected) {
|
||||
qdx_port_delay_ms(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Leave space for maximum TCP MTU read */
|
||||
if (sizeof(ctx->recv_buffer) - ctx->recv_len > 0) {
|
||||
int32_t bytes =
|
||||
qdx_port_tcp_recv(ctx->sock, ctx->recv_buffer + ctx->recv_len,
|
||||
sizeof(ctx->recv_buffer) - ctx->recv_len);
|
||||
|
||||
if (bytes > 0) {
|
||||
ctx->recv_len += bytes;
|
||||
ctx->last_activity_ms = qdx_port_get_tick_ms();
|
||||
tcp_process_rx_buffer(ctx);
|
||||
} else if (bytes < 0) {
|
||||
/* Disconnected / Error */
|
||||
tcp_stream_disconnect(ctx);
|
||||
}
|
||||
} else {
|
||||
/* Buffer completely full but no valid packet found. Prevent overflow
|
||||
* lock. */
|
||||
ctx->recv_len = 0;
|
||||
}
|
||||
|
||||
qdx_port_delay_ms(10);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Main Background Manager
|
||||
* ============================================================ */
|
||||
|
||||
static void manager_thread_entry(void *arg) {
|
||||
while (1) {
|
||||
uint32_t now = qdx_port_get_tick_ms();
|
||||
|
||||
/* DevID Reassignment Handling */
|
||||
if (g_TcpLogic.pending_new_dev_id >= 0) {
|
||||
g_TcpLogic.dev_id = (uint16_t)g_TcpLogic.pending_new_dev_id;
|
||||
g_TcpLogic.pending_new_dev_id = -1;
|
||||
|
||||
tcp_stream_disconnect(&g_TcpLogic.control_stream);
|
||||
tcp_stream_disconnect(&g_TcpLogic.data_stream);
|
||||
|
||||
g_TcpLogic.control_stream.last_reconnect_ms = 0;
|
||||
g_TcpLogic.data_stream.last_reconnect_ms = 0;
|
||||
qdx_port_delay_ms(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Connection Management: Control Stream */
|
||||
if (!g_TcpLogic.control_stream.is_connected) {
|
||||
if (now - g_TcpLogic.control_stream.last_reconnect_ms >
|
||||
RECONNECT_DELAY_MS) {
|
||||
if (tcp_stream_connect(&g_TcpLogic.control_stream, SERVER_IP,
|
||||
CONTROL_PORT) == 0) {
|
||||
tcp_send_handshake(&g_TcpLogic.control_stream);
|
||||
}
|
||||
g_TcpLogic.control_stream.last_reconnect_ms = qdx_port_get_tick_ms();
|
||||
}
|
||||
}
|
||||
|
||||
/* Connection Management: Data Stream */
|
||||
if (!g_TcpLogic.data_stream.is_connected) {
|
||||
if (now - g_TcpLogic.data_stream.last_reconnect_ms > RECONNECT_DELAY_MS) {
|
||||
if (tcp_stream_connect(&g_TcpLogic.data_stream, SERVER_IP, DATA_PORT) ==
|
||||
0) {
|
||||
tcp_send_handshake(&g_TcpLogic.data_stream);
|
||||
}
|
||||
g_TcpLogic.data_stream.last_reconnect_ms = qdx_port_get_tick_ms();
|
||||
}
|
||||
}
|
||||
|
||||
/* Heartbeat & Timeout checks */
|
||||
if (g_TcpLogic.control_stream.is_connected) {
|
||||
if (now - g_TcpLogic.control_stream.last_heartbeat_ms >
|
||||
HEARTBEAT_INTERVAL_MS) {
|
||||
tcp_send_heartbeat(&g_TcpLogic.control_stream);
|
||||
g_TcpLogic.control_stream.last_heartbeat_ms = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_TcpLogic.data_stream.is_connected) {
|
||||
if (now - g_TcpLogic.data_stream.last_heartbeat_ms >
|
||||
HEARTBEAT_INTERVAL_MS) {
|
||||
tcp_send_heartbeat(&g_TcpLogic.data_stream);
|
||||
g_TcpLogic.data_stream.last_heartbeat_ms = now;
|
||||
}
|
||||
}
|
||||
|
||||
qdx_port_delay_ms(100);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API Implementations
|
||||
* ============================================================ */
|
||||
|
||||
int8_t TcpLogic_Init(const uint8_t *deviceUUID, const uint8_t *authToken) {
|
||||
memset(&g_TcpLogic, 0, sizeof(g_TcpLogic));
|
||||
|
||||
if (deviceUUID)
|
||||
memcpy(g_TcpLogic.uuid, deviceUUID, 16);
|
||||
if (authToken)
|
||||
memcpy(g_TcpLogic.auth_token, authToken, 16);
|
||||
|
||||
/* Default DevID = 101 */
|
||||
g_TcpLogic.dev_id = 101;
|
||||
g_TcpLogic.pending_new_dev_id = -1;
|
||||
|
||||
g_TcpLogic.config_mutex = qdx_port_mutex_create();
|
||||
if (g_TcpLogic.config_mutex == NULL)
|
||||
return -1;
|
||||
|
||||
tcp_stream_init(&g_TcpLogic.control_stream, "Control");
|
||||
tcp_stream_init(&g_TcpLogic.data_stream, "Data");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TcpLogic_Start(void) {
|
||||
g_TcpLogic.control_stream.is_running = 1;
|
||||
g_TcpLogic.data_stream.is_running = 1;
|
||||
|
||||
qdx_port_thread_create("tcp_mgr", manager_thread_entry, NULL, 2048, 3);
|
||||
qdx_port_thread_create("tcp_rx_c", recv_thread_entry,
|
||||
&g_TcpLogic.control_stream, 2048, 4);
|
||||
qdx_port_thread_create("tcp_rx_d", recv_thread_entry, &g_TcpLogic.data_stream,
|
||||
2048, 4);
|
||||
}
|
||||
|
||||
int8_t TcpLogic_GetLatestConfig(ConfigCommon_t *out_common,
|
||||
Config2D_t *out_cfg2d, Config1D_t *out_cfg1d) {
|
||||
if (!out_common || !out_cfg2d || !out_cfg1d)
|
||||
return -2;
|
||||
|
||||
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
|
||||
if (!g_TcpLogic.has_valid_config) {
|
||||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(out_common, &g_TcpLogic.cached_common, sizeof(ConfigCommon_t));
|
||||
memcpy(out_cfg2d, &g_TcpLogic.cached_cfg2d, sizeof(Config2D_t));
|
||||
memcpy(out_cfg1d, &g_TcpLogic.cached_cfg1d, sizeof(Config1D_t));
|
||||
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TcpLogic_RegisterConfigCallback(ConfigUpdateCallback_t cb) {
|
||||
g_TcpLogic.config_cb = cb;
|
||||
}
|
||||
|
||||
void TcpLogic_RegisterDetectionCallback(DetectionResultCallback_t cb) {
|
||||
g_TcpLogic.detect_cb = cb;
|
||||
}
|
||||
|
||||
void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb) {
|
||||
g_TcpLogic.temp_req_cb = cb;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Zero-Copy Frame Building & Fragmentation for Temperature Data
|
||||
* ============================================================ */
|
||||
|
||||
int8_t
|
||||
TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t *io_buffer,
|
||||
const PreprocessResult_t *processMeta,
|
||||
uint8_t frameType, uint8_t is2D) {
|
||||
|
||||
if (!g_TcpLogic.data_stream.is_connected || !io_buffer || !processMeta)
|
||||
return -1;
|
||||
if (io_buffer->ValidPayloadLen == 0)
|
||||
return -2;
|
||||
|
||||
g_TcpLogic.frame_count++;
|
||||
|
||||
/* We need to prepend: TLV Header (3) + TemperatureFrameHeader_t (18) */
|
||||
uint32_t tlv_wrapper_len = TLV_HEADER_SIZE + sizeof(TemperatureFrameHeader_t);
|
||||
|
||||
/* Ensure application left enough head room */
|
||||
if (io_buffer->HeadOffset < HEADER_SIZE + tlv_wrapper_len) {
|
||||
return -3; /* Not enough offset space allocated by user memory pool */
|
||||
}
|
||||
|
||||
/* Start writing right before the application payload */
|
||||
uint8_t *tlv_start =
|
||||
io_buffer->pBuffer + io_buffer->HeadOffset - tlv_wrapper_len;
|
||||
|
||||
/* 1. Fill TLV Header manually via shift */
|
||||
uint32_t tlv_value_len =
|
||||
sizeof(TemperatureFrameHeader_t) + io_buffer->ValidPayloadLen;
|
||||
tlv_start[0] = TYPE_TEMP_FRAME;
|
||||
qdx_write_u16_le(tlv_start + 1, (uint16_t)tlv_value_len);
|
||||
|
||||
/* 2. Fill TemperatureFrameHeader manually via shift to avoid alignment
|
||||
* faults
|
||||
*/
|
||||
uint8_t *temp_hdr = tlv_start + TLV_HEADER_SIZE;
|
||||
qdx_write_u32_le(temp_hdr + 0, processMeta->FrameNumber);
|
||||
qdx_write_u16_le(temp_hdr + 4, processMeta->ValidWidth);
|
||||
qdx_write_u16_le(temp_hdr + 6, processMeta->ValidHeight);
|
||||
qdx_write_u16_le(temp_hdr + 8, (uint16_t)processMeta->MinTemp);
|
||||
qdx_write_u16_le(temp_hdr + 10, (uint16_t)processMeta->MaxTemp);
|
||||
qdx_write_u16_le(temp_hdr + 12, (uint16_t)processMeta->AvgTemp);
|
||||
qdx_write_u16_le(temp_hdr + 14, (uint16_t)processMeta->RoiTemp);
|
||||
temp_hdr[16] = frameType;
|
||||
temp_hdr[17] = processMeta->Status;
|
||||
temp_hdr[18] = is2D;
|
||||
temp_hdr[19] = 0; /* Reserved */
|
||||
|
||||
/* Total payload length is the entire TLV block */
|
||||
uint32_t total_tlv_len = TLV_HEADER_SIZE + tlv_value_len;
|
||||
|
||||
/* 3. Handle Fragmentation if necessary */
|
||||
if (total_tlv_len <= MAX_FRAGMENT_PAYLOAD) {
|
||||
/* No fragmentation needed, build frame in place at the front */
|
||||
uint8_t *frame_start = tlv_start - HEADER_SIZE;
|
||||
uint16_t seq = g_TcpLogic.data_stream.sequence++;
|
||||
|
||||
int final_len = qdx_build_frame_inplace(
|
||||
frame_start, CLASS_DATA, (uint16_t)total_tlv_len, g_TcpLogic.dev_id,
|
||||
seq, qdx_port_get_tick_ms(), 0);
|
||||
|
||||
int32_t sent =
|
||||
qdx_port_tcp_send(g_TcpLogic.data_stream.sock, frame_start, final_len);
|
||||
return (sent >= 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Fragmentation required.
|
||||
Note: For zero-copy fragmentation, we send piece by piece.
|
||||
We need an external buffer for each piece's frame header + CRC.
|
||||
We can't easily prepend headers to later fragments inline. */
|
||||
|
||||
uint32_t offset = 0;
|
||||
uint8_t frag_buf[HEADER_SIZE + MAX_FRAGMENT_PAYLOAD + CRC_SIZE];
|
||||
uint32_t frag_count =
|
||||
(total_tlv_len + MAX_FRAGMENT_PAYLOAD - 1) / MAX_FRAGMENT_PAYLOAD;
|
||||
|
||||
for (uint32_t i = 0; i < frag_count; i++) {
|
||||
uint32_t chunk_len = total_tlv_len - offset;
|
||||
if (chunk_len > MAX_FRAGMENT_PAYLOAD)
|
||||
chunk_len = MAX_FRAGMENT_PAYLOAD;
|
||||
|
||||
uint8_t flags = (i == frag_count - 1) ? FLAG_LAST_FRAGMENT : 0;
|
||||
uint16_t seq = g_TcpLogic.data_stream.sequence++;
|
||||
|
||||
/* We copy the chunk into frag_buf to append Header/CRC.
|
||||
This involves ONE copy of the chunk, but it's small (1400 bytes at a
|
||||
time), and ensures we don't need additional memory pools. */
|
||||
int frame_len = qdx_build_fragment_frame(
|
||||
frag_buf, CLASS_DATA, tlv_start + offset, (uint16_t)chunk_len,
|
||||
g_TcpLogic.dev_id, seq, qdx_port_get_tick_ms(), flags);
|
||||
|
||||
int32_t sent =
|
||||
qdx_port_tcp_send(g_TcpLogic.data_stream.sock, frag_buf, frame_len);
|
||||
if (sent < 0) {
|
||||
tcp_stream_disconnect(&g_TcpLogic.data_stream);
|
||||
return -1;
|
||||
}
|
||||
|
||||
offset += chunk_len;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
112
prj/middle/QDXnetworkStack/qdx_tcp_logic.h
Normal file
112
prj/middle/QDXnetworkStack/qdx_tcp_logic.h
Normal file
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @file qdx_tcp_logic.h
|
||||
* @brief Zero-Copy TCP Network Logic and State Machine tailored for MCU
|
||||
*
|
||||
* Implements connection management (Dual Stream 5511/5512),
|
||||
* packet handling, heartbeating, config caching, and callbacks.
|
||||
*/
|
||||
|
||||
#ifndef QDX_TCP_LOGIC_H
|
||||
#define QDX_TCP_LOGIC_H
|
||||
|
||||
#include "qdx_protocol.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize the TCP logic module
|
||||
*
|
||||
* @param deviceUUID Provide standard UUID for the device.
|
||||
* @param authToken Optional auth token for connections.
|
||||
* @return 0 on success, < 0 on failure.
|
||||
*/
|
||||
int8_t TcpLogic_Init(const uint8_t *deviceUUID, const uint8_t *authToken);
|
||||
|
||||
/**
|
||||
* @brief Wait for and start non-blocking network connection attempts.
|
||||
*
|
||||
* It will spawn background threads for Control and Data streams
|
||||
* connecting to the server.
|
||||
*/
|
||||
void TcpLogic_Start(void);
|
||||
|
||||
/**
|
||||
* @brief Encapsulate and send a temperature frame without generic memory copy.
|
||||
*
|
||||
* Uses the pre-filled `TcpTxBuffer_t` (containing image array) to prepend
|
||||
* the required nested headers directly into the buffer offset.
|
||||
*
|
||||
* @param io_buffer The user-allocated buffer containing Image payload starting
|
||||
* at HeadOffset
|
||||
* @param processMeta Real-time analysis metadata for the image data
|
||||
* @param frameType Frame type: 0=LIVE, 1=TRIGGER, 2=MASKED
|
||||
* @param is2D 1 for 2D matrix, 0 for 1D array
|
||||
* @return 0 successfully sent to transmission queue, < 0 if failed
|
||||
*/
|
||||
int8_t
|
||||
TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t *io_buffer,
|
||||
const PreprocessResult_t *processMeta,
|
||||
uint8_t frameType, uint8_t is2D);
|
||||
|
||||
/**
|
||||
* @brief Retrieves a distinct copy of the latest active configuration
|
||||
* structure.
|
||||
*
|
||||
* Recommended for safe reads of configuration during offline or fallback
|
||||
* scenarios. Requires pointers to pre-allocated Config* structures.
|
||||
*
|
||||
* @param out_common Required pointer to common config structure.
|
||||
* @param out_cfg2d Required pointer to 2d config structure.
|
||||
* @param out_cfg1d Required pointer to 1d config structure.
|
||||
* @return 0 on success, -1 if no configuration has been stored or received.
|
||||
*/
|
||||
int8_t TcpLogic_GetLatestConfig(ConfigCommon_t *out_common,
|
||||
Config2D_t *out_cfg2d, Config1D_t *out_cfg1d);
|
||||
|
||||
/**
|
||||
* @brief Register configuration parsing event callback.
|
||||
*
|
||||
* Fired immediately after the host sends a Configuration payload and it is
|
||||
* safely cached to the internal registers.
|
||||
*
|
||||
* @param cb Callable function matching the interface
|
||||
*/
|
||||
void TcpLogic_RegisterConfigCallback(ConfigUpdateCallback_t cb);
|
||||
|
||||
/**
|
||||
* @brief Register remote decision consequence callback (e.g. Reject / Ok
|
||||
* processing result).
|
||||
*
|
||||
* Fired shortly after the host finishes processing a sent 2D or 1D target
|
||||
* frame.
|
||||
*
|
||||
* @param cb Callable function matching the interface
|
||||
*/
|
||||
void TcpLogic_RegisterDetectionCallback(DetectionResultCallback_t cb);
|
||||
|
||||
/**
|
||||
* @brief Callback for when host requests a temperature frame.
|
||||
*
|
||||
* Fired when the host sends a TYPE_TEMP_FRAME request (typically empty or
|
||||
* carrying trigger context). The device should capture an image and reply by
|
||||
* calling TcpLogic_BuildAndSendTemperatureFrame.
|
||||
*
|
||||
* @param is2dRequest Non-zero if request is specifically for 2D frame, zero if
|
||||
* for 1D.
|
||||
*/
|
||||
typedef void (*TempFrameRequestCallback_t)(uint8_t is2dRequest);
|
||||
|
||||
/**
|
||||
* @brief Register host temperature frame request callback.
|
||||
*
|
||||
* @param cb Callable function matching the interface
|
||||
*/
|
||||
void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* QDX_TCP_LOGIC_H */
|
||||
Loading…
x
Reference in New Issue
Block a user