Works with BEC v3

This commit is contained in:
gac-x05la
2024-11-27 17:18:16 +01:00
committed by mohacsi_i
parent f1445fdc23
commit 7dfa47077b
7 changed files with 102 additions and 84 deletions

View File

@@ -71,7 +71,7 @@ es1_ismc:
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
readoutPriority: monitored
softwareTrigger: false
es1_tasks:
@@ -157,7 +157,7 @@ gfdaq:
onFailure: buffer
readOnly: false
readoutPriority: monitored
softwareTrigger: true
softwareTrigger: false
daq_stream0:
description: stdDAQ preview (2 every 555)

View File

@@ -20,6 +20,10 @@ logger = bec_logger.logger
class AerotechDriveDataCollectionMixin(CustomDeviceMixin):
"""Configuration and staging
NOTE: scripted scans start drive data collection internally
"""
# parent : aa1Tasks
def on_stage(self) -> None:
"""Configuration and staging"""
@@ -48,10 +52,10 @@ class AerotechDriveDataCollectionMixin(CustomDeviceMixin):
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
self.parent.configure(d=d)
# Stage the DDC distance module
# Stage the data collection if not in internally launced mode
# NOTE: Scripted scans start acquiring from the scrits
if self.parent.scaninfo.scan_type not in ("script", "scripted"):
self.parent._switch.set("Start", settle_time=0.2).wait()
self.parent.bluestage()
def on_unstage(self):
"""Standard bluesky unstage"""
@@ -127,6 +131,10 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
new = self.read_configuration()
return (old, new)
def bluestage(self) -> None:
"""Bluesky-style stage"""
self._switch.set("Start", settle_time=0.2).wait()
def reset(self):
"""Reset incremental readback"""
self._switch.set("ResetRB", settle_time=0.1).wait()

View File

@@ -21,11 +21,8 @@ class AerotechPsoDistanceMixin(CustomDeviceMixin):
when not in use. I.e. this method is not expected to be called when
PSO is not needed or when it'd conflict with other devices.
"""
# logger.warning(self.parent.scaninfo.scan_msg.info['kwargs'].keys())
# Fish out configuration from scaninfo
# NOTE: Scans don't have to fully configure the device, but it is expected that it's
# Fish out configuration from scaninfo (does not need to be full configuration)
scanparam = self.parent.scaninfo.scan_msg.info
alias = self.parent.parent.name if self.parent.parent is not None else self.parent.name
logger.warning(f"[{alias}] Scan parameters:\n{scanparam}")
@@ -49,14 +46,7 @@ class AerotechPsoDistanceMixin(CustomDeviceMixin):
self.parent.configure(d=d)
# Stage the PSO distance module
if isinstance(self.parent._distance_value, (np.ndarray, list, tuple)):
self.dstArrayRearm.set(1).wait()
# Wait for polling
sleep(0.5)
# Start monitoring the counters if distance is valid
if self.parent.dstDistanceVal.get() > 0:
self.parent.dstEventsEna.set("On").wait()
self.parent.dstCounterEna.set("On").wait()
self.parent.bluestage()
def on_unstage(self):
"""Standard bluesky unstage"""
@@ -286,6 +276,19 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
new = self.read_configuration()
logger.info(f"[{self.name}] PSO configured to {pso_wavemode} mode")
return (old, new)
def bluestage(self) -> None:
"""Bluesky style stage"""
# Stage the PSO distance module and zero counter
if isinstance(self._distance_value, (np.ndarray, list, tuple)):
self.dstArrayRearm.set(1).wait()
# Wait for polling
sleep(0.5)
# Start monitoring the counters if distance is valid
if self.dstDistanceVal.get() > 0:
self.dstEventsEna.set("On").wait()
self.dstCounterEna.set("On").wait()
# # ########################################################################
# # Bluesky flyer interface

View File

@@ -91,6 +91,8 @@ program
Dwell($fSettlingTimeSec)
PsoEventGenerateSingle($axis)
Dwell($fExposureTimeSec)
PsoEventGenerateSingle($axis)
$iglobal[1] = $ii
end

View File

@@ -57,20 +57,7 @@ class AerotechTasksMixin(CustomDeviceMixin):
self.parent.configure(d=d)
# The actual staging
settle_time = 0.2
if self.parent._is_configured:
if self.parent._text_to_execute is not None:
status = self.parent._execute.set(self.parent._text_to_execute, settle_time=settle_time)
else:
status = self.parent.switch.set("Run", settle_time=settle_time)
else:
status = DeviceStatus(self.parent)
status.set_finished()
if settle_time is not None and settle_time > 0:
sleep(settle_time)
if self.parent._failure.value:
raise RuntimeError("Failed to kick off task, please check the Aerotech IOC")
return status
self.parent.bluestage()
def on_unstage(self):
"""Stop the currently selected task"""
@@ -205,6 +192,25 @@ class aa1Tasks(PSIDeviceBase):
new = self.read_configuration()
return (old, new)
def bluestage(self) -> None:
"""Bluesky style stage"""
settle_time = 0.2
if self._is_configured:
if self.taskIndex.get() in (0, 1):
logger.warning(f"[{self.name}] Woah, launching AeroScript on a system task. Daring today are we?")
if self._text_to_execute is not None:
status = self._execute.set(self._text_to_execute, settle_time=settle_time)
else:
status = self.switch.set("Run", settle_time=settle_time)
else:
status = DeviceStatus(self)
status.set_finished()
if settle_time is not None and settle_time > 0:
sleep(settle_time)
if self._failure.value:
raise RuntimeError("Failed to kick off task, please check the Aerotech IOC")
return status
##########################################################################
# Bluesky flyer interface
def complete(self) -> DeviceStatus:

View File

@@ -58,7 +58,7 @@ class StdDaqMixin(CustomDeviceMixin):
if len(d) > 0:
# Stop if current status is not idle
if self.parent.state() != "idle":
self.parent.safestop()
self.parent.surestop()
# Configure new run (will restart the stdDAQ)
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
self.parent.configure(d=d)
@@ -74,7 +74,7 @@ class StdDaqMixin(CustomDeviceMixin):
if reply is not None:
reply = json.loads(reply)
self.parent.status.set(reply["status"], force=True).wait()
logger.info(f"[{self.parent.name}] Start DAQ reply: {reply['status']}")
logger.info(f"[{self.parent.name}] Start DAQ reply: {reply}")
# Give it more time to reconfigure
if reply["status"] in ("rejected"):
# FIXME: running exposure is a nogo
@@ -99,19 +99,12 @@ class StdDaqMixin(CustomDeviceMixin):
def on_unstage(self):
""" Stop a running acquisition and close connection
"""
try:
self.parent.message({"command": "stop_all"}, wait_reply=False)
except (RuntimeError, TypeError):
# The poller thread locks recv raising a RuntimeError
pass
finally:
logger.debug(f"[{self.parent.name}] Deleting socket after unstage")
self.parent._wsclient = None
self.parent.surestop()
def on_stop(self):
""" Stop a running acquisition and close connection
"""
self.parent.unstage()
self.parent.surestop()
def poll(self) -> None:
""" Monitor status messages while connection is open. This will block the reply monitoring
@@ -151,7 +144,7 @@ class StdDaqClient(PSIDeviceBase):
"""
# pylint: disable=too-many-instance-attributes
custom_prepare_cls = StdDaqMixin
USER_ACCESS = ["set_daq_config", "get_daq_config", "safestop", "restart", "connect", "message", "state"]
USER_ACCESS = ["set_daq_config", "get_daq_config", "surestop", "nuke", "connect", "message", "state"]
_wsclient = None
# Status attributes
@@ -223,7 +216,7 @@ class StdDaqClient(PSIDeviceBase):
"The stdDAQ websocket interface refused connection 5 times.")
logger.debug(f"[{self.name}] Connected to DAQ after {num_retry} tries")
def message(self, message: dict, timeout=1, wait_reply=True) -> None | str:
def message(self, message: dict, timeout=1, wait_reply=True, client=None) -> None | str:
"""Send a message to the StdDAQ and receive a reply
Note: finishing acquisition means StdDAQ will close connection, so
@@ -301,7 +294,7 @@ class StdDaqClient(PSIDeviceBase):
if cfg['image_pixel_height'] != self.cfg_pixel_height.get() or cfg['image_pixel_width'] != self.cfg_pixel_width.get():
# Stop if current status is not idle
if self.state() != "idle":
self.safestop()
self.surestop()
# Stop running acquisition
self.unstage()
@@ -348,7 +341,7 @@ class StdDaqClient(PSIDeviceBase):
raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}")
return r.json()
def restart(self):
def nuke(self):
""" Reconfigures the stdDAQ to restart the services
"""
cfg = self.get_daq_config()
@@ -363,35 +356,43 @@ class StdDaqClient(PSIDeviceBase):
r = json.loads(r)
return r['status']
def safestop(self, timeout=5):
def surestop(self, timeout=5):
""" Stops a running acquisition
REST reconfiguration restarts with systemd and can corrupt currently written files.
"""
# Retries to steal connection from poller
state = self.state()
for rr in range(5):
try:
r = self.message({"command": "stop_all"})
if r is not None:
r = json.loads(r)
logger.warning(f"[{self.name}] Stop-all command {r['status']}")
else:
# NOTE: stdDAQ just closes connection if idle
logger.warning(f"[{self.name}] Stop-all command unknown")
finally:
sleep(0.2)
client = connect(self.ws_url.get())
msg = json.dumps({"command": "stop_all"})
client.send(msg)
reply = client.recv(timeout=1)
reply = json.loads(reply)
logger.warning(reply)
# if r['status'] == 'success':
# break
sleep(0.5)
client = connect(self.ws_url.get())
msg = json.dumps({"command": "status"})
client.send(msg)
reply = client.recv(timeout=1)
reply = json.loads(reply)
if reply['status'] in ['idle', 'stoped']:
logger.warning(f"[{self.name}] Stop-all command finished in {reply['status']}")
return
# If stop_all didn't stop, nuke the whole thing
logger.error(f"[{self.name}] Stop-all command did not finish in time, nuking the stdDAQ")
self.nuke()
sleep(timeout)
# Wait until status is back to idle
if timeout > 0.2:
wait_time = 0
while wait_time < timeout/5:
state = self.state()
if state is not None:
if state in ['idle', 'stoped']:
return
sleep(0.2)
wait_time += 0.2
# Automatically connect to microXAS testbench if directly invoked

View File

@@ -28,8 +28,7 @@ class TomcatStepScan(ScanBase):
Example class for simple BEC-based step scan using the low-level API. It's just a standard
'line_scan' with the only difference that overrides burst behavior to use camera burst instead
of software triggers.
of individual software triggers.
NOTE: As decided by Tomcat, the scans should not manage the scope of devices
- All enabled devices are expected to be configured for acquisition by the end of stage
@@ -98,13 +97,13 @@ class TomcatStepScan(ScanBase):
yield from self._move_scan_motors_and_wait(pos)
time.sleep(self.settling_time)
# yield from self.stubs.trigger(min_wait=trigger_time)
yield from self.stubs.trigger(group='trigger', point_id=self.point_id)
trigger_time = self.exp_time * self.burst_at_each_point
time.sleep(trigger_time)
yield from self.stubs.trigger(min_wait=trigger_time)
# yield from self.stubs.trigger(group='trigger', point_id=self.point_id)
# time.sleep(trigger_time)
yield from self.stubs.read(group="monitored", point_id=self.point_id, wait_group=None)
yield from self.stubs.read(group="monitored", point_id=self.point_id)
# yield from self.stubs.read(group="monitored", point_id=self.point_id, wait_group=None)
self.point_id += 1
@@ -195,15 +194,14 @@ class TomcatSnapNStep(AsyncFlyScanBase):
def render_file(self, filename, filesubs):
"""Prepare action: render AeroScript file"""
# Load the test file
if filename is not None:
filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename)
logger.info(f"Attempting to load file {filename}")
with open(filename) as f:
templatetext = f.read()
filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename)
logger.info(f"Attempting to load file {filename}")
with open(filename) as f:
templatetext = f.read()
# Substitute jinja template
tm = jinja2.Template(templatetext)
filetext = tm.render(scan=filesubs)
# Substitute jinja template
tm = jinja2.Template(templatetext)
filetext = tm.render(scan=filesubs)
return filetext
def scan_core(self):
@@ -215,7 +213,7 @@ class TomcatSnapNStep(AsyncFlyScanBase):
# FIXME: this will swallow errors
# yield from self.stubs.complete(device="es1_tasks")
st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
st.wait()
# st.wait()
task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get")
if task_states[4] == 8:
raise RuntimeError(f"Task {4} finished in ERROR state")
@@ -346,7 +344,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
# FIXME: this will swallow errors
# yield from self.stubs.complete(device="es1_tasks")
st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
st.wait()
# st.wait()
task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get")
if task_states[4] == 8:
raise RuntimeError(f"Task {4} finished in ERROR state")