节点1: modbus通信,作为ros2 service服务器, 节点2: 作为ros2 service服务器,状态话题发布,命令话题订阅, 节点3: 状态话题订阅,命令话题发布

程序框架

节点1:

# modbus_rtu_server.py
#### command lines ####
# source install/setup.bash
# # 查看USB串口
# ls -al /dev/ttyUSB*
#  # 当前用户加入用户组
# sudo usermod -a -G dialout $USER
# #查看当前用户是否在设备组
# groups $USER
# # 组权限更新
# newgrp dialout
# # 是否被其他程序占用
# sudo lsof /dev/ttyUSB0
# ros2 run pkg_joints modbus_rtu_server --ros-args -p stopbits:=2 -p port:="/dev/ttyUSB0" 

# 01 (0x01): 读线圈状态
# 02 (0x02): 读离散输入
# 03 (0x03): 读保持寄存器
# 04 (0x04): 读输入寄存器

# 05 (0x05): 写单个线圈
# 06 (0x06): 写单个寄存器
# 16 (0x10): 写多个寄存器

# 读操作(01-04): 使用address和count
# 写单个(05,06): 使用address和value
# 写多个(16): 使用address和registers数组

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup

from pkg_interfaces.srv import ModbusRTU

from pymodbus.client import ModbusSerialClient as ModbusClient
from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse
import time
# 在ROS2中,MultiThreadedExecutor 和 SingleThreadedExecutor 
# 是两种核心的任务调度器,用于管理节点回调函数的执行。
from rclpy.executors import MultiThreadedExecutor 

class ndModbusRTUServer(Node):
    def __init__(self):
        super().__init__('modbus_rtu_server')
        # 添加参数变更回调
        #self.add_on_set_parameters_callback(self.param_callback)
        # 参数配置
        self.declare_parameters(
            namespace='',
            parameters=[
                ('port', '/dev/ttyUSB0'),
                ('method', 'rtu'),
                ('baudrate', 9600),
                ('timeout', 0.1),
                ('stopbits', 1),
                ('bytesize', 8),
                ('parity', "N")  # 默认值设置为字符串
            ]
        )

        # # 硬编码参数
        # modbus_config = {
        #     'port': '/dev/ttyUSB0',
        #     'method': 'rtu',
        #     'baudrate': 9600,
        #     'timeout': 0.1,
        #     'stopbits': 1,
        #     'bytesize': 8,
        #     'parity': 'N'  # 确保是字符串
        # }
        
        # # 初始化Modbus客户端
        # self.client_mb = ModbusClient(**modbus_config)
        
        # 初始化Modbus客户端
        self.client_mb = ModbusClient(
            method=self.get_parameter('method').value,
            port=self.get_parameter('port').value,
            baudrate=self.get_parameter('baudrate').value,
            timeout=self.get_parameter('timeout').value,
            stopbits=self.get_parameter('stopbits').value,
            bytesize=self.get_parameter('bytesize').value,
            parity=self.get_parameter('parity').value
        )

        if not self.client_mb.connect():
            self.get_logger().error("Modbus连接失败!")
            raise RuntimeError("Modbus连接失败")

        # 创建互斥回调组
        self.mutex_callback_group = MutuallyExclusiveCallbackGroup()
        # 创建服务
        self.srv = self.create_service(
            ModbusRTU, 
            '/modbus_rtu_service', 
            self.handle_request,
            callback_group=self.mutex_callback_group) #共享资源 串口
        self.get_logger().info("Modbus RTU服务已启动...")

    # def param_callback(self, params):
    #     """参数回调函数"""
    #     results = []
    #     for param in params:
    #         # 检查 parity 参数
    #         if param.name == 'parity':
    #             if not isinstance(param.value, str):
    #                 # 拒绝无效类型
    #                 result = rclpy.parameter.SetParametersResult()
    #                 result.successful = False
    #                 result.reason = f"参数 'parity' 必须是字符串类型,当前类型为 {type(param.value)}"
    #                 results.append(result)
    #                 continue
                
    #             # 检查有效值
    #             if param.value not in ['N', 'E', 'O']:
    #                 result = rclpy.parameter.SetParametersResult()
    #                 result.successful = False
    #                 result.reason = "无效的校验位值,必须是 'N', 'E' 或 'O'"
    #                 results.append(result)
    #                 continue
            
    #         # 接受其他参数
    #         result = rclpy.parameter.SetParametersResult(successful=True)
    #         results.append(result)
        
    #     return results
    
    def handle_request(self, request, response):
        result = None
        try:
            # 根据功能码处理不同请求
            if request.function_code == 1:  # 读线圈状态
                result = self.client_mb.read_coils(
                    address=request.address,
                    count=request.count,
                    slave=request.slave_id
                )
                response.registers = [1 if bit else 0 for bit in result.bits]
                
            elif request.function_code == 2:  # 读离散输入
                result = self.client_mb.read_discrete_inputs(
                    address=request.address,
                    count=request.count,
                    slave=request.slave_id
                )
                response.registers = [1 if bit else 0 for bit in result.bits]
                
            elif request.function_code == 3:  # 读保持寄存器
                result = self.client_mb.read_holding_registers(
                    address=request.address,
                    count=request.count,
                    slave=request.slave_id
                )
                response.registers = result.registers
                
            elif request.function_code == 4:  # 读输入寄存器
                result = self.client_mb.read_input_registers(
                    address=request.address,
                    count=request.count,
                    slave=request.slave_id
                )
                response.registers = result.registers
                
            elif request.function_code == 5:  # 写单个线圈
                result = self.client_mb.write_coil(
                    address=request.address,
                    value=request.value,
                    slave=request.slave_id
                )
                response.registers = []
                
            elif request.function_code == 6:  # 写单个寄存器
                result = self.client_mb.write_register(
                    address=request.address,
                    value=request.value,
                    slave=request.slave_id
                )
                response.registers = []
                
            elif request.function_code == 16:  # 写多个寄存器
                result = self.client_mb.write_registers(
                    address=request.address,
                    values=request.registers,
                    slave=request.slave_id
                )
                response.registers =[]
                
            else:
                raise ValueError(f"不支持的功能码: {request.function_code}")

            # 检查Modbus异常响应
            if isinstance(result, ExceptionResponse):
                raise ModbusException(f"从站返回异常: {result}")
            
            if result.isError():
                response.success = False
                response.message = "{result.message}"   
            else:
                response.success = True
                response.message = "通信成功"
            
        except ModbusException as e:
            response.success = False
            response.message = f"Modbus错误: {str(e)}"
        except Exception as e:
            response.success = False
            response.message = f"{str(e)} <<< {result.message}"
            
        return response

def main(args=None):
    # rclpy.init(args=args)
    # nd_server = ndModbusRTUServer()
    # try:
    #     rclpy.spin(nd_server)
    # except KeyboardInterrupt:
    #     pass
    # finally:
    #     nd_server.client.close()
    #     nd_server.destroy_node()
    #     rclpy.shutdown()
    rclpy.init(args=args)
    try:
        server = ndModbusRTUServer()
        executor = MultiThreadedExecutor(num_threads=4)
        executor.add_node(server)
        
        try:
            executor.spin()
        except KeyboardInterrupt:
            server.get_logger().info("Shutting down...")
        finally:
            server.destroy_node()
    finally:
        rclpy.shutdown()

if __name__ == '__main__':
    main()
View Code

 

节点2:

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.qos import QoSProfile, ReliabilityPolicy,DurabilityPolicy

from pkg_interfaces.srv import ModbusRTU
from std_msgs.msg import String
from concurrent.futures import ThreadPoolExecutor
import threading
from collections import deque
import json

import logging
#设置调试信息打印
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s.%(msecs)03d - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
    #datefmt='%Y-%m-%d %H:%M:%S'
    datefmt='%H:%M:%S'
)
class MultiRequestModbusClient(Node):
    def __init__(self):
        super().__init__('multi_request_modbus_client')
        
        # 参数配置
        self.declare_parameters(namespace='',
            parameters=[
                ('max_workers', 4),
                ('max_retries', 3),
                ('request_timeout', 2.0),
                ('queue_size', 100),
                ('qos_profile', 10)
            ])
        
        # 初始化服务客户端
        self.modbus_client = self.create_client(
            ModbusRTU,
            '/modbus_rtu_service',
            callback_group=ReentrantCallbackGroup()
        )
        while not self.modbus_client.wait_for_service(timeout_sec=1.0):
            logging.info('等待Modbus服务上线...')
        
        # 消息处理队列
        self.msg_queue = deque(maxlen=self.get_parameter('queue_size').value)
        self.queue_lock = threading.Lock()
        
        # 工作线程池(仅用于耗时操作)
        self.worker_pool = ThreadPoolExecutor(
            max_workers=self.get_parameter('max_workers').value
        )
        # 配置QoS
        qos_profile = QoSProfile(
            reliability= ReliabilityPolicy.RELIABLE,
            durability = DurabilityPolicy.VOLATILE,
            depth= self.get_parameter('qos_profile').value
        )
        # 创建订阅者
        self.subscription = self.create_subscription(
            String,
            '/motors_commands',
            self.message_callback,
            qos_profile=qos_profile,
            callback_group=ReentrantCallbackGroup()
        )
        # 发布者
        # 创建发布者,发布到'/motors_status'主题,消息类型String,队列大小10
        self.publisher = self.create_publisher(
            String, 
            '/motors_status', 
            qos_profile=qos_profile,
            callback_group=ReentrantCallbackGroup())  # 使用自定义QoS

        logging.info("节点初始化完成")
    def publish_status(self, msg_str):
        """发布状态方法"""
        msg = String()  # 创建String消息
        msg.data = msg_str  # 设置消息数据
        self.publisher.publish(msg)  # 发布消息
        logging.info(f'发布状态: {msg_str}')  # 记录日志 

    def message_callback(self, msg):
        """非阻塞消息回调"""
        try:
            data = json.loads(msg.data)
            with self.queue_lock:
                self.msg_queue.append(data)
            logging.info(data)
            # 提交处理任务
            self.worker_pool.submit(self.process_message)
            
        except json.JSONDecodeError:
            logging.error(f"无效JSON格式: {msg.data}")
        except Exception as e:
            logging.error(f"消息处理异常: {str(e)}")

    def process_message(self):
        """处理队列中的消息"""
        with self.queue_lock:
            if not self.msg_queue:
                return
            data = self.msg_queue.popleft()
        
        try:
            # 构造Modbus请求
            request = ModbusRTU.Request()
            request.slave_id = data.get('device_id', 2)
            request.address = data.get('address', 0x1000)
            request.function_code = data.get('function_code', 3)
            request.count = data.get('count', 0x1a)
            
            # 异步调用服务
            future = self.modbus_client.call_async(request)
            future.add_done_callback(
                lambda f: self.handle_response(f, data)
            )
            
        except Exception as e:
            logging.error(f"请求构造失败: {str(e)}")

    def handle_response(self, future, original_msg):
        """处理服务响应"""
        try:
            response = future.result()
            if response.success:
                logging.info(
                    f"请求成功 [ID:{original_msg.get('device_id')}] "
                    f"数据: {response.registers}"
                )
                self.publish_status(f"数据: {response.registers}")
            else:
                logging.warning(
                    f"请求失败: {response.message}"
                )
        except Exception as e:
            logging.error(f"响应处理异常: {str(e)}")

    def destroy_node(self):
        """安全的资源清理"""
        logging.info("正在关闭节点...")
        try:
            if hasattr(self, 'worker_pool'):
                self.worker_pool.shutdown(wait=True)
        except Exception as e:
            logging.error(f"关闭线程池时出错: {str(e)}")
        finally:
            super().destroy_node()

def main(args=None):
    rclpy.init(args=args)
    
    try:
        # 创建节点
        node = MultiRequestModbusClient()
        
        # 使用多线程执行器
        executor = MultiThreadedExecutor(num_threads=4)
        executor.add_node(node)
        
        try:
            executor.spin()
        except KeyboardInterrupt:
            node.get_logger().info("接收到终止信号")
        finally:
            node.destroy_node()
            executor.shutdown()
            
    except Exception as e:
        print(f"节点启动失败: {str(e)}")
    finally:
        rclpy.shutdown()

if __name__ == '__main__':
    main()
View Code

 

节点3:

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup
from rclpy.qos import QoSProfile, ReliabilityPolicy,DurabilityPolicy
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup
from std_msgs.msg import Header
from std_msgs.msg import String

from pkg_interfaces.srv import TrajectoryRTU
import random
import time
import array
import json
from typing import Dict, Any, Union, List, Optional
import logging
#设置调试信息打印
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s.%(msecs)03d - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
    #datefmt='%Y-%m-%d %H:%M:%S'
    datefmt='%H:%M:%S'
)

###################################################################################
# ROS2 消息  字典  JSON字符串  JSON文件操作与转换
class ROS2MSG_2JSON:
    """
    支持:ROS2消息(支持基本类型和数组)与字典、JSON转换
         
        ROS2消息<--->字典<--->JSON字符串
    
    必须库:
    import json    #json数据处理
    import array   # 导入array模块
    from typing import Dict, Any, Union, List, Optional  # 类型检查
    """    
    """
    支持:ROS2消息(支持基本类型和数组)与字典、JSON转换
         
        ROS2消息<--->字典<--->JSON字符串
    
    必须库:
    import json    #json数据处理
    import array   # 导入array模块
    from typing import Dict, Any, Union, List, Optional  # 类型检查
    """    
    @staticmethod
    def ros_message_to_dict(msg) -> Dict[str, Any]:
        """ROS2消息转字典(支持基本类型和数组)
        
        Args:
            msg: ROS2消息对象
            
        Returns:
            字典,数组自动转为list
            
        Raises:
            TypeError: 输入不是ROS2消息时
        """
        if not hasattr(msg, 'get_fields_and_field_types'):
            raise TypeError("Input is not a ROS2 message")

        result = {}
        # 获取消息字段信息
        fields = msg.get_fields_and_field_types().keys()
        
        for field in fields:
            value = getattr(msg, field)
            
            # 处理数组类型(包括array.array)
            if isinstance(value, (list, array.array)):
                result[field] = list(value)
            # 处理基本类型
            else:
                result[field] = value
                
        return result
    @staticmethod
    def dict_to_ros_message(msg_type, data: Dict[str, Any]):
        """字典转ROS2消息(支持基本类型和数组)
        
        Args:
            msg_type: ROS2消息类(如TrajectoryRTU.Request)
            data: 字典数据
            
        Returns:
            构建好的ROS2消息对象
            
        Raises:
            TypeError: 输入不是ROS2消息类时
        """
        if not hasattr(msg_type, 'get_fields_and_field_types'):
            raise TypeError("Input is not a ROS2 message class")

        msg = msg_type()
        fields = msg.get_fields_and_field_types().keys()
        
        for field in fields:
            if field not in data:
                continue
                
            value = data[field]
            current_value = getattr(msg, field)
            
            # 处理数组字段
            if isinstance(current_value, (list, array.array)):
                container = getattr(msg, field)
                del container[:]
                if isinstance(value, (list, tuple)):
                    container.extend(value)
            # 处理基本类型
            else:
                setattr(msg, field, value)
                
        return msg
    @staticmethod
    def set_ros_attrs(
        ros_msg_obj: Union[TrajectoryRTU.Request, TrajectoryRTU.Response],
        data_dict: Dict[str, Any]
    ) -> None:
        """安全设置ROS2消息的属性值
        
        Args:
            ros_msg_obj: ROS2消息对象
            data_dict: 包含属性值的字典
            
        Raises:
            ValueError: 如果输入参数无效
            TypeError: 如果类型不匹配
        """
        if not ros_msg_obj or not data_dict:
            raise ValueError("ros_msg_obj and data_dict cannot be None")
            
        if not isinstance(data_dict, dict):
            raise TypeError("data_dict must be a dictionary")

        for field, value in data_dict.items():
            if not hasattr(ros_msg_obj, field):
                continue
                
            attr = getattr(ros_msg_obj, field)
            
            if isinstance(attr, (list, array.array)):
                if not isinstance(value, (list, tuple, array.array)):
                    raise TypeError(f"Field {field} requires sequence type, got {type(value)}")
                del attr[:]#attr.clear()
                attr.extend(value)
            else:
                setattr(ros_msg_obj, field, value)
    @staticmethod
    def get_ros_attr(
        ros_msg_obj: Union[TrajectoryRTU.Request, TrajectoryRTU.Response],
        field: str
    ) -> Union[Any, List[Any]]:
        """安全获取ROS2消息的属性值
        
        Args:
            ros_msg_obj: ROS2消息对象
            field: 要获取的字段名
            
        Returns:
            字段值,数组类型会转换为list
            
        Raises:
            ValueError: 如果输入参数无效
            AttributeError: 如果字段不存在
        """
        if not ros_msg_obj or not field:
            raise ValueError("ros_msg_obj and field cannot be None")
            
        if not hasattr(ros_msg_obj, field):
            raise AttributeError(f"Field {field} does not exist")
            
        attr = getattr(ros_msg_obj, field)
        return list(attr) if isinstance(attr, (list, array.array)) else attr
    @staticmethod
    def dict_to_jsonStr(data_dict: Dict[str, Any]) -> Optional[str]:
        """字典转JSON字符串
        
        Args:
            data_dict: 要转换的字典
            
        Returns:
            JSON字符串或None(转换失败时)
        """
        if not isinstance(data_dict, dict):
            return None
            
        try:
            # 字典转JSON(ensure_ascii=False确保支持非ASCII字符)
            json_str = json.dumps(
                data_dict,
                ensure_ascii=False,
                indent=None  # 紧凑格式
            )
            return json_str
            #return json.dumps(data_dict)
        except (TypeError, ValueError) as e:
            logging.debug(f"JSON serialization error: {e}")
            return None
    @staticmethod
    def jsonStr_to_dict(json_str: str) -> Optional[Dict[str, Any]]:
        """JSON字符串转字典
        
        Args:
            json_str: JSON格式的字符串
            
        Returns:
            字典或None(解析失败时)
        """
        if not isinstance(json_str, str):
            return None
            
        try:
            return json.loads(json_str)
        except json.JSONDecodeError as e:
            logging.debug(f"JSON parse error: {e}")
            return None
        except TypeError as e:
            logging.debug(f"Invalid JSON format: {e}")
            return None
    @staticmethod
    def dict_to_jsonf(file_path: str, data_dict: Dict[str, Any], indent: Optional[int] = 4) -> bool:
        """
        将字典保存为 JSON 文件。

        Args:
            file_path (str): JSON 文件路径(如 "data/output.json")。
            data_dict (Dict[str, Any]): 要保存的字典数据。
            indent (Optional[int]): JSON 缩进空格数,默认为 4。设为 None 取消格式化。

        Returns:
            bool: 成功返回 True,失败返回 False。

        Examples:
            >>> data = {"name": "张三", "age": 30}
            >>> dict_to_json("data.json", data)
        """
        try:
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(data_dict, f, ensure_ascii=False, indent=indent)
            return True
        except (IOError, TypeError, json.JSONEncodeError) as e:
            logging.error(f"Failed to save dict to JSON: {e}", exc_info=True)
            return False
    @staticmethod
    def jsonf_to_dict(file_path: str) -> Optional[Dict[str, Any]]:
        """
        从 JSON 文件读取数据并返回字典。

        Args:
            file_path (str): JSON 文件路径。

        Returns:
            Optional[Dict[str, Any]]: 成功返回字典,失败返回 None。

        Examples:
            >>> data = json_to_dict("data.json")
            >>> print(data)
        """
        try:
            with open(file_path, "r", encoding="utf-8") as f:
                return json.load(f)
        except (FileNotFoundError, json.JSONDecodeError, UnicodeDecodeError) as e:
            logging.error(f"Failed to load JSON to dict: {e}", exc_info=True)
            return None
    @staticmethod
    def ROS2MSG_2JSON_GETCFG()-> Optional[Dict[str, Any]]:
        file_path = "x_y_z_claw_cfg_dict.json"
        recovered_dict = ROS2MSG_2JSON.jsonf_to_dict(file_path=file_path)
        print("Json file recovere dict:",  type(recovered_dict))       
        return recovered_dict
    @staticmethod
    def ROS2MSG_2JSON_SETCFG():
        # 按照ROS2接口文件设置 key名称 和 value类型
        x_y_z_claw_cfg_dict ={
            'id_group': 1,#必须是0-255
            'id_act': 2,#必须是0-255
            'joint_name': ["x_axis", "y_axis", "z_axis","claw"],# 按照ROS2接口文件设置
            'joint_id': [2, 3, 4, 5],#必须是1-255
            'fun_code': [0, 0, 0, 0],#必须是0-255  
            'displace': [0.5, 0.3, 0.1, float(0x00FF)],#必须是float,带小数点
            'speed_rpm': [100, 100, 100, 0],#必须是0-5000 rpm

            'max_displace':[1.2, 1.8, 0.8,float(0xFF00)], #必须是float,带小数点
            'min_displace':[0.0, 0.0, 0.0,float(0x00FF)],  #必须是float,带小数点  
            # 轨迹使用
            
            #根据零件在物流箱体内,机械爪释放零件时的点组成
            "opposite_vertex_pairs": [[0., 0., 0., 0.], [1., 2., 3., 0.5]],# 物流箱
            "dot_lattice": [1, 2, 3, 1],# 物流箱
            #限GUI 使用
            'gui_tab_mode': 'tab_auto', # 'tab_manual'
        }
        # dict保存为jsonfile
        file_path = "x_y_z_claw_cfg_dict.json"
        ROS2MSG_2JSON.dict_to_jsonf(file_path=file_path,
                                    data_dict=x_y_z_claw_cfg_dict)
    @staticmethod
    def ROS2MSG_2JSON_demo():
        #ROS2MSG_2JSON.ROS2MSG_2JSON_demo()
        ROS2MSG_2JSON.ROS2MSG_2JSON_SETCFG()
        x_y_z_claw_cfg_dict =ROS2MSG_2JSON.ROS2MSG_2JSON_GETCFG()
        # 读取jsonfile到dict
        file_path = "x_y_z_claw_cfg_dict.json"
        recovered_dict = ROS2MSG_2JSON.jsonf_to_dict(file_path=file_path)
        print("Json file recovere dict:",  type(recovered_dict))
        print("Json file recovere dict:", recovered_dict)
        # 创建并初始化Request
        request = TrajectoryRTU.Request()
        ROS2MSG_2JSON.set_ros_attrs(request,data_dict= x_y_z_claw_cfg_dict)
        print("ROS2 interface Request:", type(request))  
        print("ROS2 interface Request:", request)    
        # 转换为字典
        request_dict = ROS2MSG_2JSON.ros_message_to_dict(request)
        print("Request as dict:", type(request_dict))
        print("Request as dict:", request_dict)
        
        # 从字典恢复
        recovered_request = ROS2MSG_2JSON.dict_to_ros_message(TrajectoryRTU.Request, request_dict)
        # 转换为JSONStr
        request_json = ROS2MSG_2JSON.dict_to_jsonStr(request_dict)
        print("Request as JSON Str:", type(request_json))
        print("Request as JSON Str:", request_json)
        
        # 从JSONStr恢复
        recovered_dict = ROS2MSG_2JSON.jsonStr_to_dict(request_json)
        print("Recovered dict:",  type(recovered_dict))
        print("Recovered dict:", recovered_dict)
#########################################################################
# 定义状态枚举
from enum import Enum, auto
class ServiceState(Enum):
    IDLE = auto()
    REQUEST = auto()
    RESPONSE = auto()
# 伺服驱动器状态类
class ServoENUM:
    """伺服系统状态码定义 (使用8位二进制表示,兼容标准状态和扩展标志)"""
    # 基础状态(低4位)
    POWER_READY      = 0b0000_0001  # 0001 主电源正常(就绪基础状态)
    ENABLED          = 0b0000_0010  # 0010 伺服使能标志
    INPOSITION       = 0b0000_0100  # 0100 位置指令完成
    HOME_COMPLETE    = 0b0000_1000  # 1000 原点回归完成
    
    # 复合状态(推荐使用组合)
    ## 完成,等待新命令
    STATUS_READY     = POWER_READY | ENABLED | INPOSITION  # 0b0000_0111 (电源+使能+定位完成)
    ## 移动中
    STATUS_MOVING    = POWER_READY | ENABLED               # 0b0000_0011 (电源+使能)
    STATUS_ERROR     = 0b1000_0000  # 最高位表示错误
    STATUS_TIMEOUT   = 0b1000_0001  # 超时错误(含电源标志)

    ## CMD 命令值
    CMD_NOP  = 0 # 空命令,无操作
    CMD_GOTO = 1 # 坐标移动命令

    CMD_STAT = 2 # 查询设备状态字
    CMD_POSI = 3 # 查询位置坐标
    
    CMD_RESET = 4 # 查询设备状态字
    CMD_REZP  = 5 # 归零点


    # 状态检查方法
    @staticmethod
    def is_ready(status: int) -> bool:
        return (status & 0b0000_0111) == 0b0000_0111
    
    @staticmethod
    def is_moving(status: int) -> bool:
        return (status & 0b0000_0011) == 0b0000_0011 and not (status & 0b1000_0000)
    
    @staticmethod
    def is_error(status: int) -> bool:
        return bool(status & 0b1000_0000)
    
###################################################################################
# 定义状态枚举
class ServiceState(Enum):
    IDLE = auto()
    REQUEST = auto()
    RESPONSE = auto()

class JointsServer_Test_Client(Node):
    def __init__(self):
        super().__init__('jointsserver_test_client')
        self.declare_parameter('qos_profile', 10)
        # 配置自动适应QoS
        qos_profile = QoSProfile(
            reliability= ReliabilityPolicy.RELIABLE,
            durability = DurabilityPolicy.VOLATILE,
            depth= self.get_parameter('qos_profile').value
        )
        self.group_mutex = MutuallyExclusiveCallbackGroup()  # 互斥组

        # 创建发布者,发布到'/motors_status'主题,消息类型String,队列大小10
        self.publisher_motor = self.create_publisher(
            String, 
            '/motors_commands', 
            qos_profile=qos_profile,
            callback_group=self.group_mutex)  # 使用自定义QoS
        
        # 等待话题发布
        while not self._topic_exists('/motors_status'):
            time.sleep(1)
            logging.info("the Topic '/motors_status' from Joints_server node doesnt exist, waiting  ")

        # 创建订阅者,订阅'/motors_commands'主题,收到消息时调用status_callback
        self.subscription_motor = self.create_subscription(
            String,
            '/motors_status',
            self.motors_status_callback,
            qos_profile=qos_profile,
            callback_group=self.group_mutex)  # 使用自定义QoS 
        
        # Use a MutuallyExclusiveCallbackGroup for sequential operations
        self.cb_group = rclpy.callback_groups.MutuallyExclusiveCallbackGroup()
        self.timer = self.create_timer(
            5,  # Longer interval between tests
            self.test_jointsserver_operations,
            callback_group=self.cb_group
        )
        self.test_count = 0

    def _topic_exists(self, topic_name: str) -> bool:
        """检查 Topic 是否存在"""
        topic_info = self.get_topic_names_and_types()
        for name, _ in topic_info:# name, types in topic_info:
            if name == topic_name:
                return True
        return False
    
    def test_jointsserver_operations(self):
         
        self.test_count += 1
        self.get_logger().info(f"\n=== Running Test #{self.test_count} ===")

        id_group =0
        id_act   = 1
        joint_name =["x_axis","y_axis","z_axis","claw"]
        joint_id =[2,3,4,5]
        fun_code =[ServoENUM.CMD_POSI,ServoENUM.CMD_POSI,ServoENUM.CMD_POSI,ServoENUM.CMD_POSI]
        #fun_code =[ServoENUM.CMD_NOP,ServoENUM.CMD_NOP,ServoENUM.CMD_NOP,ServoENUM.CMD_NOP]
        displace =[0.2*(self.test_count%2)+0.4, 0.2*(self.test_count%2)+0.4, 0.0,0]
        speed_rpm =[200,200,100,100]

        request_dict ={
            "id_group":   id_group,
            "id_act":     id_act,
            "joint_name": joint_name,
            "joint_id":   joint_id,  # uint8数组
            "fun_code":   fun_code,  # uint8数组
            "displace":   displace,  # float32数组  array('f',[2.0, 3.0, 1.5])          
            "speed_rpm":  speed_rpm, # uint16数组
        }
        # 时间戳处理(从ROS 2时间转换为uint64)
        now = self.get_clock().now()
        request_dict ["stamp_ns"] = now.nanoseconds  # uint64  
        # 格式JSON字符串
        jsonStr=ROS2MSG_2JSON.dict_to_jsonStr(request_dict )

        # 发布话题
        self.publish_command(jsonStr)
        # 
        #self.send_request(request_dict=request_dict)
    def publish_command(self, msg_str):
        """发布命令方法"""
        msg = String()  # 创建String消息
        msg.data = msg_str  # 设置消息数据
        self.publisher_motor.publish(msg)  # 发布消息
        logging.info(f'发布命令: {msg_str}')  # 记录日志   

    def motors_status_callback(self,msg):
        msg_json = msg.data
        # 格式JSON字符串
        response_dict=ROS2MSG_2JSON.jsonStr_to_dict(msg_json)
        logging.info(f'{ msg_json}')

def main(args=None):
    rclpy.init(args=args)
    test_client = JointsServer_Test_Client()
    try:
        rclpy.spin(test_client)
    except KeyboardInterrupt:
        test_client.get_logger().info("Keyboard interrupt, shutting down...")
    finally:
        test_client.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()
View Code

 

 

节点2:标注

  1 #!/usr/bin/env python3
  2 import rclpy
  3 from rclpy.node import Node
  4 from rclpy.executors import MultiThreadedExecutor
  5 from rclpy.callback_groups import ReentrantCallbackGroup
  6 from rclpy.qos import QoSProfile, ReliabilityPolicy,DurabilityPolicy
  7 
  8 from pkg_interfaces.srv import ModbusRTU
  9 from std_msgs.msg import String
 10 from concurrent.futures import ThreadPoolExecutor
 11 import threading
 12 from collections import deque
 13 import json
 14 
 15 import logging
 16 #设置调试信息打印
 17 logging.basicConfig(
 18     level=logging.INFO,
 19     format='%(asctime)s.%(msecs)03d - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
 20     #datefmt='%Y-%m-%d %H:%M:%S'
 21     datefmt='%H:%M:%S'
 22 )
 23 class MultiRequestModbusClient(Node):
 24     def __init__(self):
 25         super().__init__('multi_request_modbus_client')
 26         
 27         # 参数配置
 28         self.declare_parameters(namespace='',
 29             parameters=[
 30                 ('max_workers', 4),
 31                 ('max_retries', 3),
 32                 ('request_timeout', 2.0),
 33                 ('queue_size', 100),
 34                 ('qos_profile', 10)
 35             ])
 36         
 37         # 初始化服务客户端
 38         self.modbus_client = self.create_client(
 39             ModbusRTU,
 40             '/modbus_rtu_service',
 41             callback_group=ReentrantCallbackGroup()
 42         )
 43         while not self.modbus_client.wait_for_service(timeout_sec=1.0):
 44             logging.info('等待Modbus服务上线...')
 45         
 46         # 消息处理队列
 47         self.msg_queue = deque(maxlen=self.get_parameter('queue_size').value)
 48         self.queue_lock = threading.Lock()
 49         
 50         # 工作线程池(仅用于耗时操作)
 51         self.worker_pool = ThreadPoolExecutor(
 52             max_workers=self.get_parameter('max_workers').value
 53         )
 54         # 配置QoS
 55         qos_profile = QoSProfile(
 56             reliability= ReliabilityPolicy.RELIABLE,
 57             durability = DurabilityPolicy.VOLATILE,
 58             depth= self.get_parameter('qos_profile').value
 59         )
 60         # 创建订阅者
 61         self.subscription = self.create_subscription(
 62             String,
 63             '/motors_commands',
 64             self.message_callback,
 65             qos_profile=qos_profile,
 66             callback_group=ReentrantCallbackGroup()
 67         )
 68         # 发布者
 69         # 创建发布者,发布到'/motors_status'主题,消息类型String,队列大小10
 70         self.publisher = self.create_publisher(
 71             String, 
 72             '/motors_status', 
 73             qos_profile=qos_profile,
 74             callback_group=ReentrantCallbackGroup())  # 使用自定义QoS
 75 
 76         logging.info("节点初始化完成")
 77     def publish_status(self, msg_str):
 78         """发布状态方法"""
 79         msg = String()  # 创建String消息
 80         msg.data = msg_str  # 设置消息数据
 81         self.publisher.publish(msg)  # 发布消息
 82         logging.info(f'发布状态: {msg_str}')  # 记录日志 
 83 
 84     def message_callback(self, msg):
 85         """非阻塞消息回调"""
 86         try:
 87             data = json.loads(msg.data)
 88             with self.queue_lock:
 89                 self.msg_queue.append(data)
 90             logging.info(data)
 91             # 提交处理任务
 92             self.worker_pool.submit(self.process_message)
 93             
 94         except json.JSONDecodeError:
 95             logging.error(f"无效JSON格式: {msg.data}")
 96         except Exception as e:
 97             logging.error(f"消息处理异常: {str(e)}")
 98 
 99     def process_message(self):
100         """处理队列中的消息"""
101         with self.queue_lock:
102             if not self.msg_queue:
103                 return
104             data = self.msg_queue.popleft()
105         
106         try:
107             # 构造Modbus请求
108             request = ModbusRTU.Request()
109             request.slave_id = data.get('device_id', 2)
110             request.address = data.get('address', 0x1000)
111             request.function_code = data.get('function_code', 3)
112             request.count = data.get('count', 0x1a)
113             
114             # 异步调用服务
115             future = self.modbus_client.call_async(request)
116             future.add_done_callback(
117                 lambda f: self.handle_response(f, data)#f参数代表一个已经完成的Future对象
118             )
119             
120         except Exception as e:
121             logging.error(f"请求构造失败: {str(e)}")
122 
123     def handle_response(self, future, original_msg):
124         """处理服务响应"""
125         try:
126             response = future.result()
127             if response.success:
128                 logging.info(
129                     f"请求成功 [ID:{original_msg.get('device_id')}] "
130                     f"数据: {response.registers}"
131                 )
132                 self.publish_status(f"数据: {response.registers}")
133             else:
134                 logging.warning(
135                     f"请求失败: {response.message}"
136                 )
137         except Exception as e:
138             logging.error(f"响应处理异常: {str(e)}")
139 
140     def destroy_node(self):
141         """安全的资源清理"""
142         logging.info("正在关闭节点...")
143         try:
144             if hasattr(self, 'worker_pool'):
145                 self.worker_pool.shutdown(wait=True)
146         except Exception as e:
147             logging.error(f"关闭线程池时出错: {str(e)}")
148         finally:
149             super().destroy_node()
150 
151 def main(args=None):
152     rclpy.init(args=args)
153     
154     try:
155         # 创建节点
156         node = MultiRequestModbusClient()
157         
158         # 使用多线程执行器
159         executor = MultiThreadedExecutor(num_threads=4)
160         executor.add_node(node)
161         
162         try:
163             executor.spin()
164         except KeyboardInterrupt:
165             node.get_logger().info("接收到终止信号")
166         finally:
167             node.destroy_node()
168             executor.shutdown()
169             
170     except Exception as e:
171         print(f"节点启动失败: {str(e)}")
172     finally:
173         rclpy.shutdown()
174 
175 if __name__ == '__main__':
176     main()

 

模板

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.qos import QoSProfile, ReliabilityPolicy,DurabilityPolicy

#from pkg_interfaces.srv import ModbusRTU # 自定义
from pkg_interfaces.srv import TrajectoryRTU # 自定义

from std_msgs.msg import String
from concurrent.futures import ThreadPoolExecutor
import threading
from collections import deque
import json

import logging
#设置调试信息打印
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s.%(msecs)03d - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
    #datefmt='%Y-%m-%d %H:%M:%S'
    datefmt='%H:%M:%S'
)
class MultiRequestModbusClient(Node):
    """ TOPIC订阅GUI命令信息 ----> SERVICE客户端命令关节移动 ---->TOPIC发布信息至GUI"""
    def __init__(self):
        super().__init__('multi_request_modbus_client')
        # 核心变量示例 - 格式化整理
        data_dict = {
            "request": {
                'id_group': 1,                     # uint8 (0-255)
                'id_act': 2,                       # uint8 (0-255)
                'joint_name': ["x_axis", "y_axis", "z_axis", "claw"],  # ROS2的关节标识符号
                'joint_id': [2, 3, 4, 5],          # uint8 (1-255)
                'fun_code': [0, 0, 0, 0],          # uint8 (0-255)
                'displace': [0.5, 0.3, 0.1, float(0x00FF)],  # float32
                'speed_rpm': [100, 100, 100, 0],    # uint16 (0-3000 RPM)
                
                # 安全限制
                'max_displace': [1.2, 1.8, 0.8, float(0xFF00)],  # float32
                'min_displace': [0.0, 0.0, 0.0, float(0x00FF)],  # float32
                
                # 轨迹规划参数
                'opposite_vertex_pairs': [
                    [0.0, 0.0, 0.0, 0.0],          # 起始点
                    [1.0, 2.0, 3.0, 0.5]           # 终止点 (物流箱坐标系)
                ],
                'dot_lattice': [2, 3, 2, 1]        # 物流箱栅格尺寸
            },
            
            "response": {                          # GUI显示数据
                "displace": [0.0, 0.0, 0.0, 0.0],  # 当前坐标
                "status": [0, 0, 0, 0],            # 设备状态码
                "success": [True, False, True, False],  # 操作状态
                "message": ['x', 'y', 'z', 'claw'] # 调试信息
            },
            "pallet_gui": {                        # 托盘可视化数据
                "box_xyz":  [2, 3, 2],             # [col, row, layer],即物流箱栅格尺寸
                "pos_xyz": [0, 0, 0],              # 当前位置
                "part_id": 0                       # 物料ID
            }
        }
        logging.info(data_dict)
        """异步线程池设置"""
        # 消息处理队列
        self.msg_queue = deque(maxlen=self.get_parameter('queue_size').value)
        self.queue_lock = threading.Lock()

        # 工作线程池(仅用于耗时操作)
        self.worker_pool = ThreadPoolExecutor(
            max_workers=self.get_parameter('max_workers').value
        )
        # 参数配置
        self.declare_parameters(namespace='',
            parameters=[
                ('max_workers', 4),
                ('max_retries', 3),
                ('request_timeout', 2.0),
                ('queue_size', 100),
                ('qos_profile', 10)
            ])
        """ROS2 Service Client 关节电机"""
        # 初始化服务客户端
        # 配置QoS
        qos_profile = QoSProfile(
            reliability= ReliabilityPolicy.RELIABLE,
            durability = DurabilityPolicy.VOLATILE,
            depth= self.get_parameter('qos_profile').value
        )
        self.modbus_client = self.create_client(
            String, 
            '/joints_service',
            qos_profile= qos_profile,
            callback_group=ReentrantCallbackGroup()
        )
        while not self.modbus_client.wait_for_service(timeout_sec=1.0):
            logging.info('等待Modbus服务上线...')

        # 配置QoS
        qos_profile = QoSProfile(
            reliability= ReliabilityPolicy.RELIABLE,
            durability = DurabilityPolicy.VOLATILE,
            depth= self.get_parameter('qos_profile').value
        )
        # 创建订阅者
        self.subscription = self.create_subscription(
            String,
            '/motors_commands',
            self.subscribe_message_callback,
            qos_profile=qos_profile,
            callback_group=ReentrantCallbackGroup()
        )
        # 发布者
        # 创建发布者,发布到'/motors_status'主题,消息类型String,队列大小10
        self.publisher = self.create_publisher(
            String,
            '/motors_status',
            qos_profile=qos_profile,
            callback_group=ReentrantCallbackGroup())  # 使用自定义QoS

        logging.info("节点初始化完成")

    def publish_status(self, msg_str):
        """发布状态方法"""
        msg = String()  # 创建String消息
        msg.data = msg_str  # 设置消息数据
        self.publisher.publish(msg)  # 发布消息
        logging.info(f'发布状态: {msg_str}')  # 记录日志

    def subscribe_message_callback(self, msg):
        """非阻塞消息回调"""
        try:
            data = json.loads(msg.data)
            with self.queue_lock:
                 #加入待处理队列在线程中处理
                self.msg_queue.append(data)
            logging.info(data)
            # 作为线程提交处理任务进入线程池  
            self.worker_pool.submit(self.process_message)

        except json.JSONDecodeError:
            logging.error(f"无效JSON格式: {msg.data}")
        except Exception as e:
            logging.error(f"消息处理异常: {str(e)}")

    def process_message(self):
        """处理队列中的消息"""
        with self.queue_lock:
            if not self.msg_queue:
                return
            #待处理队列 按照FIFO取数据 
            data = self.msg_queue.popleft()#提取并删除第一个元素
            #data = self.msg_queue[0]# 查看但不删除第一个元素

        try:
            # 构造ROS2 SERVICE请求
            request = TrajectoryRTU.Request()
            # request.slave_id = data.get('device_id', 2)
            # request.address = data.get('address', 0x1000)
            # request.function_code = data.get('function_code', 3)
            # request.count = data.get('count', 0x1a)

            # 异步调用服务
            future = self.modbus_client.call_async(request)
            future.add_done_callback(
                lambda f: self.handle_service_response(f, data)#f参数代表一个已经完成的Future对象
            )

        except Exception as e:
            logging.error(f"请求构造失败: {str(e)}")

    def handle_service_response(self, future, original_msg):
        """处理服务响应"""
        try:
            response = future.result()
            if response.success:
                logging.info(
                    f"请求成功 [ID:{original_msg.get('device_id')}] "
                    f"数据: {response.registers}"
                )
                self.publish_status(f"数据: {response.registers}")
            else:
                logging.warning(
                    f"请求失败: {response.message}"
                )
        except Exception as e:
            logging.error(f"响应处理异常: {str(e)}")

    def destroy_node(self):
        """安全的资源清理"""
        logging.info("正在关闭节点...")
        try:
            if hasattr(self, 'worker_pool'):
                self.worker_pool.shutdown(wait=True)
        except Exception as e:
            logging.error(f"关闭线程池时出错: {str(e)}")
        finally:
            super().destroy_node()

def main(args=None):
    rclpy.init(args=args)

    try:
        # 创建节点
        node = MultiRequestModbusClient()

        # 使用多线程执行器
        executor = MultiThreadedExecutor(num_threads=4)
        executor.add_node(node)

        try:
            executor.spin()
        except KeyboardInterrupt:
            node.get_logger().info("接收到终止信号")
        finally:
            node.destroy_node()
            executor.shutdown()

    except Exception as e:
        print(f"节点启动失败: {str(e)}")
    finally:
        rclpy.shutdown()

if __name__ == '__main__':
    main()

 

posted @ 2025-07-30 17:00  辛河  阅读(55)  评论(0)    收藏  举报