adjust gripper sensor voltage in config #232
BIN
Binary file not shown.
@@ -380,7 +380,7 @@ class FlomniInitStagesMixin:
|
||||
umv(dev.fsamy, flomni_samy_in)
|
||||
|
||||
# after init reduce vertical stage speed
|
||||
dev.fsamy.controller.socket_put_confirmed("axspeed[5]=20000")
|
||||
dev.fsamy.controller.socket_put_confirmed("axspeed[5]=5000")
|
||||
|
||||
umv(dev.feyey, -8)
|
||||
|
||||
@@ -1751,9 +1751,12 @@ class Flomni(
|
||||
start_angle (float, optional): The start angle of the scan. Defaults to None.
|
||||
"""
|
||||
|
||||
if start_angle is not None:
|
||||
explicit_start_angle = start_angle is not None
|
||||
if explicit_start_angle:
|
||||
print(f"Sub tomo scan with start angle {start_angle} requested.")
|
||||
|
||||
max_allowed_angle = self.tomo_angle_range + 0.05 + self.tomo_angle_stepsize
|
||||
|
||||
if start_angle is None:
|
||||
if subtomo_number == 1:
|
||||
start_angle = 0
|
||||
@@ -1772,22 +1775,31 @@ class Flomni(
|
||||
elif subtomo_number == 8:
|
||||
start_angle = self.tomo_angle_stepsize / 8.0 * 7
|
||||
|
||||
if not subtomo_number % 2: # even = reverse
|
||||
# The table above gives the LOW end of this sub-tomogram's
|
||||
# angular phase (same convention as the forward/odd
|
||||
# sub-tomograms - it's what makes the combined 8 sub-tomograms
|
||||
# interlace into one fine angular grid). A reverse sweep must
|
||||
# begin at the HIGH end of that span and descend, so shift the
|
||||
# freshly-computed phase up by one full angular range. This
|
||||
# step is skipped when start_angle is given explicitly (i.e.
|
||||
# we are resuming mid sub-tomogram), since then the value is
|
||||
# already the literal current angle.
|
||||
start_angle = min(start_angle + self.tomo_angle_range, max_allowed_angle)
|
||||
|
||||
# _tomo_shift_angles (potential global variable)
|
||||
_tomo_shift_angles = 0
|
||||
# compute number of projections
|
||||
|
||||
start = start_angle + _tomo_shift_angles
|
||||
|
||||
if subtomo_number % 2: # odd = forward
|
||||
max_allowed_angle = self.tomo_angle_range + 0.05 + self.tomo_angle_stepsize
|
||||
proposed_end = start + self.tomo_angle_range
|
||||
angle_end = min(proposed_end, max_allowed_angle)
|
||||
if subtomo_number % 2: # odd = forward: low -> high
|
||||
angle_end = min(start + self.tomo_angle_range, max_allowed_angle)
|
||||
span = angle_end - start
|
||||
|
||||
else: # even = reverse
|
||||
else: # even = reverse: high -> low
|
||||
min_allowed_angle = 0
|
||||
proposed_end = start - self.tomo_angle_range
|
||||
angle_end = max(proposed_end, min_allowed_angle)
|
||||
angle_end = max(start - self.tomo_angle_range, min_allowed_angle)
|
||||
span = start - angle_end
|
||||
|
||||
# number of projections needed to maintain step size
|
||||
@@ -1795,22 +1807,6 @@ class Flomni(
|
||||
|
||||
angles = np.linspace(start, angle_end, num=N, endpoint=True)
|
||||
|
||||
if subtomo_number % 2: # odd subtomos → forward direction
|
||||
# clamp end angle to max allowed
|
||||
max_allowed_angle = self.tomo_angle_range + 0.05 + self.tomo_angle_stepsize
|
||||
proposed_end = start + self.tomo_angle_range
|
||||
angle_end = min(proposed_end, max_allowed_angle)
|
||||
|
||||
angles = np.linspace(start, angle_end, num=N, endpoint=True)
|
||||
|
||||
else: # even subtomos → reverse direction
|
||||
# go FROM start_angle down toward 0
|
||||
min_allowed_angle = 0
|
||||
proposed_end = start - self.tomo_angle_range
|
||||
angle_end = max(proposed_end, min_allowed_angle)
|
||||
|
||||
angles = np.linspace(start, angle_end, num=N, endpoint=True)
|
||||
|
||||
for i, angle in enumerate(angles):
|
||||
|
||||
self.progress["subtomo"] = subtomo_number
|
||||
@@ -1818,17 +1814,16 @@ class Flomni(
|
||||
# --- NEW LOGIC FOR OFFSET WHEN start_angle IS SPECIFIED ---
|
||||
if i == 0:
|
||||
step = self.tomo_angle_stepsize
|
||||
sa = start_angle
|
||||
|
||||
if start_angle is None:
|
||||
if not explicit_start_angle:
|
||||
# normal operation: always start at zero
|
||||
self._subtomo_offset = 0
|
||||
|
||||
else:
|
||||
if subtomo_number % 2: # odd = forward direction
|
||||
self._subtomo_offset = round(sa / step)
|
||||
self._subtomo_offset = round(start_angle / step)
|
||||
else: # even = reverse direction
|
||||
self._subtomo_offset = round((self.tomo_angle_range - sa) / step)
|
||||
self._subtomo_offset = round((self.tomo_angle_range - start_angle) / step)
|
||||
|
||||
# progress index must always increase
|
||||
self.progress["subtomo_projection"] = self._subtomo_offset + i
|
||||
@@ -2147,13 +2142,17 @@ class Flomni(
|
||||
|
||||
return angle, subtomo_number
|
||||
|
||||
def tomo_reconstruct(self, base_path="~/data/raw/logs/reconstruction_queue"):
|
||||
|
||||
def tomo_reconstruct(
|
||||
self, base_path="~/data/raw/logs/reconstruction_queue", probe_propagation: float | None = None
|
||||
):
|
||||
"""write the tomo reconstruct file for the reconstruction queue"""
|
||||
bec = builtins.__dict__.get("bec")
|
||||
self.reconstructor.write(
|
||||
scan_list=self._current_scan_list,
|
||||
next_scan_number=bec.queue.next_scan_number,
|
||||
base_path=base_path,
|
||||
probe_file_propagation=probe_propagation,
|
||||
)
|
||||
|
||||
def _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None:
|
||||
@@ -2183,6 +2182,9 @@ class Flomni(
|
||||
+ self.manual_shift_y
|
||||
)
|
||||
sum_offset_z = offsets[2]
|
||||
#TODO this fix is while the tracker z is broken
|
||||
probe_propagation = -sum_offset_z * 1e-6
|
||||
sum_offset_z = 0
|
||||
|
||||
self._current_scan_list = []
|
||||
|
||||
@@ -2221,7 +2223,7 @@ class Flomni(
|
||||
|
||||
scans.flomni_fermat_scan(**scan_kwargs)
|
||||
|
||||
self.tomo_reconstruct()
|
||||
self.tomo_reconstruct(probe_propagation=probe_propagation)
|
||||
|
||||
def tomo_acquire_at_angle(self, angle: float, frames_per_trigger: int | None = None):
|
||||
"""
|
||||
@@ -2357,6 +2359,7 @@ class Flomni(
|
||||
self.frames_per_trigger = self._get_val(
|
||||
"Frames per trigger (burst)", self.frames_per_trigger, int
|
||||
)
|
||||
self.manual_shift_y = self._get_val("<manual_shift_y> um", self.manual_shift_y, float)
|
||||
self.single_point_instead_of_fermat_scan = bool(
|
||||
self._get_val(
|
||||
"Single point instead of fermat scan (acquire at angle) 1/0?",
|
||||
|
||||
@@ -30,12 +30,14 @@ class FlomniOpticsMixin:
|
||||
|
||||
# move rotation stage to zero to avoid problems with wires
|
||||
umv(dev.fsamroy, 0)
|
||||
# umv(dev.fttrx1, 9.2)
|
||||
fttrx_in = self._get_user_param_safe("feyex", "fttrx_in")
|
||||
umv(dev.fttrx1, fttrx_in)
|
||||
|
||||
def feye_in(self):
|
||||
bec.queue.next_dataset_number += 1
|
||||
# umv(dev.fttrx1, -17)
|
||||
|
||||
fttrx_out = self._get_user_param_safe("feyex", "fttrx_out")
|
||||
umv(dev.fttrx1, fttrx_out)
|
||||
|
||||
feyex_in = self._get_user_param_safe("feyex", "in")
|
||||
feyey_in = self._get_user_param_safe("feyey", "in")
|
||||
|
||||
@@ -148,7 +150,6 @@ class FlomniOpticsMixin:
|
||||
# --- expected IN positions ---
|
||||
foptx_in = self._get_user_param_safe("foptx", "in")
|
||||
fopty_in = self._get_user_param_safe("fopty", "in")
|
||||
foptz_in = self._get_user_param_safe("foptz", "in")
|
||||
|
||||
# --- expected OUT condition for the X-ray eye ---
|
||||
# eye is OUT when it is *not within tolerance* of its IN position
|
||||
@@ -159,7 +160,6 @@ class FlomniOpticsMixin:
|
||||
|
||||
cx_foptx = dev.foptx.readback.get()
|
||||
cx_fopty = dev.fopty.readback.get()
|
||||
cx_foptz = dev.foptz.readback.get()
|
||||
|
||||
# --- check eye OUT ---
|
||||
eye_out = (
|
||||
@@ -169,8 +169,7 @@ class FlomniOpticsMixin:
|
||||
# --- check optics IN ---
|
||||
optics_in = (
|
||||
np.isclose(cx_foptx, foptx_in, atol=tol) and
|
||||
np.isclose(cx_fopty, fopty_in, atol=tol) and
|
||||
np.isclose(cx_foptz, foptz_in, atol=tol)
|
||||
np.isclose(cx_fopty, fopty_in, atol=tol)
|
||||
)
|
||||
|
||||
fosax_in = self._get_user_param_safe("fosax", "in")
|
||||
@@ -205,7 +204,7 @@ class FlomniOpticsMixin:
|
||||
|
||||
if mokev_val == -1:
|
||||
try:
|
||||
mokev_val = dev.mokev.readback.get()
|
||||
mokev_val = dev.ccm_energy.get().user_readback
|
||||
except:
|
||||
print(
|
||||
"Device mokev does not exist. You can specify the energy in keV as an argument instead."
|
||||
|
||||
@@ -179,6 +179,102 @@ def _gvar(bec_client, key, fmt=None, suffix=""):
|
||||
except Exception:
|
||||
return str(val)
|
||||
|
||||
def _read_beamline_states_info(bec_client) -> dict:
|
||||
"""Return a diagnostic snapshot of all configured beamline states, for
|
||||
display only (NOT used to derive the 'blocked' experiment status — that
|
||||
is computed from the queue's own locks via _read_queue_locks(), which is
|
||||
the authoritative source; see its docstring for why).
|
||||
|
||||
Returns:
|
||||
{
|
||||
"enabled": bool | None, # bec.builtin_actors.scan_interlock.enabled
|
||||
"states": [
|
||||
{
|
||||
"name": str,
|
||||
"status": str, # 'valid' | 'invalid' | 'warning' | 'unknown'
|
||||
"label": str,
|
||||
"watched": bool, # True if present in states_watched
|
||||
"accepted": list[str] | None, # accepted statuses if watched
|
||||
"mismatched": bool, # True if watched AND status not in accepted
|
||||
},
|
||||
...
|
||||
],
|
||||
}
|
||||
|
||||
Beamline states are registered dynamically and can differ from
|
||||
experiment to experiment, so names are read from the manager's _states
|
||||
dict (the same one show_all() iterates over internally — there is no
|
||||
public "list names" method as of this BEC version). states_watched and
|
||||
enabled are read through the public scan_interlock HLI properties.
|
||||
"""
|
||||
manager = getattr(bec_client, "beamline_states", None)
|
||||
if manager is None:
|
||||
return {"enabled": None, "states": []}
|
||||
|
||||
try:
|
||||
names = list(getattr(manager, "_states", {}).keys())
|
||||
except Exception:
|
||||
names = []
|
||||
|
||||
try:
|
||||
interlock = bec_client.builtin_actors.scan_interlock
|
||||
enabled = interlock.enabled
|
||||
states_watched = interlock.states_watched or {}
|
||||
except Exception:
|
||||
enabled = None
|
||||
states_watched = {}
|
||||
|
||||
states = []
|
||||
for name in names:
|
||||
try:
|
||||
state_obj = getattr(manager, name, None)
|
||||
if state_obj is None:
|
||||
continue
|
||||
info = state_obj.get() # {"status": ..., "label": ...}
|
||||
status = info.get("status", "unknown")
|
||||
label = info.get("label", name)
|
||||
except Exception:
|
||||
status, label = "unknown", name
|
||||
|
||||
accepted = states_watched.get(name)
|
||||
watched = accepted is not None
|
||||
mismatched = watched and status not in accepted
|
||||
|
||||
states.append({
|
||||
"name": name,
|
||||
"status": status,
|
||||
"label": label,
|
||||
"watched": watched,
|
||||
"accepted": accepted,
|
||||
"mismatched": mismatched,
|
||||
})
|
||||
|
||||
return {"enabled": enabled, "states": states}
|
||||
|
||||
|
||||
def _read_queue_locks(primary) -> list:
|
||||
"""Return a list of {"identifier": str, "reason": str} for every lock
|
||||
currently applied to the primary scan queue.
|
||||
|
||||
BEC's scan_queue.status becomes "LOCKED" (saving the prior status to
|
||||
restore later) whenever any lock is added, e.g. by the ScanInterlockActor
|
||||
when a watched beamline state goes out of spec (see scan_interlock.py:
|
||||
add_queue_lock(queue="primary", reason=..., lock_id="ScanInterlockActor")).
|
||||
Reading the lock list directly is more robust than re-deriving "blocked"
|
||||
from beamline states ourselves: it reflects whatever is actually holding
|
||||
the queue, regardless of which actor (interlock or otherwise) caused it,
|
||||
and survives changes to which beamline states are configured/watched.
|
||||
"""
|
||||
if primary is None:
|
||||
return []
|
||||
try:
|
||||
return [
|
||||
{"identifier": lock.identifier, "reason": lock.reason}
|
||||
for lock in (primary.locks or [])
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status derivation
|
||||
@@ -189,19 +285,27 @@ def _derive_status(
|
||||
queue_has_active_scan: bool,
|
||||
last_active_time,
|
||||
had_activity: bool,
|
||||
queue_locks: list | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Returns one of:
|
||||
scanning -- tomo heartbeat fresh (< _TOMO_HEARTBEAT_STALE_S), OR
|
||||
heartbeat recently seen AND queue still has active scan
|
||||
(handles long individual projections > heartbeat timeout)
|
||||
running -- queue has active scan but heartbeat has never been seen
|
||||
blocked -- queue has active scan, heartbeat stale (beyond the long-
|
||||
projection grace window), AND the primary queue has at
|
||||
least one lock applied (e.g. by BEC's ScanInterlockActor
|
||||
when a watched beamline state goes out of spec). Only
|
||||
reported when it is actually preventing a queued/active
|
||||
scan from progressing.
|
||||
running -- queue has active scan but heartbeat has never been seen,
|
||||
and the queue is not locked
|
||||
idle -- not scanning, last_active_time known
|
||||
unknown -- no activity ever seen since generator started
|
||||
|
||||
'unknown' is ONLY returned before any scan activity has been observed.
|
||||
Once activity has been seen the status goes directly:
|
||||
scanning/running -> idle
|
||||
scanning/running/blocked -> idle
|
||||
never through 'unknown'.
|
||||
"""
|
||||
hb_age = _heartbeat_age_s(progress.get("heartbeat"))
|
||||
@@ -213,6 +317,8 @@ def _derive_status(
|
||||
if queue_has_active_scan and hb_age < _TOMO_HEARTBEAT_STALE_S * 10:
|
||||
return "scanning"
|
||||
if queue_has_active_scan:
|
||||
if queue_locks:
|
||||
return "blocked"
|
||||
return "running"
|
||||
if last_active_time is not None or had_activity:
|
||||
return "idle"
|
||||
@@ -852,7 +958,10 @@ class WebpageGeneratorBase:
|
||||
progress = self._bec.get_global_var("tomo_progress") or {}
|
||||
|
||||
# ── Queue ────────────────────────────────────────────────────
|
||||
# queue_status is always 'RUNNING' while BEC is alive.
|
||||
# queue_status reflects BEC's ScanQueueStatus: 'RUNNING', 'PAUSED',
|
||||
# or 'LOCKED' (the latter set whenever any lock — e.g. from
|
||||
# ScanInterlockActor — is applied to the queue; the prior status is
|
||||
# restored once the lock is released).
|
||||
# A scan is actually executing only when info is non-empty AND
|
||||
# active_request_block is set on the first entry.
|
||||
try:
|
||||
@@ -864,9 +973,30 @@ class WebpageGeneratorBase:
|
||||
and len(primary.info) > 0
|
||||
and primary.info[0].active_request_block is not None
|
||||
)
|
||||
queue_locks = _read_queue_locks(primary)
|
||||
except Exception:
|
||||
queue_status = "unknown"
|
||||
queue_has_active_scan = False
|
||||
queue_locks = []
|
||||
|
||||
# ── Beamline states (diagnostic display only; not used for the
|
||||
# 'blocked' experiment_status decision — see _read_queue_locks) ──
|
||||
try:
|
||||
beamline_states_info = _read_beamline_states_info(self._bec)
|
||||
except Exception as exc:
|
||||
self._log(VERBOSITY_VERBOSE,
|
||||
f"beamline_states read error: {exc}", level="warning")
|
||||
beamline_states_info = {"enabled": None, "states": []}
|
||||
|
||||
# Current scan number (the BEC scan in progress right now, e.g. for
|
||||
# comparison against ptycho reconstruction filenames like S06770).
|
||||
# Only meaningful while a scan is actually active; None otherwise.
|
||||
current_scan_number = None
|
||||
if queue_has_active_scan:
|
||||
try:
|
||||
current_scan_number = primary.info[0].active_request_block.scan_number
|
||||
except Exception:
|
||||
current_scan_number = None
|
||||
|
||||
# ── Idle tracking ────────────────────────────────────────────
|
||||
hb_age = _heartbeat_age_s(progress.get("heartbeat"))
|
||||
@@ -893,6 +1023,7 @@ class WebpageGeneratorBase:
|
||||
exp_status = _derive_status(
|
||||
progress, queue_has_active_scan,
|
||||
self._last_active_time, self._had_activity,
|
||||
queue_locks,
|
||||
)
|
||||
idle_for_s = (
|
||||
None if self._last_active_time is None
|
||||
@@ -928,6 +1059,8 @@ class WebpageGeneratorBase:
|
||||
"queue_has_active_scan": queue_has_active_scan,
|
||||
"idle_for_s": idle_for_s,
|
||||
"idle_for_human": _format_duration(idle_for_s),
|
||||
"queue_locks": queue_locks,
|
||||
"beamline_states": beamline_states_info,
|
||||
"progress": {
|
||||
"tomo_type": progress.get("tomo_type", "N/A"),
|
||||
"projection": progress.get("projection", 0),
|
||||
@@ -937,10 +1070,13 @@ class WebpageGeneratorBase:
|
||||
"subtomo_total_projections": progress.get("subtomo_total_projections", 1),
|
||||
"angle": progress.get("angle", 0),
|
||||
"tomo_start_time": progress.get("tomo_start_time"),
|
||||
"tomo_start_scan_number": progress.get("tomo_start_scan_number"),
|
||||
"current_scan_number": current_scan_number,
|
||||
"estimated_remaining_s": progress.get("estimated_remaining_time"),
|
||||
"estimated_remaining_human": _format_duration(
|
||||
progress.get("estimated_remaining_time")
|
||||
),
|
||||
"estimated_finish_time": progress.get("estimated_finish_time"),
|
||||
"tomo_heartbeat": progress.get("heartbeat"),
|
||||
"tomo_heartbeat_age_s": round(hb_age, 1) if hb_age != float("inf") else None,
|
||||
},
|
||||
@@ -985,12 +1121,16 @@ class WebpageGeneratorBase:
|
||||
self._uploader.upload_changed_async(self._output_dir)
|
||||
|
||||
# ── Console feedback ──────────────────────────────────────────
|
||||
blocked_summary = (
|
||||
f" locked_by={','.join(l['identifier'] for l in queue_locks)}"
|
||||
if queue_locks else ""
|
||||
)
|
||||
self._log(VERBOSITY_VERBOSE,
|
||||
f"[{_now_iso()}] {exp_status:<12} active={queue_has_active_scan} "
|
||||
f"proj={payload['progress']['projection']}/"
|
||||
f"{payload['progress']['total_projections']} "
|
||||
f"hb={payload['progress']['tomo_heartbeat_age_s']}s "
|
||||
f"idle={_format_duration(idle_for_s)}")
|
||||
f"idle={_format_duration(idle_for_s)}" + blocked_summary)
|
||||
self._log(VERBOSITY_DEBUG,
|
||||
f" payload:\n{json.dumps(payload, indent=4, default=str)}")
|
||||
|
||||
@@ -1331,6 +1471,8 @@ def _render_html(phone_numbers: list) -> str:
|
||||
--c-scanning: #89dceb;
|
||||
--c-running: #a6e3a1;
|
||||
--c-idle-short: #f9e2af;
|
||||
--c-idle-long: #f38ba8;
|
||||
--c-blocked: #fab387;
|
||||
--c-error: #f38ba8;
|
||||
--status-color: #6c7a9c;
|
||||
--ring-blend: #4a5568;
|
||||
@@ -1373,6 +1515,7 @@ def _render_html(phone_numbers: list) -> str:
|
||||
body.scanning {{ --status-color: var(--c-scanning); --ring-blend: #3a6b74; }}
|
||||
body.running {{ --status-color: var(--c-running); --ring-blend: #3a6644; }}
|
||||
body.idle {{ --status-color: var(--c-idle-short); --ring-blend: #7a6e44; }}
|
||||
body.blocked {{ --status-color: var(--c-blocked); --ring-blend: #7a5a3a; }}
|
||||
body.error {{ --status-color: var(--c-error); --ring-blend: #7a3a44; }}
|
||||
body.unknown {{ --status-color: var(--text-dim); --ring-blend: #4a5568; }}
|
||||
|
||||
@@ -1585,6 +1728,36 @@ def _render_html(phone_numbers: list) -> str:
|
||||
border-radius: 4px; box-shadow: 0 8px 40px rgba(0,0,0,0.6);
|
||||
}}
|
||||
|
||||
/* Help dialog */
|
||||
#help-dialog {{
|
||||
background: var(--surface); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 1.5rem 1.75rem; max-width: 560px; width: 90vw;
|
||||
max-height: 80vh; overflow-y: auto;
|
||||
font-family: var(--sans); font-weight: 300;
|
||||
}}
|
||||
#help-dialog::backdrop {{ background: rgba(0,0,0,0.6); }}
|
||||
.help-header {{
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border); padding-bottom: 0.75rem; margin-bottom: 1rem;
|
||||
}}
|
||||
.help-close {{
|
||||
background: none; border: none; color: var(--text-dim);
|
||||
font-size: 1.4rem; line-height: 1; padding: 0 0.3rem; min-height: auto;
|
||||
cursor: pointer;
|
||||
}}
|
||||
.help-close:hover {{ color: var(--text); background: none; }}
|
||||
.help-body h3 {{
|
||||
font-family: var(--mono); font-size: 0.7rem; font-weight: 700;
|
||||
letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-dim);
|
||||
margin: 1.1rem 0 0.5rem;
|
||||
}}
|
||||
.help-body h3:first-child {{ margin-top: 0; }}
|
||||
.help-body p {{ font-size: 0.85rem; line-height: 1.6; color: var(--text-dim); margin-bottom: 0.5rem; }}
|
||||
.help-body ul {{ margin: 0 0 0.5rem 1.1rem; padding: 0; }}
|
||||
.help-body li {{ font-size: 0.85rem; line-height: 1.6; color: var(--text-dim); margin-bottom: 0.4rem; }}
|
||||
.help-body strong {{ color: var(--text); font-weight: 600; }}
|
||||
|
||||
/* ── Instrument details ── */
|
||||
.instrument-grid {{
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem 2rem;
|
||||
@@ -1602,6 +1775,38 @@ def _render_html(phone_numbers: list) -> str:
|
||||
.kv-table td:last-child {{ color: var(--text); font-weight: 600; text-align: right; }}
|
||||
.temp-null {{ color: var(--text-dim) !important; font-weight: 400 !important; }}
|
||||
|
||||
/* ── Beamline states ── */
|
||||
.blstates-summary {{
|
||||
font-size: 0.82rem; color: var(--text-dim); margin-bottom: 0.9rem;
|
||||
}}
|
||||
.blstates-summary strong {{ color: var(--text); font-weight: 600; }}
|
||||
.blstates-table {{ width: 100%; border-collapse: collapse; }}
|
||||
.blstates-table th {{
|
||||
font-family: var(--mono); font-size: 0.6rem; font-weight: 700;
|
||||
letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-dim);
|
||||
text-align: left; padding: 0 0.6rem 0.5rem 0; border-bottom: 1px solid var(--border);
|
||||
}}
|
||||
.blstates-table td {{
|
||||
padding: 0.45rem 0.6rem 0.45rem 0; font-size: 0.82rem;
|
||||
border-bottom: 1px solid var(--border); vertical-align: middle;
|
||||
}}
|
||||
.blstates-table tr:last-child td {{ border-bottom: none; }}
|
||||
.blstates-table tr.mismatched td {{ color: var(--c-blocked); }}
|
||||
.bl-name {{ font-family: var(--mono); font-size: 0.78rem; color: var(--text); white-space: nowrap; }}
|
||||
.bl-label {{ color: var(--text-dim); }}
|
||||
.bl-badge {{
|
||||
display: inline-block; font-family: var(--mono); font-size: 0.6rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase; padding: 0.15rem 0.5rem;
|
||||
border-radius: 100px; white-space: nowrap;
|
||||
}}
|
||||
.bl-badge.bl-valid {{ background: color-mix(in srgb, var(--c-running) 22%, transparent); color: var(--c-running); }}
|
||||
.bl-badge.bl-invalid {{ background: color-mix(in srgb, var(--c-error) 22%, transparent); color: var(--c-error); }}
|
||||
.bl-badge.bl-warning {{ background: color-mix(in srgb, var(--c-idle-short) 25%, transparent); color: var(--c-idle-short); }}
|
||||
.bl-badge.bl-unknown {{ background: var(--surface2); color: var(--text-dim); }}
|
||||
.bl-badge.bl-watched-yes {{ background: color-mix(in srgb, var(--c-scanning) 18%, transparent); color: var(--c-scanning); }}
|
||||
.bl-badge.bl-watched-no {{ background: var(--surface2); color: var(--text-dim); }}
|
||||
.blstates-none {{ font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); padding: 0.5rem 0; }}
|
||||
|
||||
/* ── Audio card ── */
|
||||
.audio-card {{
|
||||
display: flex; align-items: center;
|
||||
@@ -1628,6 +1833,9 @@ def _render_html(phone_numbers: list) -> str:
|
||||
.led.led-warning {{ background: var(--c-idle-long); border-color: var(--c-idle-long);
|
||||
box-shadow: 0 0 8px var(--c-idle-long);
|
||||
animation: led-pulse 1s ease-in-out infinite; }}
|
||||
.led.led-blocked {{ background: var(--c-blocked); border-color: var(--c-blocked);
|
||||
box-shadow: 0 0 8px var(--c-blocked);
|
||||
animation: led-pulse 1s ease-in-out infinite; }}
|
||||
@keyframes led-pulse {{
|
||||
0%,100% {{ opacity: 1; }} 50% {{ opacity: 0.4; }}
|
||||
}}
|
||||
@@ -1716,6 +1924,7 @@ def _render_html(phone_numbers: list) -> str:
|
||||
<span class="logo-suffix">· STATUS</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="theme-btn" id="help-btn" onclick="openHelp()" title="What does this page show?">?</button>
|
||||
<div class="theme-switcher">
|
||||
<span class="ts-label">Theme</span>
|
||||
<button class="theme-btn" id="theme-auto" onclick="setTheme('auto')" >Auto</button>
|
||||
@@ -1753,7 +1962,8 @@ def _render_html(phone_numbers: list) -> str:
|
||||
<div class="info-item"><span class="label">Sub-tomo</span><span class="value" id="pi-subtomo">-</span></div>
|
||||
<div class="info-item"><span class="label">Angle</span><span class="value" id="pi-angle">-</span></div>
|
||||
<div class="info-item"><span class="label">Tomo type</span><span class="value" id="pi-type">-</span></div>
|
||||
<div class="info-item"><span class="label">ETA</span><span class="value" id="pi-eta">-</span></div>
|
||||
<div class="info-item"><span class="label">Remaining</span><span class="value" id="pi-remaining">-</span></div>
|
||||
<div class="info-item"><span class="label">Finish time</span><span class="value" id="pi-finish">-</span></div>
|
||||
<div class="info-item"><span class="label">Started</span><span class="value" id="pi-start">-</span></div>
|
||||
</div>
|
||||
|
||||
@@ -1765,6 +1975,39 @@ def _render_html(phone_numbers: list) -> str:
|
||||
<img id="ptycho-lightbox-img" src="" alt="">
|
||||
</div>
|
||||
|
||||
<!-- Help dialog -->
|
||||
<dialog id="help-dialog">
|
||||
<div class="help-header">
|
||||
<span class="card-title" style="border-bottom:none;margin-bottom:0;padding-bottom:0">About this page</span>
|
||||
<button class="help-close" onclick="closeHelp()" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="help-body">
|
||||
<h3>Measurement status</h3>
|
||||
<p>The coloured pill at the top reflects what the instrument is currently doing:</p>
|
||||
<ul>
|
||||
<li><strong>Scanning</strong> — a tomography scan is actively running (a heartbeat from the scan loop has been seen recently).</li>
|
||||
<li><strong>Running</strong> — the scan queue has an active item, but it is not a tomo scan with a heartbeat (e.g. an alignment scan).</li>
|
||||
<li><strong>Blocked</strong> — a scan is queued or active, but the scan queue itself has been locked (for example by BEC's scan interlock when a watched beamline condition, such as the shutter or ring current, is out of spec). This is usually external and self-resolves once the condition clears.</li>
|
||||
<li><strong>Idle</strong> — nothing is running or queued right now.</li>
|
||||
<li><strong>Unknown</strong> — shown only before any activity has been observed since the generator started.</li>
|
||||
</ul>
|
||||
<h3>Tomography progress</h3>
|
||||
<p>The rings show overall progress (outer) and progress within the current sub-tomogram (inner). <strong>Remaining</strong> is the estimated duration left; <strong>Finish time</strong> is the estimated wall-clock time the measurement will complete. The projection count is followed by the BEC scan number(s) in parentheses, e.g. (S06650→S06770), for direct comparison with reconstruction filenames.</p>
|
||||
<h3>Audio warnings</h3>
|
||||
<ul>
|
||||
<li><strong>System LED</strong> — on when audio is enabled on this device.</li>
|
||||
<li><strong>Watch LED</strong> — armed (green) while a scan is running; pulses orange if a scan stops unexpectedly, with a chime every 30s until confirmed.</li>
|
||||
<li><strong>Blocked LED</strong> — pulses orange while the queue is locked. A chime fires automatically after 60 continuous minutes blocked, then repeats hourly until cleared. No confirmation needed — it clears itself once the block resolves.</li>
|
||||
<li><strong>Live LED</strong> — green while this page's data feed is fresh; pulses orange if the feed goes stale or a fetch fails.</li>
|
||||
</ul>
|
||||
<p>On iPhone/iPad, tap <strong>Enable</strong> once to unlock audio — this is a browser requirement and only needs to be done once per visit.</p>
|
||||
<h3>Beamline states</h3>
|
||||
<p>Lists every beamline condition currently registered with BEC, its live status (valid/invalid/warning/unknown), and whether the scan interlock is <strong>watching</strong> it. A watched state that is not in its accepted status is what actually causes the <strong>Blocked</strong> status above — states that are not watched (like an unrelated simulated shutter) can be invalid without blocking anything.</p>
|
||||
<h3>Other features</h3>
|
||||
<p>Cards below the fixed status/progress section can be dragged by their ⋮⋮ handle to reorder; the order is remembered on this device. The theme switcher (top right) follows your OS by default, or can be forced to light/dark.</p>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Draggable cards container -->
|
||||
<div id="card-container">
|
||||
|
||||
@@ -1780,6 +2023,10 @@ def _render_html(phone_numbers: list) -> str:
|
||||
<div class="led" id="led-watch"></div>
|
||||
<span class="led-label">Watch</span>
|
||||
</div>
|
||||
<div class="led-group">
|
||||
<div class="led" id="led-blocked"></div>
|
||||
<span class="led-label">Blocked</span>
|
||||
</div>
|
||||
<div class="led-group">
|
||||
<div class="led" id="led-conn"></div>
|
||||
<span class="led-label">Live</span>
|
||||
@@ -1822,7 +2069,20 @@ def _render_html(phone_numbers: list) -> str:
|
||||
<div class="instrument-grid" id="instrument-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Contacts -->
|
||||
<!-- 4. Beamline states -->
|
||||
<div class="card draggable-card" id="blstates-card" data-card-id="blstates">
|
||||
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
|
||||
<div class="card-title">Beamline states</div>
|
||||
<div class="blstates-summary" id="blstates-summary"></div>
|
||||
<table class="blstates-table" id="blstates-table">
|
||||
<thead>
|
||||
<tr><th>State</th><th>Status</th><th>Watched</th><th>Label</th></tr>
|
||||
</thead>
|
||||
<tbody id="blstates-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 5. Contacts -->
|
||||
<div class="card draggable-card" data-card-id="contacts">
|
||||
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
|
||||
<div class="card-title">Contacts</div>
|
||||
@@ -1860,7 +2120,7 @@ function setTheme(t) {{
|
||||
|
||||
// ── Drag-and-drop card ordering ──────────────────────────────────────────
|
||||
const CARD_ORDER_KEY = 'cardOrder';
|
||||
const DEFAULT_ORDER = ['audio','recon-queue','ptycho','instrument','contacts'];
|
||||
const DEFAULT_ORDER = ['audio','recon-queue','ptycho','instrument','blstates','contacts'];
|
||||
let _dragSrc = null;
|
||||
|
||||
function savedOrder() {{
|
||||
@@ -1932,6 +2192,9 @@ initDrag();
|
||||
let audioCtx=null, audioEnabled=false;
|
||||
let audioArmed=false, warningActive=false, warningTimer=null, lastStatus=null;
|
||||
let staleActive=false, staleTimer=null, staleConfirmed=false;
|
||||
let blockedSince=null, blockedWarningActive=false, blockedChimeTimer=null;
|
||||
const BLOCKED_WARNING_DELAY_MS=60*60*1000; // first chime after 60 min continuously blocked
|
||||
const BLOCKED_CHIME_REPEAT_MS=60*60*1000; // repeat every 60 min while still blocked
|
||||
|
||||
function getCtx(){{
|
||||
if(!audioCtx) audioCtx=new(window.AudioContext||window.webkitAudioContext)();
|
||||
@@ -1967,6 +2230,12 @@ function staleChime(){{
|
||||
setTimeout(()=>beep(1200,0.12,0.35),180);
|
||||
setTimeout(()=>beep(1200,0.25,0.35),360);
|
||||
}}
|
||||
function blockedChime(){{
|
||||
// Distinct from warningChime/staleChime: a slow low->low->high triple knock.
|
||||
beep(330,0.25,0.4);
|
||||
setTimeout(()=>beep(330,0.25,0.4),300);
|
||||
setTimeout(()=>beep(523,0.4, 0.4),600);
|
||||
}}
|
||||
|
||||
function testSound(){{
|
||||
// Gesture handler — unlock first, then delay beeps 80ms for resume().
|
||||
@@ -1986,8 +2255,10 @@ function toggleAudio(){{
|
||||
document.getElementById('btn-confirm').style.display='none';
|
||||
stopStaleWarning(); staleActive=false; staleConfirmed=false;
|
||||
document.getElementById('btn-confirm-stale').style.display='none';
|
||||
stopBlockedChime(); blockedWarningActive=false;
|
||||
}} else {{
|
||||
if(lastStatus==='scanning' && !audioArmed) audioArmed=true;
|
||||
if(blockedSince!==null && blockedChimeTimer===null) scheduleBlockedChime();
|
||||
}}
|
||||
updateAudioUI();
|
||||
}}
|
||||
@@ -2048,9 +2319,45 @@ function handleStale(isStale){{
|
||||
}}
|
||||
}}
|
||||
|
||||
function stopBlockedChime(){{
|
||||
if(blockedChimeTimer){{clearTimeout(blockedChimeTimer);blockedChimeTimer=null;}}
|
||||
}}
|
||||
|
||||
function scheduleBlockedChime(){{
|
||||
// Fires once after BLOCKED_WARNING_DELAY_MS of continuous 'blocked' status,
|
||||
// then repeats every BLOCKED_CHIME_REPEAT_MS for as long as it stays blocked.
|
||||
// No confirm button — purely informational, auto-clears on recovery, since
|
||||
// blocked states are external (beamline) and typically self-resolve.
|
||||
blockedChimeTimer=setTimeout(()=>{{
|
||||
if(audioEnabled) blockedChime();
|
||||
blockedWarningActive=true;
|
||||
updateAudioUI();
|
||||
scheduleBlockedChime();
|
||||
}}, blockedWarningActive ? BLOCKED_CHIME_REPEAT_MS : BLOCKED_WARNING_DELAY_MS);
|
||||
}}
|
||||
|
||||
function handleBlocked(isBlocked){{
|
||||
if(isBlocked){{
|
||||
if(blockedSince===null){{
|
||||
blockedSince=Date.now();
|
||||
blockedWarningActive=false;
|
||||
stopBlockedChime();
|
||||
if(audioEnabled) scheduleBlockedChime();
|
||||
}}
|
||||
}}else{{
|
||||
if(blockedSince!==null){{
|
||||
blockedSince=null;
|
||||
blockedWarningActive=false;
|
||||
stopBlockedChime();
|
||||
updateAudioUI();
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
function updateAudioUI(){{
|
||||
const ledSys=document.getElementById('led-system'),
|
||||
ledWatch=document.getElementById('led-watch'),
|
||||
ledBlocked=document.getElementById('led-blocked'),
|
||||
ledConn=document.getElementById('led-conn'),
|
||||
btn=document.getElementById('btn-toggle'),
|
||||
txt=document.getElementById('audio-text');
|
||||
@@ -2060,6 +2367,7 @@ function updateAudioUI(){{
|
||||
btn.classList.toggle('active',audioEnabled);
|
||||
|
||||
ledConn.className='led'+(staleActive?' led-warning':' led-live');
|
||||
ledBlocked.className='led'+(blockedSince!==null?' led-blocked':'');
|
||||
|
||||
const scanRunning=(lastStatus==='scanning');
|
||||
if(!audioEnabled){{
|
||||
@@ -2074,6 +2382,11 @@ function updateAudioUI(){{
|
||||
}}else if(staleActive){{
|
||||
ledWatch.className='led';
|
||||
txt.textContent='Live feed lost \u2014 confirm to silence';
|
||||
}}else if(blockedSince!==null){{
|
||||
ledWatch.className='led';
|
||||
txt.textContent=blockedWarningActive
|
||||
? 'Beamline blocked \u2014 chiming hourly until cleared'
|
||||
: 'Beamline blocked \u2014 will chime after 60 min if unresolved';
|
||||
}}else if(audioArmed && scanRunning){{
|
||||
ledWatch.className='led led-armed';
|
||||
txt.textContent='Armed \u2014 will warn when measurement stops';
|
||||
@@ -2107,6 +2420,17 @@ function handleAudioForStatus(status, prevStatus){{
|
||||
}}
|
||||
}}
|
||||
|
||||
// ── Help dialog ──────────────────────────────────────────────────────────
|
||||
function openHelp(){{
|
||||
document.getElementById('help-dialog').showModal();
|
||||
}}
|
||||
function closeHelp(){{
|
||||
document.getElementById('help-dialog').close();
|
||||
}}
|
||||
document.getElementById('help-dialog').addEventListener('click', e=>{{
|
||||
if(e.target.id==='help-dialog') closeHelp(); // click on backdrop area
|
||||
}});
|
||||
|
||||
// ── Ptychography images ────────────────────────────────────────────
|
||||
let _ptychoScanId = null;
|
||||
|
||||
@@ -2157,11 +2481,13 @@ function renderPtycho(ptycho){{
|
||||
}}
|
||||
|
||||
// ── Status rendering ──────────────────────────────────────────────────────
|
||||
const LABELS={{scanning:'SCANNING',running:'RUNNING',idle:'IDLE',error:'STOPPED',unknown:'UNKNOWN'}};
|
||||
const LABELS={{scanning:'SCANNING',running:'RUNNING',idle:'IDLE',blocked:'BLOCKED',error:'STOPPED',unknown:'UNKNOWN'}};
|
||||
const DETAILS={{
|
||||
scanning: d=>'Tomo scan in progress · projection '+(d.progress.projection||0)+' of '+(d.progress.total_projections||0)+' · '+(d.progress.tomo_type||''),
|
||||
running: d=>'Queue active · outside tomo heartbeat window',
|
||||
idle: d=>'Idle for <strong>'+d.idle_for_human+'</strong>',
|
||||
blocked: d=>'Queue locked'+((d.queue_locks&&d.queue_locks.length>1)?' by multiple locks':'')+': <strong>'
|
||||
+(d.queue_locks||[]).map(l=>esc(l.reason||l.identifier)).join('; ')+'</strong>',
|
||||
error: d=>'Queue stopped unexpectedly · idle for <strong>'+(d.idle_for_human||'?')+'</strong>',
|
||||
unknown: d=>'Waiting for first data\u2026',
|
||||
}};
|
||||
@@ -2169,6 +2495,12 @@ const DETAILS={{
|
||||
function setRing(id,circ,pct){{document.getElementById(id).style.strokeDashoffset=circ*(1-Math.min(Math.max(pct,0),1));}}
|
||||
function fmtAngle(v){{const n=parseFloat(v);return isNaN(n)?'-':n.toFixed(2)+'\u00b0';}}
|
||||
function fmtTime(iso){{if(!iso)return'-';try{{return new Date(iso).toLocaleTimeString([],{{hour:'2-digit',minute:'2-digit'}});}}catch{{return iso;}}}}
|
||||
function fmtScan(n){{
|
||||
if(n==null) return null;
|
||||
const i=parseInt(n,10);
|
||||
if(isNaN(i)) return null;
|
||||
return 'S'+String(i).padStart(5,'0');
|
||||
}}
|
||||
function esc(s){{return String(s==null?'N/A':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}}
|
||||
|
||||
function renderInstrument(setup){{
|
||||
@@ -2192,6 +2524,39 @@ function renderInstrument(setup){{
|
||||
grid.innerHTML=html;
|
||||
}}
|
||||
|
||||
function renderBeamlineStates(bl){{
|
||||
const summary=document.getElementById('blstates-summary'),
|
||||
tbody=document.getElementById('blstates-tbody');
|
||||
const states=(bl&&bl.states)||[];
|
||||
const enabledTxt = bl&&bl.enabled===true ? 'enabled' : (bl&&bl.enabled===false ? 'disabled' : 'unknown');
|
||||
const mismatched = states.filter(s=>s.mismatched);
|
||||
|
||||
if(states.length===0){{
|
||||
summary.innerHTML='Scan interlock <strong>'+enabledTxt+'</strong> · no beamline states configured';
|
||||
tbody.innerHTML='<tr><td colspan="4"><div class="blstates-none">No beamline states configured</div></td></tr>';
|
||||
return;
|
||||
}}
|
||||
|
||||
summary.innerHTML = 'Scan interlock <strong>'+enabledTxt+'</strong>'
|
||||
+ (mismatched.length>0
|
||||
? ' · <strong>'+mismatched.length+'</strong> watched state'+(mismatched.length>1?'s':'')+' currently blocking'
|
||||
: ' · all watched states OK');
|
||||
|
||||
let html='';
|
||||
states.forEach(s=>{{
|
||||
const statusCls='bl-badge bl-'+(s.status||'unknown');
|
||||
const watchedCls='bl-badge '+(s.watched?'bl-watched-yes':'bl-watched-no');
|
||||
const rowCls=s.mismatched?' class="mismatched"':'';
|
||||
html+='<tr'+rowCls+'>'
|
||||
+'<td class="bl-name">'+esc(s.name)+'</td>'
|
||||
+'<td><span class="'+statusCls+'">'+esc(s.status)+'</span></td>'
|
||||
+'<td><span class="'+watchedCls+'">'+(s.watched?'watched':'not watched')+'</span></td>'
|
||||
+'<td class="bl-label">'+esc(s.label)+'</td>'
|
||||
+'</tr>';
|
||||
}});
|
||||
tbody.innerHTML=html;
|
||||
}}
|
||||
|
||||
function render(d){{
|
||||
const s=d.experiment_status||'unknown',p=d.progress||{{}};
|
||||
document.body.className=s;
|
||||
@@ -2201,11 +2566,17 @@ function render(d){{
|
||||
const sPct=p.subtomo_total_projections>0?p.subtomo_projection/p.subtomo_total_projections:0;
|
||||
setRing('ring-outer',289.03,oPct); setRing('ring-inner',213.63,sPct);
|
||||
document.getElementById('ring-pct').textContent=Math.round(oPct*100)+'%';
|
||||
document.getElementById('pi-proj').textContent=(p.projection||0)+' / '+(p.total_projections||0);
|
||||
let projText=(p.projection||0)+' / '+(p.total_projections||0);
|
||||
const startScan=fmtScan(p.tomo_start_scan_number), curScan=fmtScan(p.current_scan_number);
|
||||
if(startScan && curScan && startScan!==curScan) projText+=' ('+startScan+'\u2192'+curScan+')';
|
||||
else if(curScan) projText+=' ('+curScan+')';
|
||||
else if(startScan) projText+=' ('+startScan+')';
|
||||
document.getElementById('pi-proj').textContent=projText;
|
||||
document.getElementById('pi-subtomo').textContent=p.subtomo||'-';
|
||||
document.getElementById('pi-angle').textContent=fmtAngle(p.angle);
|
||||
document.getElementById('pi-type').textContent=p.tomo_type||'-';
|
||||
document.getElementById('pi-eta').textContent=p.estimated_remaining_human||'-';
|
||||
document.getElementById('pi-remaining').textContent=p.estimated_remaining_human||'-';
|
||||
document.getElementById('pi-finish').textContent=fmtTime(p.estimated_finish_time);
|
||||
document.getElementById('pi-start').textContent=fmtTime(p.tomo_start_time);
|
||||
|
||||
if(d.recon){{
|
||||
@@ -2216,18 +2587,21 @@ function render(d){{
|
||||
document.getElementById('recon-path').textContent=d.recon.folder_path||'';
|
||||
}}
|
||||
renderInstrument(d.setup);
|
||||
renderBeamlineStates(d.beamline_states);
|
||||
renderPtycho(d.ptycho);
|
||||
document.getElementById('last-update').textContent='updated '+new Date(d.generated_at).toLocaleTimeString();
|
||||
const ageS=(Date.now()/1000)-d.generated_at_epoch;
|
||||
const isStale=ageS>STALE_S;
|
||||
document.getElementById('outdated-banner').classList.toggle('visible',isStale);
|
||||
handleStale(isStale);
|
||||
handleBlocked(s==='blocked');
|
||||
document.getElementById('footer-gen').textContent='generator: '+((d.generator||{{}}).owner_id||'-');
|
||||
document.getElementById('footer-hb').textContent='tomo_heartbeat: '+(p.tomo_heartbeat_age_s!=null?p.tomo_heartbeat_age_s+'s ago':'none');
|
||||
|
||||
const prevStatus=lastStatus;
|
||||
lastStatus=s;
|
||||
handleAudioForStatus(s, prevStatus);
|
||||
updateAudioUI();
|
||||
}}
|
||||
|
||||
let _fetchFailCount=0;
|
||||
|
||||
@@ -59,7 +59,7 @@ class XrayEyeAlign:
|
||||
|
||||
dev.fsh.fshopen()
|
||||
|
||||
time.sleep(0.5)
|
||||
time.sleep(1)
|
||||
# stop live view
|
||||
if not keep_shutter_open:
|
||||
self.gui.on_live_view_enabled(False)
|
||||
@@ -94,6 +94,8 @@ class XrayEyeAlign:
|
||||
def align(self, keep_shutter_open=False):
|
||||
self.flomni.flomnigui_show_xeyealign()
|
||||
self.gui.set_dap_params_forwarding(True)
|
||||
self.gui.reset_zoom()
|
||||
|
||||
try:
|
||||
self._align_impl(keep_shutter_open)
|
||||
finally:
|
||||
@@ -101,6 +103,10 @@ class XrayEyeAlign:
|
||||
self.gui.set_dap_params_forwarding(False)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.warning(f"Failed to disable XRayEye DAP parameter forwarding: {exc}")
|
||||
try:
|
||||
self.gui.hide_crosshair()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.warning(f"Failed to hide XRayEye alignment crosshair: {exc}")
|
||||
|
||||
def _align_impl(self, keep_shutter_open=False):
|
||||
if not keep_shutter_open:
|
||||
@@ -145,12 +151,11 @@ class XrayEyeAlign:
|
||||
|
||||
if not self.test_wo_movements:
|
||||
self.flomni.fosa_out()
|
||||
|
||||
self.flomni.ffzp_in()
|
||||
self.flomni.feedback_disable()
|
||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
||||
umv(dev.fsamx, fsamx_in - 0.25)
|
||||
|
||||
self.flomni.ffzp_in()
|
||||
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
self.gui.enable_submit_button(True)
|
||||
@@ -188,6 +193,15 @@ class XrayEyeAlign:
|
||||
self.flomni.feedback_enable_with_reset()
|
||||
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
# Mark the FZP center on the live view: it stays visible as a
|
||||
# fixed reference while the sample is aligned at each
|
||||
# subsequent rotation angle (steps 1-4 below).
|
||||
fzp_center_x = dev.omny_xray_gui.xval_x_0.get()
|
||||
fzp_center_y = dev.omny_xray_gui.yval_y_0.get()
|
||||
self.gui.set_crosshair_position(fzp_center_x, fzp_center_y)
|
||||
self.gui.show_crosshair()
|
||||
|
||||
self.send_message("Step 1/5: Adjust sample height and submit center")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, True)
|
||||
@@ -211,6 +225,7 @@ class XrayEyeAlign:
|
||||
self.gui.enable_submit_button(False)
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.update_fov(k)
|
||||
self.gui.hide_crosshair()
|
||||
break
|
||||
|
||||
k += 1
|
||||
@@ -298,4 +313,4 @@ class XrayEyeAlign:
|
||||
)
|
||||
self.gui.submit_fit_array(data)
|
||||
print(f"fit submited with {data}")
|
||||
# self.flomni.flomnigui_show_xeyealign_fittab()
|
||||
# self.flomni.flomnigui_show_xeyealign_fittab()
|
||||
@@ -189,7 +189,13 @@ class PtychoReconstructor:
|
||||
logger.warning("Failed to compare active account to system user.")
|
||||
return False
|
||||
|
||||
def write(self, scan_list: list, next_scan_number: int, base_path: str = "~/data/raw/analysis/"):
|
||||
def write(
|
||||
self,
|
||||
scan_list: list,
|
||||
next_scan_number: int,
|
||||
base_path: str = "~/data/raw/analysis/",
|
||||
probe_file_propagation: float | None = None,
|
||||
):
|
||||
"""Write a reconstruction queue file for the given scan list.
|
||||
|
||||
Args:
|
||||
@@ -198,6 +204,13 @@ class PtychoReconstructor:
|
||||
next_scan_number (int): The current next scan number, used to
|
||||
name the queue file.
|
||||
base_path (str): Root path under which the queue folder lives.
|
||||
probe_file_propagation (float, optional): Distance [m] by which the
|
||||
reconstruction should numerically propagate the probe, used in
|
||||
place of a physical sample z-move (e.g. when the z stage/piezo
|
||||
is unavailable). Written as `p.probe_file_propagation` if given;
|
||||
omitted entirely otherwise, matching the old reconstruction.mac
|
||||
behavior of only writing this parameter when
|
||||
progagateprobeinsteadofsample==1.
|
||||
"""
|
||||
if not self._accounts_match():
|
||||
logger.warning("Active BEC account does not match system user — skipping queue file write.")
|
||||
@@ -214,6 +227,8 @@ class PtychoReconstructor:
|
||||
with open(queue_file, "w") as f:
|
||||
scans = " ".join(str(s) for s in scan_list)
|
||||
f.write(f"p.scan_number {scans}\n")
|
||||
if probe_file_propagation is not None:
|
||||
f.write(f"p.probe_file_propagation {probe_file_propagation:.6f}\n")
|
||||
f.write("p.check_nextscan_started 1\n")
|
||||
|
||||
|
||||
|
||||
@@ -116,6 +116,63 @@ class XRayEye(RPCBase):
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def show_crosshair(self):
|
||||
"""
|
||||
Show the alignment target crosshair on the image view.
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def hide_crosshair(self):
|
||||
"""
|
||||
Hide the alignment target crosshair on the image view.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def crosshair_visible(self) -> "bool":
|
||||
"""
|
||||
Whether the alignment target crosshair is currently shown.
|
||||
"""
|
||||
|
||||
@crosshair_visible.setter
|
||||
@rpc_call
|
||||
def crosshair_visible(self) -> "bool":
|
||||
"""
|
||||
Whether the alignment target crosshair is currently shown.
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def set_crosshair_position(self, x: "float", y: "float"):
|
||||
"""
|
||||
Move the alignment target crosshair to (x, y). Does not change visibility.
|
||||
|
||||
Args:
|
||||
x(float): x position, in the same image/data coordinates as ROIs
|
||||
(see e.g. ``omny_xray_gui.xval_x_*``).
|
||||
y(float): y position, in the same image/data coordinates as ROIs
|
||||
(see e.g. ``omny_xray_gui.yval_y_*``).
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def crosshair_position(self) -> "tuple[float, float]":
|
||||
"""
|
||||
Current position of the alignment target crosshair as (x, y).
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def reset_zoom(self):
|
||||
"""
|
||||
Reset the image view to fit the current frame, discarding any manual
|
||||
zoom/pan. Intended to be called once at the start of an alignment
|
||||
routine; live view re-enabling does not reset zoom on its own (see
|
||||
on_live_view_enabled).
|
||||
"""
|
||||
|
||||
|
||||
class XRayEye2DControl(RPCBase):
|
||||
_IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_qthemes import material_icon
|
||||
@@ -31,6 +32,109 @@ logger = bec_logger.logger
|
||||
CAMERA = ("cam_xeye", "image")
|
||||
|
||||
|
||||
class TargetCrosshair:
|
||||
"""
|
||||
Fixed, RPC-positionable crosshair overlay for an image plot item.
|
||||
|
||||
This is intentionally separate from bec_widgets' built-in mouse-tracking
|
||||
``Crosshair`` (toggled from the image toolbar): that one follows the
|
||||
cursor and is purely a UI convenience. This crosshair never reacts to the
|
||||
mouse - it marks a single target position that is shown, hidden and
|
||||
moved entirely under program control (e.g. from ``xray_eye_align.py`` or
|
||||
any other BEC client via RPC), so it can be used to mark a reference
|
||||
position (such as a previously submitted alignment center) on the live
|
||||
view while the user works through subsequent steps.
|
||||
|
||||
Position is expressed in the same image/data coordinate system used by
|
||||
the widget's ROIs (see ``XRayEye.submit``), so values read back from
|
||||
``omny_xray_gui.xval_x_*`` / ``yval_y_*`` can be passed in directly.
|
||||
"""
|
||||
|
||||
def __init__(self, plot_item: pg.PlotItem):
|
||||
self.plot_item = plot_item
|
||||
pen = pg.mkPen(color="#ff2e2e", width=2, style=Qt.PenStyle.DashLine)
|
||||
self.v_line = pg.InfiniteLine(angle=90, movable=False, pen=pen)
|
||||
self.h_line = pg.InfiniteLine(angle=0, movable=False, pen=pen)
|
||||
for line in (self.v_line, self.h_line):
|
||||
line.skip_auto_range = True
|
||||
line.setVisible(False)
|
||||
self.plot_item.addItem(line, ignoreBounds=True)
|
||||
|
||||
def set_position(self, x: float, y: float):
|
||||
"""Move the crosshair to (x, y) in image/data coordinates."""
|
||||
self.v_line.setPos(x)
|
||||
self.h_line.setPos(y)
|
||||
|
||||
def position(self) -> tuple[float, float]:
|
||||
"""Current crosshair position as (x, y) in image/data coordinates."""
|
||||
return (self.v_line.value(), self.h_line.value())
|
||||
|
||||
def set_visible(self, visible: bool):
|
||||
"""Show or hide the crosshair without changing its position."""
|
||||
self.v_line.setVisible(visible)
|
||||
self.h_line.setVisible(visible)
|
||||
|
||||
def is_visible(self) -> bool:
|
||||
return self.v_line.isVisible()
|
||||
|
||||
def cleanup(self):
|
||||
self.plot_item.removeItem(self.v_line)
|
||||
self.plot_item.removeItem(self.h_line)
|
||||
|
||||
|
||||
class ImageZoomControl(QWidget):
|
||||
"""
|
||||
Discrete zoom controls for an Image widget's view.
|
||||
|
||||
Mouse-wheel zoom steps can be far too coarse to use precisely over a
|
||||
remote desktop connection, so this provides explicit zoom in/out buttons
|
||||
plus a "fit to view" button that resets pan/zoom to frame the current
|
||||
image (the same operation as ``XRayEye.reset_zoom()``).
|
||||
"""
|
||||
|
||||
# scaleBy() factor for one "zoom in" click; >1 (its inverse) zooms out.
|
||||
ZOOM_STEP_FACTOR = 0.8
|
||||
|
||||
def __init__(self, parent=None, image_widget: Image | None = None, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
self._image_widget = image_widget
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(4)
|
||||
|
||||
self.zoom_out_button = QToolButton(parent=self)
|
||||
self.zoom_out_button.setIcon(material_icon("zoom_out"))
|
||||
self.zoom_out_button.setToolTip("Zoom out")
|
||||
layout.addWidget(self.zoom_out_button)
|
||||
|
||||
self.zoom_in_button = QToolButton(parent=self)
|
||||
self.zoom_in_button.setIcon(material_icon("zoom_in"))
|
||||
self.zoom_in_button.setToolTip("Zoom in")
|
||||
layout.addWidget(self.zoom_in_button)
|
||||
|
||||
self.fit_view_button = QToolButton(parent=self)
|
||||
self.fit_view_button.setIcon(material_icon("fit_screen"))
|
||||
self.fit_view_button.setToolTip("Reset zoom/pan to fit the image")
|
||||
layout.addWidget(self.fit_view_button)
|
||||
|
||||
self.zoom_in_button.clicked.connect(lambda: self.zoom(self.ZOOM_STEP_FACTOR))
|
||||
self.zoom_out_button.clicked.connect(lambda: self.zoom(1 / self.ZOOM_STEP_FACTOR))
|
||||
self.fit_view_button.clicked.connect(self.reset_view)
|
||||
|
||||
def zoom(self, factor: float):
|
||||
"""Scale the view by `factor` around the current view center."""
|
||||
if self._image_widget is None:
|
||||
return
|
||||
self._image_widget.plot_item.vb.scaleBy((factor, factor))
|
||||
|
||||
def reset_view(self):
|
||||
"""Reset pan/zoom to fit the current image."""
|
||||
if self._image_widget is None:
|
||||
return
|
||||
self._image_widget.auto_range(True)
|
||||
|
||||
|
||||
class XRayEye2DControl(BECWidget, QWidget):
|
||||
def __init__(self, parent=None, step_size: int = 100, *arg, **kwargs):
|
||||
super().__init__(parent=parent, *arg, **kwargs)
|
||||
@@ -142,9 +246,20 @@ class XRayEye(BECWidget, QWidget):
|
||||
"switch_tab",
|
||||
"set_dap_params_forwarding",
|
||||
"submit_fit_array",
|
||||
"show_crosshair",
|
||||
"hide_crosshair",
|
||||
"crosshair_visible",
|
||||
"crosshair_visible.setter",
|
||||
"set_crosshair_position",
|
||||
"crosshair_position",
|
||||
"reset_zoom",
|
||||
]
|
||||
PLUGIN = True
|
||||
|
||||
# Styling for ROIs drawn in the (single, compact-mode) alignment view.
|
||||
ROI_LINE_COLOR = "blue"
|
||||
ROI_LINE_WIDTH = 2
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self._connected_motor = None
|
||||
@@ -157,6 +272,7 @@ class XRayEye(BECWidget, QWidget):
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
self._init_ui()
|
||||
self.target_crosshair = TargetCrosshair(self.image.plot_item)
|
||||
self._make_connections()
|
||||
|
||||
# Connection to redis endpoints
|
||||
@@ -200,7 +316,11 @@ class XRayEye(BECWidget, QWidget):
|
||||
|
||||
# ROI toolbar + Live toggle (header row)
|
||||
self.roi_manager = ROIPropertyTree(
|
||||
parent=self, image_widget=self.image, compact=True, compact_orientation="horizontal"
|
||||
parent=self,
|
||||
image_widget=self.image,
|
||||
compact=True,
|
||||
compact_orientation="horizontal",
|
||||
compact_color=self.ROI_LINE_COLOR,
|
||||
)
|
||||
header_row = QHBoxLayout()
|
||||
header_row.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -242,6 +362,12 @@ class XRayEye(BECWidget, QWidget):
|
||||
self.motor_control_2d, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignCenter
|
||||
)
|
||||
|
||||
# Zoom controls (mouse-wheel zoom steps are too coarse over remote desktop)
|
||||
self.zoom_control = ImageZoomControl(parent=self, image_widget=self.image)
|
||||
self.control_panel_layout.addWidget(
|
||||
self.zoom_control, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignCenter
|
||||
)
|
||||
|
||||
# separator
|
||||
self.control_panel_layout.addWidget(self._create_separator())
|
||||
|
||||
@@ -341,6 +467,14 @@ class XRayEye(BECWidget, QWidget):
|
||||
lambda x: self.motor_control_2d.setProperty("step_size", x)
|
||||
)
|
||||
self.submit_button.clicked.connect(self.submit)
|
||||
# ROIPropertyTree's compact_color only styles the line color; line width
|
||||
# still needs to be forced per-ROI here.
|
||||
self.roi_manager.controller.roiAdded.connect(self._style_new_roi)
|
||||
|
||||
@SafeSlot(object)
|
||||
def _style_new_roi(self, roi):
|
||||
"""Force a thinner outline on newly drawn ROIs (color is set via compact_color)."""
|
||||
roi.line_width = self.ROI_LINE_WIDTH
|
||||
|
||||
def _create_separator(self):
|
||||
sep = QFrame(parent=self)
|
||||
@@ -478,6 +612,58 @@ class XRayEye(BECWidget, QWidget):
|
||||
else:
|
||||
self.tab_widget.setCurrentIndex(0)
|
||||
|
||||
@SafeSlot()
|
||||
@rpc_timeout(20)
|
||||
def show_crosshair(self):
|
||||
"""Show the alignment target crosshair on the image view."""
|
||||
self.target_crosshair.set_visible(True)
|
||||
|
||||
@SafeSlot()
|
||||
@rpc_timeout(20)
|
||||
def hide_crosshair(self):
|
||||
"""Hide the alignment target crosshair on the image view."""
|
||||
self.target_crosshair.set_visible(False)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def crosshair_visible(self) -> bool:
|
||||
"""Whether the alignment target crosshair is currently shown."""
|
||||
return self.target_crosshair.is_visible()
|
||||
|
||||
@crosshair_visible.setter
|
||||
@rpc_timeout(20)
|
||||
def crosshair_visible(self, visible: bool):
|
||||
self.target_crosshair.set_visible(visible)
|
||||
|
||||
@SafeSlot(float, float)
|
||||
@rpc_timeout(20)
|
||||
def set_crosshair_position(self, x: float, y: float):
|
||||
"""
|
||||
Move the alignment target crosshair to (x, y). Does not change visibility.
|
||||
|
||||
Args:
|
||||
x(float): x position, in the same image/data coordinates as ROIs
|
||||
(see e.g. ``omny_xray_gui.xval_x_*``).
|
||||
y(float): y position, in the same image/data coordinates as ROIs
|
||||
(see e.g. ``omny_xray_gui.yval_y_*``).
|
||||
"""
|
||||
self.target_crosshair.set_position(x, y)
|
||||
|
||||
@SafeSlot()
|
||||
def crosshair_position(self) -> tuple[float, float]:
|
||||
"""Current position of the alignment target crosshair as (x, y)."""
|
||||
return self.target_crosshair.position()
|
||||
|
||||
@SafeSlot()
|
||||
@rpc_timeout(20)
|
||||
def reset_zoom(self):
|
||||
"""
|
||||
Reset the image view to fit the current frame, discarding any manual
|
||||
zoom/pan. Intended to be called once at the start of an alignment
|
||||
routine; live view re-enabling does not reset zoom on its own (see
|
||||
on_live_view_enabled).
|
||||
"""
|
||||
self.zoom_control.reset_view()
|
||||
|
||||
@SafeSlot()
|
||||
def get_roi_coordinates(self) -> dict | None:
|
||||
"""Get the coordinates of the currently active ROI."""
|
||||
@@ -499,6 +685,15 @@ class XRayEye(BECWidget, QWidget):
|
||||
if enabled:
|
||||
self.live_preview_toggle.checked = enabled
|
||||
self.image.image(device=CAMERA[0], signal=CAMERA[1])
|
||||
# Reconnecting the monitor schedules a one-shot view autorange on
|
||||
# the next incoming frame (bec_widgets Image._autorange_on_next_update),
|
||||
# which would silently discard any manual zoom/pan every time live
|
||||
# view is re-enabled (e.g. once per step of an alignment routine).
|
||||
# Suppress it; an explicit reset is available via reset_zoom() /
|
||||
# the "fit to view" button. Private attribute - re-check this if
|
||||
# bec_widgets' Image implementation changes.
|
||||
if hasattr(self.image, "_autorange_on_next_update"):
|
||||
self.image._autorange_on_next_update = False
|
||||
self.live_preview_toggle.blockSignals(False)
|
||||
return
|
||||
|
||||
@@ -674,6 +869,7 @@ class XRayEye(BECWidget, QWidget):
|
||||
def cleanup(self):
|
||||
"""Cleanup connections on widget close -> disconnect slots and stop live mode of camera."""
|
||||
self._queue_idle_timer.stop()
|
||||
self.target_crosshair.cleanup()
|
||||
if self._connected_motor is not None:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_tomo_angle_readback, MessageEndpoints.device_readback(self._connected_motor)
|
||||
@@ -707,4 +903,4 @@ if __name__ == "__main__":
|
||||
|
||||
win.resize(1000, 800)
|
||||
win.show()
|
||||
sys.exit(app.exec_())
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,17 +1,6 @@
|
||||
# eiger_1_5:
|
||||
# description: Eiger 1.5M in-vacuum detector
|
||||
# deviceClass: csaxs_bec.devices.jungfraujoch.eiger_1_5m.Eiger1_5M
|
||||
# deviceConfig:
|
||||
# detector_distance: 100
|
||||
# beam_center: [0, 0]
|
||||
# onFailure: raise
|
||||
# enabled: True
|
||||
# readoutPriority: async
|
||||
# softwareTrigger: False
|
||||
|
||||
eiger_9:
|
||||
description: Eiger 9M detector
|
||||
deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M
|
||||
eiger_1_5:
|
||||
description: Eiger 1.5M in-vacuum detector
|
||||
deviceClass: csaxs_bec.devices.jungfraujoch.eiger_1_5m.Eiger1_5M
|
||||
deviceConfig:
|
||||
detector_distance: 2150
|
||||
beam_center: [860, 1219]
|
||||
@@ -20,6 +9,17 @@ eiger_9:
|
||||
readoutPriority: async
|
||||
softwareTrigger: False
|
||||
|
||||
# eiger_9:
|
||||
# description: Eiger 9M detector
|
||||
# deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M
|
||||
# deviceConfig:
|
||||
# detector_distance: 2200
|
||||
# beam_center: [870, 1203]
|
||||
# onFailure: raise
|
||||
# enabled: True
|
||||
# readoutPriority: async
|
||||
# softwareTrigger: False
|
||||
|
||||
# ids_cam:
|
||||
# description: IDS camera for live image acquisition
|
||||
# deviceClass: csaxs_bec.devices.ids_cameras.IDSCamera
|
||||
|
||||
@@ -855,6 +855,9 @@ ebsupport:
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
userParameter:
|
||||
one_reflection: -93.0
|
||||
two_reflections: 10.9787
|
||||
|
||||
fttrx1:
|
||||
description: FTS1 translation X
|
||||
@@ -1039,7 +1042,7 @@ gain_bpm_xbox2:
|
||||
gain_lsb: galilrioesxbox.digital_out.ch0 # Pin 10 -> Galil ch0
|
||||
gain_mid: galilrioesxbox.digital_out.ch1 # Pin 11 -> Galil ch1
|
||||
gain_msb: galilrioesxbox.digital_out.ch2 # Pin 12 -> Galil ch2
|
||||
coupling: galilrioesxbox.digital_out.ch3 # Pin 13 -> Galil ch3
|
||||
coupling_ref: galilrioesxbox.digital_out.ch3 # Pin 13 -> Galil ch3
|
||||
speed_mode: galilrioesxbox.digital_out.ch4 # Pin 14 -> Galil ch4
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
@@ -1051,10 +1054,10 @@ bpm_xbox2_slowrb:
|
||||
description: BPM Xbox 2 (First Xbox in ES hutch) readback
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.bpm.BPM
|
||||
deviceConfig:
|
||||
left_top: galilrioesxbox.analog_in.ch0
|
||||
right_top: galilrioesxbox.analog_in.ch1
|
||||
right_bot: galilrioesxbox.analog_in.ch2
|
||||
left_bot: galilrioesxbox.analog_in.ch3
|
||||
left_top_ref: galilrioesxbox.analog_in.ch0
|
||||
right_top_ref: galilrioesxbox.analog_in.ch1
|
||||
right_bot_ref: galilrioesxbox.analog_in.ch2
|
||||
left_bot_ref: galilrioesxbox.analog_in.ch3
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
onFailure: retry
|
||||
@@ -1068,7 +1071,7 @@ gain_bim_xbox3:
|
||||
gain_lsb: galilrioesxbox.digital_out.ch6 # Pin 10 -> Galil ch0
|
||||
gain_mid: galilrioesxbox.digital_out.ch7 # Pin 11 -> Galil ch1
|
||||
gain_msb: galilrioesxbox.digital_out.ch8 # Pin 12 -> Galil ch2
|
||||
coupling: galilrioesxbox.digital_out.ch9 # Pin 13 -> Galil ch3
|
||||
coupling_ref: galilrioesxbox.digital_out.ch9 # Pin 13 -> Galil ch3
|
||||
speed_mode: galilrioesxbox.digital_out.ch10 # Pin 14 -> Galil ch4
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
@@ -1080,7 +1083,7 @@ bim_xbox3_slowrb:
|
||||
description: Beam intensity slow readback ES XBox3
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder
|
||||
deviceConfig:
|
||||
signal: galilrioesxbox.analog_in.ch6
|
||||
signal_ref: galilrioesxbox.analog_in.ch6
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
onFailure: retry
|
||||
@@ -1112,7 +1115,7 @@ gain_beamstop_diode:
|
||||
gain_lsb: galilrioesft.digital_out.ch0 # Pin 10 -> Galil ch0
|
||||
gain_mid: galilrioesft.digital_out.ch1 # Pin 11 -> Galil ch1
|
||||
gain_msb: galilrioesft.digital_out.ch2 # Pin 12 -> Galil ch2
|
||||
coupling: galilrioesft.digital_out.ch3 # Pin 13 -> Galil ch3
|
||||
coupling_ref: galilrioesft.digital_out.ch3 # Pin 13 -> Galil ch3
|
||||
speed_mode: galilrioesft.digital_out.ch4 # Pin 14 -> Galil ch4
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
@@ -1124,7 +1127,7 @@ beamstop_intensity:
|
||||
description: Beamstop intensity from Galil analog input ch6
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder
|
||||
deviceConfig:
|
||||
signal: galilrioesft.analog_in.ch0
|
||||
signal_ref: galilrioesft.analog_in.ch0
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
onFailure: retry
|
||||
|
||||
@@ -203,10 +203,10 @@ bpm1:
|
||||
description: 'XBPM1 (frontend)'
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.bpm.BPM
|
||||
deviceConfig:
|
||||
left_top: xbpm1c1
|
||||
right_top: xbpm1c2
|
||||
right_bot: xbpm1c3
|
||||
left_bot: xbpm1c4
|
||||
left_top_ref: xbpm1c1
|
||||
right_top_ref: xbpm1c2
|
||||
right_bot_ref: xbpm1c3
|
||||
left_bot_ref: xbpm1c4
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
|
||||
@@ -201,48 +201,48 @@ ccm_energy:
|
||||
######################## SMARACT STAGES ##################################
|
||||
##########################################################################
|
||||
|
||||
# xbpm2x:
|
||||
# description: X-ray beam position monitor 1 in OPbox
|
||||
# deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
# deviceConfig:
|
||||
# axis_Id: A
|
||||
# host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
# limits:
|
||||
# - -200
|
||||
# - 200
|
||||
# port: 5000
|
||||
# sign: 1
|
||||
# enabled: true
|
||||
# onFailure: retry
|
||||
# readOnly: false
|
||||
# readoutPriority: baseline
|
||||
# connectionTimeout: 20
|
||||
# userParameter:
|
||||
# init_position: 22.5
|
||||
# in_position: -1.5
|
||||
# # bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
# bl_smar_stage: 0
|
||||
xbpm2x:
|
||||
description: X-ray beam position monitor 1 in OPbox
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: retry
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
init_position: 22.5
|
||||
in_position: -1.5
|
||||
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
bl_smar_stage: 0
|
||||
|
||||
# xbpm2y:
|
||||
# description: X-ray beam position monitor 1 in OPbox
|
||||
# deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
# deviceConfig:
|
||||
# axis_Id: B
|
||||
# host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
# limits:
|
||||
# - -200
|
||||
# - 200
|
||||
# port: 5000
|
||||
# sign: 1
|
||||
# enabled: true
|
||||
# onFailure: retry
|
||||
# readOnly: false
|
||||
# readoutPriority: baseline
|
||||
# connectionTimeout: 20
|
||||
# userParameter:
|
||||
# in_position: -1.0
|
||||
# # bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
# bl_smar_stage: 1
|
||||
xbpm2y:
|
||||
description: X-ray beam position monitor 1 in OPbox
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: retry
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in_position: -1.0
|
||||
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
bl_smar_stage: 1
|
||||
|
||||
scinx:
|
||||
description: scintillator in OPbox
|
||||
@@ -266,47 +266,47 @@ scinx:
|
||||
bl_smar_stage: 2
|
||||
in_position: -12.5
|
||||
|
||||
poly:
|
||||
description: polarizer holder in OPbox
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: retry
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
init_position: -23
|
||||
bl_smar_stage: 1
|
||||
# poly:
|
||||
# description: polarizer holder in OPbox
|
||||
# deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
# deviceConfig:
|
||||
# axis_Id: B
|
||||
# host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
# limits:
|
||||
# - -200
|
||||
# - 200
|
||||
# port: 5000
|
||||
# sign: 1
|
||||
# enabled: true
|
||||
# onFailure: retry
|
||||
# readOnly: false
|
||||
# readoutPriority: baseline
|
||||
# connectionTimeout: 20
|
||||
# userParameter:
|
||||
# # bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
# init_position: -23
|
||||
# bl_smar_stage: 1
|
||||
|
||||
polrot:
|
||||
description: rotation of crytal of the polarizer
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: retry
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in_position: -1.0
|
||||
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
bl_smar_stage: 0
|
||||
# polrot:
|
||||
# description: rotation of crytal of the polarizer
|
||||
# deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
# deviceConfig:
|
||||
# axis_Id: A
|
||||
# host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
# limits:
|
||||
# - -200
|
||||
# - 200
|
||||
# port: 5000
|
||||
# sign: 1
|
||||
# enabled: true
|
||||
# onFailure: retry
|
||||
# readOnly: false
|
||||
# readoutPriority: baseline
|
||||
# connectionTimeout: 20
|
||||
# userParameter:
|
||||
# in_position: -1.0
|
||||
# # bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
# bl_smar_stage: 0
|
||||
|
||||
# dmm1_trx_readback_example: # This is the same template as for i.e. bpm4i
|
||||
# description: 'This is an example of a read-only Epics signal'
|
||||
@@ -356,7 +356,7 @@ galilrioop:
|
||||
# gain_lsb: galilrioop.digital_out.ch0 # Pin 10 -> Galil ch0
|
||||
# gain_mid: galilrioop.digital_out.ch1 # Pin 11 -> Galil ch1
|
||||
# gain_msb: galilrioop.digital_out.ch2 # Pin 12 -> Galil ch2
|
||||
# coupling: galilrioop.digital_out.ch3 # Pin 13 -> Galil ch3
|
||||
# coupling_ref: galilrioop.digital_out.ch3 # Pin 13 -> Galil ch3
|
||||
# speed_mode: galilrioop.digital_out.ch4 # Pin 14 -> Galil ch4
|
||||
# enabled: true
|
||||
# readoutPriority: monitored
|
||||
@@ -368,10 +368,10 @@ galilrioop:
|
||||
# description: BPM Xbox 1 (OP hutch) readback
|
||||
# deviceClass: csaxs_bec.devices.pseudo_devices.bpm.BPM
|
||||
# deviceConfig:
|
||||
# left_top: galilrioop.analog_in.ch0
|
||||
# right_top: galilrioop.analog_in.ch1
|
||||
# right_bot: galilrioop.analog_in.ch2
|
||||
# left_bot: galilrioop.analog_in.ch3
|
||||
# left_top_ref: galilrioop.analog_in.ch0
|
||||
# right_top_ref: galilrioop.analog_in.ch1
|
||||
# right_bot_ref: galilrioop.analog_in.ch2
|
||||
# left_bot_ref: galilrioop.analog_in.ch3
|
||||
# enabled: true
|
||||
# readoutPriority: monitored
|
||||
# onFailure: retry
|
||||
@@ -385,7 +385,7 @@ gain_diodes_xbox1:
|
||||
gain_lsb: galilrioop.digital_out.ch6 # Pin 10 -> Galil ch0
|
||||
gain_mid: galilrioop.digital_out.ch7 # Pin 11 -> Galil ch1
|
||||
gain_msb: galilrioop.digital_out.ch8 # Pin 12 -> Galil ch2
|
||||
coupling: galilrioop.digital_out.ch9 # Pin 13 -> Galil ch3
|
||||
coupling_ref: galilrioop.digital_out.ch9 # Pin 13 -> Galil ch3
|
||||
speed_mode: galilrioop.digital_out.ch10 # Pin 14 -> Galil ch4
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
@@ -397,7 +397,7 @@ diode_horizontal_xbox1_slowrb:
|
||||
description: Slow readback diode horizontal XBox OP (polarization diagnostics)
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder
|
||||
deviceConfig:
|
||||
signal: galilrioop.analog_in.ch6
|
||||
signal_ref: galilrioop.analog_in.ch6
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
onFailure: retry
|
||||
@@ -408,7 +408,7 @@ diode_vertical_xbox1_slowrb:
|
||||
description: Slow readback diode vertical XBox OP (polarization diagnostics)
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.signal_forwarder.SignalForwarder
|
||||
deviceConfig:
|
||||
signal: galilrioop.analog_in.ch7
|
||||
signal_ref: galilrioop.analog_in.ch7
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
onFailure: retry
|
||||
@@ -750,6 +750,9 @@ kbhtry:
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- optics
|
||||
userParameter:
|
||||
one_reflection: -5.0
|
||||
two_reflections: -0.95
|
||||
|
||||
kbhyaw:
|
||||
description: "KB Horizontal Focusing Mirror, yaw"
|
||||
|
||||
@@ -19,8 +19,8 @@ detectors:
|
||||
#sastt:
|
||||
# - !include ./sastt.yaml
|
||||
|
||||
# flomni:
|
||||
# - !include ./ptycho_flomni.yaml
|
||||
flomni:
|
||||
- !include ./ptycho_flomni.yaml
|
||||
|
||||
#omny:
|
||||
# - !include ./ptycho_omny.yaml
|
||||
@@ -28,7 +28,7 @@ detectors:
|
||||
#lamni:
|
||||
# - !include ./ptycho_lamni.yaml
|
||||
|
||||
user setup:
|
||||
- !include ./user_setup.yaml
|
||||
# user setup:
|
||||
# - !include ./user_setup.yaml
|
||||
|
||||
|
||||
|
||||
@@ -19,8 +19,11 @@ feyex:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -16.267
|
||||
in: -16.453
|
||||
out: -1
|
||||
fttrx_in: 2.3
|
||||
fttrx_out: -24
|
||||
|
||||
feyey:
|
||||
description: Xray eye Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -38,7 +41,7 @@ feyey:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -10.467
|
||||
in: -10.09
|
||||
fheater:
|
||||
description: Heater Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -72,7 +75,8 @@ foptx:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -13.761
|
||||
#170 micron, 60 nm
|
||||
in: -13.831
|
||||
fopty:
|
||||
description: Optics Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -90,8 +94,9 @@ fopty:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0.552
|
||||
out: 0.752
|
||||
#170 micron, 60 nm
|
||||
in: 0.42
|
||||
out: 0.57
|
||||
foptz:
|
||||
description: Optics Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -152,7 +157,7 @@ fsamy:
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 2
|
||||
- 3.1
|
||||
- 3.3
|
||||
port: 8081
|
||||
sign: 1
|
||||
enabled: true
|
||||
@@ -161,7 +166,7 @@ fsamy:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 2.75
|
||||
in: 3
|
||||
ftracky:
|
||||
description: Laser Tracker coarse Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -227,7 +232,7 @@ ftransy:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
sensor_voltage: -1.1
|
||||
sensor_voltage: -1.6
|
||||
ftransz:
|
||||
description: Sample transer Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -296,8 +301,12 @@ fosax:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 9.124
|
||||
out: 5.3
|
||||
#170 micron, 60 nm, 7.6 kev
|
||||
#in: 8.7568
|
||||
#out: 5.1
|
||||
#170 micron, 60 nm, 7.9 kev
|
||||
in: 8.731922
|
||||
out: 5.1
|
||||
fosay:
|
||||
description: OSA Y
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
@@ -315,7 +324,11 @@ fosay:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 0.367
|
||||
#170 micron, 60 nm, 7.6 kev
|
||||
#in: -0.0235
|
||||
#170 micron, 60 nm, 7.6 kev
|
||||
in: -0.0422
|
||||
|
||||
fosaz:
|
||||
description: OSA Z
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
@@ -333,7 +346,11 @@ fosaz:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 8.5
|
||||
#170 micron, 60 nm, 7.6 kev
|
||||
#in: 8.5
|
||||
#out: 6
|
||||
#170 micron, 60 nm, 7.9 kev, foptz 15.9
|
||||
in: 11.9
|
||||
out: 6
|
||||
|
||||
############################################################
|
||||
@@ -439,8 +456,8 @@ cam_xeye:
|
||||
deviceConfig:
|
||||
camera_id: 11
|
||||
bits_per_pixel: 24
|
||||
num_rotation_90: 3
|
||||
transpose: false
|
||||
num_rotation_90: 0
|
||||
transpose: true
|
||||
force_monochrome: true
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
@@ -511,33 +528,33 @@ calculated_signal:
|
||||
############################################################
|
||||
#################### OMNY Pandabox #########################
|
||||
############################################################
|
||||
# omny_panda:
|
||||
# readoutPriority: async
|
||||
# deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY
|
||||
# deviceConfig:
|
||||
# host: omny-panda.psi.ch
|
||||
# signal_alias:
|
||||
# FMC_IN.VAL1.Min: cap_voltage_fzp_y_min
|
||||
# FMC_IN.VAL1.Max: cap_voltage_fzp_y_max
|
||||
# FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean
|
||||
# FMC_IN.VAL2.Min: cap_voltage_fzp_x_min
|
||||
# FMC_IN.VAL2.Max: cap_voltage_fzp_x_max
|
||||
# FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean
|
||||
# INENC1.VAL.Max: interf_st_fzp_y_max
|
||||
# INENC1.VAL.Mean: interf_st_fzp_y_mean
|
||||
# INENC1.VAL.Min: interf_st_fzp_y_min
|
||||
# INENC2.VAL.Max: interf_st_fzp_x_max
|
||||
# INENC2.VAL.Mean: interf_st_fzp_x_mean
|
||||
# INENC2.VAL.Min: interf_st_fzp_x_min
|
||||
# INENC3.VAL.Max: interf_st_rotz_max
|
||||
# INENC3.VAL.Mean: interf_st_rotz_mean
|
||||
# INENC3.VAL.Min: interf_st_rotz_min
|
||||
# INENC4.VAL.Max: interf_st_rotx_max
|
||||
# INENC4.VAL.Mean: interf_st_rotx_mean
|
||||
# INENC4.VAL.Min: interf_st_rotx_min
|
||||
# PCAP.GATE_DURATION.Value: pcap_gate_duration_value
|
||||
# deviceTags:
|
||||
# - detector
|
||||
# enabled: true
|
||||
# readOnly: false
|
||||
# softwareTrigger: false
|
||||
omny_panda:
|
||||
readoutPriority: async
|
||||
deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY
|
||||
deviceConfig:
|
||||
host: omny-panda.psi.ch
|
||||
signal_alias:
|
||||
FMC_IN.VAL1.Min: cap_voltage_fzp_y_min
|
||||
FMC_IN.VAL1.Max: cap_voltage_fzp_y_max
|
||||
FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean
|
||||
FMC_IN.VAL2.Min: cap_voltage_fzp_x_min
|
||||
FMC_IN.VAL2.Max: cap_voltage_fzp_x_max
|
||||
FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean
|
||||
INENC1.VAL.Max: interf_st_fzp_y_max
|
||||
INENC1.VAL.Mean: interf_st_fzp_y_mean
|
||||
INENC1.VAL.Min: interf_st_fzp_y_min
|
||||
INENC2.VAL.Max: interf_st_fzp_x_max
|
||||
INENC2.VAL.Mean: interf_st_fzp_x_mean
|
||||
INENC2.VAL.Min: interf_st_fzp_x_min
|
||||
INENC3.VAL.Max: interf_st_rotz_max
|
||||
INENC3.VAL.Mean: interf_st_rotz_mean
|
||||
INENC3.VAL.Min: interf_st_rotz_min
|
||||
INENC4.VAL.Max: interf_st_rotx_max
|
||||
INENC4.VAL.Mean: interf_st_rotx_mean
|
||||
INENC4.VAL.Min: interf_st_rotx_min
|
||||
PCAP.GATE_DURATION.Value: pcap_gate_duration_value
|
||||
deviceTags:
|
||||
- detector
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
@@ -12,10 +12,10 @@ bpm1:
|
||||
readoutPriority: baseline
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.bpm.BPM
|
||||
deviceConfig:
|
||||
blade_t: galilrioesxbox.analog_in.ch0
|
||||
blade_r: galilrioesxbox.analog_in.ch1
|
||||
blade_b: galilrioesxbox.analog_in.ch2
|
||||
blade_l: galilrioesxbox.analog_in.ch3
|
||||
left_top_ref: galilrioesxbox.analog_in.ch0
|
||||
right_top_ref: galilrioesxbox.analog_in.ch1
|
||||
right_bot_ref: galilrioesxbox.analog_in.ch2
|
||||
left_bot_ref: galilrioesxbox.analog_in.ch3
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: true
|
||||
|
||||
@@ -454,33 +454,36 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
NOTE! This logic currently works for any step scan, but has to be extended for fly scans.
|
||||
"""
|
||||
while not self._scan_done_thread_kill_event.is_set():
|
||||
while self._start_monitor_async_data_emission.wait():
|
||||
try:
|
||||
if (
|
||||
hasattr(self.scan_parameters, "num_points")
|
||||
and self.scan_parameters.num_points is not None
|
||||
):
|
||||
if self.scan_parameters.scan_type == "software_triggered":
|
||||
logger.info(
|
||||
f"Software triggered scan: {self._current_data_index}/{self.scan_parameters.num_points} points received."
|
||||
)
|
||||
if self._current_data_index == self.scan_parameters.num_points:
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=None)
|
||||
else:
|
||||
if self._current_data_index >= 1:
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=None)
|
||||
|
||||
time.sleep(0.02) # 20ms delay to avoid busy loop
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
content = traceback.format_exc()
|
||||
logger.error(
|
||||
f"Exception in monitoring thread of complete for {self.name}:\n{content}"
|
||||
"Running callbacks to avoid deadlock."
|
||||
)
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=exc)
|
||||
# 20ms delay to avoid busy loop
|
||||
if not self._start_monitor_async_data_emission.wait(timeout=0.02):
|
||||
continue # Wait for the event to be set before checking data emission
|
||||
try:
|
||||
if (
|
||||
hasattr(self.scan_parameters, "num_points")
|
||||
and self.scan_parameters.num_points is not None
|
||||
):
|
||||
if self.scan_parameters.scan_type == "software_triggered":
|
||||
logger.info(
|
||||
f"Software triggered scan: {self._current_data_index}/{self.scan_parameters.num_points} points received."
|
||||
)
|
||||
if self._current_data_index == self.scan_parameters.num_points:
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=None)
|
||||
else:
|
||||
if self._current_data_index >= 1:
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=None)
|
||||
# 20ms delay to avoid busy loop
|
||||
if self._scan_done_thread_kill_event.wait(timeout=0.02):
|
||||
continue
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
content = traceback.format_exc()
|
||||
logger.error(
|
||||
f"Exception in monitoring thread of complete for {self.name}:\n{content}"
|
||||
"Running callbacks to avoid deadlock."
|
||||
)
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=exc)
|
||||
|
||||
def _status_callback(self, status: StatusBase, exception=None) -> None:
|
||||
"""Callback for status completion."""
|
||||
@@ -580,6 +583,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Hook called when the device is stopped. In addition, any status that is registered through cancel_on_stop will be cancelled here."""
|
||||
self._start_monitor_async_data_emission.clear()
|
||||
with suppress_mca_callbacks(self):
|
||||
self.stop_all.put(1)
|
||||
self.erase_all.put(1)
|
||||
|
||||
@@ -274,16 +274,14 @@ class Eiger(PSIDeviceBase):
|
||||
start_time = time.time()
|
||||
self.scan_parameters = fetch_scan_info(self.scan_info)
|
||||
|
||||
# TODO: Check mono energy from device in BEC
|
||||
# Setting incident energy in keV
|
||||
|
||||
# TODO Reactivate
|
||||
try:
|
||||
incident_energy = self._get_beam_energy(self.device_manager)
|
||||
if self._incident_energy is None:
|
||||
self._incident_energy = round(float(incident_energy), 3)
|
||||
elif not np.isclose(
|
||||
self._incident_energy, incident_energy, atol=0.01
|
||||
): # 10 keV tolerance
|
||||
self._incident_energy, incident_energy, atol=0.1
|
||||
): # 0.1 keV tolerance
|
||||
logger.warning(
|
||||
f"Incident energy changed from {self._incident_energy} keV to {incident_energy} keV for device {self.name}. "
|
||||
)
|
||||
@@ -328,7 +326,7 @@ class Eiger(PSIDeviceBase):
|
||||
beam_x_pxl=int(self._beam_center[0]),
|
||||
beam_y_pxl=int(self._beam_center[1]),
|
||||
detector_distance_mm=self.detector_distance,
|
||||
incident_energy_ke_v=incident_energy,
|
||||
incident_energy_ke_v=self._incident_energy,
|
||||
)
|
||||
logger.debug(f"Setting data_settings: {yaml.dump(data_settings.to_dict(), indent=4)}")
|
||||
prep_time = time.time()
|
||||
@@ -388,12 +386,12 @@ class Eiger(PSIDeviceBase):
|
||||
f"JungfrauJoch broker status: {yaml.dump(broker_status.to_dict(), indent=4)}"
|
||||
)
|
||||
if broker_status.message_severity == "error": # Raise on error
|
||||
# raise EigerError(
|
||||
# f"Device {self.name} acquisition completed with error status from JungfrauJoch broker: {yaml.dump(broker_status.to_dict(), indent=4)}"
|
||||
# )
|
||||
logger.warning(
|
||||
raise EigerError(
|
||||
f"Device {self.name} acquisition completed with error status from JungfrauJoch broker: {yaml.dump(broker_status.to_dict(), indent=4)}"
|
||||
)
|
||||
# logger.warning(
|
||||
# f"Device {self.name} acquisition completed with error status from JungfrauJoch broker: {yaml.dump(broker_status.to_dict(), indent=4)}"
|
||||
# )
|
||||
# Call API endpoint to get statistics
|
||||
statistics: MeasurementStatistics = (
|
||||
self.jfj_client.api.statistics_data_collection_get(_request_timeout=5)
|
||||
@@ -450,9 +448,6 @@ class Eiger(PSIDeviceBase):
|
||||
Returns:
|
||||
float: The beam energy in keV.
|
||||
"""
|
||||
if hasattr(device_manager, "devices") and hasattr(device_manager.devices, "ccm_energy"):
|
||||
energy = device_manager.devices.ccm_energy.read()[
|
||||
device_manager.devices.ccm_energy.name
|
||||
]["value"]
|
||||
|
||||
return energy
|
||||
return device_manager.devices.ccm_energy.read()[device_manager.devices.ccm_energy.name][
|
||||
"value"
|
||||
]
|
||||
|
||||
@@ -333,7 +333,11 @@ class GalilController(Controller):
|
||||
|
||||
def galil_show_all(self) -> None:
|
||||
for controller in self._controller_instances.values():
|
||||
if isinstance(controller, GalilController):
|
||||
if any(
|
||||
GalilController.__name__ == entry.__name__.split(".")[-1]
|
||||
for entry in controller.__class__.mro()
|
||||
):
|
||||
# if isinstance(controller, GalilController):
|
||||
controller.describe()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -72,10 +72,10 @@ class BPM(PSIPseudoDeviceBase):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
left_top: str,
|
||||
right_top: str,
|
||||
right_bot: str,
|
||||
left_bot: str,
|
||||
left_top_ref: str,
|
||||
right_top_ref: str,
|
||||
right_bot_ref: str,
|
||||
left_bot_ref: str,
|
||||
device_manager=None,
|
||||
scan_info=None,
|
||||
**kwargs,
|
||||
@@ -83,16 +83,16 @@ class BPM(PSIPseudoDeviceBase):
|
||||
super().__init__(name=name, device_manager=device_manager, scan_info=scan_info, **kwargs)
|
||||
# Get all blade signal objects from utility method
|
||||
signal_t = self.left_top.get_device_object_from_bec(
|
||||
object_name=left_top, signal_name=self.name, device_manager=device_manager
|
||||
object_name=left_top_ref, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
signal_r = self.right_top.get_device_object_from_bec(
|
||||
object_name=right_top, signal_name=self.name, device_manager=device_manager
|
||||
object_name=right_top_ref, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
signal_b = self.right_bot.get_device_object_from_bec(
|
||||
object_name=right_bot, signal_name=self.name, device_manager=device_manager
|
||||
object_name=right_bot_ref, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
signal_l = self.left_bot.get_device_object_from_bec(
|
||||
object_name=left_bot, signal_name=self.name, device_manager=device_manager
|
||||
object_name=left_bot_ref, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
|
||||
# Set compute methods for blade signals and virtual signals
|
||||
|
||||
@@ -50,7 +50,7 @@ class BPMControl(PSIPseudoDeviceBase):
|
||||
"""
|
||||
BPM amplifier control pseudo device. It is responsible for controlling the
|
||||
gain and coupling for the BPM amplifier. It relies on signals from a device
|
||||
in BEC to be available. For cSAXS, these are most liikely to be from the
|
||||
in BEC to be available. For cSAXS, these are most likely to be from the
|
||||
GalilRIO device that controls the BPM amplifier.
|
||||
|
||||
Args:
|
||||
@@ -97,7 +97,7 @@ class BPMControl(PSIPseudoDeviceBase):
|
||||
gain_lsb: str,
|
||||
gain_mid: str,
|
||||
gain_msb: str,
|
||||
coupling: str,
|
||||
coupling_ref: str,
|
||||
speed_mode: str,
|
||||
device_manager: DeviceManagerDS | None = None,
|
||||
scan_info: ScanInfo | None = None,
|
||||
@@ -116,7 +116,7 @@ class BPMControl(PSIPseudoDeviceBase):
|
||||
object_name=gain_msb, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
self._coupling = self.gain.get_device_object_from_bec(
|
||||
object_name=coupling, signal_name=self.name, device_manager=device_manager
|
||||
object_name=coupling_ref, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
self._speed_mode = self.gain.get_device_object_from_bec(
|
||||
object_name=speed_mode, signal_name=self.name, device_manager=device_manager
|
||||
|
||||
@@ -20,11 +20,11 @@ class SignalForwarder(PSIPseudoDeviceBase):
|
||||
doc="Forwarded signal",
|
||||
)
|
||||
|
||||
def __init__(self, name, signal: str, device_manager=None, scan_info=None, **kwargs):
|
||||
def __init__(self, name, signal_ref: str, device_manager=None, scan_info=None, **kwargs):
|
||||
super().__init__(name=name, device_manager=device_manager, scan_info=scan_info, **kwargs)
|
||||
|
||||
src = self.signal.get_device_object_from_bec(
|
||||
object_name=signal, signal_name=self.name, device_manager=device_manager
|
||||
object_name=signal_ref, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
|
||||
self.signal.set_compute_method(self._compute_signal, signal=src)
|
||||
|
||||
@@ -62,7 +62,7 @@ _To bypass the fine alignment: `flomni.feye_out`_
|
||||
|
||||
|
||||
1. `flomni.tomo_parameters()` Adjust the ptychographic scan parameters for performing an alignment scan. Typically FOVX = FOVX(Xrayeye)+20 mu, shell step = beamsize/2.5, number of projections and tomo mode are ignored in the alignment scans.
|
||||
1. `flomni.tomo_alignment_scan()` perform the alignment scan. When the first scan is running, switch to a matlab session and run `SPEC_ptycho_align` again. Click left and right. The third click can define the height of the scan, but is not needed and ignored by default. The widest horizontal field of view will be printed at the end of the matlab session.
|
||||
1. `flomni.tomo_alignment_scan()` perform the alignment scan. When the first scan is running, switch to a matlab session and run `BEC_ptycho_align` again. Click left and right. The third click can define the height of the scan, but is not needed and ignored by default. The widest horizontal field of view will be printed at the end of the matlab session.
|
||||
1. `flomni.read_alignment_offset()` Load alignment parameters calculated in matlab.
|
||||
|
||||
### Tomographic Measurement
|
||||
|
||||
@@ -43,7 +43,7 @@ def bpm_control(patched_dm):
|
||||
"gain_lsb": "gain_lsb",
|
||||
"gain_mid": "gain_mid",
|
||||
"gain_msb": "gain_msb",
|
||||
"coupling": "coupling",
|
||||
"coupling_ref": "coupling",
|
||||
"speed_mode": "speed_mode",
|
||||
},
|
||||
needs=["gain_lsb", "gain_mid", "gain_msb", "coupling", "speed_mode"],
|
||||
@@ -55,7 +55,7 @@ def bpm_control(patched_dm):
|
||||
gain_lsb="gain_lsb",
|
||||
gain_mid="gain_mid",
|
||||
gain_msb="gain_msb",
|
||||
coupling="coupling",
|
||||
coupling_ref="coupling",
|
||||
speed_mode="speed_mode",
|
||||
device_manager=patched_dm,
|
||||
)
|
||||
@@ -150,10 +150,10 @@ def bpm(patched_dm_bpm):
|
||||
enabled=True,
|
||||
readoutPriority="baseline",
|
||||
deviceConfig={
|
||||
"left_top": "left_top",
|
||||
"right_top": "right_top",
|
||||
"right_bot": "right_bot",
|
||||
"left_bot": "left_bot",
|
||||
"left_top_ref": "left_top",
|
||||
"right_top_ref": "right_top",
|
||||
"right_bot_ref": "right_bot",
|
||||
"left_bot_ref": "left_bot",
|
||||
},
|
||||
needs=["left_top", "right_top", "right_bot", "left_bot"],
|
||||
)
|
||||
@@ -161,10 +161,10 @@ def bpm(patched_dm_bpm):
|
||||
try:
|
||||
bpm = BPM(
|
||||
name=name,
|
||||
left_top="left_top",
|
||||
right_top="right_top",
|
||||
right_bot="right_bot",
|
||||
left_bot="left_bot",
|
||||
left_top_ref="left_top",
|
||||
right_top_ref="right_top",
|
||||
right_bot_ref="right_bot",
|
||||
left_bot_ref="left_bot",
|
||||
device_manager=patched_dm_bpm,
|
||||
)
|
||||
patched_dm_bpm.devices._add_device(bpm.name, bpm)
|
||||
|
||||
Reference in New Issue
Block a user