mccode-plumber 0.14.0__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.
- mccode_plumber-0.14.0/.github/workflows/pip.yml +38 -0
- mccode_plumber-0.14.0/.github/workflows/wheels.yml +52 -0
- mccode_plumber-0.14.0/.gitignore +5 -0
- mccode_plumber-0.14.0/PKG-INFO +17 -0
- mccode_plumber-0.14.0/README.md +2 -0
- mccode_plumber-0.14.0/pyproject.toml +46 -0
- mccode_plumber-0.14.0/setup.cfg +4 -0
- mccode_plumber-0.14.0/src/mccode_plumber/__init__.py +6 -0
- mccode_plumber-0.14.0/src/mccode_plumber/conductor.py +0 -0
- mccode_plumber-0.14.0/src/mccode_plumber/epics.py +186 -0
- mccode_plumber-0.14.0/src/mccode_plumber/epics_watcher.py +125 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/CommandChannel.py +236 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/CommandHandler.py +58 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/CommandStatus.py +151 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/InThreadStatusTracker.py +228 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/JobHandler.py +102 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/JobStatus.py +147 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/KafkaTopicUrl.py +22 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/StateExtractor.py +58 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/WorkerFinder.py +139 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/WorkerJobPool.py +70 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/WorkerStatus.py +88 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/WriteJob.py +83 -0
- mccode_plumber-0.14.0/src/mccode_plumber/file_writer_control/__init__.py +13 -0
- mccode_plumber-0.14.0/src/mccode_plumber/forwarder.py +95 -0
- mccode_plumber-0.14.0/src/mccode_plumber/kafka.py +68 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/__init__.py +26 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/efu.py +142 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/ensure.py +73 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/epics.py +33 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/forwarder.py +79 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/manager.py +113 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/orchestrate.py +435 -0
- mccode_plumber-0.14.0/src/mccode_plumber/manage/writer.py +60 -0
- mccode_plumber-0.14.0/src/mccode_plumber/mccode.py +59 -0
- mccode_plumber-0.14.0/src/mccode_plumber/splitrun.py +32 -0
- mccode_plumber-0.14.0/src/mccode_plumber/utils.py +68 -0
- mccode_plumber-0.14.0/src/mccode_plumber/writer.py +498 -0
- mccode_plumber-0.14.0/src/mccode_plumber.egg-info/PKG-INFO +17 -0
- mccode_plumber-0.14.0/src/mccode_plumber.egg-info/SOURCES.txt +45 -0
- mccode_plumber-0.14.0/src/mccode_plumber.egg-info/dependency_links.txt +1 -0
- mccode_plumber-0.14.0/src/mccode_plumber.egg-info/entry_points.txt +18 -0
- mccode_plumber-0.14.0/src/mccode_plumber.egg-info/requires.txt +8 -0
- mccode_plumber-0.14.0/src/mccode_plumber.egg-info/top_level.txt +1 -0
- mccode_plumber-0.14.0/tests/test_epics.py +50 -0
- mccode_plumber-0.14.0/tests/test_splitrun.py +69 -0
- mccode_plumber-0.14.0/tests/test_writer.py +71 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Pip
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
# pull_request:
|
|
6
|
+
# push:
|
|
7
|
+
# branches:
|
|
8
|
+
# - main
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build with Pip
|
|
13
|
+
runs-on: ${{ matrix.platform }}
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
platform: [windows-latest, macos-latest, ubuntu-latest]
|
|
18
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v4
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Set min macOS version
|
|
28
|
+
if: runner.os == 'macOS'
|
|
29
|
+
run: |
|
|
30
|
+
echo "MACOS_DEPLOYMENT_TARGET=10.14" >> $GITHUB_ENV
|
|
31
|
+
|
|
32
|
+
- name: Build and install
|
|
33
|
+
run: pip install --verbose .
|
|
34
|
+
|
|
35
|
+
- name: Test
|
|
36
|
+
run: |
|
|
37
|
+
python -m pip install pytest
|
|
38
|
+
python -m pytest
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Wheels
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
pull_request:
|
|
6
|
+
push:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
release:
|
|
10
|
+
types:
|
|
11
|
+
- published
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build_sdist_and_wheel:
|
|
15
|
+
name: Build SDist and Wheel
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
submodules: true
|
|
22
|
+
|
|
23
|
+
- name: Build SDist
|
|
24
|
+
run: pipx run build
|
|
25
|
+
|
|
26
|
+
- name: Check metadata
|
|
27
|
+
run: pipx run twine check dist/*
|
|
28
|
+
|
|
29
|
+
- uses: actions/upload-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
path: dist/*
|
|
32
|
+
|
|
33
|
+
upload_all:
|
|
34
|
+
name: Upload if release
|
|
35
|
+
needs: [build_sdist_and_wheel]
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment:
|
|
38
|
+
name: pypi
|
|
39
|
+
url: https://pypi.org/p/mccode-plumber
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write
|
|
42
|
+
if: github.event_name == 'release' && github.event.action == 'published'
|
|
43
|
+
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/setup-python@v5
|
|
46
|
+
|
|
47
|
+
- uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: artifact
|
|
50
|
+
path: dist
|
|
51
|
+
|
|
52
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mccode-plumber
|
|
3
|
+
Version: 0.14.0
|
|
4
|
+
Author-email: Gregory Tucker <gregory.tucker@ess.eu>
|
|
5
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: p4p
|
|
8
|
+
Requires-Dist: kafka-python>=2.2.11
|
|
9
|
+
Requires-Dist: ess-streaming-data-types>=0.14.0
|
|
10
|
+
Requires-Dist: restage>=0.9.0
|
|
11
|
+
Requires-Dist: mccode-to-kafka>=0.2.2
|
|
12
|
+
Requires-Dist: moreniius>=0.6.0
|
|
13
|
+
Requires-Dist: icecream
|
|
14
|
+
Requires-Dist: ephemeral-port-reserve
|
|
15
|
+
|
|
16
|
+
# McCode Plumber
|
|
17
|
+
Setup, run, and teardown the infrastructure for splitrun McCode scans sending data through Kafka into NeXus
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['setuptools>=60', 'setuptools_scm>=8.0']
|
|
3
|
+
build-backend = 'setuptools.build_meta'
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = 'mccode-plumber'
|
|
7
|
+
dependencies = [
|
|
8
|
+
'p4p',
|
|
9
|
+
'kafka-python>=2.2.11',
|
|
10
|
+
'ess-streaming-data-types>=0.14.0',
|
|
11
|
+
'restage>=0.9.0',
|
|
12
|
+
'mccode-to-kafka>=0.2.2',
|
|
13
|
+
'moreniius>=0.6.0',
|
|
14
|
+
'icecream',
|
|
15
|
+
'ephemeral-port-reserve',
|
|
16
|
+
]
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
authors = [
|
|
19
|
+
{ name = "Gregory Tucker", email = "gregory.tucker@ess.eu" },
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"License :: OSI Approved :: BSD License",
|
|
23
|
+
]
|
|
24
|
+
dynamic = ['version']
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
mp-splitrun = 'mccode_plumber.splitrun:main'
|
|
28
|
+
mp-epics = 'mccode_plumber.epics:run'
|
|
29
|
+
mp-epics-strings = 'mccode_plumber.epics:run_strings'
|
|
30
|
+
mp-epics-update = 'mccode_plumber.epics:update'
|
|
31
|
+
mp-epics-watch = 'mccode_plumber.epics_watcher:run_instr'
|
|
32
|
+
mp-forwarder-setup = 'mccode_plumber.forwarder:setup'
|
|
33
|
+
mp-forwarder-teardown = 'mccode_plumber.forwarder:teardown'
|
|
34
|
+
mp-writer-from = 'mccode_plumber.writer:print_time'
|
|
35
|
+
mp-writer-write = 'mccode_plumber.writer:start_writer'
|
|
36
|
+
mp-writer-wait = 'mccode_plumber.writer:wait_on_writer'
|
|
37
|
+
mp-writer-list = 'mccode_plumber.writer:list_status'
|
|
38
|
+
mp-writer-kill = 'mccode_plumber.writer:kill_job'
|
|
39
|
+
mp-writer-killall = 'mccode_plumber.writer:kill_all'
|
|
40
|
+
mp-register-topics = 'mccode_plumber.kafka:register_topics'
|
|
41
|
+
mp-insert-hdf5-instr = 'mccode_plumber.mccode:insert'
|
|
42
|
+
mp-nexus-splitrun = 'mccode_plumber.manage.orchestrate:main'
|
|
43
|
+
mp-nexus-services = 'mccode_plumber.manage.orchestrate:services'
|
|
44
|
+
|
|
45
|
+
[tool.setuptools_scm]
|
|
46
|
+
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from p4p.nt import NTScalar
|
|
3
|
+
from p4p.server import Server, StaticProvider
|
|
4
|
+
from p4p.server.thread import SharedPV
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
def instr_par_to_nt_primitive(parameters):
|
|
9
|
+
from mccode_antlr.common.expression import DataType, ShapeType
|
|
10
|
+
out = []
|
|
11
|
+
for p in parameters:
|
|
12
|
+
expr = p.value
|
|
13
|
+
if expr.is_str:
|
|
14
|
+
t, d = 's', ''
|
|
15
|
+
elif expr.data_type == DataType.int:
|
|
16
|
+
t, d = 'i', 0
|
|
17
|
+
elif expr.data_type == DataType.float:
|
|
18
|
+
t, d = 'd', 0.0
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError(f"Unknown parameter type {expr.data_type}")
|
|
21
|
+
if expr.shape_type == ShapeType.vector:
|
|
22
|
+
t, d = 'a' + t, [d]
|
|
23
|
+
out.append((p.name, t, d))
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
def instr_par_nt_to_strings(parameters):
|
|
27
|
+
return [f'{n}:{t}:{d}'.replace(' ','') for n, t, d in instr_par_to_nt_primitive(parameters)]
|
|
28
|
+
|
|
29
|
+
def strings_to_instr_par_nt(strings):
|
|
30
|
+
out = []
|
|
31
|
+
for string in strings:
|
|
32
|
+
name, t, dstr = string.split(':')
|
|
33
|
+
trans = None
|
|
34
|
+
if 'i' in t:
|
|
35
|
+
trans = int
|
|
36
|
+
elif 'd' in t:
|
|
37
|
+
trans = float
|
|
38
|
+
elif 's' in t:
|
|
39
|
+
trans = str
|
|
40
|
+
else:
|
|
41
|
+
ValueError(f"Unknown type in {string}")
|
|
42
|
+
if t.startswith('a'):
|
|
43
|
+
d = [trans(x) for x in dstr.translate(str.maketrans(',',' ','[]')).split()]
|
|
44
|
+
else:
|
|
45
|
+
d = trans(dstr)
|
|
46
|
+
out.append((name, t, d))
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
def convert_strings_to_nt(strings):
|
|
50
|
+
return {n: NTScalar(t).wrap(d) for n, t, d in strings_to_instr_par_nt(strings)}
|
|
51
|
+
|
|
52
|
+
def convert_instr_parameters_to_nt(parameters):
|
|
53
|
+
out = {n: NTScalar(t).wrap(d) for n, t, d in instr_par_to_nt_primitive(parameters)}
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_instr_nt_values(instr: Union[Path, str]):
|
|
58
|
+
"""Get the instrument parameters from an Instr a or a parseable Instr file and convert to NTScalar values"""
|
|
59
|
+
from .mccode import get_mccode_instr_parameters
|
|
60
|
+
nts = convert_instr_parameters_to_nt(get_mccode_instr_parameters(instr))
|
|
61
|
+
if 'mcpl_filename' not in nts:
|
|
62
|
+
nts['mcpl_filename'] = NTScalar('s').wrap('')
|
|
63
|
+
return nts
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MailboxHandler:
|
|
67
|
+
@staticmethod
|
|
68
|
+
def put(pv, op):
|
|
69
|
+
from datetime import datetime, timezone
|
|
70
|
+
val = op.value()
|
|
71
|
+
|
|
72
|
+
if pv.nt is None:
|
|
73
|
+
# Assume that this means wrap wasn't provided ...
|
|
74
|
+
pv.nt = NTScalar(val.type()['value'])
|
|
75
|
+
pv._wrap = pv.nt.wrap
|
|
76
|
+
|
|
77
|
+
# Notify any subscribers of the new value, adding the timestamp, so they know when it was set.
|
|
78
|
+
pv.post(val, timestamp=datetime.now(timezone.utc).timestamp())
|
|
79
|
+
# Notify the client making this PUT operation that it has now completed
|
|
80
|
+
op.done()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_parser():
|
|
84
|
+
from argparse import ArgumentParser
|
|
85
|
+
from mccode_plumber import __version__
|
|
86
|
+
p = ArgumentParser()
|
|
87
|
+
p.add_argument('instr', type=str, help='The instrument file to read')
|
|
88
|
+
p.add_argument('-p', '--prefix', type=str, help='The EPICS PV prefix to use', default='mcstas:')
|
|
89
|
+
p.add_argument('-v', '--version', action='version', version=__version__)
|
|
90
|
+
return p
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_args():
|
|
94
|
+
args = get_parser().parse_args()
|
|
95
|
+
parameters = parse_instr_nt_values(args.instr)
|
|
96
|
+
return parameters, args
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main(names: dict[str, NTScalar], prefix: str = None, filename_required: bool = True):
|
|
100
|
+
provider = StaticProvider('mailbox') # 'mailbox' is an arbitrary name
|
|
101
|
+
|
|
102
|
+
if filename_required and 'mcpl_filename' not in names:
|
|
103
|
+
names['mcpl_filename'] = NTScalar('s').wrap('')
|
|
104
|
+
|
|
105
|
+
pvs = [] # we must keep a reference in order to keep the Handler from being collected
|
|
106
|
+
for name, value in names.items():
|
|
107
|
+
pv = SharedPV(initial=value, handler=MailboxHandler())
|
|
108
|
+
provider.add(f'{prefix}{name}' if prefix else name, pv)
|
|
109
|
+
pvs.append(pv)
|
|
110
|
+
|
|
111
|
+
print(f'Start mailbox server for {len(pvs)} PVs with prefix {prefix}')
|
|
112
|
+
Server.forever(providers=[provider])
|
|
113
|
+
print('Done')
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run():
|
|
117
|
+
parameters, args = parse_args()
|
|
118
|
+
main(parameters, prefix=args.prefix)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def start(parameters, prefix: str = None):
|
|
122
|
+
from multiprocessing import Process
|
|
123
|
+
proc = Process(target=main, args=(parameters, prefix))
|
|
124
|
+
proc.start()
|
|
125
|
+
return proc
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def stop(proc):
|
|
129
|
+
proc.terminate()
|
|
130
|
+
proc.join(1)
|
|
131
|
+
proc.close()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def update():
|
|
135
|
+
from argparse import ArgumentParser
|
|
136
|
+
from p4p.client.thread import Context
|
|
137
|
+
parser = ArgumentParser(description="Update the mailbox server with new values")
|
|
138
|
+
parser.add_argument('address value', type=str, nargs='+', help='The mailbox address and value to be updated')
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
addresses_values = getattr(args, 'address value')
|
|
141
|
+
|
|
142
|
+
if len(addresses_values) == 0:
|
|
143
|
+
parser.print_help()
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
addresses = addresses_values[::2]
|
|
147
|
+
values = addresses_values[1::2]
|
|
148
|
+
|
|
149
|
+
if len(addresses_values) % 2:
|
|
150
|
+
print(f'Please provide address-value pairs. Provided {addresses=} {values=}')
|
|
151
|
+
|
|
152
|
+
ctx = Context('pva')
|
|
153
|
+
for address, value in zip(addresses, values):
|
|
154
|
+
pv = ctx.get(address, throw=False)
|
|
155
|
+
if isinstance(pv, float):
|
|
156
|
+
ctx.put(address, float(value))
|
|
157
|
+
elif isinstance(pv, int):
|
|
158
|
+
ctx.put(address, int(value))
|
|
159
|
+
elif isinstance(pv, str):
|
|
160
|
+
ctx.put(address, str(value))
|
|
161
|
+
elif isinstance(pv, TimeoutError):
|
|
162
|
+
print(f'[Timeout] Failed to update {address} with {value} (Unknown to EPICS?)')
|
|
163
|
+
else:
|
|
164
|
+
raise ValueError(f'Address {address} has unknown type {type(pv)}')
|
|
165
|
+
|
|
166
|
+
ctx.disconnect()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_strings_parser():
|
|
170
|
+
from argparse import ArgumentParser
|
|
171
|
+
from mccode_plumber import __version__
|
|
172
|
+
p = ArgumentParser()
|
|
173
|
+
p.add_argument('strings', type=str, nargs='+', help='The string encoded NTScalars to read, each name:type-char:default')
|
|
174
|
+
p.add_argument('-p', '--prefix', type=str, help='The EPICS PV prefix to use', default='mcstas:')
|
|
175
|
+
p.add_argument('-v', '--version', action='version', version=__version__)
|
|
176
|
+
return p
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def run_strings():
|
|
180
|
+
args = get_strings_parser().parse_args()
|
|
181
|
+
main(convert_strings_to_nt(args.strings), prefix=args.prefix)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == '__main__':
|
|
186
|
+
run()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from textual.app import App, ComposeResult
|
|
2
|
+
from textual.containers import VerticalScroll
|
|
3
|
+
from textual.widgets import Static
|
|
4
|
+
from textual.reactive import reactive
|
|
5
|
+
from textual import events
|
|
6
|
+
|
|
7
|
+
from p4p.client.thread import Context
|
|
8
|
+
from p4p.client.thread import Disconnected
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PVWidget(Static):
|
|
15
|
+
value: reactive[str] = reactive("Connecting...")
|
|
16
|
+
|
|
17
|
+
def __init__(self, pvname: str):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.pvname = pvname
|
|
20
|
+
self.sid = pvname.replace(':', '')
|
|
21
|
+
self.set_class(True, "pv-widget")
|
|
22
|
+
self.value_widget = None # Store reference directly
|
|
23
|
+
|
|
24
|
+
def compose(self) -> ComposeResult:
|
|
25
|
+
yield Static(f"[b]{self.pvname}[/b]", id=f"label-{self.sid}")
|
|
26
|
+
self.value_widget = Static(self.value, id=f"value-{self.sid}")
|
|
27
|
+
yield self.value_widget
|
|
28
|
+
|
|
29
|
+
def watch_value(self, value: str):
|
|
30
|
+
if self.value_widget:
|
|
31
|
+
self.value_widget.update(value)
|
|
32
|
+
|
|
33
|
+
def update_value(self, new_value: str):
|
|
34
|
+
self.value = new_value
|
|
35
|
+
|
|
36
|
+
class PVMonitorApp(App):
|
|
37
|
+
CSS = """
|
|
38
|
+
.pv-widget {
|
|
39
|
+
padding: 1 1;
|
|
40
|
+
border: round $primary;
|
|
41
|
+
margin: 1;
|
|
42
|
+
}
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, prefix: str, names: list[str]):
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.pv_widgets = {}
|
|
48
|
+
self.ctx = Context("pva") # Or 'ca' if using Channel Access
|
|
49
|
+
self.prefix = prefix
|
|
50
|
+
self.names = names
|
|
51
|
+
self.grid = None
|
|
52
|
+
|
|
53
|
+
def compose(self) -> ComposeResult:
|
|
54
|
+
from textual.containers import Grid
|
|
55
|
+
self.grid = Grid(id="pv-grid")
|
|
56
|
+
yield self.grid
|
|
57
|
+
|
|
58
|
+
def on_mount(self) -> None:
|
|
59
|
+
self.grid.styles.grid_columns = ["1fr", "1fr", "1fr"] # 3 columns
|
|
60
|
+
self.grid.styles.grid_gap = (1, 1)
|
|
61
|
+
for name in self.names:
|
|
62
|
+
pv = f'{self.prefix}{name}'
|
|
63
|
+
widget = PVWidget(pv)
|
|
64
|
+
self.pv_widgets[pv] = widget
|
|
65
|
+
self.grid.mount(widget)
|
|
66
|
+
threading.Thread(target=self.monitor_pv, args=(pv,), daemon=True).start()
|
|
67
|
+
|
|
68
|
+
def monitor_pv(self, pvname: str):
|
|
69
|
+
def callback(value):
|
|
70
|
+
if isinstance(value, Disconnected):
|
|
71
|
+
new_value = "Disconnected"
|
|
72
|
+
else:
|
|
73
|
+
new_value = str(value)
|
|
74
|
+
# Schedule update on the main thread
|
|
75
|
+
asyncio.run_coroutine_threadsafe(
|
|
76
|
+
self.update_widget_value(pvname, new_value),
|
|
77
|
+
self._loop,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.ctx.monitor(pvname, callback)
|
|
81
|
+
|
|
82
|
+
async def update_widget_value(self, pvname: str, new_value: str):
|
|
83
|
+
widget = self.pv_widgets.get(pvname)
|
|
84
|
+
if widget:
|
|
85
|
+
widget.update_value(new_value)
|
|
86
|
+
|
|
87
|
+
async def on_shutdown(self) -> None:
|
|
88
|
+
self.ctx.close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_names_parser():
|
|
93
|
+
from argparse import ArgumentParser
|
|
94
|
+
from mccode_plumber import __version__
|
|
95
|
+
p = ArgumentParser()
|
|
96
|
+
p.add_argument('name', type=str, nargs='+', help='The NTScalar names to watch')
|
|
97
|
+
p.add_argument('-p', '--prefix', type=str, help='The EPICS PV prefix to use', default='mcstas:')
|
|
98
|
+
p.add_argument('-v', '--version', action='version', version=__version__)
|
|
99
|
+
return p
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_strings():
|
|
103
|
+
args = get_names_parser().parse_args()
|
|
104
|
+
PVMonitorApp(args.prefix, args.name).run()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_instr_parser():
|
|
108
|
+
from argparse import ArgumentParser
|
|
109
|
+
from mccode_plumber import __version__
|
|
110
|
+
p = ArgumentParser()
|
|
111
|
+
p.add_argument('instr', type=str, help='The instrument which defines names to watch')
|
|
112
|
+
p.add_argument('-p', '--prefix', type=str, help='The EPICS PV prefix to use', default='mcstas:')
|
|
113
|
+
p.add_argument('-v', '--version', action='version', version=__version__)
|
|
114
|
+
return p
|
|
115
|
+
|
|
116
|
+
def run_instr():
|
|
117
|
+
from mccode_plumber.manage.orchestrate import get_instr_name_and_parameters
|
|
118
|
+
args = get_instr_parser().parse_args()
|
|
119
|
+
_, parameters = get_instr_name_and_parameters(args.instr)
|
|
120
|
+
names = [p.name for p in parameters]
|
|
121
|
+
PVMonitorApp(args.prefix, names).run()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == '__main__':
|
|
125
|
+
run_instr()
|