79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
"""Read-only CA monitor cache; GUI reads never issue blocking network requests."""
|
|
import math
|
|
from threading import Lock
|
|
|
|
|
|
class IocSource:
|
|
SUFFIXES = {
|
|
'pose_set': 'hex-pose-set',
|
|
'pose_actual': 'hex-pose-actual',
|
|
'leg_set': 'hex-leg-set',
|
|
'leg_actual': 'hex-leg-actual',
|
|
'fault': 'hex-fault',
|
|
'residual': 'hex-fk-residual',
|
|
}
|
|
ARRAYS = {'pose_set', 'pose_actual', 'leg_set', 'leg_actual'}
|
|
|
|
def __init__(self, prefix, pv_factory=None):
|
|
if pv_factory is None:
|
|
from epics import PV
|
|
pv_factory = PV
|
|
self._lock = Lock()
|
|
self._values = {}
|
|
self._connected = {}
|
|
self._errors = {}
|
|
self.pvs = {}
|
|
for key, suffix in self.SUFFIXES.items():
|
|
self.pvs[key] = pv_factory(
|
|
prefix + suffix, auto_monitor=True,
|
|
callback=lambda value=None, _key=key, **kw: self._update(_key, value),
|
|
connection_callback=lambda conn=False, _key=key, **kw: self._connection(_key, conn))
|
|
|
|
def _connection(self, key, connected):
|
|
with self._lock:
|
|
self._connected[key] = connected
|
|
if not connected:
|
|
self._values.pop(key, None)
|
|
self._errors.pop(key, None)
|
|
|
|
def _update(self, key, value):
|
|
try:
|
|
if key in self.ARRAYS:
|
|
value = tuple(float(v) for v in value)
|
|
if len(value) != 6 or not all(math.isfinite(v) for v in value):
|
|
raise ValueError('expected six finite values')
|
|
else:
|
|
value = float(value)
|
|
if not math.isfinite(value):
|
|
raise ValueError('expected finite scalar')
|
|
if key == 'fault':
|
|
if not value.is_integer():
|
|
raise ValueError('expected integer fault code')
|
|
value = int(value)
|
|
except (TypeError, ValueError, OverflowError) as exc:
|
|
with self._lock:
|
|
self._values.pop(key, None)
|
|
self._errors[key] = str(exc)
|
|
return
|
|
with self._lock:
|
|
self._values[key] = value
|
|
self._errors.pop(key, None)
|
|
|
|
def snapshot(self):
|
|
# Consistent local cache copy; separate CA records are not a single IOC cycle.
|
|
with self._lock:
|
|
missing = [self.SUFFIXES[k] for k in self.SUFFIXES if not self._connected.get(k)]
|
|
if missing:
|
|
raise RuntimeError('EPICS disconnected:\n' + '\n'.join(missing))
|
|
if self._errors:
|
|
key = next(iter(self._errors))
|
|
raise RuntimeError(f'{self.SUFFIXES[key]}:\n{self._errors[key]}')
|
|
pending = [self.SUFFIXES[k] for k in self.SUFFIXES if k not in self._values]
|
|
if pending:
|
|
raise RuntimeError('Waiting for PV data:\n' + '\n'.join(pending))
|
|
return dict(self._values)
|
|
|
|
def close(self):
|
|
for pv in self.pvs.values():
|
|
pv.disconnect()
|