7474 ~DualPf4FilterBox
7575 ~EpicsDescriptionMixin
7676 ~KohzuSeqCtl_Monochromator
77+ ~ProcessController
7778 ~Struck3820
7879
7980Internal routines
121122
122123logger = logging .getLogger (__name__ )
123124
124- """for convenience""" # TODO: contribute to ophyd?
125+ """for convenience""" # TODO: contribute to ophyd?
125126SCALER_AUTOCOUNT_MODE = 1
126127
127128
@@ -1322,60 +1323,81 @@ class KohzuSeqCtl_Monochromator(Device):
13221323 crystal_type = Component (EpicsSignal , "BraggTypeMO" )
13231324
13241325
1325- class TemperatureController_Base (Device ):
1326+ class ProcessController (Device ):
13261327 """
1327- common parts of temperature controller support
1328+ common parts of a process controller support
1329+
1330+ A process controller keeps a signal (a readback value such as
1331+ temperature, vacuum, himdity, etc.) as close as possible
1332+ to a target (set point) value. It has additional fields
1333+ that describe parameters specific to the controller such
1334+ as PID loop, on/off, applied controller power, and other
1335+ details.
1336+
1337+ This is a base class to standardize the few common terms
1338+ used to command and record the target and readback values
1339+ of a process controller.
1340+
1341+ Subclasses should redefine (override) `controller_name`,
1342+ ``signal``, ``target``, and ``units`` such as the example below.
1343+ Also set values for ``tolerance``, ``report_interval_s``, and
1344+ ``poll_s`` suitable for the specific controller used.
1345+
1346+ *Floats*: ``signal``, ``target`', and ``tolerance`` will be
1347+ considered as floating point numbers in the code.
1348+
1349+ It is assumed in "meth"`settled()` that: ``|signal - target| <= tolerance``.
1350+ Override this *property* method if a different decision is needed.
13281351
13291352 EXAMPLE::
13301353
1331- class MyLinkam(TemperatureController_Base):
1332- controller_name = "MyLinkam"
1333- temperature = Component(EpicsSignalRO, "temp")
1334- set_point = Component(EpicsSignal, "setLimit", kind="omitted")
1354+ class MyLinkam(ProcessController):
1355+ controller_name = "MyLinkam Controller"
1356+ signal = Component(EpicsSignalRO, "temp")
1357+ target = Component(EpicsSignal, "setLimit", kind="omitted")
1358+ units = Component(Signal, kind="omitted", value="C")
13351359
13361360 controller = MyLinkam("my:linkam:", name="controller")
13371361 RE(controller.wait_until_settled(timeout=10))
13381362
1339- controller.record_temperature ()
1340- print(f"{controller.controller_name} controller settled? {controller.settled}")
1363+ controller.record_signal ()
1364+ print(f"{controller.controller_name} settled? {controller.settled}")
13411365
13421366 def rampUp_rampDown():
13431367 '''ramp temperature up, then back down'''
1344- yield from controller.set_temperature (25, timeout=180)
1345- controller.report_interval = 10 # change report interval to 10s
1368+ yield from controller.set_target (25, timeout=180)
1369+ controller.report_interval_s = 10 # change report interval to 10s
13461370 for i in range(10, 0, -1):
1347- print(f"hold at ( self.value:.2f)C , time remaining: {i}s")
1371+ print(f"hold at { self.value:.2f}{self.units.value} , time remaining: {i}s")
13481372 yield from bps.sleep(1)
1349- yield from controller.set_temperature (0, timeout=180)
1373+ yield from controller.set_target (0, timeout=180)
13501374
13511375 RE(test_plan())
13521376
13531377 """
13541378
1355- controller_name = "TemperatureController_Base"
1356- temperature = Component (Signal ) # override in subclass
1357- set_point = Component (Signal , kind = "omitted" ) # override in subclass
1379+ controller_name = "ProcessController"
1380+ signal = Component (Signal ) # override in subclass
1381+ target = Component (Signal , kind = "omitted" ) # override in subclass
1382+ tolerance = Component (Signal , kind = "omitted" , value = 1 ) # override in subclass
1383+ units = Component (Signal , kind = "omitted" , value = "" ) # override in subclass
13581384
1359- tolerance = 1 # requirement: |T - target| must be <= this, degree C
1360- report_interval = 5 # time between reports during loop, s
1385+ tolerance = 1 # requirement: |signal - target| <= tolerance (see `settled()`)
1386+ report_interval_s = 5 # time between reports during loop, s
13611387 poll_s = 0.02 # time to wait during polling loop, s
13621388
1363- def record_temperature (self ):
1364- """write temperatures as comment"""
1365- global specwriter
1366- msg = f"{ self .controller_name } Temperature: { self .value :.2f} C"
1367- specwriter ._cmt ("event" , msg )
1389+ def record_signal (self ):
1390+ """write signal to the console"""
1391+ msg = f"{ self .controller_name } signal: { self .value :.2f} { self .units .value } "
13681392 print (msg )
1393+ return msg
13691394
1370- def set_temperature (self , set_point , wait = True , timeout = None , timeout_fail = False ):
1371- """change controller to new temperature set point"""
1372- global specwriter
1395+ def set_target (self , target , wait = True , timeout = None , timeout_fail = False ):
1396+ """change controller to new signal set point"""
1397+ yield from bps . mv ( self . target , target )
13731398
1374- yield from bps .mv (self .set_point , set_point )
1375-
1376- msg = f"Set { self .controller_name } Temperature to { set_point :.2f} C"
1399+ msg = f"Set { self .controller_name } target to { target :.2f} { self .units .value } "
13771400 print (msg )
1378- specwriter ._cmt ("event" , msg )
13791401
13801402 if wait :
13811403 yield from self .wait_until_settled (
@@ -1384,58 +1406,58 @@ def set_temperature(self, set_point, wait=True, timeout=None, timeout_fail=False
13841406
13851407 @property
13861408 def value (self ):
1387- """shortcut to self.temperature .value"""
1388- return self .temperature .value
1409+ """shortcut to self.signal .value"""
1410+ return self .signal .value
13891411
13901412 @property
13911413 def settled (self ):
1392- """Is temperature close enough to target?"""
1393- diff = abs (self .temperature .get () - self .set_point .value )
1414+ """Is signal close enough to target?"""
1415+ diff = abs (self .signal .get () - self .target .value )
13941416 return diff <= self .tolerance
13951417
13961418 def wait_until_settled (self , timeout = None , timeout_fail = False ):
13971419 """
1398- wait for controller to reach target temperature
1420+ plan: wait for controller signal to reach target within tolerance
13991421 """
14001422 # see: https://stackoverflow.com/questions/2829329/catch-a-threads-exception-in-the-caller-thread-in-python
14011423 t0 = time .time ()
1402- _st = DeviceStatus (self .temperature )
1403- started = False
1404-
1405- def changing_cb (value , timestamp , ** kwargs ):
1406- if started and self .settled :
1407- _st ._finished (success = True )
1424+ _st = DeviceStatus (self .signal )
14081425
1409- token = self .temperature .subscribe (changing_cb )
1410- started = True
1411-
1412- report = 0
1413- while not _st .done and not self .settled :
1414- elapsed = time .time () - t0
1415- if timeout is not None and elapsed > timeout :
1416- _st ._finished (success = self .settled )
1417- msg = f"Temperature Controller Timeout after { elapsed :.2f} s"
1418- msg += f", target { self .set_point .value :.2f} C"
1419- msg += f", now { self .temperature .get ():.2f} C"
1420- # msg += f", status={_st}"
1421- print (msg )
1422- if timeout_fail :
1423- raise TimeoutError (msg )
1424- continue
1425- if elapsed >= report :
1426- report += self .report_interval
1427- msg = f"Waiting { elapsed :.1f} s"
1428- msg += f" to reach { self .set_point .value :.2f} C"
1429- msg += f", now { self .temperature .get ():.2f} C"
1430- print (msg )
1431- yield from bps .sleep (self .poll_s )
1432-
1433- if not _st .done and self .settled :
1434- # just in case self.temperature already at temperature
1426+ if self .settled :
1427+ # just in case signal already at target
14351428 _st ._finished (success = True )
1436-
1437- self .temperature .unsubscribe (token )
1438- self .record_temperature ()
1429+ else :
1430+ started = False
1431+
1432+ def changing_cb (* args , ** kwargs ):
1433+ if started and self .settled :
1434+ _st ._finished (success = True )
1435+
1436+ token = self .signal .subscribe (changing_cb )
1437+ started = True
1438+ report = 0
1439+ while not _st .done and not self .settled :
1440+ elapsed = time .time () - t0
1441+ if timeout is not None and elapsed > timeout :
1442+ _st ._finished (success = self .settled )
1443+ msg = f"{ self .controller_name } Timeout after { elapsed :.2f} s"
1444+ msg += f", target { self .target .value :.2f} { self .units .value } "
1445+ msg += f", now { self .signal .get ():.2f} { self .units .value } "
1446+ print (msg )
1447+ if timeout_fail :
1448+ raise TimeoutError (msg )
1449+ continue
1450+ if elapsed >= report :
1451+ report += self .report_interval_s .value
1452+ msg = f"Waiting { elapsed :.1f} s"
1453+ msg += f" to reach { self .target .value :.2f} { self .units .value } "
1454+ msg += f", now { self .signal .get ():.2f} { self .units .value } "
1455+ print (msg )
1456+ yield from bps .sleep (self .poll_s )
1457+
1458+ self .signal .unsubscribe (token )
1459+
1460+ self .record_signal ()
14391461 elapsed = time .time () - t0
14401462 print (f"Total time: { elapsed :.3f} s, settled:{ _st .success } " )
14411463
0 commit comments