Files
2026-10-23 08:51:30 +02:00

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()