"""Read-only CA monitor cache; GUI reads never issue blocking network requests.""" import math from threading import Lock class IocSource: SUFFIXES = { 'pose_set': 'arm-pose-set', 'pose_actual': 'arm-pose-actual', 'joint_set': 'arm-joint-set', 'joint_actual': 'arm-joint-actual', 'fault': 'arm-fault', 'residual': 'arm-ik-residual', } ARRAYS = {'pose_set', 'pose_actual', 'joint_set', 'joint_actual'} def __init__(self, prefix, pv_factory=None): prefix = prefix.rstrip(':') + ':' 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()