Draft of new semantics

This commit is contained in:
Joel Collins 2020-01-29 11:55:39 +00:00
parent b39a0653f2
commit 19af98736d
13 changed files with 252 additions and 187 deletions

View file

@ -3,38 +3,9 @@ from openflexure_microscope.camera.capture import build_captures_from_exif
import logging 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() 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 # Restore loaded capture array to camera object
logging.debug("Restoring captures...") logging.debug("Restoring captures...")
default_microscope.camera.images = build_captures_from_exif( default_microscope.camera.images = build_captures_from_exif(

View file

@ -30,7 +30,7 @@ class SettingsProperty(View):
logging.debug("Updating settings from PUT request:") logging.debug("Updating settings from PUT request:")
logging.debug(payload.json) logging.debug(payload.json)
microscope.apply_settings(payload.json) microscope.update_settings(payload.json)
microscope.save_settings() microscope.save_settings()
return self.get() return self.get()
@ -65,7 +65,7 @@ class NestedSettingsProperty(View):
dictionary = create_from_path(keys) dictionary = create_from_path(keys)
set_by_path(dictionary, keys, payload.json) set_by_path(dictionary, keys, payload.json)
microscope.apply_settings(dictionary) microscope.update_settings(dictionary)
microscope.save_settings() microscope.save_settings()
return self.get(route) return self.get(route)

View file

@ -121,7 +121,8 @@ class BaseCamera(metaclass=ABCMeta):
self.stream_timeout = 20 self.stream_timeout = 20
self.stream_timeout_enabled = False 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} self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
@ -129,8 +130,24 @@ class BaseCamera(metaclass=ABCMeta):
self.images = [] self.images = []
self.videos = [] self.videos = []
@property
@abstractmethod @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""" """Update settings from a config dictionary"""
with self.lock: with self.lock:
# Apply valid config params to camera object # Apply valid config params to camera object
@ -187,7 +204,7 @@ class BaseCamera(metaclass=ABCMeta):
self.last_access = time.time() self.last_access = time.time()
self.stop = False self.stop = False
if not self.status["stream_active"]: if not self.stream_active:
# start background frame thread # start background frame thread
self.thread = threading.Thread(target=self._thread) self.thread = threading.Thread(target=self._thread)
self.thread.daemon = True self.thread.daemon = True
@ -207,12 +224,12 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("Stopping worker thread") logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
if self.status["stream_active"]: if self.stream_active:
self.stop = True self.stop = True
self.thread.join() # Wait for stream thread to exit self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting 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: if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.") logging.debug("Timeout waiting for worker thread close.")
raise TimeoutError("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() self.frames_iterator = self.frames()
logging.debug("Entering worker thread.") logging.debug("Entering worker thread.")
self.status["stream_active"] = True self.stream_active = True
for frame in self.frames_iterator: for frame in self.frames_iterator:
self.frame = frame self.frame = frame
@ -365,9 +382,7 @@ class BaseCamera(metaclass=ABCMeta):
and ( # If using timeout and ( # If using timeout
time.time() - self.last_access > self.stream_timeout time.time() - self.last_access > self.stream_timeout
) )
and not self.status[ # And timeout time and not self.preview_active # And GPU preview is not active
"preview_active"
] # And GPU preview is not active
): ):
self.frames_iterator.close() self.frames_iterator.close()
break break
@ -383,4 +398,4 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("BaseCamera worker thread exiting...") logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state # Set stream_activate state
self.status["stream_active"] = False self.stream_active = False

View file

@ -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 = logging.getLogger("PIL")
pil_logger.setLevel(logging.INFO) pil_logger.setLevel(logging.INFO)
# MAIN CLASS # MAIN CLASS
class MockStreamer(BaseCamera): class MissingCamera(BaseCamera):
def __init__(self): def __init__(self):
# Run BaseCamera init # Run BaseCamera init
BaseCamera.__init__(self) BaseCamera.__init__(self)
# Store state of PiCameraStreamer
self.status.update(
{"stream_active": False, "record_active": False, "board": None}
)
# Update config properties # Update config properties
self.image_resolution = (1312, 976) self.image_resolution = (1312, 976)
self.stream_resolution = (640, 480) self.stream_resolution = (640, 480)
@ -71,6 +67,16 @@ class MockStreamer(BaseCamera):
image.save(self.stream, format="JPEG") 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): def initialisation(self):
"""Run any initialisation code when the frame iterator starts.""" """Run any initialisation code when the frame iterator starts."""
pass pass
@ -101,7 +107,7 @@ class MockStreamer(BaseCamera):
return conf_dict return conf_dict
def apply_settings(self, config: dict): def update_settings(self, config: dict):
""" """
Write a config dictionary to the PiCameraStreamer config. Write a config dictionary to the PiCameraStreamer config.
@ -117,7 +123,7 @@ class MockStreamer(BaseCamera):
with self.lock: with self.lock:
# Apply valid config params to camera object # 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 for key, value in config.items(): # For each provided setting
if hasattr(self, key): if hasattr(self, key):

View file

@ -47,6 +47,7 @@ from .base import BaseCamera, CaptureObject
from .set_picamera_gain import set_analog_gain, set_digital_gain from .set_picamera_gain import set_analog_gain, set_digital_gain
from openflexure_microscope.paths import settings_file_path from openflexure_microscope.paths import settings_file_path
from openflexure_microscope.utilities import serialise_array_b64
# MAIN CLASS # MAIN CLASS
@ -85,14 +86,7 @@ class PiCameraStreamer(BaseCamera):
) #: :py:class:`picamera.PiCamera`: Picamera object ) #: :py:class:`picamera.PiCamera`: Picamera object
# Store status of PiCameraStreamer # Store status of PiCameraStreamer
self.status.update( self.preview_active = False
{
"stream_active": False,
"record_active": False,
"preview_active": False,
"board": f"picamera_{self.camera.revision}",
}
)
# Reset variable states # Reset variable states
self.set_zoom(1.0) self.set_zoom(1.0)
@ -116,15 +110,37 @@ class PiCameraStreamer(BaseCamera):
"picamera_lst.npy" "picamera_lst.npy"
) #: str: Path of .npy lens shading table file ) #: str: Path of .npy lens shading table file
# Update board identifier
self.status.update({})
# Create an empty stream # Create an empty stream
self.stream = io.BytesIO() self.stream = io.BytesIO()
# Start streaming # Start streaming
self.start_worker() 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): def initialisation(self):
"""Run any initialisation code when the frame iterator starts.""" """Run any initialisation code when the frame iterator starts."""
pass pass
@ -176,7 +192,7 @@ class PiCameraStreamer(BaseCamera):
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path)) logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
self.save_lens_shading_table() 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. Write a config dictionary to the PiCameraStreamer config.
@ -194,10 +210,10 @@ class PiCameraStreamer(BaseCamera):
with self.lock: with self.lock:
# Apply valid config params to Picamera object # 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 # 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.") logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done paused_stream = True # Remember to unpause stream when done
@ -365,7 +381,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.preview.window = window self.camera.preview.window = window
if fullscreen: if fullscreen:
self.camera.preview.fullscreen = fullscreen self.camera.preview.fullscreen = fullscreen
self.status["preview_active"] = True self.preview_active = True
except picamera.exc.PiCameraMMALError as e: except picamera.exc.PiCameraMMALError as e:
logging.error( logging.error(
"Suppressed a MMALError in start_preview. Exception: {}".format(e) "Suppressed a MMALError in start_preview. Exception: {}".format(e)
@ -380,7 +396,7 @@ class PiCameraStreamer(BaseCamera):
def stop_preview(self): def stop_preview(self):
"""Stop the on board GPU camera preview.""" """Stop the on board GPU camera preview."""
self.camera.stop_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): def start_recording(self, output, fmt: str = "h264", quality: int = 15):
"""Start recording. """Start recording.
@ -398,7 +414,7 @@ class PiCameraStreamer(BaseCamera):
""" """
with self.lock: with self.lock:
# Start recording method only if a current recording is not running # 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 # Start the camera video recording on port 2
logging.info("Recording to {}".format(output)) logging.info("Recording to {}".format(output))
@ -412,7 +428,7 @@ class PiCameraStreamer(BaseCamera):
) )
# Update status dictionary # Update status dictionary
self.status["record_active"] = True self.record_active = True
return output return output
@ -432,7 +448,7 @@ class PiCameraStreamer(BaseCamera):
logging.info("Recording stopped") logging.info("Recording stopped")
# Update status dictionary # Update status dictionary
self.status["record_active"] = False self.record_active = False
def stop_stream_recording( def stop_stream_recording(
self, splitter_port: int = 1, resolution: Tuple[int, int] = None self, splitter_port: int = 1, resolution: Tuple[int, int] = None
@ -500,7 +516,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.resolution = resolution self.camera.resolution = resolution
# If the stream should be active # If the stream should be active
if self.status["stream_active"]: if self.stream_active:
try: try:
# Start recording on stream port # Start recording on stream port
self.camera.start_recording( self.camera.start_recording(

View file

@ -7,7 +7,7 @@ from uuid import UUID
import numpy as np import numpy as np
from fractions import Fraction 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: class OpenflexureSettingsFile:
@ -19,21 +19,21 @@ class OpenflexureSettingsFile:
expand (bool): Expand paths to valid auxillary config files. expand (bool): Expand paths to valid auxillary config files.
""" """
def __init__(self, config_path: str = None): def __init__(self, path: str, defaults: dict = {}):
global DEFAULT_CONFIG, CONFIG_FILE_PATH global DEFAULT_SETTINGS
# Set arguments # 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 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: def load(self) -> dict:
""" """
Loads settings from a file on-disk. Loads settings from a file on-disk.
""" """
# Unexpanded config dictionary (used at load/save time) # 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") logging.debug("Reading settings from disk")
return loaded_config return loaded_config
@ -50,11 +50,11 @@ class OpenflexureSettingsFile:
save_settings = config save_settings = config
if backup: if backup:
if os.path.isfile(self.config_path): if os.path.isfile(self.path):
shutil.copyfile(self.config_path, self.config_path + ".bk") shutil.copyfile(self.path, self.path + ".bk")
logging.debug("Saving settings dictionary to disk") 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: def merge(self, config: dict) -> dict:
""" """
@ -178,10 +178,17 @@ def initialise_file(config_path, populate: str = "{}\n"):
outfile.write(populate) outfile.write(populate)
# Load the default config # Load the default settings
with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc: with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
DEFAULT_CONFIG = default_rc.read() DEFAULT_SETTINGS = default_settings.read()
#: Default user settings object #: 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)

View file

@ -6,15 +6,20 @@ import logging
import pkg_resources import pkg_resources
import uuid import uuid
from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.stage.mock import MissingStage
from openflexure_microscope.stage.mock import MockStage from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.stage.sanga import SangaStage
from openflexure_microscope.camera.mock import MockStreamer 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.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.lock import CompositeLock
from labthings.core.utilities import rupdate
class Microscope: class Microscope:
@ -24,20 +29,32 @@ class Microscope:
The camera and stage objects may already be initialised, and can be passed as arguments. The camera and stage objects may already be initialised, and can be passed as arguments.
""" """
def __init__(self): def __init__(self, settings = user_settings, configuration = user_configuration):
# Initial attributes # Store settings and configuration files
self.id = uuid.uuid4() #: Microscope UUID self.settings_file = settings
self.name = self.id #: Microscope name (modifiable) self.configuration_file = configuration
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
# Initialise with an empty composite lock # Initialise with an empty composite lock
#: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage #: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage
self.lock = CompositeLock([]) 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 # 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): def __enter__(self):
"""Create microscope on context enter.""" """Create microscope on context enter."""
@ -56,58 +73,49 @@ class Microscope:
self.stage.close() self.stage.close()
logging.info("Closed {}".format(self)) 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. Attach microscope components based on initially passed configuration file
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
""" """
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...") ### Stage
self.camera = ( if configuration.get("stage"):
camera stage_type = configuration["stage"].get("type")
) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object 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: if not self.camera:
logging.info("No camera attached.") self.camera = MissingCamera()
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
if not self.stage: if not self.stage:
logging.info("No stage attached.") self.stage = MissingStage()
else:
logging.info("Attached stage {}".format(stage))
if hasattr(self.stage, "lock"): # If stage object has a lock ### Locks
logging.info( if hasattr(self.camera, "lock"):
"Attaching lock {} to composite lock.".format(self.stage.lock) self.lock.locks.append(self.camera.lock)
) if hasattr(self.stage, "lock"):
# Add the lock to the microscope composite lock self.lock.locks.append(self.stage.lock)
self.lock.locks.append(self.stage.lock)
logging.info("Reapplying settings to newly attached devices")
self.apply_settings(settings_full)
def has_real_stage(self) -> bool: def has_real_stage(self) -> bool:
""" """
Check if a real (non-mock) stage is currently attached. 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 return True
else: else:
return False return False
@ -116,27 +124,26 @@ class Microscope:
""" """
Check if a real (non-mock) camera is currently attached. 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 return True
else: else:
return False return False
# Create unified status # Create unified status
@property @property
def status(self): def state(self):
"""Dictionary of the basic microscope status. """Dictionary of the basic microscope status.
Return: Return:
dict: Dictionary containing complete microscope status dict: Dictionary containing complete microscope status
""" """
state = { state = {
"camera": self.camera.status, "camera": self.camera.state,
"stage": self.stage.status, "stage": self.stage.state,
"version": pkg_resources.get_distribution("openflexure_microscope").version,
} }
return 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. 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 attached to a camera
if ("camera_settings" in config) and self.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 attached to a stage
if ("stage_settings" in config) and self.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 # Todo: tidy up with some loopy goodness
if "id" in config:
self.id = config["id"]
if "name" in config: if "name" in config:
self.name = config["name"] self.name = config["name"]
if "fov" in config: if "fov" in config:
self.fov = config["fov"] self.fov = config["fov"]
def read_settings(self, json_safe=False): def read_settings(self, full: bool=True):
""" """
Get an updated settings dictionary. Get an updated settings dictionary.
@ -169,7 +174,7 @@ class Microscope:
don't get removed from the settings file. 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 attached to a camera
if self.camera: if self.camera:
@ -181,9 +186,12 @@ class Microscope:
settings_current_stage = self.stage.read_settings() settings_current_stage = self.stage.read_settings()
settings_current["stage_settings"] = settings_current_stage 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): def save_settings(self):
""" """
@ -196,7 +204,7 @@ class Microscope:
self.camera.save_settings() self.camera.save_settings()
if self.stage: if self.stage:
self.stage.save_settings() self.stage.save_settings()
user_settings.save(current_config, backup=True) self.settings_file.save(current_config, backup=True)
@property @property
def metadata(self): def metadata(self):
@ -204,10 +212,10 @@ class Microscope:
Microscope system metadata, to be applied to basically all captures Microscope system metadata, to be applied to basically all captures
""" """
system_metadata = { system_metadata = {
"microscope_settings": self.read_settings(), "@ID": self.id,
"microscope_state": self.status, "settings": self.read_settings(full=False),
"microscope_id": self.id, "state": self.state,
"microscope_name": self.name, "configuration": self.configuration
} }
# Store an encoded copy of the PiCamera lens shading table, if it exists # 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) 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, "b64_string": b64_string,
"dtype": dtype, "dtype": dtype,
"shape": shape, "shape": shape,
} }
return system_metadata 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

View file

@ -0,0 +1,17 @@
{
"microscope": {
"stepsPerView": [4100, 3146]
},
"detector": {
"type": "PiCamera"
},
"stage": {
"type": "SangaStage",
"port": null
},
"lightSource": {
"type": "LED"
},
"objective": {
}
}

View file

@ -1,7 +1,3 @@
{ {
"fov": [ "jpeg_quality": 100
4100,
3146
],
"jpeg_quality": 75
} }

View file

@ -32,8 +32,9 @@ def logs_file_path(filename: str):
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
#: Path of default (first-run) microscope settings #: 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 # BASE PATHS
@ -60,7 +61,9 @@ else:
# SERVER PATHS # SERVER PATHS
#: Path of microscope settings directory #: Path of microscope settings file
CONFIG_FILE_PATH = settings_file_path("microscope_settings.json") 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 #: Path of microscope extensions directory
OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions") OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")

View file

@ -14,7 +14,7 @@ class BaseStage(metaclass=ABCMeta):
self.lock = StrictLock(timeout=5) self.lock = StrictLock(timeout=5)
@abstractmethod @abstractmethod
def apply_settings(self, config: dict): def update_settings(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
pass pass
@ -29,12 +29,14 @@ class BaseStage(metaclass=ABCMeta):
@property @property
@abstractmethod @abstractmethod
def status(self): def state(self):
"""The general state dictionary of the board. """The general state dictionary of the board."""
Should at least contain 'position', and 'board' keys. pass
Note: A None/Null value for 'board' will disable stage
movement in the OpenFlexure eV client software, @property
""" @abstractmethod
def configuration(self):
"""The general stage configuration."""
pass pass
@property @property

View file

@ -7,7 +7,7 @@ import time
import logging import logging
class MockStage(BaseStage): class MissingStage(BaseStage):
def __init__(self, port=None, **kwargs): def __init__(self, port=None, **kwargs):
BaseStage.__init__(self) BaseStage.__init__(self)
self._position = [0, 0, 0] self._position = [0, 0, 0]
@ -17,19 +17,17 @@ class MockStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property @property
def status(self): def state(self):
"""The general status dictionary of the board.""" """The general status dictionary of the board."""
status = { status = {"position": self.position_map}
"position": self.position_map,
"board": None,
"firmware": None,
"version": None,
}
return status return status
def apply_settings(self, config: dict): @property
"""Update settings from a config dictionary""" def configuration(self):
return {}
def update_settings(self, config: dict):
"""Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels # Set backlash. Expects a dictionary with axis labels
if "backlash" in config: if "backlash" in config:
# Construct backlash array # Construct backlash array

View file

@ -23,6 +23,7 @@ class SangaStage(BaseStage):
"""Class managing serial communications with the motors for an Openflexure stage""" """Class managing serial communications with the motors for an Openflexure stage"""
BaseStage.__init__(self) BaseStage.__init__(self)
self.port = port
self.board = Sangaboard(port, **kwargs) self.board = Sangaboard(port, **kwargs)
self._backlash = ( self._backlash = (
@ -31,14 +32,17 @@ class SangaStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property @property
def status(self): def state(self):
"""The general status dictionary of the board.""" """The general status dictionary of the board."""
status = { return {"position": self.position_map}
"position": self.position_map,
@property
def configuration(self):
return {
"port": self.port,
"board": self.board.board, "board": self.board.board,
"firmware": self.board.firmware, "firmware": self.board.firmware,
} }
return status
@property @property
def n_axes(self): def n_axes(self):
@ -81,7 +85,7 @@ class SangaStage(BaseStage):
else: else:
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) 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""" """Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels # Set backlash. Expects a dictionary with axis labels