diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index cefbdb2..ff95871 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -15,7 +15,7 @@ jobs: submodules: recursive - name: Build SDist run: pipx run build --sdist - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz @@ -23,11 +23,11 @@ jobs: needs: make_sdist runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: artifact path: dist - - uses: pypa/gh-action-pypi-publish@v1.8.10 + - uses: pypa/gh-action-pypi-publish@v1.8.11 with: skip_existing: true user: __token__ diff --git a/README.md b/README.md index e4a452d..81f2078 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ for communication between processes. It is not intended to be a replacement for

+## Why ZeroROS? +See these discussions in [ROS Discourse](https://discourse.ros.org/t/teaching-with-ros-or-zeroros-at-university/32124) and this one in [reddit/ROS](https://www.reddit.com/r/ROS/comments/14kh7pt/teaching_ros_zeroros_at_university_students/). + ## Installation Use pip to install the library: diff --git a/setup.py b/setup.py index 8f9526c..842f1cc 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup( name="zeroros", - version="1.0.3", + version="1.0.5", description="ZeroROS middleware", long_description=long_description, long_description_content_type="text/markdown", diff --git a/src/zeroros/datalogger.py b/src/zeroros/datalogger.py index ffff342..b7a4a1d 100644 --- a/src/zeroros/datalogger.py +++ b/src/zeroros/datalogger.py @@ -20,6 +20,8 @@ def log(self, msg: type[Message]): self.file.write( '{"class": "' + str(type(msg).__name__) + + '", "timestamp": "' + + str(datetime.datetime.utcnow().timestamp()) + '", "message": ' + json.dumps(msg.to_json()) + "}\n" diff --git a/src/zeroros/messages/sensor_msgs.py b/src/zeroros/messages/sensor_msgs.py index 3c05e0c..d3ec87c 100644 --- a/src/zeroros/messages/sensor_msgs.py +++ b/src/zeroros/messages/sensor_msgs.py @@ -5,7 +5,7 @@ class LaserScan(Message): - def __init__(self, ranges=[], intensitites=[]): + def __init__(self, ranges=[], intensitites=[], angles=[]): self.header = Header() self.angle_min = None self.angle_max = None @@ -15,6 +15,7 @@ def __init__(self, ranges=[], intensitites=[]): self.range_min = None self.range_max = None self.ranges = np.array(ranges) + self.angles = np.array(angles) # Change NaNs to range_max if np.isnan(self.ranges).any(): self.ranges[np.isnan(self.ranges)] = self.range_max @@ -32,6 +33,7 @@ def __str__(self): msg += "Range Max: " + str(self.range_max) + "\n" msg += "Ranges: " + str(self.ranges) + "\n" msg += "Intensities: " + str(self.intensities) + "\n" + msg += "Angles: " + str(self.angles) + "\n" return msg def to_json(self): @@ -46,6 +48,7 @@ def to_json(self): "range_max": self.range_max, "ranges": self.ranges.tolist(), "intensities": self.intensities.tolist(), + "angles": self.angles.tolist(), } def from_json(self, msg): @@ -59,3 +62,4 @@ def from_json(self, msg): self.range_max = msg["range_max"] self.ranges = np.array(msg["ranges"]) self.intensities = np.array(msg["intensities"]) + self.angles = np.array(msg["angles"]) diff --git a/src/zeroros/messages/std_msgs.py b/src/zeroros/messages/std_msgs.py index 665450d..eaa944d 100644 --- a/src/zeroros/messages/std_msgs.py +++ b/src/zeroros/messages/std_msgs.py @@ -1,4 +1,6 @@ -import time +import datetime +import numpy as np +import numpy.typing as npt from . import Message @@ -9,7 +11,7 @@ def __init__(self, seq: int = 0, stamp: float = None, frame_id: str = ""): self.frame_id = frame_id self.stamp = stamp if stamp is None: - self.stamp = time.time() + self.stamp = datetime.datetime.utcnow().timestamp() def __str__(self): return "Header:\n - seq={}\n - stamp={}\n - frame_id={})\n".format( @@ -96,3 +98,103 @@ def __init__(self, data: bool = False): def __str__(self): return "Bool:\n - data={}\n".format(self.data) + + +class MultiArrayDimension: + def __init__(self, label: str = "", size: int = 0, stride: int = 0): + self.label = label + self.size = size + self.stride = stride + + def __str__(self): + return ( + "MultiArrayDimension:\n - label={}\n - size={}\n - stride={}\n".format( + self.data["label"], self.data["size"], self.data["stride"] + ) + ) + + +class MultiArrayLayout: + def __init__(self, dim: MultiArrayDimension = None, data_offset: int = 0): + self.dim = dim + self.data_offset = data_offset + + +class Float32MultiArray: + def __init__( + self, layout: MultiArrayLayout = None, data: npt.NDArray[np.float32] = None + ): + self.layout = layout + self.data = data + + # Check if the layout matches the data + if layout is not None and data is not None: + if layout.dim.size != len(data): + raise ValueError( + "The size of the layout does not match the size of the data." + ) + + def __str__(self): + return "Float32MultiArray:\n - layout={}\n - data={}\n".format( + self.layout, self.data + ) + + +class Float64MultiArray: + def __init__( + self, layout: MultiArrayLayout = None, data: npt.NDArray[np.float64] = None + ): + self.layout = layout + self.data = data + + # Check if the layout matches the data + if layout is not None and data is not None: + if layout.dim.size != len(data): + raise ValueError( + "The size of the layout does not match the size of the data." + ) + + def __str__(self): + return "Float64MultiArray:\n - layout={}\n - data={}\n".format( + self.layout, self.data + ) + + +class Int32MultiArray: + def __init__( + self, layout: MultiArrayLayout = None, data: npt.NDArray[np.int32] = None + ): + self.layout = layout + self.data = data + + # Check if the layout matches the data + if layout is not None and data is not None: + if layout.dim.size != len(data): + raise ValueError( + "The size of the layout does not match the size of the data." + ) + + def __str__(self): + return "Int32MultiArray:\n - layout={}\n - data={}\n".format( + self.layout, self.data + ) + + +class Int64MultiArray: + def __init__( + self, layout: MultiArrayLayout = None, data: npt.NDArray[np.int64] = None + ): + self.layout = layout + self.data = data + + # Check if the layout matches the data + if layout is not None and data is not None: + if layout.dim.size != len(data): + raise ValueError( + "The size of the layout does not match the size of the data." + ) + + def __str__(self): + return "Int64MultiArray:\n - layout={}\n - data={}\n".format( + self.layout, self.data + ) diff --git a/src/zeroros/publisher.py b/src/zeroros/publisher.py index 4665aa3..3bb198c 100644 --- a/src/zeroros/publisher.py +++ b/src/zeroros/publisher.py @@ -1,5 +1,7 @@ +import asyncio import json import time +import sys import zmq @@ -7,6 +9,10 @@ from zeroros.topic import validate_topic +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + class Publisher: def __init__( self, @@ -14,8 +20,10 @@ def __init__( message_class: type[Message], ip: str = "127.0.0.1", port: int = 5555, + verbose: bool = False, ): - print("Creating publisher for topic: ", topic) + if verbose: + print("Creating publisher for topic: ", topic) self.topic = validate_topic(topic) self.message_class = message_class self.ip = ip diff --git a/src/zeroros/subscriber.py b/src/zeroros/subscriber.py index 272cff6..0c3a8f7 100644 --- a/src/zeroros/subscriber.py +++ b/src/zeroros/subscriber.py @@ -2,6 +2,7 @@ import json import threading import time +import sys import zmq import zmq.asyncio @@ -10,6 +11,10 @@ from zeroros.topic import validate_topic +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + class Subscriber: def __init__( self, @@ -18,10 +23,13 @@ def __init__( callback_handle: callable, ip: str = "127.0.0.1", port: int = 5556, + verbose: bool = False, ): self.ip = ip self.port = port - print("Subscribing to topic: ", topic) + self.verbose = verbose + if self.verbose: + print("Subscribing to topic: ", topic) self.topic = validate_topic(topic) self.message_class = message_class self.url = "tcp://" + str(self.ip) + ":" + str(self.port) @@ -47,7 +55,8 @@ def listen(self, callback_handle: callable): except Exception as e: print(f"Error for topic {self.topic} on {self.ip}:{self.port}: {e}") time.sleep(0.05) - print("Stopping subscriber") + if self.verbose: + print("Stopping subscriber") # Check if the socket is still open if self.sock.closed is False: self.sock.close()