def sync_proxy(self, mri, block): """Abstract method telling the ClientComms to sync this proxy Block with its remote counterpart. Should wait until it is connected Args: mri (str): The mri for the remote block block (BlockModel): The local proxy Block to keep in sync """ # Send a root Subscribe to the server subscribe = Subscribe(path=[mri], delta=True) done_queue = Queue() def handle_response(response): # Called from tornado if not isinstance(response, Delta): # Return or Error is the end of our subscription, log and ignore self.log.debug("Proxy got response %r", response) done_queue.put(None) else: cothread.Callback(self._handle_response, response, block, done_queue) subscribe.set_callback(handle_response) IOLoopHelper.call(self._send_request, subscribe) done_queue.get(timeout=DEFAULT_TIMEOUT)
def test_pos_table_deltas(self): queue = Queue() subscribe = Subscribe(path=["P"], delta=True) subscribe.set_callback(queue.put) self.o.handle_request(subscribe) delta = queue.get() capture_enums = delta.changes[0][1]["positions"]["meta"]["elements"][ "capture"]["choices"] assert capture_enums[0] == PositionCapture.NO table = delta.changes[0][1]["positions"]["value"] assert table.name == ["COUNTER.OUT"] assert table.value == [0.0] assert table.scale == [1.0] assert table.offset == [0.0] assert table.capture == [PositionCapture.NO] self.o.handle_changes([("COUNTER.OUT", "20")]) delta = queue.get() assert delta.changes == [ [["positions", "value", "value"], [20.0]], [["positions", "timeStamp"], ANY], ] self.o.handle_changes([("COUNTER.OUT", "5"), ("COUNTER.OUT.SCALE", 0.5)]) delta = queue.get() assert delta.changes == [ [["positions", "value", "value"], [2.5]], [["positions", "value", "scale"], [0.5]], [["positions", "timeStamp"], ANY], ]
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 _start_client(self): # Called from cothread if self._conn is None: IOLoopHelper.call(self.recv_loop) self._connected_queue.get(timeout=self.connect_timeout) root_subscribe = Subscribe(path=[".", "blocks", "value"]) root_subscribe.set_callback(self._update_remote_blocks) IOLoopHelper.call(self._send_request, root_subscribe)
def init(self, context): # type: (AContext) -> None super(RunnableChildPart, self).init(context) # Monitor the child configure Method for changes subscription = Subscribe(path=[self.mri, "configure"], delta=True) subscription.set_callback(self.update_part_configure_args) # Wait for the first update to come in self.child_controller.handle_request(subscription).wait()
def onFirstConnect(self, pv: SharedPV) -> None: # Store the PV, but don't open it now, let the first Delta do this with self._lock: self.pv = pv path = [self.controller.mri] if self.field is not None: path.append(self.field) request = Subscribe(path=path, delta=True) request.set_callback(self.handle) # No need to wait for first update here self.controller.handle_request(request)
def on_init(self, context: AContext) -> None: self.child_controller = context.get_controller(self.mri) if self.stateful: # Wait for a while until the child is ready as it changes the # save state wait_for_stateful_block_init(context, self.mri) # Save what we have self.on_save(context) subscribe = Subscribe(path=[self.mri, "meta", "fields"]) subscribe.set_callback(self.update_part_exportable) # Wait for the first update to come in assert self.child_controller, "No child controller" self.child_controller.handle_request(subscribe).wait()
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 test_table_deltas(self): queue = Queue() subscribe = Subscribe(path=["P"], delta=True) subscribe.set_callback(queue.put) self.o.handle_request(subscribe) delta = queue.get() table = delta.changes[0][1]["bits"]["value"] assert table.name == ["TTLIN1.VAL", "TTLIN2.VAL", "PCOMP.OUT"] assert table.value == [False, False, False] assert table.capture == [False, False, False] self.o.handle_changes([("TTLIN1.VAL", "1")]) delta = queue.get() assert delta.changes == [ [["bits", "value", "value"], [True, False, False]], [["bits", "timeStamp"], ANY], ]
def test_counter_subscribe(self): q = Queue() # Subscribe to the value sub = Subscribe(id=20, path=["counting", "counter"], delta=False) sub.set_callback(q.put) self.controller.handle_request(sub) # Check initial return response = q.get(timeout=1.0) self.assertIsInstance(response, Update) assert response.id == 20 assert response.value["typeid"] == "epics:nt/NTScalar:1.0" assert response.value["value"] == 0 # Post increment() post = Post(id=21, path=["counting", "increment"]) post.set_callback(q.put) self.controller.handle_request(post) # Check the value updates... response = q.get(timeout=1) self.assertIsInstance(response, Update) assert response.id == 20 assert response.value["value"] == 1 # ... then we get the return response = q.get(timeout=1) self.assertIsInstance(response, Return) assert response.id == 21 assert response.value is None # Check we can put too put = Put(id=22, path=["counting", "counter", "value"], value=31) put.set_callback(q.put) self.controller.handle_request(put) # Check the value updates... response = q.get(timeout=1) self.assertIsInstance(response, Update) assert response.id == 20 assert response.value["value"] == 31 # ... then we get the return response = q.get(timeout=1) self.assertIsInstance(response, Return) assert response.id == 22 assert response.value is None # And that there isn't anything else with self.assertRaises(TimeoutError): q.get(timeout=0.05)
def test_concurrency(self): q = Queue() # Subscribe to the whole block sub = Subscribe(id=0, path=["mri"], delta=True) sub.set_callback(q.put) self.c.handle_request(sub) # We should get first Delta through with initial value r = q.get().to_dict() assert r["id"] == 0 assert len(r["changes"]) == 1 assert len(r["changes"][0]) == 2 assert r["changes"][0][0] == [] assert r["changes"][0][1]["meta"]["label"] == "My label" assert r["changes"][0][1]["label"]["value"] == "My label" # Do a Put on the label put = Put(id=2, path=["mri", "label", "value"], value="New", get=True) put.set_callback(q.put) self.c.handle_request(put) # Check we got two updates before the return r = q.get().to_dict() assert r["id"] == 0 assert len(r["changes"]) == 2 assert len(r["changes"][0]) == 2 assert r["changes"][0][0] == ["label", "value"] assert r["changes"][0][1] == "New" assert len(r["changes"][0]) == 2 assert r["changes"][1][0] == ["label", "timeStamp"] r = q.get().to_dict() assert r["id"] == 0 assert len(r["changes"]) == 1 assert len(r["changes"][0]) == 2 assert r["changes"][0][0] == ["meta", "label"] assert r["changes"][0][1] == "New" # Then the return r3 = q.get().to_dict() assert r3["id"] == 2 assert r3["value"] == "New"
def test_concurrent(self): q = Queue() request = Subscribe(id=40, path=["hello_block", "greet"], delta=True) request.set_callback(q.put) self.controller.handle_request(request) # Get the initial subscribe value inital = q.get(timeout=0.1) self.assertIsInstance(inital, Delta) assert inital.changes[0][1]["took"]["value"] == dict(sleep=0, name="") assert inital.changes[0][1]["returned"]["value"] == {"return": ""} # Do a greet request = Post(id=44, path=["hello_block", "greet"], parameters=dict(name="me", sleep=1)) request.set_callback(q.put) self.controller.handle_request(request) # Then an error request = Post(id=45, path=["hello_block", "error"]) request.set_callback(q.put) self.controller.handle_request(request) # We should quickly get the error response first response = q.get(timeout=1.0) self.assertIsInstance(response, Error) assert response.id == 45 # Then the long running greet delta response = q.get(timeout=3.0) self.assertIsInstance(response, Delta) assert len(response.changes) == 2 assert response.changes[0][0] == ["took"] took = response.changes[0][1] assert took.value == dict(sleep=1, name="me") assert took.present == ["name", "sleep"] assert took.alarm == Alarm.ok assert response.changes[1][0] == ["returned"] returned = response.changes[1][1] assert returned.value == {"return": "Hello me"} assert returned.present == ["return"] assert returned.alarm == Alarm.ok # Check it took about 1s to run assert abs(1 - (returned.timeStamp.to_time() - took.timeStamp.to_time())) < 0.4 # And it's response response = q.get(timeout=1.0) self.assertIsInstance(response, Return) assert response.id == 44 assert response.value == "Hello me"
def update_part_exportable(self, response: Response) -> None: # Get a child context to check if we have a config field assert self.child_controller, "No child controller" child = self.child_controller.block_view() spawned = [] if isinstance(response, Update): new_fields = response.value assert isinstance(new_fields, Sequence), f"Bad field list {new_fields}" elif isinstance(response, Return): # We got a return with None, so clear out all of the # config_subscriptions new_fields = [] else: self.log.warning("Got unexpected response {response}") return # Remove any existing subscription that is not in the new fields for subscribe in self.config_subscriptions.values(): attr_name = subscribe.path[-2] if attr_name not in new_fields: unsubscribe = Unsubscribe(subscribe.id) unsubscribe.set_callback(subscribe.callback) spawned.append(self.child_controller.handle_request(unsubscribe)) self.port_infos.pop(attr_name, None) # Add a subscription to any new field existing_fields = set(s.path[-2] for s in self.config_subscriptions.values()) for field in set(new_fields) - existing_fields: attr = getattr(child, field) if isinstance(attr, Attribute): # Cache tags here tags = attr.meta.tags # Check if the attribute has any port tags, and store for # when we are asked for LayoutInfo port_info = Port.port_tag_details(tags) info: PortInfo if port_info: is_source, port, extra = port_info if is_source: info = SourcePortInfo( name=field, port=port, connected_value=extra ) else: info = SinkPortInfo( name=field, port=port, disconnected_value=extra, value=attr.value, ) self.port_infos[field] = info # If we are config tagged then subscribe so we can calculate # if we are modified if self._unmanaged_attr(field) and get_config_tag(tags): if self.config_subscriptions: new_id = max(self.config_subscriptions) + 1 else: new_id = 1 subscribe = Subscribe(id=new_id, path=[self.mri, field, "value"]) subscribe.set_callback(self.update_part_modified) self.config_subscriptions[new_id] = subscribe spawned.append(self.child_controller.handle_request(subscribe)) # Wait for the first update to come in for every subscription for s in spawned: s.wait() port_infos = [self.port_infos[f] for f in new_fields if f in self.port_infos] assert self.registrar, "No registrar assigned" self.registrar.report(PartExportableInfo(new_fields, port_infos))
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", ], ], ]