128 lines
4.0 KiB
Python
128 lines
4.0 KiB
Python
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:
|
|
<camera_name>:<suffix>
|
|
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()
|
|
|