This commit is contained in:
zhoujie 2026-03-14 20:54:11 +08:00
parent e3d08f7f26
commit b8dbaa619d
3 changed files with 1895 additions and 0 deletions

View File

@ -0,0 +1,120 @@
# 下位机协议实现完备性分析
> **分析日期**: 2026-03-14
> **对照文档**: 采集端通信协议规范 v2.1.3 (2026-02-07)
> **分析对象**: prj/TCPClient 工程全部源码
## 前提说明
协议规范描述了 ZMQ PUB-SUB + JSON 架构,但实际 MCU 与配置端服务器之间已约定采用**自定义二进制 TLV 帧格式**`0x55AA` Magic + CRC16-MODBUS通过双 TCP 通道5511 控制 / 5512 数据)通信。以下审计基于 TLV 协议层面进行。
---
## 一、接收端(服务器 → 下位机,控制通道 5511
| # | 功能 | 状态 | 说明 |
|---|------|:----:|------|
| 1 | ConfigCommon 全量配置 | ✅ 已实现 | `TYPE_CONFIG_COMMON(0x20)` 完整解析 23 字节 |
| 2 | Config2D 全量配置 | ✅ 已实现 | `TYPE_CONFIG_2D(0x22)` 完整解析 61 字节全部字段 |
| 3 | Config1D 全量配置 | ✅ 已实现 | `TYPE_CONFIG_1D(0x23)` 完整解析 26 字节 |
| 4 | 设备 ID 分配 | ✅ 已实现 | `TYPE_DEVID_ASSIGN(0x05)` 解析后断线重连 |
| 5 | 判定结果接收 | ✅ 已实现 | `TYPE_DETECTION_RESULT(0x40)``detect_cb` 回调 NG/OK |
| 6 | 帧请求(截图/采集) | ✅ 已实现 | `TYPE_TEMP_FRAME(0x10)` 请求 → `temp_req_cb` 回调 |
| 7 | ACK 响应接收 | ✅ 已实现 | `TYPE_ACK_PAYLOAD(0x30)` 在 TLV 解析循环内可识别 |
| 8 | 部分参数更新 | ❌ 未实现 | 规范 §4.2.2 定义 `UpdateParameters` 键值点更新,无对应 TLV 类型 |
| 9 | 控制命令组 | ❌ 未实现 | Start/Stop/Pause/Resume/GetStatus/GetConfiguration/SwitchPipelineMode/ResetStats 共 8 个控制命令均无解析 |
| 10 | 文件传输 | ❌ 未实现 | 规范 §3.3.1 `{deviceId}/file` 无对应功能 |
| 11 | 时间同步 | ⚠️ 仅定义 | `TYPE_SYNC_TIME(0x03)` 已定义常量但未在 `parse_and_dispatch_tlv` 中处理 |
---
## 二、发送端(下位机 → 服务器,数据通道 5512
| # | 功能 | 状态 | 说明 |
|---|------|:----:|------|
| 1 | 握手 | ✅ 已实现 | `TYPE_HANDSHAKE(0x01)` 连接后立即发送UUID + 硬件/固件版本) |
| 2 | 心跳 | ✅ 已实现 | `TYPE_HEARTBEAT(0x02)` 每 2s 发送 UpTime + CpuLoad + MemUsage |
| 3 | ACK 应答 | ✅ 已实现 | `TYPE_ACK_PAYLOAD(0x30)` 响应带 `FLAG_ACK_REQ` 的帧 |
| 4 | 温度帧2D 触发帧) | ✅ 已实现 | `TYPE_TEMP_FRAME(0x10)` 零拷贝发送 + 自动分片 |
| 5 | 原始帧上报 | ❌ 未实现 | `TYPE_RAW_FRAME(0x11)` 仅定义常量,无发送逻辑 |
| 6 | 设备注册 | ❌ 未实现 | 规范 §8.1 `DeviceRegisterEvent` 注册事件未发送 |
| 7 | 状态上报 | ❌ 未实现 | 规范 §5.3.5 定期 `AcquisitionStatusEvent` 未实现 |
| 8 | 命令处理回执 | ❌ 未实现 | 规范 §5.3.3 `ChangedEvent` 回执未实现 |
| 9 | Masked 帧 | ❌ 未实现 | 不区分 Triggered/Masked/Live 帧类型发送 |
---
## 三、核心处理逻辑
| # | 功能 | 状态 | 说明 |
|---|------|:----:|------|
| 1 | 2D 滑窗 ROI 提取 | ✅ 已实现 | 积分图 + 滑窗搜索最大平均温度区域,提取 TargetWidth × TargetHeight |
| 2 | 内部温度触发 | ✅ 已实现 | TriggerRoi 区域 Max/Avg vs TriggerTemperatureThreshold |
| 3 | Burst 连拍 | ✅ 已实现 | BurstCount + InternalIntervalMs 状态机 |
| 4 | NG GPIO 输出 | ✅ 已实现 | PA8 脉冲,宽度从 NGioDelay 配置读取 |
| 5 | 帧分片 | ✅ 已实现 | 超 1400B 自动分片,最后一片设置 FLAG_LAST_FRAGMENT |
| 6 | CRC16 校验 | ✅ 已实现 | 收发双向 CRC16-MODBUS 校验 |
| 7 | 自动重连 | ✅ 已实现 | 检测断线后 3s 重连双通道 |
| 8 | 阈值蒙版输出 | ⚠️ 部分 | 滑窗搜索时低温 → 90 替换,但规范要求**输出数据**中低于 MaskThreshold → 替换为 0 |
| 9 | 触发延迟 (DelayMs) | ❌ 未实现 | 字段已解析但触发后无延迟等待逻辑 |
| 10 | 外部 GPIO 触发 | ❌ 未实现 | 无 EXTI 中断配置、消抖DebounceIntervalMs、触发逻辑 |
| 11 | Alarm GPIO | ❌ 未实现 | AlarmGpioLine/AlarmHoldMs 字段已解析但无 GPIO 驱动 |
| 12 | 1D 数据采集处理 | ❌ 未实现 | Config1D 可接收但无 1D 环形缓冲区/切片/触发逻辑 |
| 13 | Training 模式 | ❌ 未实现 | 字段可解析但无训练采样功能 |
| 14 | 消抖 (DebounceIntervalMs) | ❌ 未实现 | 依赖外部触发,一并缺失 |
---
## 四、汇总统计
| 类别 | ✅ 已实现 | ⚠️ 部分 | ❌ 未实现 |
|------|:---------:|:-------:|:---------:|
| 接收(控制通道) | 7 | 1 | 3 |
| 发送(数据通道) | 4 | 0 | 5 |
| 处理逻辑 | 7 | 1 | 6 |
| **合计** | **18** | **2** | **14** |
---
## 五、优先级建议
### P0 — 必须补齐(影响基本功能验证)
1. **控制命令接收** — 至少实现 `StartAcquisition` / `StopAcquisition` / `GetStatus`,否则服务端无法控制采集启停
2. **状态上报** — 周期性上报运行状态给服务端,否则服务端无法监控设备健康
3. **触发延迟 (DelayMs)** — 简单改动,在触发检测到后 delay 再开始采集
4. **阈值蒙版输出** — 提取后的数据中 `val < MaskThreshold` → 替换为 0
### P1 — 高优先级(完善核心流程)
5. **外部 GPIO 触发** — 实际产线通常用外部触发信号
6. **Alarm GPIO** — 独立于 NG GPIO 的报警输出
7. **命令处理回执 (ChangedEvent)** — 服务端需要确认命令被执行
8. **设备注册事件** — 完整的上线握手流程
### P2 — 可延后
9. 1D 数据处理(如果当前只做 2D 热成像)
10. Training 模式
11. 文件传输
12. 原始帧/Masked 帧分类发送
13. 时间同步
14. 部分参数更新
---
## 六、涉及源文件清单
| 文件 | 职责 |
|------|------|
| `Middle/QDXnetworkStack/qdx_protocol.h` | 协议常量、TLV 类型定义、结构体 |
| `Middle/QDXnetworkStack/qdx_protocol.c` | CRC16、帧构建、序列化工具 |
| `Middle/QDXnetworkStack/qdx_tcp_logic.h` | TCP 逻辑层 API 声明 |
| `Middle/QDXnetworkStack/qdx_tcp_logic.c` | 双通道连接管理、TLV 解析/分发、心跳、帧发送 |
| `Middle/QDXnetworkStack/qdx_preprocess.h` | 预处理 API 声明 |
| `Middle/QDXnetworkStack/qdx_preprocess.c` | 滑窗 ROI、温度过滤、内部触发检测 |
| `Middle/QDXnetworkStack/qdx_port.h` | HAL 抽象层声明 |
| `Middle/QDXnetworkStack/qdx_port.c` | FreeRTOS + WCHNET HAL 实现 |
| `User/main.c` | 应用主逻辑RTOS 任务、Burst 状态机、NG GPIO、测试模式 |
| `Debug/dvp.c` | DVP 图像采集驱动 |
| `Debug/mini212g2.c` | Mini212G2 传感器 UART 驱动 |

File diff suppressed because it is too large Load Diff

View File

@ -61,6 +61,17 @@ static void NG_GPIO_Init(void)
GPIO_ResetBits(NG_GPIO_PORT, NG_GPIO_PIN); GPIO_ResetBits(NG_GPIO_PORT, NG_GPIO_PIN);
} }
/* Flag set by TempFrameRequest callback; consumed by business task */
static volatile uint8_t g_temp_req_pending = 0;
static volatile uint8_t g_temp_req_is2d = 1;
void OnTempFrameRequest(uint8_t is2dRequest)
{
g_temp_req_is2d = is2dRequest;
g_temp_req_pending = 1;
DBG_APP("TempFrameReq is2d=%d\r\n", (int)is2dRequest);
}
void OnDetectionResult(uint32_t frameNumber, uint8_t resultStatus) void OnDetectionResult(uint32_t frameNumber, uint8_t resultStatus)
{ {
(void)frameNumber; (void)frameNumber;
@ -341,6 +352,58 @@ static void task_test_pattern_entry(void *pvParameters)
} }
#endif /* TEST_PATTERN_MODE */ #endif /* TEST_PATTERN_MODE */
/* ============================================================
* 1D mode: build and send a single 1D temperature frame from
* the current raw image. Scans the center row and packs
* TempPoint1D_t (2B time_offset + 2B temp, little-endian).
* ============================================================ */
static void send_1d_frame_from_raw(const RawImageBuffer_t *raw, TcpTxBuffer_t *tx_buf)
{
uint16_t w = raw->Width;
uint16_t h = raw->Height;
uint16_t *src = raw->pData;
uint16_t row = h / 2; /* center row */
tx_buf->ValidPayloadLen = 0;
uint8_t *dest = tx_buf->pBuffer + tx_buf->HeadOffset;
uint32_t capacity = tx_buf->TotalCapacity - tx_buf->HeadOffset;
uint16_t points = w;
if ((uint32_t)points * 4 > capacity)
points = (uint16_t)(capacity / 4);
int16_t min_t = 32767, max_t = -32768;
int32_t sum_t = 0;
for (uint16_t i = 0; i < points; i++) {
uint16_t temp = src[row * w + i];
uint16_t time_offset = (uint16_t)((uint32_t)i * 600 / (points > 1 ? points - 1 : 1));
int16_t t = (int16_t)temp;
if (t < min_t) min_t = t;
if (t > max_t) max_t = t;
sum_t += t;
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;
}
tx_buf->ValidPayloadLen = (uint32_t)points * 4;
PreprocessResult_t meta;
memset(&meta, 0, sizeof(meta));
meta.pValidData = tx_buf->pBuffer + tx_buf->HeadOffset;
meta.DataLength = tx_buf->ValidPayloadLen;
meta.ValidWidth = points;
meta.ValidHeight = 1;
meta.MinTemp = min_t;
meta.MaxTemp = max_t;
meta.AvgTemp = (int16_t)(sum_t / (points > 0 ? points : 1));
meta.RoiTemp = meta.AvgTemp;
meta.FrameNumber = raw->FrameNumber;
TcpLogic_BuildAndSendTemperatureFrame(tx_buf, &meta, 0x01, 0 /* IS_1D */);
}
static void task_business_entry(void *pvParameters) static void task_business_entry(void *pvParameters)
{ {
(void)pvParameters; (void)pvParameters;
@ -358,6 +421,29 @@ static void task_business_entry(void *pvParameters)
DVP_Task(); DVP_Task();
#endif #endif
/* Handle on-demand frame request from server */
if (g_temp_req_pending)
{
g_temp_req_pending = 0;
RawImageBuffer_t raw_img;
raw_img.pData = (uint16_t *)FrameBuffer;
raw_img.Width = SENSOR_WIDTH;
raw_img.Height = SENSOR_HEIGHT;
raw_img.FrameNumber = Ready_Frame_Count;
TcpTxBuffer_t *tx_buf = use_buffer_A ? &g_TxNetBuffer_A : &g_TxNetBuffer_B;
use_buffer_A = !use_buffer_A;
tx_buf->ValidPayloadLen = 0;
if (g_temp_req_is2d) {
PreprocessResult_t meta;
if (Preprocess_Execute(&raw_img, tx_buf, &meta) == 0)
TcpLogic_BuildAndSendTemperatureFrame(tx_buf, &meta, 0x02 /* REQUEST */, 1);
} else {
send_1d_frame_from_raw(&raw_img, tx_buf);
}
}
if (Frame_Ready_Flag) if (Frame_Ready_Flag)
{ {
Frame_Ready_Flag = 0; Frame_Ready_Flag = 0;
@ -475,9 +561,34 @@ int main(void)
qdx_port_init(); qdx_port_init();
Preprocess_Init(SENSOR_WIDTH, SENSOR_HEIGHT); Preprocess_Init(SENSOR_WIDTH, SENSOR_HEIGHT);
#if TEST_PATTERN_MODE
/* Set default preprocess config so trigger fires with test patterns.
* Without this, all Config2D_t fields are 0 and trigger never fires
* (roi_w==0 immediate return 0). */
{
Config2D_t test_cfg2d;
memset(&test_cfg2d, 0, sizeof(test_cfg2d));
test_cfg2d.TargetWidth = 64; /* ROI extraction 64x64 */
test_cfg2d.TargetHeight = 64;
test_cfg2d.TriggerRoiX = 0; /* Full-frame trigger area */
test_cfg2d.TriggerRoiY = 0;
test_cfg2d.TriggerRoiW = SENSOR_WIDTH;
test_cfg2d.TriggerRoiH = SENSOR_HEIGHT;
test_cfg2d.TriggerCondition = 1; /* Max temperature */
test_cfg2d.TriggerTemperatureThreshold = 8000; /* 80.00°C */
test_cfg2d.TriggerBurstCount = 3;
test_cfg2d.TriggerInternalIntervalMs = 200;
test_cfg2d.NGioDelay = 200;
Preprocess_Settings_Change(&test_cfg2d, NULL, NULL);
printf("Test default config loaded: trigger thresh=8000 ROI=full burst=3\r\n");
}
#endif
TcpLogic_Init(MACAddr, NULL); TcpLogic_Init(MACAddr, NULL);
TcpLogic_RegisterConfigCallback(OnConfigUpdate); TcpLogic_RegisterConfigCallback(OnConfigUpdate);
TcpLogic_RegisterDetectionCallback(OnDetectionResult); TcpLogic_RegisterDetectionCallback(OnDetectionResult);
TcpLogic_RegisterTempFrameRequestCallback(OnTempFrameRequest);
DBG_APP("TcpLogic_Start...\r\n"); DBG_APP("TcpLogic_Start...\r\n");
TcpLogic_Start(); TcpLogic_Start();