diff --git a/.doc/CSM-TCP-Router.excalidraw b/.doc/CSM-TCP-Router.excalidraw
new file mode 100644
index 0000000..b9b5455
--- /dev/null
+++ b/.doc/CSM-TCP-Router.excalidraw
@@ -0,0 +1,1541 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
+ "elements": [
+ {
+ "id": "06097f4ce4a944f5b840",
+ "type": "rectangle",
+ "x": -377.1428571428571,
+ "y": 1010.8571951729912,
+ "width": 710,
+ "height": 260,
+ "angle": 0,
+ "strokeColor": "#666666",
+ "backgroundColor": "#f5f5f5",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1516676960,
+ "version": 69,
+ "versionNonce": 1599584990,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "40d09f28b6f5449b8416",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "index": "a0"
+ },
+ {
+ "id": "40d09f28b6f5449b8416",
+ "type": "text",
+ "x": -140.4628571428571,
+ "y": 1125.8571951729912,
+ "width": 236.64,
+ "height": 30,
+ "angle": 0,
+ "strokeColor": "#67AB9F",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1359914060,
+ "version": 69,
+ "versionNonce": 1083748126,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "text": "Server Application",
+ "fontSize": 24,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "06097f4ce4a944f5b840",
+ "originalText": "Server Application",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "a1"
+ },
+ {
+ "id": "f2931a99db14441d92c2",
+ "type": "rectangle",
+ "x": 201.42848423549094,
+ "y": -51.142839704241055,
+ "width": 570,
+ "height": 630,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 334935149,
+ "version": 35,
+ "versionNonce": 479119682,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "d39d2f1397d84cf29c94",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "a2"
+ },
+ {
+ "id": "d39d2f1397d84cf29c94",
+ "type": "text",
+ "x": 319.388484235491,
+ "y": 243.85716029575894,
+ "width": 334.08,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1166131094,
+ "version": 35,
+ "versionNonce": 1613254914,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "Server Application",
+ "fontSize": 32,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "f2931a99db14441d92c2",
+ "originalText": "Server Application",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "a3"
+ },
+ {
+ "id": "3a12c9cd83ac49d4af82",
+ "type": "rectangle",
+ "x": 271.42848423549094,
+ "y": -1.1428397042410552,
+ "width": 450,
+ "height": 340,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 405658968,
+ "version": 53,
+ "versionNonce": 1320870082,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "96fc065142d741bfab28",
+ "type": "text"
+ },
+ {
+ "id": "4343ca43bb1b46aaab89",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "a4"
+ },
+ {
+ "id": "96fc065142d741bfab28",
+ "type": "text",
+ "x": 363.3184842354909,
+ "y": 3.857160295758945,
+ "width": 266.21999999999997,
+ "height": 33.75,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1687535625,
+ "version": 54,
+ "versionNonce": 870676610,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "CSM Module System",
+ "fontSize": 27,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "top",
+ "containerId": "3a12c9cd83ac49d4af82",
+ "originalText": "CSM Module System",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "a5"
+ },
+ {
+ "id": "58eb2e1dfd49455083d7",
+ "type": "rectangle",
+ "x": 288.2284720284597,
+ "y": 68.85716029575894,
+ "width": 203.20001220703125,
+ "height": 110,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 828310685,
+ "version": 70,
+ "versionNonce": 287227906,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "b4cb0e46d8754a6ba28b",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "a6"
+ },
+ {
+ "id": "b4cb0e46d8754a6ba28b",
+ "type": "text",
+ "x": 304.7285406930105,
+ "y": 111.35716029575894,
+ "width": 170.1998748779297,
+ "height": 25,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 540651128,
+ "version": 72,
+ "versionNonce": 353071042,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "AI (CSM Module1)",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "58eb2e1dfd49455083d7",
+ "originalText": "AI (CSM Module1)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "a7"
+ },
+ {
+ "id": "8a5c90e4c9214aff98d8",
+ "type": "rectangle",
+ "x": 288.2284720284597,
+ "y": 188.85716029575894,
+ "width": 203.20001220703125,
+ "height": 110,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 2061525606,
+ "version": 71,
+ "versionNonce": 525084546,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "15dba5756d714bc5be93",
+ "type": "text"
+ },
+ {
+ "id": "c967d534ee594bd8b6f7",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "a8"
+ },
+ {
+ "id": "15dba5756d714bc5be93",
+ "type": "text",
+ "x": 337.27851322719016,
+ "y": 218.85716029575894,
+ "width": 105.09992980957031,
+ "height": 50,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1094784021,
+ "version": 71,
+ "versionNonce": 1478485826,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "DIO1 (CSM\nModule2)",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "8a5c90e4c9214aff98d8",
+ "originalText": "DIO1 (CSM Module2)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "a9"
+ },
+ {
+ "id": "378abd886fa942b5ae6f",
+ "type": "rectangle",
+ "x": 501.42848423549094,
+ "y": 68.85716029575894,
+ "width": 200,
+ "height": 110,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1968915559,
+ "version": 52,
+ "versionNonce": 320634562,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "867f1a5210ac42c7a82d",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "aA"
+ },
+ {
+ "id": "867f1a5210ac42c7a82d",
+ "type": "text",
+ "x": 548.8785193307058,
+ "y": 98.85716029575894,
+ "width": 105.09992980957031,
+ "height": 50,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1862328426,
+ "version": 53,
+ "versionNonce": 1986221698,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "DIO1 (CSM\nModule3)",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "378abd886fa942b5ae6f",
+ "originalText": "DIO1 (CSM Module3)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aB"
+ },
+ {
+ "id": "adc34df6058e4435acf1",
+ "type": "rectangle",
+ "x": 501.42848423549094,
+ "y": 188.85716029575894,
+ "width": 200,
+ "height": 110,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 76596677,
+ "version": 52,
+ "versionNonce": 1115240002,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "109e949f39d34b6cb355",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "aC"
+ },
+ {
+ "id": "109e949f39d34b6cb355",
+ "type": "text",
+ "x": 531.4185431344167,
+ "y": 218.85716029575894,
+ "width": 140.01988220214844,
+ "height": 50,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1205658623,
+ "version": 53,
+ "versionNonce": 406658562,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "Measure (CSM\nModule4)",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "adc34df6058e4435acf1",
+ "originalText": "Measure (CSM Module4)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aD"
+ },
+ {
+ "id": "72a83ed504ed4d0da987",
+ "type": "rectangle",
+ "x": 262.62843540736594,
+ "y": 438.85716029575894,
+ "width": 458.800048828125,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 513845840,
+ "version": 87,
+ "versionNonce": 615386562,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "3b93d90e9ba740ec8d2c",
+ "type": "text"
+ },
+ {
+ "id": "4343ca43bb1b46aaab89",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "aE"
+ },
+ {
+ "id": "3b93d90e9ba740ec8d2c",
+ "type": "text",
+ "x": 272.20451572963157,
+ "y": 448.85716029575894,
+ "width": 439.64788818359375,
+ "height": 80,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 133198406,
+ "version": 89,
+ "versionNonce": 485164418,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "CSM TCP Router (based on\nJKI TCP Server)",
+ "fontSize": 32,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "72a83ed504ed4d0da987",
+ "originalText": "CSM TCP Router (based on JKI TCP Server)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aF"
+ },
+ {
+ "id": "66d9bb7d39ed49bab977",
+ "type": "rectangle",
+ "x": -556.1715218680247,
+ "y": -41.142839704241055,
+ "width": 647.6000061035156,
+ "height": 620,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1857846310,
+ "version": 78,
+ "versionNonce": 904203522,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "d61e4a50c7424aafb49c",
+ "type": "text"
+ },
+ {
+ "id": "c967d534ee594bd8b6f7",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "aG"
+ },
+ {
+ "id": "d61e4a50c7424aafb49c",
+ "type": "text",
+ "x": -365.21946498325906,
+ "y": 248.85716029575894,
+ "width": 265.6958923339844,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 531398026,
+ "version": 78,
+ "versionNonce": 1166023874,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "Client Application",
+ "fontSize": 32,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "66d9bb7d39ed49bab977",
+ "originalText": "Client Application",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aH"
+ },
+ {
+ "id": "d4b5bc67b5e84b86aec2",
+ "type": "rectangle",
+ "x": -510.97150966099343,
+ "y": 161.12381837229248,
+ "width": 564.7999877929688,
+ "height": 190.39999050564236,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 451019402,
+ "version": 149,
+ "versionNonce": 1259164738,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "e14f5e9cd4674be1bda4",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "aI"
+ },
+ {
+ "id": "e14f5e9cd4674be1bda4",
+ "type": "text",
+ "x": -505.97150966099343,
+ "y": 181.32381362511364,
+ "width": 479.3798828125,
+ "height": 150,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1029267775,
+ "version": 166,
+ "versionNonce": 1956477954,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "Server Build-in Command\n\nCSM TCP Router 定义的指令,用于管理和显示帮助\n- List: 列出所有的 CSM 模块\n- List API: 列出CSM 模块的参数\n- Help: 显示CSM模块的帮助(VI Description)",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "d4b5bc67b5e84b86aec2",
+ "originalText": "Server Build-in Command\n\nCSM TCP Router 定义的指令,用于管理和显示帮助\n- List: 列出所有的 CSM 模块\n- List API: 列出CSM 模块的参数\n- Help: 显示CSM模块的帮助(VI Description)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aJ"
+ },
+ {
+ "id": "c44e3d5dc3a5490cb039",
+ "type": "rectangle",
+ "x": -511.7714974539622,
+ "y": 369.39047644882606,
+ "width": 564.7999877929688,
+ "height": 145.06665943287038,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 286961250,
+ "version": 185,
+ "versionNonce": 290630594,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "ec5f57e035cf45908018",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "aK"
+ },
+ {
+ "id": "ec5f57e035cf45908018",
+ "type": "text",
+ "x": -506.7714974539622,
+ "y": 379.4238061652612,
+ "width": 536.919921875,
+ "height": 125,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1260928053,
+ "version": 207,
+ "versionNonce": 122413954,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "Server CSM Command\n由 CSM 模块定义的指令\nCSM 的所有消息,都被转发到 CSM Modules 中执行,因此\n支持的消息种类,由Server 中的CSM Module System 中的\nCSM模块决定",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "c44e3d5dc3a5490cb039",
+ "originalText": "Server CSM Command\n由 CSM 模块定义的指令\nCSM 的所有消息,都被转发到 CSM Modules 中执行,因此支持的消息种类,由Server 中的CSM Module System 中的CSM模块决定",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aL"
+ },
+ {
+ "id": "68d09aa25b504907b22d",
+ "type": "rectangle",
+ "x": -510.97150966099343,
+ "y": -11.142839704241055,
+ "width": 564.7999877929688,
+ "height": 167.7333249692564,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1280667824,
+ "version": 149,
+ "versionNonce": 549987138,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "0916692baae84d6a8cb2",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "index": "aM"
+ },
+ {
+ "id": "0916692baae84d6a8cb2",
+ "type": "text",
+ "x": -505.97150966099343,
+ "y": 10.223822780387138,
+ "width": 493.7198486328125,
+ "height": 125,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1861740026,
+ "version": 162,
+ "versionNonce": 136735490,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133715,
+ "link": null,
+ "locked": false,
+ "text": "Client-Def Command\nClient 定义的本地指令,和server无关\n\n- Switch: 切换发送的CSM模块,节省键入CSM模块名称\n- Script: 导入执行 scipt 文本",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "68d09aa25b504907b22d",
+ "originalText": "Client-Def Command\nClient 定义的本地指令,和server无关\n\n- Switch: 切换发送的CSM模块,节省键入CSM模块名称\n- Script: 导入执行 scipt 文本",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aN"
+ },
+ {
+ "id": "c3d7e032eee9479d92fe",
+ "type": "rectangle",
+ "x": -347.1428571428571,
+ "y": 1040.8571951729912,
+ "width": 647.199951171875,
+ "height": 60,
+ "angle": 0,
+ "strokeColor": "#82b366",
+ "backgroundColor": "#d5e8d4",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 616346550,
+ "version": 89,
+ "versionNonce": 1848712030,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "ba1ba4b5800841ef99ba",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "index": "aO"
+ },
+ {
+ "id": "ba1ba4b5800841ef99ba",
+ "type": "text",
+ "x": -154.28281075613836,
+ "y": 1055.8571951729912,
+ "width": 261.4798583984375,
+ "height": 30,
+ "angle": 0,
+ "strokeColor": "#67AB9F",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1360334683,
+ "version": 90,
+ "versionNonce": 1584529310,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "text": "TCP Layer( Reusable)",
+ "fontSize": 24,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "c3d7e032eee9479d92fe",
+ "originalText": "TCP Layer( Reusable)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aP"
+ },
+ {
+ "id": "f1fcce1f99dc4c1f8847",
+ "type": "rectangle",
+ "x": -347.1428571428571,
+ "y": 1110.8571951729912,
+ "width": 647.199951171875,
+ "height": 140,
+ "angle": 0,
+ "strokeColor": "#b85450",
+ "backgroundColor": "#f8cecc",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 527125112,
+ "version": 85,
+ "versionNonce": 361520094,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "5bab6bdeb62b40228319",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "index": "aQ"
+ },
+ {
+ "id": "5bab6bdeb62b40228319",
+ "type": "text",
+ "x": -309.78268868582586,
+ "y": 1168.3571951729912,
+ "width": 572.4796142578125,
+ "height": 25,
+ "angle": 0,
+ "strokeColor": "#67AB9F",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 894462135,
+ "version": 87,
+ "versionNonce": 1771122718,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "text": "Code Based CSM Framework(Based on the Requirements)",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "f1fcce1f99dc4c1f8847",
+ "originalText": "Code Based CSM Framework(Based on the Requirements)",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aR"
+ },
+ {
+ "id": "5c296355ff174e0195e2",
+ "type": "rectangle",
+ "x": -272.85714285714266,
+ "y": 636.857125418527,
+ "width": 100,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#D2D3D3",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 425614159,
+ "version": 71,
+ "versionNonce": 1181842526,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "b543c6d67fdf4c61a3f0",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "index": "aS"
+ },
+ {
+ "id": "b543c6d67fdf4c61a3f0",
+ "type": "text",
+ "x": -268.85714285714266,
+ "y": 661.857125418527,
+ "width": 92,
+ "height": 50,
+ "angle": 0,
+ "strokeColor": "#67AB9F",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 91453709,
+ "version": 73,
+ "versionNonce": 1912488094,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "text": "TCP\nClient",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "5c296355ff174e0195e2",
+ "originalText": "TCP Client",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aT"
+ },
+ {
+ "id": "b56d185ccff9414282e7",
+ "type": "rectangle",
+ "x": -152.85714285714266,
+ "y": 636.857125418527,
+ "width": 100,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#D2D3D3",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1346551058,
+ "version": 71,
+ "versionNonce": 1340636382,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "e1c67e3c14464e33954a",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "index": "aU"
+ },
+ {
+ "id": "e1c67e3c14464e33954a",
+ "type": "text",
+ "x": -148.85714285714266,
+ "y": 661.857125418527,
+ "width": 92,
+ "height": 50,
+ "angle": 0,
+ "strokeColor": "#67AB9F",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 757773874,
+ "version": 73,
+ "versionNonce": 633418014,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "text": "TCP\nClient",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "b56d185ccff9414282e7",
+ "originalText": "TCP Client",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aV"
+ },
+ {
+ "id": "c8513775c6fe4da58f55",
+ "type": "rectangle",
+ "x": -32.85714285714266,
+ "y": 636.857125418527,
+ "width": 100,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#D2D3D3",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 757589191,
+ "version": 71,
+ "versionNonce": 1593400670,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "709beda0e58b4b5ab6cd",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "index": "aW"
+ },
+ {
+ "id": "709beda0e58b4b5ab6cd",
+ "type": "text",
+ "x": -28.857142857142662,
+ "y": 661.857125418527,
+ "width": 92,
+ "height": 50,
+ "angle": 0,
+ "strokeColor": "#67AB9F",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1211271590,
+ "version": 73,
+ "versionNonce": 1356988830,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "text": "TCP\nClient",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "c8513775c6fe4da58f55",
+ "originalText": "TCP Client",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aX"
+ },
+ {
+ "id": "09a22cc2431a4ca7b1e0",
+ "type": "rectangle",
+ "x": 33.42878069196445,
+ "y": 839.1429792131696,
+ "width": 651.1429268973216,
+ "height": 120,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "#ffffff",
+ "fillStyle": "solid",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1821414404,
+ "version": 187,
+ "versionNonce": 2125209054,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "627048f5d60843d0a878",
+ "type": "text"
+ }
+ ],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "index": "aY"
+ },
+ {
+ "id": "627048f5d60843d0a878",
+ "type": "text",
+ "x": 88.73049926757835,
+ "y": 849.1429792131696,
+ "width": 540.5394897460938,
+ "height": 100,
+ "angle": 0,
+ "strokeColor": "#67AB9F",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 1,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 215362962,
+ "version": 147,
+ "versionNonce": 1567142430,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "text": "TCP Command\n- All CSM command from your code is supported\n- System command provided by TCP Layer is supported\nlist/help/list api ...",
+ "fontSize": 20,
+ "fontFamily": 1,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "09a22cc2431a4ca7b1e0",
+ "originalText": "TCP Command\n- All CSM command from your code is supported\n- System command provided by TCP Layer is supported\nlist/help/list api ...",
+ "lineHeight": 1.25,
+ "autoResize": false,
+ "index": "aZ"
+ },
+ {
+ "id": "4343ca43bb1b46aaab89",
+ "type": "arrow",
+ "x": 494.02845982142844,
+ "y": 427.2571236746652,
+ "width": 0.00006103515625,
+ "height": 85.5999755859375,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1789827435,
+ "version": 197,
+ "versionNonce": 63192862,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133813,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.00006103515625,
+ -85.5999755859375
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "72a83ed504ed4d0da987",
+ "focus": 0.008718202065400868,
+ "gap": 11.60003662109375
+ },
+ "endBinding": {
+ "elementId": "3a12c9cd83ac49d4af82",
+ "focus": 0.010665950554903025,
+ "gap": 2.79998779296875
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "index": "aa"
+ },
+ {
+ "id": "c967d534ee594bd8b6f7",
+ "type": "arrow",
+ "x": 91.42848423549094,
+ "y": 477.85716029575894,
+ "width": 176.19998168945312,
+ "height": 218.79998779296875,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1930751864,
+ "version": 148,
+ "versionNonce": 1475973022,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287133814,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 176.19998168945312,
+ -218.79998779296875
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "66d9bb7d39ed49bab977",
+ "focus": 0.8581630760314127,
+ "gap": 1
+ },
+ "endBinding": {
+ "elementId": "8a5c90e4c9214aff98d8",
+ "focus": 0.7537063280415125,
+ "gap": 20.600006103515625
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "index": "ab"
+ },
+ {
+ "id": "c9b3b417bdbf45d89ce0",
+ "type": "arrow",
+ "x": -222.85714285714266,
+ "y": 750.8571951729912,
+ "width": 120,
+ "height": 280,
+ "angle": 0,
+ "strokeColor": "#97D077",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 4,
+ "strokeStyle": "dashed",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1801585414,
+ "version": 120,
+ "versionNonce": 403226206,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 120,
+ 280
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": null,
+ "endBinding": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "index": "ac"
+ },
+ {
+ "id": "5f55e1df753d4fb2a6b9",
+ "type": "arrow",
+ "x": -102.85714285714266,
+ "y": 750.8571951729912,
+ "width": 0,
+ "height": 280,
+ "angle": 0,
+ "strokeColor": "#97D077",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 4,
+ "strokeStyle": "dashed",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1574452212,
+ "version": 51,
+ "versionNonce": 1398620830,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 280
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": null,
+ "endBinding": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "index": "ad"
+ },
+ {
+ "id": "7a7d7908178144bb97dd",
+ "type": "arrow",
+ "x": 17.142857142857338,
+ "y": 750.8571951729912,
+ "width": 120,
+ "height": 280,
+ "angle": 0,
+ "strokeColor": "#97D077",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 4,
+ "strokeStyle": "dashed",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1366313377,
+ "version": 51,
+ "versionNonce": 1914126046,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1777287137300,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -120,
+ 280
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": null,
+ "endBinding": null,
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "index": "ae"
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "gridStep": 5,
+ "gridModeEnabled": false,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
\ No newline at end of file
diff --git a/.doc/CSM-TCP-Router.png b/.doc/CSM-TCP-Router.png
new file mode 100644
index 0000000..37d8fab
Binary files /dev/null and b/.doc/CSM-TCP-Router.png differ
diff --git a/.doc/Protocol.v0.(zh-cn).md b/.doc/Protocol.v0.(zh-cn).md
index e4f5321..67ba818 100644
--- a/.doc/Protocol.v0.(zh-cn).md
+++ b/.doc/Protocol.v0.(zh-cn).md
@@ -200,6 +200,7 @@ sequenceDiagram
> [!NOTE]
> `status` 和 `interrupt` 两种订阅广播类型均受支持:
+>
> - `status`(`0x06`):普通广播,订阅模块的常规状态变化
> - `interrupt`(`0x07`):中断广播,订阅模块触发的中断事件
>
diff --git a/.doc/csm-tcp-router-client-console.excalidraw b/.doc/csm-tcp-router-client-console.excalidraw
new file mode 100644
index 0000000..91524ac
--- /dev/null
+++ b/.doc/csm-tcp-router-client-console.excalidraw
@@ -0,0 +1,14 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {"id":"u","type":"text","x":80,"y":60,"width":52,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":21,"version":1,"versionNonce":21,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"User","fontSize":20,"fontFamily":1,"textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"User","lineHeight":1.2},
+ {"id":"c","type":"text","x":290,"y":60,"width":150,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":22,"version":1,"versionNonce":22,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"Client Console","fontSize":20,"fontFamily":1,"textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"Client Console","lineHeight":1.2},
+ {"id":"r","type":"text","x":570,"y":60,"width":150,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":23,"version":1,"versionNonce":23,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"TCP Router","fontSize":20,"fontFamily":1,"textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"TCP Router","lineHeight":1.2},
+ {"id":"s","type":"text","x":830,"y":60,"width":104,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":24,"version":1,"versionNonce":24,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"Server Log","fontSize":20,"fontFamily":1,"textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"Server Log","lineHeight":1.2},
+ {"id":"note","type":"text","x":140,"y":180,"width":760,"height":72,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":25,"version":1,"versionNonce":25,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"Connect -> welcome(info) -> send cmd -> resp/async-resp -> Bye -> goodbye(info)","fontSize":24,"fontFamily":1,"textAlign":"left","verticalAlign":"top","containerId":null,"originalText":"Connect -> welcome(info) -> send cmd -> resp/async-resp -> Bye -> goodbye(info)","lineHeight":1.2}
+ ],
+ "appState": {"gridSize": null, "viewBackgroundColor": "#ffffff"},
+ "files": {}
+}
diff --git a/.doc/csm-tcp-router-client-console.png b/.doc/csm-tcp-router-client-console.png
new file mode 100644
index 0000000..e1998f3
Binary files /dev/null and b/.doc/csm-tcp-router-client-console.png differ
diff --git a/.doc/csm-tcp-router-command-sets.excalidraw b/.doc/csm-tcp-router-command-sets.excalidraw
new file mode 100644
index 0000000..4d00445
--- /dev/null
+++ b/.doc/csm-tcp-router-command-sets.excalidraw
@@ -0,0 +1,17 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {"id":"r0","type":"rectangle","x":420,"y":40,"width":240,"height":70,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#fff7ed","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":11,"version":1,"versionNonce":11,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t0","type":"text","x":470,"y":64,"width":140,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":12,"version":1,"versionNonce":12,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"Command Sets","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"Command Sets","lineHeight":1.2},
+ {"id":"r1","type":"rectangle","x":80,"y":200,"width":280,"height":82,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#eff6ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":13,"version":1,"versionNonce":13,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t1","type":"text","x":110,"y":230,"width":220,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":14,"version":1,"versionNonce":14,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"1) CSM Message APIs","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"1) CSM Message APIs","lineHeight":1.2},
+ {"id":"r2","type":"rectangle","x":430,"y":200,"width":280,"height":82,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#f0fdfa","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":15,"version":1,"versionNonce":15,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t2","type":"text","x":444,"y":230,"width":252,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":16,"version":1,"versionNonce":16,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"2) Router Management APIs","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"2) Router Management APIs","lineHeight":1.2},
+ {"id":"r3","type":"rectangle","x":780,"y":200,"width":280,"height":82,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#faf5ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":17,"version":1,"versionNonce":17,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t3","type":"text","x":820,"y":230,"width":200,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":18,"version":1,"versionNonce":18,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"3) Client Built-ins","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"3) Client Built-ins","lineHeight":1.2}
+ ],
+ "appState": {"gridSize": null, "viewBackgroundColor": "#ffffff"},
+ "files": {}
+}
diff --git a/.doc/csm-tcp-router-command-sets.png b/.doc/csm-tcp-router-command-sets.png
new file mode 100644
index 0000000..4ecf947
Binary files /dev/null and b/.doc/csm-tcp-router-command-sets.png differ
diff --git a/.doc/csm-tcp-router-framework.excalidraw b/.doc/csm-tcp-router-framework.excalidraw
new file mode 100644
index 0000000..30e1882
--- /dev/null
+++ b/.doc/csm-tcp-router-framework.excalidraw
@@ -0,0 +1,17 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {"id":"c1","type":"rectangle","x":40,"y":160,"width":190,"height":72,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#e8f4ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":1,"version":1,"versionNonce":1,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t1","type":"text","x":72,"y":186,"width":126,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":2,"version":1,"versionNonce":2,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"TCP Clients","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"TCP Clients","lineHeight":1.2},
+ {"id":"c2","type":"rectangle","x":290,"y":160,"width":230,"height":72,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#ecfeff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":3,"version":1,"versionNonce":3,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t2","type":"text","x":322,"y":186,"width":166,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":4,"version":1,"versionNonce":4,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"CSM-TCP-Router","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"CSM-TCP-Router","lineHeight":1.2},
+ {"id":"c3","type":"rectangle","x":580,"y":160,"width":220,"height":72,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#f5f3ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":5,"version":1,"versionNonce":5,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t3","type":"text","x":620,"y":186,"width":140,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":6,"version":1,"versionNonce":6,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"CSM Bus","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"CSM Bus","lineHeight":1.2},
+ {"id":"c4","type":"rectangle","x":860,"y":160,"width":210,"height":72,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#f0fdf4","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":{"type":3},"seed":7,"version":1,"versionNonce":7,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false},
+ {"id":"t4","type":"text","x":884,"y":186,"width":162,"height":24,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":0,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":8,"version":1,"versionNonce":8,"isDeleted":false,"boundElements":[],"updated":1,"link":null,"locked":false,"text":"CSM Modules","fontSize":20,"fontFamily":1,"textAlign":"center","verticalAlign":"middle","containerId":null,"originalText":"CSM Modules","lineHeight":1.2}
+ ],
+ "appState": {"gridSize": null, "viewBackgroundColor": "#ffffff"},
+ "files": {}
+}
diff --git a/.doc/image.png b/.doc/csm-tcp-router-framework.png
similarity index 100%
rename from .doc/image.png
rename to .doc/csm-tcp-router-framework.png
diff --git a/.doc/Protocol.v1.(zh-cn).md b/.doc/obsolete/Protocol.v1.(zh-cn).md
similarity index 100%
rename from .doc/Protocol.v1.(zh-cn).md
rename to .doc/obsolete/Protocol.v1.(zh-cn).md
diff --git a/.github/workflows/CSharp_SDK.yml b/.github/workflows/CSharp_SDK.yml
new file mode 100644
index 0000000..a402bde
--- /dev/null
+++ b/.github/workflows/CSharp_SDK.yml
@@ -0,0 +1,124 @@
+name: CSharp SDK
+
+on:
+ push:
+ paths:
+ - 'SDK/csharp/**'
+ - '.github/workflows/CSharp_SDK.yml'
+ tags:
+ - 'csharp-sdk-v*'
+ pull_request:
+ paths:
+ - 'SDK/csharp/**'
+ - '.github/workflows/CSharp_SDK.yml'
+ workflow_dispatch:
+
+defaults:
+ run:
+ working-directory: SDK/csharp
+
+jobs:
+ # -------------------------------------------------------------------------
+ # Build + test on multiple OSes
+ # -------------------------------------------------------------------------
+ test:
+ name: Build & Test (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET 8 SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore
+ run: dotnet restore CsmTcpRouter.sln
+
+ - name: Build
+ run: dotnet build CsmTcpRouter.sln -c Release --no-restore
+
+ - name: Test
+ run: dotnet test tests/CsmTcpRouter.Tests/CsmTcpRouter.Tests.csproj -c Release --no-build --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage"
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results-${{ matrix.os }}
+ path: SDK/csharp/tests/CsmTcpRouter.Tests/TestResults/
+ if-no-files-found: ignore
+
+ # -------------------------------------------------------------------------
+ # Pack the NuGet package
+ # -------------------------------------------------------------------------
+ pack:
+ name: Pack NuGet
+ runs-on: ubuntu-latest
+ needs: test
+ permissions:
+ contents: read
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET 8 SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Pack
+ run: dotnet pack src/CsmTcpRouter/CsmTcpRouter.csproj -c Release -o nupkg
+
+ - name: Upload NuGet artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: csharp-sdk-nupkg
+ path: SDK/csharp/nupkg/
+
+ # -------------------------------------------------------------------------
+ # Publish to NuGet.org (on tag push only)
+ # -------------------------------------------------------------------------
+ publish:
+ name: Publish to NuGet.org
+ runs-on: ubuntu-latest
+ needs: pack
+ if: startsWith(github.ref, 'refs/tags/csharp-sdk-v')
+ environment:
+ name: nuget
+ url: https://www.nuget.org/packages/CsmTcpRouter.Client/
+
+ permissions:
+ contents: read
+
+ steps:
+ - name: Download NuGet artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: csharp-sdk-nupkg
+ path: nupkg
+
+ - name: Set up .NET 8 SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Push to NuGet.org
+ # NUGET_API_KEY must be configured as a secret in the `nuget`
+ # environment. The workflow is gated by the `csharp-sdk-v*` tag
+ # filter above to prevent accidental publishes.
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ run: |
+ dotnet nuget push "nupkg/*.nupkg" \
+ --api-key "$NUGET_API_KEY" \
+ --source https://api.nuget.org/v3/index.json \
+ --skip-duplicate
+ working-directory: ${{ github.workspace }}
diff --git a/.github/workflows/C_SDK.yml b/.github/workflows/C_SDK.yml
new file mode 100644
index 0000000..81d4ee2
--- /dev/null
+++ b/.github/workflows/C_SDK.yml
@@ -0,0 +1,67 @@
+name: C SDK
+
+on:
+ push:
+ paths:
+ - 'SDK/c/**'
+ - '.github/workflows/C_SDK.yml'
+ tags:
+ - 'c-sdk-v*'
+ pull_request:
+ paths:
+ - 'SDK/c/**'
+ - '.github/workflows/C_SDK.yml'
+ workflow_dispatch:
+
+defaults:
+ run:
+ working-directory: SDK/c
+
+jobs:
+ # -------------------------------------------------------------------------
+ # Build & test on Linux, macOS, and Windows.
+ # -------------------------------------------------------------------------
+ build-test:
+ name: Build & test (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Configure (CMake)
+ run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
+
+ - name: Build
+ run: cmake --build build --config Release -j
+
+ - name: Run tests (ctest)
+ run: ctest --test-dir build --output-on-failure -C Release
+
+ # -------------------------------------------------------------------------
+ # Sanitizer build on Linux to catch undefined behaviour, leaks, and races.
+ # -------------------------------------------------------------------------
+ sanitizers:
+ name: Sanitizers (Linux)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Configure with ASan + UBSan
+ run: |
+ cmake -S . -B build-san \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -O1 -g" \
+ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
+
+ - name: Build (sanitizers)
+ run: cmake --build build-san -j
+
+ - name: Run tests (sanitizers)
+ run: ctest --test-dir build-san --output-on-failure
diff --git a/.github/workflows/Python_SDK.yml b/.github/workflows/Python_SDK.yml
new file mode 100644
index 0000000..d12bbda
--- /dev/null
+++ b/.github/workflows/Python_SDK.yml
@@ -0,0 +1,157 @@
+name: Python SDK
+
+on:
+ push:
+ paths:
+ - 'SDK/python/**'
+ tags:
+ - 'python-sdk-v*'
+ pull_request:
+ paths:
+ - 'SDK/python/**'
+ workflow_dispatch:
+
+defaults:
+ run:
+ working-directory: SDK/python
+
+jobs:
+ # -------------------------------------------------------------------------
+ # Lint
+ # -------------------------------------------------------------------------
+ lint:
+ name: Lint (ruff)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install ruff
+ run: pip install ruff
+
+ - name: Run ruff
+ run: ruff check src/ tests/ examples/
+
+ # -------------------------------------------------------------------------
+ # Test matrix
+ # -------------------------------------------------------------------------
+ test:
+ name: Test (Python ${{ matrix.python-version }})
+ runs-on: ubuntu-latest
+ needs: lint
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install package and test dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .
+ pip install pytest pytest-cov pytest-asyncio
+
+ - name: Run tests
+ run: pytest --cov=csm_tcp_router_client --cov-report=term-missing
+
+ # -------------------------------------------------------------------------
+ # Build (wheel + sdist)
+ # -------------------------------------------------------------------------
+ build:
+ name: Build distribution
+ runs-on: ubuntu-latest
+ needs: test
+ permissions:
+ contents: read
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install build tools
+ run: pip install build
+
+ - name: Build wheel and sdist
+ run: python -m build
+
+ - name: Verify wheel is importable
+ run: |
+ pip install dist/*.whl
+ python -c "import csm_tcp_router_client; print('Version:', csm_tcp_router_client.__version__)"
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: python-sdk-dist
+ path: SDK/python/dist/
+
+ # -------------------------------------------------------------------------
+ # Publish to TestPyPI (on tag push only – gates production publish)
+ # -------------------------------------------------------------------------
+ publish-testpypi:
+ name: Publish to TestPyPI
+ runs-on: ubuntu-latest
+ needs: build
+ if: startsWith(github.ref, 'refs/tags/python-sdk-v')
+ environment:
+ name: testpypi
+ url: https://test.pypi.org/project/csm-tcp-router-client/
+
+ permissions:
+ id-token: write # required for OIDC trusted publishing
+
+ steps:
+ - name: Download build artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: python-sdk-dist
+ path: dist/
+
+ - name: Publish to TestPyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ repository-url: https://test.pypi.org/legacy/
+
+ # -------------------------------------------------------------------------
+ # Publish to PyPI (on tag push only – after TestPyPI succeeds)
+ # -------------------------------------------------------------------------
+ publish:
+ name: Publish to PyPI
+ runs-on: ubuntu-latest
+ needs: publish-testpypi
+ if: startsWith(github.ref, 'refs/tags/python-sdk-v')
+ environment:
+ name: pypi
+ url: https://pypi.org/project/csm-tcp-router-client/
+
+ permissions:
+ id-token: write # required for trusted publishing (OIDC)
+
+ steps:
+ - name: Download build artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: python-sdk-dist
+ path: dist/
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.gitignore b/.gitignore
index 10380b3..105600d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,4 +67,25 @@ Thumbs.db
._*
.Spotlight-V100
.Trashes
-ehthumbs.db
\ No newline at end of file
+ehthumbs.db
+
+# C# / .NET
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+*.user
+*.suo
+*.userosscache
+*.sln.docstates
+.vs/
+*.nupkg
+*.snupkg
+nupkg/
+TestResults/
+[Tt]est[Rr]esults/
+coverage.cobertura.xml
+*.received.*
+project.lock.json
+project.fragment.lock.json
+artifacts/
\ No newline at end of file
diff --git a/CSM-TCP-Router.lvproj b/CSM-TCP-Router.lvproj
index 123e3ed..92feb53 100644
--- a/CSM-TCP-Router.lvproj
+++ b/CSM-TCP-Router.lvproj
@@ -89,6 +89,7 @@
+
@@ -105,6 +106,9 @@
+
+
+
diff --git a/README(zh-cn).md b/README(zh-cn).md
index 484cbfc..0c2353f 100644
--- a/README(zh-cn).md
+++ b/README(zh-cn).md
@@ -2,92 +2,77 @@
[English](./README.md) | [中文](./README(zh-cn).md)
-本仓库演示如何构建一个可复用的TCP通讯层(CSM-TCP-Router),将本地程序变成可远程控制的TCP服务器,展示了CSM框架隐形总线机制的优势。
+CSM-TCP-Router 是一个可复用的 CSM TCP 通讯层。
+它通过 CSM 隐形总线机制,将本地 CSM 程序转换为可远程控制的 TCP 服务器。
-## 功能介绍
+## 功能特性
-
+
-- 所有本地可发送的CSM消息,均可通过TCP连接以CSM同步或异步格式转发给本地程序。
-- 基于JKI-TCP-Server库,支持多个TCP客户端同时连接。
-- [client] 提供一个标准的TCP客户端,可以连接到服务器,验证远程连接、消息发送等功能。
+图示对应的 Excalidraw 源文件位于 `.doc/*.excalidraw`。
+
+- 所有本地可发送的 CSM 消息都可通过 TCP 以同步或异步格式转发。
+- 基于 JKI TCP Server,支持多个客户端并发连接。
+- 仓库内置标准客户端,可用于连接与指令联调验证。
## 通讯协议
-CSM-TCP-Router 中 TCP 数据包格式定义如下:
+TCP 数据包格式如下:
```
-| 数据长度(4B) | 版本(1B) | TYPE(1B) | FLAG1(1B) | FLAG2(1B) | 文本数据 |
-╰─────────────────────────── 包头 ──────────────────────────╯╰──── 数据长度范围 ─────╯
+| 数据长度(4B) | 版本(1B) | TYPE(1B) | FLAG1(1B) | FLAG2(1B) | 文本数据 |
+╰─────────────────────────── 包头 ───────────────────────────╯╰──── 数据长度范围 ────╯
```
-数据包类型字段为枚举值,用于描述数据包内容,目前支持以下类型:
+`TYPE` 字段当前支持:
-- 信息数据包(`info`) - `0x00`:服务端在客户端连接时(欢迎消息)和连接关闭时(告别消息)发送
-- 错误数据包(`error`) - `0x01`
-- 指令数据包(`cmd`) - `0x02`
-- 指令响应数据包(`cmd-resp`) - `0x03`
-- 同步响应数据包(`resp`) - `0x04`
-- 异步响应数据包(`async-resp`) - `0x05`
-- 状态广播数据包(`status`) - `0x06`
-- 中断广播数据包(`interrupt`) - `0x07`
+- `info` (`0x00`):客户端连接(欢迎)和断开(告别)时发送
+- `error` (`0x01`)
+- `cmd` (`0x02`)
+- `cmd-resp` (`0x03`)
+- `resp` (`0x04`)
+- `async-resp` (`0x05`)
+- `status` (`0x06`)
+- `interrupt` (`0x07`)
-详细的通讯协议定义, 见 [协议设计](.doc/Protocol.v0.(zh-cn).md)
+完整协议请见 [协议设计](.doc/Protocol.v0.(zh-cn).md)。
## 指令集
-
-
-### 1. CSM 消息指令集
-
-由现有基于CSM开发的代码定义。CSM框架通过隐形总线传递消息,无需侵入原有代码即可实现远程通讯。
-
-例如,本程序中的AI CSM模块提供了:
-
-- `Channels`: 列出所有的通道
-- `Read`:读取指定通道的值
-- `read all`:读取所有通道的值
-
-这些消息可以通过TCP连接发送给本地程序,实现远程控制。
+
-### 2. CSM-TCP-Router 指令集
+### 1)CSM 消息 API
-由CSM-TCP-Router通讯层定义,通过指令暴露CSM模块的管理功能,实现远程控制。
+由现有 CSM 业务代码定义,可经 Router 直接转发,无需侵入式改造。
-- `List`: 列出所有CSM模块
-- `List API`: 列出指定模块的所有API
-- `List State`: 列出指定模块的所有CSM状态
-- `Help`: 显示模块的帮助文件(存储在CSM VI的Documentation字段)
-- `Refresh lvcsm`: 刷新缓存文件
+### 2)Router 管理 API
-### [Client Only] 3. CSM-TCP-Router Client 指令集
+由 CSM-TCP-Router 定义,用于模块管理与运行态查询。
-代码中内置了一个标准的CSM-TCP-Router客户端,包含一些专属内置指令,这些指令在基于指令集API进行二次开发时无法使用。
+### 3)Client 内建指令(仅客户端)
-- `Bye`: 断开连接
-- `Switch`:切换模块,便于输入时省略模块名,不带参数时切换回默认模式
-- TAB键: 自动定位到输入对话框
+由内置客户端提供,不属于二次开发时可扩展的指令集 API。
-
+
## 使用方法
-1. 在VIPM中安装本工具及依赖
-2. 在CSM的范例中打开范例工程CSM-TCP-Router.lvproj
-3. 启动代码工程中的CSM-TCP-Router(Server).vi
-4. 启动Client.vi,输入服务器的IP地址和端口号,点击连接
-5. 输入指令,点击发送,可以在控制台看到返回的消息
-6. 在Server程序的界面log中,可以看到执行过的历史消息
-7. 在Client.vi中输入`Bye`断开连接
-8. 关闭Server程序
+1. 在 VIPM 中安装本工具及依赖。
+2. 在 CSM 范例中打开 `CSM-TCP-Router.lvproj`。
+3. 启动 `CSM-TCP-Router(Server).vi`。
+4. 启动 `Client.vi`,输入服务端 IP 和端口并连接。
+5. 发送指令,在客户端控制台查看返回消息。
+6. 在服务端日志界面查看历史执行记录。
+7. 在 `Client.vi` 中输入 `Bye` 断开连接。
+8. 关闭服务端程序。
### 下载
-通过VIPM搜索CSM TCP Router,即可下载安装。
+在 VIPM 中搜索 `CSM TCP Router` 并安装。
### 依赖
-- Communicable State Machine(CSM) - NEVSTOP
+- Communicable State Machine (CSM) - NEVSTOP
- JKI TCP Server - JKI
- Global Stop - NEVSTOP
- OpenG
diff --git a/README.md b/README.md
index 80c19d0..2df5371 100644
--- a/README.md
+++ b/README.md
@@ -2,88 +2,73 @@
[English](./README.md) | [中文](./README(zh-cn).md)
-This repository demonstrates how to build a reusable TCP communication layer—CSM-TCP-Router—that turns a local program into a TCP server for remote control, showcasing the power of the CSM framework's invisible bus.
+CSM-TCP-Router is a reusable TCP communication layer for CSM applications.
+It turns a local CSM program into a remotely controllable TCP server through the CSM invisible bus mechanism.
## Features
-
+
-- Any CSM message that can be sent locally can also be transmitted to the local program over TCP, using CSM synchronous and asynchronous message formats.
-- Based on the JKI-TCP-Server library, it supports multiple TCP clients connecting simultaneously.
-- [client] Provides a standard TCP client that can connect to the server to verify remote connections and message sending.
+Excalidraw source files for the diagrams are in `.doc/*.excalidraw`.
+
+- Any CSM message available locally can be forwarded through TCP in synchronous or asynchronous format.
+- Based on JKI TCP Server, it supports multiple concurrent client connections.
+- The repository includes a standard client for connection and command verification.
## Protocol
-The TCP packet format used in the CSM-TCP-Router is defined as follows:
+TCP packet format:
```
-| Data Length (4B) | Version (1B) | TYPE (1B) | FLAG1 (1B) | FLAG2 (1B) | Text Data |
-╰───────────────────────────────── Header ──────────────────────────────╯╰─── Data Length Range ──╯
+| Data Length (4B) | Version (1B) | TYPE (1B) | FLAG1 (1B) | FLAG2 (1B) | Text Data |
+╰──────────────────────────── Header ────────────────────────────╯╰─ Data Length Range ─╯
```
-This field specifies the packet type as an enumerated value. Supported types are:
-
-- Information Packet (`info`) - `0x00`: Sent by the server when a client connects (welcome message) and when the connection is closed (goodbye message)
-- Error Packet (`error`) - `0x01`
-- Command Packet (`cmd`) - `0x02`
-- Command Response Packet (`cmd-resp`) - `0x03`
-- Synchronous Response Packet (`resp`) - `0x04`
-- Asynchronous Response Packet (`async-resp`) - `0x05`
-- Status Broadcast Packet (`status`) - `0x06`
-- Interrupt Broadcast Packet (`interrupt`) - `0x07`
-
-For detailed communication protocol definitions, see [Protocol Design](.doc/Protocol.v0.(en).md).
-
-## Supported Command Sets
-
-
-
-### 1. CSM Message Command Set
+Supported packet `TYPE` values:
-Defined by the existing CSM-based application code. Because the CSM framework uses an invisible bus for message passing, remote communication requires no intrusive changes to the existing code.
+- `info` (`0x00`): sent on connect (welcome) and disconnect (goodbye)
+- `error` (`0x01`)
+- `cmd` (`0x02`)
+- `cmd-resp` (`0x03`)
+- `resp` (`0x04`)
+- `async-resp` (`0x05`)
+- `status` (`0x06`)
+- `interrupt` (`0x07`)
-For example, the AI CSM module in this program provides:
+See [Protocol Design](.doc/Protocol.v0.(en).md) for full details.
-- `Channels`: List all channels
-- `Read`: Read the value of a specified channel
-- `read all`: Read the values of all channels
+## Command Sets
-These messages can be sent to the local program via TCP connection for remote control.
+
-### 2. CSM-TCP-Router Command Set
+### 1) CSM Message APIs
-Defined by the CSM-TCP-Router layer. These commands expose the management functions of CSM modules for remote control.
+Defined by existing CSM-based application code and forwarded through the router without intrusive changes.
-- `List`: List all CSM modules
-- `List API`: List all APIs of a specified module
-- `List State`: List all CSM states of a specified module
-- `Help`: Display the help file of the module, stored in the Documentation field of the CSM VI
-- `Refresh lvcsm`: Refresh the cache file
+### 2) Router Management APIs
-### [Client Only] 3. CSM-TCP-Router Client Command Set
+Defined by CSM-TCP-Router for module management and runtime inspection.
-The bundled standard CSM-TCP-Router Client includes additional built-in commands that are not available when building on the command set API.
+### 3) Client Built-ins (Client only)
-- `Bye`: Disconnect
-- `Switch`: Switch the active module to omit the module name when entering commands; omit the parameter to switch back to the default mode
-- TAB key: Automatically focus on the input dialog box
+Built into the bundled client and not available through secondary API development.
-
+
## Usage
-1. Install this tool and dependencies via VIPM
-2. Open the example project CSM-TCP-Router.lvproj in the CSM examples
-3. Start the CSM-TCP-Router(Server).vi in the code project
-4. Start Client.vi, enter the server's IP address and port number, and click connect
-5. Enter commands and click send to see the returned messages in the console
-6. View the history of executed messages in the log interface of the Server program
-7. Enter `Bye` in Client.vi to disconnect
-8. Close the Server program
+1. Install this package and dependencies in VIPM.
+2. Open `CSM-TCP-Router.lvproj` from CSM examples.
+3. Run `CSM-TCP-Router(Server).vi`.
+4. Run `Client.vi`, enter server IP/port, and connect.
+5. Send commands and check returned messages in the client console.
+6. Check execution history in the server log panel.
+7. Enter `Bye` in `Client.vi` to disconnect.
+8. Stop the server.
### Download
-Search for CSM TCP Router in VIPM to download and install.
+Search `CSM TCP Router` in VIPM and install.
### Dependencies
diff --git a/SDK/PythonClientAPI/README.md b/SDK/PythonClientAPI/README.md
deleted file mode 100644
index 78a0e18..0000000
--- a/SDK/PythonClientAPI/README.md
+++ /dev/null
@@ -1,166 +0,0 @@
-# CSM-TCP-Router Python Client API
-
-这是一个Python版本的CSM-TCP-Router客户端API,实现了与LabVIEW版本相同的功能,可以连接到CSM-TCP-Router服务器,发送命令并接收响应。
-
-## 功能特性
-
-- 与CSM-TCP-Router服务器建立TCP连接
-- 发送同步命令并等待回复
-- 发送异步命令
-- 发送无返回异步命令
-- Ping服务器
-- 订阅状态变化通知
-- 等待服务器可用
-- 完整的错误处理和线程安全设计
-
-## 文件结构
-
-- `tcp_router_client.py`: 主要的客户端API类实现
-- `example_usage.py`: 使用示例代码
-- `README.md`: 使用说明文档
-
-## 使用方法
-
-### 基本连接
-
-```python
-from tcp_router_client import TcpRouterClient
-
-# 创建客户端实例
-client = TcpRouterClient()
-
-# 连接到服务器
-if client.connect("localhost", 9999):
- print("连接成功")
- # 执行操作...
-
- # 断开连接
- client.disconnect()
-else:
- print("连接失败")
-```
-
-### 发送同步命令
-
-```python
-# 发送命令并等待回复
-response = client.send_message_and_wait_for_reply("List")
-print(f"回复: {response}")
-```
-
-### 发送异步命令
-
-```python
-# 发送异步命令
-async_cmd = "API: Read Channels -> AI"
-client.post_message(async_cmd)
-
-# 发送无返回异步命令
-no_rep_cmd = "API: Refresh ->| System"
-client.post_no_rep_message(no_rep_cmd)
-```
-
-### 订阅状态变化
-
-```python
-# 定义状态变化回调函数
-def status_callback(status_data):
- print(f"收到状态更新: {status_data}")
-
-# 注册状态变化通知
-client.register_status_change("Status", "AI", status_callback)
-
-# 取消订阅
-client.unregister_status_change("Status", "AI")
-```
-
-### 等待服务器可用
-
-```python
-# 等待服务器可用,最多等待30秒
-success = client.wait_for_server("localhost", 9999, timeout=30)
-if success:
- print("服务器已可用")
- client.connect("localhost", 9999)
-```
-
-### Ping服务器
-
-```python
-# Ping服务器,检查连接状态
-success, elapsed = client.ping()
-if success:
- print(f"Ping成功,延迟: {elapsed*1000:.2f}ms")
-```
-
-## 通讯协议
-
-Python客户端实现了与CSM-TCP-Router相同的通讯协议,数据包格式如下:
-
-```
-| 数据长度(4B) | 版本(1B) | TYPE(1B) | FLAG1(1B) | FLAG2(1B) | 文本数据 |
-╰─────────────────────────── 包头 ──────────────────────────╯╰──── 数据长度字范围 ────╯
-```
-
-支持的数据包类型:
-- 信息数据包(info) - `0x00`
-- 错误数据包(error) - `0x01`
-- 指令数据包(cmd) - `0x02`
-- 同步响应数据包(resp) - `0x03`
-- 异步响应数据包(async-resp) - `0x04`
-- 订阅返回数据包(status) - `0x05`
-
-## 支持的指令集
-
-### 1. CSM 消息指令集
-由原有基于CSM开发的代码定义,支持:
-- 同步消息 (-@)
-- 异步消息 (->)
-- 无返回异步消息 (->|)
-
-### 2. CSM-TCP-Router 指令集
-- `List` - 列出所有的CSM模块
-- `List API`: 列出指定模块的所有API
-- `List State`: 列出指定模块的所有CSM状态
-- `Help` - 显示模块的帮助文件
-- `Refresh lvcsm`: 刷新缓存文件
-- `Ping` - 测试服务器连接
-
-## 注意事项
-
-1. 确保在使用完客户端后调用`disconnect()`或`release()`方法释放资源
-2. 回调函数将在接收线程中执行,避免在回调函数中执行长时间阻塞操作
-3. 当网络连接异常断开时,客户端会自动将`connected`标志设为False
-4. 对于频繁发送消息的场景,建议使用连接池或重用同一个客户端实例
-
-## 示例程序
-
-请参考`example_usage.py`文件,其中包含了详细的使用示例。
-
-## 依赖项
-
-本客户端API仅使用Python标准库,无需安装额外依赖:
-- `socket`: 用于TCP通信
-- `struct`: 用于解析数据包
-- `threading`: 用于多线程处理
-- `queue`: 用于线程间通信
-- `json`: 用于数据序列化(预留)
-- `time`: 用于超时和延时
-- `enum`: 用于定义数据包类型枚举
-
-## 与LabVIEW版本对比
-
-此Python版本实现了LabVIEW版本ClientAPI的所有核心功能:
-- `Obtain.vi` -> `__init__()` 和 `obtain()`
-- `Release.vi` -> `release()`
-- `Send Message and Wait for Reply.vi` -> `send_message_and_wait_for_reply()`
-- `Post Message.vi` -> `post_message()`
-- `Post No-Rep Message.vi` -> `post_no_rep_message()`
-- `Ping.vi` -> `ping()`
-- `Register Status Change.vi` -> `register_status_change()`
-- `Unregister Status Change.vi` -> `unregister_status_change()`
-- `Wait for Server.vi` -> `wait_for_server()`
-
-## 版本历史
-
-- v1.0.0: 初始版本,实现基本功能
\ No newline at end of file
diff --git a/SDK/PythonClientAPI/example_usage.py b/SDK/PythonClientAPI/example_usage.py
deleted file mode 100644
index 5569d7c..0000000
--- a/SDK/PythonClientAPI/example_usage.py
+++ /dev/null
@@ -1,103 +0,0 @@
-import time
-from tcp_router_client import TcpRouterClient
-
-"""CSM-TCP-Router Python客户端API使用示例"""
-
-def main():
- # 创建客户端实例
- client = TcpRouterClient()
-
- print("CSM-TCP-Router Python客户端API示例")
- print("================================")
-
- # 示例1: 连接到服务器
- print("\n示例1: 连接到服务器")
- if client.connect("localhost", 30007):
- print("✅ 成功连接到服务器")
- else:
- print("❌ 连接服务器失败,请确保服务器已启动")
- return
-
- # 示例2: Ping服务器
- print("\n示例2: Ping服务器")
- success, elapsed = client.ping(timeout=2)
- if success:
- print(f"✅ Ping成功,延迟: {elapsed*1000:.2f}ms")
- else:
- print("❌ Ping失败")
-
- # 示例3: 发送同步命令并等待回复
- print("\n示例3: 发送同步命令并等待回复")
- # 列出所有CSM模块
- response = client.send_message_and_wait_for_reply("List")
- print(f"命令: List")
- print(f"回复: {response}")
-
- # 列出特定模块的API
- # 注意:这里假设存在名为"AI"的模块,如果不存在,您需要修改为实际存在的模块名
- module_name = "AI"
- response = client.send_message_and_wait_for_reply(f"List API {module_name}")
- print(f"\n命令: List API {module_name}")
- print(f"回复: {response}")
-
- # 示例4: 发送异步命令
- print("\n示例4: 发送异步命令")
- # 注意:这里的命令需要根据实际的CSM模块进行调整
- async_cmd = "API: Read Channels -> AI"
- success = client.post_message(async_cmd)
- print(f"命令: {async_cmd}")
- print(f"发送结果: {'✅ 成功' if success else '❌ 失败'}")
-
- # 示例5: 发送无返回异步命令
- print("\n示例5: 发送无返回异步命令")
- # 注意:这里的命令需要根据实际的CSM模块进行调整
- no_rep_cmd = "API: Refresh ->| System"
- success = client.post_no_rep_message(no_rep_cmd)
- print(f"命令: {no_rep_cmd}")
- print(f"发送结果: {'✅ 成功' if success else '❌ 失败'}")
-
- # 示例6: 订阅状态变化
- print("\n示例6: 订阅状态变化")
- # 状态变化回调函数
- def status_callback(status_data):
- print(f"📢 收到状态更新: {status_data}")
-
- # 注册状态变化通知
- # 注意:这里假设存在名为"AI"的模块和"Status"状态,如果不存在,您需要修改为实际存在的模块名和状态名
- success = client.register_status_change("Status", "AI", status_callback)
- print(f"订阅 'Status@AI' 结果: {'✅ 成功' if success else '❌ 失败'}")
-
- # 保持连接一段时间,等待状态更新
- print("\n等待5秒,观察状态更新...")
- time.sleep(5)
-
- # 取消订阅
- success = client.unregister_status_change("Status", "AI")
- print(f"取消订阅 'Status@AI' 结果: {'✅ 成功' if success else '❌ 失败'}")
-
- # 示例7: 等待服务器可用
- print("\n示例7: 等待服务器可用(演示用,当前已连接)")
- # 断开当前连接
- client.disconnect()
- print("已断开连接")
-
- # 等待服务器可用
- print("等待服务器可用,最多等待10秒...")
- # 注意:如果服务器未运行,这个调用将会超时
- success = client.wait_for_server("localhost", 9999, timeout=10)
- print(f"服务器可用检查结果: {'✅ 服务器可用' if success else '❌ 服务器不可用'}")
-
- # 重新连接(如果服务器可用)
- if success:
- client.connect("localhost", 9999)
- print("✅ 已重新连接到服务器")
-
- # 示例8: 释放资源
- print("\n示例8: 释放资源")
- client.release()
- print("✅ 客户端资源已释放")
-
- print("\n示例执行完毕")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/SDK/PythonClientAPI/tcp_router_client.py b/SDK/PythonClientAPI/tcp_router_client.py
deleted file mode 100644
index 0780e4a..0000000
--- a/SDK/PythonClientAPI/tcp_router_client.py
+++ /dev/null
@@ -1,259 +0,0 @@
-import socket
-import struct
-import threading
-import queue
-import json
-import time
-from enum import Enum
-
-class PacketType(Enum):
- INFO = 0x00
- ERROR = 0x01
- CMD = 0x02
- RESP = 0x03
- ASYNC_RESP = 0x04
- STATUS = 0x05
-
-class TcpRouterClient:
- def __init__(self):
- self.socket = None
- self.connected = False
- self.host = ""
- self.port = 0
- self.recv_thread = None
- self.stop_event = threading.Event()
- self.response_queue = queue.Queue()
- self.async_response_callbacks = {}
- self.status_callbacks = {}
- self.async_response_queue = queue.Queue()
- self.status_queue = queue.Queue()
- self.lock = threading.Lock()
-
- def connect(self, host, port, timeout=5):
- """连接到CSM-TCP-Router服务器"""
- try:
- self.host = host
- self.port = port
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.socket.settimeout(timeout)
- self.socket.connect((host, port))
- self.connected = True
- self.stop_event.clear()
- self.recv_thread = threading.Thread(target=self._receive_thread)
- self.recv_thread.daemon = True
- self.recv_thread.start()
- return True
- except Exception as e:
- print(f"连接失败: {e}")
- self.connected = False
- return False
-
- def disconnect(self):
- """断开与服务器的连接"""
- if self.connected:
- self.stop_event.set()
- try:
- if self.socket:
- self.socket.close()
- except:
- pass
- self.connected = False
- if self.recv_thread:
- self.recv_thread.join(timeout=2)
-
- def send_message(self, message, packet_type, flag1=0, flag2=0):
- """发送消息到服务器"""
- if not self.connected:
- print("未连接到服务器")
- return False
-
- try:
- # 确保消息为字节类型
- if isinstance(message, str):
- message_bytes = message.encode() # 使用系统默认编码
- else:
- message_bytes = message
-
- # 计算数据长度
- data_len = len(message_bytes)
- # 构建数据包
- header = struct.pack('!IBBBB', data_len, 0x01, packet_type.value, flag1, flag2)
- # 发送数据包
- with self.lock:
- self.socket.sendall(header)
- self.socket.sendall(message_bytes)
- return True
- except Exception as e:
- print(f"发送消息失败: {e}")
- self.connected = False
- return False
-
- def send_message_and_wait_for_reply(self, message, timeout=5):
- """发送消息并等待回复"""
- if not self.send_message(message, PacketType.CMD):
- return None
-
- try:
- response = self.response_queue.get(timeout=timeout)
- return response
- except queue.Empty:
- print("等待回复超时")
- return None
-
- def post_message(self, message):
- """发送异步消息"""
- return self.send_message(message, PacketType.CMD)
-
- def post_no_rep_message(self, message):
- """发送无返回异步消息"""
- return self.send_message(message, PacketType.CMD)
-
- def ping(self, timeout=2):
- """Ping服务器"""
- start_time = time.time()
- response = self.send_message_and_wait_for_reply("Ping", timeout=timeout)
- if response:
- elapsed = time.time() - start_time
- return True, elapsed
- return False, 0
-
- def register_status_change(self, status_name, module_name, callback=None):
- """注册状态变化通知"""
- cmd = f"{status_name}@{module_name} ->"
- success = self.send_message(cmd, PacketType.CMD)
- if success and callback:
- with self.lock:
- self.status_callbacks[(status_name, module_name)] = callback
- return success
-
- def unregister_status_change(self, status_name, module_name):
- """取消注册状态变化通知"""
- cmd = f"{status_name}@{module_name} ->"
- success = self.send_message(cmd, PacketType.CMD)
- if success:
- with self.lock:
- key = (status_name, module_name)
- if key in self.status_callbacks:
- del self.status_callbacks[key]
- return success
-
- def wait_for_server(self, host, port, timeout=30):
- """等待服务器可用"""
- start_time = time.time()
- while time.time() - start_time < timeout:
- if self.connect(host, port, timeout=1):
- self.disconnect()
- return True
- time.sleep(0.5)
- return False
-
- def _receive_thread(self):
- """接收线程,处理来自服务器的消息"""
- while not self.stop_event.is_set():
- try:
- # 接收包头
- header = self._receive_all(8) # 4+1+1+1+1=8字节
- if not header:
- break
-
- # 解析包头
- data_len, version, packet_type, flag1, flag2 = struct.unpack('!IBBBB', header)
-
- # 接收数据(保持字节类型)
- data = self._receive_all(data_len)
-
- # 处理不同类型的数据包
- if packet_type == PacketType.RESP.value:
- self.response_queue.put(data)
- elif packet_type == PacketType.ASYNC_RESP.value:
- self._handle_async_response(data)
- elif packet_type == PacketType.STATUS.value:
- self._handle_status(data)
- elif packet_type == PacketType.INFO.value:
- print(f"[INFO] {data}")
- elif packet_type == PacketType.ERROR.value:
- print(f"[ERROR] {data}")
-
- except Exception as e:
- if not self.stop_event.is_set():
- print(f"接收数据错误: {e}")
- break
-
- # 线程结束,标记断开连接
- self.connected = False
-
- def _receive_all(self, size):
- """接收指定大小的数据"""
- data = b''
- while len(data) < size:
- packet = self.socket.recv(size - len(data))
- if not packet:
- return b''
- data += packet
- return data
-
- def _handle_async_response(self, data):
- """处理异步响应"""
- self.async_response_queue.put(data)
- # 这里可以根据需要调用注册的回调函数
- # 例如,可以解析data中的信息,找到对应的回调函数并调用
-
- def _handle_status(self, data):
- """处理状态更新"""
- self.status_queue.put(data)
- # 解析状态数据并调用相应的回调函数
- # 简化处理,实际应用中可能需要更复杂的解析逻辑
- parts = data.split(' >> ', 1)
- if len(parts) == 2:
- status_info, _ = parts
- status_parts = status_info.split(' <- ', 1)
- if len(status_parts) == 2:
- status_name, module_name = status_parts
- with self.lock:
- callback = self.status_callbacks.get((status_name, module_name))
- if callback:
- callback(data)
-
- def obtain(self):
- """获取客户端实例(模拟LabVIEW的Obtain.vi)"""
- # 在Python中,这个方法可以简单返回自身实例
- return self
-
- def release(self):
- """释放客户端资源(模拟LabVIEW的Release.vi)"""
- self.disconnect()
-
-# 示例用法
-if __name__ == "__main__":
- client = TcpRouterClient()
-
- # 连接服务器
- if client.connect("localhost", 30007):
- print("连接成功")
-
- # 发送Ping命令
- success, elapsed = client.ping()
- if success:
- print(f"Ping成功,延迟: {elapsed*1000:.2f}ms")
-
- # 发送命令并等待回复
- response = client.send_message_and_wait_for_reply("List")
- print(f"List命令回复: {response}")
-
- # 订阅状态变化
- def status_callback(data):
- print(f"收到状态更新: {data}")
-
- client.register_status_change("Status", "AI", status_callback)
-
- # 保持连接一段时间
- time.sleep(5)
-
- # 取消订阅
- client.unregister_status_change("Status", "AI")
-
- # 断开连接
- client.disconnect()
- print("已断开连接")
- else:
- print("连接失败")
\ No newline at end of file
diff --git a/SDK/c/.gitignore b/SDK/c/.gitignore
new file mode 100644
index 0000000..f5ae8e1
--- /dev/null
+++ b/SDK/c/.gitignore
@@ -0,0 +1,34 @@
+# CMake / build artefacts
+build/
+build-*/
+out/
+cmake-build-*/
+
+# Visual Studio
+vs2026/build/
+*.user
+*.suo
+*.sdf
+*.opensdf
+*.aps
+*.ipch
+.vs/
+[Dd]ebug/
+[Rr]elease/
+[Xx]64/
+[Ww]in32/
+*.obj
+*.lib
+*.dll
+*.exe
+*.pdb
+*.idb
+*.ilk
+*.exp
+
+# Compiled objects
+*.o
+*.a
+*.so
+*.so.*
+*.dylib
diff --git a/SDK/c/CHANGELOG.md b/SDK/c/CHANGELOG.md
new file mode 100644
index 0000000..fa92df1
--- /dev/null
+++ b/SDK/c/CHANGELOG.md
@@ -0,0 +1,42 @@
+# Changelog
+
+All notable changes to the C `csm-tcp-router-client` SDK will be
+documented in this file. The format is based on
+[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
+project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.1.0] - 2026-04-27
+
+Initial public release of the C SDK.
+
+### Added
+
+- Cross-platform TCP client implementation (Windows + POSIX).
+ - Single source file `src/csm_tcp_router_client.c`.
+ - Single header `include/csm_tcp_router_client.h`.
+ - Background receive thread with thread-safe public API.
+- CSM-TCP-Router protocol v0 codec (`csm_encode_packet`,
+ `csm_decode_header`, `csm_parse_packet`).
+- Synchronous command API (`csm_client_send_and_wait`).
+- Asynchronous command API (`csm_client_post`,
+ `csm_client_post_no_reply`) with callback + polling-queue delivery.
+- Status / interrupt subscription API
+ (`csm_client_subscribe_status` / `csm_client_unsubscribe_status`,
+ `csm_client_poll_status`).
+- Router management helpers: `csm_client_list_modules`,
+ `csm_client_list_api`, `csm_client_list_states`, `csm_client_help`.
+- Connection utilities: `csm_client_ping`,
+ `csm_client_wait_for_server`, `csm_client_is_connected`.
+- Server error inspection via `csm_client_last_server_error`.
+- Examples: `examples/basic_usage.c`, `examples/subscribe_status.c`.
+- Test suite using an in-process `MockServer` fixture
+ (`tests/mock_server.[ch]`) and a tiny custom test harness
+ (`tests/test_harness.h` + `tests/test_main.c`):
+ protocol codec tests, client-lifecycle tests, end-to-end integration
+ tests.
+- CMake build (`CMakeLists.txt`) with options for tests, examples, and
+ shared-library output; `ctest` integration.
+- Visual Studio 2026 (toolset `v144`) solution + projects under
+ `vs2026/` for IDE-driven build & test on Windows.
+- GitHub Actions CI workflow (`.github/workflows/C_SDK.yml`) building
+ and running tests on Ubuntu, Windows and macOS.
diff --git a/SDK/c/CMakeLists.txt b/SDK/c/CMakeLists.txt
new file mode 100644
index 0000000..73033bb
--- /dev/null
+++ b/SDK/c/CMakeLists.txt
@@ -0,0 +1,102 @@
+# CMake build for the csm-tcp-router-client C SDK.
+#
+# Usage:
+# mkdir build && cd build
+# cmake ..
+# cmake --build . --config Release
+# ctest --output-on-failure -C Release
+#
+# Options:
+# -DCSM_BUILD_TESTS=ON|OFF (default ON)
+# -DCSM_BUILD_EXAMPLES=ON|OFF (default ON)
+# -DCSM_BUILD_SHARED=ON|OFF (default OFF; ON builds a shared library)
+cmake_minimum_required(VERSION 3.15)
+project(csm_tcp_router_client
+ VERSION 0.1.0
+ DESCRIPTION "C client SDK for CSM-TCP-Router"
+ LANGUAGES C)
+
+option(CSM_BUILD_TESTS "Build the test suite" ON)
+option(CSM_BUILD_EXAMPLES "Build the example apps" ON)
+option(CSM_BUILD_SHARED "Build as a shared library" OFF)
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_C_EXTENSIONS OFF)
+
+if(MSVC)
+ add_compile_options(/W4 /D_CRT_SECURE_NO_WARNINGS)
+else()
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+# -------------------------------------------------------------------------
+# Library
+# -------------------------------------------------------------------------
+if(CSM_BUILD_SHARED)
+ add_library(csm_tcp_router_client SHARED src/csm_tcp_router_client.c)
+ target_compile_definitions(csm_tcp_router_client
+ PRIVATE CSM_BUILD_LIBRARY CSM_BUILD_SHARED
+ PUBLIC CSM_BUILD_SHARED)
+else()
+ add_library(csm_tcp_router_client STATIC src/csm_tcp_router_client.c)
+endif()
+
+target_include_directories(csm_tcp_router_client
+ PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
+ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
+
+set_target_properties(csm_tcp_router_client PROPERTIES
+ VERSION ${PROJECT_VERSION}
+ SOVERSION ${PROJECT_VERSION_MAJOR})
+
+# Platform-specific link libraries.
+if(WIN32)
+ target_link_libraries(csm_tcp_router_client PUBLIC ws2_32)
+else()
+ find_package(Threads REQUIRED)
+ target_link_libraries(csm_tcp_router_client PUBLIC Threads::Threads)
+endif()
+
+# -------------------------------------------------------------------------
+# Tests
+# -------------------------------------------------------------------------
+if(CSM_BUILD_TESTS)
+ enable_testing()
+ add_executable(csm_tcp_router_client_tests
+ tests/mock_server.c
+ tests/test_protocol.c
+ tests/test_client.c
+ tests/test_integration.c
+ tests/test_main.c)
+ target_include_directories(csm_tcp_router_client_tests
+ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
+ target_link_libraries(csm_tcp_router_client_tests
+ PRIVATE csm_tcp_router_client)
+ if(WIN32)
+ target_link_libraries(csm_tcp_router_client_tests PRIVATE ws2_32)
+ endif()
+ add_test(NAME csm_tcp_router_client_tests
+ COMMAND csm_tcp_router_client_tests)
+endif()
+
+# -------------------------------------------------------------------------
+# Examples
+# -------------------------------------------------------------------------
+if(CSM_BUILD_EXAMPLES)
+ add_executable(basic_usage examples/basic_usage.c)
+ add_executable(subscribe_status examples/subscribe_status.c)
+ add_executable(client_console examples/client_console.c)
+ target_link_libraries(basic_usage PRIVATE csm_tcp_router_client)
+ target_link_libraries(subscribe_status PRIVATE csm_tcp_router_client)
+ target_link_libraries(client_console PRIVATE csm_tcp_router_client)
+endif()
+
+# -------------------------------------------------------------------------
+# Install rules (header + library)
+# -------------------------------------------------------------------------
+install(TARGETS csm_tcp_router_client
+ RUNTIME DESTINATION bin
+ LIBRARY DESTINATION lib
+ ARCHIVE DESTINATION lib)
+install(FILES include/csm_tcp_router_client.h DESTINATION include)
diff --git a/SDK/c/LICENSE b/SDK/c/LICENSE
new file mode 100644
index 0000000..78e1cd2
--- /dev/null
+++ b/SDK/c/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 NEVSTOP-LAB
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/SDK/c/README.md b/SDK/c/README.md
new file mode 100644
index 0000000..5a93a24
--- /dev/null
+++ b/SDK/c/README.md
@@ -0,0 +1,258 @@
+# csm-tcp-router-client (C SDK)
+
+[](LICENSE)
+[](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App/actions/workflows/C_SDK.yml)
+
+C client SDK for the [CSM-TCP-Router](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App) LabVIEW server.
+
+CSM-TCP-Router exposes a LabVIEW [Communicable State Machine (CSM)](https://github.com/NEVSTOP-LAB/Communicable-State-Machine) application over TCP so that any TCP client — including native C/C++ programs, embedded devices, test harnesses, or CI pipelines — can send commands and receive responses without touching the LabVIEW code.
+
+> 📖 [中文文档 README.zh-cn.md](README.zh-cn.md)
+
+---
+
+## Features
+
+- **Synchronous commands** (`-@`) – `csm_client_send_and_wait()` blocks until the server returns the response.
+- **Asynchronous commands** (`->`) – `csm_client_post()` waits for the `cmd-resp` handshake; the eventual response is delivered via callback or polling queue.
+- **No-reply commands** (`->|`) – `csm_client_post_no_reply()` waits for the `cmd-resp` handshake; no further response expected.
+- **Status subscriptions** – `csm_client_subscribe_status()` / `csm_client_unsubscribe_status()` with optional callback or polling queue.
+- **Router management helpers** – `csm_client_list_modules()`, `csm_client_list_api()`, `csm_client_list_states()`, `csm_client_help()`.
+- **Connection utilities** – `csm_client_wait_for_server()` for polling during app startup.
+- **Thread-safe client** – every public function is safe to call from multiple threads concurrently.
+- **Multi-platform** – Windows (Winsock2 + Win32 threads) and POSIX (BSD sockets + pthreads); single source file.
+- **Zero runtime dependencies** – C99 standard library + the OS sockets/threading APIs only.
+
+---
+
+## Layout
+
+```
+SDK/c/
+├── include/
+│ └── csm_tcp_router_client.h # public API
+├── src/
+│ └── csm_tcp_router_client.c # cross-platform implementation
+├── examples/
+│ ├── basic_usage.c # mirrors examples/basic_usage.py
+│ └── subscribe_status.c # mirrors examples/subscribe_status.py
+├── tests/
+│ ├── test_harness.h # tiny in-process test harness
+│ ├── mock_server.[ch] # in-process MockServer fixture
+│ ├── test_protocol.c # codec unit tests
+│ ├── test_client.c # client-lifecycle unit tests
+│ ├── test_integration.c # end-to-end tests via MockServer
+│ └── test_main.c # runner / TESTS table
+├── vs2026/
+│ ├── csm_tcp_router_client.sln
+│ ├── csm_tcp_router_client/ # static-library project
+│ └── csm_tcp_router_client.tests/ # test-executable project
+├── CMakeLists.txt # cross-platform CMake build
+├── CHANGELOG.md
+├── LICENSE
+├── README.md
+└── README.zh-cn.md
+```
+
+This mirrors the layout of the Python SDK at `SDK/python/`.
+
+---
+
+## Building
+
+### CMake (Linux / macOS / Windows)
+
+```bash
+cd SDK/c
+cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
+cmake --build build -j
+ctest --test-dir build --output-on-failure -C Release
+```
+
+CMake options:
+
+| Option | Default | Description |
+|-------------------------|---------|--------------------------------------|
+| `CSM_BUILD_TESTS` | `ON` | Build the test executable. |
+| `CSM_BUILD_EXAMPLES` | `ON` | Build the example apps. |
+| `CSM_BUILD_SHARED` | `OFF` | Build a shared library (DLL/.so). |
+
+### Visual Studio 2026
+
+Open `SDK/c/vs2026/csm_tcp_router_client.sln` in Visual Studio 2026 and
+build (Ctrl+Shift+B). The solution provides Debug/Release × Win32/x64
+configurations using the `v144` platform toolset. Two projects are
+included:
+
+- `csm_tcp_router_client` – static library
+- `csm_tcp_router_client.tests` – console test executable (run it
+ directly to execute all unit + integration tests; exit code 0 on
+ success).
+
+See [`vs2026/README.md`](vs2026/README.md) for details.
+
+---
+
+## Quickstart
+
+```c
+#include "csm_tcp_router_client.h"
+#include
+
+int main(void) {
+ csm_client_t *c = csm_client_create();
+ if (csm_client_connect(c, "localhost", 30007, 5000) != CSM_OK) {
+ fprintf(stderr, "Connect failed\n");
+ csm_client_destroy(c);
+ return 1;
+ }
+
+ char *modules = NULL;
+ if (csm_client_list_modules(c, &modules, 5000) == CSM_OK) {
+ printf("Modules:\n%s\n", modules);
+ csm_string_free(modules);
+ }
+
+ csm_command_response_t resp = {0};
+ if (csm_client_send_and_wait(c, "API: Read -@ DAQmx", 5000, &resp) == CSM_OK) {
+ printf("Response: %s\n", (char *)resp.raw);
+ }
+ csm_command_response_dispose(&resp);
+
+ double ms = 0;
+ if (csm_client_ping(c, 2000, &ms) == CSM_OK) {
+ printf("Ping latency: %.1f ms\n", ms);
+ }
+
+ csm_client_disconnect(c);
+ csm_client_destroy(c);
+ return 0;
+}
+```
+
+---
+
+## Protocol
+
+The SDK implements the CSM-TCP-Router **protocol v0**.
+
+```
+| Data Length (4B) | Version (1B) | TYPE (1B) | FLAG1 (1B) | FLAG2 (1B) | Text Data |
+╰────────────────────────── Header (8B) ─────────────────────────────╯
+```
+
+| TYPE byte | Constant | Direction | Description |
+|-----------|---------------------|----------------|------------------------------------------------|
+| `0x00` | `CSM_PT_INFO` | Server → Client| Welcome / goodbye informational message |
+| `0x01` | `CSM_PT_ERROR` | Server → Client| CSM error: `[Error: ] ` |
+| `0x02` | `CSM_PT_CMD` | Client → Server| Command string |
+| `0x03` | `CSM_PT_CMD_RESP` | Server → Client| Handshake ACK for async / subscribe commands |
+| `0x04` | `CSM_PT_RESP` | Server → Client| Synchronous response payload |
+| `0x05` | `CSM_PT_ASYNC_RESP` | Server → Client| Async response: ` <- ` |
+| `0x06` | `CSM_PT_STATUS` | Server → Client| Status broadcast: ` >> <- ` |
+| `0x07` | `CSM_PT_INTERRUPT` | Server → Client| Interrupt broadcast (same format as STATUS) |
+
+---
+
+## API at a glance
+
+### Lifecycle
+
+| Function | Description |
+|---|---|
+| `csm_client_create()` | Allocate a new client. |
+| `csm_client_destroy(c)` | Disconnect (if connected) and free resources. |
+| `csm_client_connect(c, host, port, timeout_ms)` | Open a TCP connection and start the receive thread. |
+| `csm_client_disconnect(c)` | Close the connection; safe even when not connected. |
+| `csm_client_is_connected(c)` | Non-zero while connected. |
+| `csm_client_wait_for_server(host, port, timeout_ms, retry_ms)` | Poll until the server is reachable. |
+
+### Commands
+
+| Function | Description |
+|---|---|
+| `csm_client_send_and_wait(c, cmd, timeout, &resp)` | Synchronous command (`-@`). |
+| `csm_client_post(c, cmd, timeout)` | Async command (`->`). |
+| `csm_client_post_no_reply(c, cmd, timeout)` | No-reply async command (`->|`). |
+| `csm_client_ping(c, timeout, &elapsed_ms)` | Round-trip latency check. |
+
+### Router management helpers
+
+| Function | Description |
+|---|---|
+| `csm_client_list_modules(c, &out_text, timeout)` | `List` command. |
+| `csm_client_list_api(c, module, &out_text, timeout)` | `List API `. |
+| `csm_client_list_states(c, module, &out_text, timeout)` | `List State `. |
+| `csm_client_help(c, module, &out_text, timeout)` | `Help `. |
+
+Free `*out_text` with `csm_string_free()`.
+
+### Subscriptions
+
+| Function | Description |
+|---|---|
+| `csm_client_subscribe_status(c, status, module, cb, ud, timeout)` | Subscribe; callback invoked from receive thread. |
+| `csm_client_unsubscribe_status(c, status, module, timeout)` | Cancel a subscription. |
+| `csm_client_register_async_callback(c, cmd, cb, ud)` | Register callback for `ASYNC_RESP` packets. |
+| `csm_client_unregister_async_callback(c, cmd)` | Remove a callback. |
+
+### Polling queues (alternative to callbacks)
+
+| Function | Description |
+|---|---|
+| `csm_client_poll_status(c, &out_notif, timeout)` | Block until next STATUS / INTERRUPT. |
+| `csm_client_poll_async_response(c, &out_resp, timeout)` | Block until next `ASYNC_RESP`. |
+
+---
+
+## Result codes
+
+All public functions return a `csm_result_t`:
+
+| Code | Meaning |
+|-----------------------|-------------------------------------------------|
+| `CSM_OK` | Operation succeeded. |
+| `CSM_ERR_INVALID` | Invalid argument or NULL pointer. |
+| `CSM_ERR_CONNECTION` | Connection failed or was lost. |
+| `CSM_ERR_TIMEOUT` | Operation exceeded its timeout. |
+| `CSM_ERR_PROTOCOL` | Invalid / malformed protocol frame. |
+| `CSM_ERR_SERVER` | Server returned an `ERROR` packet (see below). |
+| `CSM_ERR_NOMEM` | Memory allocation failure. |
+| `CSM_ERR_STATE` | Operation invalid in the current state. |
+| `CSM_ERR_IO` | Underlying socket / OS I/O error. |
+
+After a `CSM_ERR_SERVER`, retrieve the error code/message via:
+
+```c
+csm_server_error_t err;
+csm_client_last_server_error(c, &err);
+fprintf(stderr, "[%s] %s\n", err.code, err.message);
+```
+
+`csm_result_str(code)` returns a static, human-readable string.
+
+---
+
+## Tests
+
+The test suite (`SDK/c/tests/`) uses a tiny in-process harness and an
+embedded `MockServer` (see `tests/mock_server.h`) that emulates the
+LabVIEW CSM-TCP-Router on `127.0.0.1`. The same tests run on Linux,
+macOS, and Windows.
+
+```bash
+cmake --build build -j
+ctest --test-dir build --output-on-failure
+```
+
+Or run the executable directly to see per-test progress:
+
+```bash
+./build/csm_tcp_router_client_tests
+```
+
+---
+
+## License
+
+[MIT](LICENSE) — © NEVSTOP-LAB
diff --git a/SDK/c/README.zh-cn.md b/SDK/c/README.zh-cn.md
new file mode 100644
index 0000000..2d2e02b
--- /dev/null
+++ b/SDK/c/README.zh-cn.md
@@ -0,0 +1,250 @@
+# csm-tcp-router-client (C SDK)
+
+[](LICENSE)
+[](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App/actions/workflows/C_SDK.yml)
+
+[CSM-TCP-Router](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App) LabVIEW 服务端的 C 语言客户端 SDK。
+
+CSM-TCP-Router 通过 TCP 暴露一个基于 LabVIEW [Communicable State Machine (CSM)](https://github.com/NEVSTOP-LAB/Communicable-State-Machine) 框架的应用程序,使任何 TCP 客户端 —— 包括原生 C/C++ 程序、嵌入式设备、测试夹具或 CI 流水线 —— 都可以发送命令并接收响应,无需修改任何 LabVIEW 代码。
+
+> 📖 [English README.md](README.md)
+
+---
+
+## 特性
+
+- **同步命令** (`-@`) – `csm_client_send_and_wait()` 阻塞直到服务器返回响应。
+- **异步命令** (`->`) – `csm_client_post()` 等待 `cmd-resp` 握手;最终响应通过回调或轮询队列送达。
+- **无回复命令** (`->|`) – `csm_client_post_no_reply()` 等待 `cmd-resp` 握手;不再有后续响应。
+- **状态订阅** – `csm_client_subscribe_status()` / `csm_client_unsubscribe_status()`,可选回调或轮询队列。
+- **路由器管理辅助函数** – `csm_client_list_modules()`、`csm_client_list_api()`、`csm_client_list_states()`、`csm_client_help()`。
+- **连接工具** – 应用启动期间使用 `csm_client_wait_for_server()` 进行轮询。
+- **线程安全的客户端** – 所有公开函数均可由多线程并发调用。
+- **多平台移植** – Windows(Winsock2 + Win32 线程)和 POSIX(BSD sockets + pthreads);单源文件实现。
+- **无运行时依赖** – 仅依赖 C99 标准库与操作系统的 sockets/线程 API。
+
+---
+
+## 目录结构
+
+```
+SDK/c/
+├── include/
+│ └── csm_tcp_router_client.h # 公开 API 头文件
+├── src/
+│ └── csm_tcp_router_client.c # 跨平台实现
+├── examples/
+│ ├── basic_usage.c # 对应 examples/basic_usage.py
+│ └── subscribe_status.c # 对应 examples/subscribe_status.py
+├── tests/
+│ ├── test_harness.h # 进程内极简测试框架
+│ ├── mock_server.[ch] # 进程内 MockServer 测试夹具
+│ ├── test_protocol.c # 协议编解码单元测试
+│ ├── test_client.c # 客户端生命周期单元测试
+│ ├── test_integration.c # 通过 MockServer 的端到端测试
+│ └── test_main.c # 测试运行器/TESTS 表
+├── vs2026/
+│ ├── csm_tcp_router_client.sln
+│ ├── csm_tcp_router_client/ # 静态库工程
+│ └── csm_tcp_router_client.tests/ # 测试可执行工程
+├── CMakeLists.txt # 跨平台 CMake 构建
+├── CHANGELOG.md
+├── LICENSE
+├── README.md
+└── README.zh-cn.md
+```
+
+整体结构与 `SDK/python/` 下的 Python SDK 保持一致。
+
+---
+
+## 编译
+
+### CMake(Linux / macOS / Windows)
+
+```bash
+cd SDK/c
+cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
+cmake --build build -j
+ctest --test-dir build --output-on-failure -C Release
+```
+
+CMake 选项:
+
+| 选项 | 默认值 | 说明 |
+|-------------------------|---------|-----------------------------------|
+| `CSM_BUILD_TESTS` | `ON` | 是否构建测试可执行文件。 |
+| `CSM_BUILD_EXAMPLES` | `ON` | 是否构建示例程序。 |
+| `CSM_BUILD_SHARED` | `OFF` | 是否编译为动态库(DLL/.so)。 |
+
+### Visual Studio 2026
+
+打开 `SDK/c/vs2026/csm_tcp_router_client.sln`,使用 Visual Studio 2026 直接构建(Ctrl+Shift+B)。该解决方案提供 Debug/Release × Win32/x64 四种配置,平台工具集为 `v144`,包含两个工程:
+
+- `csm_tcp_router_client` – 静态库
+- `csm_tcp_router_client.tests` – 控制台测试可执行文件(直接运行即可执行所有单元 + 集成测试,退出码为 0 即所有测试通过)。
+
+详见 [`vs2026/README.md`](vs2026/README.md)。
+
+---
+
+## 快速开始
+
+```c
+#include "csm_tcp_router_client.h"
+#include
+
+int main(void) {
+ csm_client_t *c = csm_client_create();
+ if (csm_client_connect(c, "localhost", 30007, 5000) != CSM_OK) {
+ fprintf(stderr, "连接失败\n");
+ csm_client_destroy(c);
+ return 1;
+ }
+
+ char *modules = NULL;
+ if (csm_client_list_modules(c, &modules, 5000) == CSM_OK) {
+ printf("已加载模块:\n%s\n", modules);
+ csm_string_free(modules);
+ }
+
+ csm_command_response_t resp = {0};
+ if (csm_client_send_and_wait(c, "API: Read -@ DAQmx", 5000, &resp) == CSM_OK) {
+ printf("响应: %s\n", (char *)resp.raw);
+ }
+ csm_command_response_dispose(&resp);
+
+ double ms = 0;
+ if (csm_client_ping(c, 2000, &ms) == CSM_OK) {
+ printf("Ping 延迟: %.1f ms\n", ms);
+ }
+
+ csm_client_disconnect(c);
+ csm_client_destroy(c);
+ return 0;
+}
+```
+
+---
+
+## 协议
+
+SDK 实现 CSM-TCP-Router **协议 v0**:
+
+```
+| 数据长度 (4B) | 版本 (1B) | TYPE (1B) | FLAG1 (1B) | FLAG2 (1B) | 文本数据 |
+╰────────────────────────── 包头 (8B) ─────────────────────────────╯
+```
+
+| TYPE 字节 | 常量 | 方向 | 说明 |
+|-----------|---------------------|----------------|----------------------------------------------------|
+| `0x00` | `CSM_PT_INFO` | 服务器 → 客户端 | 欢迎/告别等信息消息 |
+| `0x01` | `CSM_PT_ERROR` | 服务器 → 客户端 | CSM 错误:`[Error: ] ` |
+| `0x02` | `CSM_PT_CMD` | 客户端 → 服务器 | 命令字符串 |
+| `0x03` | `CSM_PT_CMD_RESP` | 服务器 → 客户端 | 异步/订阅命令的握手 ACK |
+| `0x04` | `CSM_PT_RESP` | 服务器 → 客户端 | 同步响应负载 |
+| `0x05` | `CSM_PT_ASYNC_RESP` | 服务器 → 客户端 | 异步响应:` <- ` |
+| `0x06` | `CSM_PT_STATUS` | 服务器 → 客户端 | 状态广播:` >> <- ` |
+| `0x07` | `CSM_PT_INTERRUPT` | 服务器 → 客户端 | 中断广播(与 STATUS 格式相同) |
+
+---
+
+## API 速览
+
+### 生命周期
+
+| 函数 | 说明 |
+|---|---|
+| `csm_client_create()` | 分配新的客户端。 |
+| `csm_client_destroy(c)` | 如已连接则断开并释放资源。 |
+| `csm_client_connect(c, host, port, timeout_ms)` | 建立 TCP 连接并启动接收线程。 |
+| `csm_client_disconnect(c)` | 关闭连接;未连接时调用也安全。 |
+| `csm_client_is_connected(c)` | 已连接时返回非零值。 |
+| `csm_client_wait_for_server(host, port, timeout_ms, retry_ms)` | 轮询直到服务器可达。 |
+
+### 命令
+
+| 函数 | 说明 |
+|---|---|
+| `csm_client_send_and_wait(c, cmd, timeout, &resp)` | 同步命令 (`-@`)。 |
+| `csm_client_post(c, cmd, timeout)` | 异步命令 (`->`)。 |
+| `csm_client_post_no_reply(c, cmd, timeout)` | 无回复异步命令 (`->|`)。 |
+| `csm_client_ping(c, timeout, &elapsed_ms)` | 往返延迟检测。 |
+
+### 路由器管理辅助函数
+
+| 函数 | 说明 |
+|---|---|
+| `csm_client_list_modules(c, &out_text, timeout)` | `List` 命令。 |
+| `csm_client_list_api(c, module, &out_text, timeout)` | `List API `。 |
+| `csm_client_list_states(c, module, &out_text, timeout)` | `List State `。 |
+| `csm_client_help(c, module, &out_text, timeout)` | `Help `。 |
+
+请使用 `csm_string_free()` 释放 `*out_text`。
+
+### 订阅
+
+| 函数 | 说明 |
+|---|---|
+| `csm_client_subscribe_status(c, status, module, cb, ud, timeout)` | 订阅;回调由接收线程调用。 |
+| `csm_client_unsubscribe_status(c, status, module, timeout)` | 取消订阅。 |
+| `csm_client_register_async_callback(c, cmd, cb, ud)` | 为 `ASYNC_RESP` 包注册回调。 |
+| `csm_client_unregister_async_callback(c, cmd)` | 移除回调。 |
+
+### 轮询队列(回调的替代方式)
+
+| 函数 | 说明 |
+|---|---|
+| `csm_client_poll_status(c, &out_notif, timeout)` | 阻塞直到下一条 STATUS / INTERRUPT。 |
+| `csm_client_poll_async_response(c, &out_resp, timeout)` | 阻塞直到下一条 `ASYNC_RESP`。 |
+
+---
+
+## 返回值
+
+所有公开函数均返回 `csm_result_t`:
+
+| 代码 | 含义 |
+|-----------------------|-------------------------------------------------|
+| `CSM_OK` | 操作成功。 |
+| `CSM_ERR_INVALID` | 参数无效或为 NULL。 |
+| `CSM_ERR_CONNECTION` | 连接失败或连接丢失。 |
+| `CSM_ERR_TIMEOUT` | 操作超时。 |
+| `CSM_ERR_PROTOCOL` | 协议帧无效或损坏。 |
+| `CSM_ERR_SERVER` | 服务器返回了 `ERROR` 数据包(见下文)。 |
+| `CSM_ERR_NOMEM` | 内存分配失败。 |
+| `CSM_ERR_STATE` | 当前状态下操作无效。 |
+| `CSM_ERR_IO` | 底层 socket / 操作系统 I/O 错误。 |
+
+收到 `CSM_ERR_SERVER` 后,可通过以下方式获取错误码与消息:
+
+```c
+csm_server_error_t err;
+csm_client_last_server_error(c, &err);
+fprintf(stderr, "[%s] %s\n", err.code, err.message);
+```
+
+`csm_result_str(code)` 返回静态可读字符串。
+
+---
+
+## 测试
+
+测试套件(`SDK/c/tests/`)使用进程内极简测试框架与内嵌的 `MockServer`(详见 `tests/mock_server.h`),后者在 `127.0.0.1` 上模拟 LabVIEW CSM-TCP-Router。同一套测试在 Linux、macOS 与 Windows 上均可运行。
+
+```bash
+cmake --build build -j
+ctest --test-dir build --output-on-failure
+```
+
+也可直接运行可执行文件以查看每条测试的进度:
+
+```bash
+./build/csm_tcp_router_client_tests
+```
+
+---
+
+## 许可证
+
+[MIT](LICENSE) — © NEVSTOP-LAB
diff --git a/SDK/c/examples/basic_usage.c b/SDK/c/examples/basic_usage.c
new file mode 100644
index 0000000..bb9331f
--- /dev/null
+++ b/SDK/c/examples/basic_usage.c
@@ -0,0 +1,61 @@
+/* basic_usage.c - 演示连接、Ping、列出模块以及
+ * 发送同步命令。对应 examples/basic_usage.py。 */
+#include "csm_tcp_router_client.h"
+
+#include
+#include
+
+#define HOST "localhost"
+#define PORT 30007
+
+int main(void) {
+ /* 1. 等待服务器就绪(可选)。 */
+ printf("Waiting for server ... ");
+ fflush(stdout);
+ csm_result_t r = csm_client_wait_for_server(HOST, PORT, 30000, 500);
+ if (r != CSM_OK) {
+ printf("TIMEOUT - server did not start within 30s.\n");
+ return 1;
+ }
+ printf("ready.\n");
+
+ /* 2. 创建 + 连接。 */
+ csm_client_t *c = csm_client_create();
+ if (!c) { fprintf(stderr, "Out of memory\n"); return 1; }
+
+ r = csm_client_connect(c, HOST, PORT, 5000);
+ if (r != CSM_OK) {
+ fprintf(stderr, "Connection failed: %s\n", csm_result_str(r));
+ csm_client_destroy(c);
+ return 1;
+ }
+ printf("Connected to %s:%d\n", HOST, PORT);
+
+ /* 3. Ping。 */
+ double ms = 0;
+ r = csm_client_ping(c, 2000, &ms);
+ if (r == CSM_OK) printf("Ping OK latency=%.1f ms\n", ms);
+ else printf("Ping failed: %s\n", csm_result_str(r));
+
+ /* 4. 列出 CSM 模块。 */
+ char *modules = NULL;
+ if (csm_client_list_modules(c, &modules, 5000) == CSM_OK) {
+ printf("\nLoaded modules:\n%s\n", modules);
+ csm_string_free(modules);
+ }
+
+ /* 5. 发送同步命令(连接到真实模块后取消注释)。
+ *
+ * csm_command_response_t resp = {0};
+ * if (csm_client_send_and_wait(c, "API: Read -@ DAQmx", 5000, &resp) == CSM_OK) {
+ * printf("Sync response: %s\n", (char *)resp.raw);
+ * }
+ * csm_command_response_dispose(&resp);
+ */
+
+ /* 6. 断开连接并清理资源。 */
+ csm_client_disconnect(c);
+ csm_client_destroy(c);
+ printf("Disconnected.\n");
+ return 0;
+}
diff --git a/SDK/c/examples/client_console.c b/SDK/c/examples/client_console.c
new file mode 100644
index 0000000..d40ef4f
--- /dev/null
+++ b/SDK/c/examples/client_console.c
@@ -0,0 +1,265 @@
+/* client_console.c - 交互式客户端控制台示例。
+ *
+ * 连接到正在运行的 CSM-TCP-Router 服务器,从 stdin 读取用户输入的
+ * 命令,并通过 SDK 转发。同样的命令集、提示符和输出格式也在
+ * Python(examples/client_console.py)和 C#(examples/ClientConsole)
+ * SDK 示例中实现,因此三种语言的行为一致。
+ *
+ * 用法:
+ * client_console [host] [port]
+ */
+#include "csm_tcp_router_client.h"
+
+#include
+#include
+#include
+
+#define DEFAULT_HOST "localhost"
+#define DEFAULT_PORT 30007
+#define LINE_BUFFER_SIZE 4096
+
+static const char *HELP_TEXT =
+ "Available commands:\n"
+ " help Show this help text\n"
+ " quit / exit Disconnect and exit\n"
+ " ping Measure round-trip latency\n"
+ " list List CSM modules loaded on the server\n"
+ " api List the API of a module\n"
+ " state List the states of a module\n"
+ " mhelp Server-side Help for a module\n"
+ " send Send a synchronous command and print the response\n"
+ " post Send an asynchronous command (-> suffix)\n"
+ " nopost Send a no-reply asynchronous command (->|)\n"
+ " sub @ Subscribe to a status broadcast\n"
+ " unsub @ Unsubscribe from a status broadcast";
+
+static void on_status(const csm_status_notification_t *n, void *ud) {
+ (void)ud;
+ printf("\n[STATUS] %s@%s: %s\n",
+ n->status_name ? n->status_name : "",
+ n->module_name ? n->module_name : "",
+ n->data ? n->data : "");
+}
+
+static void on_async(const csm_async_response_t *r, void *ud) {
+ (void)ud;
+ printf("\n[ASYNC] %s (cmd=%s)\n",
+ r->raw ? r->raw : "",
+ r->original_command ? r->original_command : "");
+}
+
+/* 修剪 *s* 的前后空白字符(包括换行)。原地修改。返回 *s*。 */
+static char *trim(char *s) {
+ if (!s) return s;
+ char *end;
+ while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++;
+ end = s + strlen(s);
+ while (end > s && (end[-1] == ' ' || end[-1] == '\t' ||
+ end[-1] == '\r' || end[-1] == '\n')) {
+ end--;
+ }
+ *end = '\0';
+ return s;
+}
+
+/* 不区分大小写地比较两个 NUL 终止字符串。 */
+static int ieq(const char *a, const char *b) {
+ while (*a && *b) {
+ char ca = *a, cb = *b;
+ if (ca >= 'A' && ca <= 'Z') ca = (char)(ca - 'A' + 'a');
+ if (cb >= 'A' && cb <= 'Z') cb = (char)(cb - 'A' + 'a');
+ if (ca != cb) return 0;
+ a++; b++;
+ }
+ return *a == '\0' && *b == '\0';
+}
+
+/* 在 *line* 上拆分 "@",将指针存入 out_status/out_module
+ * (指向 *line* 内的位置,*line* 会被原地修改)。失败时返回 0。 */
+static int split_status_module(char *line, char **out_status, char **out_module) {
+ char *at = strchr(line, '@');
+ if (!at) return 0;
+ *at = '\0';
+ char *status = trim(line);
+ char *module = trim(at + 1);
+ if (*status == '\0' || *module == '\0') return 0;
+ *out_status = status;
+ *out_module = module;
+ return 1;
+}
+
+/* 打印来自服务器的最近一次错误(如果有)。 */
+static void print_error(csm_client_t *c, csm_result_t r) {
+ csm_server_error_t err;
+ if (r == CSM_ERR_SERVER && csm_client_last_server_error(c, &err) == CSM_OK) {
+ if (err.code[0]) {
+ printf("Error: [%s] %s\n", err.code, err.message);
+ } else {
+ printf("Error: %s\n", err.message);
+ }
+ } else {
+ printf("Error: %s\n", csm_result_str(r));
+ }
+}
+
+/* 调用一个返回字符串的 SDK 辅助函数并打印结果。 */
+static void print_string_command(csm_client_t *c, csm_result_t r, char *text) {
+ if (r == CSM_OK) {
+ printf("%s\n", text ? text : "");
+ csm_string_free(text);
+ } else {
+ print_error(c, r);
+ }
+}
+
+/* 处理一行输入。返回 0 表示退出,否则返回 1。 */
+static int dispatch(csm_client_t *c, char *line) {
+ line = trim(line);
+ if (*line == '\0') return 1;
+
+ /* 将命令字与参数拆分(参数保留空格)。 */
+ char *cmd = line;
+ char *arg = strpbrk(line, " \t");
+ if (arg) {
+ *arg++ = '\0';
+ arg = trim(arg);
+ } else {
+ arg = (char *)"";
+ }
+
+ if (ieq(cmd, "quit") || ieq(cmd, "exit")) {
+ return 0;
+ }
+ if (ieq(cmd, "help")) {
+ printf("%s\n", HELP_TEXT);
+ return 1;
+ }
+ if (ieq(cmd, "ping")) {
+ double ms = 0;
+ csm_result_t r = csm_client_ping(c, 2000, &ms);
+ if (r == CSM_OK) printf("Ping OK latency=%.1f ms\n", ms);
+ else printf("Ping failed.\n");
+ return 1;
+ }
+ if (ieq(cmd, "list")) {
+ char *text = NULL;
+ print_string_command(c, csm_client_list_modules(c, &text, 5000), text);
+ return 1;
+ }
+ if (ieq(cmd, "api")) {
+ if (*arg == '\0') { printf("Error: usage: api \n"); return 1; }
+ char *text = NULL;
+ print_string_command(c, csm_client_list_api(c, arg, &text, 5000), text);
+ return 1;
+ }
+ if (ieq(cmd, "state")) {
+ if (*arg == '\0') { printf("Error: usage: state \n"); return 1; }
+ char *text = NULL;
+ print_string_command(c, csm_client_list_states(c, arg, &text, 5000), text);
+ return 1;
+ }
+ if (ieq(cmd, "mhelp")) {
+ if (*arg == '\0') { printf("Error: usage: mhelp \n"); return 1; }
+ char *text = NULL;
+ print_string_command(c, csm_client_help(c, arg, &text, 5000), text);
+ return 1;
+ }
+ if (ieq(cmd, "send")) {
+ if (*arg == '\0') { printf("Error: usage: send \n"); return 1; }
+ csm_command_response_t resp = {0};
+ csm_result_t r = csm_client_send_and_wait(c, arg, 5000, &resp);
+ if (r == CSM_OK) {
+ printf("Response: %s\n", resp.raw ? (const char *)resp.raw : "");
+ csm_command_response_dispose(&resp);
+ } else {
+ print_error(c, r);
+ }
+ return 1;
+ }
+ if (ieq(cmd, "post")) {
+ if (*arg == '\0') { printf("Error: usage: post \n"); return 1; }
+ csm_client_register_async_callback(c, arg, on_async, NULL);
+ csm_result_t r = csm_client_post(c, arg, 5000);
+ if (r == CSM_OK) printf("Async command sent.\n");
+ else print_error(c, r);
+ return 1;
+ }
+ if (ieq(cmd, "nopost")) {
+ if (*arg == '\0') { printf("Error: usage: nopost \n"); return 1; }
+ csm_result_t r = csm_client_post_no_reply(c, arg, 5000);
+ if (r == CSM_OK) printf("No-reply command sent.\n");
+ else print_error(c, r);
+ return 1;
+ }
+ if (ieq(cmd, "sub")) {
+ char *status = NULL, *module = NULL;
+ if (!split_status_module(arg, &status, &module)) {
+ printf("Error: expected '@'\n");
+ return 1;
+ }
+ csm_result_t r = csm_client_subscribe_status(c, status, module,
+ on_status, NULL, 5000);
+ if (r == CSM_OK) printf("Subscribed to %s@%s\n", status, module);
+ else print_error(c, r);
+ return 1;
+ }
+ if (ieq(cmd, "unsub")) {
+ char *status = NULL, *module = NULL;
+ if (!split_status_module(arg, &status, &module)) {
+ printf("Error: expected '@'\n");
+ return 1;
+ }
+ csm_result_t r = csm_client_unsubscribe_status(c, status, module, 5000);
+ if (r == CSM_OK) printf("Unsubscribed from %s@%s\n", status, module);
+ else print_error(c, r);
+ return 1;
+ }
+
+ printf("Error: unknown command '%s'. Type 'help' for the command list.\n",
+ cmd);
+ return 1;
+}
+
+int main(int argc, char **argv) {
+ const char *host = (argc > 1) ? argv[1] : DEFAULT_HOST;
+ int port = (argc > 2) ? atoi(argv[2]) : DEFAULT_PORT;
+ if (port <= 0 || port > 65535) {
+ fprintf(stderr, "Error: invalid port '%s'\n", argv[2]);
+ return 1;
+ }
+
+ printf("CSM-TCP-Router Client Console\n");
+ printf("Connecting to %s:%d ...\n", host, port);
+
+ csm_client_t *c = csm_client_create();
+ if (!c) {
+ fprintf(stderr, "Out of memory\n");
+ return 1;
+ }
+
+ csm_result_t r = csm_client_connect(c, host, (uint16_t)port, 5000);
+ if (r != CSM_OK) {
+ printf("Error: %s\n", csm_result_str(r));
+ csm_client_destroy(c);
+ return 1;
+ }
+
+ printf("Connected to %s:%d. Type 'help' for commands, 'quit' to exit.\n",
+ host, port);
+
+ char line[LINE_BUFFER_SIZE];
+ for (;;) {
+ printf("csm> ");
+ fflush(stdout);
+ if (!fgets(line, (int)sizeof(line), stdin)) {
+ printf("\n");
+ break;
+ }
+ if (!dispatch(c, line)) break;
+ }
+
+ csm_client_disconnect(c);
+ csm_client_destroy(c);
+ printf("Disconnected.\n");
+ return 0;
+}
diff --git a/SDK/c/examples/subscribe_status.c b/SDK/c/examples/subscribe_status.c
new file mode 100644
index 0000000..f4396bb
--- /dev/null
+++ b/SDK/c/examples/subscribe_status.c
@@ -0,0 +1,65 @@
+/* subscribe_status.c - 演示使用回调进行实时状态订阅,
+ * 对应 examples/subscribe_status.py。 */
+
+#if !defined(_WIN32)
+# ifndef _POSIX_C_SOURCE
+# define _POSIX_C_SOURCE 200809L
+# endif
+#endif
+
+#include "csm_tcp_router_client.h"
+
+#include
+#include
+
+#if defined(_WIN32)
+# include
+static void sleep_ms(unsigned int ms){ Sleep(ms); }
+#else
+# include
+static void sleep_ms(unsigned int ms){
+ struct timespec ts; ts.tv_sec=ms/1000; ts.tv_nsec=(long)(ms%1000)*1000000L;
+ nanosleep(&ts, NULL);
+}
+#endif
+
+#define HOST "localhost"
+#define PORT 30007
+
+static void on_status(const csm_status_notification_t *n, void *ud) {
+ (void)ud;
+ printf("[%s @ %s] %s\n", n->status_name, n->module_name, n->data);
+}
+
+int main(int argc, char **argv) {
+ const char *status_name = (argc > 1) ? argv[1] : "Status";
+ const char *module_name = (argc > 2) ? argv[2] : "DAQmx";
+
+ csm_client_t *c = csm_client_create();
+ if (!c) return 1;
+
+ csm_result_t r = csm_client_connect(c, HOST, PORT, 5000);
+ if (r != CSM_OK) {
+ fprintf(stderr, "Connection failed: %s\n", csm_result_str(r));
+ csm_client_destroy(c);
+ return 1;
+ }
+
+ r = csm_client_subscribe_status(c, status_name, module_name,
+ on_status, NULL, 5000);
+ if (r != CSM_OK) {
+ fprintf(stderr, "Subscribe failed: %s\n", csm_result_str(r));
+ csm_client_disconnect(c);
+ csm_client_destroy(c);
+ return 1;
+ }
+
+ printf("Subscribed to %s@%s. Listening for 30s ...\n",
+ status_name, module_name);
+ sleep_ms(30000);
+
+ csm_client_unsubscribe_status(c, status_name, module_name, 5000);
+ csm_client_disconnect(c);
+ csm_client_destroy(c);
+ return 0;
+}
diff --git a/SDK/c/include/csm_tcp_router_client.h b/SDK/c/include/csm_tcp_router_client.h
new file mode 100644
index 0000000..378ad80
--- /dev/null
+++ b/SDK/c/include/csm_tcp_router_client.h
@@ -0,0 +1,399 @@
+/* csm_tcp_router_client.h - 适用于 CSM-TCP-Router 客户端 SDK 的单头文件公共 C API。
+ *
+ * 本 SDK 是 Python `csm_tcp_router_client` 模块的 C 对应版本。
+ * 它通过 TCP 实现 CSM-TCP-Router 协议 v0,并提供一个
+ * 线程安全的同步客户端(`csm_client_t`),支持阻塞调用
+ * 以及异步回调/轮询队列两种投递方式。
+ *
+ * 线帧格式(8 字节头部,大端序):
+ *
+ * | Data Length (4B) | Version (1B=0x01) | Type (1B) | FLAG1 (1B) | FLAG2 (1B) | Payload |
+ * +---------------------------- Header (8B) ----------------------------+
+ *
+ * 快速入门:
+ *
+ * csm_client_t *c = csm_client_create();
+ * if (csm_client_connect(c, "localhost", 30007, 5000) == CSM_OK) {
+ * char *modules = NULL;
+ * if (csm_client_list_modules(c, &modules, 5000) == CSM_OK) {
+ * printf("%s\n", modules);
+ * csm_string_free(modules);
+ * }
+ * csm_client_disconnect(c);
+ * }
+ * csm_client_destroy(c);
+ *
+ * 本库可跨平台运行于 Windows(Winsock2 + Win32 线程)和
+ * POSIX 系统(BSD 套接字 + pthreads),通过附带的 CMake / Visual Studio 项目
+ * 构建为静态库或共享库。
+ */
+#ifndef CSM_TCP_ROUTER_CLIENT_H
+#define CSM_TCP_ROUTER_CLIENT_H
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* ------------------------------------------------------------------------- */
+/* 版本和 DLL 导出 */
+/* ------------------------------------------------------------------------- */
+
+#define CSM_VERSION_MAJOR 0
+#define CSM_VERSION_MINOR 1
+#define CSM_VERSION_PATCH 0
+#define CSM_VERSION_STRING "0.1.0"
+
+#if defined(_WIN32) && defined(CSM_BUILD_SHARED)
+# ifdef CSM_BUILD_LIBRARY
+# define CSM_API __declspec(dllexport)
+# else
+# define CSM_API __declspec(dllimport)
+# endif
+#else
+# define CSM_API
+#endif
+
+/* ------------------------------------------------------------------------- */
+/* 返回码 */
+/* ------------------------------------------------------------------------- */
+
+/** 所有公共 SDK 函数返回的结果码。 */
+typedef enum csm_result {
+ CSM_OK = 0, /**< 操作成功。 */
+ CSM_ERR_INVALID = -1, /**< 无效参数或 NULL 指针。 */
+ CSM_ERR_CONNECTION = -2, /**< 连接失败或已断开。 */
+ CSM_ERR_TIMEOUT = -3, /**< 操作超时。 */
+ CSM_ERR_PROTOCOL = -4, /**< 无效/格式错误的协议帧。 */
+ CSM_ERR_SERVER = -5, /**< 服务器返回了 ERROR 数据包。 */
+ CSM_ERR_NOMEM = -6, /**< 内存分配失败。 */
+ CSM_ERR_STATE = -7, /**< 当前状态下操作无效。 */
+ CSM_ERR_IO = -8 /**< 底层套接字/操作系统 I/O 错误。 */
+} csm_result_t;
+
+/** 返回 *code* 对应的静态可读字符串。 */
+CSM_API const char *csm_result_str(csm_result_t code);
+
+/* ------------------------------------------------------------------------- */
+/* 协议常量 */
+/* ------------------------------------------------------------------------- */
+
+/** 数据包类型字节值(CSM-TCP-Router 协议 v0)。 */
+typedef enum csm_packet_type {
+ CSM_PT_INFO = 0x00, /**< 欢迎/再见信息文本。 */
+ CSM_PT_ERROR = 0x01, /**< 服务器错误:"[Error: ] " */
+ CSM_PT_CMD = 0x02, /**< 命令数据包(客户端 -> 服务器)。 */
+ CSM_PT_CMD_RESP = 0x03, /**< 异步/订阅握手 ACK。 */
+ CSM_PT_RESP = 0x04, /**< 同步响应载荷。 */
+ CSM_PT_ASYNC_RESP = 0x05, /**< 异步响应:" <- "。 */
+ CSM_PT_STATUS = 0x06, /**< 状态广播。 */
+ CSM_PT_INTERRUPT = 0x07 /**< 中断广播。 */
+} csm_packet_type_t;
+
+/** 固定线帧格式头部的字节数。 */
+#define CSM_HEADER_SIZE 8
+
+/** 每个发出数据包中发送的协议版本字节。 */
+#define CSM_PROTOCOL_VERSION 0x01
+
+/* ------------------------------------------------------------------------- */
+/* 公共数据模型 */
+/* ------------------------------------------------------------------------- */
+
+/** 已解码的数据包(头部字段 + 堆分配的主体)。 */
+typedef struct csm_packet {
+ csm_packet_type_t type;
+ uint8_t version;
+ uint8_t flag1;
+ uint8_t flag2;
+ uint8_t *data; /**< 拥有的载荷缓冲区(或 NULL)。 */
+ size_t data_len; /**< `data` 的字节长度。 */
+} csm_packet_t;
+
+/** 成功的同步响应。 */
+typedef struct csm_command_response {
+ uint8_t *raw; /**< NUL 终止的 UTF-8 载荷(已拥有)。 */
+ size_t raw_len; /**< `raw` 的字节长度(不含 NUL)。 */
+} csm_command_response_t;
+
+/** ASYNC_RESP 数据包:载荷 + 服务器回显的原始命令。
+ *
+ * 服务器格式:``" <- "``。 */
+typedef struct csm_async_response {
+ char *raw; /**< 响应载荷(已拥有,NUL 终止)。 */
+ size_t raw_len;
+ char *original_command; /**< 回显的命令文本(已拥有)。 */
+} csm_async_response_t;
+
+/** STATUS 或 INTERRUPT 广播。
+ *
+ * 服务器格式:``" >> <- "``。 */
+typedef struct csm_status_notification {
+ csm_packet_type_t packet_type; /**< CSM_PT_STATUS 或 CSM_PT_INTERRUPT */
+ char *raw; /**< 完整载荷(已拥有)。 */
+ size_t raw_len;
+ char *status_name; /**< 已拥有,NUL 终止。 */
+ char *data; /**< 已拥有,NUL 终止。 */
+ char *module_name; /**< 已拥有,NUL 终止。 */
+} csm_status_notification_t;
+
+/** 释放通过出参(例如由 `csm_client_list_modules` 返回)分配的字符串。
+ * 传入 NULL 时安全。 */
+CSM_API void csm_string_free(char *s);
+
+/** 释放 `csm_command_response_t` 的堆成员(不释放结构体本身)。 */
+CSM_API void csm_command_response_dispose(csm_command_response_t *resp);
+
+/** 释放 `csm_async_response_t` 的堆成员(不释放结构体本身)。 */
+CSM_API void csm_async_response_dispose(csm_async_response_t *resp);
+
+/** 释放 `csm_status_notification_t` 的堆成员(不释放结构体本身)。 */
+CSM_API void csm_status_notification_dispose(csm_status_notification_t *n);
+
+/** 释放 `csm_packet_t` 的堆成员(不释放结构体本身)。 */
+CSM_API void csm_packet_dispose(csm_packet_t *pkt);
+
+/* ------------------------------------------------------------------------- */
+/* 服务器错误信息 */
+/* ------------------------------------------------------------------------- */
+
+/** 函数最近返回 CSM_ERR_SERVER 时的相关信息。 */
+typedef struct csm_server_error {
+ char code[32]; /**< NUL 终止的 CSM 错误码(可为空)。 */
+ char message[256]; /**< NUL 终止的错误消息(已截断)。 */
+} csm_server_error_t;
+
+/* ------------------------------------------------------------------------- */
+/* 协议编解码(暴露用于高级用途/测试) */
+/* ------------------------------------------------------------------------- */
+
+/** 将 *data*(`data_len` 字节)编码为完整的线帧格式数据包。
+ *
+ * 调用者必须传入至少 `CSM_HEADER_SIZE + data_len` 字节的 `out_buf`。
+ * 成功时 `*out_len` 将被设置为写入的字节数。
+ */
+CSM_API csm_result_t csm_encode_packet(const void *data,
+ size_t data_len,
+ csm_packet_type_t type,
+ uint8_t flag1,
+ uint8_t flag2,
+ uint8_t *out_buf,
+ size_t out_buf_size,
+ size_t *out_len);
+
+/** 将 8 字节头部解码为各组成字段。 */
+CSM_API csm_result_t csm_decode_header(const uint8_t *header_bytes,
+ size_t header_len,
+ uint32_t *out_data_len,
+ uint8_t *out_version,
+ uint8_t *out_type,
+ uint8_t *out_flag1,
+ uint8_t *out_flag2);
+
+/** 从原始头部 + 主体构建 `csm_packet_t`。返回的数据包
+ * **拥有** 主体的副本;使用 `csm_packet_dispose` 释放。
+ *
+ * 未知数据包类型字节将被映射到 `CSM_PT_INFO` 以实现前向
+ * 兼容性(服务器可能在未来版本中引入新类型)。
+ */
+CSM_API csm_result_t csm_parse_packet(const uint8_t *header_bytes,
+ size_t header_len,
+ const uint8_t *body,
+ size_t body_len,
+ csm_packet_t *out_packet);
+
+/* ------------------------------------------------------------------------- */
+/* 回调签名 */
+/* ------------------------------------------------------------------------- */
+
+/** 状态/中断通知回调。
+ *
+ * 从接收线程调用。必须快速且非阻塞。`notif`
+ * 指针及其成员仅在调用期间有效;
+ * 请在返回前复制所需数据。
+ */
+typedef void (*csm_status_callback_fn)(const csm_status_notification_t *notif,
+ void *user_data);
+
+/** 异步响应回调。与 `csm_status_callback_fn` 使用相同的线程规则。 */
+typedef void (*csm_async_callback_fn)(const csm_async_response_t *resp,
+ void *user_data);
+
+/* ------------------------------------------------------------------------- */
+/* 客户端生命周期 */
+/* ------------------------------------------------------------------------- */
+
+/** 不透明的线程安全客户端句柄。 */
+typedef struct csm_client csm_client_t;
+
+/** 创建新的客户端实例。分配失败时返回 NULL。 */
+CSM_API csm_client_t *csm_client_create(void);
+
+/** 断开连接(如已连接)并释放 *client* 持有的所有资源。 */
+CSM_API void csm_client_destroy(csm_client_t *client);
+
+/** 打开 TCP 连接并启动后台接收线程。
+ *
+ * @param connect_timeout_ms 连接超时时间(毫秒)。
+ * @return CSM_OK 或 CSM_ERR_CONNECTION / CSM_ERR_STATE / CSM_ERR_INVALID。
+ */
+CSM_API csm_result_t csm_client_connect(csm_client_t *client,
+ const char *host,
+ uint16_t port,
+ unsigned int connect_timeout_ms);
+
+/** 关闭连接并停止接收线程。未连接时调用安全;
+ * 任何被阻塞的调用者将立即收到 CSM_ERR_CONNECTION。 */
+CSM_API csm_result_t csm_client_disconnect(csm_client_t *client);
+
+/** 底层套接字打开时返回非零值。 */
+CSM_API int csm_client_is_connected(const csm_client_t *client);
+
+/** 轮询直到 *host*:*port* 接受连接或 *timeout_ms* 超时。
+ *
+ * @return 服务器可达时返回 CSM_OK;否则返回 CSM_ERR_TIMEOUT。 */
+CSM_API csm_result_t csm_client_wait_for_server(const char *host,
+ uint16_t port,
+ unsigned int timeout_ms,
+ unsigned int retry_interval_ms);
+
+/* ------------------------------------------------------------------------- */
+/* 核心命令方法 */
+/* ------------------------------------------------------------------------- */
+
+/** 发送同步命令并阻塞直到响应到达。
+ *
+ * 返回 CSM_OK 时调用者拥有 `*out_resp`,必须通过
+ * `csm_command_response_dispose` 释放。 */
+CSM_API csm_result_t csm_client_send_and_wait(csm_client_t *client,
+ const char *command,
+ unsigned int timeout_ms,
+ csm_command_response_t *out_resp);
+
+/** 发送异步命令(`->` 后缀)并阻塞直到
+ * `cmd-resp` 握手到达。最终的 `async-resp` 将被投递到
+ * 通过 `csm_client_register_async_callback` 注册的回调
+ * 以及轮询队列(`csm_client_poll_async_response`)。 */
+CSM_API csm_result_t csm_client_post(csm_client_t *client,
+ const char *command,
+ unsigned int timeout_ms);
+
+/** 发送异步无回复命令(`->|` 后缀)并阻塞直到
+ * `cmd-resp` 握手到达。 */
+CSM_API csm_result_t csm_client_post_no_reply(csm_client_t *client,
+ const char *command,
+ unsigned int timeout_ms);
+
+/** 发送 `Ping` 并测量往返延迟。
+ *
+ * @param out_elapsed_ms 成功时设置为往返时间(毫秒)。
+ * @return CSM_OK 或常规错误码之一。
+ */
+CSM_API csm_result_t csm_client_ping(csm_client_t *client,
+ unsigned int timeout_ms,
+ double *out_elapsed_ms);
+
+/* ------------------------------------------------------------------------- */
+/* 路由器管理辅助函数 */
+/* ------------------------------------------------------------------------- */
+
+/** 执行 `List` 并返回响应文本。调用者通过
+ * `csm_string_free` 释放 `*out_text`。 */
+CSM_API csm_result_t csm_client_list_modules(csm_client_t *client,
+ char **out_text,
+ unsigned int timeout_ms);
+
+/** 执行 `List API `。调用者释放 `*out_text`。 */
+CSM_API csm_result_t csm_client_list_api(csm_client_t *client,
+ const char *module,
+ char **out_text,
+ unsigned int timeout_ms);
+
+/** 执行 `List State `。调用者释放 `*out_text`。 */
+CSM_API csm_result_t csm_client_list_states(csm_client_t *client,
+ const char *module,
+ char **out_text,
+ unsigned int timeout_ms);
+
+/** 执行 `Help `。调用者释放 `*out_text`。 */
+CSM_API csm_result_t csm_client_help(csm_client_t *client,
+ const char *module,
+ char **out_text,
+ unsigned int timeout_ms);
+
+/* ------------------------------------------------------------------------- */
+/* 状态/中断订阅 */
+/* ------------------------------------------------------------------------- */
+
+/** 订阅 CSM 模块的状态广播。
+ *
+ * 发送 ``"@ ->"`` 并阻塞直到
+ * `cmd-resp` 握手到达。`callback`(若非 NULL)将从
+ * 接收线程对每个通知调用;通知同时也会入队以供
+ * 通过 `csm_client_poll_status` 轮询。
+ */
+CSM_API csm_result_t csm_client_subscribe_status(csm_client_t *client,
+ const char *status_name,
+ const char *module_name,
+ csm_status_callback_fn callback,
+ void *user_data,
+ unsigned int timeout_ms);
+
+/** 取消状态订阅。 */
+CSM_API csm_result_t csm_client_unsubscribe_status(csm_client_t *client,
+ const char *status_name,
+ const char *module_name,
+ unsigned int timeout_ms);
+
+/** 为匹配 *original_command* 的 `async-resp` 数据包注册回调。 */
+CSM_API csm_result_t csm_client_register_async_callback(csm_client_t *client,
+ const char *original_command,
+ csm_async_callback_fn callback,
+ void *user_data);
+
+/** 移除之前注册的异步回调。 */
+CSM_API csm_result_t csm_client_unregister_async_callback(csm_client_t *client,
+ const char *original_command);
+
+/* ------------------------------------------------------------------------- */
+/* 轮询队列(回调的替代方案) */
+/* ------------------------------------------------------------------------- */
+
+/** 从轮询队列弹出下一个状态/中断通知。
+ *
+ * @param timeout_ms 0 = 非阻塞;>0 = 最多阻塞 N 毫秒。
+ * @return 返回 CSM_OK 且 `*out_notif` 已填充(调用者通过
+ * `csm_status_notification_dispose` 释放);若队列在
+ * 超时内为空则返回 CSM_ERR_TIMEOUT;若断开连接则返回 CSM_ERR_CONNECTION。
+ */
+CSM_API csm_result_t csm_client_poll_status(csm_client_t *client,
+ csm_status_notification_t *out_notif,
+ unsigned int timeout_ms);
+
+/** 从轮询队列弹出下一个异步响应。 */
+CSM_API csm_result_t csm_client_poll_async_response(csm_client_t *client,
+ csm_async_response_t *out_resp,
+ unsigned int timeout_ms);
+
+/* ------------------------------------------------------------------------- */
+/* 最后一次服务器错误 */
+/* ------------------------------------------------------------------------- */
+
+/** 获取 *client* 观察到的最后一次 CSM_ERR_SERVER 的信息。
+ *
+ * 返回 CSM_OK 并用最近捕获的服务器错误码/消息填充 *out_err*;
+ * 否则(该客户端从未观察到服务器错误)返回 CSM_ERR_STATE。
+ * 存储的错误将无限期保留,直到下一次 CSM_ERR_SERVER 将其覆盖,
+ * 因此可以在操作失败后立即调用此函数,
+ * 而无需担心被中间无关的成功操作清除。
+ */
+CSM_API csm_result_t csm_client_last_server_error(const csm_client_t *client,
+ csm_server_error_t *out_err);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* CSM_TCP_ROUTER_CLIENT_H */
diff --git a/SDK/c/src/csm_tcp_router_client.c b/SDK/c/src/csm_tcp_router_client.c
new file mode 100644
index 0000000..7c3849a
--- /dev/null
+++ b/SDK/c/src/csm_tcp_router_client.c
@@ -0,0 +1,1622 @@
+/* csm_tcp_router_client.c - CSM-TCP-Router C 客户端 SDK 的跨平台实现。
+ *
+ * 线程模型:接收循环运行于单个后台线程。所有公共函数均可从任意线程安全调用;
+ * 客户端分别对同步(RESP)和命令握手(CMD_RESP)响应的并发等待者进行串行化,
+ * 与 Python SDK 保持一致。
+ *
+ * 套接字 / 线程抽象:
+ * - Windows:Winsock2 + Win32 CRITICAL_SECTION / CONDITION_VARIABLE / 线程。
+ * - POSIX: BSD 套接字 + pthreads。
+ */
+
+#if !defined(_WIN32)
+# ifndef _POSIX_C_SOURCE
+# define _POSIX_C_SOURCE 200809L
+# endif
+# ifndef _DEFAULT_SOURCE
+# define _DEFAULT_SOURCE 1
+# endif
+#endif
+
+#include "csm_tcp_router_client.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* CSM_BUILD_LIBRARY 由构建系统(CMake / MSBuild)在编译库时定义,
+ * 以便 csm_tcp_router_client.h 使用正确的 __declspec 装饰共享构建中
+ * 导出的符号。在此处无条件定义会破坏那些将此 .c 文件直接编译进
+ * 自己的 DLL(采用不同导出约定)的使用者。 */
+
+/* ========================================================================= */
+/* 平台抽象 */
+/* ========================================================================= */
+
+#if defined(_WIN32)
+# define WIN32_LEAN_AND_MEAN
+# include
+# include
+# include
+# pragma comment(lib, "Ws2_32.lib")
+
+typedef SOCKET csm_socket_t;
+# define CSM_INVALID_SOCKET INVALID_SOCKET
+# define csm_close_socket(s) closesocket(s)
+# define csm_socket_errno() WSAGetLastError()
+
+typedef CRITICAL_SECTION csm_mutex_t;
+typedef CONDITION_VARIABLE csm_cond_t;
+typedef HANDLE csm_thread_t;
+
+static void csm_mutex_init(csm_mutex_t *m) { InitializeCriticalSection(m); }
+static void csm_mutex_destroy(csm_mutex_t *m) { DeleteCriticalSection(m); }
+static void csm_mutex_lock(csm_mutex_t *m) { EnterCriticalSection(m); }
+static void csm_mutex_unlock(csm_mutex_t *m) { LeaveCriticalSection(m); }
+
+static void csm_cond_init(csm_cond_t *c) { InitializeConditionVariable(c); }
+static void csm_cond_destroy(csm_cond_t *c) { (void)c; }
+static void csm_cond_signal(csm_cond_t *c) { WakeConditionVariable(c); }
+#if 0 /* 保留供将来广播使用 */
+static void csm_cond_broadcast(csm_cond_t *c) { WakeAllConditionVariable(c); }
+#endif
+
+/* 有信号时返回 1,超时返回 0。 */
+static int csm_cond_wait_ms(csm_cond_t *c, csm_mutex_t *m, unsigned int ms) {
+ BOOL ok = SleepConditionVariableCS(c, m, ms == 0 ? INFINITE : ms);
+ if (ok) return 1;
+ return 0;
+}
+
+static void csm_sleep_ms(unsigned int ms) { Sleep(ms); }
+
+static double csm_monotonic_ms(void) {
+ LARGE_INTEGER freq, now;
+ QueryPerformanceFrequency(&freq);
+ QueryPerformanceCounter(&now);
+ return (double)now.QuadPart * 1000.0 / (double)freq.QuadPart;
+}
+
+#else /* POSIX */
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+
+typedef int csm_socket_t;
+# define CSM_INVALID_SOCKET (-1)
+# define csm_close_socket(s) close(s)
+# define csm_socket_errno() errno
+
+typedef pthread_mutex_t csm_mutex_t;
+typedef pthread_cond_t csm_cond_t;
+typedef pthread_t csm_thread_t;
+
+static void csm_mutex_init(csm_mutex_t *m) { pthread_mutex_init(m, NULL); }
+static void csm_mutex_destroy(csm_mutex_t *m) { pthread_mutex_destroy(m); }
+static void csm_mutex_lock(csm_mutex_t *m) { pthread_mutex_lock(m); }
+static void csm_mutex_unlock(csm_mutex_t *m) { pthread_mutex_unlock(m); }
+
+static void csm_cond_init(csm_cond_t *c) { pthread_cond_init(c, NULL); }
+static void csm_cond_destroy(csm_cond_t *c) { pthread_cond_destroy(c); }
+static void csm_cond_signal(csm_cond_t *c) { pthread_cond_signal(c); }
+#if 0 /* 保留供将来广播使用 */
+static void csm_cond_broadcast(csm_cond_t *c) { pthread_cond_broadcast(c); }
+#endif
+
+static int csm_cond_wait_ms(csm_cond_t *c, csm_mutex_t *m, unsigned int ms) {
+ if (ms == 0) {
+ pthread_cond_wait(c, m);
+ return 1;
+ }
+ struct timespec ts;
+# if defined(CLOCK_REALTIME)
+ clock_gettime(CLOCK_REALTIME, &ts);
+# else
+ struct timeval tv; gettimeofday(&tv, NULL);
+ ts.tv_sec = tv.tv_sec;
+ ts.tv_nsec = tv.tv_usec * 1000;
+# endif
+ ts.tv_sec += ms / 1000;
+ ts.tv_nsec += (long)(ms % 1000) * 1000000L;
+ if (ts.tv_nsec >= 1000000000L) {
+ ts.tv_sec += ts.tv_nsec / 1000000000L;
+ ts.tv_nsec = ts.tv_nsec % 1000000000L;
+ }
+ int rc = pthread_cond_timedwait(c, m, &ts);
+ return rc == 0 ? 1 : 0;
+}
+
+static void csm_sleep_ms(unsigned int ms) {
+ struct timespec ts;
+ ts.tv_sec = ms / 1000;
+ ts.tv_nsec = (long)(ms % 1000) * 1000000L;
+ nanosleep(&ts, NULL);
+}
+
+static double csm_monotonic_ms(void) {
+ struct timespec ts;
+# if defined(CLOCK_MONOTONIC)
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+# else
+ struct timeval tv; gettimeofday(&tv, NULL);
+ ts.tv_sec = tv.tv_sec;
+ ts.tv_nsec = tv.tv_usec * 1000;
+# endif
+ return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1.0e6;
+}
+#endif
+
+/* ========================================================================= */
+/* WSA 引导(仅 Windows)— 引用计数 */
+/* ========================================================================= */
+
+#if defined(_WIN32)
+static csm_mutex_t g_wsa_lock;
+static INIT_ONCE g_wsa_lock_init_once_state = INIT_ONCE_STATIC_INIT;
+static int g_wsa_refcount = 0;
+
+static BOOL CALLBACK csm_wsa_lock_init_once_cb(PINIT_ONCE init_once,
+ PVOID param,
+ PVOID *context) {
+ (void)init_once; (void)param; (void)context;
+ csm_mutex_init(&g_wsa_lock);
+ return TRUE;
+}
+
+static void csm_wsa_lock_init_once(void) {
+ /* InitOnceExecuteOnce 保证回调在进程内所有线程中恰好执行一次,
+ * 因此即使在并发创建客户端时,临界区也只会被初始化一次。 */
+ InitOnceExecuteOnce(&g_wsa_lock_init_once_state,
+ csm_wsa_lock_init_once_cb, NULL, NULL);
+}
+
+static int csm_wsa_startup(void) {
+ csm_wsa_lock_init_once();
+ csm_mutex_lock(&g_wsa_lock);
+ if (g_wsa_refcount == 0) {
+ WSADATA d;
+ if (WSAStartup(MAKEWORD(2, 2), &d) != 0) {
+ csm_mutex_unlock(&g_wsa_lock);
+ return -1;
+ }
+ }
+ g_wsa_refcount++;
+ csm_mutex_unlock(&g_wsa_lock);
+ return 0;
+}
+
+static void csm_wsa_cleanup(void) {
+ csm_wsa_lock_init_once();
+ csm_mutex_lock(&g_wsa_lock);
+ if (g_wsa_refcount > 0) {
+ g_wsa_refcount--;
+ if (g_wsa_refcount == 0) WSACleanup();
+ }
+ csm_mutex_unlock(&g_wsa_lock);
+}
+#else
+static int csm_wsa_startup(void) { return 0; }
+static void csm_wsa_cleanup(void) {}
+#endif
+
+/* ========================================================================= */
+/* 结果码辅助函数 */
+/* ========================================================================= */
+
+const char *csm_result_str(csm_result_t code) {
+ switch (code) {
+ case CSM_OK: return "OK";
+ case CSM_ERR_INVALID: return "Invalid argument";
+ case CSM_ERR_CONNECTION: return "Connection error";
+ case CSM_ERR_TIMEOUT: return "Timeout";
+ case CSM_ERR_PROTOCOL: return "Protocol error";
+ case CSM_ERR_SERVER: return "Server error";
+ case CSM_ERR_NOMEM: return "Out of memory";
+ case CSM_ERR_STATE: return "Invalid state";
+ case CSM_ERR_IO: return "I/O error";
+ }
+ return "Unknown";
+}
+
+/* ========================================================================= */
+/* 内存辅助函数 */
+/* ========================================================================= */
+
+static char *csm_strdup_n(const char *s, size_t n) {
+ char *out = (char *)malloc(n + 1);
+ if (!out) return NULL;
+ if (n) memcpy(out, s, n);
+ out[n] = '\0';
+ return out;
+}
+
+static char *csm_strdup_str(const char *s) {
+ return csm_strdup_n(s ? s : "", s ? strlen(s) : 0);
+}
+
+void csm_string_free(char *s) { free(s); }
+
+void csm_command_response_dispose(csm_command_response_t *resp) {
+ if (!resp) return;
+ free(resp->raw);
+ resp->raw = NULL;
+ resp->raw_len = 0;
+}
+
+void csm_async_response_dispose(csm_async_response_t *resp) {
+ if (!resp) return;
+ free(resp->raw);
+ free(resp->original_command);
+ resp->raw = NULL;
+ resp->original_command = NULL;
+ resp->raw_len = 0;
+}
+
+void csm_status_notification_dispose(csm_status_notification_t *n) {
+ if (!n) return;
+ free(n->raw);
+ free(n->status_name);
+ free(n->data);
+ free(n->module_name);
+ n->raw = NULL;
+ n->status_name = NULL;
+ n->data = NULL;
+ n->module_name = NULL;
+ n->raw_len = 0;
+}
+
+void csm_packet_dispose(csm_packet_t *pkt) {
+ if (!pkt) return;
+ free(pkt->data);
+ pkt->data = NULL;
+ pkt->data_len = 0;
+}
+
+/* ========================================================================= */
+/* 协议编解码 */
+/* ========================================================================= */
+
+static void csm_pack_be32(uint8_t *buf, uint32_t v) {
+ buf[0] = (uint8_t)((v >> 24) & 0xFF);
+ buf[1] = (uint8_t)((v >> 16) & 0xFF);
+ buf[2] = (uint8_t)((v >> 8) & 0xFF);
+ buf[3] = (uint8_t)( v & 0xFF);
+}
+
+static uint32_t csm_unpack_be32(const uint8_t *buf) {
+ return ((uint32_t)buf[0] << 24) |
+ ((uint32_t)buf[1] << 16) |
+ ((uint32_t)buf[2] << 8) |
+ (uint32_t)buf[3];
+}
+
+csm_result_t csm_encode_packet(const void *data,
+ size_t data_len,
+ csm_packet_type_t type,
+ uint8_t flag1,
+ uint8_t flag2,
+ uint8_t *out_buf,
+ size_t out_buf_size,
+ size_t *out_len) {
+ if (!out_buf || (data_len > 0 && !data)) return CSM_ERR_INVALID;
+ if (out_buf_size < CSM_HEADER_SIZE + data_len) return CSM_ERR_INVALID;
+
+ csm_pack_be32(out_buf, (uint32_t)data_len);
+ out_buf[4] = CSM_PROTOCOL_VERSION;
+ out_buf[5] = (uint8_t)type;
+ out_buf[6] = flag1;
+ out_buf[7] = flag2;
+ if (data_len) memcpy(out_buf + CSM_HEADER_SIZE, data, data_len);
+ if (out_len) *out_len = CSM_HEADER_SIZE + data_len;
+ return CSM_OK;
+}
+
+csm_result_t csm_decode_header(const uint8_t *header_bytes,
+ size_t header_len,
+ uint32_t *out_data_len,
+ uint8_t *out_version,
+ uint8_t *out_type,
+ uint8_t *out_flag1,
+ uint8_t *out_flag2) {
+ if (!header_bytes || header_len != CSM_HEADER_SIZE) return CSM_ERR_PROTOCOL;
+ if (out_data_len) *out_data_len = csm_unpack_be32(header_bytes);
+ if (out_version) *out_version = header_bytes[4];
+ if (out_type) *out_type = header_bytes[5];
+ if (out_flag1) *out_flag1 = header_bytes[6];
+ if (out_flag2) *out_flag2 = header_bytes[7];
+ return CSM_OK;
+}
+
+csm_result_t csm_parse_packet(const uint8_t *header_bytes,
+ size_t header_len,
+ const uint8_t *body,
+ size_t body_len,
+ csm_packet_t *out_packet) {
+ if (!out_packet) return CSM_ERR_INVALID;
+ uint32_t data_len = 0;
+ uint8_t version = 0, type_byte = 0, flag1 = 0, flag2 = 0;
+ csm_result_t r = csm_decode_header(header_bytes, header_len, &data_len,
+ &version, &type_byte, &flag1, &flag2);
+ if (r != CSM_OK) return r;
+ if ((size_t)data_len != body_len) return CSM_ERR_PROTOCOL;
+
+ /* 向前兼容:未知类型字节映射为 INFO。 */
+ csm_packet_type_t pt;
+ switch (type_byte) {
+ case CSM_PT_INFO:
+ case CSM_PT_ERROR:
+ case CSM_PT_CMD:
+ case CSM_PT_CMD_RESP:
+ case CSM_PT_RESP:
+ case CSM_PT_ASYNC_RESP:
+ case CSM_PT_STATUS:
+ case CSM_PT_INTERRUPT:
+ pt = (csm_packet_type_t)type_byte;
+ break;
+ default:
+ pt = CSM_PT_INFO;
+ break;
+ }
+
+ out_packet->type = pt;
+ out_packet->version = version;
+ out_packet->flag1 = flag1;
+ out_packet->flag2 = flag2;
+ out_packet->data_len = body_len;
+ out_packet->data = NULL;
+ if (body_len > 0) {
+ out_packet->data = (uint8_t *)malloc(body_len);
+ if (!out_packet->data) return CSM_ERR_NOMEM;
+ memcpy(out_packet->data, body, body_len);
+ }
+ return CSM_OK;
+}
+
+/* ========================================================================= */
+/* 内部:服务器错误解析 */
+/* ========================================================================= */
+
+/* 将形如 "[Error: ] " 的数据包载荷解析到 out_err。 */
+static void csm_parse_server_error(const uint8_t *data,
+ size_t len,
+ csm_server_error_t *out_err) {
+ out_err->code[0] = '\0';
+ out_err->message[0] = '\0';
+
+ /* 复制到以 NUL 结尾的栈缓冲区(截断至上限)。 */
+ char buf[1024];
+ size_t copy_len = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
+ if (copy_len) memcpy(buf, data, copy_len);
+ buf[copy_len] = '\0';
+
+ /* 去除尾部空白字符。 */
+ while (copy_len > 0 && (buf[copy_len - 1] == ' ' ||
+ buf[copy_len - 1] == '\r' ||
+ buf[copy_len - 1] == '\n' ||
+ buf[copy_len - 1] == '\t')) {
+ buf[--copy_len] = '\0';
+ }
+
+ const char *prefix = "[Error:";
+ size_t prefix_len = strlen(prefix);
+ const char *msg = buf;
+ if (copy_len >= prefix_len && strncmp(buf, prefix, prefix_len) == 0) {
+ char *end = strchr(buf, ']');
+ if (end) {
+ size_t code_len = (size_t)(end - (buf + prefix_len));
+ /* 去除错误码首尾的空格。 */
+ const char *cs = buf + prefix_len;
+ while (code_len && *cs == ' ') { cs++; code_len--; }
+ while (code_len && cs[code_len - 1] == ' ') code_len--;
+ if (code_len >= sizeof(out_err->code))
+ code_len = sizeof(out_err->code) - 1;
+ memcpy(out_err->code, cs, code_len);
+ out_err->code[code_len] = '\0';
+ msg = end + 1;
+ while (*msg == ' ') msg++;
+ }
+ }
+
+ size_t msg_len = strlen(msg);
+ if (msg_len >= sizeof(out_err->message))
+ msg_len = sizeof(out_err->message) - 1;
+ memcpy(out_err->message, msg, msg_len);
+ out_err->message[msg_len] = '\0';
+}
+
+/* ========================================================================= */
+/* 内部:有界队列 */
+/* ========================================================================= */
+
+/* 通用队列节点。元素持有数据包(用于 resp/cmd_resp)、
+ * 通知 / 异步响应(用于轮询队列),或哨兵(通过 `is_disconnect` 发出信号)。 */
+typedef struct csm_queue_node {
+ struct csm_queue_node *next;
+ void *item; /* 类型取决于所属队列 */
+ int is_disconnect;
+ int is_server_error;
+ csm_server_error_t server_error;
+} csm_queue_node_t;
+
+typedef struct csm_queue {
+ csm_queue_node_t *head;
+ csm_queue_node_t *tail;
+ csm_mutex_t lock;
+ csm_cond_t cond;
+} csm_queue_t;
+
+static void csm_queue_init(csm_queue_t *q) {
+ q->head = q->tail = NULL;
+ csm_mutex_init(&q->lock);
+ csm_cond_init(&q->cond);
+}
+
+static void csm_queue_destroy_with(csm_queue_t *q,
+ void (*free_item)(void *)) {
+ csm_queue_node_t *n = q->head;
+ while (n) {
+ csm_queue_node_t *next = n->next;
+ if (n->item && free_item) free_item(n->item);
+ free(n);
+ n = next;
+ }
+ q->head = q->tail = NULL;
+ csm_cond_destroy(&q->cond);
+ csm_mutex_destroy(&q->lock);
+}
+
+/* 压入一个元素;成功时获得 *item* 的所有权。 */
+static int csm_queue_push(csm_queue_t *q, void *item,
+ int is_disconnect, int is_server_error,
+ const csm_server_error_t *err) {
+ csm_queue_node_t *n = (csm_queue_node_t *)calloc(1, sizeof(*n));
+ if (!n) return -1;
+ n->item = item;
+ n->is_disconnect = is_disconnect;
+ n->is_server_error = is_server_error;
+ if (err) n->server_error = *err;
+
+ csm_mutex_lock(&q->lock);
+ if (q->tail) q->tail->next = n;
+ else q->head = n;
+ q->tail = n;
+ csm_cond_signal(&q->cond);
+ csm_mutex_unlock(&q->lock);
+ return 0;
+}
+
+/* 弹出一个元素,最多阻塞 *timeout_ms* 毫秒。成功时返回 CSM_OK 并设置 *out_item*
+ * (所有权转移),超时返回 CSM_ERR_TIMEOUT,连接断开返回 CSM_ERR_CONNECTION
+ *(哨兵),或返回 CSM_ERR_SERVER(同时填充 *out_err*)。 */
+static csm_result_t csm_queue_pop(csm_queue_t *q,
+ unsigned int timeout_ms,
+ void **out_item,
+ csm_server_error_t *out_err) {
+ if (out_item) *out_item = NULL;
+ double deadline = csm_monotonic_ms() + (double)timeout_ms;
+ csm_mutex_lock(&q->lock);
+ while (q->head == NULL) {
+ double remaining = deadline - csm_monotonic_ms();
+ if (remaining <= 0) {
+ csm_mutex_unlock(&q->lock);
+ return CSM_ERR_TIMEOUT;
+ }
+ unsigned int wait_ms = (unsigned int)remaining;
+ if (wait_ms == 0) wait_ms = 1;
+ csm_cond_wait_ms(&q->cond, &q->lock, wait_ms);
+ }
+ csm_queue_node_t *n = q->head;
+ q->head = n->next;
+ if (q->head == NULL) q->tail = NULL;
+ csm_mutex_unlock(&q->lock);
+
+ csm_result_t result = CSM_OK;
+ if (n->is_disconnect) {
+ result = CSM_ERR_CONNECTION;
+ } else if (n->is_server_error) {
+ if (out_err) *out_err = n->server_error;
+ result = CSM_ERR_SERVER;
+ } else if (out_item) {
+ *out_item = n->item;
+ n->item = NULL;
+ }
+ if (n->item) {
+ /* 元素未被消费(例如调用者传入了 NULL out_item)。
+ * 默认以字节方式通过调用者队列特定包装器设置的释放函数释放,
+ * 此处直接丢弃以保证无内存泄漏。 */
+ free(n->item);
+ }
+ free(n);
+ return result;
+}
+
+/* ========================================================================= */
+/* 订阅 / 异步回调注册表 */
+/* ========================================================================= */
+
+typedef struct csm_status_sub {
+ struct csm_status_sub *next;
+ char *status_name;
+ char *module_name;
+ csm_status_callback_fn callback;
+ void *user_data;
+} csm_status_sub_t;
+
+typedef struct csm_async_sub {
+ struct csm_async_sub *next;
+ char *original_command;
+ csm_async_callback_fn callback;
+ void *user_data;
+} csm_async_sub_t;
+
+/* ========================================================================= */
+/* 客户端 */
+/* ========================================================================= */
+
+struct csm_client {
+ csm_socket_t sock;
+ csm_thread_t recv_thread;
+ int recv_thread_running;
+ int connected; /* 在 state_lock 下设置 */
+ int stop_flag; /* 置位以请求关闭 */
+
+ csm_mutex_t state_lock; /* 保护 connected/stop_flag/sock */
+ csm_mutex_t send_lock; /* 串行化 sendall() */
+
+ csm_mutex_t resp_lock; /* 最多一个在途 RESP 等待者 */
+ csm_mutex_t cmd_resp_lock; /* 最多一个在途 CMD_RESP 等待者 */
+
+ csm_queue_t resp_queue; /* 元素:csm_packet_t* */
+ csm_queue_t cmd_resp_queue; /* 元素:csm_packet_t*(或 NULL) */
+ csm_queue_t status_queue; /* 元素:csm_status_notification_t* */
+ csm_queue_t async_queue; /* 元素:csm_async_response_t* */
+
+ csm_mutex_t sub_lock; /* 保护订阅注册表 */
+ csm_status_sub_t *status_subs;
+ csm_async_sub_t *async_subs;
+
+ csm_mutex_t err_lock;
+ int has_server_error;
+ csm_server_error_t last_server_error;
+};
+
+/* --- 辅助函数 --- */
+
+static void csm_packet_free_void(void *p) {
+ csm_packet_t *pkt = (csm_packet_t *)p;
+ if (!pkt) return;
+ csm_packet_dispose(pkt);
+ free(pkt);
+}
+
+static void csm_status_notif_free_void(void *p) {
+ csm_status_notification_t *n = (csm_status_notification_t *)p;
+ if (!n) return;
+ csm_status_notification_dispose(n);
+ free(n);
+}
+
+static void csm_async_resp_free_void(void *p) {
+ csm_async_response_t *r = (csm_async_response_t *)p;
+ if (!r) return;
+ csm_async_response_dispose(r);
+ free(r);
+}
+
+/* 在 state_lock 下设置 client.sock;关闭旧套接字(如有)。 */
+static void csm_set_socket_locked(csm_client_t *c, csm_socket_t s) {
+ if (c->sock != CSM_INVALID_SOCKET) csm_close_socket(c->sock);
+ c->sock = s;
+}
+
+/* 记录最近一次服务器错误,以便调用者在收到 CSM_ERR_SERVER 后获取。 */
+static void csm_record_server_error(csm_client_t *c,
+ const csm_server_error_t *err) {
+ csm_mutex_lock(&c->err_lock);
+ c->has_server_error = 1;
+ c->last_server_error = *err;
+ csm_mutex_unlock(&c->err_lock);
+}
+
+/* --- 订阅注册表(在 sub_lock 下操作)--- */
+
+static csm_status_sub_t *csm_find_status_sub(csm_client_t *c,
+ const char *status_name,
+ const char *module_name) {
+ csm_status_sub_t *s = c->status_subs;
+ while (s) {
+ if (strcmp(s->status_name, status_name) == 0 &&
+ strcmp(s->module_name, module_name) == 0) {
+ return s;
+ }
+ s = s->next;
+ }
+ return NULL;
+}
+
+static csm_result_t csm_register_status_sub(csm_client_t *c,
+ const char *status_name,
+ const char *module_name,
+ csm_status_callback_fn callback,
+ void *user_data) {
+ csm_mutex_lock(&c->sub_lock);
+ csm_status_sub_t *existing = csm_find_status_sub(c, status_name, module_name);
+ if (existing) {
+ existing->callback = callback;
+ existing->user_data = user_data;
+ csm_mutex_unlock(&c->sub_lock);
+ return CSM_OK;
+ }
+ csm_status_sub_t *s = (csm_status_sub_t *)calloc(1, sizeof(*s));
+ if (!s) { csm_mutex_unlock(&c->sub_lock); return CSM_ERR_NOMEM; }
+ s->status_name = csm_strdup_str(status_name);
+ s->module_name = csm_strdup_str(module_name);
+ if (!s->status_name || !s->module_name) {
+ free(s->status_name); free(s->module_name); free(s);
+ csm_mutex_unlock(&c->sub_lock);
+ return CSM_ERR_NOMEM;
+ }
+ s->callback = callback;
+ s->user_data = user_data;
+ s->next = c->status_subs;
+ c->status_subs = s;
+ csm_mutex_unlock(&c->sub_lock);
+ return CSM_OK;
+}
+
+static void csm_remove_status_sub(csm_client_t *c,
+ const char *status_name,
+ const char *module_name) {
+ csm_mutex_lock(&c->sub_lock);
+ csm_status_sub_t **pp = &c->status_subs;
+ while (*pp) {
+ csm_status_sub_t *s = *pp;
+ if (strcmp(s->status_name, status_name) == 0 &&
+ strcmp(s->module_name, module_name) == 0) {
+ *pp = s->next;
+ free(s->status_name);
+ free(s->module_name);
+ free(s);
+ break;
+ }
+ pp = &s->next;
+ }
+ csm_mutex_unlock(&c->sub_lock);
+}
+
+static csm_result_t csm_register_async_sub(csm_client_t *c,
+ const char *original_command,
+ csm_async_callback_fn callback,
+ void *user_data) {
+ csm_mutex_lock(&c->sub_lock);
+ csm_async_sub_t *s = c->async_subs;
+ while (s) {
+ if (strcmp(s->original_command, original_command) == 0) {
+ s->callback = callback;
+ s->user_data = user_data;
+ csm_mutex_unlock(&c->sub_lock);
+ return CSM_OK;
+ }
+ s = s->next;
+ }
+ s = (csm_async_sub_t *)calloc(1, sizeof(*s));
+ if (!s) { csm_mutex_unlock(&c->sub_lock); return CSM_ERR_NOMEM; }
+ s->original_command = csm_strdup_str(original_command);
+ if (!s->original_command) { free(s); csm_mutex_unlock(&c->sub_lock); return CSM_ERR_NOMEM; }
+ s->callback = callback;
+ s->user_data = user_data;
+ s->next = c->async_subs;
+ c->async_subs = s;
+ csm_mutex_unlock(&c->sub_lock);
+ return CSM_OK;
+}
+
+static void csm_remove_async_sub(csm_client_t *c, const char *original_command) {
+ csm_mutex_lock(&c->sub_lock);
+ csm_async_sub_t **pp = &c->async_subs;
+ while (*pp) {
+ csm_async_sub_t *s = *pp;
+ if (strcmp(s->original_command, original_command) == 0) {
+ *pp = s->next;
+ free(s->original_command);
+ free(s);
+ break;
+ }
+ pp = &s->next;
+ }
+ csm_mutex_unlock(&c->sub_lock);
+}
+
+static void csm_free_all_subs(csm_client_t *c) {
+ csm_mutex_lock(&c->sub_lock);
+ csm_status_sub_t *s = c->status_subs;
+ while (s) { csm_status_sub_t *n = s->next; free(s->status_name); free(s->module_name); free(s); s = n; }
+ c->status_subs = NULL;
+ csm_async_sub_t *a = c->async_subs;
+ while (a) { csm_async_sub_t *n = a->next; free(a->original_command); free(a); a = n; }
+ c->async_subs = NULL;
+ csm_mutex_unlock(&c->sub_lock);
+}
+
+/* --- 接收辅助函数 --- */
+
+/* 从套接字精确读取 *size* 字节;成功返回 0,EOF/错误返回 -1。 */
+static int csm_recv_all(csm_socket_t sock, uint8_t *buf, size_t size) {
+ size_t total = 0;
+ while (total < size) {
+#if defined(_WIN32)
+ int n = recv(sock, (char *)buf + total, (int)(size - total), 0);
+#else
+ ssize_t n = recv(sock, buf + total, size - total, 0);
+#endif
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+ return 0;
+}
+
+/* --- ASYNC_RESP / STATUS 载荷的解析辅助函数 --- */
+
+static void csm_async_resp_free_void(void *p);
+static void csm_status_notif_free_void(void *p);
+
+/* 从原始载荷数据构造 csm_async_response_t。 */
+static csm_async_response_t *csm_make_async_response(const uint8_t *data,
+ size_t len) {
+ csm_async_response_t *r = (csm_async_response_t *)calloc(1, sizeof(*r));
+ if (!r) return NULL;
+ /* 服务器格式:" <- "。 */
+ const char *sep = " <- ";
+ const size_t seplen = 4;
+ size_t split = (size_t)-1;
+ if (len >= seplen) {
+ for (size_t i = 0; i + seplen <= len; ++i) {
+ if (memcmp(data + i, sep, seplen) == 0) { split = i; break; }
+ }
+ }
+ if (split != (size_t)-1) {
+ r->raw = csm_strdup_n((const char *)data, split);
+ r->raw_len = split;
+ r->original_command = csm_strdup_n((const char *)data + split + seplen,
+ len - split - seplen);
+ } else {
+ r->raw = csm_strdup_n((const char *)data, len);
+ r->raw_len = len;
+ r->original_command = csm_strdup_str("");
+ }
+ if (!r->raw || !r->original_command) {
+ csm_async_resp_free_void(r);
+ return NULL;
+ }
+ return r;
+}
+
+/* 从原始载荷数据构造 csm_status_notification_t。 */
+static csm_status_notification_t *csm_make_status_notif(csm_packet_type_t pt,
+ const uint8_t *data,
+ size_t len) {
+ csm_status_notification_t *n = (csm_status_notification_t *)calloc(1, sizeof(*n));
+ if (!n) return NULL;
+ n->packet_type = pt;
+ n->raw = (char *)malloc(len + 1);
+ if (!n->raw) { free(n); return NULL; }
+ if (len) memcpy(n->raw, data, len);
+ n->raw[len] = '\0';
+ n->raw_len = len;
+
+ /* 从右向左查找最后一个 " <- " 分隔符(rsplit by 1)。 */
+ const char *raw_str = n->raw;
+ const char *left = raw_str;
+ size_t left_len = len;
+ const char *module_start = NULL;
+ size_t module_len = 0;
+ if (len >= 4) {
+ for (size_t i = len - 4 + 1; i-- > 0; ) {
+ if (memcmp(raw_str + i, " <- ", 4) == 0) {
+ left_len = i;
+ module_start = raw_str + i + 4;
+ module_len = len - i - 4;
+ break;
+ }
+ }
+ }
+
+ /* 去除模块名首尾空白字符。 */
+ while (module_len && (*module_start == ' ' || *module_start == '\t')) {
+ module_start++; module_len--;
+ }
+ while (module_len && (module_start[module_len - 1] == ' ' ||
+ module_start[module_len - 1] == '\t' ||
+ module_start[module_len - 1] == '\r' ||
+ module_start[module_len - 1] == '\n')) {
+ module_len--;
+ }
+
+ /* 以 " >> " 将左半部分拆分为 status_name 和 data。 */
+ const char *status_start = NULL;
+ size_t status_len = 0;
+ const char *data_start = left;
+ size_t data_len_local = left_len;
+ if (left_len >= 4) {
+ for (size_t i = 0; i + 4 <= left_len; ++i) {
+ if (memcmp(left + i, " >> ", 4) == 0) {
+ status_start = left;
+ status_len = i;
+ data_start = left + i + 4;
+ data_len_local = left_len - i - 4;
+ break;
+ }
+ }
+ }
+
+ /* 去除 status_name 和 data 首尾空白字符。 */
+ while (status_len && (*status_start == ' ' || *status_start == '\t')) { status_start++; status_len--; }
+ while (status_len && (status_start[status_len - 1] == ' ' || status_start[status_len - 1] == '\t')) status_len--;
+ while (data_len_local && (*data_start == ' ' || *data_start == '\t')) { data_start++; data_len_local--; }
+ while (data_len_local && (data_start[data_len_local - 1] == ' ' ||
+ data_start[data_len_local - 1] == '\t' ||
+ data_start[data_len_local - 1] == '\r' ||
+ data_start[data_len_local - 1] == '\n')) data_len_local--;
+
+ n->status_name = csm_strdup_n(status_start ? status_start : "", status_len);
+ n->data = csm_strdup_n(data_start, data_len_local);
+ n->module_name = csm_strdup_n(module_start ? module_start : "", module_len);
+ if (!n->status_name || !n->data || !n->module_name) {
+ csm_status_notif_free_void(n);
+ return NULL;
+ }
+ return n;
+}
+
+/* --- 接收线程 --- */
+
+static void csm_dispatch_packet(csm_client_t *c, csm_packet_t *pkt) {
+ /* 收到 RESP / CMD_RESP / ERROR 时,将数据包(或错误哨兵)的所有权转入队列。
+ * 收到 STATUS / ASYNC_RESP / INTERRUPT 时,构造高层对象并释放原始数据包。 */
+ switch (pkt->type) {
+ case CSM_PT_RESP: {
+ csm_packet_t *heap = (csm_packet_t *)malloc(sizeof(*heap));
+ if (!heap) { csm_packet_free_void(pkt); return; }
+ *heap = *pkt;
+ /* 压入 resp 队列;队列节点持有所有权。 */
+ if (csm_queue_push(&c->resp_queue, heap, 0, 0, NULL) != 0) {
+ csm_packet_free_void(heap);
+ }
+ free(pkt);
+ return;
+ }
+ case CSM_PT_CMD_RESP: {
+ csm_packet_t *heap = (csm_packet_t *)malloc(sizeof(*heap));
+ if (!heap) { csm_packet_free_void(pkt); return; }
+ *heap = *pkt;
+ if (csm_queue_push(&c->cmd_resp_queue, heap, 0, 0, NULL) != 0) {
+ csm_packet_free_void(heap);
+ }
+ free(pkt);
+ return;
+ }
+ case CSM_PT_ERROR: {
+ csm_server_error_t err;
+ csm_parse_server_error(pkt->data, pkt->data_len, &err);
+ csm_record_server_error(c, &err);
+ csm_queue_push(&c->resp_queue, NULL, 0, 1, &err);
+ csm_queue_push(&c->cmd_resp_queue, NULL, 0, 1, &err);
+ csm_packet_free_void(pkt);
+ return;
+ }
+ case CSM_PT_ASYNC_RESP: {
+ csm_async_response_t *r = csm_make_async_response(pkt->data, pkt->data_len);
+ if (r) {
+ /* 在 sub_lock 下查找回调。 */
+ csm_mutex_lock(&c->sub_lock);
+ csm_async_callback_fn cb = NULL; void *ud = NULL;
+ csm_async_sub_t *s = c->async_subs;
+ while (s) {
+ if (strcmp(s->original_command, r->original_command) == 0) {
+ cb = s->callback; ud = s->user_data; break;
+ }
+ s = s->next;
+ }
+ csm_mutex_unlock(&c->sub_lock);
+ if (cb) cb(r, ud);
+ /* 将副本压入轮询队列,使回调用户与轮询用户相互独立。 */
+ csm_async_response_t *queued = (csm_async_response_t *)calloc(1, sizeof(*queued));
+ if (queued) {
+ queued->raw = csm_strdup_n(r->raw, r->raw_len);
+ queued->raw_len = r->raw_len;
+ queued->original_command = csm_strdup_str(r->original_command);
+ if (queued->raw && queued->original_command) {
+ if (csm_queue_push(&c->async_queue, queued, 0, 0, NULL) != 0)
+ csm_async_resp_free_void(queued);
+ } else {
+ csm_async_resp_free_void(queued);
+ }
+ }
+ csm_async_resp_free_void(r);
+ }
+ csm_packet_free_void(pkt);
+ return;
+ }
+ case CSM_PT_STATUS:
+ case CSM_PT_INTERRUPT: {
+ csm_status_notification_t *n = csm_make_status_notif(pkt->type, pkt->data, pkt->data_len);
+ if (n) {
+ csm_mutex_lock(&c->sub_lock);
+ csm_status_callback_fn cb = NULL; void *ud = NULL;
+ csm_status_sub_t *s = c->status_subs;
+ while (s) {
+ if (strcmp(s->status_name, n->status_name) == 0 &&
+ strcmp(s->module_name, n->module_name) == 0) {
+ cb = s->callback; ud = s->user_data; break;
+ }
+ s = s->next;
+ }
+ csm_mutex_unlock(&c->sub_lock);
+ if (cb) cb(n, ud);
+ /* 将副本压入轮询队列。 */
+ csm_status_notification_t *q = (csm_status_notification_t *)calloc(1, sizeof(*q));
+ if (q) {
+ q->packet_type = n->packet_type;
+ q->raw = csm_strdup_n(n->raw, n->raw_len);
+ q->raw_len = n->raw_len;
+ q->status_name = csm_strdup_str(n->status_name);
+ q->data = csm_strdup_str(n->data);
+ q->module_name = csm_strdup_str(n->module_name);
+ if (q->raw && q->status_name && q->data && q->module_name) {
+ if (csm_queue_push(&c->status_queue, q, 0, 0, NULL) != 0)
+ csm_status_notif_free_void(q);
+ } else {
+ csm_status_notif_free_void(q);
+ }
+ }
+ csm_status_notif_free_void(n);
+ }
+ csm_packet_free_void(pkt);
+ return;
+ }
+ case CSM_PT_INFO:
+ case CSM_PT_CMD:
+ default:
+ /* INFO 静默丢弃;CMD 不由服务器发送。 */
+ csm_packet_free_void(pkt);
+ return;
+ }
+}
+
+#if defined(_WIN32)
+static unsigned __stdcall csm_recv_thread_main(void *arg)
+#else
+static void *csm_recv_thread_main(void *arg)
+#endif
+{
+ csm_client_t *c = (csm_client_t *)arg;
+ uint8_t header[CSM_HEADER_SIZE];
+ for (;;) {
+ /* 在 state_lock 下快照 stop_flag 和 sock。csm_client_disconnect()
+ * 在同一锁下修改这两个字段,因此不会出现撕裂读或过时的 sock 值,
+ * TSAN/UBSan 也不会产生警告。 */
+ csm_mutex_lock(&c->state_lock);
+ int stop_flag = c->stop_flag;
+ csm_socket_t sock = c->sock;
+ csm_mutex_unlock(&c->state_lock);
+
+ if (stop_flag) break;
+ if (sock == CSM_INVALID_SOCKET) break;
+ if (csm_recv_all(sock, header, CSM_HEADER_SIZE) != 0) break;
+ uint32_t data_len = csm_unpack_be32(header);
+ uint8_t *body = NULL;
+ if (data_len > 0) {
+ body = (uint8_t *)malloc(data_len);
+ if (!body) break;
+ if (csm_recv_all(sock, body, data_len) != 0) {
+ free(body);
+ break;
+ }
+ }
+ csm_packet_t parsed = {0};
+ csm_result_t r = csm_parse_packet(header, CSM_HEADER_SIZE, body, data_len, &parsed);
+ free(body);
+ if (r != CSM_OK) {
+ /* 跳过损坏帧;保持循环运行。 */
+ continue;
+ }
+ /* 分配堆副本以将所有权传递给派发函数。 */
+ csm_packet_t *heap_pkt = (csm_packet_t *)malloc(sizeof(*heap_pkt));
+ if (!heap_pkt) {
+ csm_packet_dispose(&parsed);
+ continue;
+ }
+ *heap_pkt = parsed;
+ csm_dispatch_packet(c, heap_pkt);
+ }
+
+ /* 通知所有阻塞的等待者连接已断开。 */
+ csm_queue_push(&c->resp_queue, NULL, 1, 0, NULL);
+ csm_queue_push(&c->cmd_resp_queue, NULL, 1, 0, NULL);
+ csm_queue_push(&c->status_queue, NULL, 1, 0, NULL);
+ csm_queue_push(&c->async_queue, NULL, 1, 0, NULL);
+
+ csm_mutex_lock(&c->state_lock);
+ c->connected = 0;
+ csm_mutex_unlock(&c->state_lock);
+#if defined(_WIN32)
+ return 0;
+#else
+ return NULL;
+#endif
+}
+
+/* --- 生命周期 --- */
+
+csm_client_t *csm_client_create(void) {
+ if (csm_wsa_startup() != 0) return NULL;
+ csm_client_t *c = (csm_client_t *)calloc(1, sizeof(*c));
+ if (!c) { csm_wsa_cleanup(); return NULL; }
+ c->sock = CSM_INVALID_SOCKET;
+ csm_mutex_init(&c->state_lock);
+ csm_mutex_init(&c->send_lock);
+ csm_mutex_init(&c->resp_lock);
+ csm_mutex_init(&c->cmd_resp_lock);
+ csm_mutex_init(&c->sub_lock);
+ csm_mutex_init(&c->err_lock);
+ csm_queue_init(&c->resp_queue);
+ csm_queue_init(&c->cmd_resp_queue);
+ csm_queue_init(&c->status_queue);
+ csm_queue_init(&c->async_queue);
+ return c;
+}
+
+void csm_client_destroy(csm_client_t *client) {
+ if (!client) return;
+ csm_client_disconnect(client);
+ csm_queue_destroy_with(&client->resp_queue, csm_packet_free_void);
+ csm_queue_destroy_with(&client->cmd_resp_queue, csm_packet_free_void);
+ csm_queue_destroy_with(&client->status_queue, csm_status_notif_free_void);
+ csm_queue_destroy_with(&client->async_queue, csm_async_resp_free_void);
+ csm_free_all_subs(client);
+ csm_mutex_destroy(&client->state_lock);
+ csm_mutex_destroy(&client->send_lock);
+ csm_mutex_destroy(&client->resp_lock);
+ csm_mutex_destroy(&client->cmd_resp_lock);
+ csm_mutex_destroy(&client->sub_lock);
+ csm_mutex_destroy(&client->err_lock);
+ free(client);
+ csm_wsa_cleanup();
+}
+
+/* 解析主机名并在超时限制内建立连接。返回 CSM_OK 或
+ * CSM_ERR_CONNECTION / CSM_ERR_TIMEOUT。 */
+static csm_result_t csm_do_connect(const char *host, uint16_t port,
+ unsigned int timeout_ms,
+ csm_socket_t *out_sock) {
+ char port_str[16];
+ snprintf(port_str, sizeof(port_str), "%u", (unsigned)port);
+
+ struct addrinfo hints, *res = NULL;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+ if (getaddrinfo(host, port_str, &hints, &res) != 0 || !res) {
+ return CSM_ERR_CONNECTION;
+ }
+
+ csm_result_t result = CSM_ERR_CONNECTION;
+ csm_socket_t sock = CSM_INVALID_SOCKET;
+ for (struct addrinfo *ai = res; ai; ai = ai->ai_next) {
+ sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ if (sock == CSM_INVALID_SOCKET) continue;
+
+ /* 切换为非阻塞模式以实现带超时的 connect。 */
+#if defined(_WIN32)
+ u_long mode = 1;
+ ioctlsocket(sock, FIONBIO, &mode);
+#else
+ int flags = fcntl(sock, F_GETFL, 0);
+ if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) {
+ csm_close_socket(sock);
+ sock = CSM_INVALID_SOCKET;
+ continue;
+ }
+#endif
+
+ int cr = connect(sock, ai->ai_addr, (int)ai->ai_addrlen);
+ if (cr == 0) {
+ result = CSM_OK;
+ } else {
+#if defined(_WIN32)
+ int err = WSAGetLastError();
+ int in_progress = (err == WSAEWOULDBLOCK);
+#else
+ int in_progress = (errno == EINPROGRESS);
+#endif
+ if (in_progress) {
+ fd_set wfds;
+ FD_ZERO(&wfds);
+ FD_SET(sock, &wfds);
+ struct timeval tv;
+ tv.tv_sec = timeout_ms / 1000;
+ tv.tv_usec = (long)(timeout_ms % 1000) * 1000;
+ int sel = select((int)(sock + 1), NULL, &wfds, NULL,
+ timeout_ms > 0 ? &tv : NULL);
+ if (sel > 0) {
+ int so_err = 0;
+ socklen_t sl = sizeof(so_err);
+ if (getsockopt(sock, SOL_SOCKET, SO_ERROR,
+ (char *)&so_err, &sl) == 0 && so_err == 0) {
+ result = CSM_OK;
+ }
+ } else if (sel == 0) {
+ result = CSM_ERR_TIMEOUT;
+ }
+ }
+ }
+
+ if (result == CSM_OK) {
+ /* 切换回阻塞模式供接收循环使用。 */
+#if defined(_WIN32)
+ u_long mode2 = 0;
+ ioctlsocket(sock, FIONBIO, &mode2);
+#else
+ int flags2 = fcntl(sock, F_GETFL, 0);
+ fcntl(sock, F_SETFL, flags2 & ~O_NONBLOCK);
+#endif
+ *out_sock = sock;
+ break;
+ }
+ csm_close_socket(sock);
+ sock = CSM_INVALID_SOCKET;
+ }
+
+ freeaddrinfo(res);
+ return result;
+}
+
+csm_result_t csm_client_connect(csm_client_t *client,
+ const char *host,
+ uint16_t port,
+ unsigned int connect_timeout_ms) {
+ if (!client || !host) return CSM_ERR_INVALID;
+
+ csm_mutex_lock(&client->state_lock);
+ if (client->connected) {
+ csm_mutex_unlock(&client->state_lock);
+ return CSM_ERR_STATE;
+ }
+ csm_mutex_unlock(&client->state_lock);
+
+ csm_socket_t sock = CSM_INVALID_SOCKET;
+ csm_result_t r = csm_do_connect(host, port,
+ connect_timeout_ms ? connect_timeout_ms : 5000,
+ &sock);
+ if (r != CSM_OK) return r;
+
+ csm_mutex_lock(&client->state_lock);
+ csm_set_socket_locked(client, sock);
+ client->connected = 1;
+ client->stop_flag = 0;
+ csm_mutex_unlock(&client->state_lock);
+
+ csm_mutex_lock(&client->err_lock);
+ client->has_server_error = 0;
+ csm_mutex_unlock(&client->err_lock);
+
+#if defined(_WIN32)
+ client->recv_thread = (HANDLE)_beginthreadex(NULL, 0, csm_recv_thread_main,
+ client, 0, NULL);
+ if (client->recv_thread == NULL) {
+ csm_mutex_lock(&client->state_lock);
+ csm_set_socket_locked(client, CSM_INVALID_SOCKET);
+ client->connected = 0;
+ csm_mutex_unlock(&client->state_lock);
+ return CSM_ERR_IO;
+ }
+#else
+ if (pthread_create(&client->recv_thread, NULL, csm_recv_thread_main, client) != 0) {
+ csm_mutex_lock(&client->state_lock);
+ csm_set_socket_locked(client, CSM_INVALID_SOCKET);
+ client->connected = 0;
+ csm_mutex_unlock(&client->state_lock);
+ return CSM_ERR_IO;
+ }
+#endif
+ client->recv_thread_running = 1;
+ return CSM_OK;
+}
+
+csm_result_t csm_client_disconnect(csm_client_t *client) {
+ if (!client) return CSM_ERR_INVALID;
+ csm_mutex_lock(&client->state_lock);
+ int was_connected = client->connected;
+ client->stop_flag = 1;
+ client->connected = 0;
+ csm_socket_t s = client->sock;
+ client->sock = CSM_INVALID_SOCKET;
+ csm_mutex_unlock(&client->state_lock);
+
+ /* 在关闭套接字前唤醒所有阻塞的等待者。 */
+ csm_queue_push(&client->resp_queue, NULL, 1, 0, NULL);
+ csm_queue_push(&client->cmd_resp_queue, NULL, 1, 0, NULL);
+ csm_queue_push(&client->status_queue, NULL, 1, 0, NULL);
+ csm_queue_push(&client->async_queue, NULL, 1, 0, NULL);
+
+ if (s != CSM_INVALID_SOCKET) {
+#if defined(_WIN32)
+ shutdown(s, SD_BOTH);
+#else
+ shutdown(s, SHUT_RDWR);
+#endif
+ csm_close_socket(s);
+ }
+
+ if (client->recv_thread_running) {
+#if defined(_WIN32)
+ WaitForSingleObject(client->recv_thread, 2000);
+ CloseHandle(client->recv_thread);
+#else
+ pthread_join(client->recv_thread, NULL);
+#endif
+ client->recv_thread_running = 0;
+ }
+ return was_connected ? CSM_OK : CSM_OK;
+}
+
+int csm_client_is_connected(const csm_client_t *client) {
+ if (!client) return 0;
+ /* 去除 const 以获取锁;逻辑上这是一次读操作。 */
+ csm_client_t *mc = (csm_client_t *)client;
+ csm_mutex_lock(&mc->state_lock);
+ int v = mc->connected;
+ csm_mutex_unlock(&mc->state_lock);
+ return v;
+}
+
+csm_result_t csm_client_wait_for_server(const char *host,
+ uint16_t port,
+ unsigned int timeout_ms,
+ unsigned int retry_interval_ms) {
+ if (!host) return CSM_ERR_INVALID;
+ if (csm_wsa_startup() != 0) return CSM_ERR_IO;
+ double deadline = csm_monotonic_ms() + (double)timeout_ms;
+ csm_result_t result = CSM_ERR_TIMEOUT;
+ while (csm_monotonic_ms() < deadline) {
+ csm_socket_t s = CSM_INVALID_SOCKET;
+ csm_result_t r = csm_do_connect(host, port, 1000, &s);
+ if (r == CSM_OK) {
+ csm_close_socket(s);
+ result = CSM_OK;
+ break;
+ }
+ csm_sleep_ms(retry_interval_ms ? retry_interval_ms : 500);
+ }
+ csm_wsa_cleanup();
+ return result;
+}
+
+/* --- 发送辅助函数 --- */
+
+static csm_result_t csm_send_raw(csm_client_t *client,
+ const uint8_t *data, size_t len) {
+ csm_mutex_lock(&client->state_lock);
+ if (!client->connected) {
+ csm_mutex_unlock(&client->state_lock);
+ return CSM_ERR_CONNECTION;
+ }
+ csm_socket_t sock = client->sock;
+ csm_mutex_unlock(&client->state_lock);
+
+ csm_mutex_lock(&client->send_lock);
+ size_t total = 0;
+#if defined(MSG_NOSIGNAL)
+ int send_flags = MSG_NOSIGNAL;
+#else
+ int send_flags = 0;
+#endif
+ while (total < len) {
+#if defined(_WIN32)
+ int n = send(sock, (const char *)data + total, (int)(len - total), 0);
+ (void)send_flags;
+#else
+ ssize_t n = send(sock, data + total, len - total, send_flags);
+#endif
+ if (n <= 0) {
+ csm_mutex_unlock(&client->send_lock);
+ csm_mutex_lock(&client->state_lock);
+ client->stop_flag = 1;
+ csm_mutex_unlock(&client->state_lock);
+ return CSM_ERR_CONNECTION;
+ }
+ total += (size_t)n;
+ }
+ csm_mutex_unlock(&client->send_lock);
+ return CSM_OK;
+}
+
+/* 打包并发送 CMD 数据包。 */
+static csm_result_t csm_send_cmd(csm_client_t *client, const char *command) {
+ size_t len = strlen(command);
+ uint8_t *buf = (uint8_t *)malloc(CSM_HEADER_SIZE + len);
+ if (!buf) return CSM_ERR_NOMEM;
+ size_t out_len = 0;
+ csm_result_t r = csm_encode_packet(command, len, CSM_PT_CMD, 0, 0,
+ buf, CSM_HEADER_SIZE + len, &out_len);
+ if (r == CSM_OK) r = csm_send_raw(client, buf, out_len);
+ free(buf);
+ return r;
+}
+
+/* --- 等待辅助函数 --- */
+
+static csm_result_t csm_wait_for_resp(csm_client_t *client,
+ unsigned int timeout_ms,
+ csm_command_response_t *out_resp) {
+ void *item = NULL;
+ csm_server_error_t err = {0};
+ csm_result_t r = csm_queue_pop(&client->resp_queue, timeout_ms, &item, &err);
+ if (r == CSM_ERR_SERVER) {
+ csm_record_server_error(client, &err);
+ return CSM_ERR_SERVER;
+ }
+ if (r != CSM_OK) return r;
+ csm_packet_t *pkt = (csm_packet_t *)item;
+ if (out_resp) {
+ out_resp->raw_len = pkt->data_len;
+ out_resp->raw = (uint8_t *)malloc(pkt->data_len + 1);
+ if (!out_resp->raw) { csm_packet_free_void(pkt); return CSM_ERR_NOMEM; }
+ if (pkt->data_len) memcpy(out_resp->raw, pkt->data, pkt->data_len);
+ out_resp->raw[pkt->data_len] = 0;
+ }
+ csm_packet_free_void(pkt);
+ return CSM_OK;
+}
+
+static csm_result_t csm_wait_for_cmd_resp(csm_client_t *client,
+ unsigned int timeout_ms) {
+ void *item = NULL;
+ csm_server_error_t err = {0};
+ csm_result_t r = csm_queue_pop(&client->cmd_resp_queue, timeout_ms, &item, &err);
+ if (r == CSM_ERR_SERVER) {
+ csm_record_server_error(client, &err);
+ return CSM_ERR_SERVER;
+ }
+ if (r != CSM_OK) return r;
+ /* 丢弃握手载荷。 */
+ csm_packet_t *pkt = (csm_packet_t *)item;
+ csm_packet_free_void(pkt);
+ return CSM_OK;
+}
+
+/* --- 公共命令 API --- */
+
+csm_result_t csm_client_send_and_wait(csm_client_t *client,
+ const char *command,
+ unsigned int timeout_ms,
+ csm_command_response_t *out_resp) {
+ if (!client || !command) return CSM_ERR_INVALID;
+ if (out_resp) { out_resp->raw = NULL; out_resp->raw_len = 0; }
+
+ csm_mutex_lock(&client->resp_lock);
+ csm_result_t r = csm_send_cmd(client, command);
+ if (r == CSM_OK) r = csm_wait_for_resp(client, timeout_ms ? timeout_ms : 5000, out_resp);
+ csm_mutex_unlock(&client->resp_lock);
+ return r;
+}
+
+csm_result_t csm_client_post(csm_client_t *client, const char *command,
+ unsigned int timeout_ms) {
+ if (!client || !command) return CSM_ERR_INVALID;
+ csm_mutex_lock(&client->cmd_resp_lock);
+ csm_result_t r = csm_send_cmd(client, command);
+ if (r == CSM_OK) r = csm_wait_for_cmd_resp(client, timeout_ms ? timeout_ms : 5000);
+ csm_mutex_unlock(&client->cmd_resp_lock);
+ return r;
+}
+
+csm_result_t csm_client_post_no_reply(csm_client_t *client, const char *command,
+ unsigned int timeout_ms) {
+ return csm_client_post(client, command, timeout_ms);
+}
+
+csm_result_t csm_client_ping(csm_client_t *client, unsigned int timeout_ms,
+ double *out_elapsed_ms) {
+ if (out_elapsed_ms) *out_elapsed_ms = 0.0;
+ csm_command_response_t resp = {0};
+ double t0 = csm_monotonic_ms();
+ csm_result_t r = csm_client_send_and_wait(client, "Ping",
+ timeout_ms ? timeout_ms : 2000, &resp);
+ csm_command_response_dispose(&resp);
+ if (r != CSM_OK) return r;
+ if (out_elapsed_ms) *out_elapsed_ms = csm_monotonic_ms() - t0;
+ return CSM_OK;
+}
+
+/* 共享辅助函数:发送固定文本命令并返回响应文本。 */
+static csm_result_t csm_send_text_query(csm_client_t *client,
+ const char *command,
+ char **out_text,
+ unsigned int timeout_ms) {
+ if (!out_text) return CSM_ERR_INVALID;
+ *out_text = NULL;
+ csm_command_response_t resp = {0};
+ csm_result_t r = csm_client_send_and_wait(client, command, timeout_ms, &resp);
+ if (r == CSM_OK) {
+ *out_text = (char *)resp.raw; /* 转移所有权;已以 NUL 结尾 */
+ resp.raw = NULL;
+ } else {
+ csm_command_response_dispose(&resp);
+ }
+ return r;
+}
+
+csm_result_t csm_client_list_modules(csm_client_t *client, char **out_text,
+ unsigned int timeout_ms) {
+ return csm_send_text_query(client, "List", out_text, timeout_ms);
+}
+
+/* 构造 " " 命令并发送。 */
+static csm_result_t csm_send_text_query_2(csm_client_t *client,
+ const char *prefix,
+ const char *module,
+ char **out_text,
+ unsigned int timeout_ms) {
+ if (!module) return CSM_ERR_INVALID;
+ size_t plen = strlen(prefix);
+ size_t mlen = strlen(module);
+ char *cmd = (char *)malloc(plen + 1 + mlen + 1);
+ if (!cmd) return CSM_ERR_NOMEM;
+ memcpy(cmd, prefix, plen);
+ cmd[plen] = ' ';
+ memcpy(cmd + plen + 1, module, mlen);
+ cmd[plen + 1 + mlen] = '\0';
+ csm_result_t r = csm_send_text_query(client, cmd, out_text, timeout_ms);
+ free(cmd);
+ return r;
+}
+
+csm_result_t csm_client_list_api(csm_client_t *client, const char *module,
+ char **out_text, unsigned int timeout_ms) {
+ return csm_send_text_query_2(client, "List API", module, out_text, timeout_ms);
+}
+
+csm_result_t csm_client_list_states(csm_client_t *client, const char *module,
+ char **out_text, unsigned int timeout_ms) {
+ return csm_send_text_query_2(client, "List State", module, out_text, timeout_ms);
+}
+
+csm_result_t csm_client_help(csm_client_t *client, const char *module,
+ char **out_text, unsigned int timeout_ms) {
+ return csm_send_text_query_2(client, "Help", module, out_text, timeout_ms);
+}
+
+/* --- 订阅 --- */
+
+csm_result_t csm_client_subscribe_status(csm_client_t *client,
+ const char *status_name,
+ const char *module_name,
+ csm_status_callback_fn callback,
+ void *user_data,
+ unsigned int timeout_ms) {
+ if (!client || !status_name || !module_name) return CSM_ERR_INVALID;
+
+ /* 先注册,以消除 STATUS 在回调存储前到达的竞态条件。 */
+ csm_result_t r = csm_register_status_sub(client, status_name, module_name,
+ callback, user_data);
+ if (r != CSM_OK) return r;
+
+ /* 构造 "@ ->"。 */
+ size_t s_len = strlen(status_name);
+ size_t m_len = strlen(module_name);
+ const char *suffix = " ->";
+ size_t suf_len = strlen(suffix);
+ char *cmd = (char *)malloc(s_len + 1 + m_len + suf_len + 1);
+ if (!cmd) { csm_remove_status_sub(client, status_name, module_name); return CSM_ERR_NOMEM; }
+ memcpy(cmd, status_name, s_len);
+ cmd[s_len] = '@';
+ memcpy(cmd + s_len + 1, module_name, m_len);
+ memcpy(cmd + s_len + 1 + m_len, suffix, suf_len);
+ cmd[s_len + 1 + m_len + suf_len] = '\0';
+
+ csm_mutex_lock(&client->cmd_resp_lock);
+ r = csm_send_cmd(client, cmd);
+ if (r == CSM_OK) r = csm_wait_for_cmd_resp(client, timeout_ms ? timeout_ms : 5000);
+ csm_mutex_unlock(&client->cmd_resp_lock);
+ free(cmd);
+
+ if (r != CSM_OK) csm_remove_status_sub(client, status_name, module_name);
+ return r;
+}
+
+csm_result_t csm_client_unsubscribe_status(csm_client_t *client,
+ const char *status_name,
+ const char *module_name,
+ unsigned int timeout_ms) {
+ if (!client || !status_name || !module_name) return CSM_ERR_INVALID;
+ size_t s_len = strlen(status_name);
+ size_t m_len = strlen(module_name);
+ const char *suffix = " ->";
+ size_t suf_len = strlen(suffix);
+ char *cmd = (char *)malloc(s_len + 1 + m_len + suf_len + 1);
+ if (!cmd) return CSM_ERR_NOMEM;
+ memcpy(cmd, status_name, s_len);
+ cmd[s_len] = '@';
+ memcpy(cmd + s_len + 1, module_name, m_len);
+ memcpy(cmd + s_len + 1 + m_len, suffix, suf_len);
+ cmd[s_len + 1 + m_len + suf_len] = '\0';
+
+ csm_mutex_lock(&client->cmd_resp_lock);
+ csm_result_t r = csm_send_cmd(client, cmd);
+ if (r == CSM_OK) r = csm_wait_for_cmd_resp(client, timeout_ms ? timeout_ms : 5000);
+ csm_mutex_unlock(&client->cmd_resp_lock);
+ free(cmd);
+
+ csm_remove_status_sub(client, status_name, module_name);
+ return r;
+}
+
+csm_result_t csm_client_register_async_callback(csm_client_t *client,
+ const char *original_command,
+ csm_async_callback_fn callback,
+ void *user_data) {
+ if (!client || !original_command || !callback) return CSM_ERR_INVALID;
+ return csm_register_async_sub(client, original_command, callback, user_data);
+}
+
+csm_result_t csm_client_unregister_async_callback(csm_client_t *client,
+ const char *original_command) {
+ if (!client || !original_command) return CSM_ERR_INVALID;
+ csm_remove_async_sub(client, original_command);
+ return CSM_OK;
+}
+
+/* --- 轮询队列 --- */
+
+csm_result_t csm_client_poll_status(csm_client_t *client,
+ csm_status_notification_t *out_notif,
+ unsigned int timeout_ms) {
+ if (!client || !out_notif) return CSM_ERR_INVALID;
+ memset(out_notif, 0, sizeof(*out_notif));
+ void *item = NULL;
+ csm_result_t r = csm_queue_pop(&client->status_queue, timeout_ms, &item, NULL);
+ if (r != CSM_OK) return r;
+ csm_status_notification_t *src = (csm_status_notification_t *)item;
+ /* 将字段所有权从 src 移至 out_notif。 */
+ *out_notif = *src;
+ free(src);
+ return CSM_OK;
+}
+
+csm_result_t csm_client_poll_async_response(csm_client_t *client,
+ csm_async_response_t *out_resp,
+ unsigned int timeout_ms) {
+ if (!client || !out_resp) return CSM_ERR_INVALID;
+ memset(out_resp, 0, sizeof(*out_resp));
+ void *item = NULL;
+ csm_result_t r = csm_queue_pop(&client->async_queue, timeout_ms, &item, NULL);
+ if (r != CSM_OK) return r;
+ csm_async_response_t *src = (csm_async_response_t *)item;
+ *out_resp = *src;
+ free(src);
+ return CSM_OK;
+}
+
+csm_result_t csm_client_last_server_error(const csm_client_t *client,
+ csm_server_error_t *out_err) {
+ if (!client || !out_err) return CSM_ERR_INVALID;
+ csm_client_t *mc = (csm_client_t *)client;
+ csm_mutex_lock(&mc->err_lock);
+ csm_result_t r = mc->has_server_error ? CSM_OK : CSM_ERR_STATE;
+ if (r == CSM_OK) *out_err = mc->last_server_error;
+ csm_mutex_unlock(&mc->err_lock);
+ return r;
+}
diff --git a/SDK/c/tests/mock_server.c b/SDK/c/tests/mock_server.c
new file mode 100644
index 0000000..7c62f4b
--- /dev/null
+++ b/SDK/c/tests/mock_server.c
@@ -0,0 +1,562 @@
+/* mock_server.c - 测试模拟服务器的跨平台实现。 */
+
+#if !defined(_WIN32)
+# ifndef _POSIX_C_SOURCE
+# define _POSIX_C_SOURCE 200809L
+# endif
+# ifndef _DEFAULT_SOURCE
+# define _DEFAULT_SOURCE 1
+# endif
+#endif
+
+#include "mock_server.h"
+
+#include
+#include
+#include
+
+#if defined(_WIN32)
+# define WIN32_LEAN_AND_MEAN
+# include
+# include
+# include
+# include
+# pragma comment(lib, "Ws2_32.lib")
+typedef SOCKET ms_socket_t;
+# define MS_INVALID_SOCKET INVALID_SOCKET
+# define ms_close_socket(s) closesocket(s)
+typedef CRITICAL_SECTION ms_mutex_t;
+typedef CONDITION_VARIABLE ms_cond_t;
+typedef HANDLE ms_thread_t;
+static void ms_mutex_init(ms_mutex_t *m){InitializeCriticalSection(m);}
+static void ms_mutex_destroy(ms_mutex_t *m){DeleteCriticalSection(m);}
+static void ms_mutex_lock(ms_mutex_t *m){EnterCriticalSection(m);}
+static void ms_mutex_unlock(ms_mutex_t *m){LeaveCriticalSection(m);}
+static void ms_cond_init(ms_cond_t *c){InitializeConditionVariable(c);}
+static void ms_cond_destroy(ms_cond_t *c){(void)c;}
+static void ms_cond_signal(ms_cond_t *c){WakeConditionVariable(c);}
+static int ms_cond_wait_ms(ms_cond_t *c, ms_mutex_t *m, unsigned int ms){
+ return SleepConditionVariableCS(c, m, ms == 0 ? INFINITE : ms) ? 1 : 0;
+}
+#if 0 /* 保留供将来使用 */
+static void ms_sleep_ms(unsigned int ms){Sleep(ms);}
+#endif
+#else
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+typedef int ms_socket_t;
+# define MS_INVALID_SOCKET (-1)
+# define ms_close_socket(s) close(s)
+typedef pthread_mutex_t ms_mutex_t;
+typedef pthread_cond_t ms_cond_t;
+typedef pthread_t ms_thread_t;
+static void ms_mutex_init(ms_mutex_t *m){pthread_mutex_init(m,NULL);}
+static void ms_mutex_destroy(ms_mutex_t *m){pthread_mutex_destroy(m);}
+static void ms_mutex_lock(ms_mutex_t *m){pthread_mutex_lock(m);}
+static void ms_mutex_unlock(ms_mutex_t *m){pthread_mutex_unlock(m);}
+static void ms_cond_init(ms_cond_t *c){pthread_cond_init(c,NULL);}
+static void ms_cond_destroy(ms_cond_t *c){pthread_cond_destroy(c);}
+static void ms_cond_signal(ms_cond_t *c){pthread_cond_signal(c);}
+static int ms_cond_wait_ms(ms_cond_t *c, ms_mutex_t *m, unsigned int ms){
+ if (ms == 0){pthread_cond_wait(c,m);return 1;}
+ struct timespec ts; clock_gettime(CLOCK_REALTIME,&ts);
+ ts.tv_sec += ms/1000;
+ ts.tv_nsec += (long)(ms%1000)*1000000L;
+ if (ts.tv_nsec>=1000000000L){ts.tv_sec+=ts.tv_nsec/1000000000L;ts.tv_nsec%=1000000000L;}
+ return pthread_cond_timedwait(c,m,&ts) == 0 ? 1 : 0;
+}
+#if 0 /* 保留供将来使用 */
+static void ms_sleep_ms(unsigned int ms){
+ struct timespec ts; ts.tv_sec=ms/1000; ts.tv_nsec=(long)(ms%1000)*1000000L;
+ nanosleep(&ts,NULL);
+}
+#endif
+#endif
+
+#ifndef INADDR_LOOPBACK
+# define INADDR_LOOPBACK 0x7F000001UL
+#endif
+
+#define MS_MAX_CLIENTS 16
+#define MS_HEADER 8
+#define MS_VER 0x01
+
+/* --- 响应映射 --- */
+typedef struct ms_resp {
+ struct ms_resp *next;
+ char *cmd;
+ csm_packet_type_t type;
+ uint8_t *data;
+ size_t data_len;
+} ms_resp_t;
+
+/* --- 已接收命令队列 --- */
+typedef struct ms_msg {
+ struct ms_msg *next;
+ char *text;
+} ms_msg_t;
+
+struct csm_mock_server {
+ ms_socket_t listen_sock;
+ uint16_t port;
+ int stop;
+ int thread_started;
+ ms_thread_t accept_thread;
+
+ ms_mutex_t resp_lock;
+ ms_resp_t *responses;
+
+ ms_mutex_t recv_lock;
+ ms_cond_t recv_cond;
+ ms_msg_t *msg_head;
+ ms_msg_t *msg_tail;
+
+ ms_mutex_t client_lock;
+ ms_socket_t clients[MS_MAX_CLIENTS];
+
+ ms_mutex_t handler_lock;
+ int handler_count;
+ ms_cond_t handler_done;
+};
+
+static int ms_wsa_init(void) {
+#if defined(_WIN32)
+ WSADATA d; return WSAStartup(MAKEWORD(2,2), &d) == 0 ? 0 : -1;
+#else
+ return 0;
+#endif
+}
+static void ms_wsa_cleanup(void) {
+#if defined(_WIN32)
+ WSACleanup();
+#endif
+}
+
+static void ms_pack_be32(uint8_t *b, uint32_t v) {
+ b[0]=(uint8_t)((v>>24)&0xFF); b[1]=(uint8_t)((v>>16)&0xFF);
+ b[2]=(uint8_t)((v>>8)&0xFF); b[3]=(uint8_t)(v&0xFF);
+}
+static uint32_t ms_unpack_be32(const uint8_t *b){
+ return ((uint32_t)b[0]<<24)|((uint32_t)b[1]<<16)|((uint32_t)b[2]<<8)|(uint32_t)b[3];
+}
+
+/* 将头部 + 载荷编码到新分配的缓冲区中;调用者负责释放。 */
+static uint8_t *ms_encode(csm_packet_type_t type, const void *data, size_t len, size_t *out_len) {
+ uint8_t *buf = (uint8_t *)malloc(MS_HEADER + len);
+ if (!buf) return NULL;
+ ms_pack_be32(buf, (uint32_t)len);
+ buf[4] = MS_VER; buf[5] = (uint8_t)type; buf[6] = 0; buf[7] = 0;
+ if (len) memcpy(buf + MS_HEADER, data, len);
+ *out_len = MS_HEADER + len;
+ return buf;
+}
+
+static int ms_send_all(ms_socket_t s, const uint8_t *buf, size_t len) {
+ size_t total = 0;
+#if defined(MSG_NOSIGNAL)
+ int flags = MSG_NOSIGNAL;
+#else
+ int flags = 0;
+#endif
+ while (total < len) {
+#if defined(_WIN32)
+ int n = send(s, (const char *)buf + total, (int)(len - total), 0);
+ (void)flags;
+#else
+ ssize_t n = send(s, buf + total, len - total, flags);
+#endif
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+ return 0;
+}
+
+static int ms_recv_all(ms_socket_t s, uint8_t *buf, size_t len) {
+ size_t total = 0;
+ while (total < len) {
+#if defined(_WIN32)
+ int n = recv(s, (char *)buf + total, (int)(len - total), 0);
+#else
+ ssize_t n = recv(s, buf + total, len - total, 0);
+#endif
+ if (n <= 0) return -1;
+ total += (size_t)n;
+ }
+ return 0;
+}
+
+/* --- 公共 API --- */
+
+csm_mock_server_t *csm_mock_server_create(void) {
+ if (ms_wsa_init() != 0) return NULL;
+ csm_mock_server_t *s = (csm_mock_server_t *)calloc(1, sizeof(*s));
+ if (!s) { ms_wsa_cleanup(); return NULL; }
+ s->listen_sock = MS_INVALID_SOCKET;
+ ms_mutex_init(&s->resp_lock);
+ ms_mutex_init(&s->recv_lock);
+ ms_cond_init(&s->recv_cond);
+ ms_mutex_init(&s->client_lock);
+ ms_mutex_init(&s->handler_lock);
+ ms_cond_init(&s->handler_done);
+ for (int i = 0; i < MS_MAX_CLIENTS; ++i) s->clients[i] = MS_INVALID_SOCKET;
+ return s;
+}
+
+static void ms_handle_command(csm_mock_server_t *s, ms_socket_t conn,
+ const char *cmd) {
+ /* 查找自定义响应。 */
+ ms_mutex_lock(&s->resp_lock);
+ ms_resp_t *r = s->responses;
+ while (r) {
+ if (strcmp(r->cmd, cmd) == 0) {
+ size_t out_len = 0;
+ uint8_t *wire = ms_encode(r->type, r->data, r->data_len, &out_len);
+ ms_mutex_unlock(&s->resp_lock);
+ if (wire) { ms_send_all(conn, wire, out_len); free(wire); }
+ return;
+ }
+ r = r->next;
+ }
+ ms_mutex_unlock(&s->resp_lock);
+
+ /* 内置默认值。 */
+ size_t out_len = 0;
+ uint8_t *wire = NULL;
+ if (strcmp(cmd, "Ping") == 0) {
+ wire = ms_encode(CSM_PT_RESP, "Pong", 4, &out_len);
+ } else if (strcmp(cmd, "List") == 0) {
+ const char *txt = "AI\nDIO\nSystem";
+ wire = ms_encode(CSM_PT_RESP, txt, strlen(txt), &out_len);
+ } else if (strncmp(cmd, "List API ", 9) == 0) {
+ char buf[256];
+ snprintf(buf, sizeof(buf), "API: Start -> %s\nAPI: Stop -> %s",
+ cmd + 9, cmd + 9);
+ wire = ms_encode(CSM_PT_RESP, buf, strlen(buf), &out_len);
+ } else if (strncmp(cmd, "List State ", 11) == 0) {
+ char buf[256];
+ snprintf(buf, sizeof(buf), "Idle <- %s\nRunning <- %s",
+ cmd + 11, cmd + 11);
+ wire = ms_encode(CSM_PT_RESP, buf, strlen(buf), &out_len);
+ } else if (strstr(cmd, "->") || strstr(cmd, "->")) {
+ wire = ms_encode(CSM_PT_CMD_RESP, "", 0, &out_len);
+ } else {
+ /* 通用异步握手。 */
+ wire = ms_encode(CSM_PT_CMD_RESP, "", 0, &out_len);
+ }
+ if (wire) { ms_send_all(conn, wire, out_len); free(wire); }
+}
+
+#if defined(_WIN32)
+static unsigned __stdcall
+#else
+static void *
+#endif
+ms_client_thread(void *arg) {
+ typedef struct { csm_mock_server_t *s; ms_socket_t conn; } ms_ctx_t;
+ ms_ctx_t *ctx = (ms_ctx_t *)arg;
+ csm_mock_server_t *s = ctx->s;
+ ms_socket_t conn = ctx->conn;
+ free(ctx);
+
+ /* 发送欢迎 INFO 包。 */
+ size_t wlen = 0;
+ uint8_t *welcome = ms_encode(CSM_PT_INFO, "Welcome to mock server", 22, &wlen);
+ if (welcome) { ms_send_all(conn, welcome, wlen); free(welcome); }
+
+ while (!s->stop) {
+ uint8_t hdr[MS_HEADER];
+ if (ms_recv_all(conn, hdr, MS_HEADER) != 0) break;
+ uint32_t data_len = ms_unpack_be32(hdr);
+ uint8_t *body = NULL;
+ if (data_len) {
+ body = (uint8_t *)malloc(data_len);
+ if (!body) break;
+ if (ms_recv_all(conn, body, data_len) != 0) { free(body); break; }
+ }
+ if (hdr[5] == CSM_PT_CMD) {
+ char *cmd = (char *)malloc(data_len + 1);
+ if (cmd) {
+ if (data_len) memcpy(cmd, body, data_len);
+ cmd[data_len] = '\0';
+
+ /* 修剪尾部空白。 */
+ size_t L = strlen(cmd);
+ while (L && (cmd[L-1]==' '||cmd[L-1]=='\r'||cmd[L-1]=='\n'||cmd[L-1]=='\t'))
+ cmd[--L] = '\0';
+
+ /* 先处理命令(使用本地副本)。 */
+ ms_handle_command(s, conn, cmd);
+
+ /* 然后将副本入队供测试检查。 */
+ ms_msg_t *m = (ms_msg_t *)calloc(1, sizeof(*m));
+ if (m) {
+ m->text = (char *)malloc(strlen(cmd) + 1);
+ if (m->text) {
+ strcpy(m->text, cmd);
+ ms_mutex_lock(&s->recv_lock);
+ if (s->msg_tail) s->msg_tail->next = m;
+ else s->msg_head = m;
+ s->msg_tail = m;
+ ms_cond_signal(&s->recv_cond);
+ ms_mutex_unlock(&s->recv_lock);
+ } else {
+ free(m);
+ }
+ }
+ free(cmd);
+ }
+ }
+ free(body);
+ }
+
+ /* 从客户端列表中移除。 */
+ ms_mutex_lock(&s->client_lock);
+ for (int i = 0; i < MS_MAX_CLIENTS; ++i) {
+ if (s->clients[i] == conn) { s->clients[i] = MS_INVALID_SOCKET; break; }
+ }
+ ms_mutex_unlock(&s->client_lock);
+ ms_close_socket(conn);
+
+ ms_mutex_lock(&s->handler_lock);
+ s->handler_count--;
+ if (s->handler_count == 0) ms_cond_signal(&s->handler_done);
+ ms_mutex_unlock(&s->handler_lock);
+#if defined(_WIN32)
+ return 0;
+#else
+ return NULL;
+#endif
+}
+
+#if defined(_WIN32)
+static unsigned __stdcall
+#else
+static void *
+#endif
+ms_accept_thread(void *arg) {
+ csm_mock_server_t *s = (csm_mock_server_t *)arg;
+ while (!s->stop) {
+ fd_set rfds; FD_ZERO(&rfds); FD_SET(s->listen_sock, &rfds);
+ struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 200 * 1000;
+ int sel = select((int)(s->listen_sock + 1), &rfds, NULL, NULL, &tv);
+ if (sel <= 0) continue;
+ struct sockaddr_in addr; socklen_t alen = sizeof(addr);
+ ms_socket_t conn = accept(s->listen_sock, (struct sockaddr *)&addr, &alen);
+ if (conn == MS_INVALID_SOCKET) continue;
+ ms_mutex_lock(&s->client_lock);
+ for (int i = 0; i < MS_MAX_CLIENTS; ++i) {
+ if (s->clients[i] == MS_INVALID_SOCKET) { s->clients[i] = conn; break; }
+ }
+ ms_mutex_unlock(&s->client_lock);
+
+ typedef struct { csm_mock_server_t *s; ms_socket_t conn; } ms_ctx_t;
+ ms_ctx_t *ctx = (ms_ctx_t *)malloc(sizeof(*ctx));
+ if (!ctx) { ms_close_socket(conn); continue; }
+ ctx->s = s; ctx->conn = conn;
+
+ ms_mutex_lock(&s->handler_lock);
+ s->handler_count++;
+ ms_mutex_unlock(&s->handler_lock);
+
+#if defined(_WIN32)
+ HANDLE t = (HANDLE)_beginthreadex(NULL, 0, ms_client_thread, ctx, 0, NULL);
+ if (t) CloseHandle(t);
+ else {
+ free(ctx); ms_close_socket(conn);
+ ms_mutex_lock(&s->handler_lock); s->handler_count--; ms_mutex_unlock(&s->handler_lock);
+ }
+#else
+ pthread_t t;
+ if (pthread_create(&t, NULL, ms_client_thread, ctx) == 0) {
+ pthread_detach(t);
+ } else {
+ free(ctx); ms_close_socket(conn);
+ ms_mutex_lock(&s->handler_lock); s->handler_count--; ms_mutex_unlock(&s->handler_lock);
+ }
+#endif
+ }
+#if defined(_WIN32)
+ return 0;
+#else
+ return NULL;
+#endif
+}
+
+int csm_mock_server_start(csm_mock_server_t *s) {
+ if (!s) return -1;
+ s->listen_sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (s->listen_sock == MS_INVALID_SOCKET) return -1;
+ int yes = 1;
+ setsockopt(s->listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
+ struct sockaddr_in a; memset(&a, 0, sizeof(a));
+ a.sin_family = AF_INET;
+ a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ a.sin_port = 0;
+ if (bind(s->listen_sock, (struct sockaddr *)&a, sizeof(a)) != 0) {
+ ms_close_socket(s->listen_sock); s->listen_sock = MS_INVALID_SOCKET; return -1;
+ }
+ socklen_t alen = sizeof(a);
+ getsockname(s->listen_sock, (struct sockaddr *)&a, &alen);
+ s->port = ntohs(a.sin_port);
+ if (listen(s->listen_sock, 8) != 0) {
+ ms_close_socket(s->listen_sock); s->listen_sock = MS_INVALID_SOCKET; return -1;
+ }
+ s->stop = 0;
+#if defined(_WIN32)
+ s->accept_thread = (HANDLE)_beginthreadex(NULL, 0, ms_accept_thread, s, 0, NULL);
+ if (s->accept_thread == NULL) return -1;
+#else
+ if (pthread_create(&s->accept_thread, NULL, ms_accept_thread, s) != 0) return -1;
+#endif
+ s->thread_started = 1;
+ return 0;
+}
+
+void csm_mock_server_stop(csm_mock_server_t *s) {
+ if (!s) return;
+ s->stop = 1;
+ if (s->listen_sock != MS_INVALID_SOCKET) {
+#if defined(_WIN32)
+ shutdown(s->listen_sock, SD_BOTH);
+#else
+ shutdown(s->listen_sock, SHUT_RDWR);
+#endif
+ ms_close_socket(s->listen_sock);
+ s->listen_sock = MS_INVALID_SOCKET;
+ }
+ /* 关闭客户端套接字以唤醒处理程序。 */
+ ms_mutex_lock(&s->client_lock);
+ for (int i = 0; i < MS_MAX_CLIENTS; ++i) {
+ if (s->clients[i] != MS_INVALID_SOCKET) {
+#if defined(_WIN32)
+ shutdown(s->clients[i], SD_BOTH);
+#else
+ shutdown(s->clients[i], SHUT_RDWR);
+#endif
+ ms_close_socket(s->clients[i]);
+ s->clients[i] = MS_INVALID_SOCKET;
+ }
+ }
+ ms_mutex_unlock(&s->client_lock);
+
+ if (s->thread_started) {
+#if defined(_WIN32)
+ WaitForSingleObject(s->accept_thread, 2000);
+ CloseHandle(s->accept_thread);
+#else
+ pthread_join(s->accept_thread, NULL);
+#endif
+ s->thread_started = 0;
+ }
+ /* 等待处理程序完成(尽力而为,时间较短)。 */
+ ms_mutex_lock(&s->handler_lock);
+ int waited = 0;
+ while (s->handler_count > 0 && waited < 20) {
+ ms_cond_wait_ms(&s->handler_done, &s->handler_lock, 100);
+ waited++;
+ }
+ ms_mutex_unlock(&s->handler_lock);
+}
+
+void csm_mock_server_destroy(csm_mock_server_t *s) {
+ if (!s) return;
+ csm_mock_server_stop(s);
+ ms_mutex_lock(&s->resp_lock);
+ ms_resp_t *r = s->responses;
+ while (r) { ms_resp_t *n = r->next; free(r->cmd); free(r->data); free(r); r = n; }
+ s->responses = NULL;
+ ms_mutex_unlock(&s->resp_lock);
+
+ ms_mutex_lock(&s->recv_lock);
+ ms_msg_t *m = s->msg_head;
+ while (m) { ms_msg_t *n = m->next; free(m->text); free(m); m = n; }
+ s->msg_head = s->msg_tail = NULL;
+ ms_mutex_unlock(&s->recv_lock);
+
+ ms_mutex_destroy(&s->resp_lock);
+ ms_mutex_destroy(&s->recv_lock);
+ ms_cond_destroy(&s->recv_cond);
+ ms_mutex_destroy(&s->client_lock);
+ ms_mutex_destroy(&s->handler_lock);
+ ms_cond_destroy(&s->handler_done);
+ free(s);
+ ms_wsa_cleanup();
+}
+
+uint16_t csm_mock_server_port(const csm_mock_server_t *s) {
+ return s ? s->port : 0;
+}
+
+static void ms_set_response_typed(csm_mock_server_t *s, const char *cmd_text,
+ csm_packet_type_t type, const char *data) {
+ ms_resp_t *r = (ms_resp_t *)calloc(1, sizeof(*r));
+ if (!r) return;
+ r->cmd = (char *)malloc(strlen(cmd_text) + 1);
+ if (!r->cmd) {
+ free(r);
+ return;
+ }
+ strcpy(r->cmd, cmd_text);
+ r->type = type;
+ size_t dl = strlen(data);
+ if (dl > 0) {
+ r->data = (uint8_t *)malloc(dl);
+ if (!r->data) {
+ free(r->cmd);
+ free(r);
+ return;
+ }
+ memcpy(r->data, data, dl);
+ }
+ r->data_len = dl;
+ ms_mutex_lock(&s->resp_lock);
+ r->next = s->responses;
+ s->responses = r;
+ ms_mutex_unlock(&s->resp_lock);
+}
+
+void csm_mock_server_set_response(csm_mock_server_t *s, const char *cmd, const char *resp) {
+ ms_set_response_typed(s, cmd, CSM_PT_RESP, resp);
+}
+void csm_mock_server_set_error_response(csm_mock_server_t *s, const char *cmd, const char *err) {
+ ms_set_response_typed(s, cmd, CSM_PT_ERROR, err);
+}
+
+void csm_mock_server_push_status(csm_mock_server_t *s, const char *payload) {
+ if (!s || !payload) return;
+ size_t len = 0;
+ uint8_t *wire = ms_encode(CSM_PT_STATUS, payload, strlen(payload), &len);
+ if (!wire) return;
+ ms_mutex_lock(&s->client_lock);
+ for (int i = 0; i < MS_MAX_CLIENTS; ++i) {
+ if (s->clients[i] != MS_INVALID_SOCKET) {
+ ms_send_all(s->clients[i], wire, len);
+ }
+ }
+ ms_mutex_unlock(&s->client_lock);
+ free(wire);
+}
+
+char *csm_mock_server_get_received(csm_mock_server_t *s, unsigned int timeout_ms) {
+ if (!s) return NULL;
+ ms_mutex_lock(&s->recv_lock);
+ if (!s->msg_head) {
+ ms_cond_wait_ms(&s->recv_cond, &s->recv_lock, timeout_ms);
+ }
+ char *out = NULL;
+ if (s->msg_head) {
+ ms_msg_t *m = s->msg_head;
+ s->msg_head = m->next;
+ if (!s->msg_head) s->msg_tail = NULL;
+ out = m->text;
+ free(m);
+ }
+ ms_mutex_unlock(&s->recv_lock);
+ return out;
+}
diff --git a/SDK/c/tests/mock_server.h b/SDK/c/tests/mock_server.h
new file mode 100644
index 0000000..8ac56d0
--- /dev/null
+++ b/SDK/c/tests/mock_server.h
@@ -0,0 +1,48 @@
+/* mock_server.h - 用于测试的进程内 TCP 服务器,模拟 CSM-TCP-Router。
+ *
+ * 对应 Python `tests/conftest.py` 中的 MockServer 夹具。
+ */
+#ifndef CSM_MOCK_SERVER_H
+#define CSM_MOCK_SERVER_H
+
+#include "csm_tcp_router_client.h"
+
+#include
+
+typedef struct csm_mock_server csm_mock_server_t;
+
+/** 创建一个已停止的模拟服务器,绑定到 127.0.0.1;
+ * 实际端口由操作系统在 csm_mock_server_start() 中分配。 */
+csm_mock_server_t *csm_mock_server_create(void);
+
+/** 释放(运行中或已停止的)模拟服务器。 */
+void csm_mock_server_destroy(csm_mock_server_t *s);
+
+/** 绑定到 127.0.0.1 的临时端口,并启动接受连接线程。 */
+int csm_mock_server_start(csm_mock_server_t *s);
+
+/** 停止接受连接线程并关闭所有客户端连接。 */
+void csm_mock_server_stop(csm_mock_server_t *s);
+
+/** 返回服务器正在监听的端口(start() 之后有效)。 */
+uint16_t csm_mock_server_port(const csm_mock_server_t *s);
+
+/** 为精确匹配的命令字符串注册自定义 RESP 回复。 */
+void csm_mock_server_set_response(csm_mock_server_t *s,
+ const char *cmd_text,
+ const char *resp_text);
+
+/** 为精确匹配的命令字符串注册 ERROR 回复。 */
+void csm_mock_server_set_error_response(csm_mock_server_t *s,
+ const char *cmd_text,
+ const char *error_text);
+
+/** 向所有当前连接的客户端推送 STATUS 数据包。 */
+void csm_mock_server_push_status(csm_mock_server_t *s, const char *payload);
+
+/** 弹出下一条已接收的命令,最多阻塞 *timeout_ms* 毫秒。返回的
+ * 字符串由调用者拥有,必须使用 csm_string_free 释放。
+ * 超时时返回 NULL。 */
+char *csm_mock_server_get_received(csm_mock_server_t *s, unsigned int timeout_ms);
+
+#endif /* CSM_MOCK_SERVER_H */
diff --git a/SDK/c/tests/test_client.c b/SDK/c/tests/test_client.c
new file mode 100644
index 0000000..2d949cd
--- /dev/null
+++ b/SDK/c/tests/test_client.c
@@ -0,0 +1,51 @@
+/* test_client.c - 客户端对象生命周期的单元测试,
+ * 无需运行中的模拟服务器。 */
+#include "csm_tcp_router_client.h"
+#include "test_harness.h"
+
+#include
+
+CSM_TEST(test_client_create_destroy) {
+ csm_client_t *c = csm_client_create();
+ CSM_ASSERT(c != NULL);
+ CSM_ASSERT_EQ_INT(csm_client_is_connected(c), 0);
+ csm_client_destroy(c);
+}
+
+CSM_TEST(test_destroy_null_safe) {
+ csm_client_destroy(NULL);
+}
+
+CSM_TEST(test_send_when_not_connected_returns_connection_error) {
+ csm_client_t *c = csm_client_create();
+ CSM_ASSERT(c != NULL);
+ csm_command_response_t resp = {0};
+ csm_result_t r = csm_client_send_and_wait(c, "Ping", 100, &resp);
+ CSM_ASSERT_EQ_INT(r, CSM_ERR_CONNECTION);
+ csm_command_response_dispose(&resp);
+ csm_client_destroy(c);
+}
+
+CSM_TEST(test_invalid_args_rejected) {
+ csm_client_t *c = csm_client_create();
+ CSM_ASSERT_EQ_INT(csm_client_connect(c, NULL, 1234, 100), CSM_ERR_INVALID);
+ CSM_ASSERT_EQ_INT(csm_client_send_and_wait(NULL, "x", 100, NULL), CSM_ERR_INVALID);
+ CSM_ASSERT_EQ_INT(csm_client_send_and_wait(c, NULL, 100, NULL), CSM_ERR_INVALID);
+ char *out = NULL;
+ CSM_ASSERT_EQ_INT(csm_client_list_api(c, NULL, &out, 100), CSM_ERR_INVALID);
+ csm_client_destroy(c);
+}
+
+CSM_TEST(test_wait_for_server_unreachable_times_out) {
+ /* 选择一个不太可能被使用的任意高端口。 */
+ csm_result_t r = csm_client_wait_for_server("127.0.0.1", 1, 200, 50);
+ CSM_ASSERT_EQ_INT(r, CSM_ERR_TIMEOUT);
+}
+
+CSM_TEST(test_connect_unreachable_returns_connection_error) {
+ csm_client_t *c = csm_client_create();
+ csm_result_t r = csm_client_connect(c, "127.0.0.1", 1, 300);
+ /* 根据操作系统,返回 CSM_ERR_CONNECTION(拒绝连接)或 CSM_ERR_TIMEOUT。 */
+ CSM_ASSERT(r == CSM_ERR_CONNECTION || r == CSM_ERR_TIMEOUT);
+ csm_client_destroy(c);
+}
diff --git a/SDK/c/tests/test_harness.h b/SDK/c/tests/test_harness.h
new file mode 100644
index 0000000..568347c
--- /dev/null
+++ b/SDK/c/tests/test_harness.h
@@ -0,0 +1,60 @@
+/* test_harness.h - csm-tcp-router-client C SDK 测试使用的极简进程内单元测试框架。
+ *
+ * 测试通过 CSM_TEST() 宏注册自身;test_main.c 中的运行器
+ * 通过链接时的 TESTS 数组收集测试,依次执行并打印汇总结果。
+ * 失败仅中止当前测试;断言使用 longjmp 回退到运行器。
+ */
+#ifndef CSM_TEST_HARNESS_H
+#define CSM_TEST_HARNESS_H
+
+#include
+#include
+#include
+
+typedef void (*csm_test_fn)(void);
+
+typedef struct {
+ const char *name;
+ csm_test_fn fn;
+} csm_test_t;
+
+/* 由 test_main.c 提供。 */
+extern jmp_buf csm_test_jmp;
+extern int csm_test_failed;
+extern int csm_test_assertions;
+
+#define CSM_TEST_FAIL(...) do { \
+ csm_test_failed = 1; \
+ fprintf(stderr, " FAIL %s:%d: ", __FILE__, __LINE__); \
+ fprintf(stderr, __VA_ARGS__); \
+ fprintf(stderr, "\n"); \
+ longjmp(csm_test_jmp, 1); \
+} while (0)
+
+#define CSM_ASSERT(cond) do { \
+ csm_test_assertions++; \
+ if (!(cond)) CSM_TEST_FAIL("assertion failed: %s", #cond); \
+} while (0)
+
+#define CSM_ASSERT_EQ_INT(a, b) do { \
+ csm_test_assertions++; \
+ long long _a = (long long)(a), _b = (long long)(b); \
+ if (_a != _b) \
+ CSM_TEST_FAIL("expected %lld, got %lld (%s == %s)", \
+ _b, _a, #a, #b); \
+} while (0)
+
+#define CSM_ASSERT_EQ_STR(a, b) do { \
+ csm_test_assertions++; \
+ const char *_a = (a), *_b = (b); \
+ if (_a == NULL || _b == NULL || strcmp(_a, _b) != 0) \
+ CSM_TEST_FAIL("expected \"%s\", got \"%s\"", \
+ _b ? _b : "(null)", _a ? _a : "(null)"); \
+} while (0)
+
+/* 定义一个测试函数。运行器通过 CSM_TEST_EXTERN 宏将每个测试声明为 extern,
+ * 并在测试表中注册。 */
+#define CSM_TEST(name) void name(void)
+#define CSM_TEST_EXTERN(name) extern void name(void)
+
+#endif /* CSM_TEST_HARNESS_H */
diff --git a/SDK/c/tests/test_integration.c b/SDK/c/tests/test_integration.c
new file mode 100644
index 0000000..75a3aa2
--- /dev/null
+++ b/SDK/c/tests/test_integration.c
@@ -0,0 +1,165 @@
+/* test_integration.c - 针对进程内 MockServer 的端到端测试。 */
+
+#if !defined(_WIN32)
+# ifndef _POSIX_C_SOURCE
+# define _POSIX_C_SOURCE 200809L
+# endif
+#endif
+
+#include "csm_tcp_router_client.h"
+#include "mock_server.h"
+#include "test_harness.h"
+
+#include
+#include
+#include
+
+#if defined(_WIN32)
+# include
+static void it_sleep_ms(unsigned int ms) { Sleep(ms); }
+#else
+# include
+static void it_sleep_ms(unsigned int ms) {
+ struct timespec ts; ts.tv_sec = ms/1000; ts.tv_nsec = (long)(ms%1000)*1000000L;
+ nanosleep(&ts, NULL);
+}
+#endif
+
+/* 辅助函数:启动服务器 + 连接客户端。 */
+static void it_setup(csm_mock_server_t **out_s, csm_client_t **out_c) {
+ csm_mock_server_t *s = csm_mock_server_create();
+ CSM_ASSERT(s != NULL);
+ CSM_ASSERT_EQ_INT(csm_mock_server_start(s), 0);
+ csm_client_t *c = csm_client_create();
+ CSM_ASSERT(c != NULL);
+ csm_result_t r = csm_client_connect(c, "127.0.0.1",
+ csm_mock_server_port(s), 2000);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ *out_s = s; *out_c = c;
+}
+
+static void it_teardown(csm_mock_server_t *s, csm_client_t *c) {
+ csm_client_destroy(c);
+ csm_mock_server_destroy(s);
+}
+
+CSM_TEST(it_ping) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ double ms = 0;
+ csm_result_t r = csm_client_ping(c, 1000, &ms);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT(ms >= 0);
+ it_teardown(s, c);
+}
+
+CSM_TEST(it_list_modules) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ char *txt = NULL;
+ csm_result_t r = csm_client_list_modules(c, &txt, 1000);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT_EQ_STR(txt, "AI\nDIO\nSystem");
+ csm_string_free(txt);
+ it_teardown(s, c);
+}
+
+CSM_TEST(it_list_api_includes_module_name) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ char *txt = NULL;
+ csm_result_t r = csm_client_list_api(c, "DAQmx", &txt, 1000);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT(strstr(txt, "DAQmx") != NULL);
+ csm_string_free(txt);
+ it_teardown(s, c);
+}
+
+CSM_TEST(it_send_and_wait_custom_response) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ csm_mock_server_set_response(s, "API: Read -@ DAQmx", "42");
+ csm_command_response_t resp = {0};
+ csm_result_t r = csm_client_send_and_wait(c, "API: Read -@ DAQmx", 1000, &resp);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT_EQ_INT(resp.raw_len, 2);
+ CSM_ASSERT(memcmp(resp.raw, "42", 2) == 0);
+ csm_command_response_dispose(&resp);
+ it_teardown(s, c);
+}
+
+CSM_TEST(it_server_error_propagated) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ csm_mock_server_set_error_response(s, "Bad", "[Error: 42] bad command");
+ csm_command_response_t resp = {0};
+ csm_result_t r = csm_client_send_and_wait(c, "Bad", 1000, &resp);
+ CSM_ASSERT_EQ_INT(r, CSM_ERR_SERVER);
+ csm_server_error_t err = {{0}, {0}};
+ CSM_ASSERT_EQ_INT(csm_client_last_server_error(c, &err), CSM_OK);
+ CSM_ASSERT_EQ_STR(err.code, "42");
+ CSM_ASSERT(strstr(err.message, "bad command") != NULL);
+ csm_command_response_dispose(&resp);
+ it_teardown(s, c);
+}
+
+CSM_TEST(it_post_async_handshake) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ csm_result_t r = csm_client_post(c, "API: Start -> DAQmx", 1000);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ char *got = csm_mock_server_get_received(s, 500);
+ CSM_ASSERT(got != NULL);
+ CSM_ASSERT_EQ_STR(got, "API: Start -> DAQmx");
+ csm_string_free(got);
+ it_teardown(s, c);
+}
+
+static void it_status_cb(const csm_status_notification_t *n, void *ud) {
+ int *count = (int *)ud;
+ (*count)++;
+ /* 完整性检查已解析的字段。 */
+ (void)n;
+}
+
+CSM_TEST(it_subscribe_status_invokes_callback) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ int count = 0;
+ csm_result_t r = csm_client_subscribe_status(c, "Status", "DAQmx",
+ it_status_cb, &count, 1000);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ /* 从已接收队列中消耗握手数据。 */
+ char *cmd = csm_mock_server_get_received(s, 500);
+ csm_string_free(cmd);
+ /* 推送一条匹配订阅的 STATUS 通知。 */
+ csm_mock_server_push_status(s, "Status >> 1.23 <- DAQmx");
+ /* 等待回调(最多轮询 1 秒)。 */
+ for (int i = 0; i < 100 && count == 0; ++i) it_sleep_ms(10);
+ CSM_ASSERT(count >= 1);
+
+ /* 同一通知也应可通过轮询获取。 */
+ csm_status_notification_t n = {0};
+ r = csm_client_poll_status(c, &n, 500);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT_EQ_STR(n.status_name, "Status");
+ CSM_ASSERT_EQ_STR(n.module_name, "DAQmx");
+ CSM_ASSERT_EQ_STR(n.data, "1.23");
+ csm_status_notification_dispose(&n);
+
+ csm_client_unsubscribe_status(c, "Status", "DAQmx", 1000);
+ it_teardown(s, c);
+}
+
+CSM_TEST(it_disconnect_unblocks_waiters) {
+ csm_mock_server_t *s = NULL; csm_client_t *c = NULL;
+ it_setup(&s, &c);
+ /* 发送一个没有预置响应的命令;模拟服务器返回 CMD_RESP。 */
+ /* 本测试中直接立即断开连接并验证后续发送失败。 */
+ csm_client_disconnect(c);
+ csm_command_response_t resp = {0};
+ csm_result_t r = csm_client_send_and_wait(c, "Ping", 200, &resp);
+ CSM_ASSERT_EQ_INT(r, CSM_ERR_CONNECTION);
+ csm_command_response_dispose(&resp);
+ it_teardown(s, c);
+}
diff --git a/SDK/c/tests/test_main.c b/SDK/c/tests/test_main.c
new file mode 100644
index 0000000..5fcdad9
--- /dev/null
+++ b/SDK/c/tests/test_main.c
@@ -0,0 +1,93 @@
+/* test_main.c - C SDK 测试套件的运行器。 */
+#include "test_harness.h"
+
+#include
+#include
+#include
+
+jmp_buf csm_test_jmp;
+int csm_test_failed = 0;
+int csm_test_assertions = 0;
+
+/* --- test_protocol.c --- */
+CSM_TEST_EXTERN(test_header_size_constant);
+CSM_TEST_EXTERN(test_encode_decode_roundtrip);
+CSM_TEST_EXTERN(test_encode_empty_payload);
+CSM_TEST_EXTERN(test_encode_buffer_too_small);
+CSM_TEST_EXTERN(test_decode_header_bad_size);
+CSM_TEST_EXTERN(test_parse_packet_unknown_type_maps_to_info);
+CSM_TEST_EXTERN(test_parse_packet_length_mismatch);
+CSM_TEST_EXTERN(test_result_str_known);
+
+/* --- test_client.c --- */
+CSM_TEST_EXTERN(test_client_create_destroy);
+CSM_TEST_EXTERN(test_destroy_null_safe);
+CSM_TEST_EXTERN(test_send_when_not_connected_returns_connection_error);
+CSM_TEST_EXTERN(test_invalid_args_rejected);
+CSM_TEST_EXTERN(test_wait_for_server_unreachable_times_out);
+CSM_TEST_EXTERN(test_connect_unreachable_returns_connection_error);
+
+/* --- test_integration.c --- */
+CSM_TEST_EXTERN(it_ping);
+CSM_TEST_EXTERN(it_list_modules);
+CSM_TEST_EXTERN(it_list_api_includes_module_name);
+CSM_TEST_EXTERN(it_send_and_wait_custom_response);
+CSM_TEST_EXTERN(it_server_error_propagated);
+CSM_TEST_EXTERN(it_post_async_handshake);
+CSM_TEST_EXTERN(it_subscribe_status_invokes_callback);
+CSM_TEST_EXTERN(it_disconnect_unblocks_waiters);
+
+static const csm_test_t TESTS[] = {
+ {"test_header_size_constant", test_header_size_constant},
+ {"test_encode_decode_roundtrip", test_encode_decode_roundtrip},
+ {"test_encode_empty_payload", test_encode_empty_payload},
+ {"test_encode_buffer_too_small", test_encode_buffer_too_small},
+ {"test_decode_header_bad_size", test_decode_header_bad_size},
+ {"test_parse_packet_unknown_type_maps_to_info", test_parse_packet_unknown_type_maps_to_info},
+ {"test_parse_packet_length_mismatch", test_parse_packet_length_mismatch},
+ {"test_result_str_known", test_result_str_known},
+ {"test_client_create_destroy", test_client_create_destroy},
+ {"test_destroy_null_safe", test_destroy_null_safe},
+ {"test_send_when_not_connected_returns_connection_error", test_send_when_not_connected_returns_connection_error},
+ {"test_invalid_args_rejected", test_invalid_args_rejected},
+ {"test_wait_for_server_unreachable_times_out", test_wait_for_server_unreachable_times_out},
+ {"test_connect_unreachable_returns_connection_error", test_connect_unreachable_returns_connection_error},
+ {"it_ping", it_ping},
+ {"it_list_modules", it_list_modules},
+ {"it_list_api_includes_module_name", it_list_api_includes_module_name},
+ {"it_send_and_wait_custom_response", it_send_and_wait_custom_response},
+ {"it_server_error_propagated", it_server_error_propagated},
+ {"it_post_async_handshake", it_post_async_handshake},
+ {"it_subscribe_status_invokes_callback", it_subscribe_status_invokes_callback},
+ {"it_disconnect_unblocks_waiters", it_disconnect_unblocks_waiters},
+};
+
+int main(int argc, char **argv) {
+ const char *only = (argc > 1) ? argv[1] : NULL;
+ /* `volatile` 确保这些变量在各个测试体内的失败断言执行 longjmp 后仍然存活。 */
+ volatile int passed = 0, failed = 0, skipped = 0;
+ volatile int total_assertions = 0;
+ size_t n = sizeof(TESTS) / sizeof(TESTS[0]);
+ volatile size_t i = 0;
+ for (; i < n; ++i) {
+ if (only && strcmp(only, TESTS[i].name) != 0) { skipped++; continue; }
+ printf("[RUN ] %s\n", TESTS[i].name);
+ csm_test_failed = 0;
+ int before = csm_test_assertions;
+ if (setjmp(csm_test_jmp) == 0) {
+ TESTS[i].fn();
+ }
+ int delta = csm_test_assertions - before;
+ total_assertions += delta;
+ if (csm_test_failed) {
+ printf("[FAIL] %s (%d asserts)\n", TESTS[i].name, delta);
+ failed++;
+ } else {
+ printf("[ OK ] %s (%d asserts)\n", TESTS[i].name, delta);
+ passed++;
+ }
+ }
+ printf("\nResults: %d passed, %d failed, %d skipped (%d assertions)\n",
+ passed, failed, skipped, total_assertions);
+ return failed == 0 ? 0 : 1;
+}
diff --git a/SDK/c/tests/test_protocol.c b/SDK/c/tests/test_protocol.c
new file mode 100644
index 0000000..a8c9e8a
--- /dev/null
+++ b/SDK/c/tests/test_protocol.c
@@ -0,0 +1,84 @@
+/* test_protocol.c - 协议编解码的单元测试。 */
+#include "csm_tcp_router_client.h"
+#include "test_harness.h"
+
+#include
+
+CSM_TEST(test_header_size_constant) {
+ CSM_ASSERT_EQ_INT(CSM_HEADER_SIZE, 8);
+ CSM_ASSERT_EQ_INT(CSM_PROTOCOL_VERSION, 0x01);
+}
+
+CSM_TEST(test_encode_decode_roundtrip) {
+ const char *payload = "Hello";
+ uint8_t buf[64];
+ size_t out_len = 0;
+ csm_result_t r = csm_encode_packet(payload, 5, CSM_PT_CMD, 0, 0,
+ buf, sizeof(buf), &out_len);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT_EQ_INT(out_len, 8 + 5);
+
+ /* 头部字节:大端序长度、版本、类型、flag1、flag2。 */
+ CSM_ASSERT_EQ_INT(buf[0], 0);
+ CSM_ASSERT_EQ_INT(buf[1], 0);
+ CSM_ASSERT_EQ_INT(buf[2], 0);
+ CSM_ASSERT_EQ_INT(buf[3], 5);
+ CSM_ASSERT_EQ_INT(buf[4], CSM_PROTOCOL_VERSION);
+ CSM_ASSERT_EQ_INT(buf[5], CSM_PT_CMD);
+ CSM_ASSERT(memcmp(buf + 8, "Hello", 5) == 0);
+
+ uint32_t len; uint8_t ver, type, f1, f2;
+ r = csm_decode_header(buf, 8, &len, &ver, &type, &f1, &f2);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT_EQ_INT(len, 5);
+ CSM_ASSERT_EQ_INT(ver, 1);
+ CSM_ASSERT_EQ_INT(type, CSM_PT_CMD);
+}
+
+CSM_TEST(test_encode_empty_payload) {
+ uint8_t buf[16];
+ size_t out_len = 0;
+ csm_result_t r = csm_encode_packet(NULL, 0, CSM_PT_INFO, 0, 0,
+ buf, sizeof(buf), &out_len);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT_EQ_INT(out_len, 8);
+}
+
+CSM_TEST(test_encode_buffer_too_small) {
+ uint8_t buf[4];
+ size_t out_len = 0;
+ csm_result_t r = csm_encode_packet("X", 1, CSM_PT_CMD, 0, 0,
+ buf, sizeof(buf), &out_len);
+ CSM_ASSERT_EQ_INT(r, CSM_ERR_INVALID);
+}
+
+CSM_TEST(test_decode_header_bad_size) {
+ uint8_t hdr[4] = {0};
+ csm_result_t r = csm_decode_header(hdr, 4, NULL, NULL, NULL, NULL, NULL);
+ CSM_ASSERT_EQ_INT(r, CSM_ERR_PROTOCOL);
+}
+
+CSM_TEST(test_parse_packet_unknown_type_maps_to_info) {
+ uint8_t hdr[8] = {0,0,0,3, 0x01, 0xFE /* unknown */, 0, 0};
+ uint8_t body[3] = {'a','b','c'};
+ csm_packet_t pkt = {0};
+ csm_result_t r = csm_parse_packet(hdr, 8, body, 3, &pkt);
+ CSM_ASSERT_EQ_INT(r, CSM_OK);
+ CSM_ASSERT_EQ_INT(pkt.type, CSM_PT_INFO);
+ CSM_ASSERT_EQ_INT(pkt.data_len, 3);
+ CSM_ASSERT(memcmp(pkt.data, "abc", 3) == 0);
+ csm_packet_dispose(&pkt);
+}
+
+CSM_TEST(test_parse_packet_length_mismatch) {
+ uint8_t hdr[8] = {0,0,0,5, 0x01, CSM_PT_RESP, 0, 0};
+ uint8_t body[3] = {'a','b','c'};
+ csm_packet_t pkt = {0};
+ csm_result_t r = csm_parse_packet(hdr, 8, body, 3, &pkt);
+ CSM_ASSERT_EQ_INT(r, CSM_ERR_PROTOCOL);
+}
+
+CSM_TEST(test_result_str_known) {
+ CSM_ASSERT(strcmp(csm_result_str(CSM_OK), "OK") == 0);
+ CSM_ASSERT(strstr(csm_result_str(CSM_ERR_TIMEOUT), "imeout") != NULL);
+}
diff --git a/SDK/c/vs2026/README.md b/SDK/c/vs2026/README.md
new file mode 100644
index 0000000..77376d2
--- /dev/null
+++ b/SDK/c/vs2026/README.md
@@ -0,0 +1,61 @@
+# Visual Studio 2026 build files
+
+This directory contains a Visual Studio 2026 (VS 18) solution and projects
+for building the C SDK and its test suite directly from the IDE on
+Windows. The CMake build at `SDK/c/CMakeLists.txt` remains the
+authoritative cross-platform build; these project files are provided as a
+convenience for Windows developers who prefer the VS IDE.
+
+## Layout
+
+```
+vs2026/
+├── csm_tcp_router_client.sln – solution file
+├── csm_tcp_router_client/
+│ └── csm_tcp_router_client.vcxproj – static library project
+└── csm_tcp_router_client.tests/
+ └── csm_tcp_router_client.tests.vcxproj – test executable
+```
+
+## Toolset
+
+| Setting | Value |
+|-------------------|-----------------------------------------|
+| Solution format | Visual Studio 18 (2026) |
+| Platform toolset | `v144` (Visual Studio 2026 C/C++) |
+| C language | `/std:c11` (`stdc11` MSBuild metadata) |
+| Configurations | Debug, Release |
+| Platforms | Win32 (x86), x64 |
+| Runtime | `/MT[d]` (statically linked CRT) |
+| Subsystem (tests) | Console |
+| Linked libraries | `Ws2_32.lib` |
+
+## Building
+
+Open `csm_tcp_router_client.sln` in Visual Studio 2026 and build the
+solution (Ctrl+Shift+B), or from a developer command prompt:
+
+```cmd
+msbuild csm_tcp_router_client.sln ^
+ /p:Configuration=Release /p:Platform=x64
+```
+
+## Running tests
+
+After building, run the test executable directly:
+
+```cmd
+build\x64\Release\csm_tcp_router_client.tests\csm_tcp_router_client.tests.exe
+```
+
+Exit code `0` indicates all tests passed; any other exit code indicates
+one or more failures (failed test names are printed to stderr).
+
+## Why VS2026 and not an older version?
+
+The user explicitly requested VS2026 (the latest Visual Studio at the
+time the SDK was added). The project files use the new `v144` platform
+toolset shipped with VS 2026; older VS versions will refuse to load
+them. If you need to build with an older VS, prefer the `CMakeLists.txt`
+in the SDK root – it works with every supported MSVC version (and on
+Linux / macOS).
diff --git a/SDK/c/vs2026/csm_tcp_router_client.sln b/SDK/c/vs2026/csm_tcp_router_client.sln
new file mode 100644
index 0000000..08a0b08
--- /dev/null
+++ b/SDK/c/vs2026/csm_tcp_router_client.sln
@@ -0,0 +1,43 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.0.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "csm_tcp_router_client", "csm_tcp_router_client\csm_tcp_router_client.vcxproj", "{DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "csm_tcp_router_client.tests", "csm_tcp_router_client.tests\csm_tcp_router_client.tests.vcxproj", "{92C7CD36-6916-46D6-803F-137C11E423CF}"
+ ProjectSection(ProjectDependencies) = postProject
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD} = {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Release|x64 = Release|x64
+ Debug|Win32 = Debug|Win32
+ Release|Win32 = Release|Win32
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Debug|x64.ActiveCfg = Debug|x64
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Debug|x64.Build.0 = Debug|x64
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Release|x64.ActiveCfg = Release|x64
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Release|x64.Build.0 = Release|x64
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Debug|Win32.ActiveCfg = Debug|Win32
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Debug|Win32.Build.0 = Debug|Win32
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Release|Win32.ActiveCfg = Release|Win32
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}.Release|Win32.Build.0 = Release|Win32
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Debug|x64.ActiveCfg = Debug|x64
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Debug|x64.Build.0 = Debug|x64
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Release|x64.ActiveCfg = Release|x64
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Release|x64.Build.0 = Release|x64
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Debug|Win32.ActiveCfg = Debug|Win32
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Debug|Win32.Build.0 = Debug|Win32
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Release|Win32.ActiveCfg = Release|Win32
+ {92C7CD36-6916-46D6-803F-137C11E423CF}.Release|Win32.Build.0 = Release|Win32
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1405610E-2603-4EE0-A392-54886017A89E}
+ EndGlobalSection
+EndGlobal
diff --git a/SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj b/SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj
new file mode 100644
index 0000000..9ea1c0b
--- /dev/null
+++ b/SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj
@@ -0,0 +1,94 @@
+
+
+
+
+
+ Debug
+ Win32
+
+
+ Release
+ Win32
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+
+ 18.0
+ {92C7CD36-6916-46D6-803F-137C11E423CF}
+ csm_tcp_router_client_tests
+ Win32Proj
+ 10.0
+
+
+
+
+
+ Application
+ v144
+ Unicode
+ true
+ false
+
+
+
+
+
+ $(SolutionDir)build\$(Platform)\$(Configuration)\
+ $(SolutionDir)build\$(Platform)\$(Configuration)\$(ProjectName)\
+
+
+
+
+ $(ProjectDir)..\..\include;$(ProjectDir)..\..\tests;%(AdditionalIncludeDirectories)
+ WIN32;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)
+ _DEBUG;%(PreprocessorDefinitions)
+ NDEBUG;%(PreprocessorDefinitions)
+ Level4
+ true
+ true
+ CompileAsC
+ stdc11
+ MultiThreadedDebug
+ MultiThreaded
+ Disabled
+ MaxSpeed
+
+
+ Console
+ Ws2_32.lib;%(AdditionalDependencies)
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}
+
+
+
+
+
diff --git a/SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj.filters b/SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj.filters
new file mode 100644
index 0000000..8c83bd3
--- /dev/null
+++ b/SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj.filters
@@ -0,0 +1,22 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+
+
+ {93995380-89BD-4B04-88EB-625FBE52EBFB}
+
+
+
+ Source Files
+ Source Files
+ Source Files
+ Source Files
+ Source Files
+
+
+ Header Files
+ Header Files
+
+
diff --git a/SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj b/SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj
new file mode 100644
index 0000000..2684b41
--- /dev/null
+++ b/SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj
@@ -0,0 +1,87 @@
+
+
+
+
+
+ Debug
+ Win32
+
+
+ Release
+ Win32
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+
+ 18.0
+ {DAE3271A-FD3C-4E9C-A4CC-54DF2EB32AFD}
+ csm_tcp_router_client
+ Win32Proj
+ 10.0
+
+
+
+
+
+ StaticLibrary
+ v144
+ Unicode
+ true
+ false
+ true
+
+
+
+
+
+ $(SolutionDir)build\$(Platform)\$(Configuration)\
+ $(SolutionDir)build\$(Platform)\$(Configuration)\$(ProjectName)\
+
+
+
+
+ $(ProjectDir)..\..\include;%(AdditionalIncludeDirectories)
+ WIN32;_WINDOWS;_CRT_SECURE_NO_WARNINGS;CSM_BUILD_LIBRARY;%(PreprocessorDefinitions)
+ _DEBUG;%(PreprocessorDefinitions)
+ NDEBUG;%(PreprocessorDefinitions)
+ Level4
+ true
+ true
+ CompileAsC
+ stdc11
+ MultiThreadedDebug
+ MultiThreaded
+ Disabled
+ MaxSpeed
+ true
+ true
+
+
+ Ws2_32.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj.filters b/SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj.filters
new file mode 100644
index 0000000..810a992
--- /dev/null
+++ b/SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj.filters
@@ -0,0 +1,23 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ c;cpp
+
+
+ {93995380-89BD-4B04-88EB-625FBE52EBFB}
+ h
+
+
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+
diff --git a/SDK/csharp/CHANGELOG.md b/SDK/csharp/CHANGELOG.md
new file mode 100644
index 0000000..14506f9
--- /dev/null
+++ b/SDK/csharp/CHANGELOG.md
@@ -0,0 +1,40 @@
+# Changelog
+
+All notable changes to `CsmTcpRouter.Client` are documented here.
+
+The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+---
+
+## [Unreleased]
+
+---
+
+## [0.1.0] – 2026-04-27
+
+### Added
+
+- Initial release of the C# / .NET client SDK for the CSM-TCP-Router LabVIEW
+ server.
+- Single-file implementation in `src/CsmTcpRouter/CsmTcpRouter.cs`.
+- Multi-targets `netstandard2.0` (for .NET Framework 4.6.2+, .NET Core 3.1+,
+ .NET 5/6/7/8/9) and `net8.0`. Zero third-party runtime dependencies.
+- Public types:
+ - `TcpRouterClient` – synchronous and asynchronous APIs:
+ `Connect/ConnectAsync`, `Disconnect`, `WaitForServer/WaitForServerAsync`,
+ `SendAndWait/SendAndWaitAsync`, `Post/PostAsync`,
+ `PostNoReply/PostNoReplyAsync`, `Ping/PingAsync`, `ListModules`,
+ `ListApi`, `ListStates`, `Help`, `SubscribeStatus/UnsubscribeStatus`,
+ `RegisterAsyncCallback/UnregisterAsyncCallback`.
+ - `PacketType` enum and data models `Packet`, `CommandResponse`,
+ `AsyncResponse`, `StatusNotification`.
+ - Exception hierarchy: `CsmTcpRouterException`, `RouterConnectionException`,
+ `RouterTimeoutException`, `ProtocolException`, `ServerException`.
+- xUnit test suite (51 tests) covering protocol codec, error parsing, model
+ parsing, and end-to-end client behaviour against an in-process MockServer.
+- Runnable console example `examples/BasicUsage/`.
+- VS 2022 / 2026 compatible solution `CsmTcpRouter.sln`.
+- GitHub Actions workflow `.github/workflows/CSharp_SDK.yml` for build, test,
+ pack, and conditional NuGet publish on `csharp-sdk-v*` tag pushes.
+- English (`README.md`) and Chinese (`README.zh-cn.md`) documentation.
diff --git a/SDK/csharp/CsmTcpRouter.sln b/SDK/csharp/CsmTcpRouter.sln
new file mode 100644
index 0000000..0f53351
--- /dev/null
+++ b/SDK/csharp/CsmTcpRouter.sln
@@ -0,0 +1,88 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsmTcpRouter", "src\CsmTcpRouter\CsmTcpRouter.csproj", "{AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsmTcpRouter.Tests", "tests\CsmTcpRouter.Tests\CsmTcpRouter.Tests.csproj", "{1EFBB3D5-6452-41A9-8501-897D421B33DB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicUsage", "examples\BasicUsage\BasicUsage.csproj", "{4DD90760-BF7D-45EB-BAE9-E47F65B053C9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClientConsole", "examples\ClientConsole\ClientConsole.csproj", "{7160908E-E970-4A92-95B4-462403BD0EE8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Debug|x64.Build.0 = Debug|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Debug|x86.Build.0 = Debug|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Release|x64.ActiveCfg = Release|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Release|x64.Build.0 = Release|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Release|x86.ActiveCfg = Release|Any CPU
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C}.Release|x86.Build.0 = Release|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Debug|x64.Build.0 = Debug|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Debug|x86.Build.0 = Debug|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Release|x64.ActiveCfg = Release|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Release|x64.Build.0 = Release|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Release|x86.ActiveCfg = Release|Any CPU
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB}.Release|x86.Build.0 = Release|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Debug|x64.Build.0 = Debug|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Debug|x86.Build.0 = Debug|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Release|x64.ActiveCfg = Release|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Release|x64.Build.0 = Release|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Release|x86.ActiveCfg = Release|Any CPU
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9}.Release|x86.Build.0 = Release|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Debug|x64.Build.0 = Debug|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Debug|x86.Build.0 = Debug|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Release|x64.ActiveCfg = Release|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Release|x64.Build.0 = Release|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Release|x86.ActiveCfg = Release|Any CPU
+ {7160908E-E970-4A92-95B4-462403BD0EE8}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {AA5DEDA8-96DD-4A05-8029-57ED1E7DCE1C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {1EFBB3D5-6452-41A9-8501-897D421B33DB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {4DD90760-BF7D-45EB-BAE9-E47F65B053C9} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
+ {7160908E-E970-4A92-95B4-462403BD0EE8} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
+ EndGlobalSection
+EndGlobal
diff --git a/SDK/csharp/LICENSE b/SDK/csharp/LICENSE
new file mode 100644
index 0000000..78e1cd2
--- /dev/null
+++ b/SDK/csharp/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 NEVSTOP-LAB
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/SDK/csharp/README.md b/SDK/csharp/README.md
new file mode 100644
index 0000000..99ce624
--- /dev/null
+++ b/SDK/csharp/README.md
@@ -0,0 +1,189 @@
+# CsmTcpRouter.Client
+
+[](https://www.nuget.org/packages/CsmTcpRouter.Client/)
+[](LICENSE)
+[](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App/actions/workflows/CSharp_SDK.yml)
+
+C# / .NET client SDK for the [CSM-TCP-Router](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App) LabVIEW server.
+
+CSM-TCP-Router exposes a LabVIEW [Communicable State Machine (CSM)](https://github.com/NEVSTOP-LAB/Communicable-State-Machine) application over TCP so that any TCP client — including .NET applications, test harnesses, or CI pipelines — can send commands and receive responses without touching the LabVIEW code.
+
+> 📖 [中文文档 README.zh-cn.md](README.zh-cn.md)
+
+The entire SDK lives in a single source file: [`src/CsmTcpRouter/CsmTcpRouter.cs`](src/CsmTcpRouter/CsmTcpRouter.cs).
+
+---
+
+## Installation
+
+```bash
+dotnet add package CsmTcpRouter.Client
+```
+
+Targets `netstandard2.0` and `net8.0`. No third-party runtime dependencies — only the .NET BCL.
+
+Supported platforms include .NET Framework 4.6.2+, .NET Core 3.1+, and .NET 5/6/7/8/9.
+
+---
+
+## Quickstart
+
+### Synchronous client
+
+```csharp
+using CsmTcpRouter;
+
+using var client = new TcpRouterClient();
+client.Connect("localhost", 30007);
+
+// List all loaded CSM modules
+Console.WriteLine(client.ListModules());
+
+// Send a synchronous command and wait for the response
+var resp = client.SendAndWait("API: Read -@ DAQmx");
+Console.WriteLine(resp.Text);
+
+// Ping the server
+var (ok, elapsed) = client.Ping();
+Console.WriteLine($"Ping: {ok}, latency={elapsed.TotalMilliseconds:F1} ms");
+```
+
+### Asynchronous client
+
+```csharp
+using CsmTcpRouter;
+
+using var client = new TcpRouterClient();
+await client.ConnectAsync("localhost", 30007);
+
+Console.WriteLine(await client.ListModulesAsync());
+var resp = await client.SendAndWaitAsync("API: Read -@ DAQmx");
+Console.WriteLine(resp.Text);
+```
+
+### Subscribe to status broadcasts
+
+```csharp
+client.SubscribeStatus("Status", "AI", notif =>
+{
+ Console.WriteLine($"{notif.StatusName} = {notif.Data}");
+});
+
+// ... later
+client.UnsubscribeStatus("Status", "AI");
+```
+
+### Async-response callbacks
+
+```csharp
+client.RegisterAsyncCallback("API: Start Sampling -> DAQmx", ar =>
+{
+ Console.WriteLine($"Async-resp: {ar.Text}");
+});
+
+client.Post("API: Start Sampling -> DAQmx");
+```
+
+See [`examples/BasicUsage/Program.cs`](examples/BasicUsage/Program.cs) for a complete runnable example.
+
+---
+
+## API reference
+
+### `TcpRouterClient`
+
+| Method | Description |
+| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
+| `Connect / ConnectAsync(host, port, timeout?)` | Open a TCP connection. |
+| `Disconnect()` | Close the connection and unblock any pending waiters with `RouterConnectionException`. |
+| `Connected` | `true` while the underlying socket is open. |
+| `WaitForServer / WaitForServerAsync(...)` | Poll `host:port` until a connection succeeds or the timeout elapses. |
+| `SendAndWait / SendAndWaitAsync(cmd, timeout?)` | Send a synchronous command (`-@`) and block until the `RESP` packet arrives. Returns `CommandResponse`. |
+| `Post / PostAsync(cmd, timeout?)` | Send an async command (`->`) and wait for the `CMD_RESP` handshake. |
+| `PostNoReply / PostNoReplyAsync(cmd, timeout?)` | Send a no-reply async command (`->\|`) and wait for the `CMD_RESP` handshake. |
+| `Ping / PingAsync(timeout?)` | Round-trip latency check; returns `(bool ok, TimeSpan elapsed)`. |
+| `ListModules / ListApi / ListStates / Help(...)` | Built-in router-management helpers. |
+| `SubscribeStatus / UnsubscribeStatus(...)` | Register / cancel a `STATUS` (or `INTERRUPT`) broadcast subscription. |
+| `RegisterAsyncCallback / UnregisterAsyncCallback(...)` | Register / remove a callback dispatched for matching `ASYNC_RESP` packets. |
+| `AsyncResponseQueue`, `StatusQueue` | Polling alternatives to callbacks (`ConcurrentQueue<>`). |
+
+### Models
+
+* `PacketType` (enum) — wire-level packet types as in the protocol v0 spec.
+* `Packet`, `CommandResponse`, `AsyncResponse`, `StatusNotification` — decoded payloads.
+
+### Exceptions
+
+* `CsmTcpRouterException` — base class.
+* `RouterConnectionException` — connect / send / disconnect failures.
+* `RouterTimeoutException` — synchronous waiter timeout.
+* `ProtocolException` — invalid wire framing.
+* `ServerException` — server returned an `ERROR` packet (`Code` and `ServerMessage`).
+
+---
+
+## Protocol
+
+CSM-TCP-Router protocol v0 uses an 8-byte header (big-endian) followed by an arbitrary payload:
+
+```
+| Data Length (4B) | Version (1B = 0x01) | Type (1B) | FLAG1 (1B) | FLAG2 (1B) |
++------------------------ Header (8B) ----------------------------+
+```
+
+Packet types:
+
+| Value | Name | Direction | Use |
+| ----- | ------------ | ----------------- | ---------------------------------------------- |
+| 0x00 | `Info` | server → client | Welcome / goodbye message. |
+| 0x01 | `Error` | server → client | Error reply (`[Error: ] `). |
+| 0x02 | `Cmd` | client → server | Command from the client. |
+| 0x03 | `CmdResp` | server → client | Async / no-reply / subscribe handshake. |
+| 0x04 | `Resp` | server → client | Synchronous command response. |
+| 0x05 | `AsyncResp` | server → client | Asynchronous command response (echoes cmd). |
+| 0x06 | `Status` | server → client | Status broadcast from a subscribed module. |
+| 0x07 | `Interrupt` | server → client | Interrupt broadcast from a subscribed module. |
+
+---
+
+## Building from source
+
+```bash
+# Restore + build the whole solution
+dotnet build SDK/csharp/CsmTcpRouter.sln -c Release
+
+# Run the unit + integration test suite
+dotnet test SDK/csharp/CsmTcpRouter.Tests/CsmTcpRouter.Tests.csproj -c Release
+
+# Build the NuGet package
+dotnet pack SDK/csharp/src/CsmTcpRouter/CsmTcpRouter.csproj -c Release -o nupkg
+```
+
+The solution opens directly in **Visual Studio 2022 / 2026** (or **JetBrains Rider**); SDK-style projects are forward-compatible with all current Visual Studio versions.
+
+---
+
+## Project layout
+
+```
+SDK/csharp/
+├── CsmTcpRouter.sln
+├── README.md / README.zh-cn.md / CHANGELOG.md / LICENSE
+├── src/CsmTcpRouter/ ← single-file SDK
+│ ├── CsmTcpRouter.cs
+│ └── CsmTcpRouter.csproj
+├── tests/CsmTcpRouter.Tests/ ← xUnit test project
+│ ├── ProtocolTests.cs
+│ ├── ClientIntegrationTests.cs
+│ ├── MockServer.cs
+│ └── CsmTcpRouter.Tests.csproj
+└── examples/BasicUsage/ ← runnable console example
+ ├── Program.cs
+ └── BasicUsage.csproj
+```
+
+---
+
+## License
+
+Released under the [MIT License](LICENSE) — © 2026 NEVSTOP-LAB.
diff --git a/SDK/csharp/README.zh-cn.md b/SDK/csharp/README.zh-cn.md
new file mode 100644
index 0000000..ca7b6be
--- /dev/null
+++ b/SDK/csharp/README.zh-cn.md
@@ -0,0 +1,189 @@
+# CsmTcpRouter.Client
+
+[](https://www.nuget.org/packages/CsmTcpRouter.Client/)
+[](LICENSE)
+[](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App/actions/workflows/CSharp_SDK.yml)
+
+[CSM-TCP-Router](https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App) LabVIEW 服务器的 C# / .NET 客户端 SDK。
+
+CSM-TCP-Router 通过 TCP 暴露一个 LabVIEW [可通信状态机 (CSM)](https://github.com/NEVSTOP-LAB/Communicable-State-Machine) 应用程序,因此任何 TCP 客户端 — 包括 .NET 应用程序、测试夹具或 CI 流水线 — 都可以发送命令并接收响应,而无需修改 LabVIEW 代码。
+
+> 📖 [English README.md](README.md)
+
+整个 SDK 实现位于单个源文件中:[`src/CsmTcpRouter/CsmTcpRouter.cs`](src/CsmTcpRouter/CsmTcpRouter.cs)。
+
+---
+
+## 安装
+
+```bash
+dotnet add package CsmTcpRouter.Client
+```
+
+目标框架:`netstandard2.0` 与 `net8.0`。无第三方运行时依赖 — 仅使用 .NET BCL。
+
+支持的平台包括 .NET Framework 4.6.2+、.NET Core 3.1+ 以及 .NET 5/6/7/8/9。
+
+---
+
+## 快速上手
+
+### 同步客户端
+
+```csharp
+using CsmTcpRouter;
+
+using var client = new TcpRouterClient();
+client.Connect("localhost", 30007);
+
+// 列出已加载的所有 CSM 模块
+Console.WriteLine(client.ListModules());
+
+// 发送同步命令并等待响应
+var resp = client.SendAndWait("API: Read -@ DAQmx");
+Console.WriteLine(resp.Text);
+
+// Ping 服务器
+var (ok, elapsed) = client.Ping();
+Console.WriteLine($"Ping: {ok}, latency={elapsed.TotalMilliseconds:F1} ms");
+```
+
+### 异步客户端
+
+```csharp
+using CsmTcpRouter;
+
+using var client = new TcpRouterClient();
+await client.ConnectAsync("localhost", 30007);
+
+Console.WriteLine(await client.ListModulesAsync());
+var resp = await client.SendAndWaitAsync("API: Read -@ DAQmx");
+Console.WriteLine(resp.Text);
+```
+
+### 订阅状态广播
+
+```csharp
+client.SubscribeStatus("Status", "AI", notif =>
+{
+ Console.WriteLine($"{notif.StatusName} = {notif.Data}");
+});
+
+// ... 之后
+client.UnsubscribeStatus("Status", "AI");
+```
+
+### 异步响应回调
+
+```csharp
+client.RegisterAsyncCallback("API: Start Sampling -> DAQmx", ar =>
+{
+ Console.WriteLine($"异步响应: {ar.Text}");
+});
+
+client.Post("API: Start Sampling -> DAQmx");
+```
+
+完整可运行示例见 [`examples/BasicUsage/Program.cs`](examples/BasicUsage/Program.cs)。
+
+---
+
+## API 参考
+
+### `TcpRouterClient`
+
+| 方法 | 说明 |
+| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
+| `Connect / ConnectAsync(host, port, timeout?)` | 建立 TCP 连接。 |
+| `Disconnect()` | 关闭连接,并以 `RouterConnectionException` 立即唤醒所有阻塞中的等待者。 |
+| `Connected` | 套接字打开期间为 `true`。 |
+| `WaitForServer / WaitForServerAsync(...)` | 轮询 `host:port` 直到连接成功或超时。 |
+| `SendAndWait / SendAndWaitAsync(cmd, timeout?)` | 发送同步命令 (`-@`) 并阻塞直到 `RESP` 返回。返回 `CommandResponse`。 |
+| `Post / PostAsync(cmd, timeout?)` | 发送异步命令 (`->`) 并等待 `CMD_RESP` 握手。 |
+| `PostNoReply / PostNoReplyAsync(cmd, timeout?)` | 发送无响应异步命令 (`->\|`) 并等待 `CMD_RESP` 握手。 |
+| `Ping / PingAsync(timeout?)` | 测量往返延迟;返回 `(bool ok, TimeSpan elapsed)`。 |
+| `ListModules / ListApi / ListStates / Help(...)` | 内置的路由管理辅助命令。 |
+| `SubscribeStatus / UnsubscribeStatus(...)` | 订阅 / 取消 `STATUS` (或 `INTERRUPT`) 广播。 |
+| `RegisterAsyncCallback / UnregisterAsyncCallback(...)` | 注册 / 移除按原始命令匹配的 `ASYNC_RESP` 回调。 |
+| `AsyncResponseQueue`、`StatusQueue` | 与回调互补的轮询队列 (`ConcurrentQueue<>`)。 |
+
+### 数据模型
+
+* `PacketType` (枚举) — 协议 v0 中定义的报文类型。
+* `Packet`、`CommandResponse`、`AsyncResponse`、`StatusNotification` — 解析后的负载。
+
+### 异常
+
+* `CsmTcpRouterException` — 基类。
+* `RouterConnectionException` — 连接 / 发送 / 断开失败。
+* `RouterTimeoutException` — 同步等待超时。
+* `ProtocolException` — 报文格式非法。
+* `ServerException` — 服务器返回 `ERROR` 报文 (`Code` 与 `ServerMessage`)。
+
+---
+
+## 协议
+
+CSM-TCP-Router 协议 v0 使用 8 字节大端头部加上任意长度的负载:
+
+```
+| Data Length (4B) | Version (1B = 0x01) | Type (1B) | FLAG1 (1B) | FLAG2 (1B) |
++------------------------ Header (8B) ----------------------------+
+```
+
+报文类型:
+
+| 值 | 名称 | 方向 | 用途 |
+| ----- | ------------ | ------------- | ------------------------------------------ |
+| 0x00 | `Info` | 服务器 → 客户 | 欢迎 / 告别消息。 |
+| 0x01 | `Error` | 服务器 → 客户 | 错误响应 (`[Error: ] `)。 |
+| 0x02 | `Cmd` | 客户 → 服务器 | 客户端命令。 |
+| 0x03 | `CmdResp` | 服务器 → 客户 | 异步 / 无响应 / 订阅命令的握手。 |
+| 0x04 | `Resp` | 服务器 → 客户 | 同步命令响应。 |
+| 0x05 | `AsyncResp` | 服务器 → 客户 | 异步命令响应 (回显原始命令)。 |
+| 0x06 | `Status` | 服务器 → 客户 | 已订阅模块的状态广播。 |
+| 0x07 | `Interrupt` | 服务器 → 客户 | 已订阅模块的中断广播。 |
+
+---
+
+## 从源码构建
+
+```bash
+# 还原并构建整个解决方案
+dotnet build SDK/csharp/CsmTcpRouter.sln -c Release
+
+# 运行单元 + 集成测试
+dotnet test SDK/csharp/tests/CsmTcpRouter.Tests/CsmTcpRouter.Tests.csproj -c Release
+
+# 打包 NuGet
+dotnet pack SDK/csharp/src/CsmTcpRouter/CsmTcpRouter.csproj -c Release -o nupkg
+```
+
+解决方案文件可直接用 **Visual Studio 2022 / 2026**(或 **JetBrains Rider**)打开;SDK 风格项目向前兼容所有当前 Visual Studio 版本。
+
+---
+
+## 项目结构
+
+```
+SDK/csharp/
+├── CsmTcpRouter.sln
+├── README.md / README.zh-cn.md / CHANGELOG.md / LICENSE
+├── src/CsmTcpRouter/ ← 单文件 SDK
+│ ├── CsmTcpRouter.cs
+│ └── CsmTcpRouter.csproj
+├── tests/CsmTcpRouter.Tests/ ← xUnit 测试工程
+│ ├── ProtocolTests.cs
+│ ├── ClientIntegrationTests.cs
+│ ├── MockServer.cs
+│ └── CsmTcpRouter.Tests.csproj
+└── examples/BasicUsage/ ← 可运行控制台示例
+ ├── Program.cs
+ └── BasicUsage.csproj
+```
+
+---
+
+## 许可证
+
+基于 [MIT 许可证](LICENSE) 发布 — © 2026 NEVSTOP-LAB。
diff --git a/SDK/csharp/examples/BasicUsage/BasicUsage.csproj b/SDK/csharp/examples/BasicUsage/BasicUsage.csproj
new file mode 100644
index 0000000..5230e09
--- /dev/null
+++ b/SDK/csharp/examples/BasicUsage/BasicUsage.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0
+ latest
+ disable
+ CsmTcpRouter.Examples.BasicUsage
+ BasicUsage
+ false
+
+
+
+
+
+
+
diff --git a/SDK/csharp/examples/BasicUsage/Program.cs b/SDK/csharp/examples/BasicUsage/Program.cs
new file mode 100644
index 0000000..2bdd24a
--- /dev/null
+++ b/SDK/csharp/examples/BasicUsage/Program.cs
@@ -0,0 +1,91 @@
+using System;
+using CsmTcpRouter;
+
+namespace CsmTcpRouter.Examples.BasicUsage
+{
+ ///
+ /// csm-tcp-router-client(C#)的基本用法示例。
+ /// 镜像 SDK/python/examples/basic_usage.py。
+ ///
+ /// 前提条件:正在运行的 CSM-TCP-Router 服务器(LabVIEW 应用程序)。
+ /// 参考应用程序默认使用端口 30007。
+ ///
+ public static class Program
+ {
+ private const string Host = "localhost";
+ private const int Port = 30007;
+
+ public static int Main(string[] args)
+ {
+ // 1. 等待服务器就绪。
+ Console.Write("Waiting for server ... ");
+ using (var probe = new TcpRouterClient())
+ {
+ bool ok = probe.WaitForServer(Host, Port, TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(500));
+ if (!ok)
+ {
+ Console.WriteLine("TIMEOUT - server did not start within 30 s.");
+ return 1;
+ }
+ }
+ Console.WriteLine("ready.");
+
+ // 2. 连接(使用 IDisposable,确保始终调用 Disconnect)。
+ using (var client = new TcpRouterClient())
+ {
+ try
+ {
+ client.Connect(Host, Port, TimeSpan.FromSeconds(5));
+ }
+ catch (RouterConnectionException exc)
+ {
+ Console.WriteLine($"Connection failed: {exc.Message}");
+ return 1;
+ }
+
+ Console.WriteLine($"Connected to {Host}:{Port}");
+
+ // 3. Ping 测试
+ var (ok, elapsed) = client.Ping(TimeSpan.FromSeconds(2));
+ Console.WriteLine(ok
+ ? $"Ping OK latency={elapsed.TotalMilliseconds:F1} ms"
+ : "Ping failed.");
+
+ // 4. 列出 CSM 模块
+ string modules = client.ListModules();
+ Console.WriteLine($"\nLoaded modules:\n{modules}");
+
+ // 5. 列出第一个模块的 API(如有)
+ string firstModule = null;
+ foreach (var line in modules.Split('\n'))
+ {
+ var trimmed = line.Trim();
+ if (!string.IsNullOrEmpty(trimmed))
+ {
+ firstModule = trimmed;
+ break;
+ }
+ }
+ if (firstModule != null)
+ {
+ string api = client.ListApi(firstModule);
+ Console.WriteLine($"\nAPI for '{firstModule}':\n{api}");
+ }
+
+ // 6. 发送同步命令(取消注释并适配您的 CSM):
+ // var resp = client.SendAndWait("API: Read -@ DAQmx");
+ // Console.WriteLine($"\nSync response: {resp.Text}");
+
+ // 7. 发送异步命令并等待 cmd-resp 握手:
+ // client.Post("API: Start Sampling -> DAQmx");
+
+ // 8. 发送无回复命令:
+ // client.PostNoReply("API: Reset ->| DAQmx");
+
+ Console.WriteLine("\nDone.");
+ }
+ Console.WriteLine("Disconnected.");
+ return 0;
+ }
+ }
+}
diff --git a/SDK/csharp/examples/ClientConsole/ClientConsole.csproj b/SDK/csharp/examples/ClientConsole/ClientConsole.csproj
new file mode 100644
index 0000000..8d84430
--- /dev/null
+++ b/SDK/csharp/examples/ClientConsole/ClientConsole.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0
+ latest
+ disable
+ CsmTcpRouter.Examples.ClientConsole
+ ClientConsole
+ false
+
+
+
+
+
+
+
diff --git a/SDK/csharp/examples/ClientConsole/Program.cs b/SDK/csharp/examples/ClientConsole/Program.cs
new file mode 100644
index 0000000..29823ab
--- /dev/null
+++ b/SDK/csharp/examples/ClientConsole/Program.cs
@@ -0,0 +1,236 @@
+using System;
+using CsmTcpRouter;
+
+namespace CsmTcpRouter.Examples.ClientConsole
+{
+ ///
+ /// 交互式客户端控制台示例。连接到正在运行的 CSM-TCP-Router 服务器,
+ /// 接收用户从 stdin 输入的命令,并通过 SDK 转发。
+ ///
+ /// 同样的命令集、提示符和输出格式也在 Python(examples/client_console.py)
+ /// 和 C(examples/client_console.c)SDK 示例中实现,因此三种语言的行为一致。
+ ///
+ public static class Program
+ {
+ private const string DefaultHost = "localhost";
+ private const int DefaultPort = 30007;
+
+ private const string HelpText =
+ "Available commands:\n" +
+ " help Show this help text\n" +
+ " quit / exit Disconnect and exit\n" +
+ " ping Measure round-trip latency\n" +
+ " list List CSM modules loaded on the server\n" +
+ " api List the API of a module\n" +
+ " state List the states of a module\n" +
+ " mhelp Server-side Help for a module\n" +
+ " send Send a synchronous command and print the response\n" +
+ " post Send an asynchronous command (-> suffix)\n" +
+ " nopost Send a no-reply asynchronous command (->|)\n" +
+ " sub @ Subscribe to a status broadcast\n" +
+ " unsub @ Unsubscribe from a status broadcast";
+
+ public static int Main(string[] args)
+ {
+ string host = args.Length > 0 ? args[0] : DefaultHost;
+ int port = DefaultPort;
+ if (args.Length > 1)
+ {
+ if (!int.TryParse(args[1], out port) || port < 1 || port > 65535)
+ {
+ Console.WriteLine($"Error: invalid port '{args[1]}'");
+ return 1;
+ }
+ }
+
+ Console.WriteLine("CSM-TCP-Router Client Console");
+ Console.WriteLine($"Connecting to {host}:{port} ...");
+
+ using var client = new TcpRouterClient();
+ try
+ {
+ client.Connect(host, port);
+ }
+ catch (RouterConnectionException exc)
+ {
+ Console.WriteLine($"Error: {exc.Message}");
+ return 1;
+ }
+
+ Console.WriteLine($"Connected to {host}:{port}. Type 'help' for commands, 'quit' to exit.");
+
+ while (true)
+ {
+ Console.Write("csm> ");
+ string line = Console.ReadLine();
+ if (line == null)
+ {
+ Console.WriteLine();
+ break;
+ }
+
+ bool keepRunning;
+ try
+ {
+ keepRunning = Dispatch(client, line);
+ }
+ catch (CsmTcpRouterException exc)
+ {
+ Console.WriteLine($"Error: {exc.Message}");
+ continue;
+ }
+ catch (ArgumentException exc)
+ {
+ Console.WriteLine($"Error: {exc.Message}");
+ continue;
+ }
+
+ if (!keepRunning)
+ {
+ break;
+ }
+ }
+
+ client.Disconnect();
+ Console.WriteLine("Disconnected.");
+ return 0;
+ }
+
+ private static bool Dispatch(TcpRouterClient client, string line)
+ {
+ line = line.Trim();
+ if (line.Length == 0)
+ {
+ return true;
+ }
+
+ int spaceIndex = line.IndexOf(' ');
+ string cmd = (spaceIndex < 0 ? line : line.Substring(0, spaceIndex)).ToLowerInvariant();
+ string arg = spaceIndex < 0 ? string.Empty : line.Substring(spaceIndex + 1).Trim();
+
+ switch (cmd)
+ {
+ case "quit":
+ case "exit":
+ return false;
+
+ case "help":
+ Console.WriteLine(HelpText);
+ return true;
+
+ case "ping":
+ {
+ var (ok, elapsed) = client.Ping();
+ Console.WriteLine(ok
+ ? $"Ping OK latency={elapsed.TotalMilliseconds:F1} ms"
+ : "Ping failed.");
+ return true;
+ }
+
+ case "list":
+ Console.WriteLine(client.ListModules());
+ return true;
+
+ case "api":
+ if (arg.Length == 0) Console.WriteLine("Error: usage: api ");
+ else Console.WriteLine(client.ListApi(arg));
+ return true;
+
+ case "state":
+ if (arg.Length == 0) Console.WriteLine("Error: usage: state ");
+ else Console.WriteLine(client.ListStates(arg));
+ return true;
+
+ case "mhelp":
+ if (arg.Length == 0) Console.WriteLine("Error: usage: mhelp ");
+ else Console.WriteLine(client.Help(arg));
+ return true;
+
+ case "send":
+ if (arg.Length == 0)
+ {
+ Console.WriteLine("Error: usage: send ");
+ }
+ else
+ {
+ var resp = client.SendAndWait(arg);
+ Console.WriteLine($"Response: {resp.Text}");
+ }
+ return true;
+
+ case "post":
+ if (arg.Length == 0)
+ {
+ Console.WriteLine("Error: usage: post ");
+ }
+ else
+ {
+ client.RegisterAsyncCallback(arg, OnAsync);
+ client.Post(arg);
+ Console.WriteLine("Async command sent.");
+ }
+ return true;
+
+ case "nopost":
+ if (arg.Length == 0)
+ {
+ Console.WriteLine("Error: usage: nopost ");
+ }
+ else
+ {
+ client.PostNoReply(arg);
+ Console.WriteLine("No-reply command sent.");
+ }
+ return true;
+
+ case "sub":
+ {
+ var (status, module) = SplitStatusModule(arg);
+ client.SubscribeStatus(status, module, OnStatus);
+ Console.WriteLine($"Subscribed to {status}@{module}");
+ return true;
+ }
+
+ case "unsub":
+ {
+ var (status, module) = SplitStatusModule(arg);
+ client.UnsubscribeStatus(status, module);
+ Console.WriteLine($"Unsubscribed from {status}@{module}");
+ return true;
+ }
+
+ default:
+ Console.WriteLine($"Error: unknown command '{cmd}'. Type 'help' for the command list.");
+ return true;
+ }
+ }
+
+ private static (string Status, string Module) SplitStatusModule(string arg)
+ {
+ int at = arg.IndexOf('@');
+ if (at < 0)
+ {
+ throw new ArgumentException("expected '@'");
+ }
+ string status = arg.Substring(0, at).Trim();
+ string module = arg.Substring(at + 1).Trim();
+ if (status.Length == 0 || module.Length == 0)
+ {
+ throw new ArgumentException("expected '@'");
+ }
+ return (status, module);
+ }
+
+ private static void OnStatus(StatusNotification notification)
+ {
+ Console.WriteLine();
+ Console.WriteLine($"[STATUS] {notification.StatusName}@{notification.ModuleName}: {notification.Data}");
+ }
+
+ private static void OnAsync(AsyncResponse response)
+ {
+ Console.WriteLine();
+ Console.WriteLine($"[ASYNC] {response.Text} (cmd={response.OriginalCommand})");
+ }
+ }
+}
diff --git a/SDK/csharp/src/CsmTcpRouter/CsmTcpRouter.cs b/SDK/csharp/src/CsmTcpRouter/CsmTcpRouter.cs
new file mode 100644
index 0000000..6d1fa91
--- /dev/null
+++ b/SDK/csharp/src/CsmTcpRouter/CsmTcpRouter.cs
@@ -0,0 +1,920 @@
+// CsmTcpRouter.cs
+// ---------------------------------------------------------------------------
+// csm-tcp-router-client - CSM-TCP-Router LabVIEW 服务器的 C# 客户端 SDK。
+//
+// 单文件 SDK,实现 CSM-TCP-Router 协议 v0。镜像了
+// Python `csm_tcp_router` 包的布局和功能:
+//
+// * 协议编解码器(8 字节头,大端序,8 种数据包类型)。
+// * 后台接收 TCP 传输层。
+// * 高层 TcpRouterClient,提供同步和异步 API:
+// - SendAndWait / SendAndWaitAsync (同步 CMD/RESP)
+// - Post / PostAsync (带 cmd-resp 握手的异步 CMD)
+// - PostNoReply / PostNoReplyAsync (无回复异步 CMD)
+// - Ping / PingAsync (往返延迟)
+// - ListModules / ListApi / ListStates / Help
+// - SubscribeStatus / UnsubscribeStatus
+// - RegisterAsyncCallback / UnregisterAsyncCallback
+//
+// 线路格式(8 字节头,大端序):
+//
+// | Data Length (4B) | Version (1B=0x01) | Type (1B) | FLAG1 (1B) | FLAG2 (1B) |
+// +------------------------ Header (8B) -----------------------+
+//
+// 后跟恰好 `Data Length` 字节的有效载荷。
+//
+// Copyright (c) 2026 NEVSTOP-LAB. Released under the MIT License.
+// ---------------------------------------------------------------------------
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Sockets;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+[assembly: InternalsVisibleTo("CsmTcpRouter.Tests")]
+
+namespace CsmTcpRouter
+{
+ // -----------------------------------------------------------------------
+ // 公共枚举
+ // -----------------------------------------------------------------------
+
+ ///
+ /// CSM-TCP-Router 协议 v0 中定义的数据包类型常量。
+ ///
+ public enum PacketType : byte
+ {
+ /// 信息消息(欢迎/再见)。
+ Info = 0x00,
+ /// 来自服务器的错误数据包。
+ Error = 0x01,
+ /// 客户端发送的命令。
+ Cmd = 0x02,
+ /// 服务器对异步/无回复/订阅命令的握手确认。
+ CmdResp = 0x03,
+ /// 同步响应有效载荷。
+ Resp = 0x04,
+ /// 异步响应有效载荷。
+ AsyncResp = 0x05,
+ /// 来自已订阅 CSM 模块的状态广播。
+ Status = 0x06,
+ /// 来自已订阅 CSM 模块的中断广播。
+ Interrupt = 0x07,
+ }
+
+ // -----------------------------------------------------------------------
+ // 公共数据模型
+ // -----------------------------------------------------------------------
+
+ /// 从服务器接收到的已解码数据包。
+ public sealed class Packet
+ {
+ public PacketType Type { get; }
+ public byte[] Data { get; }
+ public byte Version { get; }
+ public byte Flag1 { get; }
+ public byte Flag2 { get; }
+
+ public Packet(PacketType type, byte[] data, byte version = 1, byte flag1 = 0, byte flag2 = 0)
+ {
+ Type = type;
+ Data = data ?? Array.Empty();
+ Version = version;
+ Flag1 = flag1;
+ Flag2 = flag2;
+ }
+ }
+
+ /// 同步命令()的结果。
+ public sealed class CommandResponse
+ {
+ public byte[] Raw { get; }
+ public string Text => Encoding.UTF8.GetString(Raw);
+
+ public CommandResponse(byte[] raw)
+ {
+ Raw = raw ?? Array.Empty();
+ }
+
+ public override string ToString() => $"CommandResponse(\"{Text}\")";
+ }
+
+ /// 通过异步响应数据包传递的异步响应有效载荷。
+ public sealed class AsyncResponse
+ {
+ public byte[] Raw { get; }
+ public string OriginalCommand { get; }
+ public string Text => Encoding.UTF8.GetString(Raw);
+
+ public AsyncResponse(byte[] raw, string originalCommand = "")
+ {
+ Raw = raw ?? Array.Empty();
+ OriginalCommand = originalCommand ?? string.Empty;
+ }
+
+ ///
+ /// 解析 ASYNC_RESP 数据包。服务器格式:
+ /// "<response-data> <- <original-command>"。
+ ///
+ public static AsyncResponse FromPacket(Packet packet)
+ {
+ if (packet == null) throw new ArgumentNullException(nameof(packet));
+ string text = Encoding.UTF8.GetString(packet.Data);
+ int sep = text.IndexOf(" <- ", StringComparison.Ordinal);
+ if (sep >= 0)
+ {
+ string left = text.Substring(0, sep);
+ string right = text.Substring(sep + 4);
+ return new AsyncResponse(Encoding.UTF8.GetBytes(left), right);
+ }
+ return new AsyncResponse(packet.Data);
+ }
+
+ public override string ToString() => $"AsyncResponse(\"{Text}\", cmd=\"{OriginalCommand}\")";
+ }
+
+ /// 通过 STATUS 或 INTERRUPT 数据包传递的状态广播。
+ public sealed class StatusNotification
+ {
+ public byte[] Raw { get; }
+ public PacketType PacketType { get; }
+ public string StatusName { get; }
+ public string Data { get; }
+ public string ModuleName { get; }
+
+ public StatusNotification(
+ byte[] raw,
+ PacketType packetType = PacketType.Status,
+ string statusName = "",
+ string data = "",
+ string moduleName = "")
+ {
+ Raw = raw ?? Array.Empty();
+ PacketType = packetType;
+ StatusName = statusName ?? string.Empty;
+ Data = data ?? string.Empty;
+ ModuleName = moduleName ?? string.Empty;
+ }
+
+ ///
+ /// 解析 STATUS 或 INTERRUPT 数据包。服务器格式:
+ /// "<status-name> >> <data> <- <module>"。
+ ///
+ public static StatusNotification FromPacket(Packet packet)
+ {
+ if (packet == null) throw new ArgumentNullException(nameof(packet));
+ string text = Encoding.UTF8.GetString(packet.Data);
+ string module = string.Empty;
+ string left = text;
+ int sepArrow = text.LastIndexOf(" <- ", StringComparison.Ordinal);
+ if (sepArrow >= 0)
+ {
+ left = text.Substring(0, sepArrow);
+ module = text.Substring(sepArrow + 4).Trim();
+ }
+ string statusName = string.Empty;
+ string data = left.Trim();
+ int sepGtGt = left.IndexOf(" >> ", StringComparison.Ordinal);
+ if (sepGtGt >= 0)
+ {
+ statusName = left.Substring(0, sepGtGt).Trim();
+ data = left.Substring(sepGtGt + 4).Trim();
+ }
+ return new StatusNotification(packet.Data, packet.Type, statusName, data, module);
+ }
+
+ public override string ToString() =>
+ $"StatusNotification(status=\"{StatusName}\", data=\"{Data}\", module=\"{ModuleName}\")";
+ }
+
+ // -----------------------------------------------------------------------
+ // 异常层次结构
+ // -----------------------------------------------------------------------
+
+ /// 所有 CSM-TCP-Router 客户端错误的基础异常。
+ public class CsmTcpRouterException : Exception
+ {
+ public CsmTcpRouterException() { }
+ public CsmTcpRouterException(string message) : base(message) { }
+ public CsmTcpRouterException(string message, Exception innerException) : base(message, innerException) { }
+ }
+
+ /// 当连接无法建立或连接丢失时引发。
+ public class RouterConnectionException : CsmTcpRouterException
+ {
+ public RouterConnectionException(string message) : base(message) { }
+ public RouterConnectionException(string message, Exception innerException) : base(message, innerException) { }
+ }
+
+ /// 当同步操作超过其超时时间时引发。
+ public class RouterTimeoutException : CsmTcpRouterException
+ {
+ public RouterTimeoutException(string message) : base(message) { }
+ }
+
+ /// 当收到无效或意外的协议帧时引发。
+ public class ProtocolException : CsmTcpRouterException
+ {
+ public ProtocolException(string message) : base(message) { }
+ }
+
+ ///
+ /// 当服务器返回错误数据包时引发。CSM 错误格式:
+ /// [Error: <code>] <message>。
+ ///
+ public class ServerException : CsmTcpRouterException
+ {
+ public string Code { get; }
+ public string ServerMessage { get; }
+
+ public ServerException(string message, string code = "")
+ : base(message)
+ {
+ ServerMessage = message ?? string.Empty;
+ Code = code ?? string.Empty;
+ }
+
+ public override string ToString()
+ {
+ return string.IsNullOrEmpty(Code)
+ ? ServerMessage
+ : $"[Error: {Code}] {ServerMessage}";
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // 内部协议编解码器
+ // -----------------------------------------------------------------------
+
+ internal static class ProtocolCodec
+ {
+ public const int HeaderSize = 8;
+ public const byte ProtocolVersion = 0x01;
+
+ /// 将 编码为完整的线路格式数据包(头 + 体)。
+ public static byte[] EncodePacket(byte[] data, PacketType packetType, byte flag1 = 0, byte flag2 = 0)
+ {
+ data = data ?? Array.Empty();
+ var wire = new byte[HeaderSize + data.Length];
+ uint len = (uint)data.Length;
+ wire[0] = (byte)((len >> 24) & 0xFF);
+ wire[1] = (byte)((len >> 16) & 0xFF);
+ wire[2] = (byte)((len >> 8) & 0xFF);
+ wire[3] = (byte)(len & 0xFF);
+ wire[4] = ProtocolVersion;
+ wire[5] = (byte)packetType;
+ wire[6] = flag1;
+ wire[7] = flag2;
+ Buffer.BlockCopy(data, 0, wire, HeaderSize, data.Length);
+ return wire;
+ }
+
+ /// 将 8 字节头解码为其组成字段。
+ public static (uint DataLen, byte Version, byte TypeByte, byte Flag1, byte Flag2) DecodeHeader(byte[] header)
+ {
+ if (header == null || header.Length != HeaderSize)
+ throw new ProtocolException(
+ $"Expected {HeaderSize}-byte header, got {(header == null ? 0 : header.Length)} bytes.");
+ uint dataLen = ((uint)header[0] << 24) | ((uint)header[1] << 16) | ((uint)header[2] << 8) | header[3];
+ return (dataLen, header[4], header[5], header[6], header[7]);
+ }
+
+ /// 从原始头 + 体构建 。
+ public static Packet ParsePacket(byte[] header, byte[] body)
+ {
+ var (dataLen, version, typeByte, flag1, flag2) = DecodeHeader(header);
+ body = body ?? Array.Empty();
+ if ((uint)body.Length != dataLen)
+ throw new ProtocolException(
+ $"Payload length mismatch: header says {dataLen} bytes, got {body.Length} bytes.");
+ // 向前兼容:未知类型字节映射为 Info。
+ PacketType ptype = Enum.IsDefined(typeof(PacketType), typeByte)
+ ? (PacketType)typeByte
+ : PacketType.Info;
+ return new Packet(ptype, body, version, flag1, flag2);
+ }
+
+ /// 从 CSM 错误格式 [Error: code] msg 中提取代码和消息。
+ public static ServerException ParseServerError(Packet packet)
+ {
+ string text = Encoding.UTF8.GetString(packet.Data).Trim();
+ string code = string.Empty;
+ string msg = text;
+ if (text.StartsWith("[Error:", StringComparison.Ordinal))
+ {
+ int end = text.IndexOf(']');
+ if (end > 0)
+ {
+ code = text.Substring(7, end - 7).Trim();
+ msg = text.Substring(end + 1).Trim();
+ }
+ }
+ return new ServerException(msg, code);
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // 内部 TCP 传输层(后台接收任务)
+ // -----------------------------------------------------------------------
+
+ internal sealed class Transport : IDisposable
+ {
+ private readonly object _sendLock = new object();
+ private readonly Action _onPacket;
+ private readonly Action _onDisconnect;
+
+ private TcpClient _client;
+ private NetworkStream _stream;
+ private CancellationTokenSource _cts;
+ private Task _recvTask;
+ private volatile bool _stopped;
+
+ public Transport(Action onPacket, Action onDisconnect)
+ {
+ _onPacket = onPacket ?? throw new ArgumentNullException(nameof(onPacket));
+ _onDisconnect = onDisconnect ?? throw new ArgumentNullException(nameof(onDisconnect));
+ }
+
+ public bool Connected
+ {
+ get
+ {
+ var c = _client;
+ return c != null && c.Connected && !_stopped;
+ }
+ }
+
+ public void Connect(string host, int port, TimeSpan? timeout = null)
+ {
+ ConnectAsync(host, port, timeout).GetAwaiter().GetResult();
+ }
+
+ public async Task ConnectAsync(string host, int port, TimeSpan? timeout = null)
+ {
+ if (Connected)
+ throw new RouterConnectionException("Already connected; call Disconnect() first.");
+ var to = timeout ?? TimeSpan.FromSeconds(5);
+ var client = new TcpClient();
+ try
+ {
+ var connectTask = client.ConnectAsync(host, port);
+ var winner = await Task.WhenAny(connectTask, Task.Delay(to)).ConfigureAwait(false);
+ if (winner != connectTask)
+ {
+ try { client.Close(); } catch { /* 忽略 */ }
+ throw new RouterConnectionException(
+ $"Cannot connect to {host}:{port}: timed out after {to.TotalSeconds:F1}s.");
+ }
+ await connectTask.ConfigureAwait(false); // 让任何连接异常浮现
+ }
+ catch (RouterConnectionException)
+ {
+ throw;
+ }
+ catch (Exception exc)
+ {
+ try { client.Close(); } catch { /* 忽略 */ }
+ throw new RouterConnectionException($"Cannot connect to {host}:{port}: {exc.Message}", exc);
+ }
+
+ _client = client;
+ _stream = client.GetStream();
+ _stopped = false;
+ _cts = new CancellationTokenSource();
+ _recvTask = Task.Run(() => RecvLoopAsync(_cts.Token));
+ }
+
+ public void Disconnect(TimeSpan? joinTimeout = null)
+ {
+ _stopped = true;
+ try { _cts?.Cancel(); } catch { /* 忽略 */ }
+ try { _stream?.Close(); } catch { /* 忽略 */ }
+ try { _client?.Close(); } catch { /* 忽略 */ }
+ _stream = null;
+ _client = null;
+ var jt = joinTimeout ?? TimeSpan.FromSeconds(2);
+ try { _recvTask?.Wait(jt); } catch { /* 忽略 */ }
+ _recvTask = null;
+ try { _cts?.Dispose(); } catch { /* 忽略 */ }
+ _cts = null;
+ }
+
+ public void SendRaw(byte[] data)
+ {
+ if (!Connected) throw new RouterConnectionException("Not connected.");
+ lock (_sendLock)
+ {
+ try
+ {
+ var s = _stream;
+ if (s == null) throw new RouterConnectionException("Not connected.");
+ s.Write(data, 0, data.Length);
+ }
+ catch (Exception exc) when (!(exc is RouterConnectionException))
+ {
+ _stopped = true;
+ throw new RouterConnectionException($"Send failed: {exc.Message}", exc);
+ }
+ }
+ }
+
+ public void Dispose() => Disconnect();
+
+ // ---------------------------------------------------------------
+ // 内部:后台接收循环
+ // ---------------------------------------------------------------
+
+ private async Task RecvLoopAsync(CancellationToken ct)
+ {
+ var stream = _stream;
+ try
+ {
+ var headerBuf = new byte[ProtocolCodec.HeaderSize];
+ while (!ct.IsCancellationRequested)
+ {
+ if (!await ReadExactlyAsync(stream, headerBuf, 0, headerBuf.Length, ct).ConfigureAwait(false))
+ break;
+
+ uint dataLen = ((uint)headerBuf[0] << 24) | ((uint)headerBuf[1] << 16) | ((uint)headerBuf[2] << 8) | headerBuf[3];
+ byte[] body = dataLen == 0 ? Array.Empty() : new byte[dataLen];
+ if (dataLen > 0 && !await ReadExactlyAsync(stream, body, 0, body.Length, ct).ConfigureAwait(false))
+ break;
+
+ Packet packet;
+ try
+ {
+ packet = ProtocolCodec.ParsePacket(headerBuf, body);
+ }
+ catch (ProtocolException)
+ {
+ // 损坏的帧——跳过并保持循环运行。
+ continue;
+ }
+
+ try { _onPacket(packet); } catch { /* swallow callback errors */ }
+ }
+ }
+ catch (IOException) { /* 连接已断开 */ }
+ catch (ObjectDisposedException) { /* 读取期间套接字已关闭 */ }
+ catch (OperationCanceledException) { /* 正在关闭 */ }
+ finally
+ {
+ if (!_stopped)
+ {
+ _stopped = true;
+ try { _onDisconnect(); } catch { /* 忽略 */ }
+ }
+ }
+ }
+
+ private static async Task ReadExactlyAsync(Stream stream, byte[] buf, int offset, int count, CancellationToken ct)
+ {
+ int read = 0;
+ while (read < count)
+ {
+ int n;
+ try
+ {
+ n = await stream.ReadAsync(buf, offset + read, count - read, ct).ConfigureAwait(false);
+ }
+ catch (IOException) { return false; }
+ catch (ObjectDisposedException) { return false; }
+ if (n == 0) return false;
+ read += n;
+ }
+ return true;
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // 高层客户端
+ // -----------------------------------------------------------------------
+
+ /// 用于状态/中断广播的回调委托。
+ public delegate void StatusCallback(StatusNotification notification);
+
+ /// 用于异步响应数据包的回调委托。
+ public delegate void AsyncResponseCallback(AsyncResponse response);
+
+ ///
+ /// CSM-TCP-Router 服务器的 C# 客户端。镜像了 LabVIEW ClientAPI VI
+ /// 和 Python TcpRouterClient;使用协议 v0。
+ ///
+ /// 该类是线程安全的。任意时刻最多只能有一个正在执行的同步命令
+ /// 和一个正在执行的异步/订阅命令;并发调用者由内部信号量序列化。
+ ///
+ public sealed class TcpRouterClient : IDisposable
+ {
+ // 通过 TCS 实现的单元素"队列",用于同步等待。
+ // 每个等待者在相应的锁内将其重置为新的 TCS。
+ private TaskCompletionSource