from logging import getLogger from cam_server.pipeline.data_processing import functions, processor from cam_server.utils import create_thread_pvs, epics_lock from collections import deque from threading import Thread import time import numpy as np _logger = getLogger(__name__) # per-camera state (so multiple camera_name streams don't fight each other) _states = {} # channels coming out of processor.process_image() CHANNELS = [ "intensity", "x_center_of_mass", "x_fwhm", "x_rms", "x_fit_amplitude", "x_fit_mean", "x_fit_offset", "x_fit_standard_deviation", "x_profile", "y_center_of_mass", "y_fwhm", "y_rms", "y_fit_amplitude", "y_fit_mean", "y_fit_offset", "y_fit_standard_deviation", "y_profile", ] # which of the above are arrays (these should be waveform records in your IOC) ARRAY_SUFFIXES = {"x_profile", "y_profile"} def _pv_safe(val, pvname, array_pvs): """Match your example: ensure arrays are 1D float64, scalars are floats.""" if val is None: return None if pvname in array_pvs: if isinstance(val, np.ndarray): if val.ndim == 1: return val.astype(np.float64, copy=False) if val.ndim == 0: return np.array([val.item()], dtype=np.float64) raise ValueError(f"{pvname}: expected 1D array, got shape {val.shape}") if isinstance(val, (list, tuple)): return np.array(val, dtype=np.float64) if isinstance(val, (float, int, np.generic)): return np.array([float(val)], dtype=np.float64) raise TypeError(f"{pvname}: expected array, got {type(val)}") # scalar PV if isinstance(val, np.ndarray): if val.size == 1: return float(val.item()) raise ValueError(f"{pvname}: expected scalar, got array of size {val.size}") if isinstance(val, (np.generic, float, int)): return float(val) raise TypeError(f"{pvname}: expected scalar, got {type(val)}") def update_PVs(buffer, *pv_names): pvs = create_thread_pvs(list(pv_names)) while True: time.sleep(0.01) try: rec = buffer.popleft() except IndexError: continue for pv, val in zip(pvs, rec): if pv and pv.connected and (val is not None): try: pv.put(val) except Exception as e: _logger.error(f"Error writing to {pv.pvname}: {e}") def _initialize_camera_state(camera_name: str): buffer = deque(maxlen=10) pv_names = [f"{camera_name}:{c}" for c in CHANNELS] array_pvs = {f"{camera_name}:{c}" for c in ARRAY_SUFFIXES} thread = Thread(target=update_PVs, args=(buffer, *pv_names), daemon=True) thread.start() return { "buffer": buffer, "pv_names": pv_names, "array_pvs": array_pvs, "sent_pid": -1, } def process_image(image, pulse_id, timestamp, x_axis, y_axis, parameters, bsdata): """ Calls processor.process_image(...), then pushes results to EPICS PVs: : No dict returned. """ camera = parameters["camera_name"] roi = parameters["roi"] invert_image = parameters["invert_image"] st = _states.get(camera) if st is None: st = _states[camera] = _initialize_camera_state(camera) if roi: image = image[roi[0]:roi[1],roi[2]:roi[3]] if invert_image: image = 1/image y_axis_len, x_axis_len = np.shape(image) x_axis = np.arange(x_axis_len) y_axis = np.arange(y_axis_len) r = processor.process_image(image, pulse_id, timestamp, x_axis, y_axis, parameters, bsdata) if not epics_lock.acquire(False): return try: if pulse_id <= st["sent_pid"]: return st["sent_pid"] = pulse_id entry = [] for suffix in CHANNELS: pvname = f"{camera}:{suffix}" entry.append(_pv_safe(r.get(suffix), pvname, st["array_pvs"])) st["buffer"].append(tuple(entry)) finally: epics_lock.release()