Exemple #1
0
 def __init__(
     self,
     name: util.APartName,
     description: util.AMetaDescription,
     pv: util.APv = "",
     rbv: util.ARbv = "",
     rbv_suffix: util.ARbvSuffix = "",
     min_delta: util.AMinDelta = 0.05,
     timeout: util.ATimeout = DEFAULT_TIMEOUT,
     sink_port: util.ASinkPort = None,
     widget: util.AWidget = None,
     group: util.AGroup = None,
     config: util.AConfig = True,
 ) -> None:
     super().__init__(name)
     self.meta = ChoiceMeta(description)
     self.caa = util.CAAttribute(
         self.meta,
         util.catools.DBR_ENUM,
         pv,
         rbv,
         rbv_suffix,
         min_delta,
         timeout,
         sink_port,
         widget,
         group,
         config,
         self.on_connect,
     )
Exemple #2
0
class CAChoicePart(Part):
    """Defines a choice `Attribute` that talks to a DBR_ENUM mbbo PV"""

    def __init__(self,
                 name,  # type: util.APartName
                 description,  # type: util.AMetaDescription
                 pv="",  # type: util.APv
                 rbv="",  # type: util.ARbv
                 rbv_suffix="",  # type: util.ARbvSuffix
                 min_delta=0.05,  # type: util.AMinDelta
                 timeout=DEFAULT_TIMEOUT,  # type: util.ATimeout
                 sink_port=None,  # type: util.ASinkPort
                 widget=None,  # type: util.AWidget
                 group=None,  # type: util.AGroup
                 config=True,  # type: util.AConfig
                 ):
        # type: (...) -> None
        super(CAChoicePart, self).__init__(name)
        self.meta = ChoiceMeta(description)
        self.caa = util.CAAttribute(
            self.meta, util.catools.DBR_ENUM, pv, rbv, rbv_suffix, min_delta,
            timeout, sink_port, widget, group, config, self.on_connect)

    def on_connect(self, value):
        self.meta.set_choices(value.enums)

    def caput(self, value):
        # Turn the string value int the index of the choice list. We are
        # passed a validated value, so it is guaranteed to be in choices
        value = self.meta.choices.index(value)
        self.caa.caput(value)

    def setup(self, registrar):
        # type: (PartRegistrar) -> None
        self.caa.setup(registrar, self.name, self.register_hooked, self.caput)
Exemple #3
0
 def setUp(self):
     self.choice_meta = ChoiceMeta("test description", ["a", "b"])
     self.serialized = OrderedDict()
     self.serialized["typeid"] = "malcolm:core/ChoiceMeta:1.0"
     self.serialized["description"] = "desc"
     self.serialized["choices"] = ["a", "b"]
     self.serialized["tags"] = []
     self.serialized["writeable"] = False
     self.serialized["label"] = "name"
Exemple #4
0
 def test_init(self):
     self.choice_meta = ChoiceMeta(
         "test description", ["a", "b"])
     assert (
                "test description") == self.choice_meta.description
     assert (
                self.choice_meta.typeid) == "malcolm:core/ChoiceMeta:1.0"
     assert (
                self.choice_meta.label) == ""
     assert (
                self.choice_meta.choices) == ["a", "b"]
Exemple #5
0
 def __init__(
     self,
     name: APartName,
     description: AMetaDescription,
     choices: UChoices,
     value: AValue,
     writeable: AWriteable = False,
     config: AConfig = 1,
     group: AGroup = None,
     widget: AWidget = None,
 ) -> None:
     super().__init__(name)
     meta = ChoiceMeta(description, choices)
     set_tags(meta, writeable, config, group, widget)
     self.attr = meta.create_attribute_model(value)
     self.writeable_func = self.attr.set_value if writeable else None
 def __init__(self, mri: AMri, description: ADescription = "") -> None:
     super().__init__(mri, description)
     self._children_writeable: ChildrenWriteable = {}
     self.state = ChoiceMeta(
         "StateMachine State of Block",
         self.state_set.possible_states,
         tags=[Widget.MULTILINETEXTUPDATE.tag()]
         # Start DISABLING so we can immediately go to DISABLED
     ).create_attribute_model(ss.DISABLING)
     self.field_registry.add_attribute_model("state", self.state)
     self.field_registry.add_method_model(self.disable)
     self.set_writeable_in(self.field_registry.add_method_model(self.reset),
                           ss.DISABLED, ss.FAULT)
     self.transition(ss.DISABLED)
     self.register_hooked(ProcessStartHook, self.init)
     self.register_hooked(ProcessStopHook, self.halt)
Exemple #7
0
 def test_from_dict(self):
     bm = ChoiceMeta.from_dict(self.serialized)
     assert type(bm) == ChoiceMeta
     assert bm.description == "desc"
     assert bm.choices == ["a", "b"]
     assert bm.tags == []
     assert not bm.writeable
     assert bm.label == "name"
Exemple #8
0
 def _make_ext_capture(self, field_name: str, field_data: FieldData) -> None:
     group = self._make_group("outputs")
     meta = ChoiceMeta(
         "Capture %s in PCAP?" % field_name,
         field_data.labels,
         tags=[group, Widget.COMBO.tag()],
     )
     self._make_field_part(field_name + ".CAPTURE", meta, writeable=True)
Exemple #9
0
 def __init__(
         self,
         name,  # type: APartName
         description,  # type: AMetaDescription
         choices,  # type: UChoices
         value,  # type: AValue
         writeable=False,  # type: AWriteable
         config=1,  # type: AConfig
         group=None,  # type: AGroup
         widget=None,  # type: AWidget
 ):
     # type: (...) -> None
     super(ChoicePart, self).__init__(name)
     meta = ChoiceMeta(description, choices)
     set_tags(meta, writeable, config, group, widget)
     self.attr = meta.create_attribute_model(value)
     self.writeable_func = self.attr.set_value if writeable else None
Exemple #10
0
 def __init__(self,
              pv_prefix: APvPrefix,
              group: ca.util.AGroup = None) -> None:
     super().__init__("sinkPorts")
     self.pvs = [pv_prefix + ":CsPort", pv_prefix + ":CsAxis"]
     self.rbvs = [
         pv_prefix + ":CsPort_RBV",
         pv_prefix + ":CsAxis_RBV",
         pv_prefix + ".OUT",
     ]
     meta = ChoiceMeta("CS Axis")
     builtin.util.set_tags(meta,
                           writeable=True,
                           group=group,
                           sink_port=Port.MOTOR)
     self.cs_attr = meta.create_attribute_model()
     meta = StringMeta("Parent PMAC Port name")
     builtin.util.set_tags(meta, group=group, sink_port=Port.MOTOR)
     self.pmac_attr = meta.create_attribute_model()
     meta = NumberMeta("int32", "Parent PMAC Axis number")
     builtin.util.set_tags(meta, group=group)
     self.axis_num_attr = meta.create_attribute_model()
     # Subscriptions
     self.monitors: List = []
     self.port = None
     self.axis = None
     self.port_choices: List = []
Exemple #11
0
 def __init__(self,
              name,  # type: util.APartName
              description,  # type: util.AMetaDescription
              pv="",  # type: util.APv
              rbv="",  # type: util.ARbv
              rbv_suffix="",  # type: util.ARbvSuffix
              min_delta=0.05,  # type: util.AMinDelta
              timeout=DEFAULT_TIMEOUT,  # type: util.ATimeout
              sink_port=None,  # type: util.ASinkPort
              widget=None,  # type: util.AWidget
              group=None,  # type: util.AGroup
              config=True,  # type: util.AConfig
              ):
     # type: (...) -> None
     super(CAChoicePart, self).__init__(name)
     self.meta = ChoiceMeta(description)
     self.caa = util.CAAttribute(
         self.meta, util.catools.DBR_ENUM, pv, rbv, rbv_suffix, min_delta,
         timeout, sink_port, widget, group, config, self.on_connect)
Exemple #12
0
class TestChoiceMeta(unittest.TestCase):
    def setUp(self):
        self.choice_meta = ChoiceMeta(
            "test description", ["a", "b"])
        self.serialized = OrderedDict()
        self.serialized["typeid"] = "malcolm:core/ChoiceMeta:1.0"
        self.serialized["description"] = "desc"
        self.serialized["choices"] = ["a", "b"]
        self.serialized["tags"] = []
        self.serialized["writeable"] = False
        self.serialized["label"] = "name"

    def test_init(self):
        self.choice_meta = ChoiceMeta(
            "test description", ["a", "b"])
        assert (
                   "test description") == self.choice_meta.description
        assert (
                   self.choice_meta.typeid) == "malcolm:core/ChoiceMeta:1.0"
        assert (
                   self.choice_meta.label) == ""
        assert (
                   self.choice_meta.choices) == ["a", "b"]

    def test_given_valid_value_then_return(self):
        response = self.choice_meta.validate("a")
        assert "a" == response

    def test_int_validate(self):
        response = self.choice_meta.validate(1)
        assert "b" == response

    def test_None_valid(self):
        response = self.choice_meta.validate(None)
        assert "a" == response

    def test_given_invalid_value_then_raises(self):
        with self.assertRaises(ValueError):
            self.choice_meta.validate('badname')

    def test_set_choices(self):
        self.choice_meta.set_choices(["4"])

        assert ["4"] == self.choice_meta.choices

    def test_to_dict(self):
        bm = ChoiceMeta("desc", ["a", "b"], label="name")
        assert bm.to_dict() == self.serialized

    def test_from_dict(self):
        bm = ChoiceMeta.from_dict(self.serialized)
        assert type(bm) == ChoiceMeta
        assert bm.description == "desc"
        assert bm.choices == ["a", "b"]
        assert bm.tags == []
        assert not bm.writeable
        assert bm.label == "name"
Exemple #13
0
 def __init__(self, name, prefix, group=None):
     # type: (ca.util.APartName, APrefix, ca.util.AGroup) -> None
     super(RawMotorCSPart, self).__init__(name)
     self.pvs = [prefix + ":CsPort", prefix + ":CsAxis"]
     self.rbvs = [prefix + ":CsPort_RBV", prefix + ":CsAxis_RBV"]
     meta = ChoiceMeta("CS Axis")
     builtin.util.set_tags(meta,
                           writeable=True,
                           group=group,
                           sink_port=Port.MOTOR)
     self.attr = meta.create_attribute_model()
     # Subscriptions
     self.monitors = []
     self.port = None
     self.axis = None
     self.port_choices = []
     # Hooks
     self.register_hooked(builtin.hooks.DisableHook, self.disconnect)
     self.register_hooked((builtin.hooks.InitHook, builtin.hooks.ResetHook),
                          self.reconnect)
 def _make_time_parts(self, field_name, field_data, writeable):
     description = field_data.description
     if writeable:
         widget = Widget.TEXTINPUT
         group = self._make_group("parameters")
     else:
         widget = Widget.TEXTUPDATE
         group = self._make_group("readbacks")
     meta = NumberMeta("float64", description, [group, widget.tag()])
     # We must change time units before value, so restore value in 2nd
     # iteration
     self._make_field_part(field_name, meta, writeable, iteration=2)
     meta = ChoiceMeta(description + " time units", ["s", "ms", "us"],
                       tags=[group, Widget.COMBO.tag()])
     self._make_field_part(field_name + ".UNITS", meta, writeable=True)
Exemple #15
0
 def _make_mux(
     self, field_name: str, field_data: FieldData, port_type: Port
 ) -> None:
     group = self._make_group("inputs")
     labels = [x for x in field_data.labels if x in ("ZERO", "ONE")] + sorted(
         x for x in field_data.labels if x not in ("ZERO", "ONE")
     )
     tags = [group, port_type.sink_port_tag("ZERO"), Widget.COMBO.tag()]
     if port_type == Port.BOOL:
         # Bits have a delay, use it as a badge
         delay_name = snake_to_camel(field_name) + "Delay"
         tags.append(badge_value_tag(self.mri, delay_name))
     meta = ChoiceMeta(field_data.description, labels, tags=tags)
     self._make_field_part(field_name, meta, writeable=True)
     self.mux_metas[field_name] = meta
 def _make_mux(self, field_name, field_data, typ):
     group = self._make_group("inputs")
     if typ == "bit":
         port_type = Port.BOOL
     else:
         port_type = Port.INT32
     labels = [x for x in field_data.labels if x in ("ZERO", "ONE")] + \
         sorted(x for x in field_data.labels if x not in ("ZERO", "ONE"))
     meta = ChoiceMeta(
         field_data.description,
         labels,
         tags=[group,
               port_type.sink_port_tag("ZERO"),
               Widget.COMBO.tag()])
     self._make_field_part(field_name, meta, writeable=True)
     meta = make_meta(typ,
                      "%s current value" % field_name,
                      tags=[group],
                      writeable=False)
     self._make_field_part(field_name + ".CURRENT", meta, writeable=False)
Exemple #17
0
 def __init__(self, name: APartName, description: AMetaDescription) -> None:
     super().__init__(name)
     meta = ChoiceMeta(description, ["expanded", "collapsed"])
     set_tags(meta, writeable=True, widget=Widget.GROUP)
     self.attr = meta.create_attribute_model("expanded")
 def __init__(
     self,
     mri: AMri,
     config_dir: AConfigDir,
     template_designs: ATemplateDesigns = "",
     initial_design: AInitialDesign = "",
     use_git: AUseGit = True,
     description: ADescription = "",
 ) -> None:
     super().__init__(mri=mri, description=description)
     assert os.path.isdir(config_dir), "%s is not a directory" % config_dir
     self.config_dir = config_dir
     self.initial_design = initial_design
     self.use_git = use_git
     self.template_designs = template_designs
     self.git_config: Tuple[str, ...]
     if use_git:
         if check_git_version("1.7.2"):
             self.git_email = os.environ["USER"] + "@" + socket.gethostname(
             )
             self.git_name = "Malcolm"
             self.git_config = (
                 "-c",
                 "user.name=%s" % self.git_name,
                 "-c",
                 'user.email="%s"' % self.git_email,
             )
         else:
             self.git_config = ()
     # last saved layout and exports
     self.saved_visibility = None
     self.saved_exports = None
     # ((name, AttributeModel/MethodModel, setter, needs_context))
     self._current_part_fields = ()
     self._subscriptions: List[Subscribe] = []
     self.port_info: Dict[APartName, List[PortInfo]] = {}
     self.part_exportable: Dict[Part, List[AAttributeName]] = {}
     # TODO: turn this into "exported attribute modified"
     self.context_modified: Dict[Part, Set[str]] = {}
     self.part_modified: Dict[Part, PartModifiedInfo] = {}
     # The attributes our part has published
     self.our_config_attributes: Dict[str, AttributeModel] = {}
     # The reportable infos we are listening for
     self.info_registry.add_reportable(PartModifiedInfo,
                                       self.update_modified)
     # Update queue of exportable fields
     self.info_registry.add_reportable(PartExportableInfo,
                                       self.update_exportable)
     # Create a port for ourself
     self.field_registry.add_attribute_model(
         "mri",
         StringMeta(
             "A port for giving our MRI to things that might use us",
             tags=[Port.BLOCK.source_port_tag(self.mri)],
         ).create_attribute_model(self.mri),
     )
     # Create a layout table attribute for setting block positions
     self.layout = TableMeta.from_table(
         LayoutTable,
         "Layout of child blocks",
         Widget.FLOWGRAPH,
         writeable=["x", "y", "visible"],
     ).create_attribute_model()
     self.set_writeable_in(self.layout, ss.READY)
     self.field_registry.add_attribute_model("layout", self.layout,
                                             self.set_layout)
     # Create a design attribute for loading an existing layout
     self.design = ChoiceMeta("Design name to load",
                              tags=[config_tag(),
                                    Widget.COMBO.tag()
                                    ]).create_attribute_model()
     self.field_registry.add_attribute_model("design", self.design,
                                             self.set_design)
     self.set_writeable_in(self.design, ss.READY)
     # Create an export table for mirroring exported fields
     self.exports = TableMeta.from_table(
         ExportTable,
         "Exported fields of child blocks",
         writeable=list(ExportTable.call_types),
     ).create_attribute_model()
     # Overwrite the sources meta to be a ChoiceArrayMeta
     self.exports.meta.elements["source"] = ChoiceArrayMeta(
         "Name of the block.field to export",
         writeable=True,
         tags=[Widget.COMBO.tag()],
     )
     self.set_writeable_in(self.exports, ss.READY)
     self.field_registry.add_attribute_model("exports", self.exports,
                                             self.set_exports)
     # Create read-only indicator for when things are modified
     self.modified = BooleanMeta("Whether the design is modified",
                                 tags=[Widget.LED.tag()
                                       ]).create_attribute_model()
     self.field_registry.add_attribute_model("modified", self.modified)
     # Create the save method
     self.set_writeable_in(self.field_registry.add_method_model(self.save),
                           ss.READY)
class ManagerController(StatefulController):
    """RunnableDevice implementer that also exposes GUI for child parts"""

    state_set = ss()

    def __init__(
        self,
        mri: AMri,
        config_dir: AConfigDir,
        template_designs: ATemplateDesigns = "",
        initial_design: AInitialDesign = "",
        use_git: AUseGit = True,
        description: ADescription = "",
    ) -> None:
        super().__init__(mri=mri, description=description)
        assert os.path.isdir(config_dir), "%s is not a directory" % config_dir
        self.config_dir = config_dir
        self.initial_design = initial_design
        self.use_git = use_git
        self.template_designs = template_designs
        self.git_config: Tuple[str, ...]
        if use_git:
            if check_git_version("1.7.2"):
                self.git_email = os.environ["USER"] + "@" + socket.gethostname(
                )
                self.git_name = "Malcolm"
                self.git_config = (
                    "-c",
                    "user.name=%s" % self.git_name,
                    "-c",
                    'user.email="%s"' % self.git_email,
                )
            else:
                self.git_config = ()
        # last saved layout and exports
        self.saved_visibility = None
        self.saved_exports = None
        # ((name, AttributeModel/MethodModel, setter, needs_context))
        self._current_part_fields = ()
        self._subscriptions: List[Subscribe] = []
        self.port_info: Dict[APartName, List[PortInfo]] = {}
        self.part_exportable: Dict[Part, List[AAttributeName]] = {}
        # TODO: turn this into "exported attribute modified"
        self.context_modified: Dict[Part, Set[str]] = {}
        self.part_modified: Dict[Part, PartModifiedInfo] = {}
        # The attributes our part has published
        self.our_config_attributes: Dict[str, AttributeModel] = {}
        # The reportable infos we are listening for
        self.info_registry.add_reportable(PartModifiedInfo,
                                          self.update_modified)
        # Update queue of exportable fields
        self.info_registry.add_reportable(PartExportableInfo,
                                          self.update_exportable)
        # Create a port for ourself
        self.field_registry.add_attribute_model(
            "mri",
            StringMeta(
                "A port for giving our MRI to things that might use us",
                tags=[Port.BLOCK.source_port_tag(self.mri)],
            ).create_attribute_model(self.mri),
        )
        # Create a layout table attribute for setting block positions
        self.layout = TableMeta.from_table(
            LayoutTable,
            "Layout of child blocks",
            Widget.FLOWGRAPH,
            writeable=["x", "y", "visible"],
        ).create_attribute_model()
        self.set_writeable_in(self.layout, ss.READY)
        self.field_registry.add_attribute_model("layout", self.layout,
                                                self.set_layout)
        # Create a design attribute for loading an existing layout
        self.design = ChoiceMeta("Design name to load",
                                 tags=[config_tag(),
                                       Widget.COMBO.tag()
                                       ]).create_attribute_model()
        self.field_registry.add_attribute_model("design", self.design,
                                                self.set_design)
        self.set_writeable_in(self.design, ss.READY)
        # Create an export table for mirroring exported fields
        self.exports = TableMeta.from_table(
            ExportTable,
            "Exported fields of child blocks",
            writeable=list(ExportTable.call_types),
        ).create_attribute_model()
        # Overwrite the sources meta to be a ChoiceArrayMeta
        self.exports.meta.elements["source"] = ChoiceArrayMeta(
            "Name of the block.field to export",
            writeable=True,
            tags=[Widget.COMBO.tag()],
        )
        self.set_writeable_in(self.exports, ss.READY)
        self.field_registry.add_attribute_model("exports", self.exports,
                                                self.set_exports)
        # Create read-only indicator for when things are modified
        self.modified = BooleanMeta("Whether the design is modified",
                                    tags=[Widget.LED.tag()
                                          ]).create_attribute_model()
        self.field_registry.add_attribute_model("modified", self.modified)
        # Create the save method
        self.set_writeable_in(self.field_registry.add_method_model(self.save),
                              ss.READY)

    def _run_git_cmd(self, *args, **kwargs):
        # Run git command, don't care if it fails, logging the output
        cwd = kwargs.get("cwd", self.config_dir)
        if self.use_git:
            try:
                output = subprocess.check_output(
                    ("git", ) + self.git_config + args, cwd=cwd)
            except subprocess.CalledProcessError as e:
                self.log.warning("Git command failed: %s\n%s", e, e.output)
                return None
            else:
                self.log.debug("Git command completed: %s", output)
                return output

    def do_init(self):
        super().do_init()
        # Try and make it a git repo, don't care if it fails
        self._run_git_cmd("init")
        # List the config_dir and add to choices
        self._set_layout_names()
        # If given a default config, load this
        if self.initial_design:
            self.do_load(self.initial_design, init=True)
        else:
            # This will trigger all parts to report their layout, making sure
            # the layout table has a valid value. This will also call
            # self._update_block_endpoints()
            self.set_default_layout()

    def set_default_layout(self):
        self.set_layout(LayoutTable([], [], [], [], []))

    def set_layout(self, value):
        """Set the layout table value. Called on attribute put"""
        # Can't do this with changes_squashed as it will call update_modified
        # from another thread and deadlock. Need RLock.is_owned() from update_*
        part_info = self.run_hooks(
            LayoutHook(p, c, self.port_info, value)
            for p, c in self.create_part_contexts(only_visible=False).items())
        with self.changes_squashed:
            layout_parts = LayoutInfo.filter_parts(part_info)
            name, mri, x, y, visible = [], [], [], [], []
            for part_name, layout_infos in layout_parts.items():
                for layout_info in layout_infos:
                    name.append(part_name)
                    mri.append(layout_info.mri)
                    x.append(layout_info.x)
                    y.append(layout_info.y)
                    visible.append(layout_info.visible)
            layout_table = LayoutTable(name, mri, x, y, visible)
            visibility_changed = layout_table.visible != self.layout.value.visible
            self.layout.set_value(layout_table)
            if self.saved_visibility is None:
                # First write of table, set layout and exports saves
                self.saved_visibility = layout_table.visible
                self.saved_exports = self.exports.value
                # Force visibility changed so we update_block_endpoints
                # even if there weren't any visible
                visibility_changed = True
            if visibility_changed:
                self.update_modified()
                self.update_exportable()
                # Part visibility changed, might have attributes or methods
                # that we need to hide or show
                self.update_block_endpoints()

    def set_exports(self, value):
        # Validate
        for export_name in value.export:
            assert CAMEL_RE.match(export_name), ("Field %r is not camelCase" %
                                                 export_name)
        with self.changes_squashed:
            self.exports.set_value(value)
            self.update_modified()
            self.update_block_endpoints()

    def update_modified(self,
                        part: Part = None,
                        info: PartModifiedInfo = None) -> None:
        with self.changes_squashed:
            if part:
                assert info, "No info to update part"
                # Update the alarm for the given part
                self.part_modified[part] = info
            # Find the modified alarms for each visible part
            message_list = []
            only_modified_by_us = True
            for part_name, visible in zip(self.layout.value.name,
                                          self.layout.value.visible):
                part = self.parts[part_name]
                info = self.part_modified.get(part, None)
                if visible and info:
                    for name, message in sorted(info.modified.items()):
                        # Attribute flagged as been modified, is it by the
                        # context we passed to the part?
                        if name in self.context_modified.get(part, {}):
                            message = "(We modified) %s" % (message, )
                        else:
                            only_modified_by_us = False
                        message_list.append(message)
            # Add in any modification messages from the layout and export tables
            if self.layout.value.visible != self.saved_visibility:
                message_list.append("layout changed")
                only_modified_by_us = False
            if self.exports.value != self.saved_exports:
                message_list.append("exports changed")
                only_modified_by_us = False
            if message_list:
                if only_modified_by_us:
                    severity = AlarmSeverity.NO_ALARM
                else:
                    severity = AlarmSeverity.MINOR_ALARM
                alarm = Alarm(severity, AlarmStatus.CONF_STATUS,
                              "\n".join(message_list))
                self.modified.set_value(True, alarm=alarm)
            else:
                self.modified.set_value(False)

    def update_exportable(self,
                          part: Part = None,
                          info: PartExportableInfo = None) -> None:
        with self.changes_squashed:
            if part:
                assert info, "No info to update part"
                self.part_exportable[part] = info.names
                self.port_info[part.name] = info.port_infos
            # If we haven't saved visibility yet these have been called
            # during do_init, so don't update block endpoints yet, this will
            # be done as a batch at the end of do_init
            if self.saved_visibility is not None:
                # Find the exportable fields for each visible part
                names = []
                for part in self.parts.values():
                    fields = self.part_exportable.get(part, [])
                    for attr_name in fields:
                        names.append("%s.%s" % (part.name, attr_name))
                changed_names = set(names).symmetric_difference(
                    self.exports.meta.elements["source"].choices)
                changed_exports = changed_names.intersection(
                    self.exports.value.source)
                self.exports.meta.elements["source"].set_choices(names)
                # Update the block endpoints if anything currently exported is
                # added or deleted
                if changed_exports:
                    self.update_block_endpoints()

    def update_block_endpoints(self):
        if self._current_part_fields:
            for name, child, _, _ in self._current_part_fields:
                self._block.remove_endpoint(name)
                for state, state_writeable in self._children_writeable.items():
                    state_writeable.pop(child, None)
        self._current_part_fields = tuple(self._get_current_part_fields())
        for name, child, writeable_func, needs_context in self._current_part_fields:
            self.add_block_field(name, child, writeable_func, needs_context)

    def add_part(self, part: Part) -> None:
        super().add_part(part)
        # Strip out the config tags of what we just added, as we will be
        # saving them ourself
        for name, field, _, _ in self.field_registry.fields.get(part, []):
            if isinstance(field, AttributeModel):
                tags = field.meta.tags
                if get_config_tag(tags):
                    # Strip off the "config" tags from attributes
                    field.meta.set_tags(without_config_tags(tags))
                    self.our_config_attributes[name] = field

    def add_initial_part_fields(self):
        # Only add our own fields to start with, the rest will be added on load
        for name, child, writeable_func, needs_context in self.field_registry.fields[
                None]:
            self.add_block_field(name, child, writeable_func, needs_context)

    def _get_current_part_fields(self):
        # Clear out the current subscriptions
        for subscription in self._subscriptions:
            controller = self.process.get_controller(subscription.path[0])
            unsubscribe = Unsubscribe(subscription.id)
            unsubscribe.set_callback(subscription.callback)
            controller.handle_request(unsubscribe)
        self._subscriptions = []

        # Find the mris of parts
        mris = {}
        invisible = set()
        for part_name, mri, visible in zip(self.layout.value.name,
                                           self.layout.value.mri,
                                           self.layout.value.visible):
            if visible:
                mris[part_name] = mri
            else:
                invisible.add(part_name)

        # Add fields from parts that aren't invisible
        for part_name, part in self.parts.items():
            if part_name not in invisible:
                for data in self.field_registry.fields.get(part, []):
                    yield data

        # Add exported fields from visible parts
        for source, export_name in self.exports.value.rows():
            part_name, attr_name = source.rsplit(".", 1)
            part = self.parts[part_name]
            # If part is visible, get its mri
            mri = mris.get(part_name, None)
            if mri and attr_name in self.part_exportable.get(part, []):
                if not export_name:
                    export_name = attr_name
                export, setter = self._make_export_field(
                    mri, attr_name, export_name)
                yield export_name, export, setter, False

    def _make_export_field(self, mri, attr_name, export_name):
        controller = self.process.get_controller(mri)
        path = [mri, attr_name]
        label = camel_to_title(export_name)
        ret = {}

        def update_field(response):
            if not isinstance(response, Delta):
                # Return or Error is the end of our subscription, log and ignore
                self.log.debug("Export got response %r", response)
                return
            if not ret:
                # First call, create the initial object
                export = deserialize_object(response.changes[0][1])
                if isinstance(export, AttributeModel):

                    def setter(v):
                        context = Context(self.process)
                        context.put(path, v)

                    # Strip out tags that we shouldn't export
                    # TODO: need to strip out port tags too...
                    export.meta.set_tags(
                        without_config_tags(
                            without_group_tags(export.meta.tags)))

                    ret["setter"] = setter
                else:

                    def setter_star_args(*args):
                        context = Context(self.process)
                        context.post(path, *args)

                    ret["setter"] = setter_star_args

                # Regenerate label
                export.meta.set_label(label)
                ret["export"] = export
            else:
                # Subsequent calls, update it
                with self.changes_squashed:
                    for change in response.changes:
                        ret["export"].apply_change(*change)

        subscription = Subscribe(path=path, delta=True)
        subscription.set_callback(update_field)
        self._subscriptions.append(subscription)
        # When we have waited for the subscription, the first update_field
        # will have been called
        controller.handle_request(subscription).wait()
        return ret["export"], ret["setter"]

    def create_part_contexts(self, only_visible=True):
        part_contexts = super().create_part_contexts()
        if only_visible:
            for part_name, visible in zip(self.layout.value.name,
                                          self.layout.value.visible):
                part = self.parts[part_name]
                if not visible:
                    part_contexts.pop(part)
                else:
                    part_contexts[part].set_notify_dispatch_request(
                        part.notify_dispatch_request)

        return part_contexts

    # Allow CamelCase for arguments as they will be exposed in the Block Method
    # noinspection PyPep8Naming
    @add_call_types
    def save(self, designName: ASaveDesign = "") -> None:
        """Save the current design to file"""
        self.try_stateful_function(ss.SAVING, ss.READY, self.do_save,
                                   designName)

    def do_save(self, design=""):
        if not design:
            design = self.design.value
        assert design, "Please specify save design name when saving from new"
        assert not design.startswith(
            "template_"), "Cannot save over a template"
        structure = OrderedDict()
        attributes = structure.setdefault("attributes", OrderedDict())
        # Add the layout table
        layout = attributes.setdefault("layout", OrderedDict())
        for name, mri, x, y, visible in self.layout.value.rows():
            layout_structure = OrderedDict()
            layout_structure["x"] = x
            layout_structure["y"] = y
            layout_structure["visible"] = visible
            layout[name] = layout_structure
        # Add the exports table
        exports = attributes.setdefault("exports", OrderedDict())
        for source, export in self.exports.value.rows():
            exports[source] = export
        # Add other attributes
        for name, attribute in self.our_config_attributes.items():
            attributes[name] = attribute.value
        # Add any structure that a child part wants to save
        structure["children"] = self.run_hooks(
            SaveHook(p, c)
            for p, c in self.create_part_contexts(only_visible=False).items())
        text = json_encode(structure, indent=2)
        filename = self._validated_config_filename(design)
        if filename.startswith("/tmp"):
            self.log.warning("Saving to tmp directory %s" % filename)
        with open(filename, "w") as f:
            f.write(text)
        # Run a sync command to make sure we flush this file to disk
        subprocess.call("sync")
        # Try and commit the file to git, don't care if it fails
        self._run_git_cmd("add", filename)
        msg = "Saved %s %s" % (self.mri, design)
        self._run_git_cmd("commit", "--allow-empty", "-m", msg, filename)
        self._mark_clean(design)

    def _set_layout_names(self, extra_name=None):
        names = [""]
        dir_name = self._make_config_dir()
        for f in os.listdir(dir_name):
            if os.path.isfile(os.path.join(dir_name,
                                           f)) and f.endswith(".json"):
                names.append(f.split(".json")[0])
        if extra_name and str(extra_name) not in names:
            names.append(str(extra_name))
        names.sort()
        if os.path.isdir(self.template_designs):
            for f in sorted(os.listdir(self.template_designs)):
                assert f.startswith("template_") and f.endswith(".json"), (
                    "Template design %s/%s should start with 'template_' "
                    "and end with .json" % (self.template_designs, f))
                t_name = f.split(".json")[0]
                if t_name not in names:
                    names.append(t_name)
        self.design.meta.set_choices(names)

    def _validated_config_filename(self, name):
        """Make config dir and return full file path and extension

        Args:
            name (str): Filename without dir or extension

        Returns:
            str: Full path including extension
        """

        if name.startswith("template_"):
            # Load from templates dir
            dir_name = self.template_designs
        else:
            # Load from config dir
            dir_name = self._make_config_dir()
        filename = os.path.join(dir_name, name.split(".json")[0] + ".json")
        return filename

    def _make_config_dir(self):
        dir_name = os.path.join(self.config_dir, self.mri)
        try:
            os.mkdir(dir_name)
        except OSError:
            # OK if already exists, if not then it will fail on write anyway
            pass
        return dir_name

    def set_design(self, value):
        value = self.design.meta.validate(value)
        self.try_stateful_function(ss.LOADING, ss.READY, self.do_load, value)

    def do_load(self, design: str, init: bool = False) -> None:
        """Load a design name, running the child LoadHooks.

        Args:
            design: Name of the design json file, without extension
            init: Passed to the LoadHook to tell the children if this is being
                run at Init or not
        """
        if design:
            filename = self._validated_config_filename(design)
            with open(filename, "r") as f:
                text = f.read()
            structure = json_decode(text)
        else:
            structure = {}
        # Attributes and Children used to be merged, support this
        attributes = structure.get("attributes", structure)
        children = structure.get("children", structure)
        # Set the layout table
        name, mri, x, y, visible = [], [], [], [], []
        for part_name, d in attributes.get("layout", {}).items():
            name.append(part_name)
            mri.append("")
            x.append(d["x"])
            y.append(d["y"])
            visible.append(d["visible"])
        self.set_layout(LayoutTable(name, mri, x, y, visible))
        # Set the exports table
        source, export = [], []
        for source_name, export_name in attributes.get("exports", {}).items():
            source.append(source_name)
            export.append(export_name)
        self.exports.set_value(ExportTable(source, export))
        # Set other attributes
        our_values = {
            k: v
            for k, v in attributes.items() if k in self.our_config_attributes
        }
        block = self.block_view()
        block.put_attribute_values(our_values)
        # Run the load hook to get parts to load their own structure
        self.run_hooks(
            LoadHook(p, c, children.get(p.name, {}), init)
            for p, c in self.create_part_contexts(only_visible=False).items())
        self._mark_clean(design, init)

    def _mark_clean(self, design, init=False):
        with self.changes_squashed:
            self.saved_visibility = self.layout.value.visible
            self.saved_exports = self.exports.value
            # Now we are clean, modified should clear
            if not init:
                # Don't clear at init, because some things may not be
                # clean at init
                self.part_modified = {}
            self.update_modified()
            self._set_layout_names(design)
            self.design.set_value(design)
            self.update_block_endpoints()
class StatefulController(BasicController):
    """A controller that implements `StatefulStates`"""

    # The state_set that this controller implements
    state_set = ss()

    def __init__(self, mri: AMri, description: ADescription = "") -> None:
        super().__init__(mri, description)
        self._children_writeable: ChildrenWriteable = {}
        self.state = ChoiceMeta(
            "StateMachine State of Block",
            self.state_set.possible_states,
            tags=[Widget.MULTILINETEXTUPDATE.tag()]
            # Start DISABLING so we can immediately go to DISABLED
        ).create_attribute_model(ss.DISABLING)
        self.field_registry.add_attribute_model("state", self.state)
        self.field_registry.add_method_model(self.disable)
        self.set_writeable_in(self.field_registry.add_method_model(self.reset),
                              ss.DISABLED, ss.FAULT)
        self.transition(ss.DISABLED)
        self.register_hooked(ProcessStartHook, self.init)
        self.register_hooked(ProcessStopHook, self.halt)

    def set_writeable_in(self, field, *states):
        # Field has defined when it should be writeable, just check that
        # this is valid for this state_set
        for state in states:
            assert (state in self.state_set.possible_states
                    ), "State %s is not one of the valid states %s" % (
                        state,
                        self.state_set.possible_states,
                    )
        for state in self.state_set.possible_states:
            state_writeable = self._children_writeable.setdefault(state, {})
            state_writeable[field] = state in states

    def create_part_contexts(self) -> Dict[Part, Context]:
        part_contexts = OrderedDict()
        assert self.process, "No attached process"
        for part in self.parts.values():
            part_contexts[part] = Context(self.process)
        return part_contexts

    def init(self):
        self.try_stateful_function(ss.RESETTING, ss.READY, self.do_init)

    def do_init(self):
        self.run_hooks(
            InitHook(part, context)
            for part, context in self.create_part_contexts().items())

    def halt(self):
        self.run_hooks(
            HaltHook(part, context)
            for part, context in self.create_part_contexts().items())
        self.disable()

    def disable(self):
        self.try_stateful_function(ss.DISABLING, ss.DISABLED, self.do_disable)

    def do_disable(self):
        self.run_hooks(
            DisableHook(part, context)
            for part, context in self.create_part_contexts().items())

    def reset(self):
        self.try_stateful_function(ss.RESETTING, ss.READY, self.do_reset)

    def do_reset(self):
        self.run_hooks(
            ResetHook(part, context)
            for part, context in self.create_part_contexts().items())

    def go_to_error_state(self, exception):
        if self.state.value != ss.FAULT:
            self.transition(ss.FAULT, str(exception))

    def check_field_writeable(self, field):
        try:
            super().check_field_writeable(field)
        except NotWriteableError as e:
            msg = "%s, maybe because Block state = %s" % (e, self.state.value)
            raise NotWriteableError(msg)

    def transition(self, state, message=""):
        """Change to a new state if the transition is allowed

        Args:
            state (str): State to transition to
            message (str): Message if the transition is to a fault state
        """
        with self.changes_squashed:
            initial_state = self.state.value
            if self.state_set.transition_allowed(initial_state=initial_state,
                                                 target_state=state):
                self.log.debug("%s: Transitioning from %s to %s", self.mri,
                               initial_state, state)
                if state == ss.DISABLED:
                    alarm = Alarm.invalid("Disabled")
                elif state == ss.FAULT:
                    alarm = Alarm.major(message)
                else:
                    alarm = Alarm()
                self.update_health(self, HealthInfo(alarm))
                self.state.set_value(state)
                self.state.set_alarm(alarm)
                for child, writeable in self._children_writeable[state].items(
                ):
                    child.meta.set_writeable(writeable)
            else:
                raise TypeError("Cannot transition from %s to %s" %
                                (initial_state, state))

    def try_stateful_function(self, start_state, end_state, func, *args,
                              **kwargs):
        try:
            self.transition(start_state)
            func(*args, **kwargs)
            self.transition(end_state)
        except Exception as e:  # pylint:disable=broad-except
            self.log.debug(
                "Exception running %s %s %s transitioning from %s to %s",
                func,
                args,
                kwargs,
                start_state,
                end_state,
                exc_info=True,
            )
            self.go_to_error_state(e)
            raise

    def add_block_field(self, name, child, writeable_func, needs_context):
        super().add_block_field(name, child, writeable_func, needs_context)
        # If we don't have a writeable func it can never be writeable
        if writeable_func is None:
            return
        # If we have already registered an explicit set then we are done
        for state in self.state_set.possible_states:
            state_writeable = self._children_writeable.get(state, {})
            if child in state_writeable:
                return
        # Field is writeable but has not defined when it should be
        # writeable, so calculate it from the possible states
        states = [
            state for state in self.state_set.possible_states
            if state not in (ss.DISABLING, ss.DISABLED)
        ]
        for state in self.state_set.possible_states:
            state_writeable = self._children_writeable.setdefault(state, {})
            state_writeable[child] = state in states
Exemple #21
0
def make_meta(subtyp, description, tags, writeable=True, labels=None):
    if subtyp == "enum":
        meta = ChoiceMeta(description, labels)
    elif subtyp == "bit":
        meta = BooleanMeta(description)
    elif subtyp in ("uint", ""):
        meta = NumberMeta("uint32", description)
    elif subtyp in ("int", "pos"):
        meta = NumberMeta("int32", description)
    elif subtyp == "scalar":
        meta = NumberMeta("float64", description)
    elif subtyp == "lut":
        meta = StringMeta(description)
    else:
        raise ValueError("Unknown subtype %r" % subtyp)
    meta.set_writeable(writeable)
    tags.append(meta.default_widget().tag())
    meta.set_tags(tags)
    return meta
Exemple #22
0
 def test_to_dict(self):
     bm = ChoiceMeta("desc", ["a", "b"], label="name")
     assert bm.to_dict() == self.serialized