diff --git a/launch/visualiser.launch b/launch/visualiser.launch index d0e9d06ed6f1fb2dfa77190f7fb8f09a4b5c490e..98234399c28a4e49e54c71a0b4308d933ebf24c7 100644 --- a/launch/visualiser.launch +++ b/launch/visualiser.launch @@ -1,13 +1,6 @@ <launch> <node pkg="visualising" type="visualiser" name="visualiser"> - <!-- frequency at which the watchdog aggregates metrics and domains --> - <param name="/wdg_freq" value="0.2" type="double" /> - <!-- - frequency at which the visualisation of the robot state is updated, - must be lesser than 0.2 and only is relevant for the emotion - visualisation - --> - <param name="/vis_freq" value="0.2" type="double" /> + <param name="/vis_freq" value="0.1" type="double" /> <param name="/arduino_port" value="/dev/ttyUSB0" type="str" /> <param name="/arduino_baud" value="57600" type="int" /> @@ -15,7 +8,7 @@ <!-- set visualisation strategy --> <!-- 1: emotion --> <!-- 2: abstract --> - <param name="/visualisation_strategy" value="1" type="int" /> + <param name="/visualisation_strategy" value="2" type="int" /> <!-- example of how to set an aggregation strategy for a metric --> <!-- strategy 1 : Take the highest error level of any metric in a domain. --> @@ -34,6 +27,6 @@ <!-- strategy 1 : Take the highest error level of any domain. --> <!-- strategy 2 : Take the lowes error level of any domain. --> <!-- strategy 0 | default strategy : Take the average error level of every domain. --> - <param name="aggregation_strategy_domains" value="2" type="int" /> + <param name="aggregation_strategy_domains" value="0" type="int" /> </node> </launch> diff --git a/scripts/abstract.py b/scripts/abstract.py index 550b6f4f4108b5b1bdf2edbfd6f3c9bf5fd6804b..0675836a59763e5b30a34d64a6e20f774700e0f4 100755 --- a/scripts/abstract.py +++ b/scripts/abstract.py @@ -1,14 +1,11 @@ #!/usr/bin/env python import argparse -import time - -from visualising.expression.tool import Tool -from visualising.expression.abstract import Abstract +from visualising.communication.animation.animation import Animation +from visualising.communication.connection import Connection from visualising.communication.arduino import Arduino -from visualising.communication.illustration.animation import Animation -from visualising.communication.channel.connection import Connection +from visualising.expression.abstract import Abstract parser = argparse.ArgumentParser(description="script to play an animation") parser.add_argument("-p", "--port", help="port to which the Arduino is connected", type=str, default="/dev/ttyUSB0") @@ -20,17 +17,13 @@ port = args["port"] baud = args["baud"] delay = args["time"] -color = [10, 10, 10] - arduino = Arduino(Connection(port, baud)) -states = [0, 1.0, 0.10, 0.15, 0.20, 0.85, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65, - 0.70, 0.75, 0.80, 0.25, 0.90, 0.05, 0.95] -text = ["A", "CPU", "A", "A", "A", "ARM", "A", "A", "A", "A", "A", "A", "A", "A", +name = ["A", "CPU", "A", "A", "A", "ARM", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "RAM", "A", "CAMERA"] -ensemble = Abstract.cycle(states) -time.sleep(10) +error = [0, 1.0, 0.10, 0.15, 0.20, 0.85, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65, + 0.70, 0.75, 0.80, 0.25, 0.90, 0.05, 0.95] -arduino.stream_animations(Abstract.highlight(ensemble, states, text, 4)) -# arduino.stream_animation(Animation([ensemble], 50, 2)) +ensemble = Abstract.cycle(error) +arduino.stream_animations(Abstract.highlight(ensemble, name, error, 4)) diff --git a/scripts/emotion.py b/scripts/emotion.py index 9c9ead4df7616ff2b7b2f3e76df8e3245f0407cd..384ddcdb329a954259685140365d9083bfe4c797 100755 --- a/scripts/emotion.py +++ b/scripts/emotion.py @@ -2,16 +2,17 @@ import argparse -from visualising.expression.tool import Tool -from visualising.expression.emotion import Emotion +from visualising.communication.animation.color.rgb import RGB +from visualising.communication.animation.animation import Animation +from visualising.communication.connection import Connection from visualising.communication.arduino import Arduino -from visualising.communication.channel.connection import Connection -from visualising.communication.illustration.color.rgb import RGB +from visualising.expression.library.raw import Raw +from visualising.expression.emotion import Emotion parser = argparse.ArgumentParser(description="script to play an animation") parser.add_argument("-p", "--port", help="port to which the Arduino is connected", type=str, default="/dev/ttyUSB0") parser.add_argument("-b", "--baud", help="baud rate of the connection", type=int, default=57600) -parser.add_argument("-f", "--file", help="file in the library directory to be played", type=str, required=True) +parser.add_argument("-f", "--file", help="emotion to be played", type=str, required=True) parser.add_argument("-t", "--time", help="time between ensembles", type=int, default=100) args = vars(parser.parse_args()) @@ -20,16 +21,10 @@ baud = args["baud"] file = args["file"] time = args["time"] -# 50, 10, 10 - color = RGB(10, 0, 0) -print(color.r) -print(color.g) -print(color.b) arduino = Arduino(Connection(port, baud)) - -ensembles = Tool.create_ensembles(file, "emotion") - -arduino.stream_animation(Emotion.build_emotion_parallel(ensembles[0], time, color)) +ensembles = Raw.to_ensemble_list(file, "emotion") +animation = Animation(Emotion.build_parallel(ensembles[0], color), time, 1) +arduino.stream_animation(animation) diff --git a/scripts/play_animation.py b/scripts/play_animation.py index fffbd223678ec980152aec71bff2963e03fca51d..99921bfe6d243bcd51878b8ff5df607d0d769205 100755 --- a/scripts/play_animation.py +++ b/scripts/play_animation.py @@ -2,25 +2,26 @@ import argparse -from visualising.expression.tool import Tool +from visualising.communication.connection import Connection from visualising.communication.arduino import Arduino -from visualising.communication.channel.connection import Connection +from visualising.expression.library.raw import Raw parser = argparse.ArgumentParser(description="script to play an animation") parser.add_argument("-p", "--port", help="port to which the Arduino is connected", type=str, default="/dev/ttyUSB0") parser.add_argument("-b", "--baud", help="baud rate of the connection", type=int, default=57600) -parser.add_argument("-f", "--file", help="file in the library directory to be played", type=str, required=True) +parser.add_argument("-f", "--file", help="file to be played", type=str, required=True) +parser.add_argument("-d", "--dict", help="folder containing the file to be played", type=str, required=True) parser.add_argument("-t", "--time", help="time between ensembles", type=int, default=100) -parser.add_argument("-i", "--iterations", help="number of iterations", type=int, default=1) +parser.add_argument("-i", "--iter", help="number of iterations", type=int, default=1) args = vars(parser.parse_args()) port = args["port"] baud = args["baud"] file = args["file"] +dict = args["dict"] time = args["time"] -iter = args["iterations"] +iter = args["iter"] arduino = Arduino(Connection(port, baud)) -animation = Tool.create_animation(file, "alphabet", time, iter) - +animation = Raw.to_animation(file, dict, time, iter) arduino.stream_animation(animation) diff --git a/scripts/reset_display.py b/scripts/reset_display.py index c2e86e237fa39462e4f7735910a14820088ae32e..10f318528cce1811825eaa51f7fe6a982afe4356 100755 --- a/scripts/reset_display.py +++ b/scripts/reset_display.py @@ -2,8 +2,8 @@ import argparse +from visualising.communication.connection import Connection from visualising.communication.arduino import Arduino -from visualising.communication.channel.connection import Connection parser = argparse.ArgumentParser(description="script to reset the NeoPixel rings") parser.add_argument("-p", "--port", help="port to which the Arduino is connected", type=str, default="/dev/ttyUSB0") @@ -14,5 +14,4 @@ port = args["port"] baud = args["baud"] arduino = Arduino(Connection(port, baud)) - arduino.reset_display() diff --git a/scripts/write.py b/scripts/write.py index 447c9b80e64f92da9d69d0d88ef6eb2f4475b85f..ceb0f9c1dc248dc815676988274e64493e0391f9 100755 --- a/scripts/write.py +++ b/scripts/write.py @@ -2,23 +2,25 @@ import argparse -from visualising.expression.tool import Tool +from visualising.communication.animation.animation import Animation +from visualising.communication.connection import Connection from visualising.communication.arduino import Arduino -from visualising.communication.illustration.animation import Animation -from visualising.communication.channel.connection import Connection +from visualising.expression.abstract import Abstract parser = argparse.ArgumentParser(description="script to play an animation") parser.add_argument("-p", "--port", help="port to which the Arduino is connected", type=str, default="/dev/ttyUSB0") parser.add_argument("-b", "--baud", help="baud rate of the connection", type=int, default=57600) -parser.add_argument("-t", "--time", help="time between ensembles", type=int, default=100) +parser.add_argument("-t", "--time", help="time between ensembles", type=int, default=1000) +parser.add_argument("-w", "--word", help="word to be displayed", type=str, required=True) +parser.add_argument("-r", "--ring", help="ring that is supposed to represent the word", type=str, default="r") args = vars(parser.parse_args()) port = args["port"] baud = args["baud"] time = args["time"] +word = args["word"] +ring = args["ring"] arduino = Arduino(Connection(port, baud)) - -animation = Tool.write("HALLONORMAN") - +animation = Abstract.write(word, ring) arduino.stream_animation(Animation(animation, time, 1)) diff --git a/src/visualising/communication/animation/animation.py b/src/visualising/communication/animation/animation.py index 3ebf2809a2c279f8f5fe6b9236a07bde67b041fd..21712aaf5d4867ef30d082bcbaa11ebd7059247f 100755 --- a/src/visualising/communication/animation/animation.py +++ b/src/visualising/communication/animation/animation.py @@ -1,8 +1,9 @@ #!/usr/bin/env python + class Animation: # The ensemble time is given in milliseconds. - def __init__(self, ensembles, ensemble_time=0, num_iter=1): + def __init__(self, ensembles, ensemble_time, num_iter): self.ensembles = ensembles self._ensemble_time = ensemble_time self._num_iter = num_iter diff --git a/src/visualising/communication/animation/color/rgb.py b/src/visualising/communication/animation/color/rgb.py index 08cab69f159900e3bbb0759374c467d0e578772f..bf8a6d0bb8e55109cb98d0316185aa084430e4a6 100644 --- a/src/visualising/communication/animation/color/rgb.py +++ b/src/visualising/communication/animation/color/rgb.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from visualising.communication.illustration.color.color import Color +from visualising.communication.animation.color.color import Color class RGB(Color): diff --git a/src/visualising/communication/animation/ensemble.py b/src/visualising/communication/animation/ensemble.py index 96b41ce71f7d240e09810687bab307e9dc51fd89..f924cf15aead9f442e21d55543f530e44606c596 100644 --- a/src/visualising/communication/animation/ensemble.py +++ b/src/visualising/communication/animation/ensemble.py @@ -1,11 +1,8 @@ #!/usr/bin/env python -import re -import importlib.resources - -from visualising.illustration.animation.color.rgb import RGB -from visualising.illustration.animation.pixel import Pixel -from visualising.illustration.animation.frame import Frame +from visualising.communication.animation.color.rgb import RGB +from visualising.communication.animation.pixel import Pixel +from visualising.communication.animation.frame import Frame class Ensemble: @@ -13,6 +10,14 @@ class Ensemble: self.l_frame = l_frame self.r_frame = r_frame + def color(self, index): + if not 0 <= index <= 31: + raise ValueError("The parameter index must be an integer between 0 and 31!") + if index < 16: + return self.l_frame.color(index) + else: + return self.r_frame.color(index - 16) + def illuminated(self, index): if not 0 <= index <= 31: raise ValueError("The parameter index must be an integer between 0 and 31!") diff --git a/src/visualising/communication/animation/frame.py b/src/visualising/communication/animation/frame.py index 1c5fe2560257a2b968e1fa618376623f07c720ed..67dbae809ad29fe4b23f63f446d744a3c2dee874 100644 --- a/src/visualising/communication/animation/frame.py +++ b/src/visualising/communication/animation/frame.py @@ -14,6 +14,11 @@ class Frame: raise ValueError("The parameter pixels must be a list of exactly 16 objects of the Pixel class!") self._pixels = pixels + def color(self, index): + if not 0 <= index <= 15: + raise ValueError("The parameter index must be an integer between 0 and 15!") + return self.pixels[index].color + def illuminated(self, index): if not 0 <= index <= 15: raise ValueError("The parameter index must be an integer between 0 and 15!") diff --git a/src/visualising/communication/arduino.py b/src/visualising/communication/arduino.py index 735b3244892e0c2fcb8439e387929cd806d9757d..bbc95d1ad2d9e7fc4cca9c6c0ba7fe72c2ac7403 100755 --- a/src/visualising/communication/arduino.py +++ b/src/visualising/communication/arduino.py @@ -2,10 +2,10 @@ import time -from visualising.communication.channel.message.msg_frame import MsgFrame -from visualising.communication.channel.message.msg_instr import MsgInstr -from visualising.communication.channel.message.response import Response -from visualising.communication.illustration.animation import Animation +from visualising.communication.animation.animation import Animation +from visualising.communication.message.msg_frame import MsgFrame +from visualising.communication.message.msg_instr import MsgInstr +from visualising.communication.message.response import Response class Arduino: @@ -52,9 +52,6 @@ class Arduino: # animation is then started. In addition, it is checked whether the # animation was played successfully. def play_animation(self, animation): - if not isinstance(animation, Animation): - raise TypeError("The parameter animation must be an object of the Animation class!") - self.load_animation(animation) self.start_playback() @@ -73,14 +70,13 @@ class Arduino: # the received ensemble and then confirms the playback, whereupon a new # ensemble is sent. def stream_animation(self, animation): - if not isinstance(animation, Animation): - raise TypeError("The parameter animation must be an object of the Animation class!") - for _ in range(animation.num_iter): for ensemble in animation.ensembles: self.play_animation(Animation([ensemble], animation.ensemble_time, 1)) - # Streams multiple animations in succession to the Arduino. + # Streams multiple animations in succession to the Arduino. This functionality + # makes it possible to send individual ensembles as animations and thus to play + # back individual ensembles with individual ensemble time. def stream_animations(self, animations): for animation in animations: self.stream_animation(animation) diff --git a/src/visualising/communication/channel/connection.py b/src/visualising/communication/connection.py similarity index 91% rename from src/visualising/communication/channel/connection.py rename to src/visualising/communication/connection.py index 332fda5bc3c90084edc841f6c9d7aa0ec3dc5303..d6cf1cc22545259cca5b00bd0304686cdd884d66 100755 --- a/src/visualising/communication/channel/connection.py +++ b/src/visualising/communication/connection.py @@ -3,7 +3,7 @@ import time import serial -from visualising.communication.channel.message.response import Response +from visualising.communication.message.response import Response class ArduinoException(Exception): @@ -55,10 +55,10 @@ class Connection: raise ValueError("The parameter resends must be an integer greater 0!") def evaluate(): - bool = resends > 0 + boolean = resends > 0 for expectation in expectations: - bool = bool and not response.compare(expectation) - return bool + boolean = boolean and not response.compare(expectation) + return boolean while True: self.send_msg(msg) diff --git a/src/visualising/communication/channel/__init__.py b/src/visualising/communication/message/__init__.py similarity index 100% rename from src/visualising/communication/channel/__init__.py rename to src/visualising/communication/message/__init__.py diff --git a/src/visualising/communication/channel/message/message.py b/src/visualising/communication/message/message.py similarity index 100% rename from src/visualising/communication/channel/message/message.py rename to src/visualising/communication/message/message.py diff --git a/src/visualising/communication/channel/message/msg_frame.py b/src/visualising/communication/message/msg_frame.py similarity index 91% rename from src/visualising/communication/channel/message/msg_frame.py rename to src/visualising/communication/message/msg_frame.py index 0a0ebe3fa86ecdfe8f6397236201677124388bcf..f3b2865bceea9605cd8dc7c7d97839262d5b03cb 100644 --- a/src/visualising/communication/channel/message/msg_frame.py +++ b/src/visualising/communication/message/msg_frame.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from visualising.communication.channel.message.message import Message +from visualising.communication.message.message import Message class MsgFrame(Message): diff --git a/src/visualising/communication/channel/message/msg_instr.py b/src/visualising/communication/message/msg_instr.py similarity index 96% rename from src/visualising/communication/channel/message/msg_instr.py rename to src/visualising/communication/message/msg_instr.py index 14b0a1f2301f4447cc6254d2d46159a464190b9a..6d43b2d2edb4059093a095cb5eaa09fcacfd401a 100644 --- a/src/visualising/communication/channel/message/msg_instr.py +++ b/src/visualising/communication/message/msg_instr.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from visualising.communication.channel.message.message import Message +from visualising.communication.message.message import Message class MsgInstr(Message): diff --git a/src/visualising/communication/channel/message/response.py b/src/visualising/communication/message/response.py similarity index 89% rename from src/visualising/communication/channel/message/response.py rename to src/visualising/communication/message/response.py index 78aaeb9310195b262137bb681db73ff1e253832a..53e11dadeec918946ca03e1bfed3e1253f025fca 100644 --- a/src/visualising/communication/channel/message/response.py +++ b/src/visualising/communication/message/response.py @@ -4,7 +4,16 @@ class Response: # Generates an Arduino message, which is composed of a message byte from the Arduino # and a translation of the message byte. def __init__(self, byte): - self.byte = byte + self._byte = byte + self.desc = self.translate() + + @property + def byte(self): + return self._byte + + @byte.setter + def byte(self, byte): + self._byte = byte def translate(self): translation = { diff --git a/src/visualising/expression/abstract.py b/src/visualising/expression/abstract.py index 9aa6fb28f8e8656670caa1f23e7498cc2e6ea962..773c0d683e3468126183735bdf453c1db85c96e9 100644 --- a/src/visualising/expression/abstract.py +++ b/src/visualising/expression/abstract.py @@ -1,95 +1,112 @@ +#!/usr/bin/env python + import time import rospy import numpy as np -from visualising.expression.tool import Tool +from visualising.communication.animation.color.rgb import RGB +from visualising.communication.animation.pixel import Pixel +from visualising.communication.animation.ensemble import Ensemble +from visualising.communication.animation.animation import Animation +from visualising.expression.library.raw import Raw from visualising.expression.expression import Expression -from visualising.communication.illustration.animation import Animation -from visualising.communication.illustration.color.rgb import RGB +from visualising.expression.tool import Tool class Abstract(Expression): def __init__(self, arduino): - self.arduino = arduino - self.playback_start = 0 + self._arduino = arduino + self.displayed = True + + @property + def arduino(self): + return self._arduino + + @arduino.setter + def arduino(self, arduino): + self._arduino = arduino - def react(self, state): - if len(state[1]) > 32: - rospy.logwarn("Only displaying the first 32 metric aggregations!") - state = state[:32] + def react(self, condition): + if self.displayed: + self.displayed = False + + animations = Abstract.highlight(Abstract.cycle(condition.error()), condition.name(), condition.error(), 5) + + self.arduino.stream_animations(animations) + self.displayed = True @staticmethod - def cycle(state_vector): - ensemble = Tool.create_empty_ensemble() + def cycle(error_vector): + empty = Ensemble.empty() index = 0 - for state in state_vector: - color = Tool.state_to_color(state, RGB(0, 50, 0), RGB(50, 0, 0)) - ensemble = Tool.set_pixel_color(ensemble, index, color) + for error_value in error_vector: + color = Tool.value_to_color(error_value, RGB(0, 50, 0), RGB(50, 0, 0)) + empty.replace_pixel(index, Pixel(color)) index = index + 1 + return empty - return ensemble + @staticmethod + def write(text, ring, index=None, color=None): + if not text.isalpha(): + raise ValueError("The parameter text must be a string that only contains alphabetic characters!") + + text = text.upper() + + ensembles = [] + for char in text: + if ring == "l": + filename = "l_" + char + ".txt" + elif ring == "r": + filename = "r_" + char + ".txt" + else: + raise ValueError("The parameter ring must be a character that is either l or r!") + + letter_ensemble = Raw.to_ensemble_list(filename, "alphabet") + if index is not None: + letter_ensemble[0].replace_pixel(index, Pixel(color)) + ensembles = ensembles + letter_ensemble + if index is not None: + empty = Ensemble.empty() + empty.replace_pixel(index, Pixel(color)) + ensembles.append(empty) + else: + ensembles.append(Ensemble.empty()) + return ensembles @staticmethod - def highlight(cycle, states, text, number): - number = min(len(states), number) - highlight = sorted(zip(states, range(len(states))), reverse=True)[:number] + def highlight(generated_cycle, name_vector, error_vector, number): + number = min(len(name_vector), number) + zipped = sorted(zip(error_vector, range(len(error_vector))), reverse=True)[:number] - cycle_animation = Animation([cycle], 5000, 1) + cycle_animation = Animation([generated_cycle], 5000, 1) animations = [cycle_animation] + for tuple in zipped: + highlighted_ring = "" + highlighted_color = None + highlighted_index = None - for tuple in highlight: + empty = Ensemble.empty() ensembles = [] - highlight = Tool.create_empty_ensemble() - ring = "" - highlight_pixel = None - color = None for i in range(32): if tuple[1] != i: - highlight = Tool.set_pixel_color(highlight, i, RGB(0, 0, 0)) + empty.replace_pixel(i, Pixel(RGB(0, 0, 0))) else: - highlight_pixel = i + highlighted_index = i if i < 16: - ring = "l" - color = cycle.l_frame.pixels[i].color + highlighted_ring = "l" else: - ring = "r" - color = cycle.r_frame.pixels[i % 16].color + highlighted_ring = "r" - highlight = Tool.set_pixel_color(highlight, i, color) + highlighted_color = generated_cycle.color(i) + empty.replace_pixel(i, Pixel(highlighted_color)) - ensembles.append(highlight) - ensembles = ensembles + Abstract.write(text[highlight_pixel], ring, highlight_pixel, color) + ensembles.append(empty) + ensembles = ensembles + Abstract.write(name_vector[highlighted_index], highlighted_ring, highlighted_index, + highlighted_color) animations.append(Animation(ensembles, 1000, 1)) animations.append(cycle_animation) - animations.append(Animation([Tool.create_empty_ensemble()], 1000, 1)) + animations.append(Animation([Ensemble.empty()], 1000, 1)) return animations - - @staticmethod - def write(text, ring, highlight=None, color=None): - if not isinstance(text, str) or not text.isalpha(): - raise ValueError("The parameter text must be a string that only contains alphabetic characters!") - - text = text.upper() - - ensembles = [] - for char in text: - if ring == "l": - file = "l_" + char + ".txt" - elif ring == "r": - file = "r_" + char + ".txt" - else: - raise ValueError("The parameter index must be a string that is either l or r!") - - letter = Tool.create_ensembles(file, "alphabet") - if highlight is not None: - letter = [Tool.set_pixel_color(letter[0], highlight, color)] - ensembles = ensembles + letter - if highlight is not None: - ensembles.append(Tool.set_pixel_color(Tool.create_empty_ensemble(), highlight, color)) - else: - ensembles.append(Tool.create_empty_ensemble()) - - return ensembles diff --git a/src/visualising/expression/emotion.py b/src/visualising/expression/emotion.py index 079df3722a56797c6fc52571155b67668108c295..96275acbce67120913dc27d4ac8d45e8f5fe38b6 100644 --- a/src/visualising/expression/emotion.py +++ b/src/visualising/expression/emotion.py @@ -1,78 +1,90 @@ +#!/usr/bin/env python + import time +from visualising.communication.animation.color.rgb import RGB +from visualising.communication.animation.pixel import Pixel +from visualising.communication.animation.ensemble import Ensemble +from visualising.communication.animation.animation import Animation from visualising.expression.library.raw import Raw -from visualising.communication.illustration.ensemble import Ensemble -from visualising.communication.illustration.color.rgb import RGB from visualising.expression.expression import Expression from visualising.expression.tool import Tool class Emotion(Expression): def __init__(self, arduino): - self.arduino = arduino - self.playback_start = 0 - self.refresh = 5 + self._arduino = arduino + self.displayed = True + + self.happy = "emotion_happy.txt" + self.ok = "emotion_ok.txt" + self.angry = "emotion_angry.txt" + + @property + def arduino(self): + return self._arduino - self.happy = "expression_happy.txt" - self.ok = "expression_ok.txt" - self.angry = "expression_angry.txt" + @arduino.setter + def arduino(self, arduino): + self._arduino = arduino - def react(self, state): - if time.time() - self.playback_start > self.refresh: - color = Tool.state_to_color(state[0], RGB(25, 25, 25), RGB(25, 0, 0)) - if state[0] > 0.7: + def react(self, condition): + aggregated_domains = condition.aggregated_domains + if self.displayed: + self.displayed = False + color = Tool.value_to_color(aggregated_domains, RGB(25, 25, 25), RGB(25, 0, 0)) + if aggregated_domains > 0.7: self.visualise(self.angry, color) else: - if state[0] > 0.3: + if aggregated_domains > 0.3: self.visualise(self.ok, color) else: self.visualise(self.happy, color) def visualise(self, filepath, color): still = Raw.to_ensemble_list(filepath, "emotion") - buildup = Tool.build_emotion(still[0], color) + buildup = Emotion.build_parallel(Tool.color_ensemble(still[0], color), color) animation = Animation(buildup, 50, 1) + self.arduino.stream_animation(animation) - self.playback_start = time.time() + self.displayed = True + # The function is given an ensemble. Then an animation is generated that builds + # up the given ensemble. @staticmethod - def build_emotion(still, color): + def build(still, color): ensembles = [] for i in range(32 + 1): empty = Ensemble.empty() - for j in range(i + 1): - if still.illuminated(j) or (j == i and i != 32): - empty.replace_pixel(j, Pixel(color)) - ensembles.append(empty) + if i < 32: + for j in range(i + 1): + if still.illuminated(j) or j == i: + empty.replace_pixel(j, Pixel(color)) + ensembles.append(empty) + else: + ensembles.append(still) + ensembles.append(still) return ensembles + # The function is given an ensemble. Then an animation is generated that builds + # up the given ensemble. The generated animation builds up the given ensemble + # on both NeoPixel rings in parallel. @staticmethod - def build_emotion_parallel(still, color): + def build_parallel(still, color): ensembles = [] for i in range(16 + 1): empty = Ensemble.empty() - for j in range(i + 1): - if still.l_frame.illuminated(j): - empty.replace_pixel(j, Pixel(color)) - if still.r_frame.illuminated(j): - empty.replace_pixel(j, Pixel(color)) - if j == i: - empty.replace_pixel(j, Pixel(color)) - empty.replace_pixel(16 + j, Pixel(color)) - - ensembles.append(ensemble) - - ensemble = Tool.create_empty_ensemble() - for k in range(32): - if k < 16: - illuminated = blueprint.l_frame.illuminated(k) + if i < 16: + for j in range(i + 1): + if still.illuminated(j): + empty.replace_pixel(j, Pixel(color)) + if still.illuminated(j + 16): + empty.replace_pixel(j + 16, Pixel(color)) + if j == i: + empty.replace_pixel(j, Pixel(color)) + empty.replace_pixel(j + 16, Pixel(color)) + ensembles.append(empty) else: - illuminated = blueprint.r_frame.illuminated(k % 16) - - if illuminated: - ensemble = Tool.set_pixel_color(ensemble, k, color) - - ensembles.append(ensemble) - - return Animation(ensembles, time, 1) - + ensembles.append(still) + ensembles.append(still) + return ensembles diff --git a/src/visualising/communication/channel/message/__init__.py b/src/visualising/expression/library/animation/__init__.py similarity index 100% rename from src/visualising/communication/channel/message/__init__.py rename to src/visualising/expression/library/animation/__init__.py diff --git a/src/visualising/expression/library/animation_happy.txt b/src/visualising/expression/library/animation/animation_happy.txt similarity index 100% rename from src/visualising/expression/library/animation_happy.txt rename to src/visualising/expression/library/animation/animation_happy.txt diff --git a/src/visualising/expression/library/animation_ok.txt b/src/visualising/expression/library/animation/animation_ok.txt similarity index 100% rename from src/visualising/expression/library/animation_ok.txt rename to src/visualising/expression/library/animation/animation_ok.txt diff --git a/src/visualising/expression/library/animation_sad.txt b/src/visualising/expression/library/animation/animation_sad.txt similarity index 100% rename from src/visualising/expression/library/animation_sad.txt rename to src/visualising/expression/library/animation/animation_sad.txt diff --git a/src/visualising/expression/library/expression/__init__.py b/src/visualising/expression/library/emotion/__init__.py similarity index 100% rename from src/visualising/expression/library/expression/__init__.py rename to src/visualising/expression/library/emotion/__init__.py diff --git a/src/visualising/expression/library/expression/expression_angry.txt b/src/visualising/expression/library/emotion/emotion_angry.txt similarity index 100% rename from src/visualising/expression/library/expression/expression_angry.txt rename to src/visualising/expression/library/emotion/emotion_angry.txt diff --git a/src/visualising/expression/library/expression/expression_clear.txt b/src/visualising/expression/library/emotion/emotion_clear.txt similarity index 100% rename from src/visualising/expression/library/expression/expression_clear.txt rename to src/visualising/expression/library/emotion/emotion_clear.txt diff --git a/src/visualising/expression/library/expression/expression_happy.txt b/src/visualising/expression/library/emotion/emotion_happy.txt similarity index 100% rename from src/visualising/expression/library/expression/expression_happy.txt rename to src/visualising/expression/library/emotion/emotion_happy.txt diff --git a/src/visualising/expression/library/expression/expression_ok.txt b/src/visualising/expression/library/emotion/emotion_ok.txt similarity index 100% rename from src/visualising/expression/library/expression/expression_ok.txt rename to src/visualising/expression/library/emotion/emotion_ok.txt diff --git a/src/visualising/expression/library/raw.py b/src/visualising/expression/library/raw.py index 4cb1bcd03b532566c15fed4ced324995cfd7184d..a42c9657c113e1d087869a82080bdd544b75a8d2 100644 --- a/src/visualising/expression/library/raw.py +++ b/src/visualising/expression/library/raw.py @@ -3,13 +3,15 @@ import re import importlib.resources -from visualising.illustration.animation.color.rgb import RGB -from visualising.illustration.animation.pixel import Pixel -from visualising.illustration.animation.frame import Frame -from visualising.illustration.animation.ensemble import Ensemble -from visualising.illustration.animation.animation import Animation +from visualising.communication.animation.color.rgb import RGB +from visualising.communication.animation.pixel import Pixel +from visualising.communication.animation.frame import Frame +from visualising.communication.animation.ensemble import Ensemble +from visualising.communication.animation.animation import Animation from visualising.expression.library import alphabet -from visualising.expression.library import expression +from visualising.expression.library import animation +from visualising.expression.library import emotion +from visualising.expression.library import transition class Raw: @@ -19,25 +21,30 @@ class Raw: @staticmethod def to_ensemble_list(filename, directory): def load(): - if directory != "expression" and directory != "alphabet": + if directory != "emotion" and directory != "alphabet" and directory != "animation" \ + and directory != "transition": raise ValueError("The parameter directory describes a unknown directory!") - if directory == "expression": + if directory == "emotion": return importlib.resources.open_text(emotion, filename) if directory == "alphabet": return importlib.resources.open_text(alphabet, filename) + if directory == "animation": + return importlib.resources.open_text(animation, filename) + if directory == "transition": + return importlib.resources.open_text(transition, filename) def to_pixel_list(line): pixels = [] - for (r, g, b) in line.split(","): - pixels.append( - Pixel( - RGB( - int(re.search(r'\d+', r).group()), - int(re.search(r'\d+', g).group()), - int(re.search(r'\d+', b).group()) - ) - ) - ) + i, r, g, b = 0, 0, 0, 0 + for value in line.split(","): + if i % 3 == 0: + r = int(re.search(r'\d+', value).group()) + if i % 3 == 1: + g = int(re.search(r'\d+', value).group()) + if i % 3 == 2: + b = int(re.search(r'\d+', value).group()) + pixels.append(Pixel(RGB(r, g, b))) + i = i + 1 return pixels file = load() diff --git a/src/visualising/expression/library/test.txt b/src/visualising/expression/library/test.txt deleted file mode 100644 index a2d81257e9a9404c7bea1d63cbf57ba2ad023753..0000000000000000000000000000000000000000 --- a/src/visualising/expression/library/test.txt +++ /dev/null @@ -1,2 +0,0 @@ -0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,10,0,0,10,0,0,10,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 -10,0,0,10,0,0,10,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0 diff --git a/src/visualising/expression/library/transition/__init__.py b/src/visualising/expression/library/transition/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/visualising/expression/library/transition_happy_happy.txt b/src/visualising/expression/library/transition/transition_happy_happy.txt similarity index 100% rename from src/visualising/expression/library/transition_happy_happy.txt rename to src/visualising/expression/library/transition/transition_happy_happy.txt diff --git a/src/visualising/expression/tool.py b/src/visualising/expression/tool.py index b38bad83e3b6df9f4342911b318cd9b4b3ac06ef..8e36f36d9405d1107a0508fb4bc820d9ff3b5c5c 100644 --- a/src/visualising/expression/tool.py +++ b/src/visualising/expression/tool.py @@ -2,7 +2,8 @@ import numpy as np -from visualising.communication.illustration.color.rgb import RGB +from visualising.communication.animation.color.rgb import RGB +from visualising.communication.animation.pixel import Pixel class Tool: @@ -10,12 +11,18 @@ class Tool: pass @staticmethod - def state_to_color(state, start_color, end_color): - if not 0 <= state <= 1: - raise ValueError("The parameter state must be a floating point number between 0 and 1!") + def value_to_color(value, start_color, end_color): + if not 0 <= value <= 1: + raise ValueError("The parameter value must be a floating point number between 0 and 1!") - color = (1 - state) * np.array(start_color.export_color()) + state * np.array(end_color.export_color()) + color = (1 - value) * np.array(start_color.export_color()) + value * np.array(end_color.export_color()) color = color.tolist() return RGB(int(color[0]), int(color[1]), int(color[2])) + @staticmethod + def color_ensemble(ensemble, color): + for i in range(32): + if ensemble.illuminated(i): + ensemble.replace_pixel(i, Pixel(color)) + return ensemble diff --git a/src/visualising/monitoring/log/__init__.py b/src/visualising/monitoring/log/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/visualising/monitoring/log/condition.py b/src/visualising/monitoring/log/condition.py new file mode 100644 index 0000000000000000000000000000000000000000..d715a634cc68406433540ee24e24bbc899e077c2 --- /dev/null +++ b/src/visualising/monitoring/log/condition.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +class Condition: + def __init__(self, aggregated_metrics, aggregated_domains): + self._aggregated_metrics = aggregated_metrics + self.aggregated_domains = aggregated_domains + + @property + def aggregated_metrics(self): + return self._aggregated_metrics + + @aggregated_metrics.setter + def aggregated_metrics(self, aggregated_metrics): + if len(aggregated_metrics) > 32: + # Only saving the first 32 metric aggregations! + self._aggregated_metrics = aggregated_metrics[:32] + else: + self._aggregated_metrics = aggregated_metrics + + def name(self): + unzipped = [[i for i, j in self._aggregated_metrics], + [j for i, j in self._aggregated_metrics]] + return unzipped[0] + + def error(self): + unzipped = [[i for i, j in self._aggregated_metrics], + [j for i, j in self._aggregated_metrics]] + return unzipped[1] diff --git a/src/visualising/monitoring/log/log.py b/src/visualising/monitoring/log/log.py new file mode 100644 index 0000000000000000000000000000000000000000..1f3ded4fa52309e59b3d913cf6f03f938d2fbf9f --- /dev/null +++ b/src/visualising/monitoring/log/log.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +from visualising.monitoring.log.condition import Condition + + +class Log: + def __init__(self): + self.domains = [] + self.aggregated_metrics = None + self.aggregated_domains = None + + def add(self, domain): + self.domains.append(domain) + + def exists(self, domain): + for x in self.domains: + if x.compare(domain): + return x + return None + + def max_error(self): + max_error = 0.0 + for domain in self.aggregated_metrics: + if domain[1] > max_error: + max_error = domain[1] + return max_error + + def min_error(self): + min_error = 1.0 + for domain in self.aggregated_metrics: + if domain[1] < min_error: + min_error = domain[1] + return min_error + + def avg_error(self): + count = 0.001 + avg_error = 0.0 + for domain in self.aggregated_metrics: + count = count + 1 + avg_error = avg_error + domain[1] + return avg_error / count + + # TODO: Add more modes! + # TODO: Think about efficiency! + def aggregate_metrics(self, aggregation_strategy_metrics): + aggregated_metrics = [] + for domain in self.domains: + if domain.name in aggregation_strategy_metrics and aggregation_strategy_metrics[domain.name] != 0: + + # Strategy 1: Take the highest error level of any metric in a domain. + if aggregation_strategy_metrics[domain] == 1: + aggregated_metrics.append((domain.name, domain.max_error())) + + # Strategy 2: Take the lowest error level of any metric in a domain. + elif aggregation_strategy_metrics[domain] == 2: + aggregated_metrics.append((domain.name, domain.min_error())) + + else: + raise ValueError("The specified method for aggregating metrics is not recognised!") + + # Default strategy: Take the average error level of every metric in a domain. + else: + aggregated_metrics.append((domain.name, domain.avg_error())) + + self.aggregated_metrics = aggregated_metrics + + # TODO: Add more modes! + def aggregate_domains(self, aggregation_strategy_domains): + # Strategy 1: Take the highest error level of any domain. + if aggregation_strategy_domains == 1: + self.aggregated_domains = self.max_error() + + # Strategy 2: Take the lowes error level of any domain. + elif aggregation_strategy_domains == 2: + self.aggregated_domains = self.min_error() + + # Default strategy: Take the average error level of every domain. + else: + self.aggregated_domains = self.avg_error() + + def condition(self, aggregation_strategy_metrics, aggregation_strategy_domains): + self.aggregate_metrics(aggregation_strategy_metrics) + self.aggregate_domains(aggregation_strategy_domains) + return Condition(self.aggregated_metrics, self.aggregated_domains) diff --git a/src/visualising/monitoring/log/log_domain.py b/src/visualising/monitoring/log/log_domain.py new file mode 100644 index 0000000000000000000000000000000000000000..65c36ee6d9c13bf924658d8cc836ef958d60ab06 --- /dev/null +++ b/src/visualising/monitoring/log/log_domain.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +class Domain: + def __init__(self, name): + self.name = name + self.metrics = [] + + def compare(self, domain): + return self.name == domain.name + + def add(self, metric): + self.metrics.append(metric) + + def exists(self, metric): + for x in self.metrics: + if x.compare(metric): + return x + return None + + def max_error(self): + max_error = 0.0 + for metric in self.metrics: + if metric.error > max_error: + max_error = metric.error + return max_error + + def min_error(self): + min_error = 1.0 + for metric in self.metrics: + if metric.error < min_error: + min_error = metric.error + return min_error + + def avg_error(self): + count = 0.001 + avg_error = 0.0 + for metric in self.metrics: + count = count + 1 + avg_error = avg_error + metric.error + return avg_error / count diff --git a/src/visualising/monitoring/log/log_metric.py b/src/visualising/monitoring/log/log_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..884a0fe0ecdd6ba3ad486fee7197a923b7ca6a98 --- /dev/null +++ b/src/visualising/monitoring/log/log_metric.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +class Metric: + def __init__(self, origin, label, unit): + self.origin = origin # Origin of the monitored metric. + self.label = label # Label of the metric. + self.value = None # Value of the metric. + self.error = None # Error level of the metric. + self.unit = unit # Unit of the metric. + + def compare(self, metric): + return self.origin == metric.origin and self.label == metric.label + + def update(self, value, error): + self.value = value + self.error = error diff --git a/src/visualising/monitoring/watchdog.py b/src/visualising/monitoring/watchdog.py index 2c1eb8cf6a44fe7b8fdf6e86577fff8b63c20ebf..516a5616db4581ec83a040a114b57edc3f18de8c 100755 --- a/src/visualising/monitoring/watchdog.py +++ b/src/visualising/monitoring/watchdog.py @@ -3,6 +3,9 @@ import rospy from monitoring.msg import Monitoring +from visualising.monitoring.log.log_metric import Metric +from visualising.monitoring.log.log_domain import Domain +from visualising.monitoring.log.log import Log class Watchdog: @@ -10,134 +13,31 @@ class Watchdog: self.sub = rospy.Subscriber("/monitoring", Monitoring, self.update_log) # Stores the most current values of each metric. - self.log = {} + self.log = Log() # Strategies that specify how the critical level of each metric should be aggregated in a domain. # It is possible to specify a different aggregation strategy for every single domain. self.aggregation_strategy_metrics = rospy.get_param("/visualiser/aggregation_strategy_metrics") - # Dictionary that stores the aggregated critical levels of the metrics of a domain. - self.aggregated_metrics = {} # Strategy that specifies how the critical levels of all domains should be aggregated. self.aggregation_strategy_domains = rospy.get_param("/visualiser/aggregation_strategy_domains") - # Variable that stores the aggregated critical level of all domains. - self.aggregated_domains = None - # Frequency at which the metrics in the log are aggregated. - wdg_freq = rospy.get_param("/visualiser/wdg_freq", 1.0) - - if not wdg_freq > 0.0: - rospy.logwarn( - "Watchdog: The frequency at which the metrics of the robot are aggregated must be greater then 0! " - "Using 1 as frequency!") - wdg_freq = 1.0 - - self.timer = rospy.Timer(rospy.Duration(1.0 / wdg_freq), self.update_condition) - - # TODO: Think about efficiency! def update_log(self, monitoring): - origin = monitoring.origin - metric = monitoring.metric + new_metric = Metric(monitoring.origin, monitoring.metric.label, monitoring.metric.unit) + new_metric.update(monitoring.metric.value, monitoring.metric.error) - domain = metric.domain - label = metric.label - value = metric.value - unit = metric.unit - error = metric.error + new_domain = Domain(monitoring.metric.domain) + new_domain.add(new_metric) - if domain in self.log: - if origin in self.log[domain]: - self.log[domain][origin].update({label: {"value": value, "unit": unit, "error": error}}) - else: - self.log[domain][origin] = {} - self.log[domain][origin][label] = {"value": value, "unit": unit, "error": error} + domain = self.log.exists(new_domain) + if domain is None: + self.log.add(new_domain) else: - self.log[domain] = {} - self.log[domain][origin] = {} - self.log[domain][origin][label] = {"value": value, "unit": unit, "error": error} - - def update_condition(self, event): - self.aggregate_metrics() - self.aggregate_domains() - rospy.logwarn(self.aggregated_domains) - - # TODO: Add more modes! - def aggregate_metrics(self): - for domain in self.log: - if domain in self.aggregation_strategy_metrics and self.aggregation_strategy_metrics[domain] != 0: - - # Strategy 1: Take the highest error level of any metric in a domain. - if self.aggregation_strategy_metrics[domain] == 1: - highest_error = 0.0 - - for origin in self.log[domain]: - for label in self.log[domain][origin]: - local_error = self.log[domain][origin][label]["error"] - if local_error > highest_error: - highest_error = local_error - - self.aggregated_metrics.update({domain: highest_error}) - - # Strategy 2: Take the lowest error level of any metric in a domain. - elif self.aggregation_strategy_metrics[domain] == 2: - lowest_error = 1.0 - - for origin in self.log[domain]: - for label in self.log[domain][origin]: - local_error = self.log[domain][origin][label]["error"] - if local_error < lowest_error: - lowest_error = local_error - - self.aggregated_metrics.update({domain: lowest_error}) - - else: - rospy.logwarn("Watchdog: The specified method for aggregating metrics is not recognised!") - - # Default strategy: Take the average error level of every metric in a domain. + metric = domain.exists(new_metric) + if metric is None: + domain.add(new_metric) else: - count = 0.001 - aggregated_error = 0.0 - - for origin in self.log[domain]: - for label in self.log[domain][origin]: - count = count + 1 - aggregated_error = aggregated_error + self.log[domain][origin][label]["error"] - - quotient = aggregated_error / count - self.aggregated_metrics.update({domain: quotient}) - - # TODO: Add more modes! - def aggregate_domains(self): - # Strategy 1: Take the highest error level of any domain. - if self.aggregation_strategy_domains == 1: - highest_error = 0.0 - - for domain in self.aggregated_metrics: - local_error = self.aggregated_metrics[domain] - if local_error > highest_error: - highest_error = local_error - - self.aggregated_domains = highest_error - - # Strategy 2: Take the lowes error level of any domain. - elif self.aggregation_strategy_domains == 2: - lowest_error = 1.0 - - for domain in self.aggregated_metrics: - local_error = self.aggregated_metrics[domain] - if local_error < lowest_error: - lowest_error = local_error - - self.aggregated_domains = lowest_error - - # Default strategy: Take the average error level of every domain. - else: - count = 0.001 - aggregated_error = 0.0 - - for domain in self.aggregated_metrics: - count = count + 1 - aggregated_error = aggregated_error + self.aggregated_metrics[domain] + metric.update(monitoring.metric.value, monitoring.metric.error) - quotient = aggregated_error / count - self.aggregated_domains = quotient + def condition(self): + return self.log.condition(self.aggregation_strategy_metrics, self.aggregation_strategy_domains) diff --git a/src/visualising/visualiser.py b/src/visualising/visualiser.py index f1db8e67639c0133e0a329fbdc8ca17e53cb4a6a..47c6cf7b20939a14c3f3330d7ea22e056dcc971a 100644 --- a/src/visualising/visualiser.py +++ b/src/visualising/visualiser.py @@ -2,10 +2,11 @@ import rospy -from visualising.monitoring.watchdog import Watchdog -from visualising.expression.emotion import Emotion +from visualising.communication.connection import Connection from visualising.communication.arduino import Arduino -from visualising.communication.channel.connection import Connection +from visualising.expression.emotion import Emotion +from visualising.expression.abstract import Abstract +from visualising.monitoring.watchdog import Watchdog class Visualiser: @@ -20,7 +21,7 @@ class Visualiser: if rospy.get_param("/visualiser/visualisation_strategy") == 1: self.expression = Emotion(arduino) else: - self.expression = None + self.expression = Abstract(arduino) if not freq > 0.0: rospy.logwarn( @@ -30,6 +31,5 @@ class Visualiser: self.timer = rospy.Timer(rospy.Duration(1.0 / freq), self.visualise) - def visualise(self, event): - state = [self.watchdog.aggregated_domains, self.watchdog.aggregated_metrics] - self.expression.react(state) + def visualise(self, _): + self.expression.react(self.watchdog.condition())