Files
frappy/frappy/ctrlby.py
Markus Zolliker b45635e4f8 make controlled_by configuration work properly
- secnode.py: initialize all modules before creating description
- fixes in ctrlby.py
2025-10-30 13:43:20 +01:00

215 lines
7.6 KiB
Python

# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from frappy.datatypes import BoolType, EnumType, Enum
from frappy.core import Parameter, Attached
class WrapControlledBy:
"""mixin to add controlled_by to writable modules
Create a wrapper class inheriting from this mixin
to add controlled_by to a Writable module
Usage:
class Enhanced(WrapControlledBy, BaseWritable):
pass
# typically nothing else has to be implemented
from a module with output (inheriting from HasOutput), the
method update_target must be called for internal updates
"""
controlled_by = Parameter('source of target value', EnumType(members={'self': 0}), default=0)
target = Parameter() # make sure target is a parameter
inputCallbacks = ()
def register_input(self, name, deactivate_control):
"""register input
:param name: the name of the module (for controlled_by enum)
:param deactivate_control: a method on the input module to switch off control
called by <controller module>.initModule
"""
if not self.inputCallbacks:
self.inputCallbacks = {}
self.inputCallbacks[name] = deactivate_control
prev_enum = self.parameters['controlled_by'].datatype.export_datatype()['members']
# add enum member, using autoincrement feature of Enum
self.parameters['controlled_by'].datatype = EnumType(Enum(prev_enum, **{name: None}))
self.log.info('enumtype %r', self.parameters['controlled_by'].datatype)
def write_controlled_by(self, modulename):
result = modulename
if modulename in ('self', self.name):
# inform the deactivate_control methods, that we have already switched
self.controlled_by = result = 'self'
for name, deactivate_control in self.inputCallbacks.items():
if name != modulename:
deactivate_control(modulename)
return result
def self_controlled(self):
"""method to change controlled_by to self
to be called from the write_target method
"""
if self.controlled_by != 0:
self.write_controlled_by('self')
def set_off(self):
"""to be overridden if the off state should be different from the default
on a FloatRange() the default value is 0
"""
zero = self.parameters['target'].datatype.default
try:
self.internal_set_target(zero)
except Exception as e:
self.target = zero
def update_target(self, module, value):
"""update internal target value
as write_target would switch to manual mode, the controlling module
has to use this method to update the value
override and super call, if other actions are needed
"""
if self.controlled_by != module:
deactivate_control = self.inputCallbacks.get(self.controlled_by)
if deactivate_control:
deactivate_control(module)
self.controlled_by = module
target = self.internal_set_target(value)
self.target = value if target is None else target
def write_target(self, target):
self.self_controlled()
return self.internal_set_target(target)
def internal_set_target(self, target):
# we need this additional indirection:
# super().write_target must refer to an inherited base class
# which is after WrapControlledBy in the method resolution order
return super().write_target(target)
class HasControlledBy(WrapControlledBy):
"""mixin for controlled_by functionality
Create a wrapper class inheriting from this mixin
to add controlled_by to a Writable module
Usage:
class Enhanced(HasControlledBy, BaseWritable):
def set_target(self, value):
# implement here hardware access for setting target
# do not override write_target!
from a module with output (inheriting from HasOutput), the
method update_target must be called for internal updates
"""
def set_target(self, value):
"""to be overridden for setting target of HW"""
raise NotImplementedError
def internal_set_target(self, value):
# we need this additional indirection:
# self.write_target must refer to a base class which
# is before HasControlledBy in the method resolution order
return self.set_target(value)
def set_off(self):
"""typically needs to be overridden"""
class HasOutputModule:
"""mixin for modules having an output module
this module will call the update_target method of an output module
"""
# mandatory=False: it should be possible to configure a module with fixed control
output_module = Attached(WrapControlledBy, mandatory=False)
control_active = Parameter('control mode', BoolType(), default=False)
target = Parameter() # make sure target is a parameter
def initModule(self):
super().initModule()
if self.output_module:
self.output_module.register_input(self.name, self.deactivate_control)
def write_control_active(self, value):
"""override with supercall if needed
control_active is readonly by default, as specified in the SECoP standard.
This method is meant to be called internally.
However, it is possible to override control_active with readonly=False
and this is quite useful IMHO in some situations
"""
out = self.output_module
if out:
if value:
if out.controlled_by != self.name:
# deactivate control an all modules controlling our output_module
out.write_controlled_by(self.name)
else:
if out.controlled_by == self.name:
out.set_off() # this sets out.controlled_by to 0 (=self)
def set_control_active(self, active):
"""to be overridden for switching hw control
TODO: remove this legacy method (replaced by write_control_active)
"""
self.control_active = active
def activate_control(self):
"""method to switch control_active on
TODO: remove this legacy method (replaced by write_control_active)
"""
self.write_control_active(True)
def deactivate_control(self, source=None):
"""called when another module takes over control
registered to be called from the controlled module(s)
"""
if self.control_active:
self.write_control_active(False)
self.log.warning(f'switched to manual mode by {source or self.name}')
def write_target(self, target):
self.write_control_active(True)
return self.set_target(target)
def set_target(self, target):
"""to be overridden"""
raise NotImplementedError