Skip to content

Commit aec97b0

Browse files
authored
Merge pull request #4 from benjaminwp18/message-serialization
Default ROS serialization
2 parents 5805da8 + 8f68230 commit aec97b0

File tree

2 files changed

+101
-23
lines changed

2 files changed

+101
-23
lines changed

mqtt_ros_bridge/bridge_node.py

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
1-
from typing import Any
1+
from typing import Any, TypeVar, Callable, Generic
22
from dataclasses import dataclass
33

44
import rclpy
55
from rclpy.node import Node
66
from rclpy.publisher import Publisher
77
from rclpy.subscription import Subscription
8+
from rclpy._rclpy_pybind11 import RMWError
89

910
from std_msgs.msg import String
1011

1112
import paho.mqtt.client as MQTT
1213

14+
from mqtt_ros_bridge.serializer import Serializer, ROSDefaultSerializer
15+
16+
17+
T = TypeVar('T')
18+
1319

1420
@dataclass
15-
class TopicInfo():
21+
class TopicInfo(Generic[T]):
1622
"""Metadata about a single topic."""
1723

1824
name: str
25+
msg_type: T
26+
serializer: type[Serializer]
1927
publish_on_ros: bool
2028

2129

22-
TOPICS: list[TopicInfo] = [
23-
TopicInfo('pub_topic', True),
24-
TopicInfo('sub_topic', False)
25-
]
30+
TOPICS: dict[str, TopicInfo] = {
31+
'pub_topic': TopicInfo('pub_topic', String, ROSDefaultSerializer, True),
32+
'sub_topic': TopicInfo('sub_topic', String, ROSDefaultSerializer, False)
33+
}
34+
35+
MQTT_PORT = 1883
36+
MQTT_KEEPALIVE = 60
2637

2738

2839
class BridgeNode(Node):
@@ -31,45 +42,47 @@ class BridgeNode(Node):
3142
def __init__(self) -> None:
3243
super().__init__('mqtt_bridge_node')
3344

34-
print('Creating MQTT ROS bridge node')
45+
self.get_logger().info('Creating MQTT ROS bridge node')
3546

3647
self.mqtt_client = MQTT.Client()
3748
self.mqtt_client.enable_logger()
38-
self.mqtt_client.connect('localhost', port=1883, keepalive=60)
49+
self.mqtt_client.connect('localhost', port=MQTT_PORT, keepalive=MQTT_KEEPALIVE)
3950
self.mqtt_client.loop_start()
4051

4152
self.ros_publishers: dict[str, Publisher] = {}
4253
self.ros_subscriptions: list[Subscription] = []
4354

44-
for topic_info in TOPICS:
55+
for topic_info in TOPICS.values():
4556
if topic_info.publish_on_ros:
46-
publisher = self.create_publisher(String, topic_info.name, 10)
57+
publisher = self.create_publisher(topic_info.msg_type, topic_info.name, 10)
4758
self.ros_publishers[topic_info.name] = publisher
4859
self.mqtt_client.subscribe(topic_info.name)
4960
else:
61+
callback = self.make_ros_callback(topic_info)
5062
subscription = self.create_subscription(
51-
String, topic_info.name, self.make_ros_receiver(topic_info.name), 10)
63+
topic_info.msg_type, topic_info.name, callback, 10)
5264
self.ros_subscriptions.append(subscription)
5365

54-
self.mqtt_client.on_message = self.mqtt_msg_received
66+
self.mqtt_client.on_message = self.mqtt_callback
5567

56-
def make_ros_receiver(self, topic: str):
68+
def make_ros_callback(self, topic_info: TopicInfo[T]) -> Callable[[T], None]:
5769
"""
5870
Create a callback function which re-publishes messages on the same topic in MQTT.
5971
6072
Parameters
6173
----------
62-
topic : str
63-
the topic that the callback will publish on
74+
topic_info : TopicInfo
75+
information about the topic that the callback will publish on
6476
6577
"""
66-
def callback(msg: String):
67-
self.get_logger().info(f"ROS RECEIVED: Topic: '{topic}' Payload: '{msg}'")
68-
self.mqtt_client.publish(topic, msg.data)
78+
def callback(msg: T) -> None:
79+
self.get_logger().info(f'ROS RECEIVED: Topic: "{topic_info.name}" Payload: "{msg}"')
80+
self.mqtt_client.publish(topic_info.name, topic_info.serializer.serialize(msg))
6981

7082
return callback
7183

72-
def mqtt_msg_received(self, _client: MQTT.Client, _userdata: Any, mqtt_msg: MQTT.MQTTMessage):
84+
def mqtt_callback(self, _client: MQTT.Client,
85+
_userdata: Any, mqtt_msg: MQTT.MQTTMessage) -> None:
7386
"""
7487
Re-publish messages from MQTT on the same topic in ROS.
7588
@@ -83,12 +96,19 @@ def mqtt_msg_received(self, _client: MQTT.Client, _userdata: Any, mqtt_msg: MQTT
8396
the message received over MQTT
8497
8598
"""
99+
topic_info = TOPICS[mqtt_msg.topic]
100+
86101
self.get_logger().info(
87-
f"MQTT RECEIVED: Topic: '{mqtt_msg.topic}' Payload: '{mqtt_msg.payload!r}'")
102+
f'MQTT RECEIVED: Topic: "{topic_info.name}" Payload: "{mqtt_msg.payload!r}"')
103+
104+
try:
105+
ros_msg = topic_info.serializer.deserialize(mqtt_msg.payload, topic_info.msg_type)
106+
except RMWError:
107+
self.get_logger().info('Dropping message with bad serialization received from' +
108+
f'MQTT on topic "{mqtt_msg.topic}": "{mqtt_msg.payload!r}"')
109+
return
88110

89-
ros_msg = String()
90-
ros_msg.data = mqtt_msg.payload.decode('utf-8')
91-
self.ros_publishers[mqtt_msg.topic].publish(ros_msg)
111+
self.ros_publishers[topic_info.name].publish(ros_msg)
92112

93113

94114
def main(args=None):

mqtt_ros_bridge/serializer.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Any
2+
from abc import ABC, abstractmethod
3+
4+
from rclpy.serialization import serialize_message, deserialize_message
5+
6+
7+
class Serializer(ABC):
8+
"""Serializes and deserializes ROS messages for transmission over MQTT."""
9+
10+
@staticmethod
11+
@abstractmethod
12+
def serialize(message: Any) -> bytes:
13+
"""
14+
Serialize the provided ROS message to a bytes for MQTT.
15+
16+
Parameters
17+
----------
18+
message : Any
19+
the ROS message to serialize
20+
21+
Returns
22+
-------
23+
bytes
24+
the bytes formed by serializing the ROS message
25+
26+
"""
27+
28+
@staticmethod
29+
@abstractmethod
30+
def deserialize(serialized_message: bytes, message_type: Any) -> Any:
31+
"""
32+
Deserialize the provided bytes into a ROS message of the provided type.
33+
34+
Parameters
35+
----------
36+
serialized_message : bytes
37+
the bytes generated by serializing a ROS message
38+
message_type : Any
39+
the type of the message to create
40+
41+
Returns
42+
-------
43+
Any
44+
the ROS message formed by deserializing the bytes
45+
46+
"""
47+
48+
49+
class ROSDefaultSerializer(Serializer):
50+
"""Serialize and deserialize messages using the default ROS message serializer."""
51+
52+
@staticmethod
53+
def serialize(message: Any) -> bytes:
54+
return serialize_message(message)
55+
56+
@staticmethod
57+
def deserialize(serialized_message: bytes, message_type: Any) -> Any:
58+
return deserialize_message(serialized_message, message_type)

0 commit comments

Comments
 (0)