def create_scheduler_from_config(observer=None, *args, **kwargs): """ Sets up the scheduler that will be used by the observatory """ logger = get_logger() scheduler_config = get_config('scheduler', default=None) logger.info(f'scheduler_config: {scheduler_config!r}') if scheduler_config is None or len(scheduler_config) == 0: logger.info("No scheduler in config") return None if not observer: logger.debug(f'No Observer provided, creating from config.') site_details = create_location_from_config() observer = site_details['observer'] scheduler_type = scheduler_config.get('type', 'dispatch') # Read the targets from the file fields_file = scheduler_config.get('fields_file', 'simple.yaml') fields_path = os.path.join(get_config('directories.targets'), fields_file) logger.debug(f'Creating scheduler: {fields_path}') if os.path.exists(fields_path): try: # Load the required module module = load_module(f'panoptes.pocs.scheduler.{scheduler_type}') obstruction_list = get_config('location.obstructions', default=[]) default_horizon = get_config('location.horizon', default=30 * u.degree) horizon_line = horizon_utils.Horizon( obstructions=obstruction_list, default_horizon=default_horizon.value) # Simple constraint for now constraints = [ Altitude(horizon=horizon_line), MoonAvoidance(), Duration(default_horizon, weight=5.) ] # Create the Scheduler instance scheduler = module.Scheduler(observer, fields_file=fields_path, constraints=constraints, *args, **kwargs) logger.debug("Scheduler created") except error.NotFound as e: raise error.NotFound(msg=e) else: raise error.NotFound( msg=f"Fields file does not exist: fields_file={fields_file!r}") return scheduler
def connect(self): """Connect to filter wheel.""" if self.is_connected: self.logger.warning("Already connected.") return # Scan for connected filterwheels. This needs to be done at least once before # opening a connectio. FilterWheel._filterwheels = self._driver.get_devices() if len(FilterWheel._filterwheels) == 0: msg = "No ZWO filterwheels found." self.logger.error(msg) raise error.NotFound(msg) if self._device_name: self.logger.debug( f"Looking for filterwheel with device name '{self._device_name}'." ) if self._device_name not in FilterWheel._filterwheels: msg = f"No filterwheel with device name '{self._device_name}'." self.logger.error(msg) raise error.NotFound(msg) if self._device_name in FilterWheel._assigned_filterwheels: msg = f"Filterwheel '{self._device_name}' already in use." self.logger.error(msg) raise error.PanError(msg) else: self.logger.debug( "No device name specified, claiming 1st available filterwheel." ) for device_name in FilterWheel._filterwheels: if device_name not in FilterWheel._assigned_filterwheels: self._device_name = device_name break if not self._device_name: msg = "All filterwheels already in use." self.logger.error(msg) raise error.PanError(msg) self.logger.debug(f"Claiming filterwheel '{self._device_name}'.") FilterWheel._assigned_filterwheels.add(self._device_name) self._handle = FilterWheel._filterwheels[self._device_name] self._driver.open(self._handle) info = self._driver.get_property(self._handle) self._model = info['name'] self._n_positions = info['slot_num'] if len(self.filter_names) != self.n_positions: msg = "Number of names in filter_names ({}) doesn't".format(len(self.filter_names)) + \ " match number of positions in filter wheel ({})".format(self.n_positions) self.logger.error(msg) raise ValueError(msg) self.logger.info("Filter wheel {} initialised".format(self)) self._connected = True
def load_module(module_name): """Dynamically load a module. >>> from panoptes.utils.library import load_module >>> error = load_module('panoptes.utils.error') >>> error.__name__ 'panoptes.utils.error' >>> error.__package__ 'panoptes.utils' Args: module_name (str): Name of module to import. Returns: module: an imported module name Raises: error.NotFound: If module cannot be imported. """ try: module = resolve_name(module_name) except ImportError: raise error.NotFound(msg=module_name) return module
def load_c_library(name, path=None, mode=ctypes.DEFAULT_MODE, **kwargs): """Utility function to load a shared/dynamically linked library (.so/.dylib/.dll). The name and location of the shared library can be manually specified with the library_path argument, otherwise the ctypes.util.find_library function will be used to try to locate based on library_name. Args: name (str): name of the library (without 'lib' prefix or any suffixes, e.g. 'fli'). path (str, optional): path to the library e.g. '/usr/local/lib/libfli.so'. mode (int, optional): mode in which to load the library, see dlopen(3) man page for details. Should be one of ctypes.RTLD_GLOBAL, ctypes.RTLD_LOCAL, or ctypes.DEFAULT_MODE. Default is ctypes.DEFAULT_MODE. Returns: ctypes.CDLL Raises: pocs.utils.error.NotFound: raised if library_path not given & find_library fails to locate the library. OSError: raises if the ctypes.CDLL loader cannot load the library. """ if mode is None: # Interpret a value of None as the default. mode = ctypes.DEFAULT_MODE # Open library logger.debug(f"Opening {name} library") if not path: path = ctypes.util.find_library(name) if not path: raise error.NotFound(f"Cound not find {name} library!") # This CDLL loader will raise OSError if the library could not be loaded return ctypes.CDLL(path, mode=mode)
def get_devices(self): """Get connected device 'UIDs' and corresponding device nodes/handles/IDs. EFW SDK has no way to access any unique identifier for connected filterwheels. Instead we construct an ID from combination of filterwheel name, number of positions and integer ID. This will probably not be deterministic, in general, and is only guaranteed to be unique between multiple filterwheels on a single computer. """ n_filterwheels = self.get_num( ) # Nothing works if you don't call this first. if n_filterwheels < 1: raise error.NotFound("No ZWO EFW filterwheels found.") filterwheels = {} for i in range(n_filterwheels): fw_id = self.get_ID(i) try: self.open(fw_id) except error.PanError as err: self.logger.error( f"Error opening filterwheel {fw_id}. {err!r}") else: info = self.get_property(fw_id) filterwheels[ f"{info['name']}_{info['slot_num']}_{fw_id}"] = fw_id finally: self.close(fw_id) if not filterwheels: self.logger.warning( "Could not get properties of any EFW filterwheels.") return filterwheels
def load_c_library(name, path=None, logger=None): """Utility function to load a shared/dynamically linked library (.so/.dylib/.dll). The name and location of the shared library can be manually specified with the library_path argument, otherwise the ctypes.util.find_library function will be used to try to locate based on library_name. Args: name (str): name of the library (without 'lib' prefix or any suffixes, e.g. 'fli'). path (str, optional): path to the library e.g. '/usr/local/lib/libfli.so'. Returns: ctypes.CDLL Raises: panoptes.utils.error.NotFound: raised if library_path not given & find_libary fails to locate the library. OSError: raises if the ctypes.CDLL loader cannot load the library. """ # Open library if logger: logger.debug("Opening {} library".format(name)) if not path: path = ctypes.util.find_library(name) if not path: raise error.NotFound("Cound not find {} library!".format(name)) # This CDLL loader will raise OSError if the library could not be loaded return ctypes.CDLL(path)
def list_connected_cameras(): """Detect connected cameras. Uses gphoto2 to try and detect which cameras are connected. Cameras should be known and placed in config but this is a useful utility. Returns: list: A list of the ports with detected cameras. """ gphoto2 = shutil.which('gphoto2') if not gphoto2: # pragma: no cover raise error.NotFound('The gphoto2 command is missing, please install.') command = [gphoto2, '--auto-detect'] result = subprocess.check_output(command) lines = result.decode('utf-8').split('\n') ports = [] for line in lines: camera_match = re.match(r'([\w\d\s_\.]{30})\s(usb:\d{3},\d{3})', line) if camera_match: # camera_name = camera_match.group(1).strip() port = camera_match.group(2).strip() ports.append(port) return ports
def move_to_dark_position(self, blocking=False): """ Move to filterwheel position for taking darks. """ try: self.logger.debug( f"Ensuring filterwheel {self} is at dark position.") return self.move_to(self._dark_position, blocking=blocking) except ValueError: msg = f"Request to move to dark position but {self} has no dark_position set." raise error.NotFound(msg)
def move_to_light_position(self, blocking=False): """ Return to last filterwheel position from before taking darks. """ try: self.logger.debug( f"Ensuring filterwheel {self} is not at dark position.") return self.move_to(self._last_light_position, blocking=blocking) except ValueError: msg = f"Request to revert to last light position but {self} has" + \ "no light position stored." raise error.NotFound(msg)
def get_product_ids(self): """Get product IDs of supported(?) EFW filterwheels. The SDK documentation does not explain what the product IDs returned by this function are, but from experiment and analogy with a similar function in the ASI camera SDK it appears this is a list of the product IDs of the filterwheels that the SDK supports, not the product IDs of the connected filterwheels. There appears to be no way to obtain the product IDs of the connected filterwheel(s). """ n_filterwheels = self._CDLL.EFWGetProductIDs(0) if n_filterwheels > 0: product_ids = (ctypes.c_int * n_filterwheels)() assert n_filterwheels == self._CDLL.EFWGetProductIDs( ctypes.byref(product_ids)) else: raise error.NotFound("No connected EFW filterwheels.") product_ids = list(product_ids) self.logger.debug(f"Got supported product IDs: {product_ids}") return product_ids
def find_serial_port(vendor_id, product_id, return_all=False): # pragma: no cover """Finds the serial port that matches the given vendor and product id. .. doctest:: >>> from panoptes.utils.rs232 import find_serial_port >>> vendor_id = 0x2a03 # arduino >>> product_id = 0x0043 # Uno Rev 3 >>> find_serial_port(vendor_id, product_id) # doctest: +SKIP '/dev/ttyACM0' >>> # Raises error when not found. >>> find_serial_port(0x1234, 0x4321) Traceback (most recent call last): ... panoptes.utils.error.NotFound: NotFound: No serial ports for vendor_id=4660 and product_id=17185 Args: vendor_id (int): The vendor id, can be hex or int. product_id (int): The product id, can be hex or int. return_all (bool): If more than one serial port matches, return all devices, default False. Returns: str or list: Either the path to the detected port or a list of all comports that match. """ # Get all serial ports. matched_ports = [ p for p in get_serial_port_info() if p.vid == vendor_id and p.pid == product_id ] if len(matched_ports) == 1: return matched_ports[0].device elif return_all: return matched_ports else: raise error.NotFound( f'No serial ports for vendor_id={vendor_id:x} and product_id={product_id:x}' )
def _create_subcomponent(self, class_path, subcomponent): """ Creates a subcomponent as an attribute of the camera. Can do this from either an instance of the appropriate subcomponent class, or from a dictionary of keyword arguments for the subcomponent class' constructor. Args: subcomponent (instance of sub_name | dict): the subcomponent object, or the keyword arguments required to create it. class_path (str): Full namespace of the subcomponent, e.g. 'panoptes.pocs.focuser.focuser.AbstractFocuser'. Returns: object: an instance of the subcomponent object. Raises: panoptes.utils.error.NotFound: Not found error. """ # Load the module for the subcomponent. sub_parts = class_path.split('.') base_class_name = sub_parts.pop() base_module_name = '.'.join(sub_parts) self.logger.debug(f'Loading base_module_name={base_module_name!r}') try: base_module = load_module(base_module_name) except error.NotFound as err: self.logger.critical( f"Couldn't import base_module_name={base_module_name!r}") raise err # Get the base class from the loaded module. self.logger.debug( f'Trying to get base_class_name={base_class_name!r} from {base_module}' ) base_class = getattr(base_module, base_class_name) # If we get an instance, just use it. if isinstance(subcomponent, base_class): self.logger.debug( f"subcomponent={subcomponent!r} is already a base_class={base_class!r} instance" ) # If we get a dict, use them as params to create instance. elif isinstance(subcomponent, dict): try: model = subcomponent['model'] self.logger.debug( f"subcomponent={subcomponent!r} is a dict but has model={model!r} keyword, " f"trying to create a base_class={base_class!r} instance") base_class = load_module(model) except (KeyError, error.NotFound) as err: raise error.NotFound( f"Can't create a class_path={class_path!r} from subcomponent={subcomponent!r}" ) self.logger.debug( f'Creating the base_class_name={base_class_name!r} object from dict' ) # Copy dict creation items and add the camera. subcomponent_kwargs = copy.deepcopy(subcomponent) subcomponent_kwargs.update({'camera': self}) try: subcomponent = base_class(**subcomponent_kwargs) except TypeError: raise error.NotFound( f'base_class={base_class!r} is not a callable class. ' f'Please specify full path to class (not module).') self.logger.success( f'{subcomponent} created for {base_class_name}') else: # Should have been passed either an instance of base_class or dict with subcomponent # configuration. Got something else... raise error.NotFound( f"Expected either a {base_class_name} instance or dict, got {subcomponent!r}" ) # Give the subcomponent a reference back to the camera. setattr(subcomponent, 'camera', self) return subcomponent
def create_cameras_from_config(config=None, cameras=None, auto_primary=True, recreate_existing=False, *args, **kwargs): """Create camera object(s) based on the config. Creates a camera for each camera item listed in the config. Ensures the appropriate camera module is loaded. Args: config (dict or None): A config object for a camera or None to lookup in config-server. cameras (list of panoptes.pocs.camera.Camera or None): A list of camera objects or None. auto_primary (bool): If True, when no camera is marked as the primary camera, the first camera in the list will be used as primary. Default True. recreate_existing (bool): If True, a camera object will be recreated if an existing camera with the same `uid` is already assigned. Should currently only affect cameras that use the `sdk` (i.g. not DSLRs). Default False raises an exception if camera is already assigned. *args (list): Passed to `get_config`. **kwargs (dict): Can pass a `cameras` object that overrides the info in the configuration file. Can also pass `auto_detect`(bool) to try and automatically discover the ports. Any other items as passed to `get_config`. Returns: OrderedDict: An ordered dictionary of created camera objects, with the camera name as key and camera instance as value. Returns an empty OrderedDict if there is no camera configuration items. Raises: error.CameraNotFound: Raised if camera cannot be found at specified port or if auto_detect=True and no cameras are found. error.PanError: Description """ camera_config = config or get_config('cameras', *args, **kwargs) if not camera_config: # cameras section either missing or empty logger.info('No camera information in config.') return None logger.debug(f"camera_config={camera_config!r}") camera_defaults = camera_config.get('defaults', dict()) cameras = cameras or OrderedDict() ports = list() auto_detect = camera_defaults.get('auto_detect', False) # Lookup the connected ports if auto_detect: logger.debug("Auto-detecting ports for cameras") try: ports = list_connected_cameras() except error.PanError as e: logger.warning(e) if len(ports) == 0: raise error.CameraNotFound(msg="No cameras detected. For testing, use camera simulator.") else: logger.debug(f"Detected ports={ports!r}") primary_camera = None device_info = camera_config['devices'] for cam_num, cfg in enumerate(device_info): # Get a copy of the camera defaults and update with device config. device_config = camera_defaults.copy() device_config.update(cfg) cam_name = device_config.setdefault('name', f'Cam{cam_num:02d}') # Check for proper connection method. model = device_config['model'] # Assign an auto-detected port. If none are left, skip if auto_detect: try: device_config['port'] = ports.pop() except IndexError: logger.warning(f"No ports left for {cam_name}, skipping.") continue elif model == 'simulator': device_config['port'] = f'usb:999,{random.randint(0, 1000):03d}' logger.debug(f'Creating camera: {model}') try: module = load_module(model) logger.debug(f'Camera module: module={module!r}') if recreate_existing: with suppress(AttributeError): module._assigned_cameras = set() # We either got a class or a module. if callable(module): camera = module(**device_config) else: if hasattr(module, 'Camera'): camera = module.Camera(**device_config) else: raise error.NotFound(f'module={module!r} does not have a Camera object') except error.NotFound: logger.error(f"Cannot find camera module with config: {device_config}") except Exception as e: logger.error(f"Cannot create camera type: {model} {e}") else: # Check if the config specified a primary camera and if it matches. if camera.uid == camera_config.get('primary'): camera.is_primary = True primary_camera = camera logger.debug(f"Camera created: camera={camera!r}") cameras[cam_name] = camera if len(cameras) == 0: raise error.CameraNotFound(msg="No cameras available") # If no camera was specified as primary use the first if primary_camera is None and auto_primary: logger.info(f'No primary camera given, assigning the first camera (auto_primary={auto_primary!r})') primary_camera = list(cameras.values())[0] # First camera primary_camera.is_primary = True logger.info(f"Primary camera: {primary_camera}") logger.success(f"{len(cameras)} cameras created") return cameras