From ba7c1e972ea74790001cbd38650244a72a278eee Mon Sep 17 00:00:00 2001 From: Maciej Janicki Date: Tue, 28 May 2024 19:35:34 +0200 Subject: [PATCH 1/7] Feature: mqttprotogenerator initial commit --- src/mqttprotogenerator.py | 139 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/mqttprotogenerator.py diff --git a/src/mqttprotogenerator.py b/src/mqttprotogenerator.py new file mode 100644 index 0000000..e1ed3d4 --- /dev/null +++ b/src/mqttprotogenerator.py @@ -0,0 +1,139 @@ +from pkgutil import iter_modules +import protofiles +import importlib +from google.protobuf.message import Message +import random +import string +import pandas as pd +import sys + + +class MqttProtoGenerator(): + logger = None + + def __init__(self, message_name: string, message_file_path=''): + self.message_constructors = MqttProtoGenerator.get_message_constructors() + self.message_constructor = self.message_constructors[message_name] + self.constructed_messages = None + if message_file_path != '': + self.constructed_messages = self.read_message_csv( + message_name, message_file_path) + self.curr_message = 0 + + def get_message_constructors(): + message_constructors = {} + for submodule in iter_modules(protofiles.__path__): + submodule_name = submodule.name + submodule_fullname = f"protofiles.{submodule_name}" + imported_module = importlib.import_module(submodule_fullname) + for v in vars(imported_module).values(): + if not (isinstance(v, type) and issubclass(v, Message)): + continue + message_constructors[v.__name__] = v + return message_constructors + + def get_random_message(self): + message = self.message_constructor() + for field_descriptor in self.message_constructor.DESCRIPTOR.fields: + + message_type = field_descriptor.message_type + if field_descriptor.message_type is not None: + gen = MqttProtoGenerator(message_type.name) + m = gen.get_random_message() + attr = getattr(message, field_descriptor.name) + attr.CopyFrom(m) + continue + field_type = field_descriptor.type + # boolean + if field_type == 8: + setattr(message, field_descriptor.name, random.getrandbits(1)) + # string + elif field_type == 9: + setattr(message, field_descriptor.name, ''.join( + random.choice(string.ascii_lowercase) for i in range(10))) + # float or double + elif field_type == 2 or field_type == 1: + setattr(message, field_descriptor.name, random.random()) + # bytes + elif field_type == 12: + setattr(message, field_descriptor.name, random.randbytes(10)) + # int + else: + setattr(message, field_descriptor.name, + random.randint(0, 1000)) + return message + + def get_next_message(self): + ret = None + if self.constructed_messages is not None: + ret = self.constructed_messages[self.curr_message] + self.curr_message = (self.curr_message + + 1) % len(self.constructed_messages) + else: + ret = self.get_random_message() + return ret + + def read_message_csv(self, message_name: str, message_file: str) -> list: + df = None + try: + df = pd.read_csv(message_file) + except FileNotFoundError: + MqttProtoGenerator.logger.error( + f"ERROR: File {message_file} not found. Defaulting to sending random messages") + return + + messages = [] + for i in range(df.shape[0]): + fields = dict() + for field in df.columns: + fields[field] = df[field][i] + messages.append(self.construct_message(message_name, fields)) + return messages + + def construct_message(self, message_name: str, fields: dict) -> Message: + message_constructor = self.message_constructors[message_name] + new_message = message_constructor() + for field_descriptor in message_constructor.DESCRIPTOR.fields: + + message_type = field_descriptor.message_type + if field_descriptor.message_type is not None: + passed_on_fields = dict() + for field in fields.keys(): + if message_type.name not in field: + continue + sub_fields = field.split('.') + new_field_name = sub_fields[sub_fields.index( + message_type.name)+1:] + passed_on_fields['.'.join( + new_field_name)] = fields[field] + m = self.construct_message(message_type.name, passed_on_fields) + attr = getattr(new_message, field_descriptor.name) + attr.CopyFrom(m) + continue + field_type = field_descriptor.type + if field_descriptor.name not in fields.keys(): + MqttProtoGenerator.logger.error( + f"ERROR: data for field \'{field_descriptor.name}\' not found when constructing message \'{message_name}\'.") + continue + # bolean + if field_type == 8: + setattr(new_message, field_descriptor.name, + bool(fields[field_descriptor.name])) + # string + elif field_type == 9: + setattr(new_message, field_descriptor.name, + fields[field_descriptor.name]) + # float or double + elif field_type == 2 or field_type == 1: + setattr(new_message, field_descriptor.name, + float(fields[field_descriptor.name])) + # bytes + elif field_type == 12: + setattr(new_message, field_descriptor.name, + fields[field_descriptor.name]) + + # int + else: + setattr(new_message, field_descriptor.name, + int(fields[field_descriptor.name])) + return new_message From 0ca285802097917c6b097ca95bd6b5fe3980f0f2 Mon Sep 17 00:00:00 2001 From: Maciej Janicki Date: Tue, 28 May 2024 19:36:15 +0200 Subject: [PATCH 2/7] Update: random bugfixes with f-strings --- src/gui/ui.py | 34 ++++++++++++++++++++++------------ src/mqttsim.py | 21 ++++++++++++++------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/gui/ui.py b/src/gui/ui.py index 7ee8577..31bf579 100644 --- a/src/gui/ui.py +++ b/src/gui/ui.py @@ -26,7 +26,8 @@ def __init__(self, icon: str, tooltip: str): super(MqttSimTopicToolButton, self).__init__() self.setCursor(Qt.CursorShape.PointingHandCursor) self.setIcon(QIcon(icon)) - self.setToolTip(QCoreApplication.translate("MainWindow", tooltip, None)) + self.setToolTip(QCoreApplication.translate( + "MainWindow", tooltip, None)) class MqttSimAddTopicWindow(Ui_AddTopicDialog, QDialog): @@ -34,12 +35,14 @@ def __init__(self): super(MqttSimAddTopicWindow, self).__init__() self.setupUi(self) self.setWindowIcon(QIcon(":/icons/mqtt.svg")) - self.load_from_file_btn.clicked.connect(self.__on_load_from_file_btn_clicked) + self.load_from_file_btn.clicked.connect( + self.__on_load_from_file_btn_clicked) self.__load_patterns() self.predefined_pattern_combo_box.currentIndexChanged.connect( self.__on_pattern_selected ) - self.save_as_pattern_btn.clicked.connect(self.__on_save_as_pattern_btn_clicked) + self.save_as_pattern_btn.clicked.connect( + self.__on_save_as_pattern_btn_clicked) def __on_load_from_file_btn_clicked(self) -> None: filename, _ = QFileDialog.getOpenFileName( @@ -89,7 +92,8 @@ def __load_patterns(self): path.basename(pattern_filename).replace("_", " ") )[0] pattern_value = file.read() - self.predefined_pattern_combo_box.addItem(pattern_name, pattern_value) + self.predefined_pattern_combo_box.addItem( + pattern_name, pattern_value) def __on_save_as_pattern_btn_clicked(self): pattern_name, accepted = QInputDialog.getText( @@ -164,7 +168,8 @@ def __init__(self, topic_name: str): # edit btn self.edit_btn = MqttSimTopicToolButton( - ":/icons/edit.svg", QCoreApplication.translate("MainWindow", "Edit", None) + ":/icons/edit.svg", QCoreApplication.translate( + "MainWindow", "Edit", None) ) self.edit_btn.setCursor(Qt.CursorShape.PointingHandCursor) hlayout.addWidget(self.edit_btn) @@ -180,6 +185,7 @@ def set_topic_name(self, new_topic_name: str) -> None: self.topic = new_topic_name self.topic_lbl.setText(new_topic_name) + class MqttSimMainWindow(Ui_MainWindow, QMainWindow): def __init__(self, sim: MqttSim): super(MqttSimMainWindow, self).__init__() @@ -207,12 +213,14 @@ def on_broker_connect_btn_clicked() -> None: QCoreApplication.translate("MainWindow", "Connect", None) ) self.broker_connect_btn.setToolTip( - QCoreApplication.translate("MainWindow", "Connect to broker", None) + QCoreApplication.translate( + "MainWindow", "Connect to broker", None) ) else: if self.__sim.connect_to_broker(): self.broker_connect_btn.setText( - QCoreApplication.translate("MainWindow", "Disconnect", None) + QCoreApplication.translate( + "MainWindow", "Disconnect", None) ) self.broker_connect_btn.setToolTip( QCoreApplication.translate( @@ -227,7 +235,7 @@ def on_clear_logs_btn_clicked() -> None: def on_add_topic_btn_clicked() -> None: def validate_input(topic_config) -> bool: return len(topic_config.get("topic")) > 0 - + add_topic_window = MqttSimAddTopicWindow() while True: if add_topic_window.exec() == QDialog.Accepted: @@ -247,7 +255,8 @@ def validate_input(topic_config) -> bool: break # Break out of the loop if dialog is cancelled def on_broker_info_changed() -> None: - self.__sim.set_broker(self.broker_hostname.text(), self.broker_port.value()) + self.__sim.set_broker( + self.broker_hostname.text(), self.broker_port.value()) def on_topic_search_text_changed() -> None: for widget in self.topics_list_widget.findChildren(MqttSimTopicWidget): @@ -261,7 +270,8 @@ def on_topic_search_text_changed() -> None: self.add_topic_btn.clicked.connect(on_add_topic_btn_clicked) self.broker_hostname.textChanged.connect(on_broker_info_changed) self.broker_port.valueChanged.connect(on_broker_info_changed) - self.topic_search_line_edit.textChanged.connect(on_topic_search_text_changed) + self.topic_search_line_edit.textChanged.connect( + on_topic_search_text_changed) def __add_topic_to_item_list(self, topic_uuid: str) -> None: topic_name = self.__config.get_topic_data(topic_uuid).get("topic") @@ -271,7 +281,7 @@ def on_remove_btn_clicked() -> None: result = QMessageBox.question( self, "Remove topic?", - f"Are you sure you want to remove topic {self.__config.get_topic_data(topic_uuid).get("topic")} [uuid={topic_uuid}]?", + f'Are you sure you want to remove topic {self.__config.get_topic_data(topic_uuid).get("topic")} [uuid={topic_uuid}]?', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) if result == QMessageBox.StandardButton.Yes: self.__sim.remove_topic(topic_uuid) @@ -291,7 +301,7 @@ def on_edit_btn_clicked() -> None: topic_widget.set_topic_name(edited_data.get("topic")) self.__sim.edit(topic_uuid, edited_data) self.__logger.info( - f"Edited topic {data.get("topic")} [uuid={topic_uuid}] ({data} -> {edited_data})." + f'Edited topic {data.get("topic")} [uuid={topic_uuid}] ({data} -> {edited_data}).' ) topic_widget.remove_btn.clicked.connect(on_remove_btn_clicked) diff --git a/src/mqttsim.py b/src/mqttsim.py index 5235cbf..8dfdbda 100644 --- a/src/mqttsim.py +++ b/src/mqttsim.py @@ -6,6 +6,7 @@ from mqttsimdatagenerator import MqttSimDataGenerator from uuid import uuid4 + class MqttSimConfig: def __init__(self, path: str): self.__config = ConfigHandler(path) @@ -28,7 +29,7 @@ def put_broker(self, host: str, port: int, username: str | None = None, password def get_broker(self) -> tuple[str, int, str, str]: broker_info = self.__config.get("broker") if broker_info is None: - self.__config.put("broker", { "host": "localhost", "port": 1883 }) + self.__config.put("broker", {"host": "localhost", "port": 1883}) return self.get_broker() return ( broker_info.get("host", "localhost"), @@ -69,7 +70,8 @@ def time_diff_in_seconds(time1, time2) -> int: return diff_dt.total_seconds() topics_data = self.__config.get_topics() - last_sent = {topic_uuid: datetime.now() for topic_uuid in topics_data.keys()} + last_sent = {topic_uuid: datetime.now() + for topic_uuid in topics_data.keys()} while not self.__should_stop_publishing_thread: if not self.is_connected_to_broker(): @@ -95,7 +97,8 @@ def on_connect(client, userdata, flags, rc) -> None: if rc == CONNACK_ACCEPTED: self.__logger.info("Connected to broker.") else: - self.__logger.error(f"Error when connecting to broker (rc={rc}).") + self.__logger.error( + f"Error when connecting to broker (rc={rc}).") def on_disconnect(client, userdata, rc) -> None: if rc == 0: @@ -154,14 +157,16 @@ def remove_topic(self, topic_uuid: str) -> None: topic_data = self.__config.get_topic_data(topic_uuid) self.__client.unsubscribe(topic_data.get("topic")) self.__config.remove_topic(topic_uuid) - self.__logger.info(f"Removed topic: {topic_data.get("topic")}) [uuid={topic_uuid}].") + self.__logger.info( + f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') del self.__topic_data_generators[topic_uuid] # Adds topic to config (and saves it into config file). # If publishing thread was already started, it will take the topic into account. def add_topic(self, topic_config: dict) -> str: uuid = self.__config.put_topic(topic_config) - self.__logger.info(f"Added topic: {topic_config.get("topic")} [uuid={uuid}].") + self.__logger.info( + f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') self.__topic_data_generators[uuid] = MqttSimDataGenerator( topic_config.get("data_format") ) @@ -181,9 +186,11 @@ def edit(self, topic_uuid, new_data) -> None: def send_single_message(self, topic_uuid) -> None: if not self.is_connected_to_broker(): - self.__logger.error("Trying to send message when not connected to broker.") + self.__logger.error( + "Trying to send message when not connected to broker.") return topic_data = self.__config.get_topic_data(topic_uuid) - self.__logger.info(f"Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...") + self.__logger.info( + f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') message = self.__topic_data_generators.get(topic_uuid).next_message() self.__client.publish(topic_data.get("topic"), message) From 157aa2cc5fd741ec9fe1d63eb24ef77866aa6158 Mon Sep 17 00:00:00 2001 From: Maciej Janicki Date: Tue, 28 May 2024 20:08:08 +0200 Subject: [PATCH 3/7] Feature: integration of proto topic to mqttsim --- src/mqttsim.py | 62 ++++++++++++++++++++++++++++++-------- src/protofiles/__init__.py | 8 +++++ 2 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 src/protofiles/__init__.py diff --git a/src/mqttsim.py b/src/mqttsim.py index 8dfdbda..4e8c2a1 100644 --- a/src/mqttsim.py +++ b/src/mqttsim.py @@ -5,6 +5,7 @@ from time import sleep from mqttsimdatagenerator import MqttSimDataGenerator from uuid import uuid4 +from mqttprotogenerator import MqttProtoGenerator class MqttSimConfig: @@ -56,6 +57,14 @@ def __init__(self, config: MqttSimConfig, logger: any): self.__topic_data_generators = { topic_uuid: MqttSimDataGenerator(topic_config.get("data_format")) for topic_uuid, topic_config in self.__config.get_topics().items() + if "data_format" in topic_config + } + MqttProtoGenerator.logger = logger + self.__proto_topic_data_generators = { + topic_uuid: MqttProtoGenerator( + topic_config.get("message"), topic_config.get("file")) + for topic_uuid, topic_config in self.__config.get_topics().items() + if "message" in topic_config } self.__setup_client() self.__setup_publishing_thread() @@ -91,7 +100,16 @@ def time_diff_in_seconds(time1, time2) -> int: def __setup_client(self) -> None: def on_message(client, userdata, message) -> None: - self.__logger.info(f"Received message from broker: '{message}'.") + if message.topic in self.__topic_data_generators: + self.__logger.info( + f"Received message from broker {message.topic}: '{message.payload}'.") + elif message.topic in self.__proto_topic_data_generators: + message_constructor = self.__proto_topic_data_generators[ + message.topic].message_constructor + proto_message = message_constructor() + proto_message.ParseFromString(message.payload) + self.__logger.info( + f"Received message from broker on topic {message.topic}: '{proto_message}'.") def on_connect(client, userdata, flags, rc) -> None: if rc == CONNACK_ACCEPTED: @@ -137,7 +155,7 @@ def connect_to_broker(self) -> bool: return False except Exception: self.__logger.error( - f"Unknown error occured when trying to connect to broker." + "Unknown error occured when trying to connect to broker." ) return False return True @@ -157,9 +175,14 @@ def remove_topic(self, topic_uuid: str) -> None: topic_data = self.__config.get_topic_data(topic_uuid) self.__client.unsubscribe(topic_data.get("topic")) self.__config.remove_topic(topic_uuid) - self.__logger.info( - f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') - del self.__topic_data_generators[topic_uuid] + if topic_uuid in self.__topic_data_generators: + self.__logger.info( + f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') + del self.__topic_data_generators[topic_uuid] + else: + self.__logger.info( + f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') + del self.__proto_topic_data_generators[topic_uuid] # Adds topic to config (and saves it into config file). # If publishing thread was already started, it will take the topic into account. @@ -180,17 +203,30 @@ def get_config(self) -> MqttSimConfig: def edit(self, topic_uuid, new_data) -> None: self.__config.put_topic(new_data, uuid=topic_uuid) - self.__topic_data_generators[topic_uuid].reinitalize( - new_data.get("data_format") - ) + if topic_uuid in self.__topic_data_generators: + self.__topic_data_generators[topic_uuid].reinitalize( + new_data.get("data_format") + ) + else: + self.__proto_topic_data_generators[topic_uuid] = MqttProtoGenerator( + new_data.get("message"), new_data.get("file")) def send_single_message(self, topic_uuid) -> None: if not self.is_connected_to_broker(): self.__logger.error( "Trying to send message when not connected to broker.") return - topic_data = self.__config.get_topic_data(topic_uuid) - self.__logger.info( - f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') - message = self.__topic_data_generators.get(topic_uuid).next_message() - self.__client.publish(topic_data.get("topic"), message) + if topic_uuid in self.__topic_data_generators: + topic_data = self.__config.get_topic_data(topic_uuid) + self.__logger.info( + f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') + message = self.__topic_data_generators.get( + topic_uuid).next_message() + self.__client.publish(topic_data.get("topic"), message) + else: + self.__logger.info( + f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') + gen = self.__proto_topic_data_generators[topic_uuid] + proto_message = gen.get_next_message() + self.__client.publish( + topic_data.get("topic"), proto_message.SerializeToString()) diff --git a/src/protofiles/__init__.py b/src/protofiles/__init__.py new file mode 100644 index 0000000..7f2f0fe --- /dev/null +++ b/src/protofiles/__init__.py @@ -0,0 +1,8 @@ +from os.path import dirname, basename, isfile, join +import os +import sys +import glob +modules = glob.glob(join(dirname(__file__), "*.py")) +__all__ = [basename(f)[:-3] for f in modules if isfile(f) + and not f.endswith('__init__.py')] +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) From 948b40102bdb6e3ce38d6d74f1e0197b41d62e46 Mon Sep 17 00:00:00 2001 From: Maciej Janicki Date: Tue, 28 May 2024 21:07:56 +0200 Subject: [PATCH 4/7] Update: Final integrations pre-merge --- .gitignore | 4 +- readme.md | 29 ++++ src/gui/addprototopicdialog.ui | 169 +++++++++++++++++++++++ src/gui/choosetopicdialog.ui | 45 ++++++ src/gui/generated/addprototopicdialog.py | 140 +++++++++++++++++++ src/gui/generated/choosetopicdialog.py | 53 +++++++ src/gui/ui.py | 121 ++++++++++++++-- src/mqttprotogenerator.py | 37 ++++- src/mqttsim.py | 26 ++-- src/requirements.txt | 7 + 10 files changed, 601 insertions(+), 30 deletions(-) create mode 100644 src/gui/addprototopicdialog.ui create mode 100644 src/gui/choosetopicdialog.ui create mode 100644 src/gui/generated/addprototopicdialog.py create mode 100644 src/gui/generated/choosetopicdialog.py diff --git a/.gitignore b/.gitignore index a8e81fc..cda907d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ env/ venv/ .idea/ logs.txt -.DS_Store \ No newline at end of file +.DS_Store +*pb2.py +.venv diff --git a/readme.md b/readme.md index 7825ce1..6980944 100644 --- a/readme.md +++ b/readme.md @@ -125,3 +125,32 @@ Main window
Add topic window

+ +# Protobuf +To use protobuf place compiled message files in src/protofiles. + +## Custom message content + +To send a message with custom contents use *.csv file. Program will look for specified file in root directory. + +### Example: + +Given these protobuf messages: +```protobuf +message Message1 { + int32 my_int = 1; + Message2 nested_message = 2; +} +message Message2 { + int32 my_int = 1; +} +``` + +example csv file for Message1 would look like this: + +```csv +my_int,Message2.my_int +123,456 +456,1312 +123123,45123 +``` diff --git a/src/gui/addprototopicdialog.ui b/src/gui/addprototopicdialog.ui new file mode 100644 index 0000000..a94adc2 --- /dev/null +++ b/src/gui/addprototopicdialog.ui @@ -0,0 +1,169 @@ + + + AddProtoTopicDialog + + + + 0 + 0 + 457 + 337 + + + + + 12 + + + + Add topic + + + + + + + + + 14 + + + + Add topic + + + + + + + + + Name + + + + + + + Name of topic + + + + + + + Message + + + + + + + + + + Interval (seconds) + + + + + + + Interval of incoming data + + + 0.100000000000000 + + + 60.000000000000000 + + + 0.500000000000000 + + + 1.500000000000000 + + + + + + + Manual + + + + + + + If checked, the data will be automatically send every <interval> seconds + + + + + + false + + + + + + + + + + File (optional) + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + buttonBox + accepted() + AddProtoTopicDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AddProtoTopicDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/gui/choosetopicdialog.ui b/src/gui/choosetopicdialog.ui new file mode 100644 index 0000000..3e49633 --- /dev/null +++ b/src/gui/choosetopicdialog.ui @@ -0,0 +1,45 @@ + + + ChooseTopicDialog + + + + 0 + 0 + 319 + 153 + + + + Choose message type + + + + + 9 + 19 + 291 + 121 + + + + + + + Protobuf + + + + + + + JSON + + + + + + + + + diff --git a/src/gui/generated/addprototopicdialog.py b/src/gui/generated/addprototopicdialog.py new file mode 100644 index 0000000..ccc58cd --- /dev/null +++ b/src/gui/generated/addprototopicdialog.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'addprototopicdialog.ui' +## +## Created by: Qt User Interface Compiler version 6.7.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog, + QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, + QLineEdit, QListWidget, QListWidgetItem, QSizePolicy, + QVBoxLayout, QWidget) + +class Ui_AddProtoTopicDialog(object): + def setupUi(self, AddProtoTopicDialog): + if not AddProtoTopicDialog.objectName(): + AddProtoTopicDialog.setObjectName(u"AddProtoTopicDialog") + AddProtoTopicDialog.resize(457, 337) + font = QFont() + font.setPointSize(12) + AddProtoTopicDialog.setFont(font) + self.verticalLayout_2 = QVBoxLayout(AddProtoTopicDialog) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalLayout = QVBoxLayout() + self.verticalLayout.setObjectName(u"verticalLayout") + self.label = QLabel(AddProtoTopicDialog) + self.label.setObjectName(u"label") + font1 = QFont() + font1.setPointSize(14) + self.label.setFont(font1) + + self.verticalLayout.addWidget(self.label, 0, Qt.AlignmentFlag.AlignTop) + + self.formLayout = QFormLayout() + self.formLayout.setObjectName(u"formLayout") + self.name_lbl = QLabel(AddProtoTopicDialog) + self.name_lbl.setObjectName(u"name_lbl") + + self.formLayout.setWidget(0, QFormLayout.LabelRole, self.name_lbl) + + self.name_line_edit = QLineEdit(AddProtoTopicDialog) + self.name_line_edit.setObjectName(u"name_line_edit") + + self.formLayout.setWidget(0, QFormLayout.FieldRole, self.name_line_edit) + + self.format_lbl = QLabel(AddProtoTopicDialog) + self.format_lbl.setObjectName(u"format_lbl") + + self.formLayout.setWidget(1, QFormLayout.LabelRole, self.format_lbl) + + self.message_list = QListWidget(AddProtoTopicDialog) + self.message_list.setObjectName(u"message_list") + + self.formLayout.setWidget(1, QFormLayout.FieldRole, self.message_list) + + self.interval_lbl = QLabel(AddProtoTopicDialog) + self.interval_lbl.setObjectName(u"interval_lbl") + + self.formLayout.setWidget(3, QFormLayout.LabelRole, self.interval_lbl) + + self.interval_spin_box = QDoubleSpinBox(AddProtoTopicDialog) + self.interval_spin_box.setObjectName(u"interval_spin_box") + self.interval_spin_box.setMinimum(0.100000000000000) + self.interval_spin_box.setMaximum(60.000000000000000) + self.interval_spin_box.setSingleStep(0.500000000000000) + self.interval_spin_box.setValue(1.500000000000000) + + self.formLayout.setWidget(3, QFormLayout.FieldRole, self.interval_spin_box) + + self.manual_lbl = QLabel(AddProtoTopicDialog) + self.manual_lbl.setObjectName(u"manual_lbl") + + self.formLayout.setWidget(4, QFormLayout.LabelRole, self.manual_lbl) + + self.manual_check_box = QCheckBox(AddProtoTopicDialog) + self.manual_check_box.setObjectName(u"manual_check_box") + self.manual_check_box.setChecked(False) + + self.formLayout.setWidget(4, QFormLayout.FieldRole, self.manual_check_box) + + self.file_line_edit = QLineEdit(AddProtoTopicDialog) + self.file_line_edit.setObjectName(u"file_line_edit") + + self.formLayout.setWidget(2, QFormLayout.FieldRole, self.file_line_edit) + + self.path_label = QLabel(AddProtoTopicDialog) + self.path_label.setObjectName(u"path_label") + + self.formLayout.setWidget(2, QFormLayout.LabelRole, self.path_label) + + + self.verticalLayout.addLayout(self.formLayout) + + + self.verticalLayout_2.addLayout(self.verticalLayout) + + self.buttonBox = QDialogButtonBox(AddProtoTopicDialog) + self.buttonBox.setObjectName(u"buttonBox") + self.buttonBox.setOrientation(Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) + + self.verticalLayout_2.addWidget(self.buttonBox) + + + self.retranslateUi(AddProtoTopicDialog) + self.buttonBox.accepted.connect(AddProtoTopicDialog.accept) + self.buttonBox.rejected.connect(AddProtoTopicDialog.reject) + + QMetaObject.connectSlotsByName(AddProtoTopicDialog) + # setupUi + + def retranslateUi(self, AddProtoTopicDialog): + AddProtoTopicDialog.setWindowTitle(QCoreApplication.translate("AddProtoTopicDialog", u"Add topic", None)) + self.label.setText(QCoreApplication.translate("AddProtoTopicDialog", u"Add topic", None)) + self.name_lbl.setText(QCoreApplication.translate("AddProtoTopicDialog", u"Name", None)) +#if QT_CONFIG(tooltip) + self.name_line_edit.setToolTip(QCoreApplication.translate("AddProtoTopicDialog", u"Name of topic", None)) +#endif // QT_CONFIG(tooltip) + self.format_lbl.setText(QCoreApplication.translate("AddProtoTopicDialog", u"Message", None)) + self.interval_lbl.setText(QCoreApplication.translate("AddProtoTopicDialog", u"Interval (seconds)", None)) +#if QT_CONFIG(tooltip) + self.interval_spin_box.setToolTip(QCoreApplication.translate("AddProtoTopicDialog", u"Interval of incoming data", None)) +#endif // QT_CONFIG(tooltip) + self.manual_lbl.setText(QCoreApplication.translate("AddProtoTopicDialog", u"Manual", None)) +#if QT_CONFIG(tooltip) + self.manual_check_box.setToolTip(QCoreApplication.translate("AddProtoTopicDialog", u"If checked, the data will be automatically send every seconds", None)) +#endif // QT_CONFIG(tooltip) + self.manual_check_box.setText("") + self.path_label.setText(QCoreApplication.translate("AddProtoTopicDialog", u"File (optional)", None)) + # retranslateUi + diff --git a/src/gui/generated/choosetopicdialog.py b/src/gui/generated/choosetopicdialog.py new file mode 100644 index 0000000..038e7a7 --- /dev/null +++ b/src/gui/generated/choosetopicdialog.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'choosetopicdialog.ui' +## +## Created by: Qt User Interface Compiler version 6.5.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QDialog, QHBoxLayout, QPushButton, + QSizePolicy, QWidget) + +class Ui_ChooseTopicDialog(object): + def setupUi(self, ChooseTopicDialog): + if not ChooseTopicDialog.objectName(): + ChooseTopicDialog.setObjectName(u"ChooseTopicDialog") + ChooseTopicDialog.resize(319, 153) + self.horizontalLayoutWidget = QWidget(ChooseTopicDialog) + self.horizontalLayoutWidget.setObjectName(u"horizontalLayoutWidget") + self.horizontalLayoutWidget.setGeometry(QRect(9, 19, 291, 121)) + self.horizontalLayout = QHBoxLayout(self.horizontalLayoutWidget) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.choose_proto_button = QPushButton(self.horizontalLayoutWidget) + self.choose_proto_button.setObjectName(u"choose_proto_button") + + self.horizontalLayout.addWidget(self.choose_proto_button) + + self.choose_json_button = QPushButton(self.horizontalLayoutWidget) + self.choose_json_button.setObjectName(u"choose_json_button") + + self.horizontalLayout.addWidget(self.choose_json_button) + + + self.retranslateUi(ChooseTopicDialog) + + QMetaObject.connectSlotsByName(ChooseTopicDialog) + # setupUi + + def retranslateUi(self, ChooseTopicDialog): + ChooseTopicDialog.setWindowTitle(QCoreApplication.translate("ChooseTopicDialog", u"Choose message type", None)) + self.choose_proto_button.setText(QCoreApplication.translate("ChooseTopicDialog", u"Protobuf", None)) + self.choose_json_button.setText(QCoreApplication.translate("ChooseTopicDialog", u"JSON", None)) + # retranslateUi + diff --git a/src/gui/ui.py b/src/gui/ui.py index 31bf579..639f8a1 100644 --- a/src/gui/ui.py +++ b/src/gui/ui.py @@ -19,6 +19,9 @@ from logger import QListWidgetLogHandler from os import listdir, path, getcwd import icons.generated.icons +from gui.generated.addprototopicdialog import Ui_AddProtoTopicDialog +from gui.generated.choosetopicdialog import Ui_ChooseTopicDialog +from mqttprotogenerator import MqttProtoGenerator class MqttSimTopicToolButton(QToolButton): @@ -186,6 +189,41 @@ def set_topic_name(self, new_topic_name: str) -> None: self.topic_lbl.setText(new_topic_name) +class MqttSimAddProtoTopicWindow(Ui_AddProtoTopicDialog, QDialog): + def __init__(self): + super(MqttSimAddProtoTopicWindow, self).__init__() + self.setupUi(self) + self.setWindowIcon(QIcon(":/icons/mqtt.svg")) + + +class MqttSimEditProtoTopicWindow(MqttSimAddProtoTopicWindow, QDialog): + def __init__(self, topic_name: str, topic_data: dict): + super(MqttSimEditProtoTopicWindow, self).__init__() + self.__set_topic_values(topic_name, topic_data) + self.setWindowIcon(QIcon(":/icons/mqtt.svg")) + + def __set_topic_values(self, topic_name, topic_data) -> None: + messages = MqttProtoGenerator.get_message_constructors() + current_item = None + for message_name in messages.keys(): + new_item = QListWidgetItem(message_name) + self.message_list.addItem(new_item) + if message_name == topic_data["message"]: + current_item = new_item + self.message_list.setCurrentItem(current_item) + self.name_line_edit.setText(topic_name) + self.file_line_edit.setText(topic_data["file"]) + self.interval_spin_box.setValue(topic_data.get("interval")) + self.manual_check_box.setChecked(topic_data.get("manual")) + + +class MqttSimChooseTopicWindow(Ui_ChooseTopicDialog, QDialog): + def __init__(self): + super(MqttSimChooseTopicWindow, self).__init__() + self.setupUi(self) + self.setWindowIcon(QIcon(":/icons/mqtt.svg")) + + class MqttSimMainWindow(Ui_MainWindow, QMainWindow): def __init__(self, sim: MqttSim): super(MqttSimMainWindow, self).__init__() @@ -233,6 +271,18 @@ def on_clear_logs_btn_clicked() -> None: self.__logger.info("Cleared logs.") def on_add_topic_btn_clicked() -> None: + choose_topic_window = MqttSimChooseTopicWindow() + choose_topic_window.choose_json_button.clicked.connect( + choose_topic_window.close) + choose_topic_window.choose_json_button.clicked.connect( + on_add_json_btn_clicked) + choose_topic_window.choose_proto_button.clicked.connect( + choose_topic_window.close) + choose_topic_window.choose_proto_button.clicked.connect( + on_add_proto_btn_clicked) + choose_topic_window.exec() + + def on_add_json_btn_clicked() -> None: def validate_input(topic_config) -> bool: return len(topic_config.get("topic")) > 0 @@ -254,6 +304,32 @@ def validate_input(topic_config) -> bool: else: break # Break out of the loop if dialog is cancelled + def on_add_proto_btn_clicked() -> None: + def validate_input(topic_name, topic_config) -> bool: + return (len(topic_name) > 0 and topic_name not in self.__config.get_topics().keys()) + + add_topic_window = MqttSimAddProtoTopicWindow() + messages = MqttProtoGenerator.get_message_constructors() + for message_name in messages.keys(): + add_topic_window.message_list.addItem(message_name) + if add_topic_window.exec(): + if add_topic_window.message_list.currentItem() is None: + QMessageBox().critical(self, "Error!", "Invalid topic input.") + return + topic_name = add_topic_window.name_line_edit.text() + topic_config = { + "topic": add_topic_window.name_line_edit.text(), + "message": add_topic_window.message_list.currentItem().text(), + "interval": add_topic_window.interval_spin_box.value(), + "manual": add_topic_window.manual_check_box.isChecked(), + "file": add_topic_window.file_line_edit.text() + } + if validate_input(topic_name, topic_config): + uuid = self.__sim.add_proto_topic(topic_config) + self.__add_topic_to_item_list(uuid) + else: + QMessageBox().critical(self, "Error!", "Invalid topic input.") + def on_broker_info_changed() -> None: self.__sim.set_broker( self.broker_hostname.text(), self.broker_port.value()) @@ -289,20 +365,37 @@ def on_remove_btn_clicked() -> None: def on_edit_btn_clicked() -> None: data = self.__config.get_topic_data(topic_uuid) - edit_topic_window = MqttSimEditTopicWindow(data) - if edit_topic_window.exec(): - edited_data = { - "topic": edit_topic_window.name_line_edit.text(), - "data_format": edit_topic_window.format_text_edit.toPlainText(), - "interval": edit_topic_window.interval_spin_box.value(), - "manual": edit_topic_window.manual_check_box.isChecked(), - } - if edited_data != data: - topic_widget.set_topic_name(edited_data.get("topic")) - self.__sim.edit(topic_uuid, edited_data) - self.__logger.info( - f'Edited topic {data.get("topic")} [uuid={topic_uuid}] ({data} -> {edited_data}).' - ) + if "data_format" in data.keys(): + edit_topic_window = MqttSimEditTopicWindow(data) + if edit_topic_window.exec(): + edited_data = { + "topic": edit_topic_window.name_line_edit.text(), + "data_format": edit_topic_window.format_text_edit.toPlainText(), + "interval": edit_topic_window.interval_spin_box.value(), + "manual": edit_topic_window.manual_check_box.isChecked(), + } + if edited_data != data: + topic_widget.set_topic_name(edited_data.get("topic")) + self.__sim.edit(topic_uuid, edited_data) + self.__logger.info( + f'Edited topic {data.get("topic")} [uuid={topic_uuid}] ({data} -> {edited_data}).' + ) + else: + edit_topic_window = MqttSimEditProtoTopicWindow( + topic_name, data) + if edit_topic_window.exec(): + edited_topic_data = { + "topic": edit_topic_window.name_line_edit.text(), + "message": edit_topic_window.message_list.currentItem().text(), + "interval": edit_topic_window.interval_spin_box.value(), + "manual": edit_topic_window.manual_check_box.isChecked(), + "file": edit_topic_window.file_line_edit.text() + } + if edited_topic_data != data: + self.__sim.edit(topic_uuid, edited_topic_data) + self.__logger.info( + f'Edited topic {data.get("topic")} [uuid={topic_uuid}] ({data} -> {edited_topic_data}).' + ) topic_widget.remove_btn.clicked.connect(on_remove_btn_clicked) topic_widget.edit_btn.clicked.connect(on_edit_btn_clicked) diff --git a/src/mqttprotogenerator.py b/src/mqttprotogenerator.py index e1ed3d4..7c6feed 100644 --- a/src/mqttprotogenerator.py +++ b/src/mqttprotogenerator.py @@ -6,6 +6,7 @@ import string import pandas as pd import sys +import os class MqttProtoGenerator(): @@ -15,7 +16,17 @@ def __init__(self, message_name: string, message_file_path=''): self.message_constructors = MqttProtoGenerator.get_message_constructors() self.message_constructor = self.message_constructors[message_name] self.constructed_messages = None + self.message_name = message_name if message_file_path != '': + try: + self.file_time_stamp = os.stat(message_file_path).st_mtime + except FileNotFoundError: + MqttProtoGenerator.logger.error( + f"ERROR: File {message_file_path} not found. Defaulting to sending random messages") + self.message_file_path = '' + return + + self.message_file_path = message_file_path self.constructed_messages = self.read_message_csv( message_name, message_file_path) self.curr_message = 0 @@ -66,6 +77,15 @@ def get_random_message(self): def get_next_message(self): ret = None if self.constructed_messages is not None: + time_stamp = os.stat(self.message_file_path).st_mtime + if time_stamp != self.file_time_stamp: + MqttProtoGenerator.logger.info( + f"INFO: file {self.message_file_path} changed. Reconstructing messages") + self.constructed_messages = self.read_message_csv( + self.message_name, message_file=self.message_file_path) + self.file_time_stamp = time_stamp + self.curr_message = 0 + ret = self.constructed_messages[self.curr_message] self.curr_message = (self.curr_message + 1) % len(self.constructed_messages) @@ -102,8 +122,17 @@ def construct_message(self, message_name: str, fields: dict) -> Message: if message_type.name not in field: continue sub_fields = field.split('.') - new_field_name = sub_fields[sub_fields.index( - message_type.name)+1:] + try: + new_field_name = sub_fields[sub_fields.index( + message_type.name)+1:] + except ValueError: + if ' ' in field: + MqttProtoGenerator.logger.error( + f"ERROR: Field {field} contains spaces!!! You should not do that!") + else: + MqttProtoGenerator.logger.error( + f"ERROR: Field with key \'{field}\' not found in message \'{message_name}\'.") + return new_message passed_on_fields['.'.join( new_field_name)] = fields[field] m = self.construct_message(message_type.name, passed_on_fields) @@ -112,8 +141,8 @@ def construct_message(self, message_name: str, fields: dict) -> Message: continue field_type = field_descriptor.type if field_descriptor.name not in fields.keys(): - MqttProtoGenerator.logger.error( - f"ERROR: data for field \'{field_descriptor.name}\' not found when constructing message \'{message_name}\'.") + MqttProtoGenerator.logger.info( + f"INFO: data for field \'{field_descriptor.name}\' not found when constructing message \'{message_name}\'.") continue # bolean if field_type == 8: diff --git a/src/mqttsim.py b/src/mqttsim.py index 4e8c2a1..b448912 100644 --- a/src/mqttsim.py +++ b/src/mqttsim.py @@ -100,18 +100,13 @@ def time_diff_in_seconds(time1, time2) -> int: def __setup_client(self) -> None: def on_message(client, userdata, message) -> None: - if message.topic in self.__topic_data_generators: - self.__logger.info( - f"Received message from broker {message.topic}: '{message.payload}'.") - elif message.topic in self.__proto_topic_data_generators: - message_constructor = self.__proto_topic_data_generators[ - message.topic].message_constructor - proto_message = message_constructor() - proto_message.ParseFromString(message.payload) - self.__logger.info( - f"Received message from broker on topic {message.topic}: '{proto_message}'.") + self.__logger.info( + f"Received message from broker {message.topic}.") def on_connect(client, userdata, flags, rc) -> None: + topics = self.__config.get_topics() + for topic_uuid in topics: + self.__client.subscribe(topics[topic_uuid]["topic"]) if rc == CONNACK_ACCEPTED: self.__logger.info("Connected to broker.") else: @@ -216,8 +211,8 @@ def send_single_message(self, topic_uuid) -> None: self.__logger.error( "Trying to send message when not connected to broker.") return + topic_data = self.__config.get_topic_data(topic_uuid) if topic_uuid in self.__topic_data_generators: - topic_data = self.__config.get_topic_data(topic_uuid) self.__logger.info( f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') message = self.__topic_data_generators.get( @@ -230,3 +225,12 @@ def send_single_message(self, topic_uuid) -> None: proto_message = gen.get_next_message() self.__client.publish( topic_data.get("topic"), proto_message.SerializeToString()) + + def add_proto_topic(self, topic_config: dict) -> None: + uuid = self.__config.put_topic(topic_config) + self.__logger.info( + f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') + self.__proto_topic_data_generators[uuid] = MqttProtoGenerator( + topic_config.get("message"), topic_config.get("file")) + self.__client.subscribe(topic_config.get("topic")) + return uuid diff --git a/src/requirements.txt b/src/requirements.txt index 2108776..bc00439 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,5 +1,12 @@ +numpy==1.26.4 paho-mqtt==2.1.0 +pandas==2.2.2 +protobuf==5.27.0 PySide6==6.7.0 PySide6_Addons==6.7.0 PySide6_Essentials==6.7.0 +python-dateutil==2.9.0.post0 +pytz==2024.1 shiboken6==6.7.0 +six==1.16.0 +tzdata==2024.1 From 8891791dc4df7d7238ca597609faee24663043de Mon Sep 17 00:00:00 2001 From: Maciej Janicki Date: Thu, 30 May 2024 20:40:04 +0200 Subject: [PATCH 5/7] Fix: formatting. --- readme.md | 29 +++++++++++++++-------------- src/gui/ui.py | 24 ++++++++---------------- src/mqttprotogenerator.py | 23 +++++++++++------------ src/mqttsim.py | 30 ++++++++++-------------------- src/protofiles/__init__.py | 11 +++++------ 5 files changed, 49 insertions(+), 68 deletions(-) diff --git a/readme.md b/readme.md index 6980944..07055fb 100644 --- a/readme.md +++ b/readme.md @@ -112,20 +112,6 @@ You can always create ```config.json``` file yourself if you think writing it wi ``` > result - app publishes random int value on 'topic1' every second and random uint value on 'test/topic2' -## Screenshots -Keep in mind that the look of the app is dependent on user's system - QT uses native components. -

- -
-Main window -

- -

- -
-Add topic window -

- # Protobuf To use protobuf place compiled message files in src/protofiles. @@ -154,3 +140,18 @@ my_int,Message2.my_int 456,1312 123123,45123 ``` + +## Screenshots +Keep in mind that the look of the app is dependent on user's system - QT uses native components. +

+ +
+Main window +

+ +

+ +
+Add topic window +

+ diff --git a/src/gui/ui.py b/src/gui/ui.py index 639f8a1..faf7427 100644 --- a/src/gui/ui.py +++ b/src/gui/ui.py @@ -29,8 +29,7 @@ def __init__(self, icon: str, tooltip: str): super(MqttSimTopicToolButton, self).__init__() self.setCursor(Qt.CursorShape.PointingHandCursor) self.setIcon(QIcon(icon)) - self.setToolTip(QCoreApplication.translate( - "MainWindow", tooltip, None)) + self.setToolTip(QCoreApplication.translate("MainWindow", tooltip, None)) class MqttSimAddTopicWindow(Ui_AddTopicDialog, QDialog): @@ -38,8 +37,7 @@ def __init__(self): super(MqttSimAddTopicWindow, self).__init__() self.setupUi(self) self.setWindowIcon(QIcon(":/icons/mqtt.svg")) - self.load_from_file_btn.clicked.connect( - self.__on_load_from_file_btn_clicked) + self.load_from_file_btn.clicked.connect(self.__on_load_from_file_btn_clicked) self.__load_patterns() self.predefined_pattern_combo_box.currentIndexChanged.connect( self.__on_pattern_selected @@ -95,8 +93,7 @@ def __load_patterns(self): path.basename(pattern_filename).replace("_", " ") )[0] pattern_value = file.read() - self.predefined_pattern_combo_box.addItem( - pattern_name, pattern_value) + self.predefined_pattern_combo_box.addItem(pattern_name, pattern_value) def __on_save_as_pattern_btn_clicked(self): pattern_name, accepted = QInputDialog.getText( @@ -171,8 +168,7 @@ def __init__(self, topic_name: str): # edit btn self.edit_btn = MqttSimTopicToolButton( - ":/icons/edit.svg", QCoreApplication.translate( - "MainWindow", "Edit", None) + ":/icons/edit.svg", QCoreApplication.translate("MainWindow", "Edit", None) ) self.edit_btn.setCursor(Qt.CursorShape.PointingHandCursor) hlayout.addWidget(self.edit_btn) @@ -251,8 +247,7 @@ def on_broker_connect_btn_clicked() -> None: QCoreApplication.translate("MainWindow", "Connect", None) ) self.broker_connect_btn.setToolTip( - QCoreApplication.translate( - "MainWindow", "Connect to broker", None) + QCoreApplication.translate("MainWindow", "Connect to broker", None) ) else: if self.__sim.connect_to_broker(): @@ -262,8 +257,7 @@ def on_broker_connect_btn_clicked() -> None: ) self.broker_connect_btn.setToolTip( QCoreApplication.translate( - "MainWindow", "Disconnect from broker", None - ) + "MainWindow", "Disconnect from broker", None) ) def on_clear_logs_btn_clicked() -> None: @@ -331,8 +325,7 @@ def validate_input(topic_name, topic_config) -> bool: QMessageBox().critical(self, "Error!", "Invalid topic input.") def on_broker_info_changed() -> None: - self.__sim.set_broker( - self.broker_hostname.text(), self.broker_port.value()) + self.__sim.set_broker(self.broker_hostname.text(), self.broker_port.value()) def on_topic_search_text_changed() -> None: for widget in self.topics_list_widget.findChildren(MqttSimTopicWidget): @@ -346,8 +339,7 @@ def on_topic_search_text_changed() -> None: self.add_topic_btn.clicked.connect(on_add_topic_btn_clicked) self.broker_hostname.textChanged.connect(on_broker_info_changed) self.broker_port.valueChanged.connect(on_broker_info_changed) - self.topic_search_line_edit.textChanged.connect( - on_topic_search_text_changed) + self.topic_search_line_edit.textChanged.connect(on_topic_search_text_changed) def __add_topic_to_item_list(self, topic_uuid: str) -> None: topic_name = self.__config.get_topic_data(topic_uuid).get("topic") diff --git a/src/mqttprotogenerator.py b/src/mqttprotogenerator.py index 7c6feed..7e99c14 100644 --- a/src/mqttprotogenerator.py +++ b/src/mqttprotogenerator.py @@ -2,24 +2,23 @@ import protofiles import importlib from google.protobuf.message import Message -import random -import string +from random import getrandbits, randint, random, choice, randbytes +from string import ascii_lowercase import pandas as pd -import sys -import os +from os import stat class MqttProtoGenerator(): logger = None - def __init__(self, message_name: string, message_file_path=''): + def __init__(self, message_name: str, message_file_path=''): self.message_constructors = MqttProtoGenerator.get_message_constructors() self.message_constructor = self.message_constructors[message_name] self.constructed_messages = None self.message_name = message_name if message_file_path != '': try: - self.file_time_stamp = os.stat(message_file_path).st_mtime + self.file_time_stamp = stat(message_file_path).st_mtime except FileNotFoundError: MqttProtoGenerator.logger.error( f"ERROR: File {message_file_path} not found. Defaulting to sending random messages") @@ -57,27 +56,27 @@ def get_random_message(self): field_type = field_descriptor.type # boolean if field_type == 8: - setattr(message, field_descriptor.name, random.getrandbits(1)) + setattr(message, field_descriptor.name, getrandbits(1)) # string elif field_type == 9: setattr(message, field_descriptor.name, ''.join( - random.choice(string.ascii_lowercase) for i in range(10))) + choice(ascii_lowercase) for i in range(10))) # float or double elif field_type == 2 or field_type == 1: - setattr(message, field_descriptor.name, random.random()) + setattr(message, field_descriptor.name, random()) # bytes elif field_type == 12: - setattr(message, field_descriptor.name, random.randbytes(10)) + setattr(message, field_descriptor.name, randbytes(10)) # int else: setattr(message, field_descriptor.name, - random.randint(0, 1000)) + randint(0, 1000)) return message def get_next_message(self): ret = None if self.constructed_messages is not None: - time_stamp = os.stat(self.message_file_path).st_mtime + time_stamp = stat(self.message_file_path).st_mtime if time_stamp != self.file_time_stamp: MqttProtoGenerator.logger.info( f"INFO: file {self.message_file_path} changed. Reconstructing messages") diff --git a/src/mqttsim.py b/src/mqttsim.py index b448912..3c99348 100644 --- a/src/mqttsim.py +++ b/src/mqttsim.py @@ -79,8 +79,7 @@ def time_diff_in_seconds(time1, time2) -> int: return diff_dt.total_seconds() topics_data = self.__config.get_topics() - last_sent = {topic_uuid: datetime.now() - for topic_uuid in topics_data.keys()} + last_sent = {topic_uuid: datetime.now() for topic_uuid in topics_data.keys()} while not self.__should_stop_publishing_thread: if not self.is_connected_to_broker(): @@ -100,8 +99,7 @@ def time_diff_in_seconds(time1, time2) -> int: def __setup_client(self) -> None: def on_message(client, userdata, message) -> None: - self.__logger.info( - f"Received message from broker {message.topic}.") + self.__logger.info(f"Received message from broker {message.topic}.") def on_connect(client, userdata, flags, rc) -> None: topics = self.__config.get_topics() @@ -110,8 +108,7 @@ def on_connect(client, userdata, flags, rc) -> None: if rc == CONNACK_ACCEPTED: self.__logger.info("Connected to broker.") else: - self.__logger.error( - f"Error when connecting to broker (rc={rc}).") + self.__logger.error(f"Error when connecting to broker (rc={rc}).") def on_disconnect(client, userdata, rc) -> None: if rc == 0: @@ -171,20 +168,17 @@ def remove_topic(self, topic_uuid: str) -> None: self.__client.unsubscribe(topic_data.get("topic")) self.__config.remove_topic(topic_uuid) if topic_uuid in self.__topic_data_generators: - self.__logger.info( - f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') + self.__logger.info(f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') del self.__topic_data_generators[topic_uuid] else: - self.__logger.info( - f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') + self.__logger.info(f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') del self.__proto_topic_data_generators[topic_uuid] # Adds topic to config (and saves it into config file). # If publishing thread was already started, it will take the topic into account. def add_topic(self, topic_config: dict) -> str: uuid = self.__config.put_topic(topic_config) - self.__logger.info( - f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') + self.__logger.info(f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') self.__topic_data_generators[uuid] = MqttSimDataGenerator( topic_config.get("data_format") ) @@ -208,19 +202,16 @@ def edit(self, topic_uuid, new_data) -> None: def send_single_message(self, topic_uuid) -> None: if not self.is_connected_to_broker(): - self.__logger.error( - "Trying to send message when not connected to broker.") + self.__logger.error("Trying to send message when not connected to broker.") return topic_data = self.__config.get_topic_data(topic_uuid) if topic_uuid in self.__topic_data_generators: - self.__logger.info( - f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') + self.__logger.info(f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') message = self.__topic_data_generators.get( topic_uuid).next_message() self.__client.publish(topic_data.get("topic"), message) else: - self.__logger.info( - f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') + self.__logger.info(f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') gen = self.__proto_topic_data_generators[topic_uuid] proto_message = gen.get_next_message() self.__client.publish( @@ -228,8 +219,7 @@ def send_single_message(self, topic_uuid) -> None: def add_proto_topic(self, topic_config: dict) -> None: uuid = self.__config.put_topic(topic_config) - self.__logger.info( - f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') + self.__logger.info(f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') self.__proto_topic_data_generators[uuid] = MqttProtoGenerator( topic_config.get("message"), topic_config.get("file")) self.__client.subscribe(topic_config.get("topic")) diff --git a/src/protofiles/__init__.py b/src/protofiles/__init__.py index 7f2f0fe..2def8af 100644 --- a/src/protofiles/__init__.py +++ b/src/protofiles/__init__.py @@ -1,8 +1,7 @@ -from os.path import dirname, basename, isfile, join -import os -import sys -import glob -modules = glob.glob(join(dirname(__file__), "*.py")) +from os.path import dirname, basename, isfile, join, abspath, dirname +from sys import path +from glob import glob +modules = glob(join(dirname(__file__), "*.py")) __all__ = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) +path.insert(0, abspath(dirname(__file__))) From ffb2f0374c8f2b1cb99aa865c00948a25d6b6c5b Mon Sep 17 00:00:00 2001 From: Maciej Janicki Date: Sun, 9 Jun 2024 17:16:48 +0200 Subject: [PATCH 6/7] Update: changed how csv columns should be named --- readme.md | 2 +- src/mqttprotogenerator.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 07055fb..5868a63 100644 --- a/readme.md +++ b/readme.md @@ -135,7 +135,7 @@ message Message2 { example csv file for Message1 would look like this: ```csv -my_int,Message2.my_int +my_int,nested_message.my_int 123,456 456,1312 123123,45123 diff --git a/src/mqttprotogenerator.py b/src/mqttprotogenerator.py index 7e99c14..5532015 100644 --- a/src/mqttprotogenerator.py +++ b/src/mqttprotogenerator.py @@ -113,24 +113,23 @@ def construct_message(self, message_name: str, fields: dict) -> Message: message_constructor = self.message_constructors[message_name] new_message = message_constructor() for field_descriptor in message_constructor.DESCRIPTOR.fields: - message_type = field_descriptor.message_type if field_descriptor.message_type is not None: passed_on_fields = dict() for field in fields.keys(): - if message_type.name not in field: + if field_descriptor.name not in field: continue sub_fields = field.split('.') try: new_field_name = sub_fields[sub_fields.index( - message_type.name)+1:] + field_descriptor.name)+1:] except ValueError: if ' ' in field: MqttProtoGenerator.logger.error( f"ERROR: Field {field} contains spaces!!! You should not do that!") else: MqttProtoGenerator.logger.error( - f"ERROR: Field with key \'{field}\' not found in message \'{message_name}\'.") + f"ERROR: Field with key \'{field}\' not found \'{field_descriptor.name}\'.") return new_message passed_on_fields['.'.join( new_field_name)] = fields[field] From 4fb204272145b3b40018ab91817f0cfe06fb3993 Mon Sep 17 00:00:00 2001 From: Maciej Janicki Date: Mon, 10 Jun 2024 20:09:32 +0200 Subject: [PATCH 7/7] Feature: abstract layer between data generation and mqttsimulator itself --- src/abstractdatagenerator.py | 6 ++ src/arbiter.py | 11 ++++ src/gui/ui.py | 15 +++-- ...mdatagenerator.py => jsondatagenerator.py} | 8 ++- src/mqttsim.py | 65 +++++-------------- ...qttprotogenerator.py => protogenerator.py} | 32 ++++----- 6 files changed, 64 insertions(+), 73 deletions(-) create mode 100644 src/abstractdatagenerator.py create mode 100644 src/arbiter.py rename src/{mqttsimdatagenerator.py => jsondatagenerator.py} (98%) rename src/{mqttprotogenerator.py => protogenerator.py} (85%) diff --git a/src/abstractdatagenerator.py b/src/abstractdatagenerator.py new file mode 100644 index 0000000..87a7e87 --- /dev/null +++ b/src/abstractdatagenerator.py @@ -0,0 +1,6 @@ +class DataGenerator(): + def __init__(self, config): + pass + + def next_message(self) -> str: + pass diff --git a/src/arbiter.py b/src/arbiter.py new file mode 100644 index 0000000..1e0b0c2 --- /dev/null +++ b/src/arbiter.py @@ -0,0 +1,11 @@ +from abstractdatagenerator import DataGenerator +from jsondatagenerator import JsonDataGenerator +from protogenerator import ProtoDataGenerator + +def get_data_generator(config) -> DataGenerator: + if "data_format" in config: + return JsonDataGenerator(config) + elif "message" in config: + return ProtoDataGenerator(config) + + diff --git a/src/gui/ui.py b/src/gui/ui.py index faf7427..084c4ed 100644 --- a/src/gui/ui.py +++ b/src/gui/ui.py @@ -21,7 +21,7 @@ import icons.generated.icons from gui.generated.addprototopicdialog import Ui_AddProtoTopicDialog from gui.generated.choosetopicdialog import Ui_ChooseTopicDialog -from mqttprotogenerator import MqttProtoGenerator +from protogenerator import ProtoDataGenerator class MqttSimTopicToolButton(QToolButton): @@ -199,9 +199,10 @@ def __init__(self, topic_name: str, topic_data: dict): self.setWindowIcon(QIcon(":/icons/mqtt.svg")) def __set_topic_values(self, topic_name, topic_data) -> None: - messages = MqttProtoGenerator.get_message_constructors() + messages = ProtoDataGenerator.get_message_constructors() current_item = None - for message_name in messages.keys(): + message_names = sorted(list(messages.keys()), key=lambda s: (not 'Msg' in s, s)) + for message_name in message_names: new_item = QListWidgetItem(message_name) self.message_list.addItem(new_item) if message_name == topic_data["message"]: @@ -303,8 +304,10 @@ def validate_input(topic_name, topic_config) -> bool: return (len(topic_name) > 0 and topic_name not in self.__config.get_topics().keys()) add_topic_window = MqttSimAddProtoTopicWindow() - messages = MqttProtoGenerator.get_message_constructors() - for message_name in messages.keys(): + messages = ProtoDataGenerator.get_message_constructors() + + message_names = sorted(list(messages.keys()), key=lambda s: (not 'Msg' in s, s)) + for message_name in message_names: add_topic_window.message_list.addItem(message_name) if add_topic_window.exec(): if add_topic_window.message_list.currentItem() is None: @@ -319,7 +322,7 @@ def validate_input(topic_name, topic_config) -> bool: "file": add_topic_window.file_line_edit.text() } if validate_input(topic_name, topic_config): - uuid = self.__sim.add_proto_topic(topic_config) + uuid = self.__sim.add_topic(topic_config) self.__add_topic_to_item_list(uuid) else: QMessageBox().critical(self, "Error!", "Invalid topic input.") diff --git a/src/mqttsimdatagenerator.py b/src/jsondatagenerator.py similarity index 98% rename from src/mqttsimdatagenerator.py rename to src/jsondatagenerator.py index 590619b..524ef68 100644 --- a/src/mqttsimdatagenerator.py +++ b/src/jsondatagenerator.py @@ -5,10 +5,12 @@ from functools import partial from datetime import datetime from copy import copy +from abstractdatagenerator import DataGenerator -class MqttSimDataGenerator: - def __init__(self, data_format: str): - self.reinitalize(data_format) + +class JsonDataGenerator(DataGenerator): + def __init__(self, config: dict): + self.reinitalize(config.get("data_format")) def next_message(self): message = self.__format_str diff --git a/src/mqttsim.py b/src/mqttsim.py index 3c99348..fea2620 100644 --- a/src/mqttsim.py +++ b/src/mqttsim.py @@ -3,10 +3,10 @@ from threading import Thread from datetime import datetime from time import sleep -from mqttsimdatagenerator import MqttSimDataGenerator +from jsondatagenerator import JsonDataGenerator +from protogenerator import ProtoDataGenerator from uuid import uuid4 -from mqttprotogenerator import MqttProtoGenerator - +from arbiter import get_data_generator class MqttSimConfig: def __init__(self, path: str): @@ -54,18 +54,10 @@ class MqttSim: def __init__(self, config: MqttSimConfig, logger: any): self.__logger = logger self.__config = config - self.__topic_data_generators = { - topic_uuid: MqttSimDataGenerator(topic_config.get("data_format")) - for topic_uuid, topic_config in self.__config.get_topics().items() - if "data_format" in topic_config - } - MqttProtoGenerator.logger = logger - self.__proto_topic_data_generators = { - topic_uuid: MqttProtoGenerator( - topic_config.get("message"), topic_config.get("file")) - for topic_uuid, topic_config in self.__config.get_topics().items() - if "message" in topic_config - } + self.__topic_data_generators = dict() + ProtoDataGenerator.logger = logger + for topic_uuid, topic_config in self.__config.get_topics().items(): + self.__topic_data_generators[topic_uuid] = get_data_generator(topic_config) self.__setup_client() self.__setup_publishing_thread() @@ -99,7 +91,10 @@ def time_diff_in_seconds(time1, time2) -> int: def __setup_client(self) -> None: def on_message(client, userdata, message) -> None: - self.__logger.info(f"Received message from broker {message.topic}.") + message_contents = str(message.payload) + message_contents = message_contents.replace('\n', ' ') + message_contents = message_contents[:3] + '[...]' + message_contents[-3:] + self.__logger.info(f"Received message from broker {message.topic}: {message_contents}.") def on_connect(client, userdata, flags, rc) -> None: topics = self.__config.get_topics() @@ -170,18 +165,13 @@ def remove_topic(self, topic_uuid: str) -> None: if topic_uuid in self.__topic_data_generators: self.__logger.info(f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') del self.__topic_data_generators[topic_uuid] - else: - self.__logger.info(f'Removed topic: {topic_data.get("topic")} [uuid={topic_uuid}].') - del self.__proto_topic_data_generators[topic_uuid] # Adds topic to config (and saves it into config file). # If publishing thread was already started, it will take the topic into account. def add_topic(self, topic_config: dict) -> str: uuid = self.__config.put_topic(topic_config) self.__logger.info(f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') - self.__topic_data_generators[uuid] = MqttSimDataGenerator( - topic_config.get("data_format") - ) + self.__topic_data_generators[uuid] = get_data_generator(topic_config) return uuid def get_logger(self) -> any: @@ -193,34 +183,13 @@ def get_config(self) -> MqttSimConfig: def edit(self, topic_uuid, new_data) -> None: self.__config.put_topic(new_data, uuid=topic_uuid) if topic_uuid in self.__topic_data_generators: - self.__topic_data_generators[topic_uuid].reinitalize( - new_data.get("data_format") - ) - else: - self.__proto_topic_data_generators[topic_uuid] = MqttProtoGenerator( - new_data.get("message"), new_data.get("file")) - + self.__topic_data_generators[topic_uuid] = get_data_generator(new_data) def send_single_message(self, topic_uuid) -> None: if not self.is_connected_to_broker(): self.__logger.error("Trying to send message when not connected to broker.") return topic_data = self.__config.get_topic_data(topic_uuid) - if topic_uuid in self.__topic_data_generators: - self.__logger.info(f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') - message = self.__topic_data_generators.get( - topic_uuid).next_message() - self.__client.publish(topic_data.get("topic"), message) - else: - self.__logger.info(f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') - gen = self.__proto_topic_data_generators[topic_uuid] - proto_message = gen.get_next_message() - self.__client.publish( - topic_data.get("topic"), proto_message.SerializeToString()) - - def add_proto_topic(self, topic_config: dict) -> None: - uuid = self.__config.put_topic(topic_config) - self.__logger.info(f'Added topic: {topic_config.get("topic")} [uuid={uuid}].') - self.__proto_topic_data_generators[uuid] = MqttProtoGenerator( - topic_config.get("message"), topic_config.get("file")) - self.__client.subscribe(topic_config.get("topic")) - return uuid + self.__logger.info(f'Publishing data on {topic_data.get("topic")} [uuid={topic_uuid}]...') + message = self.__topic_data_generators.get( + topic_uuid).next_message() + self.__client.publish(topic_data.get("topic"), message) diff --git a/src/mqttprotogenerator.py b/src/protogenerator.py similarity index 85% rename from src/mqttprotogenerator.py rename to src/protogenerator.py index 5532015..5f67757 100644 --- a/src/mqttprotogenerator.py +++ b/src/protogenerator.py @@ -6,13 +6,17 @@ from string import ascii_lowercase import pandas as pd from os import stat +from abstractdatagenerator import DataGenerator -class MqttProtoGenerator(): +class ProtoDataGenerator(DataGenerator): logger = None - def __init__(self, message_name: str, message_file_path=''): - self.message_constructors = MqttProtoGenerator.get_message_constructors() + def __init__(self, config): + self.reinitialize(config.get("message"), config.get("file")) + + def reinitialize(self, message_name: str, message_file_path=''): + self.message_constructors = ProtoDataGenerator.get_message_constructors() self.message_constructor = self.message_constructors[message_name] self.constructed_messages = None self.message_name = message_name @@ -20,8 +24,7 @@ def __init__(self, message_name: str, message_file_path=''): try: self.file_time_stamp = stat(message_file_path).st_mtime except FileNotFoundError: - MqttProtoGenerator.logger.error( - f"ERROR: File {message_file_path} not found. Defaulting to sending random messages") + ProtoDataGenerator.logger.error(f"ERROR: File {message_file_path} not found. Defaulting to sending random messages") self.message_file_path = '' return @@ -48,7 +51,7 @@ def get_random_message(self): message_type = field_descriptor.message_type if field_descriptor.message_type is not None: - gen = MqttProtoGenerator(message_type.name) + gen = ProtoDataGenerator(message_type.name) m = gen.get_random_message() attr = getattr(message, field_descriptor.name) attr.CopyFrom(m) @@ -73,12 +76,12 @@ def get_random_message(self): randint(0, 1000)) return message - def get_next_message(self): + def next_message(self): ret = None if self.constructed_messages is not None: time_stamp = stat(self.message_file_path).st_mtime if time_stamp != self.file_time_stamp: - MqttProtoGenerator.logger.info( + ProtoDataGenerator.logger.info( f"INFO: file {self.message_file_path} changed. Reconstructing messages") self.constructed_messages = self.read_message_csv( self.message_name, message_file=self.message_file_path) @@ -90,15 +93,14 @@ def get_next_message(self): 1) % len(self.constructed_messages) else: ret = self.get_random_message() - return ret + return ret.SerializeToString() def read_message_csv(self, message_name: str, message_file: str) -> list: df = None try: df = pd.read_csv(message_file) except FileNotFoundError: - MqttProtoGenerator.logger.error( - f"ERROR: File {message_file} not found. Defaulting to sending random messages") + ProtoDataGenerator.logger.error(f"ERROR: File {message_file} not found. Defaulting to sending random messages") return messages = [] @@ -125,10 +127,9 @@ def construct_message(self, message_name: str, fields: dict) -> Message: field_descriptor.name)+1:] except ValueError: if ' ' in field: - MqttProtoGenerator.logger.error( - f"ERROR: Field {field} contains spaces!!! You should not do that!") + ProtoDataGenerator.logger.error(f"ERROR: Field {field} contains spaces!!! You should not do that!") else: - MqttProtoGenerator.logger.error( + ProtoDataGenerator.logger.error( f"ERROR: Field with key \'{field}\' not found \'{field_descriptor.name}\'.") return new_message passed_on_fields['.'.join( @@ -139,8 +140,7 @@ def construct_message(self, message_name: str, fields: dict) -> Message: continue field_type = field_descriptor.type if field_descriptor.name not in fields.keys(): - MqttProtoGenerator.logger.info( - f"INFO: data for field \'{field_descriptor.name}\' not found when constructing message \'{message_name}\'.") + ProtoDataGenerator.logger.info(f"INFO: data for field \'{field_descriptor.name}\' not found when constructing message \'{message_name}\'.") continue # bolean if field_type == 8: