diff --git a/openflexure_microscope/api/microscope.py b/openflexure_microscope/api/microscope.py index 04053892..2f534b49 100644 --- a/openflexure_microscope/api/microscope.py +++ b/openflexure_microscope/api/microscope.py @@ -3,38 +3,9 @@ from openflexure_microscope.camera.capture import build_captures_from_exif import logging -# Import device modules -# NB this will eventually be handled by the RC file, so you can choose what device -# class should be attached. -try: - from openflexure_microscope.camera.pi import PiCameraStreamer -except ImportError: - logging.warning("Unable to import PiCameraStreamer") -from openflexure_microscope.camera.mock import MockStreamer - -from openflexure_microscope.stage.sanga import SangaStage -from openflexure_microscope.stage.mock import MockStage default_microscope = Microscope() -# Initialise camera -logging.debug("Creating camera object...") -try: - api_camera = PiCameraStreamer() -except Exception as e: - logging.error(e) - logging.warning("No valid camera hardware found. Falling back to mock camera!") - api_camera = MockStreamer() - -# Initialise stage -logging.debug("Creating stage object...") - -api_stage = MockStage() - -# Attach devices to microscope -logging.debug("Attaching devices to microscope...") -default_microscope.attach(api_camera, api_stage) - # Restore loaded capture array to camera object logging.debug("Restoring captures...") default_microscope.camera.images = build_captures_from_exif( diff --git a/openflexure_microscope/api/v2/views/state.py b/openflexure_microscope/api/v2/views/state.py index aa59ffdf..34637754 100644 --- a/openflexure_microscope/api/v2/views/state.py +++ b/openflexure_microscope/api/v2/views/state.py @@ -30,7 +30,7 @@ class SettingsProperty(View): logging.debug("Updating settings from PUT request:") logging.debug(payload.json) - microscope.apply_settings(payload.json) + microscope.update_settings(payload.json) microscope.save_settings() return self.get() @@ -65,7 +65,7 @@ class NestedSettingsProperty(View): dictionary = create_from_path(keys) set_by_path(dictionary, keys, payload.json) - microscope.apply_settings(dictionary) + microscope.update_settings(dictionary) microscope.save_settings() return self.get(route) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index e6395e1f..e823cfd0 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -121,7 +121,8 @@ class BaseCamera(metaclass=ABCMeta): self.stream_timeout = 20 self.stream_timeout_enabled = False - self.status = {"board": None} + self.stream_active = False + self.record_active = False self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH} @@ -129,8 +130,24 @@ class BaseCamera(metaclass=ABCMeta): self.images = [] self.videos = [] + @property @abstractmethod - def apply_settings(self, config: dict): + def configuration(self): + """The current camera configuration.""" + pass + + @property + @abstractmethod + def state(self): + """The current read-only camera state.""" + pass + + @property + def settings(self): + return self.read_settings() + + @abstractmethod + def update_settings(self, config: dict): """Update settings from a config dictionary""" with self.lock: # Apply valid config params to camera object @@ -187,7 +204,7 @@ class BaseCamera(metaclass=ABCMeta): self.last_access = time.time() self.stop = False - if not self.status["stream_active"]: + if not self.stream_active: # start background frame thread self.thread = threading.Thread(target=self._thread) self.thread.daemon = True @@ -207,12 +224,12 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("Stopping worker thread") timeout_time = time.time() + timeout - if self.status["stream_active"]: + if self.stream_active: self.stop = True self.thread.join() # Wait for stream thread to exit logging.debug("Waiting for stream thread to exit.") - while self.status["stream_active"]: + while self.stream_active: if time.time() > timeout_time: logging.debug("Timeout waiting for worker thread close.") raise TimeoutError("Timeout waiting for worker thread close.") @@ -352,7 +369,7 @@ class BaseCamera(metaclass=ABCMeta): self.frames_iterator = self.frames() logging.debug("Entering worker thread.") - self.status["stream_active"] = True + self.stream_active = True for frame in self.frames_iterator: self.frame = frame @@ -365,9 +382,7 @@ class BaseCamera(metaclass=ABCMeta): and ( # If using timeout time.time() - self.last_access > self.stream_timeout ) - and not self.status[ # And timeout time - "preview_active" - ] # And GPU preview is not active + and not self.preview_active # And GPU preview is not active ): self.frames_iterator.close() break @@ -383,4 +398,4 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("BaseCamera worker thread exiting...") # Set stream_activate state - self.status["stream_active"] = False + self.stream_active = False diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py index abffb221..322bae58 100644 --- a/openflexure_microscope/camera/mock.py +++ b/openflexure_microscope/camera/mock.py @@ -26,17 +26,13 @@ We override the logging settings in api.app by setting a level for PIL here. pil_logger = logging.getLogger("PIL") pil_logger.setLevel(logging.INFO) + # MAIN CLASS -class MockStreamer(BaseCamera): +class MissingCamera(BaseCamera): def __init__(self): # Run BaseCamera init BaseCamera.__init__(self) - # Store state of PiCameraStreamer - self.status.update( - {"stream_active": False, "record_active": False, "board": None} - ) - # Update config properties self.image_resolution = (1312, 976) self.stream_resolution = (640, 480) @@ -71,6 +67,16 @@ class MockStreamer(BaseCamera): image.save(self.stream, format="JPEG") + @property + def configuration(self): + """The current camera configuration.""" + return {} + + @property + def state(self): + """The current read-only camera state.""" + return {} + def initialisation(self): """Run any initialisation code when the frame iterator starts.""" pass @@ -101,7 +107,7 @@ class MockStreamer(BaseCamera): return conf_dict - def apply_settings(self, config: dict): + def update_settings(self, config: dict): """ Write a config dictionary to the PiCameraStreamer config. @@ -117,7 +123,7 @@ class MockStreamer(BaseCamera): with self.lock: # Apply valid config params to camera object - if not self.status["record_active"]: # If not recording a video + if not self.record_active: # If not recording a video for key, value in config.items(): # For each provided setting if hasattr(self, key): diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index 5cafb045..bbd04e44 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -47,6 +47,7 @@ from .base import BaseCamera, CaptureObject from .set_picamera_gain import set_analog_gain, set_digital_gain from openflexure_microscope.paths import settings_file_path +from openflexure_microscope.utilities import serialise_array_b64 # MAIN CLASS @@ -85,14 +86,7 @@ class PiCameraStreamer(BaseCamera): ) #: :py:class:`picamera.PiCamera`: Picamera object # Store status of PiCameraStreamer - self.status.update( - { - "stream_active": False, - "record_active": False, - "preview_active": False, - "board": f"picamera_{self.camera.revision}", - } - ) + self.preview_active = False # Reset variable states self.set_zoom(1.0) @@ -116,15 +110,37 @@ class PiCameraStreamer(BaseCamera): "picamera_lst.npy" ) #: str: Path of .npy lens shading table file - # Update board identifier - self.status.update({}) - # Create an empty stream self.stream = io.BytesIO() # Start streaming self.start_worker() + @property + def configuration(self): + """The current camera configuration.""" + config = { + "board": self.camera.revision, + } + + if self.read_lens_shading_table(): + b64_string, dtype, shape = serialise_array_b64(self.read_lens_shading_table()) + + config.update({ + "lens_shading_table": { + "b64_string": b64_string, + "dtype": dtype, + "shape": shape, + } + }) + + return config + + @property + def state(self): + """The current read-only camera state.""" + return {} + def initialisation(self): """Run any initialisation code when the frame iterator starts.""" pass @@ -176,7 +192,7 @@ class PiCameraStreamer(BaseCamera): logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path)) self.save_lens_shading_table() - def apply_settings(self, config: dict): + def update_settings(self, config: dict): """ Write a config dictionary to the PiCameraStreamer config. @@ -194,10 +210,10 @@ class PiCameraStreamer(BaseCamera): with self.lock: # Apply valid config params to Picamera object - if not self.status["record_active"]: # If not recording a video + if not self.record_active: # If not recording a video # Pause stream while changing settings - if self.status["stream_active"]: # If stream is active + if self.stream_active: # If stream is active logging.info("Pausing stream to update config.") self.stop_stream_recording() # Pause stream paused_stream = True # Remember to unpause stream when done @@ -365,7 +381,7 @@ class PiCameraStreamer(BaseCamera): self.camera.preview.window = window if fullscreen: self.camera.preview.fullscreen = fullscreen - self.status["preview_active"] = True + self.preview_active = True except picamera.exc.PiCameraMMALError as e: logging.error( "Suppressed a MMALError in start_preview. Exception: {}".format(e) @@ -380,7 +396,7 @@ class PiCameraStreamer(BaseCamera): def stop_preview(self): """Stop the on board GPU camera preview.""" self.camera.stop_preview() - self.status["preview_active"] = False + self.preview_active = False def start_recording(self, output, fmt: str = "h264", quality: int = 15): """Start recording. @@ -398,7 +414,7 @@ class PiCameraStreamer(BaseCamera): """ with self.lock: # Start recording method only if a current recording is not running - if not self.status["record_active"]: + if not self.record_active: # Start the camera video recording on port 2 logging.info("Recording to {}".format(output)) @@ -412,7 +428,7 @@ class PiCameraStreamer(BaseCamera): ) # Update status dictionary - self.status["record_active"] = True + self.record_active = True return output @@ -432,7 +448,7 @@ class PiCameraStreamer(BaseCamera): logging.info("Recording stopped") # Update status dictionary - self.status["record_active"] = False + self.record_active = False def stop_stream_recording( self, splitter_port: int = 1, resolution: Tuple[int, int] = None @@ -500,7 +516,7 @@ class PiCameraStreamer(BaseCamera): self.camera.resolution = resolution # If the stream should be active - if self.status["stream_active"]: + if self.stream_active: try: # Start recording on stream port self.camera.start_recording( diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 8e613574..cfc458a1 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -7,7 +7,7 @@ from uuid import UUID import numpy as np from fractions import Fraction -from .paths import CONFIG_FILE_PATH, DEFAULT_CONFIG_FILE_PATH +from .paths import SETTINGS_FILE_PATH, DEFAULT_SETTINGS_FILE_PATH, CONFIGURATION_FILE_PATH, DEFAULT_CONFIGURATION_FILE_PATH class OpenflexureSettingsFile: @@ -19,21 +19,21 @@ class OpenflexureSettingsFile: expand (bool): Expand paths to valid auxillary config files. """ - def __init__(self, config_path: str = None): - global DEFAULT_CONFIG, CONFIG_FILE_PATH + def __init__(self, path: str, defaults: dict = {}): + global DEFAULT_SETTINGS # Set arguments - self.config_path = config_path or CONFIG_FILE_PATH + self.path = path # Initialise basic config file with defaults if it doesn't exist - initialise_file(self.config_path, populate=DEFAULT_CONFIG) + initialise_file(self.path, populate=defaults) def load(self) -> dict: """ Loads settings from a file on-disk. """ # Unexpanded config dictionary (used at load/save time) - loaded_config = load_json_file(self.config_path) + loaded_config = load_json_file(self.path) logging.debug("Reading settings from disk") return loaded_config @@ -50,11 +50,11 @@ class OpenflexureSettingsFile: save_settings = config if backup: - if os.path.isfile(self.config_path): - shutil.copyfile(self.config_path, self.config_path + ".bk") + if os.path.isfile(self.path): + shutil.copyfile(self.path, self.path + ".bk") logging.debug("Saving settings dictionary to disk") - save_json_file(self.config_path, save_settings) + save_json_file(self.path, save_settings) def merge(self, config: dict) -> dict: """ @@ -178,10 +178,17 @@ def initialise_file(config_path, populate: str = "{}\n"): outfile.write(populate) -# Load the default config -with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc: - DEFAULT_CONFIG = default_rc.read() - +# Load the default settings +with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings: + DEFAULT_SETTINGS = default_settings.read() #: Default user settings object -user_settings = OpenflexureSettingsFile(config_path=CONFIG_FILE_PATH) +user_settings = OpenflexureSettingsFile(path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS) + + +# Load the default configuration +with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration: + DEFAULT_CONFIGURATION = default_configuration.read() + +#: Default user settings object +user_configuration = OpenflexureSettingsFile(path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION) diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 31ea505c..2f52c6d9 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -6,15 +6,20 @@ import logging import pkg_resources import uuid -from openflexure_microscope.stage.base import BaseStage -from openflexure_microscope.stage.mock import MockStage -from openflexure_microscope.camera.base import BaseCamera -from openflexure_microscope.camera.mock import MockStreamer +from openflexure_microscope.stage.mock import MissingStage +from openflexure_microscope.camera.mock import MissingCamera +from openflexure_microscope.stage.sanga import SangaStage +try: + from openflexure_microscope.camera.pi import PiCameraStreamer +except ImportError: + logging.warning("Unable to import PiCameraStreamer") +from openflexure_microscope.camera.mock import MissingCamera from openflexure_microscope.utilities import serialise_array_b64 -from openflexure_microscope.config import user_settings +from openflexure_microscope.config import user_settings, user_configuration from labthings.core.lock import CompositeLock +from labthings.core.utilities import rupdate class Microscope: @@ -24,20 +29,32 @@ class Microscope: The camera and stage objects may already be initialised, and can be passed as arguments. """ - def __init__(self): - # Initial attributes - self.id = uuid.uuid4() #: Microscope UUID - self.name = self.id #: Microscope name (modifiable) - self.fov = [0, 0] #: Microscope field-of-view in stage motor steps - self.camera = None #: Currently connected camera object - self.stage = None #: Currently connected stage object + def __init__(self, settings = user_settings, configuration = user_configuration): + # Store settings and configuration files + self.settings_file = settings + self.configuration_file = configuration # Initialise with an empty composite lock #: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage self.lock = CompositeLock([]) + self.camera = None #: Currently connected camera object + self.stage = None #: Currently connected stage object + self.setup(self.configuration_file.load()) # Attach components + # Apply settings loaded from file - self.apply_settings(user_settings.load()) + self.update_settings(self.settings_file.load()) + + # Initial attributes + if self.configuration_file.load().get("id"): + self.id = configuration.get("id") + else: + self.id = uuid.uuid4() + self.configuration_file.save({ + "id": self.id + }) + + self.name = self.id def __enter__(self): """Create microscope on context enter.""" @@ -56,58 +73,49 @@ class Microscope: self.stage.close() logging.info("Closed {}".format(self)) - def attach(self, camera: BaseCamera, stage: BaseStage): + def setup(self, configuration): """ - Retroactively attaches a camera and stage to the microscope object. - - Allows the microscope to be created as a "dummy", with hardware communications - opened at a later time. - - Args: - camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): camera object - stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object + Attach microscope components based on initially passed configuration file """ - settings_full = self.read_settings() + ### Detector + if configuration.get("detector"): + detector_type = configuration["detector"].get("type") + if detector_type == "PiCamera" or detector_type == "PiCameraStreamer": + try: + self.camera = PiCameraStreamer() + except Exception as e: + logging.error(e) + logging.warning("No compatible camera hardware found.") - logging.debug("Attaching camera...") - self.camera = ( - camera - ) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object + ### Stage + if configuration.get("stage"): + stage_type = configuration["stage"].get("type") + stage_port = configuration["stage"].get("port") + if stage_type == "SangaBoard" or detector_type == "SangaStage": + try: + self.stage = SangaStage(port=stage_port) + except Exception as e: + logging.error(e) + logging.warning("No compatible stage hardware found.") + + ### Fallbacks if not self.camera: - logging.info("No camera attached.") - else: - logging.info("Attached camera {}".format(camera)) - - 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_microscope.stage.base.BaseStage`: OpenFlexure stage object + self.camera = MissingCamera() if not self.stage: - logging.info("No stage attached.") - else: - logging.info("Attached stage {}".format(stage)) + self.stage = MissingStage() - 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) - - logging.info("Reapplying settings to newly attached devices") - self.apply_settings(settings_full) + ### Locks + if hasattr(self.camera, "lock"): + self.lock.locks.append(self.camera.lock) + if hasattr(self.stage, "lock"): + self.lock.locks.append(self.stage.lock) def has_real_stage(self) -> bool: """ Check if a real (non-mock) stage is currently attached. """ - if hasattr(self, "stage") and not isinstance(self.stage, MockStage): + if hasattr(self, "stage") and not isinstance(self.stage, MissingStage): return True else: return False @@ -116,27 +124,26 @@ class Microscope: """ Check if a real (non-mock) camera is currently attached. """ - if hasattr(self, "camera") and not isinstance(self.camera, MockStreamer): + if hasattr(self, "camera") and not isinstance(self.camera, MissingCamera): return True else: return False # Create unified status @property - def status(self): + def state(self): """Dictionary of the basic microscope status. Return: dict: Dictionary containing complete microscope status """ state = { - "camera": self.camera.status, - "stage": self.stage.status, - "version": pkg_resources.get_distribution("openflexure_microscope").version, + "camera": self.camera.state, + "stage": self.stage.state, } return state - def apply_settings(self, config: dict): + def update_settings(self, config: dict): """ Applies a settings dictionary to the microscope. Missing parameters will be left untouched. """ @@ -144,21 +151,19 @@ class Microscope: # If attached to a camera if ("camera_settings" in config) and self.camera: - self.camera.apply_settings(config["camera_settings"]) + self.camera.update_settings(config["camera_settings"]) # If attached to a stage if ("stage_settings" in config) and self.stage: - self.stage.apply_settings(config["stage_settings"]) + self.stage.update_settings(config["stage_settings"]) # 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"] - def read_settings(self, json_safe=False): + def read_settings(self, full: bool=True): """ Get an updated settings dictionary. @@ -169,7 +174,7 @@ class Microscope: don't get removed from the settings file. """ - settings_current = {"id": self.id, "name": self.name, "fov": self.fov} + settings_current = {"name": self.name, "fov": self.fov} # If attached to a camera if self.camera: @@ -181,9 +186,12 @@ class Microscope: settings_current_stage = self.stage.read_settings() settings_current["stage_settings"] = settings_current_stage - settings_full = user_settings.merge(settings_current) + settings_full = self.settings_file.merge(settings_current) - return settings_full + if full: + return settings_full + else: + return settings_current def save_settings(self): """ @@ -196,7 +204,7 @@ class Microscope: self.camera.save_settings() if self.stage: self.stage.save_settings() - user_settings.save(current_config, backup=True) + self.settings_file.save(current_config, backup=True) @property def metadata(self): @@ -204,10 +212,10 @@ class Microscope: Microscope system metadata, to be applied to basically all captures """ system_metadata = { - "microscope_settings": self.read_settings(), - "microscope_state": self.status, - "microscope_id": self.id, - "microscope_name": self.name, + "@ID": self.id, + "settings": self.read_settings(full=False), + "state": self.state, + "configuration": self.configuration } # Store an encoded copy of the PiCamera lens shading table, if it exists @@ -217,10 +225,32 @@ class Microscope: b64_string, dtype, shape = serialise_array_b64(lst_arr) - system_metadata["lens_shading_table"] = { + system_metadata["configuration"]["detector"]["lens_shading_table"] = { "b64_string": b64_string, "dtype": dtype, "shape": shape, } return system_metadata + + @property + def configuration(self): + initial_configuration = self.configuration_file.load() + + current_configuration = { + "@application": { + "name": "openflexure_microscope", + "version": pkg_resources.get_distribution("openflexure_microscope").version + }, + "stage": { + "type": self.stage.__class__.__name__, + **self.stage.configuration + }, + "detector": { + "type": self.camera.__class__.__name__, + **self.camera.configuration + } + } + + initial_configuration.update(current_configuration) + return initial_configuration diff --git a/openflexure_microscope/microscope_configuration.default.json b/openflexure_microscope/microscope_configuration.default.json new file mode 100644 index 00000000..d7129bc8 --- /dev/null +++ b/openflexure_microscope/microscope_configuration.default.json @@ -0,0 +1,17 @@ +{ + "microscope": { + "stepsPerView": [4100, 3146] + }, + "detector": { + "type": "PiCamera" + }, + "stage": { + "type": "SangaStage", + "port": null + }, + "lightSource": { + "type": "LED" + }, + "objective": { + } +} \ No newline at end of file diff --git a/openflexure_microscope/microscope_settings.default.json b/openflexure_microscope/microscope_settings.default.json index bde62f47..fa0fcb28 100644 --- a/openflexure_microscope/microscope_settings.default.json +++ b/openflexure_microscope/microscope_settings.default.json @@ -1,7 +1,3 @@ { - "fov": [ - 4100, - 3146 - ], - "jpeg_quality": 75 + "jpeg_quality": 100 } \ No newline at end of file diff --git a/openflexure_microscope/paths.py b/openflexure_microscope/paths.py index a261d618..8fbf9b55 100644 --- a/openflexure_microscope/paths.py +++ b/openflexure_microscope/paths.py @@ -32,8 +32,9 @@ def logs_file_path(filename: str): HERE = os.path.abspath(os.path.dirname(__file__)) #: Path of default (first-run) microscope settings -DEFAULT_CONFIG_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json") - +DEFAULT_SETTINGS_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json") +#: Path of default (first-run) microscope configuration +DEFAULT_CONFIGURATION_FILE_PATH = os.path.join(HERE, "microscope_configuration.default.json") # BASE PATHS @@ -60,7 +61,9 @@ else: # SERVER PATHS -#: Path of microscope settings directory -CONFIG_FILE_PATH = settings_file_path("microscope_settings.json") +#: Path of microscope settings file +SETTINGS_FILE_PATH = settings_file_path("microscope_settings.json") +#: Path of microscope configuration file +CONFIGURATION_FILE_PATH = settings_file_path("microscope_configuration.json") #: Path of microscope extensions directory OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions") diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index d5ef31f3..564ba1a6 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -14,7 +14,7 @@ class BaseStage(metaclass=ABCMeta): self.lock = StrictLock(timeout=5) @abstractmethod - def apply_settings(self, config: dict): + def update_settings(self, config: dict): """Update settings from a config dictionary""" pass @@ -29,12 +29,14 @@ class BaseStage(metaclass=ABCMeta): @property @abstractmethod - def status(self): - """The general state dictionary of the board. - Should at least contain 'position', and 'board' keys. - Note: A None/Null value for 'board' will disable stage - movement in the OpenFlexure eV client software, - """ + def state(self): + """The general state dictionary of the board.""" + pass + + @property + @abstractmethod + def configuration(self): + """The general stage configuration.""" pass @property diff --git a/openflexure_microscope/stage/mock.py b/openflexure_microscope/stage/mock.py index e85fa43b..e4810f3c 100644 --- a/openflexure_microscope/stage/mock.py +++ b/openflexure_microscope/stage/mock.py @@ -7,7 +7,7 @@ import time import logging -class MockStage(BaseStage): +class MissingStage(BaseStage): def __init__(self, port=None, **kwargs): BaseStage.__init__(self) self._position = [0, 0, 0] @@ -17,19 +17,17 @@ class MockStage(BaseStage): self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis @property - def status(self): + def state(self): """The general status dictionary of the board.""" - status = { - "position": self.position_map, - "board": None, - "firmware": None, - "version": None, - } + status = {"position": self.position_map} return status - def apply_settings(self, config: dict): - """Update settings from a config dictionary""" + @property + def configuration(self): + return {} + def update_settings(self, config: dict): + """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels if "backlash" in config: # Construct backlash array diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 2c4fa6d9..11ffbbc6 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -23,6 +23,7 @@ class SangaStage(BaseStage): """Class managing serial communications with the motors for an Openflexure stage""" BaseStage.__init__(self) + self.port = port self.board = Sangaboard(port, **kwargs) self._backlash = ( @@ -31,14 +32,17 @@ class SangaStage(BaseStage): self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis @property - def status(self): + def state(self): """The general status dictionary of the board.""" - status = { - "position": self.position_map, + return {"position": self.position_map} + + @property + def configuration(self): + return { + "port": self.port, "board": self.board.board, "firmware": self.board.firmware, } - return status @property def n_axes(self): @@ -81,7 +85,7 @@ class SangaStage(BaseStage): else: self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) - def apply_settings(self, config: dict): + def update_settings(self, config: dict): """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels