nbs-livetable 0.1.1__tar.gz
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.
- nbs_livetable-0.1.1/.github/workflows/python-publish.yml +70 -0
- nbs_livetable-0.1.1/PKG-INFO +16 -0
- nbs_livetable-0.1.1/nbs_livetable/QtKafkaTable.py +176 -0
- nbs_livetable-0.1.1/nbs_livetable/QtZmqTable.py +151 -0
- nbs_livetable-0.1.1/nbs_livetable/__init__.py +0 -0
- nbs_livetable-0.1.1/nbs_livetable/kafka_table.py +87 -0
- nbs_livetable-0.1.1/nbs_livetable/lessEffortCallback.py +258 -0
- nbs_livetable-0.1.1/nbs_livetable/simpleConsoleMonitor.py +174 -0
- nbs_livetable-0.1.1/nbs_livetable/zmq_table.py +35 -0
- nbs_livetable-0.1.1/pyproject.toml +61 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release-build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.x"
|
|
28
|
+
|
|
29
|
+
- name: Build release distributions
|
|
30
|
+
run: |
|
|
31
|
+
# NOTE: put your own distribution build steps here.
|
|
32
|
+
python -m pip install build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
- name: Upload distributions
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: release-dists
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
pypi-publish:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
needs:
|
|
44
|
+
- release-build
|
|
45
|
+
permissions:
|
|
46
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
50
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
54
|
+
url: https://pypi.org/p/nbs-livetable
|
|
55
|
+
#
|
|
56
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
57
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
58
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- name: Retrieve release distributions
|
|
62
|
+
uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: release-dists
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- name: Publish release distributions to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist/
|
|
@@ -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,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,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nbs-livetable"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "An implementation of LiveTable running over Kafka or ZMQ"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Charles Titus", email = "ctitus@bnl.gov"}
|
|
7
|
+
]
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
keywords = ["bluesky", "nsls-ii", "gui"]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"qtpy",
|
|
12
|
+
"bluesky-kafka>=0.10.0",
|
|
13
|
+
"bluesky-widgets>=0.0.16",
|
|
14
|
+
"nslsii",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: Public Domain",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Intended Audience :: Science/Research"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
homepage = "https://github.com/xraygui/livetable"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version]
|
|
32
|
+
source = "vcs"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
kafka-livetable = "nbs_livetable.kafka_table:main"
|
|
36
|
+
qt-kafka-livetable = "nbs_livetable.QtKafkaTable:main"
|
|
37
|
+
zmq-livetable = "nbs_livetable.zmq_table:main"
|
|
38
|
+
qt-zmq-livetable = "nbs_livetable.QtZmqTable:main"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
[project.entry-points."nbs_gui.tabs"]
|
|
42
|
+
kafka-table-tab = "nbs_livetable.QtKafkaTable:QtKafkaTableTab"
|
|
43
|
+
zmq-table-tab = "nbs_livetable.QtZmqTable:QtZMQTableTab"
|
|
44
|
+
|
|
45
|
+
[tool.pixi.workspace]
|
|
46
|
+
channels = ["conda-forge"]
|
|
47
|
+
platforms = ["linux-64"]
|
|
48
|
+
|
|
49
|
+
[tool.pixi.feature.build.dependencies]
|
|
50
|
+
hatch = "*"
|
|
51
|
+
|
|
52
|
+
[tool.pixi.feature.build.tasks]
|
|
53
|
+
build = "hatch build"
|
|
54
|
+
test-release = "hatch publish -r test"
|
|
55
|
+
|
|
56
|
+
[tool.pixi.feature.test.pypi-dependencies]
|
|
57
|
+
nbs-livetable = { path=".", editable = true }
|
|
58
|
+
|
|
59
|
+
[tool.pixi.environments]
|
|
60
|
+
build = { features = ["build"], solve-group = "default" }
|
|
61
|
+
test = { features = ["test"], solve-group = "default" }
|