Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions hardwarelibrary/physicaldevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ def __init__(self, serialNumber:str, idProduct:int, idVendor:int):
self.monitoring = None
self.refreshInterval = 1.0

# Cooperative base: forward to the rest of the MRO so capability mixins
# mixed in alongside a device (e.g. WavelengthCalibratable) get their
# own __init__ run. The device-identity arguments are consumed here, so
# nothing is forwarded; a mixin __init__ must therefore take no required
# arguments and call super().__init__() itself.
super().__init__()

@classmethod
def vidpids(cls):
if cls.usesGenericSerialConverter:
Expand Down
1 change: 1 addition & 0 deletions hardwarelibrary/powermeters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

from .capabilities import Capability, WavelengthCalibratable, AutoScalable, ScaleAdjustable
from .powermeterdevice import PowerMeterDevice
from .integradevice import IntegraDevice
from .fieldmasterdevice import FieldMasterDevice, DebugFieldMasterDevice
97 changes: 97 additions & 0 deletions hardwarelibrary/powermeters/capabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from abc import ABC, abstractmethod


class Capability(ABC):
# Capability mixins are combined with a PhysicalDevice subclass, which sits
# ahead of them in the MRO. PhysicalDevice is a cooperative base (it calls
# super().__init__() after consuming the device-identity arguments), so a
# mixin that holds per-instance state may define __init__ as long as it
# takes no required arguments and forwards with super().__init__().
pass


class WavelengthCalibratable(Capability):
unit = "nm"
isReadable = True
isWritable = True

def __init__(self):
super().__init__()
self.calibrationWavelength = None

def getCalibrationWavelength(self):
self.doGetCalibrationWavelength()
return self.calibrationWavelength

def setCalibrationWavelength(self, wavelength):
self.doSetCalibrationWavelength(wavelength)
self.doGetCalibrationWavelength()

@abstractmethod
def doGetCalibrationWavelength(self):
...

@abstractmethod
def doSetCalibrationWavelength(self, wavelength):
...


class AutoScalable(Capability):
# The meter picks its measurement range automatically when auto-scaling is
# on; turning it off pins the range to whatever scale is active. A meter may
# also expose ScaleAdjustable to choose that range by hand.
def autoScaleIsOn(self) -> bool:
return self.doGetAutoScale()

def turnAutoScaleOn(self):
self.doTurnAutoScaleOn()

def turnAutoScaleOff(self):
self.doTurnAutoScaleOff()

@abstractmethod
def doGetAutoScale(self) -> bool:
...

@abstractmethod
def doTurnAutoScaleOn(self):
...

@abstractmethod
def doTurnAutoScaleOff(self):
...


class ScaleAdjustable(Capability):
# The full-scale measurement range (e.g. 200e-3 W). Independent of
# AutoScalable: setting a scale by hand generally requires auto-scaling off.
unit = "W"
isReadable = True
isWritable = True

def __init__(self):
super().__init__()
self.scale = None

def getScale(self):
self.doGetScale()
return self.scale

def setScale(self, scale):
self.doSetScale(scale)
self.doGetScale()

def availableScales(self) -> list:
return self.doGetAvailableScales()

@abstractmethod
def doGetScale(self):
...

@abstractmethod
def doSetScale(self, scale):
...

@abstractmethod
def doGetAvailableScales(self) -> list:
...
12 changes: 7 additions & 5 deletions hardwarelibrary/powermeters/fieldmasterdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
)
from hardwarelibrary.physicaldevice import PhysicalDevice
from hardwarelibrary.powermeters.powermeterdevice import PowerMeterDevice
from hardwarelibrary.powermeters.capabilities import WavelengthCalibratable

FLOAT_PATTERN = r"([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)"


class FieldMasterDevice(PowerMeterDevice):
class FieldMasterDevice(PowerMeterDevice, WavelengthCalibratable):
"""Coherent FieldMaster GS laser power/energy meter over RS-232.

The meter has no USB identity of its own: it connects through a generic
Expand Down Expand Up @@ -156,10 +157,11 @@ class DebugFieldMasterDevice(FieldMasterDevice):
usesGenericSerialConverter = False # debug device has its own fake identity

def __init__(self, serialNumber='debug'):
PhysicalDevice.__init__(self, serialNumber=serialNumber,
idProduct=self.classIdProduct, idVendor=self.classIdVendor)
self.portPath = None
self.terminator = b'\n'
# Go through the full cooperative chain (FieldMaster -> PowerMeter ->
# PhysicalDevice -> mixins) with the debug identity, rather than jumping
# straight to PhysicalDevice and skipping the intermediate __init__s.
super().__init__(serialNumber=serialNumber,
idProduct=self.classIdProduct, idVendor=self.classIdVendor)
self.version = "Debug FieldMaster GS 1.0"
self._simulatedPower = 1.43e-3
self._calibrationWavelengthInNanometers = 1064.0
Expand Down
3 changes: 2 additions & 1 deletion hardwarelibrary/powermeters/integradevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from enum import Enum
from hardwarelibrary.communication import USBPort, TextCommand, MultilineTextCommand
from hardwarelibrary.powermeters.powermeterdevice import PowerMeterDevice
from hardwarelibrary.powermeters.capabilities import WavelengthCalibratable
from hardwarelibrary.notificationcenter import NotificationCenter, Notification

class IntegraDevice(PowerMeterDevice):
class IntegraDevice(PowerMeterDevice, WavelengthCalibratable):
classIdProduct = 0x0300
classIdVendor = 0x1ad5
commands = {
Expand Down
31 changes: 13 additions & 18 deletions hardwarelibrary/powermeters/powermeterdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from hardwarelibrary.communication import USBPort, TextCommand
from hardwarelibrary.physicaldevice import *
from hardwarelibrary.notificationcenter import NotificationCenter, Notification
from hardwarelibrary.powermeters.capabilities import Capability

class PowerMeterNotification(Enum):
didMeasure = "didMeasure"
Expand All @@ -14,20 +15,22 @@ class PowerMeterDevice(PhysicalDevice):
def __init__(self, serialNumber:str, idProduct:int, idVendor:int):
super().__init__(serialNumber, idProduct, idVendor)
self.absolutePower = 0
self.calibrationWavelength = None

# Hardware hooks a driver must implement, on top of doInitializeDevice
# and doShutdownDevice inherited from PhysicalDevice.
@abstractmethod
def doGetAbsolutePower(self):
...
def capabilities(self) -> list:
# The capability mixins, not the marker nor the device class itself
# (a driver is a Capability subclass too, but it is a PhysicalDevice).
return [klass for klass in type(self).__mro__
if issubclass(klass, Capability)
and klass is not Capability
and not issubclass(klass, PhysicalDevice)]

@abstractmethod
def doGetCalibrationWavelength(self):
...
def hasCapability(self, capabilityClass) -> bool:
return isinstance(self, capabilityClass)

# Measuring absolute power is the one capability every power meter has, so
# its hook stays on the base; optional capabilities live in mixins.
@abstractmethod
def doSetCalibrationWavelength(self, wavelength):
def doGetAbsolutePower(self):
...

def measureAbsolutePower(self):
Expand All @@ -36,13 +39,5 @@ def measureAbsolutePower(self):
NotificationCenter().postNotification(PowerMeterNotification.didMeasure, notifyingObject=self, userInfo=power)
return power

def getCalibrationWavelength(self):
self.doGetCalibrationWavelength()
return self.calibrationWavelength

def setCalibrationWavelength(self, wavelength):
self.doSetCalibrationWavelength(wavelength)
self.doGetCalibrationWavelength()

def doGetStatusUserInfo(self):
return self.measureAbsolutePower()
25 changes: 25 additions & 0 deletions hardwarelibrary/tests/testFieldMasterDevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import unittest

from hardwarelibrary.powermeters import FieldMasterDevice, DebugFieldMasterDevice
from hardwarelibrary.powermeters import (
WavelengthCalibratable, AutoScalable, ScaleAdjustable,
)
from hardwarelibrary.powermeters.powermeterdevice import PowerMeterNotification
from hardwarelibrary.notificationcenter import NotificationCenter

Expand Down Expand Up @@ -44,6 +47,28 @@ def testEnergy(self):
self.assertIsInstance(self.device.getEnergy(), float)


class TestFieldMasterCapabilities(unittest.TestCase):
def setUp(self):
self.device = DebugFieldMasterDevice()

def testDeclaresWavelengthCalibratable(self):
self.assertTrue(self.device.hasCapability(WavelengthCalibratable))

def testDoesNotDeclareScaleCapabilities(self):
self.assertFalse(self.device.hasCapability(AutoScalable))
self.assertFalse(self.device.hasCapability(ScaleAdjustable))

def testCapabilitiesListsOnlyDeclaredMixins(self):
self.assertEqual(self.device.capabilities(), [WavelengthCalibratable])

def testCapabilitiesExcludeDeviceClass(self):
# The driver is itself a Capability subclass, but capabilities() must
# report only the mixins, never the device class or its base.
capabilities = self.device.capabilities()
self.assertNotIn(DebugFieldMasterDevice, capabilities)
self.assertNotIn(FieldMasterDevice, capabilities)


class TestFieldMasterDevice(unittest.TestCase):
def setUp(self):
self.device = FieldMasterDevice()
Expand Down
Loading