2016年9月25日

.Net Micro Framewrok連接IOT Hub

.Net Micro Framework適合使用在資源受限制的設備上,例如各種嵌入式設備;讓開發人員使用熟悉的Visual Studio以及C#來撰寫這些設備上的程式。在物聯網的情境下,若要與Azure IOT Hub連接發送感測資料,或是接收由IOT Hub發送下來的資料時,可以透過適用於.Net MF上的MQTT或是AMQP函式庫,直接操作底層的通訊協議;或是使用Azure IOT SDK的Micro Framework Libaray連接。

在這裡,我們使用AMQP .Net Lite這個Library來實現.Net Micro Framework與IOT Hub連接。

程式碼(文末)相當簡單,基本上只要照著範例做就可以了;比較需要注意的是連接時的username與password

  • username:其格式為<DEVICE ID>@sas.<IOT HUB NAME>;如果我的設備ID為test001,IOT Hub名稱為myIOTHub,則username為test001@sas.myIOTHub
  • password:可以透過Device Exploere產生,其格式為:SharedAccessSignature sr=<IOT HUB URL>%2Fdevices%2F<DEVICE ID>&sig=<SIGNATURE>%3D&se=<EXPIRY>

另外,如果在.NET MF Emulator上跑這個程式,會發現程式在建立Connection完就會hang住了;這是由於在.NET MF Emulator上的一個Issue,導致在Emulator上使用SSL時如果沒有資料回傳便會卡住。詳細說明以及解決方式如下,目前解決方案尚未正式釋出,但可以先透過修改Emulator Source code解決。

using Amqp;
using Amqp.Framing;
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
#if NETMF
using Microsoft.SPOT;
#endif

namespace IoTHubAmqp
{
    class Program
    {
        /*
            string HOST = "";
            int PORT = ;
            string DEVICE_ID = "<DEVICE ID>";
            string DEVICE_KEY = "<DEVICE KEY>";
            string password = "<SHARE ACEESS TOKEN>";
            string userName = "<IOTHUB-HOST URL>/<DEVICE ID>";
            string publishPath = "devices/<DEIVCE ID>/messages/events";

         * */
        private const string HOST = "<IOT HUB URL>";
        private const int PORT = 5671;
        private const string DEVICE_ID = "<DEVICe ID>";
        private const string DEVICE_KEY“ = “<DEVICE KEY>”;
        private const string SAS_TOKEN = "<SHARE ACCESS TOKEN>";
        private static Address address;
        private static Connection connection;

private static string username = “<DEVICE ID>@sas.<IOT HUB NAME>”
        private static Session session;
      
        static void Main(string[] args)
        {
            Amqp.Trace.TraceLevel = Amqp.TraceLevel.Frame | Amqp.TraceLevel.Verbose;
#if NETMF
            Amqp.Trace.TraceListener = (f, a) => Debug.Print(DateTime.Now.ToString("[hh:ss.fff]") + " " + Fx.Format(f, a));
#else
            Amqp.Trace.TraceListener = (f, a) => System.Diagnostics.Trace.WriteLine(DateTime.Now.ToString("[hh:ss.fff]") + " " + Fx.Format(f, a));
#endif
            string audience = Fx.Format("{0}/devices/{1}", HOST, DEVICE_ID);
            string resourceUri = Fx.Format("{0}/devices/{1}", HOST, DEVICE_ID);
            string sasToken = SAS_TOKEN;
#if false
            address = new Address(HOST, PORT, username,
            SAS_TOKEN,
            "AMQPS");
            connection = new Connection(address);

            bool cbs = true;
#else
            address = new Address(HOST, PORT, null, null);
            connection = new Connection(address);

            bool cbs = PutCbsToken(connection, HOST, sasToken, audience);
#endif


            if (cbs)
            {
                session = new Session(connection);

                SendEvent();
                receiverThread = new Thread(ReceiveCommands);
                receiverThread.Start();
            }

            // just as example ...
            // the application ends only after received a command or timeout on receiving
            receiverThread.Join();

            session.Close();
            connection.Close();
        }

        static private void SendEvent()
        {
            string entity = Fx.Format("/devices/{0}/messages/events", DEVICE_ID);
            SenderLink senderLink = new SenderLink(session, "sender-link", entity);

            var messageValue = Encoding.UTF8.GetBytes(DateTime.UtcNow.ToString());
            Message message = new Message()
            {
                BodySection = new Data() { Binary = messageValue }
            };

            senderLink.Send(message);
            senderLink.Close();
        }

        static private void ReceiveCommands()
        {
            string entity = Fx.Format("/devices/{0}/messages/deviceBound", DEVICE_ID);

            ReceiverLink receiveLink = new ReceiverLink(session, "receive-link", entity);
            while (true)
            {
                Thread.Sleep(0);
                Message received = receiveLink.Receive();

                //Debug.Print((string)received.Body);
                if (received != null)
                    receiveLink.Accept(received);
            }
            receiveLink.Close();
        }

        static private bool PutCbsToken(Connection connection, string host, string shareAccessSignature, string audience)
        {
            bool result = true;
            Session session = new Session(connection);

            string cbsReplyToAddress = "cbs-reply-to";
            var cbsSender = new SenderLink(session, "cbs-sender", "$cbs");
            var cbsReceiver = new ReceiverLink(session, cbsReplyToAddress, "$cbs");

            // construct the put-token message
            var request = new Message(shareAccessSignature);
            request.Properties = new Properties();
            request.Properties.MessageId = Guid.NewGuid().ToString();
            request.Properties.ReplyTo = cbsReplyToAddress;
            request.ApplicationProperties = new ApplicationProperties();
            request.ApplicationProperties["operation"] = "put-token";
            request.ApplicationProperties["type"] = "azure-devices.net:sastoken";
            request.ApplicationProperties["name"] = audience;
            cbsSender.Send(request);

            // receive the response
            var response = cbsReceiver.Receive();
            if (response == null || response.Properties == null || response.ApplicationProperties == null)
            {
                result = false;
            }
            else
            {
                int statusCode = (int)response.ApplicationProperties["status-code"];
                string statusCodeDescription = (string)response.ApplicationProperties["status-description"];
                if (statusCode != (int)202 && statusCode != (int)200) // !Accepted && !OK
                {
                    result = false;
                }
            }

            // the sender/receiver may be kept open for refreshing tokens
            cbsSender.Close();
            cbsReceiver.Close();
            session.Close();

            return result;
        }

        private static readonly long UtcReference = (new DateTime(1970, 1, 1, 0, 0, 0, 0)).Ticks;

        static string GetSharedAccessSignature(string keyName, string sharedAccessKey, string resource, TimeSpan tokenTimeToLive)
        {
            // http://msdn.microsoft.com/en-us/library/azure/dn170477.aspx
            // the canonical Uri scheme is http because the token is not amqp specific
            // signature is computed from joined encoded request Uri string and expiry string

#if NETMF
            // needed in .Net Micro Framework to use standard RFC4648 Base64 encoding alphabet
            System.Convert.UseRFC4648Encoding = true;
#endif
            string expiry = ((long)(DateTime.UtcNow - new DateTime(UtcReference, DateTimeKind.Utc) + tokenTimeToLive).TotalSeconds()).ToString();
            string encodedUri = HttpUtility.UrlEncode(resource);

            byte[] hmac = SHA.computeHMAC_SHA256(Convert.FromBase64String(sharedAccessKey), Encoding.UTF8.GetBytes(encodedUri + "\n" + expiry));
            string sig = Convert.ToBase64String(hmac);

            if (keyName != null)
            {
                return Fx.Format(
                "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}",
                encodedUri,
                HttpUtility.UrlEncode(sig),
                HttpUtility.UrlEncode(expiry),
                HttpUtility.UrlEncode(keyName));
            }
            else
            {
                return Fx.Format(
                    "SharedAccessSignature sr={0}&sig={1}&se={2}",
                    encodedUri,
                    HttpUtility.UrlEncode(sig),
                    HttpUtility.UrlEncode(expiry));
            }
        }
       
    }
}

沒有留言:

About Me