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 _subscribe_to_block(self, block_name): self.client_comms = self.process.get_client_comms(block_name) assert self.client_comms, \ "Process doesn't know about block %s" % block_name request = Subscribe(None, self, [block_name], delta=True) request.set_id(self.BLOCK_ID) self.client_comms.q.put(request)
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 do_init(self): super(WebsocketClientComms, self).do_init() self.loop = IOLoop() self._request_lookup = {} self._subscription_keys = {} self._connected_queue = Queue() root_subscribe = Subscribe(id=0, path=[".", "blocks"], callback=self._update_remote_blocks) self._subscription_keys[root_subscribe.generate_key()] = root_subscribe self._request_lookup[0] = (root_subscribe, 0) self.start_io_loop()
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 __init__(self, process, block, block_name): """ Args: process (Process): The process this should run under block (Block): The local block we should be controlling block_name (str): The local block's name """ super(ClientController, self).__init__(block=block, process=process, block_name=block_name) request = Subscribe( None, self, [self.process.name, "remoteBlocks", "value"]) request.set_id(self.REMOTE_BLOCKS_ID) self.process.q.put(request)
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 __init__(self, process, block, block_name): """ Args: process (Process): The process this should run under block (Block): The local block we should be controlling block_name (str): The local block's name """ super(ClientController, self).__init__(block=block, process=process, block_name=block_name) request = Subscribe(None, self, [self.process.name, "remoteBlocks", "value"]) request.set_id(self.REMOTE_BLOCKS_ID) self.process.q.put(request)
def init(self): subscribe = Subscribe(path=[self.params.mri], delta=True, callback=self.handle_response) self.client_comms.send_to_server(subscribe) # Wait until connected self._first_response_queue.get(timeout=5)
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_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): # Get a child context to check if we have a config field child = self.child_controller.block_view() spawned = [] if response.value: new_fields = response.value else: new_fields = [] # 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, 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): for tag in attr.meta.tags: match = port_tag_re.match(tag) if match: d, type, extra = match.groups() self.port_infos[field] = PortInfo(name=field, value=attr.value, direction=d, type=type, extra=extra) if isinstance(attr, Attribute) and config() in attr.meta.tags: if self.config_subscriptions: new_id = max(self.config_subscriptions) + 1 else: new_id = 1 subscribe = Subscribe(id=new_id, path=[self.params.mri, field, "value"], callback=self.update_part_modified) self.config_subscriptions[new_id] = subscribe # Signal that any change we get is a difference if field not in self.saved_structure: self.saved_structure[field] = None spawned.append(self.child_controller.handle_request(subscribe)) # Wait for the first update to come in for s in spawned: s.wait() # Put data on the queue, so if spawns are handled out of order we # still get the most up to date data port_infos = [ self.port_infos[f] for f in new_fields if f in self.port_infos ] self.exportable_update_queue.put((new_fields, port_infos)) self.spawn(self._update_part_exportable).wait()
def init(self, context): # Save what we have self.save(context) # Monitor the child configure for changes self.child_controller = context.get_controller(self.params.mri) subscribe = Subscribe(path=[self.params.mri, "meta", "fields"], callback=self.update_part_exportable) # Wait for the first update to come in self.child_controller.handle_request(subscribe).wait()
def init(self, context): self.configure_args_update_queue = Queue() super(RunnableChildPart, self).init(context) # Monitor the child configure Method for changes self.serialized_configure = MethodModel().to_dict() subscription = Subscribe( path=[self.params.mri, "configure"], delta=True, callback=self.update_part_configure_args) # Wait for the first update to come in self.child_controller.handle_request(subscription).wait()
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 __init__(self, mri, controller, request): super(PvaMonitorImplementation, self).__init__(mri, controller, request) self._mu = pvaccess.MonitorServiceUpdater() self.getPVStructure() self._do_update = False path, _ = self._dict_to_path_value(self._request) request = Subscribe(path=[self._mri] + path, delta=True, callback=self._on_response) self._controller.handle_request(request)
def __init__(self, process, block): QAbstractItemModel.__init__(self) self.controller = process.get_controller(block.mri) self.block = block self.id_ = 1 self.root_item = BlockItem((self.block.mri,), block) # map id -> item self.item_lookup = {} # TODO: unsubscribe when done self.response_received.connect(self.handle_response) self.send_request(Subscribe(path=[self.block.mri], delta=True)).wait()
def __init__(self, process, block): QAbstractItemModel.__init__(self) self.process = process self.block = block self.id_ = 1 self.block_path = tuple(block.path_relative_to(process)) self.root_item = BlockItem(self.block_path, block) # map id -> request self.requests = {} # TODO: unsubscribe when done self.response_received.connect(self.handle_response) request = Subscribe(None, None, self.block_path, delta=True) self.send_request(request)
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_counter_subscribe(self): q = Queue() sub = Subscribe(id=20, path=["counting", "counter"], delta=False, callback=q.put) self.controller.handle_request(sub) 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 = Post(id=21, path=["counting", "increment"], callback=q.put) self.controller.handle_request(post) response = q.get(timeout=1) self.assertIsInstance(response, Update) assert response.id == 20 assert response.value["value"] == 1 response = q.get(timeout=1) self.assertIsInstance(response, Return) assert response.id == 21 assert response.value == None with self.assertRaises(TimeoutError): q.get(timeout=0.05)
def _make_export_field(self, mri, attr_name): controller = self.process.get_controller(mri) path = [mri, attr_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 ret["export"] = deserialize_object(response.changes[0][1]) context = Context(self.process) if isinstance(ret["export"], AttributeModel): def setter(v): context.put(path, v) else: def setter(*args): context.post(path, *args) ret["setter"] = setter else: # Subsequent calls, update it with self.changes_squashed: for cp, value in response.changes: ob = ret["export"] for p in cp[:-1]: ob = ob[p] getattr(ob, "set_%s" % cp[-1])(value) subscription = Subscribe(path=path, delta=True, 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 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 subscribe_server_blocks(self): """Subscribe to process blocks""" request = Subscribe(None, None, [".", "blocks", "value"]) request.set_id(self.SERVER_BLOCKS_ID) self.send_to_server(request)
def test_handle_request(self): q = Queue() request = Get(id=41, path=["mri", "myAttribute"]) request.set_callback(q.put) self.o.handle_request(request) response = q.get(timeout=0.1) self.assertIsInstance(response, Return) assert response.id == 41 assert response.value["value"] == "hello_block" self.part.my_attribute.meta.writeable = False request = Put( id=42, path=["mri", "myAttribute"], value="hello_block2", get=True ) request.set_callback(q.put) self.o.handle_request(request) response = q.get(timeout=0.1) self.assertIsInstance(response, Error) # not writeable assert response.id == 42 self.part.my_attribute.meta.writeable = True self.o.handle_request(request) response = q.get(timeout=0.1) self.assertIsInstance(response, Return) assert response.id == 42 assert response.value == "hello_block2" request = Post(id=43, path=["mri", "method"]) request.set_callback(q.put) self.o.handle_request(request) response = q.get(timeout=0.1) self.assertIsInstance(response, Return) assert response.id == 43 assert response.value == "world" # cover the controller._handle_post path for parameters request = Post(id=43, path=["mri", "method"], parameters={"dummy": 1}) request.set_callback(q.put) self.o.handle_request(request) response = q.get(timeout=0.1) self.assertIsInstance(response, Error) assert response.id == 43 assert ( str(response.message) == "Given keys ['dummy'], some of which aren't in allowed keys []" ) request = Subscribe(id=44, path=["mri", "myAttribute"], delta=False) request.set_callback(q.put) self.o.handle_request(request) response = q.get(timeout=0.1) self.assertIsInstance(response, Update) assert response.id == 44 assert response.value["typeid"] == "epics:nt/NTScalar:1.0" assert response.value["value"] == "hello_block2" request = Unsubscribe(id=44) request.set_callback(q.put) self.o.handle_request(request) response = q.get(timeout=0.1) self.assertIsInstance(response, Return) assert response.id == 44
def _subscribe(self, block, attribute): controller = self.process.get_controller(block) request = Subscribe(path=[block, attribute, "value"], callback=self._on_response) controller.handle_request(request)
def subscribe_server_blocks(self, _): """Subscribe to process blocks""" request = Subscribe(None, None, [".", "blocks", "value"]) request.set_id(self.SERVER_BLOCKS_ID) self.loop.add_callback(self.send_to_server, request)