{"action":"created","comment":{"url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/comments/184842881","pull_request_review_id":116124315,"id":184842881,"diff_hunk":"@@ -629,513 +632,819 @@ class AzureIoTHub {\n \n     //------------------------------------------------------------------------------\n \n-    // Client transport class\n-    // NOTE: This class differs from the original Node.js SDK\n-    ClientAMQP = class {\n-\n-        _config = null;\n-        _connection = null;\n-        _sessions = null;\n-        _senders = null;\n-        _receivers = null;\n-        _transfers = null;\n-        _handlers = null;\n-        _msgQueue = null;\n-\n-        _connecting = false;\n-        _senderTokenError = false;\n-        _receiverTokenError = false;\n-        _debug = false;\n-\n-        constructor(config) {\n-            _config = config;\n-            _resetConnectionTables();\n-        }\n-\n-        // done has one param (error)\n-        // onDisconnect has one param (error)\n-        function connect(done = null, onDisconnect = null) {\n-            // set connecting flag\n-            _connecting = true;\n-\n-            // set callbacks\n-            _handlers.onConnected <- done;\n-            _handlers.onDisconnect <- onDisconnect;\n-\n-            // TODO: create a state machine to clean up tracking connection status\n-            // Don't open a connection if one is already open\n-            if( _isOpen(_connection) ) {\n-                if ( _isOpen(_sessions.auth) ) {\n-                    if ( _isOpen(_receivers.auth) ) {\n-                        if ( _isOpen(_senders.auth) ) {\n-                            if ( _isOpen(_sessions.event) ) {\n-                                if ( _isOpen(_senders.event) ) {\n-                                    _connectionCallback(null);\n-                                } else {\n-                                    _openEventSender();\n-                                }\n-                            } else {\n-                                _openEventSession();\n-                            }\n-                        } else {\n-                            _openAuthSender();\n-                        }\n-                    } else {\n-                        _openAuthReceiver();\n-                    }\n-                } else {\n-                    _openAuthSession();\n-                }\n-            } else {\n-                _connection = amqp.openconnection(\"amqps://\" + _config.host, _amqpConnectionStatusHandler.bindenv(this));\n-            }\n-        }\n+    Client = class {\n \n-        function disconnect() {\n-            _connection.close();\n-            _resetConnectionTables();\n-        }\n+        _debugEnabled               = false;\n+\n+        _connStrParsed              = null;\n+        _options                    = null;\n+        _mqttclient                 = null;\n+        _state                      = AZURE_IOT_CLIENT_STATE_DISCONNECTED;\n+        _topics                     = null;\n+\n+        // Long term user callbacks. Like onReceive, onRequest, onMethod\n+        _onConnectCb                = null;\n+        _onDisconnectCb             = null;\n+        _onMessageCb                = null;\n+        _onTwinReqCb                = null;\n+        _onMethodCb                 = null;\n+\n+        // Short term user callbacks. Like onComplete, onRetrieve\n+        // User can send several messages in parallel, so we need a map reqId -> <callback>\n+        _msgSentCbMap               = null;\n+        _msgEnabledCb               = null;\n+        _twinEnabledCb              = null;\n+        // Contains [<reqId>, <callback>, <timer>] or null\n+        _twinRetrCb                 = null;\n+        // User can update twin several times in parallel, so we need a map reqId -> [<callback>, <timer>]\n+        _twinUpdCbMap               = null;\n+        _dMethodEnabledCb              = null;\n+\n+        _reqNum                     = 0;\n+\n+\n+        // MQTT Client class constructor.\n+        //\n+        // Parameters:\n+        //     deviceConnStr : String       Device connection string: includes the host name to connect, the device Id and the shared access string.\n+        //                                  It can be obtained from the Azure Portal.\n+        //                                  However, if the device was registered using the AzureIoTHub.Registry class,\n+        //                                  the deviceConnectionString parameter can be retrieved from the AzureIoTHub.Device instance passed\n+        //                                  to the AzureIoTHub.Registry.get() or AzureIoTHub.Registry.create() method callbacks.\n+        //                                  For more guidance, please see the AzureIoTHub.registry example (README.md).\n+        //     onConnect : Function         Callback called every time the device is connected.\n+        //                                  The callback signature:\n+        //                                  onConnect(error), where\n+        //                                      error : Integer     0 if the connection is successful, an error code otherwise.\n+        //     onDisconnect : Function      Callback called every time the device is disconnected\n+        //          (optional)              The callback signature:\n+        //                                  onDisconnect(error), where\n+        //                                      error : Integer     0 if the disconnection was caused by the disconnect() method,\n+        //                                                          an error code which explains a reason of the disconnection otherwise.\n+        //     options : Table              Key-value table with optional settings.\n+        //          (optional)\n+        //\n+        // Returns:                         AzureIoTHub.Client instance created.\n+        constructor(deviceConnStr, onConnect, onDisconnect = null, options = {}) {\n+            _msgSentCbMap = {};\n+            _twinUpdCbMap = {};\n+            // TODO: Should we define options' names as constants?\n+            _options = {\n+                \"qos\" : AZURE_IOT_CLIENT_DEFAULT_QOS,\n+                \"timeout\" : AZURE_IOT_CLIENT_DEFAULT_TIMEOUT,\n+                \"twinUpdParallelReqs\" : AZURE_IOT_CLIENT_DEFAULT_TWIN_UPD_PARAL_REQS,\n+                \"sendMsgParallelReqs\" : AZURE_IOT_CLIENT_DEFAULT_MSG_SEND_PARAL_REQS\n+            };\n \n-        // done cb params - err\n-        function sendEvent(message, done = null) {\n-            if ( !_isOpen(_sessions.event) ) {\n-                if (done) done(\"Cannot send while disconnected.\");\n-            } else {\n-                if ( !_isOpen(_senders.event) ) {\n-                    _log(\"creating event sender\");\n-                    // add message to queue\n-                    _msgQueue.push({\"msg\" : message, \"cb\" : done});\n-                    // create a new sender & send message\n-                    _openEventSender();\n-                } else {\n-                    _log(\"event sender open\")\n-                    _sendEvent(message, done);\n-                }\n-            }\n-        }\n+            _onConnectCb      = onConnect;\n+            _onDisconnectCb   = onDisconnect;\n \n-        // done is called everytime a message is available, params - error, data\n-        function receive(done) {\n-            _handlers.onEvent <- done;\n+            _connStrParsed = AzureIoTHub.ConnectionString.Parse(deviceConnStr);\n+            _mqttclient = mqtt.createclient();\n+            _mqttclient.onconnect(_onConnect.bindenv(this));\n+            _mqttclient.onconnectionlost(_onDisconnect.bindenv(this));\n+            _mqttclient.onmessage(_onMessage.bindenv(this));\n \n-            if (done == null) {\n-                _receivers.event.close();\n-                // clear out handlers\n-                return this;\n+            foreach (optName, optVal in options) {\n+                _options[optName] <- optVal;\n             }\n \n-            if( !_isOpen(_sessions.event) ) {\n-                _handlers.onEvent(\"Cannot receive while disconnected.\", null);\n-            } else {\n-                // TODO: test/handle if receiver already open\n-                if ( _isOpen(_receivers.event) ) _receivers.event.close();\n-                _openEventReceiver();\n-            }\n-            return this;\n+            _fillTopics(_connStrParsed.DeviceId);\n         }\n \n-        // PRIVATE FUNCTIONS\n-        // ------------------------------------------------------------------------------------\n-        // ------------------------------------------------------------------------------------\n+        // Opens a connection to Azure IoT Hub.\n+        //\n+        // Returns:                         Nothing.\n+        function connect() {\n+            if (_state == AZURE_IOT_CLIENT_STATE_DISCONNECTED) {\n+                _log(\"Connecting...\");\n \n-        // Open Auth/Event Session/Sender/Receivers\n-        // ------------------------------------------------------------------------------------\n+                local devPath = \"/\" + _connStrParsed.DeviceId;\n+                local username = _connStrParsed.HostName + devPath + format(\"/api-version=%s\", AZURE_API_VERSION);\n+                local resourcePath = \"/devices\" + devPath + format(\"/api-version=%s\", AZURE_API_VERSION);\n+                local resourceUri = AzureIoTHub.Authorization.encodeUri(_connStrParsed.HostName + resourcePath);\n+                local passwDeadTime = AzureIoTHub.Authorization.anHourFromNow();\n+                local sas = AzureIoTHub.SharedAccessSignature.create(\n+                    resourceUri, null, _connStrParsed.SharedAccessKey, passwDeadTime).toString();\n \n-        function _openAuthSession() {\n-            _sessions.auth <- _connection.opensession(_amqpAuthSessionStatusHandler.bindenv(this));\n-        }\n+                local options = {\n+                    \"username\" : username,\n+                    \"password\" : sas\n+                };\n \n-        function _openAuthReceiver() {\n-            _receivers.auth <- _sessions.auth.openreceiver(AZURE_CLAIM_BASED_SECURITY_PATH, _amqpAuthReceiverStatusHandler.bindenv(this), _authReceiverCB.bindenv(this));\n-        }\n+                local url = \"ssl://\" + _connStrParsed.HostName;\n \n-        function _openAuthSender() {\n-            _senders.auth <- _sessions.auth.opensender(AZURE_CLAIM_BASED_SECURITY_PATH, _amqpAuthSenderStatusHandler.bindenv(this));\n-        }\n+                _state =  AZURE_IOT_CLIENT_STATE_DISCONNECTED | AZURE_IOT_CLIENT_STATE_CONNECTING;\n \n-        function _openEventSession() {\n-            _sessions.event <- _connection.opensession(_amqpEventSessionStatusHandler.bindenv(this));\n+                _mqttclient.connect(url, _connStrParsed.DeviceId, options);\n+            } else {\n+                try {\n+                    // We are connected or connecting now\n+                    _onConnectCb(_state & AZURE_IOT_CLIENT_STATE_CONNECTED ? AZURE_IOT_CLIENT_ERROR_ALREADY_CONNECTED : AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW);\n+                } catch (e) {\n+                    _error(\"Exception at onConnect user callback: \" + e);\n+                }\n+            }\n         }\n \n-        function _openEventSender() {\n-            _senders.event <- _sessions.event.opensender(AzureIoTHub.Endpoint.eventPath(_config.deviceId), _amqpEventSenderStatusHandler.bindenv(this));\n+        // Closes the connection to Azure IoT Hub. Does nothing if the connection is already closed.\n+        //\n+        // Returns:                         Nothing.\n+        function disconnect() {\n+            if (_state != AZURE_IOT_CLIENT_STATE_DISCONNECTED && !(_state & AZURE_IOT_CLIENT_STATE_DISCONNECTING)) {\n+                _state = _state | AZURE_IOT_CLIENT_STATE_DISCONNECTING;\n+                _mqttclient.disconnect(_onDisconnect.bindenv(this));\n+            }\n         }\n \n-        function _openEventReceiver() {\n-            _receivers.event <- _sessions.event.openreceiver(AzureIoTHub.Endpoint.messagePath(_config.deviceId), _amqpEventReceiverStatusHandler.bindenv(this), function(deliveries) {\n-                _handleDeliveries(deliveries, _handlers.onEvent);\n-            }.bindenv(this));\n-        }\n+        // Checks if the client is connected to Azure IoT Hub.\n+        //\n+        // Returns:                         Boolean: true if the client is connected, false otherwise.\n+        function isConnected() {\n+            return _state & AZURE_IOT_CLIENT_STATE_CONNECTED > 0;\n+        }\n+\n+        // Sends a message to Azure IoT Hub (https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#sending-device-to-cloud-messages).\n+        //\n+        // Parameters:\n+        //     msg : AzureIoTHub.Message    Message to send.\n+        //     onComplete : Function        Callback called when the operation is completed or an error happens.\n+        //          (optional)              The callback signature:\n+        //                                  onComplete(error), where\n+        //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+        //\n+        // Returns:                         Nothing.\n+        function sendMessage(msg, onComplete = null) {\n+            if ((_state & AZURE_IOT_CLIENT_STATE_CONNECTED) && !(_state & AZURE_IOT_CLIENT_STATE_DISCONNECTING)) {\n+                if (_msgSentCbMap.len() >= _options.sendMsgParallelReqs) {\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW);\n+                    return;\n+                }\n+                // TODO: http.urlencode follows RFC3986, but Azure expects RFC2396 string. Should we worry about it?\n+                local props = \"\";\n+                if (msg.getProperties() != null) {\n+                    props = http.urlencode(msg.getProperties());\n+                }\n+                local topic = _topics.msgSend + props;\n+                local reqId = _reqNum;\n+                _reqNum++;\n \n-        // Status Handlers\n-        // ------------------------------------------------------------------------------------\n-        function _amqpConnectionStatusHandler(event, data) {\n-            _log(\"Connection event: \" + event);\n-            switch(event) {\n-                case \"CONNECTION_OPEN\":\n-                    if (_connecting) _openAuthSession();\n-                    break;\n-                case \"CONNECTION_CLOSED\" :\n-                    break;\n-                case \"CONNECTION_ERROR\" :\n-                    if (_connecting) {\n-                        if (data == null) data = event;\n-                        _connectionCallback(data);\n-                    } else {\n-                        // TODO: replace this with error handling\n-                        _log(data);\n-                    }\n-                default:\n-                    // TODO: replace this with known events\n-                    _log(data);\n-                    break;\n-            }\n-        }\n+                local mqttMsg = _mqttclient.createmessage(topic, msg.getBody(), _options.qos);\n \n-        function _amqpAuthSessionStatusHandler(event, data) {\n-            _log(\"Auth Session event: \" + event);\n-            switch(event) {\n-                case \"SESSION_OPEN\":\n-                    _openAuthReceiver();\n-                    break;\n-                case \"SESSION_CLOSED\" :\n-                    break;\n-                case \"SESSION_ERROR\" :\n-                    if (_connecting) {\n-                        if (data == null) data = event;\n-                        _connectionCallback(data);\n-                    } else {\n-                        // TODO: replace this with error handling\n-                        _log(data);\n+                local msgSentCb = function (err) {\n+                    if (reqId in _msgSentCbMap) {\n+                        delete _msgSentCbMap[reqId];\n+                        // TODO: What should we pass to onComplete?\n+                        _callOnComplete(onComplete, err);\n                     }\n-                default:\n-                    // TODO: replace this with known events\n-                    _log(data);\n-                    break;\n-            }\n-        }\n+                }.bindenv(this);\n \n-        function _amqpAuthReceiverStatusHandler(event, data) {\n-            _log(\"Auth Receiver event: \" + event);\n-            switch(event) {\n-                case \"RECEIVER_OPEN\":\n-                    if ( !_isOpen(_senders.auth) )  {\n-                        _openAuthSender();\n-                    } else {\n-                        _getPutToken( function() { _log(\"put token request sent\") });\n-                    }\n-                    break;\n-                case \"RECEIVER_CLOSED\":\n-                    break;\n-                case \"RECEIVER_ERROR\":\n-                    if (_connecting) {\n-                        if (data == null) data = event;\n-                        _connectionCallback(data);\n-                    } else {\n-                        // TODO: replace this with error handling\n-                        _log(data);\n-                    }\n-                default:\n-                    // Should not need this, it's just a catch-all while in development\n-                    _log(data);\n-                    break;\n+                _msgSentCbMap[reqId] <- onComplete;\n+                mqttMsg.sendasync(msgSentCb);\n+            } else {\n+                local err = _state & AZURE_IOT_CLIENT_STATE_CONNECTED ? AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW : AZURE_IOT_CLIENT_ERROR_NOT_CONNECTED;\n+                _callOnComplete(onComplete, err);\n             }\n         }\n \n-        // Note: auth receiver will open auth sender if needed, auth\n-        function _amqpAuthSenderStatusHandler(event, data) {\n-            _log(\"Auth Sender event: \" + event);\n-            switch(event) {\n-                case \"SENDER_OPEN\":\n-                    _getPutToken( function() { _log(\"put token request sent\") });\n-                    break;\n-                case \"SENDER_CLOSED\":\n-                    break;\n-                case \"SENDER_ERROR\":\n-                    if (_connecting) {\n-                        if (data == null) data = event;\n-                        _connectionCallback(data);\n-                    } else {\n-                        // TODO: replace this with error handling\n-                        _log(data);\n-                    }\n-                    break;\n-                default:\n-                    _log(data);\n-                    break;\n-            }\n-        }\n+        // Enables or disables message receiving from Azure IoT Hub (https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#receiving-cloud-to-device-messages).\n+        //\n+        // Parameters:\n+        //     onReceive : Function         Callback called every time a new message is received. null disables the feature.\n+        //                                  The callback signature:\n+        //                                  onReceive(message), where\n+        //                                      message :           Received message.\n+        //                                          AzureIoTHub.Message\n+        //     onComplete : Function        Callback called when the operation is completed or an error happens.\n+        //          (optional)              The callback signature:\n+        //                                  onComplete(error), where\n+        //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+        //\n+        // Returns:                         Nothing.\n+        function enableMessageReceiving(onReceive, onComplete = null){\n+            if ((_state & AZURE_IOT_CLIENT_STATE_CONNECTED) && !(_state & AZURE_IOT_CLIENT_STATE_DISCONNECTING)) {\n+                if (_state & (AZURE_IOT_CLIENT_STATE_ENABLING_MSG | AZURE_IOT_CLIENT_STATE_DISABLING_MSG)) {\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW);\n+                } else if (_onMessageCb != null && onReceive == null) {\n+                    // Should unsubscribe\n+                    // TODO: Make sure we have successfully unsubscribed\n+                    // TODO: Handle the case with disconnection when unsubscribing\n+                    // TODO: Will be done once EI make callback for unsubscribe method\n+                    _state = _state | AZURE_IOT_CLIENT_STATE_DISABLING_MSG;\n+                    _mqttclient.unsubscribe(_topics.msgRecv + \"#\");\n+                    _onMessageCb = null;\n+                    _state = _state & ~AZURE_IOT_CLIENT_STATE_DISABLING_MSG;\n+                    _callOnComplete(onComplete, 0);\n+                } else if (_onMessageCb != null) {\n+                    // Already subscribed\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_ALREADY_ENABLED);\n+                } else if (onReceive == null) {\n+                    // Already disabled\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_NOT_ENABLED);\n+                } else {\n+                    // Should send subscribe packet\n+                    local topic = _topics.msgRecv + \"#\";\n \n-        function _amqpEventSessionStatusHandler(event, data) {\n-            _log(\"Event Session event: \" + event);\n+                    local subscribedCb = function (err, qos) {\n+                        if (_msgEnabledCb != null) {\n+                            if (err == 0) {\n+                                _onMessageCb = onReceive;\n+                            }\n+                            _msgEnabledCb = null;\n+                            _state = _state & ~AZURE_IOT_CLIENT_STATE_ENABLING_MSG;\n+                            // TODO: What should we pass to onComplete?\n+                            _callOnComplete(onComplete, err);\n+                        }\n+                    }.bindenv(this);\n \n-            switch(event) {\n-                case \"SESSION_OPEN\":\n-                    _openEventSender();\n-                    break;\n-                case \"SESSION_CLOSED\" :\n-                    // Session closed notify user (different error messages for debug purposes)\n-                    if (_isOpen(_connection)) {\n-                        _handlers.onDisconnect(\"Event session closed. Please reconnect to send or receive events.\");\n-                    } else {\n-                        _handlers.onDisconnect(\"Connection to IoT Hub closed. Please reconnect to send or receive events.\");\n-                    }\n-                    break;\n-                case \"SESSION_ERROR\" :\n-                    if (_connecting) {\n-                        if (data == null) data = event;\n-                        _connectionCallback(data);\n-                    } else {\n-                        // TODO: replace this with error handling\n-                        _log(data);\n-                    }\n-                default:\n-                    // TODO: replace this with known events\n-                    _log(data);\n-                    break;\n+                    _msgEnabledCb = onComplete;\n+                    _state = _state | AZURE_IOT_CLIENT_STATE_ENABLING_MSG;\n+                    _mqttclient.subscribe(topic, _options.qos, subscribedCb);\n+                }\n+            } else {\n+                local err = _state & AZURE_IOT_CLIENT_STATE_CONNECTED ? AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW : AZURE_IOT_CLIENT_ERROR_NOT_CONNECTED;\n+                _callOnComplete(onComplete, err);\n             }\n         }\n \n-        function _amqpEventSenderStatusHandler(event, data) {\n-            _log(\"Msg Sender event: \" + event);\n+        // Enables or disables Azure IoT Hub Device Twins functionality (https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-device-twins).\n+        //\n+        // Parameters:\n+        //     onRequest : Function         Callback called every time a new request with desired Device Twin properties is received. null disables the feature.\n+        //                                  The callback signature:\n+        //                                  onRequest(version, props), where\n+        //                                      version : Integer   Version of the Device Twin document which corresponds to the desired properties.\n+        //                                                          The version is always incremented by Azure IoT Hub when the document is updated.\n+        //                                      props : Table       Key-value table with the desired properties.\n+        //                                                          Every key is always a String with the name of the property.\n+        //                                                          The value is the corresponding value of the property.\n+        //                                                          Keys and values are fully application specific.\n+        //     onComplete : Function        Callback called when the operation is completed or an error happens.\n+        //          (optional)              The callback signature:\n+        //                                  onComplete(error), where\n+        //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+        //\n+        // Returns:                         Nothing.\n+        function enableTwin(onRequest, onComplete = null) {\n+            if ((_state & AZURE_IOT_CLIENT_STATE_CONNECTED) && !(_state & AZURE_IOT_CLIENT_STATE_DISCONNECTING)) {\n+                if (_state & (AZURE_IOT_CLIENT_STATE_ENABLING_TWIN | AZURE_IOT_CLIENT_STATE_DISABLING_TWIN)) {\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW);\n+                } else if (_onTwinReqCb != null && onRequest == null) {\n+                    // Should unsubscribe\n+                    // TODO: Make sure we have successfully unsubscribed\n+                    // TODO: Handle the case with disconnection when unsubscribing\n+                    // TODO: Will be done once EI make callback for unsubscribe method\n+                    _state = _state | AZURE_IOT_CLIENT_STATE_DISABLING_TWIN;\n+                    _mqttclient.unsubscribe(_topics.twinNotif + \"#\");\n+                    _mqttclient.unsubscribe(_topics.twinRecv + \"#\");\n+                    _onTwinReqCb = null;\n+                    _state = _state & ~AZURE_IOT_CLIENT_STATE_DISABLING_TWIN;\n+                    _callOnComplete(onComplete, 0);\n+                } else if (_onTwinReqCb != null) {\n+                    // Already subscribed\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_ALREADY_ENABLED);\n+                } else if (onRequest == null) {\n+                    // Already disabled\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_NOT_ENABLED);\n+                } else {\n+                    // Should send subscribe packet for notifications and GET\n \n-            switch(event) {\n-                case \"SENDER_OPEN\":\n-                    if (_connecting) {\n-                        _connectionCallback(null);\n-                    }\n-                    if (_msgQueue.len() > 0) {\n-                        while (_msgQueue.len()) {\n-                            local item = _msgQueue.remove(0);\n-                            _sendEvent(item.msg, item.cb);\n-                        }\n-                    }\n-                    break;\n-                case \"SENDER_CLOSED\":\n-                    break;\n-                case \"SENDER_ERROR\":\n-                    if (_connecting) {\n-                        if (data == null) data = event;\n-                        _connectionCallback(data);\n-                    } else if (_msgQueue.len() > 0) {\n-                        while (_msgQueue.len()) {\n-                            local item = _msgQueue.remove(0);\n-                            if (item.cb) item.cb(event, item.msg);\n+                    local subscribedTwinRecvCb = function (err, qos) {\n+                        if (_twinEnabledCb =! null) {\n+                            if (err == 0) {\n+                                _onTwinReqCb = onRequest;\n+                            }\n+                            _twinEnabledCb = null;\n+                            _state = _state & ~AZURE_IOT_CLIENT_STATE_ENABLING_TWIN;\n+                            // TODO: What should we pass to onComplete?\n+                            _callOnComplete(onComplete, err);\n                         }\n-                    } else {\n-                        _log(data);\n-                        // Renew Token\n-                        if (data.find(\"unauthorized-access\") != null) {\n-                            _senderTokenError = true;\n-                            _updateConfigSASExpiry();\n-                            _getPutToken(function() {_log(\"renewing token\")});\n+                    }.bindenv(this);\n+\n+                    local subscribedTwinNotifCb = function (err, qos) {\n+                        if (_twinEnabledCb != null) {\n+                            if (err == 0) {\n+                                local topic = _topics.twinRecv + \"#\";\n+                                _mqttclient.subscribe(topic, _options.qos, subscribedTwinRecvCb);\n+                            } else {\n+                                _twinEnabledCb = null;\n+                                _state = _state & ~AZURE_IOT_CLIENT_STATE_ENABLING_TWIN;\n+                                // TODO: What should we pass to onComplete?\n+                                _callOnComplete(onComplete, err);\n+                            }\n                         }\n-                    }\n-                    break;\n-                default:\n-                    // Should not need this, it's just a catch-all while in development\n-                    _log(data);\n-                    break;\n+                    }.bindenv(this);\n+\n+                    local topic = _topics.twinNotif + \"#\";\n+                    _twinEnabledCb = onComplete;\n+                    _state = _state | AZURE_IOT_CLIENT_STATE_ENABLING_TWIN;\n+                    _mqttclient.subscribe(topic, _options.qos, subscribedTwinNotifCb);\n+                }\n+            } else {\n+                local err = _state & AZURE_IOT_CLIENT_STATE_DISCONNECTING ? AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW : AZURE_IOT_CLIENT_ERROR_NOT_CONNECTED;\n+                _callOnComplete(onComplete, err);\n             }\n         }\n \n-        function _amqpEventReceiverStatusHandler(event, data) {\n-            _log(\"Msg Receiver event: \" + event);\n-\n-            switch(event) {\n-                case \"RECEIVER_OPEN\":\n-                    break;\n-                case \"RECEIVER_CLOSED\":\n-                    // Event session is open and we have a handlers registered, so this is an unexpected disconnect\n-                    if (_isOpen(_sessions.event) && \"onEvent\" in _handlers && _handlers.onEvent != null) {\n-                        // Call receiver callback with an error.\n-                        _handlers.onEvent(\"Unexpected disconnect\", null);  \n+        // Retrieves Device Twin properties (https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#retrieving-a-device-twins-properties).\n+        //\n+        // Parameters:\n+        //     onRetrieve : Function        Callback called when the properties are retrieved.\n+        //                                  The callback signature:\n+        //                                  onRetrieve(error, reportedProps, desiredProps), where\n+        //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+        //                                      reportedProps :     Key-value table with the reported properties and their version.\n+        //                                          Table           This parameter should be ignored if error is not 0.\n+        //                                                          Every key is always a String with the name of the property.\n+        //                                                          The value is the corresponding value of the property.\n+        //                                                          Keys and values are fully application specific.\n+        //                                      desiredProps :      Key-value table with the desired properties and their version.\n+        //                                          Table           This parameter should be ignored if error is not 0.\n+        //                                                          Every key is always a String with the name of the property.\n+        //                                                          The value is the corresponding value of the property.\n+        //                                                          Keys and values are fully application specific.\n+        //\n+        // Returns:                         Nothing.\n+        function retrieveTwinProperties(onRetrieve) {\n+            if ((_state & AZURE_IOT_CLIENT_STATE_CONNECTED) && !(_state & AZURE_IOT_CLIENT_STATE_DISCONNECTING) && _onTwinReqCb != null) {\n+                // Only one retrieve operation at a time is allowed\n+                if (_twinRetrCb != null) {\n+                    try {\n+                        onRetrieve(AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW, null, null);\n+                    } catch (e) {\n+                        _error(\"Exception at onRetrieve user callback: \" + e);\n                     }\n-                    break;\n-                case \"RECEIVER_ERROR\":\n-                    _log(data);\n-                    // TODO: is this the correct error message (it is different than event sender - should these messages be the same??)\n-                    if (data.find(\"Token Invalid\") != null) {\n-                        _receiverTokenError = true;\n-                        _updateConfigSASExpiry();\n-                        _getPutToken(function() {_log(\"renewing token\")});\n+                    return;\n+                }\n+                local reqId = _reqNum.tostring();\n+                local topic = _topics.twinGet + \"?$rid=\" + reqId;\n+                _reqNum++;\n+\n+                local mqttMsg = _mqttclient.createmessage(topic, \"\", _options.qos);\n+\n+                local msgSentCb = function (err) {\n+                    if (_twinRetrCb != null) {\n+                        if (err == 0) {\n+                            local timer = imp.wakeup(_options.timeout, function () {\n+                                if (_twinRetrCb == null) {\n+                                    return;\n+                                }\n+                                _twinRetrCb = null;\n+                                try {\n+                                    onRetrieve(AZURE_IOT_CLIENT_ERROR_OP_TIMED_OUT, null, null);\n+                                } catch (e) {\n+                                    _error(\"Exception at onRetrieve user callback: \" + e);\n+                                }\n+                            }.bindenv(this));\n+                            _twinRetrCb = [reqId, onRetrieve, timer];\n+                        } else {\n+                            try {\n+                                _twinRetrCb = null;\n+                                // TODO: What should we pass to onRetrieve?\n+                                onRetrieve(err, null, null);\n+                            } catch (e) {\n+                                _error(\"Exception at onRetrieve user callback: \" + e);\n+                            }\n+                        }\n                     }\n-                    break;\n-                default:\n-                    // Should not need this, it's just a catch-all while in development\n-                    _log(data);\n-                    break;\n+                }.bindenv(this);\n+\n+                _twinRetrCb = [reqId, onRetrieve, null];\n+                mqttMsg.sendasync(msgSentCb);\n+            } else {\n+                local err = null;\n+                if (_onTwinReqCb == null) {\n+                    err = AZURE_IOT_CLIENT_ERROR_NOT_ENABLED;\n+                } else {\n+                    // We are disconnected or disconnecting now\n+                    err = _state & AZURE_IOT_CLIENT_STATE_DISCONNECTING ? AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW : AZURE_IOT_CLIENT_ERROR_NOT_CONNECTED;\n+                }\n+                try {\n+                    onRetrieve(err, null, null);\n+                } catch (e) {\n+                    _error(\"Exception at onRetrieve user callback: \" + e);\n+                }\n             }\n         }\n \n-        // Authorization\n-        // ------------------------------------------------------------------------------------\n-\n-        function _authReceiverCB(deliveries) {\n-            _log(\"in auth receiver callback\")\n-            local authorized = false;\n-\n-            foreach(deliveryItem in deliveries) {\n-                deliveryItem.accept();\n-                local properties = deliveryItem.message().properties();\n-\n-                if (\"status-code\" in properties && properties[\"status-code\"] == 200 && \"status-description\" in properties && properties[\"status-description\"] == \"OK\") {\n-                    authorized = true;\n-\n-                    if (_senderTokenError) {\n-                        _openEventSender();\n-                        _senderTokenError = false;\n-                    } else if (_receiverTokenError) {\n-                        if (\"onEvent\" in _handlers && _handlers.onEvent != null) _openEventReceiver();\n-                        _receiverTokenError = false;\n-                    } else {\n-                        // We are connected and authorized, check event session status\n-                        if ( _isOpen(_sessions.event) ) {\n-                            if ( _isOpen(_senders.event) ) {\n-                                if (_connecting) _connectionCallback(null);\n-                            } else {\n-                                _openEventSender();\n-                            }\n+        // Updates Device Twin reported properties (https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#update-device-twins-reported-properties).\n+        //\n+        // Parameters:\n+        //     props : Table                Key-value table with the desired properties.\n+        //                                  Every key is always a String with the name of the property.\n+        //                                  The value is the corresponding value of the property.\n+        //                                  Keys and values are fully application specific.\n+        //     onComplete : Function        Callback called when the operation is completed or an error happens.\n+        //          (optional)              The callback signature:\n+        //                                  onComplete(error), where\n+        //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+        //\n+        // Returns:                         Nothing.\n+        function updateTwinProperties(props, onComplete = null) {\n+            if ((_state & AZURE_IOT_CLIENT_STATE_CONNECTED) && !(_state & AZURE_IOT_CLIENT_STATE_DISCONNECTING) && _onTwinReqCb != null) {\n+                if (_twinUpdCbMap.len() >= _options.twinUpdParallelReqs) {\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW);\n+                    return;\n+                }\n+                local reqId = _reqNum.tostring();\n+                local topic = _topics.twinUpd + \"?$rid=\" + reqId;\n+                _reqNum++;\n+\n+                local mqttMsg = _mqttclient.createmessage(topic, http.jsonencode(props), _options.qos);\n+\n+                local msgSentCb = function (err) {\n+                    if (reqId in _twinUpdCbMap) {\n+                        if (err == 0) {\n+                            local timer = imp.wakeup(_options.timeout, function () {\n+                                if (!(reqId in _twinUpdCbMap)) {\n+                                    return;\n+                                }\n+                                delete _twinUpdCbMap[reqId];\n+                                _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_OP_TIMED_OUT);\n+                            }.bindenv(this));\n+                            _twinUpdCbMap[reqId] = [onComplete, timer];\n                         } else {\n-                            _openEventSession();\n+                            delete _twinUpdCbMap[reqId];\n+                            // TODO: What should we pass to onComplete?\n+                           _callOnComplete(onComplete, err);\n                         }\n                     }\n+                }.bindenv(this);\n \n+                _twinUpdCbMap[reqId] <- [onComplete, null];\n+                mqttMsg.sendasync(msgSentCb);\n+            } else {\n+                local err = null;\n+                if (_onTwinReqCb == null) {\n+                    err = AZURE_IOT_CLIENT_ERROR_NOT_ENABLED;\n+                } else {\n+                    // We are disconnected or disconnecting now\n+                    err = _state & AZURE_IOT_CLIENT_STATE_DISCONNECTING ? AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW : AZURE_IOT_CLIENT_ERROR_NOT_CONNECTED;\n                 }\n-            } // end foreach loop\n-\n-            if (_connecting && !authorized) {\n-                _connectionCallback(\"Not authorised with Azure IOT Hub\");\n+                _callOnComplete(onComplete, err);\n             }\n         }\n \n-        function _getPutToken(done) {\n-            _log(\"get put token\");\n+        // Enables or disables Azure IoT Hub Direct Methods (https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-direct-methods).\n+        //\n+        // Parameters:\n+        //     onMethod : Function          Callback called every time a direct method is called. null disables the feature.\n+        //                                  The callback signature:\n+        //                                  onMethod(name, params), where\n+        //                                      name : String       Name of the called Direct Method.\n+        //                                      params : Table      Key-value table with the input parameters of the called Direct Method.\n+        //                                                          Every key is always a String with the name of the property.\n+        //                                                          The value is the corresponding value of the property.\n+        //                                                          Keys and values are fully application specific.\n+        //     onComplete : Function        Callback called when the operation is completed or an error happens.\n+        //                                  The callback must return an instance of the AzureIoTHub.DirectMethodResponse.\n+        //          (optional)              The callback signature:\n+        //                                  onComplete(error), where\n+        //                                      error : Integer     0 if the operation is completed successfully, an error code otherwise.\n+        //\n+        // Returns:                         Nothing.\n+        function enableDirectMethods(onMethod, onComplete = null) {\n+            if ((_state & AZURE_IOT_CLIENT_STATE_CONNECTED) && !(_state & AZURE_IOT_CLIENT_STATE_DISCONNECTING)) {\n+                if (_state & (AZURE_IOT_CLIENT_STATE_ENABLING_DMETHOD | AZURE_IOT_CLIENT_STATE_DISABLING_DMETHOD)) {\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW);\n+                } else if (_onMethodCb != null && onMethod == null) {\n+                    // Should unsubscribe\n+                    // TODO: Make sure we have successfully unsubscribed\n+                    // TODO: Handle the case with disconnection when unsubscribing\n+                    // TODO: Will be done once EI make callback for unsubscribe method\n+                    _state = _state | AZURE_IOT_CLIENT_STATE_DISABLING_DMETHOD;\n+                    _mqttclient.unsubscribe(_topics.dMethodNotif + \"#\");\n+                    _onMethodCb = null;\n+                    _state = _state & ~AZURE_IOT_CLIENT_STATE_DISABLING_DMETHOD;\n+                    _callOnComplete(onComplete, 0);\n+                } else if (_onMethodCb != null) {\n+                    // Already subscribed\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_ALREADY_ENABLED);\n+                } else if (onMethod == null) {\n+                    // Already disabled\n+                    _callOnComplete(onComplete, AZURE_IOT_CLIENT_ERROR_NOT_ENABLED);\n+                } else {\n+                    // Should send subscribe packet for methods\n \n-            local properties = {};\n-            properties[\"operation\"] <- \"put-token\";\n-            properties[\"type\"] <- \"azure-devices.net:sastoken\";\n-            properties[\"name\"] <- _config.host + \"/devices/\" + _config.deviceId;\n+                    local subscribedCb = function (err, qos) {\n+                        if (_dMethodEnabledCb != null) {\n+                            if (err == 0) {\n+                                _onMethodCb = onMethod;\n+                            }\n+                            _dMethodEnabledCb = null;\n+                            _state = _state & ~AZURE_IOT_CLIENT_STATE_ENABLING_DMETHOD;\n+                            // TODO: What should we pass to onComplete?\n+                            _callOnComplete(onComplete, err);\n+                        }\n+                    }.bindenv(this);\n \n-            // this message transfer will trigger response message when received by cloud\n-            // if auth receiver is open response will trigger _authReceiverCB\n-            local msg = amqp.createmessage(_config.sharedAccessSignature, properties);\n-            _transfers.auth <- _senders.auth.createtransfer(msg);\n-            _transfers.auth.sendasync(done.bindenv(this));\n-        }\n+                    local topic = _topics.dMethodNotif + \"#\";\n \n-        // updates the config SAS expiry time\n-        function _updateConfigSASExpiry() {\n-            if (\"sharedAccessExpiry\" in _config && \"connectionString\" in _config) {\n-                local cn = AzureIoTHub.ConnectionString.Parse(_config.connectionString);\n-                local resourceUri = cn.HostName + \"/devices/\" + AzureIoTHub.Authorization.encodeUri(cn.DeviceId);\n-                local sas = AzureIoTHub.SharedAccessSignature.create(resourceUri, null, cn.SharedAccessKey, AzureIoTHub.Authorization.anHourFromNow());\n-                _config.sharedAccessSignature = sas.toString();\n-                _config.sharedAccessExpiry = sas.se;\n+                    _dMethodEnabledCb = onComplete;\n+                    _state = _state | AZURE_IOT_CLIENT_STATE_ENABLING_DMETHOD;\n+                    _mqttclient.subscribe(topic, _options.qos, subscribedCb);\n+                }\n+            } else {\n+                local err = _state & AZURE_IOT_CLIENT_STATE_DISCONNECTING ? AZURE_IOT_CLIENT_ERROR_OP_NOT_ALLOWED_NOW : AZURE_IOT_CLIENT_ERROR_NOT_CONNECTED;\n+                _callOnComplete(onComplete, err);\n             }\n         }\n \n-        // Helper Methods\n-        // ------------------------------------------------------------------------------------\n-\n-        function _isOpen(amqpObj) {\n-            return (amqpObj != null && amqpObj.isopen());\n+        // Enables or disables the library debug output. Disabled by default.\n+        //\n+        // Parameters:\n+        //     value : Boolean              true to enable, false to disable\n+        //\n+        // Returns:                         Nothing.\n+        function setDebug(value) {\n+            _debugEnabled = value;\n+        }\n+\n+        // -------------------- PRIVATE METHODS -------------------- //\n+\n+        function _fillTopics(deviceId) {\n+            _topics = {};\n+            _topics.msgSend <- format(\"devices/%s/messages/events/\", deviceId);\n+            _topics.msgRecv <- format(\"devices/%s/messages/devicebound/\", deviceId);\n+            _topics.twinRecv <- \"$iothub/twin/res/\";\n+            _topics.twinGet <- \"$iothub/twin/GET/\";\n+            _topics.twinUpd <- \"$iothub/twin/PATCH/properties/reported/\";\n+            _topics.twinNotif <- \"$iothub/twin/PATCH/properties/desired/\";\n+            _topics.dMethodNotif <- \"$iothub/methods/POST/\";\n+            _topics.dMethodResp <- \"$iothub/methods/res/\";\n+        }\n+\n+        function _onConnect(err) {\n+            _log(\"_onConnect: \" + err);\n+            _state = err == 0 ? AZURE_IOT_CLIENT_STATE_CONNECTED : AZURE_IOT_CLIENT_STATE_DISCONNECTED;\n+            try {\n+                // TODO: What should we pass to _onConnectCb?\n+                _onConnectCb(err);\n+            } catch (e) {\n+                _error(\"Exception at onConnect user callback: \" + e);\n+            }\n         }\n \n-        function _connectionCallback(error) {\n-            _connecting = false;\n-            if (_handlers.onConnected) {\n-                imp.wakeup(0, function() {\n-                    _handlers.onConnected(error);\n-                }.bindenv(this));\n+        function _onDisconnect() {\n+            _log(\"Disconnected!\");\n+            local reason = _state & AZURE_IOT_CLIENT_STATE_DISCONNECTING ? 0 : AZURE_IOT_CLIENT_ERROR_GENERAL;\n+            _cleanup();\n+            if (_onDisconnectCb != null) {\n+                try {\n+                    _onDisconnectCb(reason);\n+                } catch (e) {\n+                    _error(\"Exception at onDisconnect user callback: \" + e);\n+                }\n             }\n         }\n \n-        function _resetConnectionTables() {\n-            _sessions = _getDefaultConnectionTable();\n-            _transfers = _getDefaultConnectionTable();\n-            _senders = _getDefaultConnectionTable();\n-            _receivers = _getDefaultConnectionTable();\n-            _msgQueue = [];\n-            _handlers = {};\n-        }\n+        function _onMessage(msg) {\n+            local message = null;\n+            local topic = null;\n+            try {\n+                message = msg[\"message\"];\n+                topic = msg[\"topic\"];\n+                _log(format(\"_onMessage: topic=%s | body=%s\", topic, message));\n+            } catch (e) {\n+                _error(\"Could not read message: \" + e);\n+                return;\n+            }\n \n-        function _getDefaultConnectionTable() {\n-            return {\"event\" : null, \"auth\" : null};\n+            // Cloud-to-device message received\n+            if (topic.find(_topics.msgRecv) != null) {\n+                _handleCloudToDevMsg(message, topic);\n+            // Desired properties were updated\n+            } else if (topic.find(_topics.twinNotif) != null) {\n+                _handleDesPropsMsg(message, topic);\n+            // Twin request result received\n+            } else if (topic.find(_topics.twinRecv) != null) {\n+                _handleTwinReqResMsg(message, topic);\n+            // Direct method called\n+            } else if (topic.find(_topics.dMethodNotif) != null) {\n+                _handleDirMethodMsg(message, topic);\n+            }\n         }\n \n-        function _sendEvent(message, done) {\n-            // create transfer and send\n-            _transfers.event <- _senders.event.createtransfer(message.createAMQPMessage());\n-            _log(\"send event transfer created\")\n-            _transfers.event.sendasync(function() {\n-                _log(\"in send event transfer callback\");\n-                if (done) done(null);\n-            }.bindenv(this));\n+        function _handleCloudToDevMsg(message, topic) {\n+            local props = {};\n+            local splittedTopic = split(topic, \"/\");\n+            if (splittedTopic[splittedTopic.len() - 1] != \"devicebound\") {\n+                // We have properties sent with message\n+                props = http.urldecode(splittedTopic[splittedTopic.len() - 1]);\n+            }\n+            try {\n+                _onMessageCb(AzureIoTHub.Message(message, props));\n+            } catch (e) {\n+                _error(\"Exception at onReceive user callback: \" + e);\n+            }\n         }\n \n-        function _handleDeliveries(deliveries, cb) {\n-            while (deliveries.len()) {\n-                local item = AzureIoTHub.Delivery(deliveries.remove(0));\n-                cb(null, item);\n+        function _handleDesPropsMsg(message, topic) {\n+            local version = null;\n+            local parsedMsg = null;\n+            try {\n+                version = split(topic, \"=\")[1];\n+                parsedMsg = http.jsondecode(message);\n+            } catch (e) {\n+                _error(\"Exception at parsing the message: \" + e);\n+                _logMsg(message, topic);\n+                return;\n+            }\n+            try {\n+                _onTwinReqCb(version, parsedMsg);\n+            } catch (e) {\n+                _error(\"Exception at onRequest user callback: \" + e);\n             }\n         }\n \n-        function _log(logMsg) {\n-            if (_debug) server.log(logMsg);\n+        function _handleTwinReqResMsg(message, topic) {\n+            local status = null;\n+            local reqId = null;\n+            local parsedMsg = null;\n+            try {\n+                local splitted = split(topic, \"/\");\n+                status = splitted[3].tointeger();\n+                reqId = http.urldecode(splitted[4])[\"?$rid\"];\n+                if (_statusIsOk(status) && message != \"\") {\n+                    parsedMsg = http.jsondecode(message);\n+                }\n+            } catch (e) {\n+                _error(\"Exception at parsing the message: \" + e);\n+                _logMsg(message, topic);\n+                return;\n+            }\n+            // Twin's properties received after UPDATE request\n+            if (reqId in _twinUpdCbMap) {\n+                local cb = _twinUpdCbMap[reqId][0];\n+                local timer = _twinUpdCbMap[reqId][1];","path":"AzureIoTHub.agent.lib.nut","position":null,"original_position":1192,"commit_id":"f9e5721d63e62c101e50f3ec941b8c4740924057","original_commit_id":"b34d56512945b8a897a32bff0af8047eb3780f20","user":{"login":"ragrus-nbl","id":38075410,"avatar_url":"https://avatars2.githubusercontent.com/u/38075410?v=4","gravatar_id":"","url":"https://api.github.com/users/ragrus-nbl","html_url":"https://github.com/ragrus-nbl","followers_url":"https://api.github.com/users/ragrus-nbl/followers","following_url":"https://api.github.com/users/ragrus-nbl/following{/other_user}","gists_url":"https://api.github.com/users/ragrus-nbl/gists{/gist_id}","starred_url":"https://api.github.com/users/ragrus-nbl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ragrus-nbl/subscriptions","organizations_url":"https://api.github.com/users/ragrus-nbl/orgs","repos_url":"https://api.github.com/users/ragrus-nbl/repos","events_url":"https://api.github.com/users/ragrus-nbl/events{/privacy}","received_events_url":"https://api.github.com/users/ragrus-nbl/received_events","type":"User","site_admin":false},"body":"Done","created_at":"2018-04-28T06:52:23Z","updated_at":"2018-04-28T06:52:23Z","html_url":"https://github.com/nobitlost/AzureIoTHub/pull/1#discussion_r184842881","pull_request_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/comments/184842881"},"html":{"href":"https://github.com/nobitlost/AzureIoTHub/pull/1#discussion_r184842881"},"pull_request":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1"}},"in_reply_to_id":184266853},"pull_request":{"url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1","id":183329860,"html_url":"https://github.com/nobitlost/AzureIoTHub/pull/1","diff_url":"https://github.com/nobitlost/AzureIoTHub/pull/1.diff","patch_url":"https://github.com/nobitlost/AzureIoTHub/pull/1.patch","issue_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/1","number":1,"state":"open","locked":false,"title":"MQTT Client Implementation","user":{"login":"ragrus-nbl","id":38075410,"avatar_url":"https://avatars2.githubusercontent.com/u/38075410?v=4","gravatar_id":"","url":"https://api.github.com/users/ragrus-nbl","html_url":"https://github.com/ragrus-nbl","followers_url":"https://api.github.com/users/ragrus-nbl/followers","following_url":"https://api.github.com/users/ragrus-nbl/following{/other_user}","gists_url":"https://api.github.com/users/ragrus-nbl/gists{/gist_id}","starred_url":"https://api.github.com/users/ragrus-nbl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ragrus-nbl/subscriptions","organizations_url":"https://api.github.com/users/ragrus-nbl/orgs","repos_url":"https://api.github.com/users/ragrus-nbl/repos","events_url":"https://api.github.com/users/ragrus-nbl/events{/privacy}","received_events_url":"https://api.github.com/users/ragrus-nbl/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-04-23T05:36:15Z","updated_at":"2018-04-28T06:52:23Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2e9d5dad426a161c7dae3feda6bcffd84c0e1f8b","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1/commits","review_comments_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1/comments","review_comment_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/comments{/number}","comments_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/1/comments","statuses_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/statuses/f9e5721d63e62c101e50f3ec941b8c4740924057","head":{"label":"nobitlost:develop","ref":"develop","sha":"f9e5721d63e62c101e50f3ec941b8c4740924057","user":{"login":"nobitlost","id":25421182,"avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"repo":{"id":129041981,"name":"AzureIoTHub","full_name":"nobitlost/AzureIoTHub","owner":{"login":"nobitlost","id":25421182,"avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nobitlost/AzureIoTHub","description":"Microsoft Azure IoT Hub client","fork":true,"url":"https://api.github.com/repos/nobitlost/AzureIoTHub","forks_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/forks","keys_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/teams","hooks_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/hooks","issue_events_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/events{/number}","events_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/events","assignees_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/assignees{/user}","branches_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/branches{/branch}","tags_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/tags","blobs_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/refs{/sha}","trees_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/statuses/{sha}","languages_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/languages","stargazers_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/stargazers","contributors_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/contributors","subscribers_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/subscribers","subscription_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/subscription","commits_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/commits{/sha}","git_commits_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/commits{/sha}","comments_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/comments{/number}","issue_comment_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/comments{/number}","contents_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/contents/{+path}","compare_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/merges","archive_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/downloads","issues_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues{/number}","pulls_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls{/number}","milestones_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/milestones{/number}","notifications_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/labels{/name}","releases_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/releases{/id}","deployments_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/deployments","created_at":"2018-04-11T06:00:33Z","updated_at":"2018-04-11T06:00:35Z","pushed_at":"2018-04-28T05:23:47Z","git_url":"git://github.com/nobitlost/AzureIoTHub.git","ssh_url":"git@github.com:nobitlost/AzureIoTHub.git","clone_url":"https://github.com/nobitlost/AzureIoTHub.git","svn_url":"https://github.com/nobitlost/AzureIoTHub","homepage":"","size":10201,"stargazers_count":0,"watchers_count":0,"language":"Squirrel","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":1,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"nobitlost:master","ref":"master","sha":"ad919c4ad4cd2df21904fb55f42cc34eaa2a2d1b","user":{"login":"nobitlost","id":25421182,"avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"repo":{"id":129041981,"name":"AzureIoTHub","full_name":"nobitlost/AzureIoTHub","owner":{"login":"nobitlost","id":25421182,"avatar_url":"https://avatars2.githubusercontent.com/u/25421182?v=4","gravatar_id":"","url":"https://api.github.com/users/nobitlost","html_url":"https://github.com/nobitlost","followers_url":"https://api.github.com/users/nobitlost/followers","following_url":"https://api.github.com/users/nobitlost/following{/other_user}","gists_url":"https://api.github.com/users/nobitlost/gists{/gist_id}","starred_url":"https://api.github.com/users/nobitlost/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nobitlost/subscriptions","organizations_url":"https://api.github.com/users/nobitlost/orgs","repos_url":"https://api.github.com/users/nobitlost/repos","events_url":"https://api.github.com/users/nobitlost/events{/privacy}","received_events_url":"https://api.github.com/users/nobitlost/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nobitlost/AzureIoTHub","description":"Microsoft Azure IoT Hub client","fork":true,"url":"https://api.github.com/repos/nobitlost/AzureIoTHub","forks_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/forks","keys_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/teams","hooks_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/hooks","issue_events_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/events{/number}","events_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/events","assignees_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/assignees{/user}","branches_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/branches{/branch}","tags_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/tags","blobs_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/refs{/sha}","trees_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/statuses/{sha}","languages_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/languages","stargazers_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/stargazers","contributors_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/contributors","subscribers_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/subscribers","subscription_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/subscription","commits_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/commits{/sha}","git_commits_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/git/commits{/sha}","comments_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/comments{/number}","issue_comment_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/comments{/number}","contents_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/contents/{+path}","compare_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/merges","archive_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/downloads","issues_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues{/number}","pulls_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls{/number}","milestones_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/milestones{/number}","notifications_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/labels{/name}","releases_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/releases{/id}","deployments_url":"https://api.github.com/repos/nobitlost/AzureIoTHub/deployments","created_at":"2018-04-11T06:00:33Z","updated_at":"2018-04-11T06:00:35Z","pushed_at":"2018-04-28T05:23:47Z","git_url":"git://github.com/nobitlost/AzureIoTHub.git","ssh_url":"git@github.com:nobitlost/AzureIoTHub.git","clone_url":"https://github.com/nobitlost/AzureIoTHub.git","svn_url":"https://github.com/nobitlost/AzureIoTHub","homepage":"","size":10201,"stargazers_count":0,"watchers_count":0,"language":"Squirrel","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":1,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1"},"html":{"href":"https://github.com/nobitlost/AzureIoTHub/pull/1"},"issue":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/1"},"comments":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/nobitlost/AzureIoTHub/statuses/f9e5721d63e62c101e50f3ec941b8c4740924057"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"ragrus-nbl"}}