def handle_changes(self, changes: Sequence[Tuple[str, str]]) -> None: ts = TimeStamp() # {block_name: {field_name: field_value}} block_changes: Dict[str, Any] = {} # {full_field: field_value} bus_changes = {} # Process bit outs that need changing bit_out_changes = self._bit_out_changes self._bit_out_changes = {} for k, v in bit_out_changes.items(): self._bit_outs[k] = v bus_changes[k] = v block_name, field_name = k.split(".") block_changes.setdefault(block_name, {})[field_name] = v # Work out which change is needed for which block for key, value in changes: self._handle_change(key, value, bus_changes, block_changes, bit_out_changes) # Notify the Blocks that they need to handle these changes if bus_changes: self.busses.handle_changes(bus_changes, ts) for block_name, block_changes_values in block_changes.items(): self._child_controllers[block_name].handle_changes( block_changes_values, ts)
def setUp(self): self.serialized = OrderedDict() self.serialized["typeid"] = "epics:nt/NTScalar:1.0" self.serialized["value"] = "some string" self.serialized["alarm"] = Alarm().to_dict() self.serialized["timeStamp"] = TimeStamp().to_dict() self.serialized["meta"] = StringMeta("desc").to_dict()
def test_block_fields_lut(self): fields = OrderedDict() block_data = BlockData(8, "Lut description", fields) fields["FUNC"] = FieldData("param", "lut", "Function", []) o = PandABlockController(self.client, "MRI", "LUT3", block_data, "/docs") self.process.add_controller(o) b = self.process.block_view("MRI:LUT3") func = b.func assert func.meta.writeable is True assert func.meta.typeid == StringMeta.typeid assert func.meta.tags == ["group:parameters", "widget:textinput", "config:1"] queue = Queue() subscribe = Subscribe(path=["MRI:LUT3"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get() assert delta.changes[0][1]["func"]["value"] == "" assert '<path id="OR"' in delta.changes[0][1]["icon"]["value"] # This is the correct FUNC.RAW value for !A&!B&!C&!D&!E self.client.get_field.return_value = "1" ts = TimeStamp() o.handle_changes({"FUNC": "!A&!B&!C&!D&!E"}, ts) self.client.get_field.assert_called_once_with("LUT3", "FUNC.RAW") delta = queue.get() assert delta.changes == [ [["func", "value"], "!A&!B&!C&!D&!E"], [["func", "timeStamp"], ts], [["icon", "value"], ANY], [["icon", "timeStamp"], ts], ] assert '<path id="OR"' not in delta.changes[2][1]
def test_scale_offset(self): ts = TimeStamp() changes = {"B1.P0.SCALE": "32", "B1.P0.OFFSET": "0.1", "B1.P0": "100"} self.o.handle_changes(changes, ts) assert self.o.positions.timeStamp is ts assert list(self.o.positions.value.rows())[0] == [ "B1.P0", 3200.1, "", 32.0, 0.1, PositionCapture.NO, ] self.o.handle_changes({"B1.P0.SCALE": "64"}, ts) assert list(self.o.positions.value.rows())[0] == [ "B1.P0", 6400.1, "", 64.0, 0.1, PositionCapture.NO, ] self.o.handle_changes({"B1.P0": "200"}, ts) assert list(self.o.positions.value.rows())[0] == [ "B1.P0", 12800.1, "", 64.0, 0.1, PositionCapture.NO, ]
def test_bits(self): ts = TimeStamp() changes = { "B1.B1": True, "B1.B3": True, } self.o.handle_changes(changes, ts) assert self.o.bits.timeStamp is ts assert list(self.o.bits.value.rows())[1] == ["B1.B1", True, False] assert list(self.o.bits.value.rows())[2] == ["B1.B2", False, False] assert list(self.o.bits.value.rows())[3] == ["B1.B3", True, False]
def update_attribute(self, block_name, field_name, value): ret = None parts = self._blocks_parts[block_name] if field_name not in parts: self.log.debug("Block %s has no field %s", block_name, field_name) return ret part = parts[field_name] attr = part.attr field_data = self._blocks_data[block_name].fields.get(field_name, None) if value == Exception: # TODO: set error self.log.warning("Field %s.%s in error", block_name, field_name) value = None # Cheaper than isinstance if attr.meta.typeid == TableMeta.typeid: value = part.table_from_list(value) elif attr.meta.typeid == BooleanMeta.typeid: value = bool(int(value)) is_bit_out = field_data and field_data.field_type == "bit_out" if is_bit_out and attr.value is value: # make bit_out things toggle while changing ret = value value = not value else: value = attr.meta.validate(value) # Update the value of our attribute and anyone listening ts = TimeStamp() attr.set_value_alarm_ts(value, Alarm.ok, ts) for dest_attr in self._listening_attrs.get(attr, []): dest_attr.set_value_alarm_ts(value, Alarm.ok, ts) # if we changed the value of a mux, update the slaved values if field_data and field_data.field_type in ("bit_mux", "pos_mux"): current_part = parts[field_name + ".CURRENT"] current_attr = current_part.attr self._update_current_attr( current_attr, value, ts, field_data.field_type) # if we changed a pos_out, its SCALE or OFFSET, update its scaled value root_field_name = field_name.split(".")[0] field_data = self._blocks_data[block_name].fields[root_field_name] if field_data.field_type == "pos_out": scale = parts[root_field_name + ".SCALE"].attr.value offset = parts[root_field_name + ".OFFSET"].attr.value scaled = parts[root_field_name].attr.value * scale + offset parts[root_field_name + ".SCALED"].attr.set_value_alarm_ts( scaled, Alarm.ok, ts) return ret
def _update_value(self, value): if not value.ok: self.attr.set_value(None, alarm=Alarm.invalid("PV disconnected")) else: if value.severity: alarm = Alarm(severity=value.severity, status=AlarmStatus.RECORD_STATUS, message="PV in alarm state") else: alarm = Alarm.ok # We only have a raw_stamp attr on monitor, the initial # caget with CTRL doesn't give us a timestamp ts = TimeStamp(*getattr(value, "raw_stamp", (None, None))) value = self.attr.meta.validate(value) self.attr.set_value_alarm_ts(value, alarm, ts)
def test_pos_capture(self): ts = TimeStamp() changes = { "B1.P2.CAPTURE": "Min Max Mean", "B1.P2.SCALE": "1", "B1.P2": "100" } self.o.handle_changes(changes, ts) assert list(self.o.positions.value.rows())[2] == [ "B1.P2", 100, "", 1.0, 0.0, PositionCapture.MIN_MAX_MEAN, ]
def setUp(self): elements = OrderedDict() elements["foo"] = StringArrayMeta(label="Foo").to_dict() elements["bar"] = StringArrayMeta().to_dict() meta = OrderedDict() meta["typeid"] = "malcolm:core/TableMeta:1.0" meta["description"] = "desc" meta["tags"] = [] meta["writeable"] = True meta["label"] = "my label" meta["elements"] = elements value = OrderedDict() value["typeid"] = "malcolm:core/Table:1.0" value["foo"] = ["foo1", "foo2"] value["bar"] = ["bar1", "bar2"] self.serialized = OrderedDict() self.serialized["typeid"] = "epics:nt/NTTable:1.0" self.serialized["labels"] = ["Foo", "bar"] self.serialized["value"] = value self.serialized["alarm"] = Alarm().to_dict() self.serialized["timeStamp"] = TimeStamp().to_dict() self.serialized["meta"] = meta
def _update_value(self, value): # Attribute value might not be raw PV, PV which triggered update is # passed as status if self._user_callback is not None: self._user_callback(value) if not value.ok: self.attr.set_value(self.attr.value, alarm=Alarm.disconnected("PV disconnected")) else: if value.severity: alarm = Alarm( severity=value.severity, status=AlarmStatus.RECORD_STATUS, message="PV in alarm state", ) else: alarm = Alarm.ok # We only have a raw_stamp attr on monitor, the initial # caget with CTRL doesn't give us a timestamp ts = TimeStamp(*getattr(value, "raw_stamp", (None, None))) value = self.attr.meta.validate(value) self.attr.set_value_alarm_ts(value, alarm, ts)
def test_set_timeStamp(self): timeStamp = TimeStamp() self.o.set_timeStamp(timeStamp) assert self.o.timeStamp == timeStamp
def test_bit_capture_change(self): ts = TimeStamp() changes = {"PCAP.BITS0.CAPTURE": "Value"} self.o.handle_changes(changes, ts) assert self.o.bits.value.capture == [True] * 6 + [False] * 3
def test_block_fields_adder(self): fields = OrderedDict() block_data = BlockData(2, "Adder description", fields) fields["INPA"] = FieldData("pos_mux", "", "Input A", ["A.OUT", "B.OUT"]) fields["INPB"] = FieldData("pos_mux", "", "Input B", ["A.OUT", "B.OUT"]) fields["DIVIDE"] = FieldData( "param", "enum", "Divide output", ["/1", "/2", "/4"] ) fields["OUT"] = FieldData("pos_out", "", "Output", ["No", "Capture"]) fields["HEALTH"] = FieldData("read", "enum", "What's wrong", ["OK", "Very Bad"]) o = PandABlockController(self.client, "MRI", "ADDER1", block_data, "/docs") self.process.add_controller(o) b = self.process.block_view("MRI:ADDER1") assert list(b) == [ "meta", "health", "icon", "label", "help", "inputs", "inpa", "inpb", "parameters", "divide", "outputs", "out", ] group = b.inputs assert group.meta.tags == ["widget:group", "config:1"] inpa = b.inpa assert inpa.meta.writeable is True assert inpa.meta.typeid == ChoiceMeta.typeid assert inpa.meta.tags == [ "group:inputs", "sinkPort:int32:ZERO", "widget:combo", "config:1", ] assert inpa.meta.choices == ["A.OUT", "B.OUT"] inpa.put_value("A.OUT") self.client.set_field.assert_called_once_with("ADDER1", "INPA", "A.OUT") self.client.reset_mock() divide = b.divide assert divide.meta.writeable is True assert divide.meta.typeid == ChoiceMeta.typeid assert divide.meta.tags == ["group:parameters", "widget:combo", "config:1"] assert divide.meta.choices == ["/1", "/2", "/4"] out = b.out assert out.meta.writeable is False assert out.meta.typeid == NumberMeta.typeid assert out.meta.dtype == "int32" assert out.meta.tags == [ "group:outputs", "sourcePort:int32:ADDER1.OUT", "widget:textupdate", ] queue = Queue() subscribe = Subscribe(path=["MRI:ADDER1", "out"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get(timeout=1) assert delta.changes[0][1]["value"] == 0 ts = TimeStamp() o.handle_changes({"OUT": "145"}, ts) delta = queue.get(timeout=1) assert delta.changes == [ [["value"], 145], [["timeStamp"], ts], ] subscribe = Subscribe(path=["MRI:ADDER1", "health"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get(timeout=1) assert delta.changes[0][1]["value"] == "OK" ts = TimeStamp() o.handle_changes({"HEALTH": "Very Bad"}, ts) delta = queue.get(timeout=1) assert delta.changes == [ [["value"], "Very Bad"], [["alarm"], Alarm.major("Very Bad")], [["timeStamp"], ts], ] o.handle_changes({"HEALTH": "OK"}, ts) delta = queue.get(timeout=1) assert delta.changes == [ [["value"], "OK"], [["alarm"], Alarm.ok], [["timeStamp"], ts], ]
def test_block_fields_pulse(self): fields = OrderedDict() block_data = BlockData(4, "Pulse description", fields) fields["DELAY"] = FieldData("time", "", "Time", []) fields["INP"] = FieldData("bit_mux", "", "Input", ["ZERO", "X.OUT", "Y.OUT"]) fields["OUT"] = FieldData("bit_out", "", "Output", []) fields["ERR_PERIOD"] = FieldData("read", "bit", "Error", []) o = PandABlockController(self.client, "MRI", "PULSE2", block_data, "/docs") self.process.add_controller(o) b = self.process.block_view("MRI:PULSE2") assert list(b) == [ "meta", "health", "icon", "label", "help", "parameters", "delay", "delayUnits", "inputs", "inp", "inpDelay", "outputs", "out", "readbacks", "errPeriod", ] assert b.meta.label == "Pulse description 2" assert b.label.value == "Pulse description 2" # check setting label b.label.put_value("A new label") assert b.meta.label == "A new label" assert b.label.value == "A new label" self.client.set_field.assert_called_once_with( "*METADATA", "LABEL_PULSE2", "A new label" ) self.client.set_field.reset_mock() # check updated with nothing o.handle_changes(dict(LABEL=""), ts=TimeStamp()) assert b.meta.label == "Pulse description 2" assert b.label.value == "Pulse description 2" self.client.set_field.assert_not_called() # check updated with something from the server o.handle_changes(dict(LABEL="A server label"), ts=TimeStamp()) assert b.meta.label == "A server label" assert b.label.value == "A server label" self.client.set_field.assert_not_called() help = b.help assert help.value == "/docs/build/pulse_doc.html" delay = b.delay assert delay.meta.writeable is True assert delay.meta.typeid == NumberMeta.typeid assert delay.meta.dtype == "float64" assert delay.meta.tags == ["group:parameters", "widget:textinput", "config:2"] units = b.delayUnits assert units.meta.writeable is True assert units.meta.typeid == ChoiceMeta.typeid assert units.meta.tags == ["group:parameters", "widget:combo", "config:1"] assert units.meta.choices == ["s", "ms", "us"] inp = b.inp assert inp.meta.writeable is True assert inp.meta.typeid == ChoiceMeta.typeid assert inp.meta.tags == [ "group:inputs", "sinkPort:bool:ZERO", "widget:combo", "badgevalue:plus:inpDelay:MRI:PULSE2", "config:1", ] assert inp.meta.choices == ["ZERO", "X.OUT", "Y.OUT"] delay = b.inpDelay assert delay.meta.writeable is True assert delay.meta.typeid == NumberMeta.typeid assert delay.meta.dtype == "uint8" assert delay.meta.tags == ["group:inputs", "widget:textinput", "config:1"] out = b.out assert out.meta.writeable is False assert out.meta.typeid == BooleanMeta.typeid assert out.meta.tags == [ "group:outputs", "sourcePort:bool:PULSE2.OUT", "widget:led", ] err = b.errPeriod assert err.meta.writeable is False assert err.meta.typeid == BooleanMeta.typeid assert err.meta.tags == ["group:readbacks", "widget:led"] queue = Queue() subscribe = Subscribe(path=["MRI:PULSE2", "inp"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get() assert delta.changes[0][1]["value"] == "ZERO" ts = TimeStamp() o.handle_changes({"INP": "X.OUT"}, ts) delta = queue.get() assert delta.changes == [ [["value"], "X.OUT"], [["timeStamp"], ts], [ ["meta", "tags"], [ "group:inputs", "sinkPort:bool:ZERO", "widget:combo", "badgevalue:plus:inpDelay:MRI:PULSE2", "config:1", "linkedvalue:out:MRI:X", ], ], ]