diff --git a/malcolm/modules/ADOdin/blocks/odin_runnable_block.yaml b/malcolm/modules/ADOdin/blocks/odin_runnable_block.yaml index 24b3b6fdf..3469654fc 100644 --- a/malcolm/modules/ADOdin/blocks/odin_runnable_block.yaml +++ b/malcolm/modules/ADOdin/blocks/odin_runnable_block.yaml @@ -45,7 +45,6 @@ description: Name of secondary dataset to link in nxs file default: sum - - builtin.defines.docstring: value: | Device block corresponding to ADOdin + Odin file writer plugin . @@ -65,7 +64,7 @@ mri: $(mri_prefix):DRV prefix: $(pv_prefix):$(drv_suffix) -- ADCore.parts.DetectorDriverPart: +- ADOdin.parts.OdinDriverPart: name: DRV mri: $(mri_prefix):DRV soft_trigger_modes: @@ -91,4 +90,4 @@ name: DSET - builtin.parts.IconPart: - svg: $(yamldir)/../icons/odin_logo.svg \ No newline at end of file + svg: $(yamldir)/../icons/odin_logo.svg diff --git a/malcolm/modules/ADOdin/blocks/odin_writer_block.yaml b/malcolm/modules/ADOdin/blocks/odin_writer_block.yaml index 453d639c0..913ea3b73 100644 --- a/malcolm/modules/ADOdin/blocks/odin_writer_block.yaml +++ b/malcolm/modules/ADOdin/blocks/odin_writer_block.yaml @@ -79,11 +79,24 @@ description: type of the pixel data rbv: $(prefix):DataType -#- ca.parts.CALongPa#-rt: -# name: flushData# PerNFrames -# description: Nu# mber of frames to capture between HDF dataset flushes -# pv: $(prefix):N# umFramesFlush -# rbv_suffix: _RB# V +#- ca.parts.CALongPart: +# name: flushDataPerNFrames +# description: Number of frames to capture between HDF dataset flushes +# pv: $(prefix):NumFramesFlush +# rbv_suffix: _RBV + +- ca.parts.CALongPart: + name: uidOffset + description: value to offset UID by + pv: $(prefix):PARAM:UID:Adjustment + rbv_suffix: _RBV + +- ca.parts.CALongPart: + name: frameOffset + description: value to offset frame index by + pv: $(prefix):OFF:Adjustment + rbv_suffix: _RBV + # Filename - ca.parts.CACharArrayPart: diff --git a/malcolm/modules/ADOdin/parts/__init__.py b/malcolm/modules/ADOdin/parts/__init__.py index 1bc9dbd8e..3b7950eab 100644 --- a/malcolm/modules/ADOdin/parts/__init__.py +++ b/malcolm/modules/ADOdin/parts/__init__.py @@ -1,4 +1,5 @@ from .odinwriterpart import OdinWriterPart, AMri, APartName +from .odindriverpart import OdinDriverPart # Expose a nice namespace from malcolm.core import submodule_all diff --git a/malcolm/modules/ADOdin/parts/odindriverpart.py b/malcolm/modules/ADOdin/parts/odindriverpart.py new file mode 100644 index 000000000..43638b0fe --- /dev/null +++ b/malcolm/modules/ADOdin/parts/odindriverpart.py @@ -0,0 +1,18 @@ +from malcolm.core import Context +from malcolm.modules import builtin, scanning +from malcolm.modules.ADCore.parts import DetectorDriverPart + +from annotypes import add_call_types, Any + +class OdinDriverPart(DetectorDriverPart): + def setup_detector(self, + context, # type: Context + completed_steps, # type: scanning.hooks.ACompletedSteps + steps_to_do, # type: scanning.hooks.AStepsToDo + duration, # type: float + part_info, # type: scanning.hooks.APartInfo + **kwargs # type: Any + ): + # type: (...) -> None + super(OdinDriverPart, self).setup_detector(context, 0, steps_to_do, duration, part_info) + diff --git a/malcolm/modules/ADOdin/parts/odinwriterpart.py b/malcolm/modules/ADOdin/parts/odinwriterpart.py index ca94cc634..708d7da3c 100644 --- a/malcolm/modules/ADOdin/parts/odinwriterpart.py +++ b/malcolm/modules/ADOdin/parts/odinwriterpart.py @@ -5,7 +5,7 @@ from vdsgen import InterleaveVDSGenerator, ReshapeVDSGenerator from scanpointgenerator import CompoundGenerator -from malcolm.core import APartName, Future, Info, PartRegistrar, BadValueError +from malcolm.core import APartName, Future, Info, PartRegistrar, BadValueError, errors from malcolm.modules import builtin, scanning if TYPE_CHECKING: @@ -230,7 +230,7 @@ def add_nexus_nodes(generator, vds_file_path): # We will set these attributes on the child block, so don't save them -@builtin.util.no_save("fileName", "filePath", "numCapture") +@builtin.util.no_save("fileName", "filePath", "numCapture", "uidOffset", "frameOffset") class OdinWriterPart(builtin.parts.ChildPart): """Part for controlling an `hdf_writer_block` in a Device""" @@ -239,6 +239,7 @@ class OdinWriterPart(builtin.parts.ChildPart): array_future = None # type: Future done_when_reaches = None # type: int unique_id_offset = None # type: int + frame_offset = None # type: int # The HDF5 layout file we write to say where the datasets go layout_filename = None # type: str exposure_time = None # type: float @@ -249,9 +250,8 @@ def __init__(self, initial_visibility=True, # type: AInitialVisibility uid_name="uid", # type: AUidName sum_name="sum", # type: ASumName - secondary_set="sum" # type: ASecondaryDataset + secondary_set="sum", # type: ASecondaryDataset ): - # type: (...) -> None self.uid_name = uid_name self.sum_name = sum_name self.secondary_set = secondary_set @@ -276,7 +276,7 @@ def setup(self, registrar): registrar.hook(scanning.hooks.RunHook, self.on_run) registrar.hook(scanning.hooks.PostRunReadyHook, self.on_post_run_ready) registrar.hook(scanning.hooks.AbortHook, self.on_abort) - registrar.hook(scanning.hooks.PauseHook, self.on_pause) + # registrar.hook(scanning.hooks.PauseHook, self.on_pause) @add_call_types def on_pause(self, context): @@ -301,8 +301,12 @@ def on_configure(self, # On initial configure, expect to get the demanded number of frames self.done_when_reaches = completed_steps + steps_to_do - self.unique_id_offset = 0 + if completed_steps == 0: + self.unique_id_offset = 1 + self.frame_offset = 0 child = context.block_view(self.mri) + child.uidOffset.put_value(self.unique_id_offset) + child.frameOffset.put_value(self.frame_offset) file_dir = fileDir.rstrip(os.sep) # derive file path from template as AreaDetector would normally do @@ -318,7 +322,7 @@ def on_configure(self, assert "." in vds_full_filename, \ "File extension for %r should be supplied" % vds_full_filename futures = child.put_attribute_values_async(dict( - numCapture=steps_to_do, + numCapture=self.done_when_reaches, filePath=file_dir + os.sep, fileName=raw_file_basename)) context.wait_all_futures(futures) @@ -347,13 +351,27 @@ def on_seek(self, ): # type: (...) -> None # This is rewinding or setting up for another batch, so the detector - # will skip to a uniqueID that has not been produced yet - self.unique_id_offset = completed_steps - self.done_when_reaches - self.done_when_reaches += steps_to_do + # will reset count to zero and set UID offset appropriately + child = context.block_view(self.mri) - # Just reset the array counter_block - child.arrayCounter.put_value(0) - # Start a future waiting for the first array + # Wait until Odin stops receiving frames + # try: + # child.when_value_matches( + # "numCaptured", self.done_when_reaches, + # event_timeout=self.exposure_time + 5) + # except errors.TimeoutError: + # pass + #current_count = child.numCaptured.value + + self.unique_id_offset = self.done_when_reaches + self.frame_offset = completed_steps + + child.uidOffset.put_value(self.unique_id_offset) + child.frameOffset.put_value(self.frame_offset) + + # TODO: point this to check written UID instead + # self.done_when_reaches = steps_to_do + current_count + self.array_future = child.when_value_matches_async( "numCaptured", greater_than_zero) diff --git a/malcolm/modules/scanning/controllers/runnablecontroller.py b/malcolm/modules/scanning/controllers/runnablecontroller.py index 98e652357..3536495f9 100644 --- a/malcolm/modules/scanning/controllers/runnablecontroller.py +++ b/malcolm/modules/scanning/controllers/runnablecontroller.py @@ -26,6 +26,8 @@ AConfigureParams = ConfigureParams with Anno("Step to mark as the last completed step, -1 for current"): ALastGoodStep = int +with Anno("Quantise seek to integer multiples of"): + ASeekMultiple = int # Pull re-used annotypes into our namespace in case we are subclassed AConfigDir = builtin.controllers.AConfigDir @@ -149,6 +151,7 @@ def __init__(self, initial_design="", # type: AInitialDesign use_git=True, # type: AUseGit description="", # type: ADescription + seek_multiple_of=1, # type: ASeekMultiple ): # type: (...) -> None super(RunnableController, self).__init__( @@ -176,6 +179,8 @@ def __init__(self, self.steps_per_run = 0 # type: int # Create sometimes writeable attribute for the current completed scan # step + self.seek_multiple_of = seek_multiple_of + # Quantise seek to integer multiples of this value self.completed_steps = NumberMeta( "int32", "Readback of number of scan steps", tags=[Widget.METER.tag()] # Widget.TEXTINPUT.tag()] @@ -565,6 +570,8 @@ def pause(self, lastGoodStep=-1): elif lastGoodStep >= total_steps: lastGoodStep = total_steps - 1 + lastGoodStep -= (lastGoodStep % self.seek_multiple_of) + if self.state.value in [ss.ARMED, ss.FINISHED]: # We don't have a run process, free to go anywhere we want next_state = ss.ARMED @@ -581,10 +588,14 @@ def do_pause(self, completed_steps): # type: (int) -> None self.run_hooks( PauseHook(p, c) for p, c in self.create_part_contexts().items()) + + completed_steps -= (completed_steps % self.seek_multiple_of) + in_run_steps = completed_steps % self.steps_per_run steps_to_do = self.steps_per_run - in_run_steps part_info = self.run_hooks( ReportStatusHook(p, c) for p, c in self.part_contexts.items()) + self.completed_steps.set_value(completed_steps) self.run_hooks( SeekHook(p, c, completed_steps, steps_to_do, part_info, **kwargs)