Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use entry points for services and service proxies #251

Merged
merged 12 commits into from
Nov 12, 2024
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/bmc_dm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from astropy.io import fits
import hcipy

@ServiceProxy.register_service_interface('bmc_dm')

class BmcDmProxy(ServiceProxy):
@property
def dm_mask(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import warnings

@ServiceProxy.register_service_interface('camera')

class CameraProxy(ServiceProxy):
def take_raw_exposures(self, num_exposures):
was_acquiring = self.is_acquiring.get()[0]
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/deformable_mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from astropy.io import fits
import hcipy

@ServiceProxy.register_service_interface('deformable_mirror')

class DeformableMirrorProxy(ServiceProxy):
@property
def device_actuator_mask(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/flip_mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

@ServiceProxy.register_service_interface('flip_mount')

class FlipMountProxy(ServiceProxy):
def move_to(self, position, wait=True):
position = self.resolve_position(position)
Expand Down
1 change: 0 additions & 1 deletion catkit2/testbed/proxies/newport_picomotor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
MAX_TIMEOUT_FOR_CHECKING = 1000 # ms


@ServiceProxy.register_service_interface('newport_picomotor')
class NewportPicomotorProxy(ServiceProxy):
log = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/newport_xps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

@ServiceProxy.register_service_interface('newport_xps_q8')

class NewportXpsQ8Proxy(ServiceProxy):
def move_absolute(self, motor_id, position, timeout=None):
command_stream = getattr(self, motor_id.lower() + '_command')
Expand Down
1 change: 0 additions & 1 deletion catkit2/testbed/proxies/ni_daq.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from ..service_proxy import ServiceProxy


@ServiceProxy.register_service_interface('ni_daq')
class NiDaqProxy(ServiceProxy):
def apply_voltage(self, channel, voltage, timeout=None):
getattr(self, channel).submit_data(voltage)
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/nkt_superk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from ..service_proxy import ServiceProxy

@ServiceProxy.register_service_interface('nkt_superk')

class NktSuperkProxy(ServiceProxy):
@property
def center_wavelength(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/oceanoptics_spectrometer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ..service_proxy import ServiceProxy

@ServiceProxy.register_service_interface('oceanoptics_spectrometer')

class OceanopticsSpectroProxy(ServiceProxy):

def take_raw_exposures(self, num_exposures):
Expand Down
1 change: 0 additions & 1 deletion catkit2/testbed/proxies/thorlabs_cube_motor_kinesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import numpy as np


@ServiceProxy.register_service_interface('thorlabs_cube_motor_kinesis')
class ThorlabsCubeMotorKinesisProxy(ServiceProxy):
def move_absolute(self, position):
position = self.resolve_position(position)
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/thorlabs_mcls1.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ..service_proxy import ServiceProxy

@ServiceProxy.register_service_interface('thorlabs_mcls1')

class ThorlabsMcls1(ServiceProxy):
@property
def channel(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/web_power_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

@ServiceProxy.register_service_interface('web_power_switch')

class WebPowerSwitchProxy(ServiceProxy):
def switch(self, outlet_name, on):
if outlet_name.lower() not in self.outlets:
Expand Down
42 changes: 15 additions & 27 deletions catkit2/testbed/service_proxy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from .. import catkit_bindings

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata


class ServiceProxy(catkit_bindings.ServiceProxy):
'''A proxy for a service connected to a server.

Expand Down Expand Up @@ -90,38 +96,20 @@ def get_service_interface(cls, interface_name):

Parameters
----------
interface_name : string
interface_name : string or None
The name of the interface.

Returns
-------
derived class of ServiceProxy or ServiceProxy
The class belonging to the interface name.
'''
if interface_name in cls._service_interfaces:
return cls._service_interfaces[interface_name]
elif interface_name is None:
return cls
else:
raise AttributeError(f"Service proxy class with interface name '{interface_name}' not found. Did you import it?")

@classmethod
def register_service_interface(cls, interface_name):
'''Register a ServiceProxy derived class.
if interface_name is None:
return ServiceProxy

Parameters
----------
interface_name : string
The name of the interface.

Returns
-------
class decorator
For decorating your ServiceProxy derived class with.
'''
def decorator(interface_class):
cls._service_interfaces[interface_name] = interface_class

return interface_class

return decorator
entry_points = importlib_metadata.entry_points()['catkit2.proxies']
for entry_point in entry_points:
if entry_point.name == interface_name:
return entry_point.load()
else:
raise AttributeError(f"Service proxy class with interface name '{interface_name}' not found. Did you set it as an entry point?")
57 changes: 33 additions & 24 deletions catkit2/testbed/testbed.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import socket
import threading
import contextlib
import importlib

import psutil
import zmq
Expand All @@ -18,6 +19,11 @@
from ..proto import testbed_pb2 as testbed_proto
from ..proto import service_pb2 as service_proto

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata


SERVICE_LIVELINESS = 5

Expand Down Expand Up @@ -213,10 +219,6 @@ def __init__(self, port, is_simulated, config):

self.log = logging.getLogger(__name__)

self.service_paths = [os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'services'))]
if 'service_paths' in self.config['testbed']:
self.service_paths.extend(self.config['testbed']['service_paths'])

self.startup_services = []
if 'safety' in self.config['testbed']:
self.startup_services.append(self.config['testbed']['safety']['service_id'])
Expand Down Expand Up @@ -274,6 +276,14 @@ def __init__(self, port, is_simulated, config):
for service_id in shut_down_list:
services_to_shut_down.remove(service_id)

# Read in service types.
self.service_type_paths = {}
for entry_point in importlib_metadata.entry_points()["catkit2.services"]:
module = entry_point.module
spec = importlib.util.find_spec(module)
path = os.path.abspath(spec.origin)
self.register_service_type(entry_point.name, path)

# Create server instance and register request handlers.
self.server = Server(port)

Expand Down Expand Up @@ -575,6 +585,18 @@ def on_shut_down(self, data):
reply = testbed_proto.ShutDownReply()
return reply.SerializeToString()

def register_service_type(self, service_type, path):
'''Register a service type.

Parameters
----------
service_type : str
The service type.
path : str
The path to the Python file to run for this service.
'''
self.service_type_paths[service_type] = path

def start_service(self, service_id):
'''Start a service.

Expand Down Expand Up @@ -607,19 +629,11 @@ def start_service(self, service_id):
service_type = self.services[service_id].service_type

# Resolve service type;
dirname = self.resolve_service_type(service_type)

# Find if Python or C++.
if os.path.exists(os.path.join(dirname, service_type + '.py')):
executable = [sys.executable, os.path.join(dirname, service_type + '.py')]
elif os.path.exists(os.path.join(dirname, service_type + '.exe')):
executable = [os.path.join(dirname, service_type + '.exe')]
elif os.path.exists(os.path.join(dirname, service_type)):
executable = [os.path.join(dirname, service_type)]
else:
self.log.warning(f"Could not find the script/executable for service type \"{service_type}\".")
path = self.resolve_service_type(service_type)
dirname = os.path.dirname(path)

raise RuntimeError(f"Service '{service_id}' is not Python or C++.")
# Build Python executable command.
executable = [sys.executable, path]

# Get unused port for this service.
port = get_unused_port()
Expand Down Expand Up @@ -743,17 +757,12 @@ def resolve_service_type(self, service_type):
Returns
-------
string
The path to where the Python script or executable for the
service can be found.
The path to the Python script of the service.
'''
for base_path in self.service_paths:
dirname = os.path.join(base_path, service_type)
if os.path.exists(dirname):
break
else:
if service_type not in self.service_type_paths:
raise ValueError(f"Service type '{service_type}' not recognized.")

return dirname
return self.service_type_paths[service_type]

def shut_down_all_services(self):
'''Shut down all running services.
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies:
- pytest
- flake8
- h5py
- importlib_metadata
- pip:
- dcps
- zwoasi>=0.0.21
63 changes: 63 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,67 @@ def build_extension(self, ext):
cmdclass=dict(build_ext=CMakeBuild),
zip_safe=False,
install_requires=[],
entry_points={
'catkit2.services': [
'aimtti_plp_device = catkit2.services.aimtti_plp_device.aimtti_plp_device',
'aimtti_plp_device_sim = catkit2.services.aimtti_plp_device_sim.aimtti_plp_device_sim',
'allied_vision_camera = catkit2.services.allied_vision_camera.allied_vision_camera',
'bmc_deformable_mirror_hardware = catkit2.services.bmc_deformable_mirror_hardware.bmc_deformable_mirror_hardware',
'bmc_deformable_mirror_sim = catkit2.services.bmc_deformable_mirror_sim.bmc_deformable_mirror_sim',
'bmc_dm = catkit2.services.bmc_dm.bmc_dm',
'bmc_dm_sim = catkit2.services.bmc_dm_sim.bmc_dm_sim',
'camera_sim = catkit2.services.camera_sim.camera_sim',
'dummy_camera = catkit2.services.dummy_camera.dummy_camera',
'empty_service = catkit2.services.empty_service.empty_service',
'flir_camera = catkit2.services.flir_camera.flir_camera',
'hamamatsu_camera = catkit2.services.hamamatsu_camera.hamamatsu_camera',
'newport_picomotor = catkit2.services.newport_picomotor.newport_picomotor',
'newport_picomotor_sim = catkit2.services.newport_picomotor_sim.newport_picomotor_sim',
'newport_xps_q8 = catkit2.services.newport_xps_q8.newport_xps_q8',
'newport_xps_q8_sim = catkit2.services.newport_xps_q8_sim.newport_xps_q8_sim',
'ni_daq = catkit2.services.ni_daq.ni_daq',
'ni_daq_sim = catkit2.services.ni_daq_sim.ni_daq_sim',
'nkt_superk = catkit2.services.nkt_superk.nkt_superk',
'nkt_superk_sim = catkit2.services.nkt_superk_sim.nkt_superk_sim',
'oceanoptics_spectrometer = catkit2.services.oceanoptics_spectrometer.oceanoptics_spectrometer',
'oceanoptics_spectrometer_sim = catkit2.services.oceanoptics_spectrometer_sim.oceanoptics_spectrometer_sim',
'omega_ithx_w3 = catkit2.services.omega_ithx_w3.omega_ithx_w3',
'omega_ithx_w3_sim = catkit2.services.omega_ithx_w3_sim.omega_ithx_w3_sim',
'safety_manual_check = catkit2.services.safety_manual_check.safety_manual_check',
'safety_monitor = catkit2.services.safety_monitor.safety_monitor',
'simple_simulator = catkit2.services.simple_simulator.simple_simulator',
'snmp_ups = catkit2.services.snmp_ups.snmp_ups',
'snmp_ups_sim = catkit2.services.snmp_ups_sim.snmp_ups_sim',
'thorlabs_cld101x = catkit2.services.thorlabs_cld101x.thorlabs_cld101x',
'thorlabs_cld101x_sim = catkit2.services.thorlabs_cld101x_sim.thorlabs_cld101x_sim',
'thorlabs_cube_motor_kinesis = catkit2.services.thorlabs_cube_motor_kinesis.thorlabs_cube_motor_kinesis',
'thorlabs_cube_motor_kinesis_sim = catkit2.services.thorlabs_cube_motor_kinesis_sim.thorlabs_cube_motor_kinesis_sim',
'thorlabs_fw102c = catkit2.services.thorlabs_fw102c.thorlabs_fw102c',
'thorlabs_mcls1 = catkit2.services.thorlabs_mcls1.thorlabs_mcls1',
'thorlabs_mcls1_sim = catkit2.services.thorlabs_mcls1_sim.thorlabs_mcls1_sim',
'thorlabs_mff101 = catkit2.services.thorlabs_mff101.thorlabs_mff101',
'thorlabs_mff101_sim = catkit2.services.thorlabs_mff101_sim.thorlabs_mff101_sim',
'thorlabs_pm = catkit2.services.thorlabs_pm.thorlabs_pm',
'thorlabs_pm_sim = catkit2.services.thorlabs_pm_sim.thorlabs_pm_sim',
'thorlabs_tsp01 = catkit2.services.thorlabs_tsp01.thorlabs_tsp01',
'thorlabs_tsp01_sim = catkit2.services.thorlabs_tsp01_sim.thorlabs_tsp01_sim',
'web_power_switch = catkit2.services.web_power_switch.web_power_switch',
'web_power_switch_sim = catkit2.services.web_power_switch_sim.web_power_switch_sim',
'zwo_camera = catkit2.services.zwo_camera.zwo_camera',
],
'catkit2.proxies': [
'bmc_dm = catkit2.testbed.proxies.bmc_dm:BmcDmProxy',
'camera = catkit2.testbed.proxies.camera:CameraProxy',
'deformable_mirror = catkit2.testbed.proxies.deformable_mirror:DeformableMirrorProxy',
'flip_mount = catkit2.testbed.proxies.flip_mount:FlipMountProxy',
'newport_picomotor = catkit2.testbed.proxies.newport_picomotor:NewportPicomotorProxy',
'newport_xps_q8 = catkit2.testbed.proxies.newport_xps:NewportXpsQ8Proxy',
'ni_daq = catkit2.testbed.proxies.ni_daq:NiDaqProxy',
'nkt_superk = catkit2.testbed.proxies.nkt_superk:NktSuperkProxy',
'oceanoptics_spectrometer = catkit2.testbed.proxies.oceanoptics_spectrometer:OceanopticsSpectroProxy',
'thorlabs_cube_motor_kinesis = catkit2.testbed.proxies.thorlabs_cube_motor_kinesis:ThorlabsCubeMotorKinesisProxy',
'thorlabs_mcls1 = catkit2.testbed.proxies.thorlabs_mcls1:ThorlabsMcls1',
'web_power_switch = catkit2.testbed.proxies.web_power_switch:WebPowerSwitchProxy'
]
}
)
2 changes: 0 additions & 2 deletions tests/config/testbed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ safety:
check_interval: 60
safe_interval: 180

service_paths:
- !path ../services/
base_data_path:
default: !path "~/temp_data"
support_data_path:
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ def run_testbed(port, config):
is_simulated = False

testbed = Testbed(port, is_simulated, config)

# Add test services manually for testing purposes.
base_path = os.path.dirname(__file__)
testbed.register_service_type('dummy_service', os.path.join(base_path, 'services/dummy_service/dummy_service.py'))
testbed.register_service_type('dummy_dm_service', os.path.join(base_path, 'services/dummy_dm_service/dummy_dm_service.py'))

testbed.run()

@pytest.fixture(scope='session')
Expand Down
Loading