nbs-livetable 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ from qtpy.QtWidgets import (
2
+ QWidget,
3
+ QLabel,
4
+ QApplication,
5
+ QVBoxLayout,
6
+ QMainWindow,
7
+ QCheckBox,
8
+ QHBoxLayout,
9
+ )
10
+ from qtpy.QtCore import QThread, Slot, Signal, QObject, Qt, QTimer
11
+ from qtpy.QtGui import QFontInfo, QFont
12
+ import argparse
13
+ import queue
14
+ import time
15
+ from .kafka_table import qt_kafka_table
16
+
17
+ # from bluesky_widgets.qt.run_engine_client import QtReConsoleMonitor
18
+
19
+ from .simpleConsoleMonitor import QtReConsoleMonitor
20
+ import sys
21
+
22
+
23
+ class LiveTableModel(QWidget):
24
+ def __init__(
25
+ self,
26
+ beamline_acronym,
27
+ config_file,
28
+ topic_string="bluesky.runengine.documents",
29
+ parent=None,
30
+ ):
31
+ super().__init__(parent)
32
+ self.msg_queue = queue.Queue()
33
+ self.kafka_dispatcher, self.callback = qt_kafka_table(
34
+ beamline_acronym,
35
+ config_file,
36
+ topic_string,
37
+ self.newMsg,
38
+ )
39
+ self.kafka_dispatcher.setParent(self)
40
+ self.kafka_dispatcher.start()
41
+
42
+ label = QLabel("Test")
43
+ vbox = QVBoxLayout()
44
+ vbox.addWidget(label)
45
+ self.setLayout(vbox)
46
+ self.destroyed.connect(lambda: self.stop_console_output_monitoring)
47
+
48
+ def newMsg(self, msg):
49
+ self.msg_queue.put(msg)
50
+ # print(msg)
51
+
52
+ def start_console_output_monitoring(self):
53
+ print("Start Console Output Monitoring")
54
+ self._stop_console_monitor = False
55
+
56
+ def stop_console_output_monitoring(self):
57
+ print("Stop Console Monitoring")
58
+ self.kafka_dispatcher.stop()
59
+ self._stop_console_monitor = True
60
+
61
+ def continue_polling(self):
62
+ return not self._stop_console_monitor
63
+
64
+ # def console_monitoring_thread(self, *, callback):
65
+ def console_monitoring_thread(self):
66
+ # print("Monitoring")
67
+ for n in range(5):
68
+ try:
69
+ msg = self.msg_queue.get(timeout=0.2)
70
+ msg = msg.rstrip("\n") + "\n"
71
+ msgtime = time.time()
72
+ return msgtime, msg
73
+
74
+ except queue.Empty:
75
+ pass
76
+ except Exception as ex:
77
+ print(f"Exception occurred: {ex}")
78
+
79
+ if self._stop_console_monitor:
80
+ print("Stop monitoring!")
81
+ return None, None
82
+
83
+
84
+ class QtKafkaTableTab(QWidget):
85
+ name = "Live Table"
86
+
87
+ def __init__(self, model, parent=None):
88
+ super().__init__(parent)
89
+ self.config = model.settings.gui_config
90
+ bl_acronym = self.config.get("kafka", {}).get("bl_acronym", "")
91
+ kafka_config = self.config.get("kafka", {}).get("config_file", "")
92
+ topic_string = self.config.get("kafka", {}).get(
93
+ "topic_string", "bluesky.runengine.documents"
94
+ )
95
+ self.kafkaTable = LiveTableModel(
96
+ bl_acronym, kafka_config, topic_string=topic_string
97
+ )
98
+ self.kafkaMonitor = QtReConsoleMonitor(self.kafkaTable, self)
99
+
100
+ # Set up monospace font
101
+ font = self.kafkaMonitor._text_edit.font()
102
+ font.setFamily("Monospace")
103
+ font.setStyleHint(QFont.Monospace)
104
+ self.kafkaMonitor._text_edit.setFont(font)
105
+
106
+ # Create baseline control
107
+ self.baselineCheck = QCheckBox("Show Baseline", self)
108
+ self.baselineCheck.setChecked(True)
109
+ self.baselineCheck.stateChanged.connect(self.toggleBaseline)
110
+
111
+ vbox = QVBoxLayout()
112
+ vbox.addWidget(QLabel("Kafka Table Monitor"))
113
+
114
+ # Add controls in a horizontal layout
115
+ controls = QHBoxLayout()
116
+ controls.addWidget(self.baselineCheck)
117
+ controls.addStretch() # Push controls to the left
118
+ vbox.addLayout(controls)
119
+
120
+ vbox.addWidget(self.kafkaMonitor)
121
+ self.setLayout(vbox)
122
+
123
+ font = self.kafkaMonitor._text_edit.font()
124
+ actual_font = QFontInfo(font)
125
+ print(f"Font used: {actual_font.family()}, Font Desired: {font.family()}")
126
+
127
+ def toggleBaseline(self, state):
128
+ """Toggle baseline readings on/off."""
129
+ if hasattr(self.kafkaTable, "callback"):
130
+ self.kafkaTable.callback.baseline_enabled = bool(state)
131
+
132
+
133
+ def main():
134
+ parser = argparse.ArgumentParser(description="Kafka LiveTable Monitor")
135
+ parser.add_argument(
136
+ "--bl", required=True, help="Beamline acronym used for kafka topic"
137
+ )
138
+ parser.add_argument(
139
+ "--config-file",
140
+ default="/etc/bluesky/kafka.yml",
141
+ help="kafka config file location",
142
+ )
143
+ parser.add_argument(
144
+ "--topic-string",
145
+ default="bluesky.runengine.documents",
146
+ help="string to be combined with acronym to create topic",
147
+ )
148
+
149
+ args = parser.parse_args()
150
+ app = QApplication([])
151
+
152
+ main_window = QMainWindow()
153
+ # model = LiveTableModel(args.bl, args.config_file, topic_string=args.topic_string)
154
+ # central_widget = LiveTableModel2(args.bl, args.config_file, topic_string=args.topic_string)
155
+ model = LiveTableModel(args.bl, args.config_file, topic_string=args.topic_string)
156
+ # central_widget = LiveTableModel3(args.bl, args.config_file, topic_string=args.topic_string)
157
+ central_widget = QtReConsoleMonitor(model, main_window)
158
+ # Ensure the font family is set to a monospace font that exists on the system
159
+ font = central_widget._text_edit.font()
160
+ font.setFamily("Monospace")
161
+ font.setStyleHint(QFont.Monospace)
162
+ central_widget._text_edit.setFont(font)
163
+ font = central_widget._text_edit.font()
164
+ actual_font = QFontInfo(font)
165
+ print(f"Font used: {actual_font.family()}, Font Desired: {font.family()}")
166
+ # model.setParent(central_widget)
167
+ # central_widget.start_console_output_monitoring()
168
+ main_window.setCentralWidget(central_widget)
169
+ central_widget.destroyed.connect(lambda: model.stop_console_output_monitoring)
170
+ main_window.show()
171
+ app_ref = app.exec_()
172
+ sys.exit(app_ref)
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()
@@ -0,0 +1,151 @@
1
+ from qtpy.QtWidgets import (
2
+ QWidget,
3
+ QLabel,
4
+ QApplication,
5
+ QVBoxLayout,
6
+ QMainWindow,
7
+ QCheckBox,
8
+ QHBoxLayout,
9
+ )
10
+ from qtpy.QtCore import QThread, Slot, Signal, QObject, Qt, QTimer
11
+ from qtpy.QtGui import QFontInfo, QFont
12
+
13
+ # import argparse
14
+ import queue
15
+ import time
16
+
17
+ # from .kafka_table import qt_kafka_table
18
+ from .zmq_table import qt_zmq_table
19
+
20
+ # from bluesky_widgets.qt.run_engine_client import QtReConsoleMonitor
21
+
22
+ from .simpleConsoleMonitor import QtReConsoleMonitor
23
+ import sys
24
+
25
+
26
+ class LiveTableModel(QWidget):
27
+ def __init__(
28
+ self,
29
+ parent=None,
30
+ ):
31
+ super().__init__(parent)
32
+ self.msg_queue = queue.Queue()
33
+ zmq_dispatcher, callback = qt_zmq_table(self.newMsg)
34
+ self.zmq_dispatcher = zmq_dispatcher
35
+ self.callback = callback
36
+ self.zmq_dispatcher.setParent(self)
37
+ self.zmq_dispatcher.start()
38
+
39
+ label = QLabel("Test")
40
+ vbox = QVBoxLayout()
41
+ vbox.addWidget(label)
42
+ self.setLayout(vbox)
43
+ self.destroyed.connect(lambda: self.stop_console_output_monitoring)
44
+
45
+ def newMsg(self, msg):
46
+ self.msg_queue.put(msg)
47
+ # print(msg)
48
+
49
+ def start_console_output_monitoring(self):
50
+ print("Start Console Output Monitoring")
51
+ self._stop_console_monitor = False
52
+
53
+ def stop_console_output_monitoring(self):
54
+ print("Stop Console Monitoring")
55
+ self.zmq_dispatcher.stop()
56
+ self._stop_console_monitor = True
57
+
58
+ def continue_polling(self):
59
+ return not self._stop_console_monitor
60
+
61
+ # def console_monitoring_thread(self, *, callback):
62
+ def console_monitoring_thread(self):
63
+ # print("Monitoring")
64
+ for n in range(5):
65
+ try:
66
+ msg = self.msg_queue.get(timeout=0.2)
67
+ msg = msg.rstrip("\n") + "\n"
68
+ msgtime = time.time()
69
+ return msgtime, msg
70
+
71
+ except queue.Empty:
72
+ pass
73
+ except Exception as ex:
74
+ print(f"Exception occurred: {ex}")
75
+
76
+ if self._stop_console_monitor:
77
+ print("Stop monitoring!")
78
+ return None, None
79
+
80
+
81
+ class QtZMQTableTab(QWidget):
82
+ name = "Live Table"
83
+
84
+ def __init__(self, model, parent=None):
85
+ super().__init__(parent)
86
+ self.config = model.settings.gui_config
87
+ self.zmqTable = LiveTableModel(self)
88
+ self.zmqMonitor = QtReConsoleMonitor(self.zmqTable, self)
89
+
90
+ # Printing the font and font family used by self.zmqMonitor
91
+ font = self.zmqMonitor._text_edit.font()
92
+ font.setFamily("Monospace")
93
+ font.setStyleHint(QFont.Monospace)
94
+ self.zmqMonitor._text_edit.setFont(font)
95
+
96
+ # Create baseline control
97
+ self.baselineCheck = QCheckBox("Show Baseline", self)
98
+ self.baselineCheck.setChecked(True)
99
+ self.baselineCheck.stateChanged.connect(self.toggleBaseline)
100
+
101
+ vbox = QVBoxLayout()
102
+ vbox.addWidget(QLabel("ZMQ Table Monitor"))
103
+
104
+ # Add controls in a horizontal layout
105
+ controls = QHBoxLayout()
106
+ controls.addWidget(self.baselineCheck)
107
+ controls.addStretch() # Push controls to the left
108
+ vbox.addLayout(controls)
109
+
110
+ vbox.addWidget(self.zmqMonitor)
111
+ self.setLayout(vbox)
112
+
113
+ font = self.zmqMonitor._text_edit.font()
114
+ actual_font = QFontInfo(font)
115
+ print(f"Font used: {actual_font.family()}, Font Desired: {font.family()}")
116
+
117
+ def toggleBaseline(self, state):
118
+ """Toggle baseline readings on/off."""
119
+ print("Toggle Baseline")
120
+ if hasattr(self.zmqTable, "callback"):
121
+ print("Baseline enabled: ", bool(state))
122
+ self.zmqTable.callback.baseline_enabled = bool(state)
123
+ else:
124
+ print("No callback found")
125
+
126
+
127
+ def main():
128
+ app = QApplication([])
129
+
130
+ main_window = QMainWindow()
131
+ model = LiveTableModel()
132
+ central_widget = QtReConsoleMonitor(model, main_window)
133
+ # Ensure the font family is set to a monospace font that exists on the system
134
+ font = central_widget._text_edit.font()
135
+ font.setFamily("Monospace")
136
+ font.setStyleHint(QFont.Monospace)
137
+ central_widget._text_edit.setFont(font)
138
+ font = central_widget._text_edit.font()
139
+ actual_font = QFontInfo(font)
140
+ print(f"Font used: {actual_font.family()}, Font Desired: {font.family()}")
141
+ # model.setParent(central_widget)
142
+ # central_widget.start_console_output_monitoring()
143
+ main_window.setCentralWidget(central_widget)
144
+ central_widget.destroyed.connect(lambda: model.stop_console_output_monitoring)
145
+ main_window.show()
146
+ app_ref = app.exec_()
147
+ sys.exit(app_ref)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
File without changes
@@ -0,0 +1,87 @@
1
+ import nslsii.kafka_utils
2
+ from bluesky_kafka import RemoteDispatcher
3
+ from bluesky_widgets.qt.kafka_dispatcher import QtRemoteDispatcher
4
+ from bluesky.callbacks.best_effort import BestEffortCallback
5
+ from .lessEffortCallback import LessEffortCallback
6
+ import uuid
7
+ import argparse
8
+
9
+
10
+ def kafka_table(
11
+ beamline_acronym,
12
+ config_file,
13
+ topic_string="bluesky.documents",
14
+ out=print,
15
+ continue_polling=None,
16
+ ):
17
+ bec = LessEffortCallback(out=out)
18
+ # bec = BestEffortCallback()
19
+ kafka_config = nslsii.kafka_utils._read_bluesky_kafka_config_file(
20
+ config_file_path=config_file
21
+ )
22
+
23
+ # this consumer should not be in a group with other consumers
24
+ # so generate a unique consumer group id for it
25
+ unique_group_id = f"echo-{beamline_acronym}-{str(uuid.uuid4())[:8]}"
26
+
27
+ kafka_dispatcher = RemoteDispatcher(
28
+ topics=[f"{beamline_acronym}.{topic_string}"],
29
+ bootstrap_servers=",".join(kafka_config["bootstrap_servers"]),
30
+ group_id=unique_group_id,
31
+ consumer_config=kafka_config["runengine_producer_config"],
32
+ )
33
+
34
+ kafka_dispatcher.subscribe(bec)
35
+ kafka_dispatcher.start(continue_polling=continue_polling)
36
+
37
+
38
+ def qt_kafka_table(
39
+ beamline_acronym,
40
+ config_file,
41
+ topic_string="bluesky.documents",
42
+ out=print,
43
+ ):
44
+ bec = LessEffortCallback(out=out)
45
+ # bec = BestEffortCallback()
46
+ kafka_config = nslsii.kafka_utils._read_bluesky_kafka_config_file(
47
+ config_file_path=config_file
48
+ )
49
+
50
+ # this consumer should not be in a group with other consumers
51
+ # so generate a unique consumer group id for it
52
+ unique_group_id = f"echo-{beamline_acronym}-{str(uuid.uuid4())[:8]}"
53
+
54
+ kafka_dispatcher = QtRemoteDispatcher(
55
+ topics=[f"{beamline_acronym}.{topic_string}"],
56
+ bootstrap_servers=",".join(kafka_config["bootstrap_servers"]),
57
+ group_id=unique_group_id,
58
+ consumer_config=kafka_config["runengine_producer_config"],
59
+ )
60
+
61
+ kafka_dispatcher.subscribe(bec)
62
+ return kafka_dispatcher, bec
63
+
64
+
65
+ def main():
66
+ parser = argparse.ArgumentParser(description="Kafka LiveTable Monitor")
67
+ parser.add_argument(
68
+ "--bl", required=True, help="Beamline acronym used for kafka topic"
69
+ )
70
+ parser.add_argument(
71
+ "--config-file",
72
+ default="/etc/bluesky/kafka.yml",
73
+ help="kafka config file location",
74
+ )
75
+ parser.add_argument(
76
+ "--topic-string",
77
+ default="bluesky.runengine.documents",
78
+ help="string to be combined with acronym to create topic",
79
+ )
80
+
81
+ args = parser.parse_args()
82
+
83
+ kafka_table(args.bl, args.config_file, args.topic_string)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
@@ -0,0 +1,258 @@
1
+ """
2
+ Best Effort Callback.
3
+ For instructions on how to test in a simulated environment please see:
4
+ tests/interactive/best_effort_cb.py
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+ import time
10
+ from datetime import datetime
11
+ from io import StringIO
12
+ from warnings import warn
13
+
14
+
15
+ from bluesky.callbacks.core import LiveTable, make_class_safe, CallbackBase
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def hinted_fields(descriptor):
21
+ # Figure out which columns to put in the table.
22
+ obj_names = list(descriptor["object_keys"])
23
+ # We will see if these objects hint at whether
24
+ # a subset of their data keys ('fields') are interesting. If they
25
+ # did, we'll use those. If these didn't, we know that the RunEngine
26
+ # *always* records their complete list of fields, so we can use
27
+ # them all unselectively.
28
+ columns = []
29
+ for obj_name in obj_names:
30
+ try:
31
+ fields = descriptor.get("hints", {}).get(obj_name, {})["fields"]
32
+ except KeyError:
33
+ fields = descriptor["object_keys"][obj_name]
34
+ columns.extend(fields)
35
+ return columns
36
+
37
+
38
+ @make_class_safe(logger=logger)
39
+ class LessEffortCallback(CallbackBase):
40
+ def __init__(
41
+ self,
42
+ *,
43
+ fig_factory=None,
44
+ table_enabled=True,
45
+ out=print,
46
+ **kwargs,
47
+ ):
48
+ super().__init__(**kwargs)
49
+ # internal state
50
+ self._start_doc = None
51
+ self._descriptors = {}
52
+ self._table = None
53
+ self._heading_enabled = True
54
+ self._table_enabled = table_enabled
55
+ self._baseline_enabled = True
56
+ self._out = out
57
+ self._cleanup_motor_heuristic = False
58
+ self._stream_names_seen = set()
59
+ self._started = False
60
+
61
+ # hack to handle the bottom border of the table
62
+ self._buffer = StringIO()
63
+ self._baseline_toggle = True
64
+
65
+ @property
66
+ def baseline_enabled(self):
67
+ """Whether baseline readings are printed."""
68
+ return self._baseline_enabled
69
+
70
+ @baseline_enabled.setter
71
+ def baseline_enabled(self, enabled):
72
+ """Enable or disable baseline readings."""
73
+ self._baseline_enabled = bool(enabled)
74
+
75
+ def enable_heading(self):
76
+ "Print timestamp and IDs at the top of a run."
77
+ self._heading_enabled = True
78
+
79
+ def disable_heading(self):
80
+ "Opposite of enable_heading()"
81
+ self._heading_enabled = False
82
+
83
+ def enable_table(self):
84
+ "Print hinted readings from the 'primary' stream in a LiveTable."
85
+ self._table_enabled = True
86
+
87
+ def disable_table(self):
88
+ "Opposite of enable_table()"
89
+ self._table_enabled = False
90
+
91
+ def __call__(self, name, doc, *args, **kwargs):
92
+ if not (self._table_enabled or self._baseline_enabled):
93
+ return
94
+ super().__call__(name, doc, *args, **kwargs)
95
+
96
+ def start(self, doc):
97
+ self.clear()
98
+ print("Start Doc Received")
99
+ self._start_doc = doc
100
+ self.plan_hints = doc.get("hints", {})
101
+
102
+ # Prepare a guess about the dimensions (independent variables) in case
103
+ # we need it.
104
+ motors = self._start_doc.get("motors")
105
+ if motors is not None:
106
+ GUESS = [([motor], "primary") for motor in motors]
107
+ else:
108
+ GUESS = [(["time"], "primary")]
109
+
110
+ # Ues the guess if there is not hint about dimensions.
111
+ dimensions = self.plan_hints.get("dimensions")
112
+ if dimensions is None:
113
+ self._cleanup_motor_heuristic = True
114
+ dimensions = GUESS
115
+
116
+ # We can only cope with all the dimensions belonging to the same
117
+ # stream unless we resample. We are not doing to handle that yet.
118
+ if len(set(d[1] for d in dimensions)) != 1: # noqa: C401
119
+ self._cleanup_motor_heuristic = True
120
+ dimensions = GUESS # Fall back on our GUESS.
121
+ warn(
122
+ "We are ignoring the dimensions hinted because we cannot "
123
+ "combine streams."
124
+ ) # noqa: B028
125
+
126
+ # for each dimension, choose one field only
127
+ # the plan can supply a list of fields. It's assumed the first
128
+ # of the list is always the one plotted against
129
+ self.dim_fields = []
130
+ for fields, stream_name in dimensions:
131
+ try:
132
+ self.dim_fields.append(fields[0])
133
+ except:
134
+ continue
135
+
136
+ # make distinction between flattened fields and plotted fields
137
+ # motivation for this is that when plotting, we find dependent variable
138
+ # by finding elements that are not independent variables
139
+ self.all_dim_fields = [
140
+ field for fields, stream_name in dimensions for field in fields
141
+ ]
142
+
143
+ _, self.dim_stream = dimensions[0]
144
+
145
+ # Print heading.
146
+ tt = datetime.fromtimestamp(self._start_doc["time"]).utctimetuple()
147
+ if self._heading_enabled:
148
+ self._out(
149
+ "\n\nTransient Scan ID: {0} Time: {1}".format( # noqa: UP030
150
+ self._start_doc.get("scan_id", ""),
151
+ time.strftime("%Y-%m-%d %H:%M:%S", tt),
152
+ )
153
+ )
154
+ self._out(
155
+ "Persistent Unique Scan ID: '{0}'".format(self._start_doc["uid"])
156
+ ) # noqa: UP030
157
+ self._started = True
158
+
159
+ def descriptor(self, doc):
160
+ if not self._started:
161
+ return
162
+ self._descriptors[doc["uid"]] = doc
163
+ stream_name = doc.get("name", "primary") # fall back for old docs
164
+
165
+ if stream_name not in self._stream_names_seen:
166
+ self._stream_names_seen.add(stream_name)
167
+ if self._table_enabled:
168
+ self._out(f"New stream: {stream_name!r}")
169
+
170
+ columns = hinted_fields(doc)
171
+
172
+ # ## This deals with old documents. ## #
173
+ if stream_name == "primary" and self._cleanup_motor_heuristic:
174
+ # We stashed object names in self.dim_fields, which we now need to
175
+ # look up the actual fields for.
176
+ self._cleanup_motor_heuristic = False
177
+ fixed_dim_fields = []
178
+ for obj_name in self.dim_fields:
179
+ # Special case: 'time' can be a dim_field, but it's not an
180
+ # object name. Just add it directly to the list of fields.
181
+ if obj_name == "time":
182
+ fixed_dim_fields.append("time")
183
+ continue
184
+ try:
185
+ fields = doc.get("hints", {}).get(obj_name, {})["fields"]
186
+ except KeyError:
187
+ fields = doc["object_keys"][obj_name]
188
+ fixed_dim_fields.extend(fields)
189
+ self.dim_fields = fixed_dim_fields
190
+
191
+ # Ensure that no independent variables ('dimensions') are
192
+ # duplicated here.
193
+ columns = [c for c in columns if c not in self.all_dim_fields]
194
+
195
+ # ## TABLE ## #
196
+ if stream_name == self.dim_stream:
197
+ if self._table_enabled:
198
+ # plot everything, independent or dependent variables
199
+ self._table = LiveTable(
200
+ list(self.all_dim_fields) + columns,
201
+ separator_lines=False,
202
+ out=self._out,
203
+ )
204
+ self._table("start", self._start_doc)
205
+ self._table("descriptor", doc)
206
+
207
+ def event(self, doc):
208
+ if not self._started:
209
+ return
210
+ descriptor = self._descriptors[doc["descriptor"]]
211
+ if descriptor.get("name") == "primary":
212
+ if self._table is not None:
213
+ self._table("event", doc)
214
+
215
+ # Show the baseline readings.
216
+ if descriptor.get("name") == "baseline":
217
+ columns = hinted_fields(descriptor)
218
+ self._baseline_toggle = not self._baseline_toggle
219
+ if self._baseline_enabled:
220
+ border = "+" + "-" * 32 + "+" + "-" * 32 + "+"
221
+ if self._baseline_toggle:
222
+ print("End-of-run baseline readings:", file=self._buffer)
223
+ print(border, file=self._buffer)
224
+ else:
225
+ self._out("Start-of-run baseline readings:")
226
+ self._out(border)
227
+ for k, v in doc["data"].items():
228
+ if k not in columns:
229
+ continue
230
+ if self._baseline_toggle:
231
+ print(f"| {k:>30} | {v:<30} |", file=self._buffer)
232
+ else:
233
+ self._out(f"| {k:>30} | {v:<30} |")
234
+ if self._baseline_toggle:
235
+ print(border, file=self._buffer)
236
+ else:
237
+ self._out(border)
238
+
239
+ def stop(self, doc):
240
+ if not self._started:
241
+ return
242
+ if self._table is not None:
243
+ self._table("stop", doc)
244
+
245
+ if self._baseline_enabled:
246
+ # Print baseline below bottom border of table.
247
+ self._buffer.seek(0)
248
+ self._out(self._buffer.read())
249
+ self._out("\n")
250
+ self._started = False
251
+
252
+ def clear(self):
253
+ self._start_doc = None
254
+ self._descriptors.clear()
255
+ self._stream_names_seen.clear()
256
+ self._table = None
257
+ self._buffer = StringIO()
258
+ self._baseline_toggle = True
@@ -0,0 +1,174 @@
1
+ from qtpy.QtCore import Qt, QTimer
2
+ from qtpy.QtGui import (
3
+ QFont,
4
+ QFontMetrics,
5
+ QPalette,
6
+ QTextCursor,
7
+ )
8
+ from qtpy.QtWidgets import (
9
+ QPlainTextEdit,
10
+ QWidget,
11
+ QVBoxLayout,
12
+ QHBoxLayout,
13
+ QLabel,
14
+ QLineEdit,
15
+ QCheckBox,
16
+ QPushButton,
17
+ )
18
+ from qtpy.QtGui import QIntValidator
19
+ from bluesky_widgets.qt.threading import FunctionWorker
20
+
21
+
22
+ class PushButtonMinimumWidth(QPushButton):
23
+ """
24
+ Push button minimum width necessary to fit the text
25
+ """
26
+
27
+ def __init__(self, *args, **kwargs):
28
+ super().__init__(*args, **kwargs)
29
+ text = self.text()
30
+ font = self.font()
31
+
32
+ fm = QFontMetrics(font)
33
+ text_width = fm.width(text) + 6
34
+ self.setFixedWidth(text_width)
35
+
36
+
37
+ class QtReConsoleMonitor(QWidget):
38
+ def __init__(self, model, parent=None):
39
+ super().__init__(parent)
40
+ # self.setAttribute(Qt.WA_DeleteOnClose)
41
+ print("New QtReConsoleMonitor with QPlainTextEdit")
42
+ self._max_lines = 1000
43
+
44
+ self._text_edit = QPlainTextEdit()
45
+ self._text_edit.setReadOnly(True)
46
+ self._text_edit.setMaximumBlockCount(self._max_lines)
47
+
48
+ p = self._text_edit.palette()
49
+ p.setColor(QPalette.Base, p.color(QPalette.Disabled, QPalette.Base))
50
+ self._text_edit.setPalette(p)
51
+
52
+ self._text_edit.setFont(QFont("Monospace"))
53
+
54
+ self._text_edit.verticalScrollBar().sliderPressed.connect(self._slider_pressed)
55
+ self._text_edit.verticalScrollBar().sliderReleased.connect(
56
+ self._slider_released
57
+ )
58
+ self._is_slider_pressed = False
59
+
60
+ self._pb_clear = PushButtonMinimumWidth("Clear")
61
+ self._pb_clear.clicked.connect(self._pb_clear_clicked)
62
+ self._lb_max_lines = QLabel("Max. Lines:")
63
+ self._le_max_lines = QLineEdit()
64
+ self._le_max_lines.setMaximumWidth(60)
65
+ self._le_max_lines.setAlignment(Qt.AlignHCenter)
66
+ self._le_max_lines.setText(f"{self._max_lines}")
67
+ self._le_max_lines.editingFinished.connect(self._le_max_lines_editing_finished)
68
+
69
+ self._le_max_lines_min = 10
70
+ self._le_max_lines_max = 10000
71
+ self._le_max_lines.setValidator(
72
+ QIntValidator(self._le_max_lines_min, self._le_max_lines_max)
73
+ )
74
+
75
+ self._autoscroll_enabled = True
76
+ self._cb_autoscroll = QCheckBox("Autoscroll")
77
+ self._cb_autoscroll.setChecked(True)
78
+ self._cb_autoscroll.stateChanged.connect(self._cb_autoscroll_state_changed)
79
+
80
+ vbox = QVBoxLayout()
81
+ hbox = QHBoxLayout()
82
+ hbox.addWidget(self._cb_autoscroll)
83
+ hbox.addStretch()
84
+ hbox.addWidget(self._lb_max_lines)
85
+ hbox.addWidget(self._le_max_lines)
86
+ hbox.addWidget(self._pb_clear)
87
+ vbox.addLayout(hbox)
88
+ vbox.addWidget(self._text_edit)
89
+ self.setLayout(vbox)
90
+
91
+ self.model = model
92
+ self.model.start_console_output_monitoring()
93
+ self._start_thread()
94
+ self._start_timer()
95
+ self._stop = False
96
+
97
+ def _process_new_console_output(self, result):
98
+ """
99
+ Process new console output and append to text edit.
100
+
101
+ Parameters
102
+ ----------
103
+ result : tuple
104
+ Tuple of (time, msg) where time is a timestamp and msg is the console output
105
+ """
106
+ time, msg = result
107
+
108
+ # Handle None or empty messages
109
+ if msg is None or not msg:
110
+ return
111
+
112
+ # Strip any trailing newlines to prevent doubles
113
+ msg = msg.rstrip("\n")
114
+
115
+ # Add the newline explicitly only if we're not at the start
116
+ if self._text_edit.document().isEmpty():
117
+ self._text_edit.insertPlainText(msg)
118
+ else:
119
+ self._text_edit.insertPlainText("\n" + msg)
120
+
121
+ if self._autoscroll_enabled and not self._is_slider_pressed:
122
+ self._text_edit.moveCursor(QTextCursor.End)
123
+
124
+ def _update_console_output(self):
125
+ """
126
+ Timer callback to update the display
127
+ """
128
+ if not self._stop:
129
+ self._start_timer()
130
+
131
+ def _start_timer(self):
132
+ """
133
+ Start the update timer
134
+ """
135
+ QTimer.singleShot(195, self._update_console_output)
136
+
137
+ def _slider_pressed(self):
138
+ self._is_slider_pressed = True
139
+
140
+ def _slider_released(self):
141
+ self._is_slider_pressed = False
142
+
143
+ def _is_slider_at_bottom(self):
144
+ sbar = self._text_edit.verticalScrollBar()
145
+ return sbar.value() == sbar.maximum() and self._autoscroll_enabled
146
+
147
+ def _pb_clear_clicked(self):
148
+ self._text_edit.clear()
149
+
150
+ def _le_max_lines_editing_finished(self):
151
+ v = int(self._le_max_lines.text())
152
+ v = max(min(v, self._le_max_lines_max), self._le_max_lines_min)
153
+ self._le_max_lines.setText(f"{v}")
154
+ self._max_lines = v
155
+ self._text_edit.setMaximumBlockCount(v)
156
+
157
+ def _cb_autoscroll_state_changed(self, state):
158
+ self._autoscroll_enabled = state == Qt.Checked
159
+
160
+ def _start_thread(self):
161
+ """
162
+ Start a new thread to monitor console output
163
+ """
164
+ self._thread = FunctionWorker(self.model.console_monitoring_thread)
165
+ self._thread.returned.connect(self._process_new_console_output)
166
+ self._thread.finished.connect(self._finished_receiving_console_output)
167
+ self._thread.start()
168
+
169
+ def _finished_receiving_console_output(self):
170
+ """
171
+ Callback when thread finishes - starts a new thread if not stopped
172
+ """
173
+ if not self._stop:
174
+ self._start_thread()
@@ -0,0 +1,35 @@
1
+ from bluesky.callbacks.zmq import RemoteDispatcher
2
+ from bluesky_widgets.qt.zmq_dispatcher import RemoteDispatcher as QtRemoteDispatcher
3
+ from bluesky.callbacks.best_effort import BestEffortCallback
4
+ from .lessEffortCallback import LessEffortCallback
5
+
6
+
7
+ def zmq_table(out=print, continue_polling=None):
8
+ callback = LessEffortCallback(out=out)
9
+ # bec = BestEffortCallback()
10
+
11
+ zmq_dispatcher = RemoteDispatcher("localhost:5578")
12
+
13
+ zmq_dispatcher.subscribe(callback)
14
+ zmq_dispatcher.start()
15
+
16
+
17
+ def qt_zmq_table(out=print):
18
+ callback = LessEffortCallback(out=out)
19
+ # bec = BestEffortCallback()
20
+
21
+ zmq_dispatcher = QtRemoteDispatcher("localhost:5578")
22
+
23
+ zmq_dispatcher.subscribe(callback)
24
+
25
+ return zmq_dispatcher, callback
26
+
27
+
28
+ def main():
29
+
30
+ zmq_table()
31
+
32
+
33
+ if __name__ == "__main__":
34
+
35
+ main()
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: nbs-livetable
3
+ Version: 0.1.1
4
+ Summary: An implementation of LiveTable running over Kafka or ZMQ
5
+ Project-URL: homepage, https://github.com/xraygui/livetable
6
+ Author-email: Charles Titus <ctitus@bnl.gov>
7
+ Keywords: bluesky,gui,nsls-ii
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: Public Domain
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Requires-Dist: bluesky-kafka>=0.10.0
14
+ Requires-Dist: bluesky-widgets>=0.0.16
15
+ Requires-Dist: nslsii
16
+ Requires-Dist: qtpy
@@ -0,0 +1,11 @@
1
+ nbs_livetable/QtKafkaTable.py,sha256=4sxkzq8H7ODyRlsCALotuR1UGZ-J3TE5zffgzxVJWMw,5834
2
+ nbs_livetable/QtZmqTable.py,sha256=n6pfDXDHfMuIZEWEhxS9P5UL_rBpXSTwOMxpMkTPPDE,4675
3
+ nbs_livetable/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ nbs_livetable/kafka_table.py,sha256=w9ntYxTI3iwozFR97IM2q-q0ugNUX65sfyImghuuqiM,2657
5
+ nbs_livetable/lessEffortCallback.py,sha256=PC75L3fj1gcdVa80lDHhuTryItIhP-2Vk8VbTRnP3rY,9211
6
+ nbs_livetable/simpleConsoleMonitor.py,sha256=OcaaDsdRUE6vp_XzPW--_q2xHr19PHHqY3_As4putKg,5499
7
+ nbs_livetable/zmq_table.py,sha256=ixI8jZ7gSBZn7CaB3oANzc9fqOVXiBzZFh779EfgDRU,814
8
+ nbs_livetable-0.1.1.dist-info/METADATA,sha256=f606KW99I8rOzrPMzTKqB-c8kEDC9FgRStJglmd7-EI,581
9
+ nbs_livetable-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
+ nbs_livetable-0.1.1.dist-info/entry_points.txt,sha256=U6NwttShqrT19WdxjhRyIlOyP5FIIH-erT8czmJ_YVk,346
11
+ nbs_livetable-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ [console_scripts]
2
+ kafka-livetable = nbs_livetable.kafka_table:main
3
+ qt-kafka-livetable = nbs_livetable.QtKafkaTable:main
4
+ qt-zmq-livetable = nbs_livetable.QtZmqTable:main
5
+ zmq-livetable = nbs_livetable.zmq_table:main
6
+
7
+ [nbs_gui.tabs]
8
+ kafka-table-tab = nbs_livetable.QtKafkaTable:QtKafkaTableTab
9
+ zmq-table-tab = nbs_livetable.QtZmqTable:QtZMQTableTab