diff --git a/CMakeLists.txt b/CMakeLists.txt index e12c94b9f02141d7cb8f9277afb0909182917f10..fdd3380862f195decb1c8dd2ef116d189b5b3b93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,9 @@ project(visualising) ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) ## is used, also find other catkin packages find_package(catkin REQUIRED COMPONENTS - rospy + rospy + std_msgs + message_generation ) ## System dependencies are found with CMake's conventions @@ -18,7 +20,7 @@ find_package(catkin REQUIRED COMPONENTS ## Uncomment this if the package has a setup.py. This macro ensures ## modules and global scripts declared therein get installed ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html -# catkin_python_setup() +catkin_python_setup() ################################################ ## Declare ROS messages, services and actions ## @@ -45,11 +47,11 @@ find_package(catkin REQUIRED COMPONENTS ## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) ## Generate messages in the 'msg' folder -# add_message_files( -# FILES +add_message_files( + FILES # Message1.msg # Message2.msg -# ) +) ## Generate services in the 'srv' folder # add_service_files( @@ -66,10 +68,10 @@ find_package(catkin REQUIRED COMPONENTS # ) ## Generate added messages and services with any dependencies listed here -# generate_messages( -# DEPENDENCIES -# std_msgs # Or other packages containing msgs -# ) +generate_messages( + DEPENDENCIES + std_msgs # Or other packages containing msgs +) ################################################ ## Declare ROS dynamic reconfigure parameters ## @@ -104,6 +106,7 @@ catkin_package( # INCLUDE_DIRS include # LIBRARIES visualising # CATKIN_DEPENDS rospy + CATKIN_DEPENDS message_runtime # DEPENDS system_lib ) @@ -157,10 +160,10 @@ include_directories( ## Mark executable scripts (Python etc.) for installation ## in contrast to setup.py, you can choose the destination -# catkin_install_python(PROGRAMS +catkin_install_python(PROGRAMS # scripts/my_python_script -# DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -# ) + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) ## Mark executables for installation ## See http://docs.ros.org/melodic/api/catkin/html/howto/format1/building_executables.html diff --git a/package.xml b/package.xml index 62596d0620ff31982d22eb27fdb0ea3cbf3f8062..14d518d3f10b6e34c678efcdc5141a6b17dc212d 100644 --- a/package.xml +++ b/package.xml @@ -37,13 +37,13 @@ <!-- <build_depend>roscpp</build_depend> --> <!-- <exec_depend>roscpp</exec_depend> --> <!-- Use build_depend for packages you need at compile time: --> - <!-- <build_depend>message_generation</build_depend> --> + <build_depend>message_generation</build_depend> <!-- Use build_export_depend for packages you need in order to build against this package: --> <!-- <build_export_depend>message_generation</build_export_depend> --> <!-- Use buildtool_depend for build tool packages: --> <!-- <buildtool_depend>catkin</buildtool_depend> --> <!-- Use exec_depend for packages you need at runtime: --> - <!-- <exec_depend>message_runtime</exec_depend> --> + <exec_depend>message_runtime</exec_depend> <!-- Use test_depend for packages you need only for testing: --> <!-- <test_depend>gtest</test_depend> --> <!-- Use doc_depend for packages you need only for building documentation: --> diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..ae086d42738c966245e0bbfca0bf685297156a76 --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +from distutils.core import setup +from catkin_pkg.python_setup import generate_distutils_setup + +setup_args = generate_distutils_setup( + packages=['visualising'], + package_dir={'': 'src'} +) + +setup(**setup_args) diff --git a/src/visualising/animation.py b/src/visualising/animation.py new file mode 100755 index 0000000000000000000000000000000000000000..162c2982d6abc6a0a426619433a9d082dc485321 --- /dev/null +++ b/src/visualising/animation.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +import re +from .exception import AnimationException + + +class Animation: + def __init__(self, frames, frame_time, num_iter): + # Set frames, number of frames and number of ensembles. + try: + animation_len = len(frames) + except TypeError: + raise AnimationException("P: -- ERROR -- The animation is empty!") + + if animation_len % 2 != 0: + raise AnimationException("P: -- ERROR -- Animation must contain an even number of frames!") + else: + for frame in frames: + try: + frame_len = len(frame) + except TypeError: + raise AnimationException("P: -- ERROR -- At least one frame is empty!") + + if frame_len != 48: + raise AnimationException("P: -- ERROR -- At least one frame doesn't have exactly 48 values!") + else: + is_numeric = True + is_positive = True + is_small = True + + for value in frame: + is_numeric = is_numeric and isinstance(value, int) + is_positive = is_positive and not (value < 0) + is_small = is_small and value < 256 + + if not is_numeric: + raise AnimationException("P: -- ERROR -- Not all values in the animation are integers!") + elif not is_positive: + raise AnimationException("P: -- ERROR -- Not all values in the animation are positive!") + elif not is_small: + raise AnimationException("P: -- ERROR -- One or more values in the animation are to large!") + else: + self.frames = frames + self.num_frames = animation_len + self.num_ensembles = int(animation_len / 2) + + # Set frame time. + if not isinstance(frame_time, int): + raise AnimationException("P: -- ERROR -- Frame time must be an integer!") + elif frame_time < 0: + raise AnimationException("P: -- ERROR -- Frame time must be positive!") + elif frame_time > 4294967295: + raise AnimationException("P: -- ERROR -- Animation delay is to high!") + else: + self.frame_time = frame_time + + # Set number of iterations. + if not isinstance(num_iter, int): + raise AnimationException("P: -- ERROR -- Number of iterations must be an integer!") + elif num_iter > 255: + raise AnimationException("P: -- ERROR -- Animation has to many iterations!") + elif num_iter <= 0: + raise AnimationException("P: -- ERROR -- Animation must have at least one iteration!") + else: + self.num_iter = num_iter + + @staticmethod + def read_frames_from_file(filename): + try: + file = open(filename, 'r') + except AttributeError: + raise AnimationException("P: -- ERROR -- The file is incorrectly formatted!") + except FileNotFoundError: + raise AnimationException("P: -- ERROR -- The specified file does not exist!") + except TypeError: + raise AnimationException("P: -- ERROR -- No animation file was specified!") + + animation = [] + for line in file: + frame = [] + for value in line.split(','): + frame.append(int(re.search(r'\d+', value).group())) + animation.append(frame) + + return animation + + @staticmethod + def create_empty_ensemble(): + frame1 = [0] * 16 + frame2 = [0] * 16 + return [frame1, frame2] + + @staticmethod + def set_pixel_color(ensemble, pixel, r, g, b): + try: + ensemble_len = len(ensemble) + except TypeError: + raise AnimationException("P: -- ERROR -- Ensemble is empty!") + + if ensemble_len != 2: + raise AnimationException("P: -- ERROR -- Ensemble doesn't contain exactly two frames!") + else: + for frame in ensemble: + try: + frame_len = len(frame) + except TypeError: + raise AnimationException("P: -- ERROR -- At least one frame is empty!") + + if frame_len != 48: + raise AnimationException("P: -- ERROR -- At least one frame doesn't have exactly 48 values!") + else: + is_numeric = True + is_positive = True + is_small = True + + for value in frame: + is_numeric = is_numeric and isinstance(value, int) + is_positive = is_positive and not (value < 0) + is_small = is_small and value < 256 + + if not is_numeric: + raise AnimationException("P: -- ERROR -- Not all values in the ensemble are integers!") + elif not is_positive: + raise AnimationException("P: -- ERROR -- Not all values in the ensemble are positive!") + elif not is_small: + raise AnimationException("P: -- ERROR -- One or more values in the ensemble are to large!") + else: + if not (0 < pixel < 33): + raise AnimationException("P: -- ERROR -- Pixel has to be in the range of 1 to 32!") + elif not (0 < r < 256): + raise AnimationException("P: -- ERROR -- Red channel value has to be in the range of 1 to " + "255!") + elif not (0 < g < 256): + raise AnimationException("P: -- ERROR -- Green channel value has to be in the range of 1 " + "to 255!") + elif not (0 < b < 256): + raise AnimationException("P: -- ERROR -- Blue channel value has to be in the range of 1 " + "to 255!") + else: + if pixel < 16: + index = 0 + else: + index = 1 + pixel = pixel - 16 + + ensemble[index][pixel + 0] = r + ensemble[index][pixel + 1] = g + ensemble[index][pixel + 2] = b + + return ensemble diff --git a/src/visualising/arduino.py b/src/visualising/arduino.py new file mode 100755 index 0000000000000000000000000000000000000000..2ed3d5d7bec57d72948fa0fe01fdfb1d071b7cc1 --- /dev/null +++ b/src/visualising/arduino.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +import time +from . message import FrameMsg, InstrMsg +from . animation import Animation +from . exception import ArduinoException + + +class Arduino: + def __init__(self, connection): + self.connection = connection + + # Translates a 8-bit message from Arduino and prints the translation. + @staticmethod + def print_response(msg): + arduino_msg = { + 240: "A: -- ERROR -- Animation has to many frame ensembles!", + 241: "A: -- ERROR -- No animation is loaded!", + 242: "A: -- ERROR -- Instruction was not recognized!", + 243: "A: -- ERROR -- No data message expected!", + 244: "A: -- ERROR -- Message has wrong format!", + 245: "A: -- ERROR -- Animation is playing!", + 246: "A: -- ERROR -- Animation has no iteration!", + 247: "A: -- ERROR -- Unequal number of frames for the left and right NeoPixel ring!", + 248: "A: -- ERROR -- Animation has no frame ensemble!", + 0: "A: No Response!", + 15: "A: -- SUCCESS -- Waiting for frames to be send!", + 31: "A: -- SUCCESS -- Animation playback has been started!", + 47: "A: -- SUCCESS -- Frame successfully received!", + 63: "A: -- SUCCESS -- Last frame successfully received!", + 79: "A: -- SUCCESS -- Animation successfully played!", + 95: "A: -- SUCCESS -- Animation playback has been paused!", + 111: "A: -- SUCCESS -- Animation playback has been resumed", + } + + try: + print(arduino_msg[int.from_bytes(msg, byteorder='big')]) + except KeyError: + raise ArduinoException("P: -- ERROR -- Unknown response from Arduino!") + + # Creates one instruction messages and multiple frame messages to load an animation + # onto the Arduino. + def load_animation(self, animation): + if animation.num_ensembles > 16: + raise ArduinoException("P: -- ERROR -- Animation hast to many frames!") + else: + self.connection.send_msg(InstrMsg('A', animation).create_arduino_msg()) + msg = self.connection.receive_msg() + + Arduino.print_response(msg) + if bytes(msg) == bytes(b'\x0f'): + for frame in animation.frames: + self.connection.send_msg(FrameMsg(frame).create_arduino_msg()) + msg = self.connection.receive_msg() + + d = 10 + while bytes(msg) != bytes(b'\x2f') and bytes(msg) != bytes(b'\x3f') and d > 0: + d = d - 1 + Arduino.print_response(msg) + self.connection.send_msg(FrameMsg(frame).create_arduino_msg()) + msg = self.connection.receive_msg() + + if d > 0: + Arduino.print_response(msg) + else: + raise ArduinoException("P: -- ERROR -- Unable to send frame message!") + + # Creates instruction message and sends it to the Arduino, + # to play a loaded animation. + def start_playback(self): + self.connection.send_msg(InstrMsg('B').create_arduino_msg()) + Arduino.print_response(self.connection.receive_msg()) + + # Creates instruction message and sends it to the Arduino, + # to pause a playing animation. + def pause_playback(self): + self.connection.send_msg(InstrMsg('C').create_arduino_msg()) + Arduino.print_response(self.connection.receive_msg()) + + # Creates instruction message and sends it to the Arduino, + # to resume playback of a loaded animation. + def resume_playback(self): + self.connection.send_msg(InstrMsg('D').create_arduino_msg()) + Arduino.print_response(self.connection.receive_msg()) + + # Streams an animation to the Arduino. For this purpose, two frames of + # the animation are repeatedly sent to the Arduino. The Arduino plays + # the received frames and then confirms the playback, whereupon new + # frames are sent. + def stream_animation(self, animation): + for _ in range(animation.num_iter): + for i in range(animation.num_ensembles): + ensemble = [animation.frames[2 * i + 0], animation.frames[2 * i + 1]] + self.load_animation(Animation(ensemble, animation.frame_time, 1)) + self.start_playback() + + curr_time = time.time() # Current time in milliseconds. + # Waits 10 times the animation delay for a response. + while bytes(self.connection.receive_msg()) != bytes(b'\x4F'): + if time.time() > curr_time + animation.frame_time / 100: + raise ArduinoException("P: -- ERROR -- Arduino is not responding!") diff --git a/src/visualising/connection.py b/src/visualising/connection.py new file mode 100755 index 0000000000000000000000000000000000000000..7d861ed3e370c48accffa941983ab4f3d0c4175a --- /dev/null +++ b/src/visualising/connection.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +import time +import serial + +from . exception import ConnectionException +from serial.serialutil import SerialException + + +class Connection: + def __init__(self, port, baud): + self.connection = serial.Serial() + self.connection.port = port + self.connection.baudrate = baud + self.connection.timeout = 1 + try: + self.connection.open() # Establishes a serial connection. + except SerialException: + raise ConnectionException("P: -- ERROR -- No serial connection established!") + except FileNotFoundError: + raise ConnectionException("P: -- ERROR -- The specified port does not exist!") + + time.sleep(2) # Prevents errors regarding pyserial. + + # Send a message. + def send_msg(self, msg): + for byte in msg: + self.connection.write(byte) + self.connection.reset_output_buffer() # Clear serial output buffer. + + # Receive a message. + def receive_msg(self): + msg = self.connection.read(1) + self.connection.reset_input_buffer() # Clear serial input buffer. + return msg diff --git a/src/visualising/exception.py b/src/visualising/exception.py new file mode 100755 index 0000000000000000000000000000000000000000..82abeb9b58619c848f5a1d6bc3e3ff01a6af33b3 --- /dev/null +++ b/src/visualising/exception.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +class Error(Exception): + pass + + +class AnimationException(Error): + def __init__(self, message): + self.message = message + + +class ConnectionException(Error): + def __init__(self, message): + self.message = message + + +class ArduinoException(Error): + def __init__(self, message): + self.message = message diff --git a/src/visualising/message.py b/src/visualising/message.py new file mode 100755 index 0000000000000000000000000000000000000000..353426756bef4e748d8578685f52f1da7a29d86c --- /dev/null +++ b/src/visualising/message.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +class FrameMsg: + def __init__(self, frame): + self.frame = frame + + # Creates a 50-bit data Message. The first and last bit are intended for authentication + # as a data message. The remaining 48 bits store exactly one animation frame. + def create_arduino_msg(self): + msg = [bytes('{', 'ascii')] + + for color in self.frame: + msg.append(color.to_bytes(1, byteorder='big')) + + msg.append(bytes('}', 'ascii')) + + return msg + + +class InstrMsg: + def __init__(self, instr, animation=None): + self.instr = instr + self.animation = animation + + # Creates a 50-bit instruction message. Since only 7 bits at most are required for the + # actual message, most of the message consists only of zeros. The first and last bit + # are intended for authentication as an instruction message. + def create_arduino_msg(self): + msg = [bytes('[', 'ascii'), bytes(self.instr, 'ascii')] + + if self.animation is not None: + msg.append(self.animation.num_frames.to_bytes(1, byteorder='big')) + + frame_time_bytes = self.animation.frame_time.to_bytes(4, byteorder='big') + msg.append((frame_time_bytes[0]).to_bytes(1, byteorder='big')) + msg.append((frame_time_bytes[1]).to_bytes(1, byteorder='big')) + msg.append((frame_time_bytes[2]).to_bytes(1, byteorder='big')) + msg.append((frame_time_bytes[3]).to_bytes(1, byteorder='big')) + + msg.append(self.animation.num_iter.to_bytes(1, byteorder='big')) + + for _ in range(41): + msg.append((0).to_bytes(1, byteorder='big')) + else: + for _ in range(47): + msg.append((0).to_bytes(1, byteorder='big')) + + msg.append(bytes(']', 'ascii')) + + return msg