Skip to content

Commit

Permalink
Merge pull request #25 from groove-x/feature/python3
Browse files Browse the repository at this point in the history
Port from Python 2 to Python 3
  • Loading branch information
ledmonster authored Dec 31, 2020
2 parents 205e524 + bd0ddee commit 0899aec
Show file tree
Hide file tree
Showing 14 changed files with 69 additions and 121 deletions.
10 changes: 5 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ version: 2.1
executors:
python:
docker:
- image: cimg/python:2.7
- image: cimg/python:3.7
auth:
username: $DOCKERHUB_USERNAME
password: $DOCKERHUB_PASSWORD
ros:
docker:
- image: ros:melodic
- image: ros:noetic
auth:
username: $DOCKERHUB_USERNAME
password: $DOCKERHUB_PASSWORD
Expand Down Expand Up @@ -37,7 +37,7 @@ jobs:
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws
ln -s /root/project src/mqtt_bridge
source /opt/ros/melodic/setup.bash
source /opt/ros/noetic/setup.bash
apt update
rosdep update
rosdep install --from-paths src --ignore-src -r -y
Expand All @@ -47,9 +47,9 @@ jobs:
name: run rostest
command: |
cd ~/catkin_ws
source /opt/ros/melodic/setup.bash
source /opt/ros/noetic/setup.bash
source devel/setup.bash
pip install mock
pip3 install mock
rostest mqtt_bridge demo.test
workflows:
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ install(DIRECTORY config
DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/config
)

add_custom_target(install_depends ALL COMMAND "pip" "install" "--user" "-r" "${PROJECT_SOURCE_DIR}/requirements.txt")
add_custom_target(install_depends ALL COMMAND "pip3" "install" "--user" "-r" "${PROJECT_SOURCE_DIR}/requirements.txt")
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ This limitation can be overcome by defining custom bridge class, though.

## Demo

### prepare MQTT broker and client
### Prerequisites

```
$ sudo apt-get install mosquitto mosquitto-clients
$ sudo apt install python3-pip
$ sudo apt install ros-noetic-rosbridge-library
$ sudo apt install mosquitto mosquitto-clients
```

### Install python modules

```bash
$ pip install -r requirements.txt
$ pip3 install -r requirements.txt
```

### launch node
Expand Down Expand Up @@ -126,11 +128,11 @@ If `mqtt/private_path` parameter is set, leading `~/` in MQTT topic path will be

### serializer and deserializer

`mqtt_bridge` uses `json` as a serializer in default. But you can also configure other serializers. For example, if you want to use messagepack for serialization, add following configuration.
`mqtt_bridge` uses `msgpack` as a serializer by default. But you can also configure other serializers. For example, if you want to use json for serialization, add following configuration.

``` yaml
serializer: msgpack:dumps
deserializer: msgpack:loads
serializer: json:dumps
deserializer: json:loads
```

### bridges
Expand Down
4 changes: 2 additions & 2 deletions config/demo_params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ mqtt:
port: 1883
keepalive: 60
private_path: device/001
serializer: msgpack:dumps
deserializer: msgpack:loads
#serializer: json:dumps
#deserializer: json:loads
bridge:
# ping pong
- factory: mqtt_bridge.bridge:RosToMqttBridge
Expand Down
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
--extra-index-url https://rospypi.github.io/simple/
catkin-pkg
geometry-msgs
inject>=3.3.1,<4.0
inject>=4.0
mock
msgpack-python>=0.4.8
paho-mqtt>=1.2
Expand Down
9 changes: 5 additions & 4 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
<buildtool_depend>catkin</buildtool_depend>
<buildtool_depend>python-setuptools</buildtool_depend>

<build_depend>python-pip</build_depend>
<build_depend>python3-pip</build_depend>
<exec_depend>rospy</exec_depend>
<exec_depend>rosbridge_library</exec_depend>
<exec_depend>std_msgs</exec_depend>
<!-- <exec_depend>python-inject-pip</exec_depend> -->
<!-- <exec_depend>python-msgpack</exec_depend> -->
<!-- <exec_depend>python-paho-mqtt-pip</exec_depend> -->
<exec_depend>python3-msgpack</exec_depend>
<!--<exec_depend>python3-paho-mqtt</exec_depend> -->
<exec_depend>python3-pymongo</exec_depend>
<!--<exec_depend>python3-inject</exec_depend> -->

<export>
</export>
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
inject>=3.3.1,<4.0
inject>=4.0
msgpack-python>=0.4.8
paho-mqtt>=1.2
pymongo
3 changes: 1 addition & 2 deletions scripts/mqtt_bridge_node.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
import rospy

from mqtt_bridge.app import mqtt_bridge_node
Expand Down
2 changes: 0 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

from catkin_pkg.python_setup import generate_distutils_setup
from setuptools import setup

Expand Down
11 changes: 4 additions & 7 deletions src/mqtt_bridge/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

import inject
import paho.mqtt.client as mqtt
import rospy
Expand All @@ -11,9 +8,9 @@


def create_config(mqtt_client, serializer, deserializer, mqtt_private_path):
if isinstance(serializer, basestring):
if isinstance(serializer, str):
serializer = lookup_object(serializer)
if isinstance(deserializer, basestring):
if isinstance(deserializer, str):
deserializer = lookup_object(deserializer)
private_path_extractor = create_private_path_extractor(mqtt_private_path)
def config(binder):
Expand Down Expand Up @@ -42,8 +39,8 @@ def mqtt_bridge_node():
mqtt_client = mqtt_client_factory(mqtt_params)

# load serializer and deserializer
serializer = params.get('serializer', 'json:dumps')
deserializer = params.get('deserializer', 'json:loads')
serializer = params.get('serializer', 'msgpack:dumps')
deserializer = params.get('deserializer', 'msgpack:loads')

# dependency injection
config = create_config(
Expand Down
86 changes: 30 additions & 56 deletions src/mqtt_bridge/bridge.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

from abc import ABCMeta, abstractmethod
from abc import ABCMeta
from typing import Optional, Type, Dict, Union

import inject
import paho.mqtt.client as mqtt
Expand All @@ -10,85 +8,66 @@
from .util import lookup_object, extract_values, populate_instance


def create_bridge(factory, msg_type, topic_from, topic_to, **kwargs):
u""" bridge generator function
:param (str|class) factory: Bridge class
:param (str|class) msg_type: ROS message type
:param str topic_from: incoming topic path
:param str topic_to: outgoing topic path
:param (float|None) frequency: publish frequency
:return Bridge: bridge object
def create_bridge(factory: Union[str, "Bridge"], msg_type: Union[str, Type[rospy.Message]], topic_from: str,
topic_to: str, frequency: Optional[float] = None, **kwargs) -> "Bridge":
""" generate bridge instance using factory callable and arguments. if `factory` or `meg_type` is provided as string,
this function will convert it to a corresponding object.
"""
if isinstance(factory, basestring):
if isinstance(factory, str):
factory = lookup_object(factory)
if not issubclass(factory, Bridge):
raise ValueError("factory should be Bridge subclass")
if isinstance(msg_type, basestring):
if isinstance(msg_type, str):
msg_type = lookup_object(msg_type)
if not issubclass(msg_type, rospy.Message):
raise TypeError(
"msg_type should be rospy.Message instance or its string"
"reprensentation")
return factory(
topic_from=topic_from, topic_to=topic_to, msg_type=msg_type, **kwargs)
topic_from=topic_from, topic_to=topic_to, msg_type=msg_type, frequency=frequency, **kwargs)


class Bridge(object):
u""" Bridge base class
:param mqtt.Client _mqtt_client: MQTT client
:param _serialize: message serialize callable
:param _deserialize: message deserialize callable
"""
__metaclass__ = ABCMeta

class Bridge(object, metaclass=ABCMeta):
""" Bridge base class """
_mqtt_client = inject.attr(mqtt.Client)
_serialize = inject.attr('serializer')
_deserialize = inject.attr('deserializer')
_extract_private_path = inject.attr('mqtt_private_path_extractor')


class RosToMqttBridge(Bridge):
u""" Bridge from ROS topic to MQTT
""" Bridge from ROS topic to MQTT
:param str topic_from: incoming ROS topic path
:param str topic_to: outgoing MQTT topic path
:param class msg_type: subclass of ROS Message
:param (float|None) frequency: publish frequency
bridge ROS messages on `topic_from` to MQTT topic `topic_to`. expect `msg_type` ROS message type.
"""

def __init__(self, topic_from, topic_to, msg_type, frequency=None):
def __init__(self, topic_from: str, topic_to: str, msg_type: rospy.Message, frequency: Optional[float] = None):
self._topic_from = topic_from
self._topic_to = self._extract_private_path(topic_to)
self._last_published = rospy.get_time()
self._interval = 0 if frequency is None else 1.0 / frequency
rospy.Subscriber(topic_from, msg_type, self._callback_ros)

def _callback_ros(self, msg):
def _callback_ros(self, msg: rospy.Message):
rospy.logdebug("ROS received from {}".format(self._topic_from))
now = rospy.get_time()
if now - self._last_published >= self._interval:
self._publish(msg)
self._last_published = now

def _publish(self, msg):
payload = bytearray(self._serialize(extract_values(msg)))
def _publish(self, msg: rospy.Message):
payload = self._serialize(extract_values(msg))
self._mqtt_client.publish(topic=self._topic_to, payload=payload)


class MqttToRosBridge(Bridge):
u""" Bridge from MQTT to ROS topic
""" Bridge from MQTT to ROS topic
:param str topic_from: incoming MQTT topic path
:param str topic_to: outgoing ROS topic path
:param class msg_type: subclass of ROS Message
:param (float|None) frequency: publish frequency
:param int queue_size: ROS publisher's queue size
bridge MQTT messages on `topic_from` to ROS topic `topic_to`. MQTT messages will be converted to `msg_type`.
"""

def __init__(self, topic_from, topic_to, msg_type, frequency=None,
queue_size=10):
def __init__(self, topic_from: str, topic_to: str, msg_type: Type[rospy.Message],
frequency: Optional[float] = None, queue_size: int = 10):
self._topic_from = self._extract_private_path(topic_from)
self._topic_to = topic_to
self._msg_type = msg_type
Expand All @@ -101,13 +80,8 @@ def __init__(self, topic_from, topic_to, msg_type, frequency=None,
self._publisher = rospy.Publisher(
self._topic_to, self._msg_type, queue_size=self._queue_size)

def _callback_mqtt(self, client, userdata, mqtt_msg):
u""" callback from MQTT
:param mqtt.Client client: MQTT client used in connection
:param userdata: user defined data
:param mqtt.MQTTMessage mqtt_msg: MQTT message
"""
def _callback_mqtt(self, client: mqtt.Client, userdata: Dict, mqtt_msg: mqtt.MQTTMessage):
""" callback from MQTT """
rospy.logdebug("MQTT received from {}".format(mqtt_msg.topic))
now = rospy.get_time()

Expand All @@ -119,13 +93,13 @@ def _callback_mqtt(self, client, userdata, mqtt_msg):
except Exception as e:
rospy.logerr(e)

def _create_ros_message(self, mqtt_msg):
u""" create ROS message from MQTT payload
:param mqtt.Message mqtt_msg: MQTT Message
:return rospy.Message: ROS Message
"""
msg_dict = self._deserialize(mqtt_msg.payload)
def _create_ros_message(self, mqtt_msg: mqtt.MQTTMessage) -> rospy.Message:
""" create ROS message from MQTT payload """
# Hack to enable both, messagepack and json deserialization.
if self._serialize.__name__ == "packb":
msg_dict = self._deserialize(mqtt_msg.payload, raw=False)
else:
msg_dict = self._deserialize(mqtt_msg.payload)
return populate_instance(msg_dict, self._msg_type())


Expand Down
13 changes: 5 additions & 8 deletions src/mqtt_bridge/mqtt_client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
import paho.mqtt.client as mqtt
from typing import Dict, Callable

import paho.mqtt.client as mqtt

def default_mqtt_client_factory(params):
u""" MQTT Client factory

:param dict param: configuration parameters
:return mqtt.Client: MQTT Client
"""
def default_mqtt_client_factory(params: Dict) -> mqtt.Client:
""" MQTT Client factory """
# create client
client_params = params.get('client', {})
client = mqtt.Client(**client_params)
Expand Down Expand Up @@ -50,7 +47,7 @@ def default_mqtt_client_factory(params):
return client


def create_private_path_extractor(mqtt_private_path):
def create_private_path_extractor(mqtt_private_path: str) -> Callable[[str], str]:
def extractor(topic_path):
if topic_path.startswith('~/'):
return '{}/{}'.format(mqtt_private_path, topic_path[2:])
Expand Down
30 changes: 5 additions & 25 deletions src/mqtt_bridge/util.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from importlib import import_module
from typing import Any, Callable, Dict

import rospy
from rosbridge_library.internal import message_conversion


def lookup_object(object_path, package='mqtt_bridge'):
def lookup_object(object_path: str, package: str='mqtt_bridge') -> Any:
""" lookup object from a some.module:object_name specification. """
module_name, obj_name = object_path.split(":")
module = import_module(module_name, package)
obj = getattr(module, obj_name)
return obj


def monkey_patch_message_conversion():
u""" modify _to_primitive_inst to distinct unicode and str conversion """
from rosbridge_library.internal.message_conversion import (
type_map, primitive_types, string_types, FieldTypeMismatchException,
)
def _to_primitive_inst(msg, rostype, roottype, stack):
# Typecheck the msg
msgtype = type(msg)
if msgtype in primitive_types and rostype in type_map[msgtype.__name__]:
return msg
elif msgtype is unicode and rostype in type_map[msgtype.__name__]:
return msg.encode("utf-8", "ignore")
elif msgtype is str and rostype in type_map[msgtype.__name__]:
return msg.decode("utf-8").encode("utf-8", "ignore")
raise FieldTypeMismatchException(roottype, stack, rostype, msgtype)
message_conversion._to_primitive_inst = _to_primitive_inst


monkey_patch_message_conversion()
extract_values = message_conversion.extract_values
populate_instance = message_conversion.populate_instance
extract_values = message_conversion.extract_values # type: Callable[[rospy.Message], Dict]
populate_instance = message_conversion.populate_instance # type: Callable[[Dict, rospy.Message], rospy.Message]


__all__ = ['lookup_object', 'extract_values', 'populate_instance']
2 changes: 1 addition & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import threading
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from http.server import HTTPServer, BaseHTTPRequestHandler

import pytest

Expand Down

0 comments on commit 0899aec

Please sign in to comment.