diff --git a/.github/workflows/Build_VIPM_Library.yml b/.github/workflows/Build_VIPM_Library.yml index 357ccf1..dd409f9 100644 --- a/.github/workflows/Build_VIPM_Library.yml +++ b/.github/workflows/Build_VIPM_Library.yml @@ -17,6 +17,7 @@ on: - '**.svg' - '**.json' - '**.yml' + - 'SDK/**/*' push: paths-ignore: @@ -27,6 +28,7 @@ on: - '**.svg' - '**.json' - '**.yml' + - 'SDK/**/*' # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/.github/workflows/Check_Broken_VIs.yml b/.github/workflows/Check_Broken_VIs.yml index 9bb1ecb..eb45d79 100644 --- a/.github/workflows/Check_Broken_VIs.yml +++ b/.github/workflows/Check_Broken_VIs.yml @@ -15,7 +15,8 @@ on: - '**.svg' - '**.json' - '**.yml' - + - 'SDK/**/*' + pull_request: branches: - main @@ -29,6 +30,7 @@ on: - '**.svg' - '**.json' - '**.yml' + - 'SDK/**/*' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff --git a/.gitignore b/.gitignore index f2c8959..10380b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,69 @@ *.aliases *.lvlps /vip/*.vip -*.bkp \ No newline at end of file +*.bkp + +# Python +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +build/ +dist/ +dist-egg/ +build-egg/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Testing +.pytest_cache/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Misc +*.py,cover +*.log +*.pid +*.seed +*.pid.lock + +# OS generated files +Thumbs.db +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db \ No newline at end of file diff --git a/CSM-TCP-Router.vipb b/CSM-TCP-Router.vipb index a239a3e..e4c1dee 100644 --- a/CSM-TCP-Router.vipb +++ b/CSM-TCP-Router.vipb @@ -1,7 +1,7 @@ - + NEVSTOP_lib_CSM_TCP_Router_Example - 2025.7.0.3 + 2025.9.0.3 false src vip @@ -74,8 +74,7 @@ NEVSTOP https://github.com/NEVSTOP-LAB/CSM-TCP-Router-App - [update] Use Queue API instead for monitoring global log. -[fix] Fix a bug in "TCP Safe Read.vi" + @@ -225,7 +224,6 @@ false false false - false true diff --git a/SDK/PythonClientAPI/README.md b/SDK/PythonClientAPI/README.md new file mode 100644 index 0000000..78a0e18 --- /dev/null +++ b/SDK/PythonClientAPI/README.md @@ -0,0 +1,166 @@ +# 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 new file mode 100644 index 0000000..5569d7c --- /dev/null +++ b/SDK/PythonClientAPI/example_usage.py @@ -0,0 +1,103 @@ +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 new file mode 100644 index 0000000..0780e4a --- /dev/null +++ b/SDK/PythonClientAPI/tcp_router_client.py @@ -0,0 +1,259 @@ +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/src/CSM-TCP-Router.lvcsm b/src/CSM-TCP-Router.lvcsm index 5e2bee7..4f05a76 100644 --- a/src/CSM-TCP-Router.lvcsm +++ b/src/CSM-TCP-Router.lvcsm @@ -128,6 +128,9 @@ Item 28 = "action: end loop" [CSM Debug Console] Response Timeout(s) = 30 History Length = 50 +Periodic Enable = TRUE +Periodic threashold(#/s) = 0.500000 +Periodic Check Peroid(s) = 3.000000 [CSMModule.CSM TCP Router] VIName = "CSM-TCP-Router.lvlib:CSM-TCP-Router.vi" diff --git a/src/CSM-TCP-Router.lvproj b/src/CSM-TCP-Router.lvproj index a4a0e6e..3ecf053 100644 --- a/src/CSM-TCP-Router.lvproj +++ b/src/CSM-TCP-Router.lvproj @@ -71,10 +71,13 @@ + + + @@ -104,7 +107,10 @@ + + + @@ -126,6 +132,7 @@ + @@ -142,6 +149,8 @@ + + diff --git a/src/Client Console/Support/Connection Input Dialog.vi b/src/Client Console/Support/Connection Input Dialog.vi index e7f634d..24fb200 100644 Binary files a/src/Client Console/Support/Connection Input Dialog.vi and b/src/Client Console/Support/Connection Input Dialog.vi differ diff --git a/src/ClientAPI Example/TCPRouter ClientAPI Example 1.1.vi b/src/ClientAPI Example/TCPRouter ClientAPI Example 1.1.vi index e54a6bd..c4f23f2 100644 Binary files a/src/ClientAPI Example/TCPRouter ClientAPI Example 1.1.vi and b/src/ClientAPI Example/TCPRouter ClientAPI Example 1.1.vi differ diff --git a/src/Server/CSM-TCP-Router(Server).vi b/src/Server/CSM-TCP-Router(Server).vi index c83ca6d..24b6bbc 100644 Binary files a/src/Server/CSM-TCP-Router(Server).vi and b/src/Server/CSM-TCP-Router(Server).vi differ diff --git a/src/Server/CSM_Modules/HAL-AI.vi b/src/Server/CSM_Modules/HAL-AI.vi index 048d7ca..752734a 100644 Binary files a/src/Server/CSM_Modules/HAL-AI.vi and b/src/Server/CSM_Modules/HAL-AI.vi differ diff --git a/src/Server/CSM_Modules/HAL-DIO.vi b/src/Server/CSM_Modules/HAL-DIO.vi index 561bc87..e830c7a 100644 Binary files a/src/Server/CSM_Modules/HAL-DIO.vi and b/src/Server/CSM_Modules/HAL-DIO.vi differ diff --git a/src/Server/CSM_Modules/MAL-TEST.vi b/src/Server/CSM_Modules/MAL-TEST.vi index 7ec0ec1..17387d7 100644 Binary files a/src/Server/CSM_Modules/MAL-TEST.vi and b/src/Server/CSM_Modules/MAL-TEST.vi differ diff --git a/src/_addons/TCP-Router/CSM-TCP-Router.vi b/src/_addons/TCP-Router/CSM-TCP-Router.vi index 61ba36c..0c29db3 100644 Binary files a/src/_addons/TCP-Router/CSM-TCP-Router.vi and b/src/_addons/TCP-Router/CSM-TCP-Router.vi differ