def __init__(self): self.topic_prefix = 'iottalk/api' self.topic = '{}/req/#'.format(self.topic_prefix) self.client = JsonClient() self.client.on_connect = self._on_connect self.client.on_json_message = self.msg_dispatcher self.client.connect(config.mqtt_conf['host'], port=config.mqtt_conf['port']) self.gapi_client = {} # graph api client self.dapi_client = {} # device api client
def test_JsonClient_pub_sub(lock, topic): def on_message(client, userdata, msg): on_message.msg = msg userdata.release() on_message.msg = None # setup c = JsonClient(userdata=lock) c.connect('localhost') c.subscribe(topic) c.on_json_message = on_message c.publish_json(topic, {'answer': 42}) c.loop_start() lock.acquire() assert c.on_json_message assert on_message.msg.topic == topic assert on_message.msg.payload == {'answer': 42} # teardown c.disconnect() c.loop_stop()
class APIServer(object): ''' For the request topic, we use the pattern ``iottalk/api/req/<client-id>/...``. Each request topic has a corresponding *response* topic. The available api calls: - graph api - device api Graph API ---------------------------------------------------------------------- The topic of graph api call should be ``iottalk/api/req/<client-id>/graph/<graph-id>``. The ``detach`` opcode is also available on: - ``iottalk/api/req/<client-id>/graph`` - for detaching all graphs - ``iottalk/api/req/client_id`` - for detaching all services The graph operation has following json format:: { 'op': 'operation', 'other_op_param': 'value', } The available operation code: - ``attach`` - ``detach`` - ``add_funcs`` - ``rm_funcs`` - ``add_link`` - ``rm_link`` - ``set_join``: setup or changing the join function ``attach`` and ``detach`` request is for graph creating and destroying:: { 'op': 'attach|detach' } .. note:: Please mark the ``detach`` as last will and testamnet, in order to prevent memory leak. If we do not ``detach`` properly, the instance of ``iot.csm.device.Device`` will not be GC. ``attach`` and ``detach`` response:: { 'op': 'attach|detach', 'state': 'ok' } ``add_funcs`` request:: { 'op': 'add_funcs', 'codes': [ 'def f(): ...', 'def g(): ...' ], 'digests':[ 'SHA256 of f', 'SHA256 of g' ], } ``add_funcs`` response:: { 'op': 'add_funcs', 'state': 'ok', 'digests':[ 'SHA256 of f', 'SHA256 of g' ] } ``rm_funcs`` request:: { 'op': 'rm_funcs', 'digest': [ '...', ], } ``add_link`` and ``rm_link`` request:: { 'op': 'add_link|rm_link', 'da_id': 'uuid', 'idf|odf': 'feature_name', 'func': 'digest', // optional field } ``add_link`` and ``rm_link`` response:: { 'op': 'add_link|rm_link', 'da_id': 'uuid', 'idf|odf': 'feature_name', 'func': 'digest', // optional field 'state': 'ok', } ``set_join`` request:: { 'op': 'set_join', 'prev': 'function digest', // ``None`` for new first setup 'new': 'function digest', } ``set_join`` response:: { 'op': set_join', 'state': 'ok', 'new': 'function digest', } The error response has following general format:: { 'op': 'opcode', 'state': 'error', 'reason': 'error_reason', 'other_payload': '...', } Device API ---------------------------------------------------------------------- The topic of graph api call should be ``iottalk/api/req/<client-id>/device``. This api endpoint provide notification about device applications. The available operation code: - ``attach`` - ``detach`` - ``anno`` for announcement The ``detach`` opcode is also available on: - ``iottalk/api/req/<client-id>`` - for detaching all services ``attach`` ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Initialize this service and get the init state of device applications. And the following device application state changes will be revealed on this channel. Request:: { 'op': 'attach', } If attach success, we will got following response. If more than one attach request arrive, we will ignore the lagger. :: { 'op': 'attach', 'state': 'ok', 'da_list': { 'id': { 'name': ..., 'id': ..., 'idf_list': ..., 'odf_list': ..., }, }, } ``detach`` ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .. note:: Please mark this ``detach`` as last will and testamnet, in order to prevent memory leak. If we do not ``detach`` properly, the instance of ``iot.csm.device.Device`` will not be GC. Request:: { 'op': 'detach', } Response:: { 'op': 'detach', 'state': 'ok', } ``anno`` ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Client will recieve announcement on the response channel if there is any D.A. state changes. There are some types of announcement message: - ``new`` - ``change`` - ``remove`` Type ``new``:: { 'op': 'anno', 'timestamp': '...', 'type': 'new', 'da_id': { ... // new and whole state } } Type ``change``:: { 'op': 'anno', 'timestamp': '...', 'type': 'change', 'da_id': { ... // changed field(s) } } Type ``remove``:: { 'op': 'anno', 'timestamp': '...', 'type': 'remove', 'da_list': ['da_id', ... ], } ''' def __init__(self): self.topic_prefix = 'iottalk/api' self.topic = '{}/req/#'.format(self.topic_prefix) self.client = JsonClient() self.client.on_connect = self._on_connect self.client.on_json_message = self.msg_dispatcher self.client.connect(config.mqtt_conf['host'], port=config.mqtt_conf['port']) self.gapi_client = {} # graph api client self.dapi_client = {} # device api client def _on_connect(self, client, userdata, flags, rc): self.client.subscribe(self.topic, qos=2) log.debug('api server connected') def loop_forever(self): with suppress(KeyboardInterrupt): self.client.loop_forever() def msg_add_callback(self, topic): ''' Get a helper function for add/remove on_message callback Available function attribute: :f.client: the mqtt client instance :f.callback: the set callback function :f.topic: the mqtt topic :f.remove_callback: function for remove callback ''' def f(callback): if f.callback is not None: raise RuntimeError( 'message callback for {!r} already set'.format(f.topic)) f.callback = mqtt_json_payload(callback, raise_error=False) f.client.message_callback_add(f.topic, f.callback) return f.callback f.topic = topic f.client = self.client f.callback = None f.remove_callback = partial(self.client.message_callback_remove, f.topic) return f def msg_dispatcher(self, client, userdata, msg): log.debug('on api server message: \n%r:\n%r', msg.topic, msg.payload) topic = msg.topic.split('/')[3:] if len(topic) == 1: client_id, api_type = topic[0], 'service' elif 2 <= len(topic) <= 3: client_id, api_type = topic[:2] else: log.warning('Invalid topic %r', topic) return if api_type == 'service': # accept the detach message if msg.payload.get('op') == 'detach': return self.detach_all(client_id) elif api_type == 'graph': if len(topic) == 3: graph_id = topic[2] return self.attach_graph_api(client_id, graph_id, msg) # maybe a detach message if msg.payload.get('op') == 'detach': return self.detach_graph_api(client_id) elif api_type == 'device': return self.attach_dev_api(client_id, msg) else: log.warning('Unknown api type: %r', api_type) return def graph_res_topic(self, client_id, graph_id=None): ''' The response topic of graph api service :param client_id: <clien-id> in the topic :param graph_id: <graph-id> in the topic ''' return '{}/res/{}/graph/{}'.format( self.topic_prefix, client_id, graph_id if graph_id else '') def attach_graph_api(self, client_id, graph_id, msg): ''' Attach to initiate ``iot.csm.graph.Graph`` :param client_id: <clien-id> in the topic :param graph_id: <graph-id> in the topic :param msg: the mqtt msg object ''' res_topic = self.graph_res_topic(client_id, graph_id) publish = partial(self.client.publish_json, res_topic) if client_id not in self.gapi_client: log.info('New client connected: %s', client_id) # init a empty dict for storing graph map # FIXME: potential race condiction when creating gapi_client self.gapi_client[client_id] = {} api_client = self.gapi_client[client_id] # FIXME: if the graph_id in api_client, imply that the Graph in woring. # and maybe no message will be heard here if graph_id not in api_client: # check attach info opcode = msg.payload.get('op') if opcode != 'attach': reason = 'Unknown graph id {}'.format(graph_id) log.warning(reason) msg.payload['state'] = 'error' msg.payload['reason'] = reason publish(msg.payload) return log.info('New graph added: %s', graph_id) # init a new graph object api_client[graph_id] = Graph( req=self.msg_add_callback(msg.topic), res=publish, destroy_cb=self.detach_graph_api_cb(client_id, graph_id)) msg.payload['state'] = 'ok' publish(msg.payload) def detach_graph_api_cb(self, client_id, graph_id): ''' The function factory of detach callback for graph api :param client_id: <clien-id> in the topic :param graph_id: <graph-id> in the topic ''' def cb(): del self.gapi_client[client_id][graph_id] return cb def detach_graph_api(self, client_id): ''' Handle the last will and testamnet of graph api connection. If we got detach msg on ``iottalk/api/<client-id>/graph``, delete all the graphs related to this client. ''' log.info('Destroy whole graph api services on client %r', client_id) for graph in tuple(self.gapi_client[client_id].values()): graph.detach() self.gapi_client[client_id].clear() del self.gapi_client[client_id] topic = self.graph_res_topic(client_id) self.client.publish_json(topic, {'op': 'detach', 'state': 'ok'}) def attach_dev_api(self, client_id, msg): ''' :param client_id: <client-id> part in the topic :param msg: the mqtt msg object ''' if self.dapi_client.get(client_id) is not None: log.warning('The device api service is not working on %r ?', client_id) return res_topic = 'iottalk/api/res/{}/device'.format(client_id) publish = partial(self.client.publish_json, res_topic) log.info('New device api service for %r', client_id) self.dapi_client[client_id] = Device( req=self.msg_add_callback(msg.topic), res=publish, destroy_cb=self.detach_dev_api_cb(client_id)) def detach_dev_api_cb(self, client_id): ''' The function factory of detach callback for device api :param client_id: <client-id> part in the topic ''' def cb(): del self.dapi_client[client_id] return cb def detach_all(self, client_id): ''' Detach all the api services. ''' if client_id in self.gapi_client: self.detach_graph_api(client_id) if client_id in self.dapi_client: self.dapi_client[client_id].detach()