diff --git a/trace/widgets/data_insight_tool.py b/trace/widgets/data_insight_tool.py index be82402..5b9fae2 100644 --- a/trace/widgets/data_insight_tool.py +++ b/trace/widgets/data_insight_tool.py @@ -23,6 +23,7 @@ from qtpy.QtWidgets import ( QLabel, QWidget, + QCheckBox, QComboBox, QFileDialog, QHBoxLayout, @@ -101,6 +102,7 @@ def __init__(self, parent: QObject = None) -> None: self.unit = None self.description = None self.caget_thread = None + self._decode_as_string = False self.network_manager = QNetworkAccessManager() self.network_manager.finished.connect(self.recieve_archive_reply) @@ -123,6 +125,8 @@ def data(self, index: QModelIndex, role: Qt.ItemDataRole = Qt.DisplayRole) -> st return None elif role == Qt.DisplayRole: val = self.df.iat[index.row(), index.column()] + if index.column() == 1 and self.decode_as_string: + val = self.list_to_ascii(val) return str(val) return None @@ -131,6 +135,20 @@ def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDat if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.df.columns[section] + @property + def decode_as_string(self) -> bool: + """weather or not to show the value column as a string or raw data""" + return self._decode_as_string + + @decode_as_string.setter + def decode_as_string(self, decode_as_string: bool): + """set the value column to display as string or raw data""" + if decode_as_string != self._decode_as_string: + self._decode_as_string = decode_as_string + start = self.index(0, 1) + end = self.index(self.rowCount(), 1) + self.dataChanged.emit(start, end) + def set_description(self, description: str) -> None: """Set the description of the curve. This is called when the CAGetThread emits a result_ready signal. @@ -267,7 +285,6 @@ def recieve_archive_reply(self, reply: QNetworkReply) -> None: reply : QNetworkReply Reply to the network request made in request_archive_data """ - self.reply_recieved.emit() if reply.error() == QNetworkReply.NoError: bytes_str = reply.readAll() try: @@ -280,6 +297,7 @@ def recieve_archive_reply(self, reply: QNetworkReply) -> None: f"Request for data from archiver failed, request url: {reply.url()} retrieved header: " f"{reply.header(QNetworkRequest.ContentTypeHeader)} error: {reply.error()}" ) + self.reply_recieved.emit() reply.deleteLater() def set_archive_data(self, data_dict: dict) -> None: @@ -333,9 +351,12 @@ def export_data(self, file_path: Path, extension: str) -> None: header_dict = {"Address": self.address, "Unit": self.unit, "Description": self.description} - export_df = self.df.copy() + export_df = self.df.copy(deep=True) export_df["Datetime"] = export_df["Datetime"].astype("int64") / 1e9 + if self.decode_as_string: + export_df["Value"] = export_df["Value"].apply(self.list_to_ascii) + if extension == ".csv": file_header = "".join([f"{k}: {v}\n" for k, v in header_dict.items()]) with file_path.open("w") as file: @@ -352,6 +373,20 @@ def export_data(self, file_path: Path, extension: str) -> None: with file_path.open("w") as file: json.dump(export_dict, file, indent=2) + def has_waveform_data(self) -> bool: + """Return True if any value in the Value column is a list or numpy array""" + return bool(self.df["Value"].apply(lambda v: isinstance(v, (list, np.ndarray))).any()) + + @staticmethod + def list_to_ascii(val: list[int]) -> str: + """Convert a list of integers into an ASCII string, ignoring null characters""" + if not isinstance(val, (list, np.ndarray)): + return str(val) + characters = map(chr, list(val)) + string = "".join(characters) + string = string.replace("\u0000", "") + return string + class DataInsightTool(QWidget): """The Data Insight Tool is a standalone widget that allows users to display @@ -370,8 +405,10 @@ def __init__(self, parent: QObject, plot: PyDMArchiverTimePlot = None) -> None: self.unopened = True self.data_vis_model.reply_recieved.connect(self.loading_label.hide) + self.data_vis_model.reply_recieved.connect(self.update_decode_as_string_visibility) self.data_vis_model.description_changed.connect(self.set_meta_data) self.export_button.clicked.connect(self.export_data_to_file) + self.decode_as_string_checkbox.toggled.connect(self.set_decode_as_string) self.pv_select_box.currentIndexChanged.connect(self.get_data) self.refresh_button.clicked.connect(self.get_data) @@ -414,6 +451,11 @@ def layout_init(self) -> None: self.meta_data_label = QLabel() self.metadata_layout.addWidget(self.meta_data_label, alignment=Qt.AlignLeft) + self.decode_as_string_checkbox = QCheckBox() + self.decode_as_string_checkbox.setText("Decode As String") + self.decode_as_string_checkbox.hide() + self.metadata_layout.addWidget(self.decode_as_string_checkbox) + self.refresh_button = QPushButton("Refresh Data") self.metadata_layout.addWidget(self.refresh_button, alignment=Qt.AlignRight) self.main_layout.addLayout(self.metadata_layout) @@ -458,6 +500,19 @@ def combobox_to_curve(self, combobox_ind: int) -> ArchivePlotCurveItem: combobox_ind = self.pv_select_box.currentIndex() return self.plot.curveAtIndex(combobox_ind) + def set_decode_as_string(self) -> None: + """set the decode_as_string flag on the self.data_vis_model based off of the self.decode_as_string_checkbox + then emit the dataChanged signal from the data_vis_model for the value column""" + if self.decode_as_string_checkbox.isChecked(): + self.data_vis_model.decode_as_string = True + else: + self.data_vis_model.decode_as_string = False + + @Slot() + def update_decode_as_string_visibility(self) -> None: + """Show the decode_as_string_checkbox only when the model's Value column contains arrays""" + self.decode_as_string_checkbox.setVisible(self.data_vis_model.has_waveform_data()) + @Slot() def update_pv_select_box(self) -> None: """Populate the pv_select_box with all curves in the plot. This is called @@ -509,6 +564,7 @@ def get_data(self, combobox_index: int = -1) -> None: curve_item = self.combobox_to_curve(combobox_index) x_range = self.plot.getXAxis().range + self.decode_as_string_checkbox.hide() self.data_vis_model.set_all_data(curve_item, x_range) self.set_meta_data() self.loading_label.show()