Skip to content

Commit

Permalink
Add support for distributed Cameras (#80)
Browse files Browse the repository at this point in the history
* Big code dump from panoptes/POCS#547

* Fixing imports, paths, removed surplus camera tests

* Use right load_config, fix camera & aardvark tests

* Changes from review comments

* Move test_pyro_camera into test_huntsman.py, remove test_observatory.py

* Reduce duplication of code in camera creation

* Some missed tidying

* Small changes from re-review
  • Loading branch information
AnthonyHorton authored Oct 7, 2018
1 parent 7d5dc5e commit 4862faf
Show file tree
Hide file tree
Showing 13 changed files with 1,482 additions and 7 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ before_install:
- pip install -r requirements.txt
- python setup.py install
- ln -s $TRAVIS_BUILD_DIR $PANDIR/huntsman-pocs
- ssh-keygen -b 2048 -t rsa -f ~/.ssh/id_rsa -N ''
- touch ~/.ssh/authorized_keys
- cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
- ssh-keyscan -H localhost >> ~/.ssh/known_hosts
install:
- cd $TRAVIS_BUILD_DIR
- pip install -r requirements.txt
Expand Down
99 changes: 99 additions & 0 deletions conf_files/pyro_camera.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Config file for a distributed camera & focuser.
#
# Contains a default configuration with a simulated camera and focuser for testing
# and example configurations for other camera and focusers (commented out).
#
# DO NOT EDIT THIS FILE, INSTEAD COPY ONE OF THE EXAMPLES TO pyro_camera_local.yaml
# BEFORE EDITING AS REQUIRED.
#
# Simulated camera and focusers:
name: camera.simulator.001
host: localhost
camera:
model: simulator
port: /dev/ttyFAKE01
set_point: 0
filter_type: r2_6
focuser:
model: simulator
port: /dev/ttyFAKE02
initial_position: 21706
autofocus_keep_files: False
autofocus_range:
- 50
- 250
autofocus_step:
- 5
- 25
autofocus_seconds: 1
autofocus_size: 500
directories:
base: /var/panoptes
images: temp
data: data
#
# SBIG camera with Birger focuser:
# name: camera.sbig.001
# host: 999.999.999.999
# camera:
# model: sbig
# port: 83F012345
# set_point: 0
# filter_type: r2_6
# focuser:
# model: birger
# port: 12345
# initial_position: 21706
# autofocus_keep_files: False
# autofocus_range:
# - 50
# - 250
# autofocus_step:
# - 5
# - 25
# autofocus_seconds: 1
# autofocus_size: 1000
#
# FLI camera with Birger focuser:
# name: camera.fli.001
# host: 999.999.999.999
# camera:
# model: fli
# port: ML12345
# set_point: -10
# filter_type: r2_4
# focuser:
# model: birger
# port: 12345
# initial_position: 21706
# autofocus_keep_files: False
# autofocus_range:
# - 50
# - 250
# autofocus_step:
# - 5
# - 25
# autofocus_seconds: 1
# autofocus_size: 1000
#
# FLI Camera with FocusLynx focuser:
# name: camera.fli.002
# host: 999.999.999.999
# camera:
# model: fli
# port: ML12345
# set_point: -10
# filter_type: g2_0
# focuser:
# model: focuslynx
# port: /dev/ttyFAKE
# initial_position: 21706
# autofocus_keep_files: False
# autofocus_range:
# - 50
# - 250
# autofocus_step:
# - 5
# - 25
# autofocus_seconds: 1
# autofocus_size: 1000
171 changes: 171 additions & 0 deletions huntsman/camera/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from collections import OrderedDict
import re
import shutil
import subprocess

import Pyro4

from pocs.utils import error
from pocs.utils import load_module

from huntsman.utils import load_config

from pocs.camera import list_connected_cameras
from pocs.camera import create_cameras_from_config as create_local_cameras
from pocs.camera.camera import AbstractCamera # pragma: no flakes
from pocs.camera.camera import AbstractGPhotoCamera # pragma: no flakes

from huntsman.camera.pyro import Camera as PyroCamera

from pocs.utils import logger as logger_module


def list_distributed_cameras(ns_host=None, logger=None):
"""Detect distributed cameras.
Looks for a Pyro name server and queries it for the list of registered cameras.
Args:
host (str, optional): hostname or IP address of the name server host. If not given
will attempt to locate the name server via UDP network broadcast.
logger (logging.Logger, optional): logger to use for messages, if not given will
use the root logger.
Returns:
dict: Dictionary of detected distributed camera name, URI pairs
"""
if not logger:
logger = logger_module.get_root_logger()

try:
# Get a proxy for the name server (will raise NamingError if not found)
with Pyro4.locateNS(host=ns_host) as name_server:
# Find all the registered POCS cameras
camera_uris = name_server.list(metadata_all={'POCS', 'Camera'})
camera_uris = OrderedDict(sorted(camera_uris.items(), key=lambda t: t[0]))
n_cameras = len(camera_uris)
if n_cameras > 0:
msg = "Found {} distributed cameras on name server".format(n_cameras)
logger.debug(msg)
else:
msg = "Found name server but no distributed cameras"
logger.warning(msg)
except Pyro4.errors.NamingError as err:
msg = "Couldn't connect to Pyro name server: {}".format(err)
logger.warning(msg)
camera_uris = OrderedDict()

return camera_uris


def create_cameras_from_config(config=None, logger=None, **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:
**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.
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
"""
if not logger:
logger = logger_module.get_root_logger()

if not config:
config = load_config(**kwargs)

# Helper method to first check kwargs then config
def kwargs_or_config(item, default=None):
return kwargs.get(item, config.get(item, default))

camera_info = kwargs_or_config('cameras')
if not camera_info:
logger.info('No camera information in config.')
return OrderedDict()

try:
cameras = create_local_cameras(config=config, logger=logger, **kwargs)
except (error.PanError, KeyError, error.CameraNotFound):
logger.debug("No local cameras")
cameras = OrderedDict()

a_simulator = 'camera' in kwargs_or_config('simulator', default=list())
distributed_cameras = kwargs.get('distributed_cameras',
camera_info.get('distributed_cameras', False))
if not a_simulator and distributed_cameras:
logger.debug("Creating distributed cameras")
cameras.update(create_distributed_cameras(camera_info, logger=logger))

if len(cameras) == 0:
raise error.CameraNotFound(
msg="No cameras available. Exiting.", exit=True)

# Find primary camera
primary_camera = None
for camera in cameras.values():
if camera.is_primary:
primary_camera = camera

# If no camera was specified as primary use the first
if primary_camera is None:
camera_names = sorted(cameras.keys())
primary_camera = cameras[camera_names[0]]
primary_camera.is_primary = True

logger.debug("Primary camera: {}", primary_camera)
logger.debug("{} cameras created", len(cameras))

return cameras


def create_distributed_cameras(camera_info, logger=None):
"""Create distributed camera object(s) based on detected cameras and config
Creates a `pocs.camera.pyro.Camera` object for each distributed camera detected.
Args:
camera_info: 'cameras' section from POCS config
logger (logging.Logger, optional): logger to use for messages, if not given will
use the root logger.
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 no distributed cameras are found.
"""
if not logger:
logger = logger_module.get_root_logger()

# Get all distributed cameras
camera_uris = list_distributed_cameras(ns_host=camera_info.get('name_server_host', None),
logger=logger)

# Create the camera objects.
# TODO: do this in parallel because initialising cameras can take a while.
cameras = OrderedDict()
primary_id = camera_info.get('primary', '')
for cam_name, cam_uri in camera_uris.items():
logger.debug('Creating camera: {}'.format(cam_name))
cam = PyroCamera(name=cam_name, uri=cam_uri)
is_primary = ''
if primary_id == cam.uid or primary_id == cam.name:
cam.is_primary = True
is_primary = ' [Primary]'

logger.debug("Camera created: {} {}{}".format(
cam.name, cam.uid, is_primary))

cameras[cam_name] = cam

return cameras
Loading

0 comments on commit 4862faf

Please sign in to comment.