Skip to content

Commit cdc76ce

Browse files
authored
Merge pull request #105 from DCC-Lab/powermeter-capabilities
Add capability mixins to the power-meter family
2 parents c715b4d + 6530e81 commit cdc76ce

7 files changed

Lines changed: 152 additions & 24 deletions

File tree

hardwarelibrary/physicaldevice.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ def __init__(self, serialNumber:str, idProduct:int, idVendor:int):
7171
self.monitoring = None
7272
self.refreshInterval = 1.0
7373

74+
# Cooperative base: forward to the rest of the MRO so capability mixins
75+
# mixed in alongside a device (e.g. WavelengthCalibratable) get their
76+
# own __init__ run. The device-identity arguments are consumed here, so
77+
# nothing is forwarded; a mixin __init__ must therefore take no required
78+
# arguments and call super().__init__() itself.
79+
super().__init__()
80+
7481
@classmethod
7582
def vidpids(cls):
7683
if cls.usesGenericSerialConverter:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
from .capabilities import Capability, WavelengthCalibratable, AutoScalable, ScaleAdjustable
23
from .powermeterdevice import PowerMeterDevice
34
from .integradevice import IntegraDevice
45
from .fieldmasterdevice import FieldMasterDevice, DebugFieldMasterDevice
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from abc import ABC, abstractmethod
2+
3+
4+
class Capability(ABC):
5+
# Capability mixins are combined with a PhysicalDevice subclass, which sits
6+
# ahead of them in the MRO. PhysicalDevice is a cooperative base (it calls
7+
# super().__init__() after consuming the device-identity arguments), so a
8+
# mixin that holds per-instance state may define __init__ as long as it
9+
# takes no required arguments and forwards with super().__init__().
10+
pass
11+
12+
13+
class WavelengthCalibratable(Capability):
14+
unit = "nm"
15+
isReadable = True
16+
isWritable = True
17+
18+
def __init__(self):
19+
super().__init__()
20+
self.calibrationWavelength = None
21+
22+
def getCalibrationWavelength(self):
23+
self.doGetCalibrationWavelength()
24+
return self.calibrationWavelength
25+
26+
def setCalibrationWavelength(self, wavelength):
27+
self.doSetCalibrationWavelength(wavelength)
28+
self.doGetCalibrationWavelength()
29+
30+
@abstractmethod
31+
def doGetCalibrationWavelength(self):
32+
...
33+
34+
@abstractmethod
35+
def doSetCalibrationWavelength(self, wavelength):
36+
...
37+
38+
39+
class AutoScalable(Capability):
40+
# The meter picks its measurement range automatically when auto-scaling is
41+
# on; turning it off pins the range to whatever scale is active. A meter may
42+
# also expose ScaleAdjustable to choose that range by hand.
43+
def autoScaleIsOn(self) -> bool:
44+
return self.doGetAutoScale()
45+
46+
def turnAutoScaleOn(self):
47+
self.doTurnAutoScaleOn()
48+
49+
def turnAutoScaleOff(self):
50+
self.doTurnAutoScaleOff()
51+
52+
@abstractmethod
53+
def doGetAutoScale(self) -> bool:
54+
...
55+
56+
@abstractmethod
57+
def doTurnAutoScaleOn(self):
58+
...
59+
60+
@abstractmethod
61+
def doTurnAutoScaleOff(self):
62+
...
63+
64+
65+
class ScaleAdjustable(Capability):
66+
# The full-scale measurement range (e.g. 200e-3 W). Independent of
67+
# AutoScalable: setting a scale by hand generally requires auto-scaling off.
68+
unit = "W"
69+
isReadable = True
70+
isWritable = True
71+
72+
def __init__(self):
73+
super().__init__()
74+
self.scale = None
75+
76+
def getScale(self):
77+
self.doGetScale()
78+
return self.scale
79+
80+
def setScale(self, scale):
81+
self.doSetScale(scale)
82+
self.doGetScale()
83+
84+
def availableScales(self) -> list:
85+
return self.doGetAvailableScales()
86+
87+
@abstractmethod
88+
def doGetScale(self):
89+
...
90+
91+
@abstractmethod
92+
def doSetScale(self, scale):
93+
...
94+
95+
@abstractmethod
96+
def doGetAvailableScales(self) -> list:
97+
...

hardwarelibrary/powermeters/fieldmasterdevice.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
)
88
from hardwarelibrary.physicaldevice import PhysicalDevice
99
from hardwarelibrary.powermeters.powermeterdevice import PowerMeterDevice
10+
from hardwarelibrary.powermeters.capabilities import WavelengthCalibratable
1011

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

1314

14-
class FieldMasterDevice(PowerMeterDevice):
15+
class FieldMasterDevice(PowerMeterDevice, WavelengthCalibratable):
1516
"""Coherent FieldMaster GS laser power/energy meter over RS-232.
1617
1718
The meter has no USB identity of its own: it connects through a generic
@@ -156,10 +157,11 @@ class DebugFieldMasterDevice(FieldMasterDevice):
156157
usesGenericSerialConverter = False # debug device has its own fake identity
157158

158159
def __init__(self, serialNumber='debug'):
159-
PhysicalDevice.__init__(self, serialNumber=serialNumber,
160-
idProduct=self.classIdProduct, idVendor=self.classIdVendor)
161-
self.portPath = None
162-
self.terminator = b'\n'
160+
# Go through the full cooperative chain (FieldMaster -> PowerMeter ->
161+
# PhysicalDevice -> mixins) with the debug identity, rather than jumping
162+
# straight to PhysicalDevice and skipping the intermediate __init__s.
163+
super().__init__(serialNumber=serialNumber,
164+
idProduct=self.classIdProduct, idVendor=self.classIdVendor)
163165
self.version = "Debug FieldMaster GS 1.0"
164166
self._simulatedPower = 1.43e-3
165167
self._calibrationWavelengthInNanometers = 1064.0

hardwarelibrary/powermeters/integradevice.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from enum import Enum
33
from hardwarelibrary.communication import USBPort, TextCommand, MultilineTextCommand
44
from hardwarelibrary.powermeters.powermeterdevice import PowerMeterDevice
5+
from hardwarelibrary.powermeters.capabilities import WavelengthCalibratable
56
from hardwarelibrary.notificationcenter import NotificationCenter, Notification
67

7-
class IntegraDevice(PowerMeterDevice):
8+
class IntegraDevice(PowerMeterDevice, WavelengthCalibratable):
89
classIdProduct = 0x0300
910
classIdVendor = 0x1ad5
1011
commands = {

hardwarelibrary/powermeters/powermeterdevice.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from hardwarelibrary.communication import USBPort, TextCommand
66
from hardwarelibrary.physicaldevice import *
77
from hardwarelibrary.notificationcenter import NotificationCenter, Notification
8+
from hardwarelibrary.powermeters.capabilities import Capability
89

910
class PowerMeterNotification(Enum):
1011
didMeasure = "didMeasure"
@@ -14,20 +15,22 @@ class PowerMeterDevice(PhysicalDevice):
1415
def __init__(self, serialNumber:str, idProduct:int, idVendor:int):
1516
super().__init__(serialNumber, idProduct, idVendor)
1617
self.absolutePower = 0
17-
self.calibrationWavelength = None
1818

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

25-
@abstractmethod
26-
def doGetCalibrationWavelength(self):
27-
...
27+
def hasCapability(self, capabilityClass) -> bool:
28+
return isinstance(self, capabilityClass)
2829

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

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

39-
def getCalibrationWavelength(self):
40-
self.doGetCalibrationWavelength()
41-
return self.calibrationWavelength
42-
43-
def setCalibrationWavelength(self, wavelength):
44-
self.doSetCalibrationWavelength(wavelength)
45-
self.doGetCalibrationWavelength()
46-
4742
def doGetStatusUserInfo(self):
4843
return self.measureAbsolutePower()

hardwarelibrary/tests/testFieldMasterDevice.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import unittest
33

44
from hardwarelibrary.powermeters import FieldMasterDevice, DebugFieldMasterDevice
5+
from hardwarelibrary.powermeters import (
6+
WavelengthCalibratable, AutoScalable, ScaleAdjustable,
7+
)
58
from hardwarelibrary.powermeters.powermeterdevice import PowerMeterNotification
69
from hardwarelibrary.notificationcenter import NotificationCenter
710

@@ -44,6 +47,28 @@ def testEnergy(self):
4447
self.assertIsInstance(self.device.getEnergy(), float)
4548

4649

50+
class TestFieldMasterCapabilities(unittest.TestCase):
51+
def setUp(self):
52+
self.device = DebugFieldMasterDevice()
53+
54+
def testDeclaresWavelengthCalibratable(self):
55+
self.assertTrue(self.device.hasCapability(WavelengthCalibratable))
56+
57+
def testDoesNotDeclareScaleCapabilities(self):
58+
self.assertFalse(self.device.hasCapability(AutoScalable))
59+
self.assertFalse(self.device.hasCapability(ScaleAdjustable))
60+
61+
def testCapabilitiesListsOnlyDeclaredMixins(self):
62+
self.assertEqual(self.device.capabilities(), [WavelengthCalibratable])
63+
64+
def testCapabilitiesExcludeDeviceClass(self):
65+
# The driver is itself a Capability subclass, but capabilities() must
66+
# report only the mixins, never the device class or its base.
67+
capabilities = self.device.capabilities()
68+
self.assertNotIn(DebugFieldMasterDevice, capabilities)
69+
self.assertNotIn(FieldMasterDevice, capabilities)
70+
71+
4772
class TestFieldMasterDevice(unittest.TestCase):
4873
def setUp(self):
4974
self.device = FieldMasterDevice()

0 commit comments

Comments
 (0)