# 硬件视觉调度时序图 ## 概述 本文档描述了硬件视觉调度系统的完整时序流程,展示了从调度车托料完成到最终调度至立锯的全过程。 ### 系统架构 | 项目 | 技术栈 | 角色 | 说明 | | ---- | ------ | ---- | ---- | | `go-auto` | Go | TCP客户端(上位机) | 硬件控制中枢,通过PLC控制下位机,通过TCP与视觉算法通信 | | `LogMax3D` | C# | TCP服务端(视觉算法) | 视觉算法服务,监听9000端口,处理3D扫描、建模、切割优化 | ### 通信协议 - **协议**: TCP长连接,JSON行协议(`\n`分隔) - **连接方向**: Go客户端 → C#服务端(上位机主动连接视觉算法) - **消息格式**: 每行一个完整JSON对象,支持单行和多行格式化JSON自动重组 - **连接确认**: 连接后C#立即推送 `connection_ack` 确认 - **错误标识**: 使用 `error_id` 字段标识错误,空字符串表示成功 ## 上位机控制时序图-视觉部分 ```mermaid sequenceDiagram autonumber participant 调度车 participant 上位机Go participant 下位机PLC participant 视觉算法C# box rgb(173, 216, 230) 硬件调度系统 participant 调度车 end box rgb(255, 218, 185) 控制系统 participant 上位机Go participant 下位机PLC end box rgb(152, 251, 152) 算法系统 participant 视觉算法C# end Note over 调度车,视觉算法C#: 系统初始化阶段 调度车->>上位机Go: 托料完成(M42上升沿) 上位机Go->>上位机Go: 生成WoodID、推送LED面板 Note over 调度车,视觉算法C#: 视觉定位阶段(对中拍照) 上位机Go->>下位机PLC: M809触发 + D2130=2630验证 上位机Go->>上位机Go: 打开红外测距仪、等待读数稳定 上位机Go->>下位机PLC: 写入D2152/D2154调整坐标、M119调整拍照位 上位机Go->>下位机PLC: M810就位 → 关闭红外、M130触发坐标调整 上位机Go->>上位机Go: M120就位 → 监控激光传感器读数 上位机Go->>视觉算法C#: center_photo_taking(wood_id+左右相机坐标) 视觉算法C#-->>上位机Go: ACK: center_scanning 视觉算法C#->>视觉算法C#: 异步拍照+端面中心计算 视觉算法C#->>上位机Go: center_photo_result(顶针目标坐标+直径) 上位机Go->>上位机Go: 设置原木直径、触发托料装置升高 上位机Go->>下位机PLC: 写入D2024/D2026/D2028/D2030顶针坐标 上位机Go->>下位机PLC: 写入D2168/D2170轨道目标坐标 上位机Go->>下位机PLC: M49启动顶料 Note over 调度车,视觉算法C#: 视觉扫描阶段 上位机Go->>上位机Go: M50上升沿 + D2130<1600 + D2042>=2300 上位机Go->>视觉算法C#: start_vision_algorithm(wood_id) 视觉算法C#-->>上位机Go: ACK: vision_scanning 视觉算法C#->>视觉算法C#: 异步启动相机采集 视觉算法C#->>上位机Go: capture_ready(status=ready) 上位机Go->>下位机PLC: 写入M103启动扫描轨道 上位机Go->>上位机Go: 启动M104监控(等待工单推送触发) 下位机PLC->>下位机PLC: 扫描轨道运行、编码器触发采集 Note over 调度车,视觉算法C#: 工单推送与算法处理阶段 上位机Go->>上位机Go: M104上升沿 + M42=true 上位机Go->>上位机Go: 读取D2134计算原木长度 上位机Go->>视觉算法C#: order_info(工单信息+规格+长度) 视觉算法C#-->>上位机Go: ACK: algorithm_coordinates_run(status=processing) 视觉算法C#->>视觉算法C#: 停止采集→建模→顶针裁剪→多规格切割优化 视觉算法C#->>上位机Go: algorithm_coordinates(切割坐标+旋转角度+材积) 视觉算法C#->>上位机Go: board_split_yield_result(板材拆分出材率) Note over 调度车,视觉算法C#: 旋转与调度执行阶段 上位机Go->>上位机Go: 保存视觉数据到数据库+推送LED面板 上位机Go->>下位机PLC: 写入D2038旋转角度 上位机Go->>下位机PLC: M45执行旋转 上位机Go->>下位机PLC: 写入D2136原木直径 上位机Go->>上位机Go: M46上升沿检测(旋转完成) 上位机Go->>上位机Go: 异步计算A/B组夹臂坐标(D2106-D2110/D2162-D2166) 上位机Go->>上位机Go: 触发立锯调度(M807=true) 调度车->>调度车: 取料并调度至立锯 ``` --- ## 流程说明 ### 1. 初始阶段 - **调度车托料完成**: M42上升沿检测,生成WoodID,推送当天原木数量至LED面板 ### 2. 视觉定位阶段(对中拍照) - **M809触发**: 升降车D2130在2630±15mm拍照位时,启动红外测距仪 - **红外测距调整**: 等待红外读数稳定,计算调整坐标写入D2152/D2154,M119调整拍照位 - **M810就位信号**: 关闭红外测距仪,写入M130触发坐标调整 - **M120就位信号**: 监控激光传感器读数,左右均在(850,1000]mm范围内时,推送`center_photo_taking` - **异步对中计算**: C#端异步执行图漾深度相机拍照和端面中心计算 - **推送对中结果**: C#推送`center_photo_result`,包含视觉顶针机构目标坐标(mm)和直径(mm) - **写入顶针坐标**: Go端将坐标写入D2024/D2026/D2028/D2030,轨道坐标写入D2168/D2170,M49启动顶料 ### 3. 视觉扫描阶段 - **M50上升沿检测**: 同时验证D2130<1600(升降车安全位)且D2042>=2300(调度车就位) - **D2138方向判断**: D2138>-2250为左扫描(left),<=-2250为右扫描(right) - **推送扫描标识**: Go发送`start_vision_algorithm`,C#异步启动4台相机持续采集 - **capture_ready信号**: C#相机就绪后推送就绪信号,Go收到后写入M103启动扫描轨道 ### 4. 工单推送与算法处理阶段 - **M104上升沿**: 扫描结束触发,同时验证M42=true - **读取原木长度**: 从D2134寄存器读取长度值,减去315mm得到实际原木长度 - **推送工单信息**: Go发送`order_info`,包含工单规格、副规格、锯路厚度等参数 - **异步算法处理**: C#端依次执行:停止采集→等待帧就绪→点云融合建模→Y轴归一化→顶针裁剪→多规格切割优化 - **推送算法结果**: C#推送`algorithm_coordinates`(切割坐标+旋转角度+材积+出材率) - **推送板材拆分**: C#异步推送`board_split_yield_result`(板材拆分出材率) ### 5. 旋转与调度执行阶段 - **数据持久化**: Go保存视觉算法数据到数据库(坐标JSON、旋转角度、材积、径级),推送出材率到LED面板 - **写入旋转角度**: D2038写入算法角度值(回读验证,最多重试5次) - **执行旋转**: M45写入true执行物理旋转 - **写入原木直径**: D2136写入原木直径值 - **旋转完成检测**: M46上升沿检测(旋转完成),并行计算A/B组夹臂伺服坐标 - **触发立锯调度**: M807=true时触发立锯调度逻辑 ## 关键特性 - **异步推送机制**: C#端收到请求后立即返回ACK,算法计算完成后异步推送结果(`center_photo_result`/`algorithm_coordinates`) - **capture_ready握手**: C#端相机就绪后推送就绪信号,Go端收到后才写入M103启动扫描轨道,避免时序错误 - **多规格对比优化**: 算法对比主规格与副规格,选出材率最高的切割方案 - **顶针裁剪**: 根据扫描方向(left/right)从点云两端裁剪固定长度,移除机械顶针和过渡区点云 - **Y轴归一化**: scan_start="right"时翻转Y轴,确保Y=0始终对应同一物理端 - **寄存器回读验证**: 关键寄存器(D2038角度值、D2024-D2030顶针坐标)写入后立即回读验证 - **材积基准统一**: TotalVolume使用middle Y截面R计算,与AvailableVolume同源,消除出材率偏高 ## 注意事项 1. 每个阶段的执行都依赖于前一阶段的成功完成 2. C#端TCP服务监听 `0.0.0.0:9000`,支持多客户端连接 3. 视觉算法的准确性直接影响整个调度流程的效果 4. Go与C#之间的TCP长连接支持自动重连(默认10秒间隔) 5. 红外测距仪读数稳定判断:连续10次相邻读数差值<0.5mm 6. 激光传感器有效范围:左/右均在(850, 1000]mm 7. 原木长度 = D2134寄存器值 - 315mm(固定偏移) 8. 顶针裁剪量根据扫描方向不同:left扫描(低Y端215mm+高Y端172mm),right扫描(低Y端170mm+高Y端209mm) ## TCP视觉算法数据接口 ### 接口概述 上位机(go-auto)作为TCP客户端,连接视觉算法服务端(LogMax3D, 监听9000端口),通过JSON行协议(`\n`分隔)进行双向通信。 **通信模型**: - **Go → C#**: 上位机发送请求(`center_photo_taking`、`start_vision_algorithm`、`order_info`) - **C# → Go**: 算法推送异步结果(`center_photo_result`、`capture_ready`、`algorithm_coordinates`、`board_split_yield_result`) ### 通用消息格式 #### 请求格式(Go → C#) ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "action": "order_info", "data": { ... }, "timestamp": 1762158534297 } ``` | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | id | string | 是 | 请求唯一标识符(UUID) | | action | string | 是 | 动作类型 | | data | object | 否 | 请求数据,根据不同action有不同结构 | | timestamp | number | 是 | 请求时间戳(毫秒) | #### 响应格式(C# → Go) ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "action": "algorithm_coordinates", "data": { ... }, "timestamp": 1762308230194, "error_id": "" } ``` | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | id | string | 是 | 对应请求的ID | | action | string | 是 | 动作类型 | | data | object | 否 | 响应数据 | | timestamp | number | 是 | 时间戳(毫秒) | | error_id | string | 否 | 错误码:空字符串=成功,非空=错误标识 | --- ### 支持的动作类型 #### 1. center_photo_taking - 视觉取中拍照命令(Go → C#) **功能说明:** 上位机下发视觉取中拍照命令,触发C#端图漾深度相机拍照并计算原木端面中心坐标。 **触发条件:** M120上升沿 && 激光传感器读数左/右均在(850,1000]mm **调用方法:** `visionClient.SendRequestWithRetry(request)` → C# `HandleCenterPhotoTaking()` **请求示例:** ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "action": "center_photo_taking", "data": { "wood_id": "WOOD_20251103_162602_004_2ace914e", "left": { "x": 55050, "y": 32849, "z": 0, "d": 910.5 }, "right": { "x": 55050, "y": 32849, "z": 0, "d": 912.3 } }, "timestamp": 1762158362326 } ``` **data参数说明:** | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | wood_id | string | 是 | 原木唯一标识ID | | left | object | 是 | 左相机在机械坐标系下的绝对位置 | | left.x | number | 是 | 左相机X坐标(PLC D2016) | | left.y | number | 是 | 左相机Y坐标(PLC D2018) | | left.z | number | 是 | 左相机Z坐标(PLC D2126) | | left.d | number | 否 | 左侧激光传感器读数(mm) | | right | object | 是 | 右相机在机械坐标系下的绝对位置 | | right.x | number | 是 | 右相机X坐标(PLC D2020) | | right.y | number | 是 | 右相机Y坐标(PLC D2022) | | right.z | number | 是 | 右相机Z坐标(PLC D2128) | | right.d | number | 否 | 右侧激光传感器读数(mm) | **即时响应(ACK):** ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "action": "center_scanning", "data": {}, "timestamp": 1762158362400, "error_id": "" } ``` --- #### 2. center_photo_result - 视觉对中结果推送(C# → Go) **功能说明:** C#端异步完成拍照和端面中心计算后,主动推送视觉顶针机构目标坐标给上位机。 **调用方法:** C# `SendCenteringResult()` → Go `handleCenterPhotoResult()` **请求示例:** ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "action": "center_photo_result", "data": { "left": { "x": 509.34, "y": 1330.28, "z": 450.0, "diameter": 345.6 }, "right": { "x": 512.15, "y": 1325.40, "z": 455.0, "diameter": 340.2 }, "x_diameter": 348.5, "y_diameter": 342.1, "wood_id": "WOOD_20251103_162602_004_2ace914e", "reg_timestamp": 1762158362326, "recv_timestamp": 1762308226907, "send_timestamp": 1762308230194 }, "timestamp": 1762308230194, "error_id": "" } ``` **data参数说明:** | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | left | object | 是 | 左端面中心点结果 | | left.x | number | 是 | 左视觉顶针机构目标X坐标(mm) = 相机位置X + RT变换偏移X | | left.y | number | 是 | 左视觉顶针机构目标Y坐标(mm) = 相机位置Y + RT变换偏移Y | | left.z | number | 是 | 相机到原木端面距离(mm) | | left.diameter | number | 否 | 左端面直径(mm) = (X径+Y径)/2 | | right | object | 是 | 右端面中心点结果 | | right.x | number | 是 | 右视觉顶针机构目标X坐标(mm) | | right.y | number | 是 | 右视觉顶针机构目标Y坐标(mm) | | right.z | number | 是 | 相机到原木端面距离(mm) | | right.diameter | number | 否 | 右端面直径(mm) | | x_diameter | number | 否 | X方向直径(mm) | | y_diameter | number | 否 | Y方向直径(mm) | | wood_id | string | 是 | 原木标识 | | reg_timestamp | number | 否 | 请求时间戳(来自上位机原始请求) | | recv_timestamp | number | 否 | 视觉系统接收时间戳 | | send_timestamp | number | 否 | 视觉系统发送结果时间戳 | **Go端处理逻辑:** 1. 直径单位转换:`diameter / 10`(mm → cm) 2. 设置全局原木直径、X轴直径、Y轴直径 3. 触发托料装置升高 4. 调用`WriteVisionPinCoordinate()`写入顶针坐标至D2024/D2026/D2028/D2030 5. 计算轨道目标坐标写入D2168/D2170 6. 写入M49启动顶料 --- #### 3. start_vision_algorithm - 开始视觉扫描(Go → C#) **功能说明:** 上位机通知视觉算法开始3D扫描,C#端异步启动4台相机进入持续采集模式。 **触发条件:** M50上升沿 && D2130<1600 && D2042>=2300 **调用方法:** `visionClient.SendRequestWithRetry(request)` → C# `HandleStartVision()` **请求示例:** ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "action": "start_vision_algorithm", "data": { "wood_id": "WOOD_20251103_162602_004_2ace914e" }, "timestamp": 1762158400000 } ``` **data参数说明:** | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | wood_id | string | 是 | 原木唯一标识ID | **即时响应(ACK):** ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "action": "vision_scanning", "data": { "status": "scanning" }, "timestamp": 1762158400100, "error_id": "" } ``` **异步响应(相机就绪后推送):** ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "action": "capture_ready", "data": { "status": "ready" }, "timestamp": 1762158401500, "error_id": "" } ``` > **说明:** `capture_ready`的`status`字段可能为`ready`(成功)或`error`(失败)。Go端收到`ready`后写入M103启动扫描轨道,收到`error`则不写入M103。 **Go端capture_ready处理逻辑:** 1. 检查`error_id`和`status`字段 2. 读取`pendingScanStart`(D2138方向判断结果) 3. 写入M103=true启动扫描轨道 4. 启动M104监控goroutine,等待工单推送触发 --- #### 4. order_info - 工单信息下发(Go → C#) **功能说明:** 上位机下发工单信息,代表扫描结束。C#端停止采集并执行建模+切割优化算法。 **触发条件:** M104上升沿 && M42=true **调用方法:** `visionClient.SendRequestWithRetry(request)` → C# `HandleOrderInfo()` **请求示例:** ```json { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "action": "order_info", "data": { "wood_id": "WOOD_20251103_162602_004_2ace914e", "order_sn": "SO20251101000002", "order_spec": "38*89*3.98", "sub_spec": ["38*89*3.98", "37*66*4.1"], "has_bark": true, "grade": "等级A", "start_distance": 115, "end_distance": 115, "length": 3785.0, "saw_thickness": 1.8, "scan_start": "left" }, "timestamp": 1762158534297 } ``` **data参数说明:** | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | wood_id | string | 是 | 原木唯一标识ID | | order_sn | string | 是 | 工单编号 | | order_spec | string | 是 | 工单主规格(格式:厚*宽*长),如"38*89*3.98" | | sub_spec | []string | 否 | 副规格列表,算法对比主规格与各副规格选出材率最高方案 | | has_bark | bool | 否 | 是否带树皮(默认true) | | grade | string | 否 | 原木等级(如"等级A") | | start_distance | number | 否 | 起始距离(mm):侧面相机至原木端面距离(常量115mm) | | end_distance | number | 否 | 结束距离(mm):侧面相机至原木端面距离(常量115mm) | | length | number | 否 | 原木长度(mm):D2134寄存器值 - 315 | | saw_thickness | number | 否 | 锯片厚度(mm),默认1.8 | | scan_start | string | 否 | 初始扫描方向:"left"或"right"(基于D2138判断) | **即时响应(ACK):** ```json { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "action": "algorithm_coordinates_run", "data": { "status": "processing" }, "timestamp": 1762158534400, "error_id": "" } ``` > **说明:** `order_info`代表扫描结束,C#收到后立即响应ACK,然后异步执行:停止采集→等待帧就绪→点云融合→建模→顶针裁剪→多规格切割优化→推送结果。 --- #### 5. algorithm_coordinates - 算法切割坐标结果(C# → Go) **功能说明:** C#端切割优化算法计算完成后,主动推送最优切割方案给上位机。 **调用方法:** C# `TcpServer.SendToClientAsync()` → Go `handleAlgorithmCoordinates()` **请求示例:** ```json { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "action": "algorithm_coordinates", "data": { "left": [120.5, 80.3, 40.1], "right": [15.2, 55.8, 96.4, 137.0], "rotation_angle": 15.5, "available_volume": 0.85, "total_volume": 1.205, "yield_rate": 70.5, "wood_id": "WOOD_20251103_162602_004_2ace914e" }, "timestamp": 1762158540000, "error_id": "" } ``` **data参数说明:** | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | left | []number | 是 | 左侧锯片坐标数组(mm),距中心距离,1-15个元素 | | right | []number | 是 | 右侧锯片坐标数组(mm),距中心距离,1-15个元素 | | rotation_angle | number | 是 | 旋转角度(度) | | available_volume | number | 是 | 可用材积(m³) | | total_volume | number | 否 | 整根原木材积(m³),基于middle Y截面R计算 | | yield_rate | number | 否 | 出材率(%),如70.5表示70.5% | | wood_id | string | 否 | 原木ID | **Go端处理逻辑:** 1. 验证`error_id`为空 2. 解析`left`/`right`数组(长度1-15)并转换为float32 3. 保存视觉数据到数据库(坐标JSON、旋转角度、材积、径级) 4. 推送出材率到LED面板 5. 写入D2038旋转角度(回读验证,最多重试5次,容差0.01度) 6. 写入M45=true执行旋转 7. 写入D2136原木直径 8. 启动M46上升沿监控(旋转完成后触发立锯调度) 9. 异步计算A/B组夹臂伺服坐标 --- #### 6. board_split_yield_result - 板材拆分出材率(C# → Go) **功能说明:** C#端异步完成板材拆分计算后,推送出材率结果给上位机。此消息在`algorithm_coordinates`之后异步推送,不阻塞主流程。 **调用方法:** C# `BoardSplitYieldService.RunAsync()` → Go `handleBoardSplitYieldResult()` **请求示例:** ```json { "id": "", "action": "board_split_yield_result", "data": { "status": "completed", "wood_id": "WOOD_20251103_162602_004_2ace914e", "yield_rate": 68.5, "n_boards": 6, "n_blocks": 42, "block_volume": 0.583, "available_volume": 0.85, "ply_file": "data/modeling/WOOD_20251103_162602_004_2ace914e/board_split.ply", "png_file": "data/modeling/WOOD_20251103_162602_004_2ace914e/board_split.png" }, "timestamp": 1762158545000, "error_id": "" } ``` **data参数说明:** | 参数 | 类型 | 必填 | 说明 | | ---- | ---- | ---- | ---- | | status | string | 是 | 计算状态:"completed"=完成,"error"=错误 | | wood_id | string | 是 | 原木唯一标识ID | | yield_rate | number | 是 | 板材拆分出材率(%) | | n_boards | number | 是 | 板材数量 | | n_blocks | number | 是 | 木方数量 | | block_volume | number | 是 | 木方总体积(m³) | | available_volume | number | 是 | 可用材积(m³) | | ply_file | string | 否 | PLY点云可视化文件路径 | | png_file | string | 否 | PNG截面图文件路径 | --- #### 7. connection_ack - 连接确认(C# → Go) **功能说明:** TCP连接建立后,C#端立即推送连接确认消息。 **请求示例:** ```json { "id": "", "action": "connection_ack", "data": null, "timestamp": 1762158000000, "error_id": "" } ``` --- ### 错误处理 #### 错误响应格式 当请求处理失败时,C#端通过`error_id`字段标识错误: ```json { "id": "req001", "action": "algorithm_coordinates", "data": {}, "timestamp": 1705123456789, "error_id": "err_algorithm" } ``` #### 常见错误码 | error_id | 说明 | 触发场景 | | -------- | ---- | -------- | | `""` (空字符串) | 成功 | 正常处理 | | `err_unknown_action` | 未知动作类型 | action不在支持列表中 | | `err_empty_data` | 请求data为空 | order_info缺少data | | `err_parse_order` | 解析OrderData失败 | JSON格式错误 | | `err_algorithm` | 算法计算异常 | 切割优化过程异常 | | `err_no_data` | 未采集到有效数据 | 相机未采集到帧数据 | | `err_no_model` | 无原木3D模型 | 未收到start_vision_algorithm | | `err_start_capture` | 启动采集失败 | capture_ready推送error状态 | | `err003` | 对中计算异常 | CenteringService计算过程异常 | | `err004` | 对中服务未初始化 | 图漾相机未连接或未启用 | ### 完整调用链路时序 ``` Go(上位机) C#(视觉算法) | | | ── center_photo_taking ──────────────────► | 对中拍照请求 | ◄── center_scanning (ACK) ─────────────── | 立即响应 | | 异步: 拍照+计算 | ◄── center_photo_result ────────────────── | 推送对中坐标 | | | ── start_vision_algorithm ───────────────► | 开始扫描请求 | ◄── vision_scanning (ACK) ─────────────── | 立即响应 | | 异步: 启动相机采集 | ◄── capture_ready ──────────────────────── | 相机就绪信号 | (写入M103启动扫描轨道) | | | | ── order_info ───────────────────────────► | 工单信息(扫描结束) | ◄── algorithm_coordinates_run (ACK) ────── | 立即响应 | | 异步: 停止采集→建模→切割优化 | ◄── algorithm_coordinates ──────────────── | 推送切割坐标+材积 | ◄── board_split_yield_result ───────────── | 推送板材拆分出材率 | | ``` ### 接口使用注意事项 1. **数据格式**:所有请求为JSON行协议,每条消息以`\n`结尾 2. **坐标精度**:坐标值支持float64精度,Go端内部转换为float32写入PLC 3. **TCP长连接**:Go端使用带自动重连的TCP客户端,断线自动重连(默认10秒间隔) 4. **异步推送**:C#端收到请求后立即返回ACK,计算完成后异步推送结果,Go端通过注册的`asyncHandler`处理 5. **请求ID**:使用UUID生成唯一请求ID,便于追踪和调试 6. **超时处理**:Go端`SendRequestWithRetry`支持重试机制(可配置重试次数和间隔) 7. **线程安全**:C#端使用SemaphoreSlim保证StreamWriter写入线程安全 8. **连接清理**:C#端每60秒自动清理已断开的客户端连接,防止僵尸连接积累 9. **直径单位**:C#推送的直径单位为mm,Go端接收后除以10转换为cm存储 10. **扫描方向**:scan_start由D2138寄存器判断,>-2250为"left",<=-2250为"right"