Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
fdede0a
upstream_devices works; also changed api's to be more logical for phy…
robnagler Jul 11, 2025
e7e81d8
ckp
robnagler Jul 11, 2025
be86e47
ckp
robnagler Jul 11, 2025
05b1f26
ckp
robnagler Jul 11, 2025
ce27b18
ckp
robnagler Jul 14, 2025
425ad76
ckp
robnagler Jul 14, 2025
ed056fb
Added assertion that device is in beampath for upstream devices to pr…
eloise-nebula Jul 16, 2025
5b49ab1
Upstream devices now accepts parameter for accessor
eloise-nebula Jul 18, 2025
c4329ae
Added remove_target to device.device_screen.
eloise-nebula Jul 19, 2025
b055489
Typo line.
eloise-nebula Jul 21, 2025
aa9d68e
forgot beamline argument
eloise-nebula Jul 21, 2025
f655cc9
Fixed beamline/beam_path typo.
eloise-nebula Jul 21, 2025
de88830
Removed upstream_device and beam_path from screen. First attempt at w…
eloise-nebula Jul 22, 2025
9b1a183
Responding to git comments.
eloise-nebula Jul 22, 2025
4bdb931
First attempt at a device_screen method that removes all upstream tar…
eloise-nebula Jul 25, 2025
cef2117
ckp
robnagler Jul 25, 2025
4b70a24
Merge branch 'main' into 122-target
robnagler Jul 25, 2025
dd7d305
ckp starting multivariate FSM
robnagler Jul 26, 2025
a0f79b5
ckp
robnagler Jul 27, 2025
b9c42de
ckp
robnagler Jul 27, 2025
38d9c7a
ckp
robnagler Jul 27, 2025
40a80cd
basically coded
robnagler Jul 27, 2025
e7af727
starting #130 mocking epics
robnagler Jul 27, 2025
e746c55
remove mock_ioc
robnagler Jul 30, 2025
b897729
cleaned up
robnagler Jul 30, 2025
e2d9202
working for basic get
robnagler Jul 30, 2025
d09eb59
doc ideas
robnagler Jul 30, 2025
99c55f8
test starts but no events published
robnagler Aug 3, 2025
5da9400
working sometimes; pyepics seems to lose responses
robnagler Aug 3, 2025
e65ed01
try CAThread but fails every few times. pretty much same
robnagler Aug 3, 2025
8469a32
defer initialization of device pv so all callbacks can be caught; moc…
robnagler Aug 4, 2025
448c0a9
remove pkdp
robnagler Aug 4, 2025
999c667
device_screen_test passes with separate queues
robnagler Aug 6, 2025
913b4d9
rename mock_ioc to ioc
robnagler Aug 7, 2025
0c410be
ckp: trying to put in target
robnagler Aug 7, 2025
6968042
put is seen in group_write
robnagler Aug 7, 2025
1091ef1
Merge branch 'main' into 122-target
robnagler Aug 7, 2025
03ca141
ckp
robnagler Aug 7, 2025
4d1a1b5
basically sort of working
robnagler Aug 7, 2025
31ff072
fixed screen test to be more stable
robnagler Aug 8, 2025
3fa291b
device.screen rename; insert/remove works
robnagler Aug 8, 2025
ff184f3
added slicops package_path for use with sql db
moellep Aug 8, 2025
1b55c47
ioc writing to yaml works
robnagler Aug 8, 2025
678ed48
upstream blocked tested
robnagler Aug 8, 2025
eb249e3
remove filter
robnagler Aug 8, 2025
87a89c6
remove pkdp
robnagler Aug 8, 2025
fab1c0c
fmt
robnagler Aug 8, 2025
cfb037c
disable carepeater in test.sh
robnagler Aug 8, 2025
2f049ed
remove improt
robnagler Aug 8, 2025
5c6d250
mock caRepeater
robnagler Aug 8, 2025
88d0eda
fpc
robnagler Aug 8, 2025
d2a4455
caproto
robnagler Aug 8, 2025
ecf1d7d
Fix #136 random port for epics
robnagler Aug 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ classifiers = [
"Topic :: Utilities",
]
dependencies = [
'caproto',
'lcls-tools @ git+https://github.com/slaclab/lcls-tools',
'pyepics',
'pykern',
Expand Down
5 changes: 5 additions & 0 deletions slicops/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@ def cfg():
tcp_port=(8000, pykern.pkasyncio.cfg_port, "port of server"),
vue_port=(8008, pykern.pkasyncio.cfg_port, "port of Vue dev server"),
),
package_path=(
tuple(["slicops"]),
tuple,
"Names of root packages that should be checked for codes and resources. Order is important, the first package with a matching code/resource will be used.",
),
)
return _cfg
120 changes: 85 additions & 35 deletions slicops/device.py → slicops/device/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import slicops.device_db
import threading

# TODO(robnagler) configure via device_db
_TIMEOUT = 5


class AccessorPutError(RuntimeError):
"""The PV for this accessor is not writable"""
Expand All @@ -34,6 +37,7 @@ class Device:
def __init__(self, device_name):
self.device_name = device_name
self.meta = slicops.device_db.meta_for_device(device_name)
self._destroyed = False
self._accessor = PKDict()
self.connected = False

Expand All @@ -45,16 +49,21 @@ def accessor(self, accessor_name):
Returns:
_Accessor: object holding PV state
"""
if self._destroyed:
raise AssertionError(f"destroyed {self}")
return self._accessor.pksetdefault(
accessor_name, lambda: _Accessor(self, accessor_name)
)[accessor_name]

def destroy(self):
"""Disconnect from PV's and remove state about device"""
if self._destroyed:
return
self._destroyed = True
x = list(self._accessor.values())
self._accessor = PKDict()
for a in x:
a.disconnect()
a.destroy()

def get(self, accessor_name):
"""Read from PV
Expand Down Expand Up @@ -85,6 +94,9 @@ def put(self, accessor_name, value):
"""
return self.accessor(accessor_name).put(value)

def __repr__(self):
return f"<Device {self.device_name}>"


class _Accessor:
"""Container for a PV, metadata, and dynamic state
Expand All @@ -96,26 +108,33 @@ class _Accessor:

def __init__(self, device, accessor_name):
self.device = device
self.accessor_name = accessor_name
self.meta = device.meta.accessor[accessor_name]
self._callback = None
self._mutex = threading.Lock()
# TODO(pjm): connection and PV timeouts need to be configurable?
self._pv = epics.PV(
self.meta.pv_name,
connection_callback=self._on_connection,
connection_timeout=4.0,
)
if accessor_name == "image":
# TODO(robnagler) this has to be done here, because you can't get pvs
# from within a monitor callback
self._image_shape = (self.device.get("n_row"), self.device.get("n_col"))

def disconnect(self):
self._destroyed = False
self._lock = threading.Lock()
self._initialized = threading.Event()
self._initializing = False
# Defer initialization
self._pv = None

def destroy(self):
"""Stop all monitoring and disconnect from PV"""
self._callback = None
if self._destroyed:
return
with self._lock:
if self._destroyed:
return
self._destroyed = True
self._initializing = False
self._callback = None
if (p := self._pv) is None:
return
self._pv = None
self._initialized.set()
try:
# Clears all callbacks
self._pv.disconnect()
p.disconnect()
except Exception as e:
pkdlog("error={} {} stack={}", e, self, pkdexc())

Expand All @@ -125,11 +144,10 @@ def get(self):
Returns:
object: the value from the PV converted to a Python type
"""

# TODO(pjm): connection and PV timeouts need to be configurable?
if (rv := self._pv.get(timeout=5.0)) is None:
p = self.__pv()
if (rv := p.get(timeout=_TIMEOUT)) is None:
raise DeviceError(f"unable to get {self}")
if not self._pv.connected:
if not p.connected:
raise DeviceError(f"disconnected {self}")
return self._fixup_value(rv)

Expand All @@ -147,23 +165,21 @@ def monitor(self, callback):
Args:
callback (callable): accepts a single `PKDict` as ag
"""
with self._mutex:
with self._lock:
self._assert_not_destroyed()
if self._callback:
raise ValueError(f"already monitoring {self}")
# should lock
self._callback_index = self._pv.add_callback(self._on_value)
self._pv.auto_monitor = True
raise AssertionError("may only call monitor once")
if self._pv or self._initializing:
raise AssertionError("monitor must be called before get/put")
self._callback = callback
self.__pv()

def monitor_stop(self):
"""Stops monitoring PV"""
with self._mutex:
if not self._callback:
with self._lock:
if self._destroyed or not self._callback:
return
self._callback = None
self._pv.auto_monitor = False
self._pv.remove_callback(self._callback_index)
self._callback_index = None

def put(self, value):
"""Set PV to value
Expand All @@ -180,18 +196,23 @@ def put(self, value):
else:
raise AccessorPutError(f"unhandled py_type={self.meta.py_type} {self}")
# ECA_NORMAL == 0 and None is normal, too, apparently
if (e := self._pv.put(v)) != 1:
p = self.__pv()
if (e := p.put(v)) != 1:
raise DeviceError(f"put error={e} value={v} {self}")
if not self._pv.connected:
if not p.connected:
raise DeviceError(f"disconnected {self}")

def _assert_not_destroyed(self):
if self._destroyed:
raise AssertionError(f"destroyed {self}")

def _fixup_value(self, raw):
def _reshape(image):
return image.reshape(self._image_shape)

if self.meta.py_type == bool:
return bool(raw)
if self.meta.accessor_name == "image":
if self.accessor_name == "image":
return _reshape(raw)
return raw

Expand Down Expand Up @@ -221,12 +242,41 @@ def _on_value(self, **kwargs):
pkdlog("error={} {} stack={}", e, self, pkdexc())
raise

def __pv(self):
with self._lock:
self._assert_not_destroyed()
if self._pv:
return self._pv
if not (i := self._initializing):
self._initializing = True
if i:
self._initialized.wait(timeout=_TIMEOUT)
else:
k = (
PKDict(callback=self._on_value, auto_monitor=True)
if self._callback
else PKDict()
)
if self.accessor_name == "image":
# TODO(robnagler) this has to be done here, because you can't get pvs
# from within a monitor callback.
# TODO(robnagler) need a better way of dealing with this
self._image_shape = (self.device.get("n_row"), self.device.get("n_col"))
self._pv = epics.PV(
self.meta.pv_name,
connection_callback=self._on_connection,
connection_timeout=_TIMEOUT,
**k,
)
self._initialized.set()
return self._pv

def __repr__(self):
return f"<_Accessor {self.device.device_name}.{self.meta.accessor_name} {self.meta.pv_name}>"
return f"<_Accessor {self.device.device_name}.{self.accessor_name} {self.meta.pv_name}>"

def _run_callback(self, **kwargs):
k = PKDict(accessor=self, **kwargs)
with self._mutex:
with self._lock:
c = self._callback
if c:
c(k)
Loading