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..5868a63 100644
--- a/readme.md
+++ b/readme.md
@@ -112,6 +112,35 @@ 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'
+# 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,nested_message.my_int
+123,456
+456,1312
+123123,45123
+```
+
## Screenshots
Keep in mind that the look of the app is dependent on user's system - QT uses native components.
@@ -125,3 +154,4 @@ Main window
Add topic window
+
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/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 7ee8577..084c4ed 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 protogenerator import ProtoDataGenerator
class MqttSimTopicToolButton(QToolButton):
@@ -39,7 +42,8 @@ def __init__(self):
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(
@@ -180,6 +184,43 @@ def set_topic_name(self, new_topic_name: str) -> None:
self.topic = new_topic_name
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 = ProtoDataGenerator.get_message_constructors()
+ current_item = None
+ 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"]:
+ 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__()
@@ -212,12 +253,12 @@ def on_broker_connect_btn_clicked() -> 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(
- "MainWindow", "Disconnect from broker", None
- )
+ "MainWindow", "Disconnect from broker", None)
)
def on_clear_logs_btn_clicked() -> None:
@@ -225,9 +266,21 @@ 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
-
+
add_topic_window = MqttSimAddTopicWindow()
while True:
if add_topic_window.exec() == QDialog.Accepted:
@@ -246,6 +299,34 @@ 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 = 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:
+ 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_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())
@@ -271,7 +352,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)
@@ -279,20 +360,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/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 5235cbf..fea2620 100644
--- a/src/mqttsim.py
+++ b/src/mqttsim.py
@@ -3,8 +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 arbiter import get_data_generator
class MqttSimConfig:
def __init__(self, path: str):
@@ -28,7 +30,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"),
@@ -52,10 +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()
- }
+ 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()
@@ -89,9 +91,15 @@ 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}'.")
+ 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()
+ for topic_uuid in topics:
+ self.__client.subscribe(topics[topic_uuid]["topic"])
if rc == CONNACK_ACCEPTED:
self.__logger.info("Connected to broker.")
else:
@@ -134,7 +142,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
@@ -154,17 +162,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}].")
- 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]
# 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.__logger.info(f'Added topic: {topic_config.get("topic")} [uuid={uuid}].')
+ self.__topic_data_generators[uuid] = get_data_generator(topic_config)
return uuid
def get_logger(self) -> any:
@@ -175,15 +182,14 @@ 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] = 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)
- 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.__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/protofiles/__init__.py b/src/protofiles/__init__.py
new file mode 100644
index 0000000..2def8af
--- /dev/null
+++ b/src/protofiles/__init__.py
@@ -0,0 +1,7 @@
+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')]
+path.insert(0, abspath(dirname(__file__)))
diff --git a/src/protogenerator.py b/src/protogenerator.py
new file mode 100644
index 0000000..5f67757
--- /dev/null
+++ b/src/protogenerator.py
@@ -0,0 +1,166 @@
+from pkgutil import iter_modules
+import protofiles
+import importlib
+from google.protobuf.message import Message
+from random import getrandbits, randint, random, choice, randbytes
+from string import ascii_lowercase
+import pandas as pd
+from os import stat
+from abstractdatagenerator import DataGenerator
+
+
+class ProtoDataGenerator(DataGenerator):
+ logger = None
+
+ 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
+ if message_file_path != '':
+ try:
+ self.file_time_stamp = stat(message_file_path).st_mtime
+ except FileNotFoundError:
+ ProtoDataGenerator.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
+
+ 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 = ProtoDataGenerator(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, getrandbits(1))
+ # string
+ elif field_type == 9:
+ setattr(message, field_descriptor.name, ''.join(
+ 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())
+ # bytes
+ elif field_type == 12:
+ setattr(message, field_descriptor.name, randbytes(10))
+ # int
+ else:
+ setattr(message, field_descriptor.name,
+ randint(0, 1000))
+ return message
+
+ 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:
+ 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)
+ 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)
+ else:
+ ret = self.get_random_message()
+ 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:
+ ProtoDataGenerator.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 field_descriptor.name not in field:
+ continue
+ sub_fields = field.split('.')
+ try:
+ new_field_name = sub_fields[sub_fields.index(
+ field_descriptor.name)+1:]
+ except ValueError:
+ if ' ' in field:
+ ProtoDataGenerator.logger.error(f"ERROR: Field {field} contains spaces!!! You should not do that!")
+ else:
+ ProtoDataGenerator.logger.error(
+ f"ERROR: Field with key \'{field}\' not found \'{field_descriptor.name}\'.")
+ return new_message
+ 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():
+ ProtoDataGenerator.logger.info(f"INFO: 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
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