Loading openflexure_microscope/microscope.py +92 −84 Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ from .plugins import PluginMount from .utilities import axes_to_array from .task import TaskOrchestrator from .lock import CompositeLock from .config import OpenflexureConfig from .config import OpenflexureSettingsFile, settings_to_json class Microscope: Loading @@ -24,7 +24,7 @@ class Microscope: The camera and stage should already be initialised, and passed as arguments. Attributes: rc (:py:class:`openflexure_microscope.config.OpenflexureConfig`): Runtime-config object, settings_file (:py:class:`openflexure_microscope.config.OpenflexureSettingsFile`): Runtime-config object, automatically created if None. lock (:py:class:`openflexure_microscope.lock.CompositeLock`): Composite lock controlling thread access to multiple pieces of hardware. Loading @@ -36,31 +36,28 @@ class Microscope: """ def __init__(self): # Create runtime config object self.rc = OpenflexureConfig(expand=True) # Initial attributes self.id = uuid.uuid4().hex self.name = self.id self.fov = [0, 0] self.plugin_maps = [] self.camera = None self.stage = None # Initialise with an empty composite lock self.lock = CompositeLock([]) # Attach dummy hardware initially # TODO: Handle better. Maybe monad? self.attach(None, None) # Create a task orchestrator self.task = TaskOrchestrator() # Create plugin mount-point self.plugin = PluginMount(self) # Attach plugins self.attach_plugins() # Set default parameters if 'name' not in self.rc.config: self.rc.write({'name': uuid.uuid4().hex}) # Attach to a settings file self.settings_file = OpenflexureSettingsFile(expand=True) # Apply settings loaded from file self.apply_config(self.settings_file.load()) if 'fov' not in self.rc.config: self.rc.write({'fov': [0, 0]}) # Create plugin mount-point and attach plugins from maps self.plugin = PluginMount(self) self.attach_plugins(self.plugin_maps) def __enter__(self): """Create microscope on context enter.""" Loading Loading @@ -90,59 +87,48 @@ class Microscope: camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object """ settings_full = self.read_config() # TODO: Actually attach dummy hardware! # TODO: Use some form of Mabye monad to say arguments may be actual hardware, else dummy? # Maybe even attach dummy hardware at __init__, and replace with real hardware if it exists logging.debug("Attaching camera...") self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object if isinstance(self.camera, StreamingCamera): if not self.camera: logging.info("No camera attached.") else: logging.info("Attached camera {}".format(camera)) logging.info("Updating camera settings") self.camera.apply_config(self.rc.read()) logging.info("Reading complete camera settings") self.write_config(self.camera.read_config()) elif not self.camera: logging.info("Attached dummy camera.") # If camera has a lock if hasattr(self.camera, 'lock'): if hasattr(self.camera, 'lock'): # If camera has a lock logging.info("Attaching {} to composite lock.".format(self.camera.lock)) # Add the lock to the microscope composite lock self.lock.locks.append(self.camera.lock) logging.debug("Attaching stage...") self.stage = stage #: :py:class:`openflexure_stage.stage.OpenFlexureStage`: OpenFlexure stage object if isinstance(self.stage, OpenFlexureStage): # If a stage object has been attached if not self.stage: logging.info("No stage attached.") else: logging.info("Attached stage {}".format(stage)) self.stage.backlash = np.zeros(3, dtype=np.int) elif not self.stage: logging.info("Attached dummy stage.") # If stage object has a lock if hasattr(self.stage, 'lock'): if hasattr(self.stage, 'lock'): # If stage object has a lock logging.info("Attaching lock {} to composite lock.".format(self.stage.lock)) # Add the lock to the microscope composite lock self.lock.locks.append(self.stage.lock) def attach_plugin_maps(self, plugin_maps): """ Attach plugins from a list of plugin map strings logging.info("Reapplying settings to newly attached devices") self.apply_config(settings_full) Args: plugin_maps (list): List of strings of plugin maps. """ logging.debug("Attaching plugins...") for plugin_map in plugin_maps: self.plugin.attach(plugin_map) logging.debug("Plugins attached.") def attach_plugins(self): def attach_plugins(self, plugin_maps: list): """ Automatically search for plugin maps in self.config, and attach. """ if 'plugins' in self.rc.config: self.attach_plugin_maps(self.rc.config['plugins']) if plugin_maps: for plugin_map in plugin_maps: self.plugin.attach(plugin_map) else: logging.warning("No plugins specified in microscope_settings.yaml. Skipping.") logging.warning("No plugins specified. Skipping.") def reload_plugins(self): """ Loading @@ -151,7 +137,7 @@ class Microscope: logging.info("Tearing down existing PluginMount...") self.plugin = PluginMount(self) logging.info("Repopulating PluginMount...") self.attach_plugins() self.attach_plugins(self.plugin_maps) # Create unified state @property Loading @@ -169,46 +155,66 @@ class Microscope: } return state def write_config(self, config: dict): def apply_config(self, config: dict): """ Writes and applies a config dictionary. Missing parameters will be left untouched. Applies a config dictionary. Missing parameters will be left untouched. """ # TODO: Have stage object handle apply_config, like camera does logging.debug("Microscope: Applying config: {}".format(config)) # If attached to a camera if self.camera: if ('camera_settings' in config) and self.camera: # Update camera config self.camera.apply_config(config) self.camera.apply_config(config['camera_settings']) # If attached to a stage # TODO: Convert stage settings into a config expansion if self.stage: if ('stage_settings' in config) and self.stage: if 'backlash' in config: # Construct backlash array backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0]) self.stage.backlash = backlash # Cache config self.rc.write(config) # Todo: tidy up with some loopy goodness if 'id' in config: self.id = config['id'] if 'name' in config: self.name = config['name'] if 'fov' in config: self.fov = config['fov'] if 'plugins' in config: self.plugin_maps = config['plugins'] def read_config(self, json_safe=False): """ Read an updated config including camera settings. Get an updated settings dictionary. Reads current attributes and properties from connected hardware, then merges those with the currently saved settings. This is to ensure that settings for currently disconnected hardware don't get removed from the settings file. """ settings_current = { 'id': self.id, 'name': self.name, 'fov': self.fov, 'plugins': self.plugin_maps } # If attached to a camera if self.camera: # Update camera config params from StreamingCamera object self.rc.write(self.camera.read_config()) settings_current_camera = self.camera.read_config() settings_current['camera_settings'] = settings_current_camera # If attached to a stage # TODO: Offload to stage object if self.stage: if hasattr(self.stage.backlash, 'tolist'): backlash = self.stage.backlash.tolist() else: backlash = self.stage.backlash else: backlash = [0, 0, 0] backlash = { settings_current_stage = { 'backlash': { 'x': backlash[0], 'y': backlash[1], Loading @@ -216,18 +222,20 @@ class Microscope: } } self.rc.write(backlash) settings_current['stage_settings'] = settings_current_stage settings_full = self.settings_file.merge(settings_current) if json_safe: settings_full = settings_to_json(settings_full, clean_keys=True) return self.rc.read(json_safe=json_safe) return settings_full def save_config(self, backup: bool = True): """ Saves the current runtime-config back to disk Merges the current settings back to disk """ # Get any changed device settings self.read_config(json_safe=False) # Save to disk self.rc.save(backup=backup) self.settings_file.save(self.read_config(), backup=True) @property def config(self) -> dict: Loading Loading
openflexure_microscope/microscope.py +92 −84 Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ from .plugins import PluginMount from .utilities import axes_to_array from .task import TaskOrchestrator from .lock import CompositeLock from .config import OpenflexureConfig from .config import OpenflexureSettingsFile, settings_to_json class Microscope: Loading @@ -24,7 +24,7 @@ class Microscope: The camera and stage should already be initialised, and passed as arguments. Attributes: rc (:py:class:`openflexure_microscope.config.OpenflexureConfig`): Runtime-config object, settings_file (:py:class:`openflexure_microscope.config.OpenflexureSettingsFile`): Runtime-config object, automatically created if None. lock (:py:class:`openflexure_microscope.lock.CompositeLock`): Composite lock controlling thread access to multiple pieces of hardware. Loading @@ -36,31 +36,28 @@ class Microscope: """ def __init__(self): # Create runtime config object self.rc = OpenflexureConfig(expand=True) # Initial attributes self.id = uuid.uuid4().hex self.name = self.id self.fov = [0, 0] self.plugin_maps = [] self.camera = None self.stage = None # Initialise with an empty composite lock self.lock = CompositeLock([]) # Attach dummy hardware initially # TODO: Handle better. Maybe monad? self.attach(None, None) # Create a task orchestrator self.task = TaskOrchestrator() # Create plugin mount-point self.plugin = PluginMount(self) # Attach plugins self.attach_plugins() # Set default parameters if 'name' not in self.rc.config: self.rc.write({'name': uuid.uuid4().hex}) # Attach to a settings file self.settings_file = OpenflexureSettingsFile(expand=True) # Apply settings loaded from file self.apply_config(self.settings_file.load()) if 'fov' not in self.rc.config: self.rc.write({'fov': [0, 0]}) # Create plugin mount-point and attach plugins from maps self.plugin = PluginMount(self) self.attach_plugins(self.plugin_maps) def __enter__(self): """Create microscope on context enter.""" Loading Loading @@ -90,59 +87,48 @@ class Microscope: camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object """ settings_full = self.read_config() # TODO: Actually attach dummy hardware! # TODO: Use some form of Mabye monad to say arguments may be actual hardware, else dummy? # Maybe even attach dummy hardware at __init__, and replace with real hardware if it exists logging.debug("Attaching camera...") self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object if isinstance(self.camera, StreamingCamera): if not self.camera: logging.info("No camera attached.") else: logging.info("Attached camera {}".format(camera)) logging.info("Updating camera settings") self.camera.apply_config(self.rc.read()) logging.info("Reading complete camera settings") self.write_config(self.camera.read_config()) elif not self.camera: logging.info("Attached dummy camera.") # If camera has a lock if hasattr(self.camera, 'lock'): if hasattr(self.camera, 'lock'): # If camera has a lock logging.info("Attaching {} to composite lock.".format(self.camera.lock)) # Add the lock to the microscope composite lock self.lock.locks.append(self.camera.lock) logging.debug("Attaching stage...") self.stage = stage #: :py:class:`openflexure_stage.stage.OpenFlexureStage`: OpenFlexure stage object if isinstance(self.stage, OpenFlexureStage): # If a stage object has been attached if not self.stage: logging.info("No stage attached.") else: logging.info("Attached stage {}".format(stage)) self.stage.backlash = np.zeros(3, dtype=np.int) elif not self.stage: logging.info("Attached dummy stage.") # If stage object has a lock if hasattr(self.stage, 'lock'): if hasattr(self.stage, 'lock'): # If stage object has a lock logging.info("Attaching lock {} to composite lock.".format(self.stage.lock)) # Add the lock to the microscope composite lock self.lock.locks.append(self.stage.lock) def attach_plugin_maps(self, plugin_maps): """ Attach plugins from a list of plugin map strings logging.info("Reapplying settings to newly attached devices") self.apply_config(settings_full) Args: plugin_maps (list): List of strings of plugin maps. """ logging.debug("Attaching plugins...") for plugin_map in plugin_maps: self.plugin.attach(plugin_map) logging.debug("Plugins attached.") def attach_plugins(self): def attach_plugins(self, plugin_maps: list): """ Automatically search for plugin maps in self.config, and attach. """ if 'plugins' in self.rc.config: self.attach_plugin_maps(self.rc.config['plugins']) if plugin_maps: for plugin_map in plugin_maps: self.plugin.attach(plugin_map) else: logging.warning("No plugins specified in microscope_settings.yaml. Skipping.") logging.warning("No plugins specified. Skipping.") def reload_plugins(self): """ Loading @@ -151,7 +137,7 @@ class Microscope: logging.info("Tearing down existing PluginMount...") self.plugin = PluginMount(self) logging.info("Repopulating PluginMount...") self.attach_plugins() self.attach_plugins(self.plugin_maps) # Create unified state @property Loading @@ -169,46 +155,66 @@ class Microscope: } return state def write_config(self, config: dict): def apply_config(self, config: dict): """ Writes and applies a config dictionary. Missing parameters will be left untouched. Applies a config dictionary. Missing parameters will be left untouched. """ # TODO: Have stage object handle apply_config, like camera does logging.debug("Microscope: Applying config: {}".format(config)) # If attached to a camera if self.camera: if ('camera_settings' in config) and self.camera: # Update camera config self.camera.apply_config(config) self.camera.apply_config(config['camera_settings']) # If attached to a stage # TODO: Convert stage settings into a config expansion if self.stage: if ('stage_settings' in config) and self.stage: if 'backlash' in config: # Construct backlash array backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0]) self.stage.backlash = backlash # Cache config self.rc.write(config) # Todo: tidy up with some loopy goodness if 'id' in config: self.id = config['id'] if 'name' in config: self.name = config['name'] if 'fov' in config: self.fov = config['fov'] if 'plugins' in config: self.plugin_maps = config['plugins'] def read_config(self, json_safe=False): """ Read an updated config including camera settings. Get an updated settings dictionary. Reads current attributes and properties from connected hardware, then merges those with the currently saved settings. This is to ensure that settings for currently disconnected hardware don't get removed from the settings file. """ settings_current = { 'id': self.id, 'name': self.name, 'fov': self.fov, 'plugins': self.plugin_maps } # If attached to a camera if self.camera: # Update camera config params from StreamingCamera object self.rc.write(self.camera.read_config()) settings_current_camera = self.camera.read_config() settings_current['camera_settings'] = settings_current_camera # If attached to a stage # TODO: Offload to stage object if self.stage: if hasattr(self.stage.backlash, 'tolist'): backlash = self.stage.backlash.tolist() else: backlash = self.stage.backlash else: backlash = [0, 0, 0] backlash = { settings_current_stage = { 'backlash': { 'x': backlash[0], 'y': backlash[1], Loading @@ -216,18 +222,20 @@ class Microscope: } } self.rc.write(backlash) settings_current['stage_settings'] = settings_current_stage settings_full = self.settings_file.merge(settings_current) if json_safe: settings_full = settings_to_json(settings_full, clean_keys=True) return self.rc.read(json_safe=json_safe) return settings_full def save_config(self, backup: bool = True): """ Saves the current runtime-config back to disk Merges the current settings back to disk """ # Get any changed device settings self.read_config(json_safe=False) # Save to disk self.rc.save(backup=backup) self.settings_file.save(self.read_config(), backup=True) @property def config(self) -> dict: Loading