2016年12月13日

建立自己的Channel與LUIS Action Binding整合

在上一篇中,我們展示了如何透過LUIS Action Binding & Action Fulfillment整合Bot Framework建立一個不需要寫程式也能運作的Chat Bot;不必寫程式碼雖然很吸引人,但是必要的複雜邏輯勢必還是要透過萬能的程式解決。

LUISAction Binding機制中,也允許我們寫自己的Channel,在ActionTrigger之後,LUIS可以去呼叫我們自己的API取得回應。下面我們用一個很簡單的範例說明。

l   首先,我們先建立一個C#API App

n   事實上,任何形式的REST API都可以;這裡只是為了簡單使用API APP。你可以使用任何熟悉的語言建立REST API

l   在這個範例中,我的API會接受LUIS判斷出來的Entity,並產生回應;因此,我先定義兩個Class,一個是DEMO.cs,代表LUIS呼叫我時的傳入參數;另一個是DemoResponse,代表我的API的回應。

n   Demo.cs

    [Serializable]

    public class Demo

    {

        public string demotype;

    }

n   DemoResponse.cs

    [Serializable]

    public class DemoResponse

    {

        public string Message;

        public DateTime Time;

    }

l   新增一個DEMOController.cs

public class DMEOController : ApiController

    {

       

 

        // POST api/DoDemo

       [SwaggerOperation("DoDemo")]

        [SwaggerResponse(HttpStatusCode.OK)]

        [HttpPost]

        public Models.DemoResponse DoDemo([FromBody]Models.Demo demotype)

        {

            return new Models.DemoResponse

            {

                Message = $"so be it:{demotype.demotype}!!",

                Time = DateTime.Now

            };

        }

       

    }

l   發布之後,記錄下網址。

l   接著,回到LUIS Portal,建立一個新的IntentDEMO;建立一個新的Entitydemotype並開始訓練模型。

l   完成後,點開DEMO Intent的設定頁面

l   新增Action Fulfillment其型別為JsonRequest;必要輸入項為demotype Entity

l   接著,在Body欄位,我們需要把LUIS辨別出來的Entity傳給API;而先前我們的API已經定義了一個DEMO Class作為傳入參數;因此,這裡我們需要輸入一個JSON Template如下:

{{"demotype":"{DEMOTYPE}"}}

n   {DEMOTYPE}就是LUIS辨別出來的Entity值,在剛剛我們指定它的名稱為DEMOTYPE,這裡用{DEMOTYPE}變數的形式帶入。

n   整個字串其實就是剛剛DEMO ClassJson型式;與一般Json的差別僅僅在於物件需要用兩個"{"包住

l   Response Template中,我們可以提供一個回應給前端的句型;如果沒有指定,則會直接把我們的API的回應回給Bot Client

n   在這裡我們先前已經定義過一個DemoResponse class,他有兩個屬性:MessageTime,在這裡我們用{{Message}}{{Time}}變數代表

n   URL的部分填上剛剛發布的API AppController網址

l   完成之後發布新的LUIS application,然後回到Bot Portal測試看看

 

 

LUIS Action Fulfillment +Bot Framework = 不需要程式碼的Chat Bot

Bot Framework搭配LUIS作為語意分析,可以讓我們的對話機器人更有人性的理解我們的需求。在LUIS新的版本中,也推出了許多方便的功能與Bot Framework搭配;其中一個Action Fulfillment可以讓我們不必寫程式也能夠完成許多基本的需求。

在這個例子中,我會建立一個LUIS應用程式透過預設的GetWeather Channel來取得現在的天氣狀況,並與Bot Framework整合。(完整的預設Channel列表在:https://www.luis.ai/Help#PublishingModel )

以下的例子假設您已經有使用LUIS的經驗,並且使用過Bot Framework Developer Portal

l   登入https://dev.botframework.com註冊一個新的Bot

n   Message Endpoint的部分可以先隨便填,稍後我們會用LUIS Endpoint取代

n   記下Bot App IDPassword,稍後會需要用到

l   首先,進入LUIS並建立一個新的Application。建立一個GetWeatherIntent並輸入一些句子訓練這個application

n   在這裡,我透過pre-builtentity來識別地點

l   打開GetWeather Intent

n   勾選"Fulfillment"並選擇Action TypeGetCurrentTemerature

n   新增一個Action

n   新增一個Parameters,名稱為LOCATIONTypegeography,並設為Required

n   因為GetCurrentTemerature Channel需要一個LOCATION作為輸入;因此在下面Action Settings的地方,"Append a Parameter"下拉選單選擇LOCATION

n   按下SAVE

l   回到Portal Publich這個Application

n   勾選Enable Action Binding using Microsoft Bot Framework

n   將剛剛註冊的Bot App IDPassword填入對應欄位

n   記下Endpoint URL for Microsoft Bot Framework Bot的網址

l   Publish完成後,回到https://dev.botframework.com打開剛剛註冊的Bot,把剛才Endpoint URL for Microsoft Bot Framework Bot填上去

l   測試看看

2016年11月25日

用Azure Functions處理物聯網事件

Azure FunctionsAzureServerless服務,非常適合用來處理需要快速開發佈署、任務單純的需求;使用Azure FunctionsDynamic層級時,系統會依照資源使用量自動調整系統規模,減少管理的人力與時間。
在為北京的客戶建立物聯網方案時,我們使用Azure Functions來作為呼叫第三方服務的方式;傳統設備端的數據進到IoT Hub之後,利用Stream Analytics挑出事件並放到Service Bus Queue中,然後利用Azure Functions把數據透過REST API傳給第三方服務。由於Queue的特性,我們可以簡單地達到保證送達;並且因為任務單純明確,使用Azure Functions可以很快的完成需求。
方案架構大約如下
<![if !supportLists]>1.       <![endif]>建立完Azure Functions後,在管理介面建立一個以C#撰寫的Http Trigger Azure Functions
<![if !supportLists]>2.       <![endif]>在這裡指定Service Bus Queue的相關資訊;由於Azure Functions底層是Web Jobs,這些連線資訊最後會被寫到Web AppApplication Settings裡。
<![if !supportLists]>        i.           <![endif]>如果服務需要額外的輸入;例如,從Service Bus Queue取得訊息之後,還必須根據他的Message IdDocument DB把相對應的文件取出來在程式中比對,可以在這裡增加額外的輸入。
<![if !supportLists]>       ii.           <![endif]>documentDB為例,假如我們的Service Bus Queue message會有一個欄位叫做documentId,那麼可以在以下Document ID的位置中輸入{documentId}告訴Azure Functions,要在指定的DocumentDB中把documentId欄位值為Service Bus Queue訊息中documentId屬性值相同的文件抓出來,做為另一個輸入。
<![if !supportLists]>3.       <![endif]>在這個例子中,我們沒有額外的輸入,也不需要輸出(回應訊息);因此直接案下完成建立即可
<![if !supportLists]>4.       <![endif]>接著回到開發頁簽,就可以在編輯器中寫程式了
<![if !supportLists]>        i.           <![endif]>因為是C#,系統預先Reference了一些預設的Assembly,詳情可以參考這裡:https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-csharp
<![if !supportLists]>       ii.           <![endif]>底層是Windows Server.Net Framework裡的Assembly可以透過#r的方式reference,例如
#r "Newtonsoft.Json"
<![if !supportLists]>      iii.           <![endif]>如果需要額外的NuGet Package,可以加入一個名為Project.json的檔案,透過Json方式定義所需的Package
<![if !supportLists]>5.       <![endif]>回到我們的範例,我的目的是把訊息從Service Bus Queue讀出來後,呼叫REST API;代碼非常簡單;完整的程式範例在:https://github.com/michael-chi/YFIOPOC/tree/master/Functions/QueueToYFIOAPI
public static void Run(string myQueueItem, TraceWriter log)
{
    log.Info($"C# ServiceBus queue trigger function processed message: {myQueueItem}");
    ChangedData data = JsonConvert.DeserializeObject<ChangedData>(myQueueItem);
   
    var logText = JsonConvert.SerializeObject(data);
    log.Info($"Message sending to YFIOAPO:{logText}");
   
    log.Info("Calling YFIO API...");
    var resp = YFIOHelper.Instance(data.ProjectId).WriteIOValues(data.Names,data.Values);
    log.Info($"Done...[RESP:{resp}]");
}
<![if !supportLists]>6.       <![endif]>完成後,在管理介面啟用即可

2016年11月23日

幾個發布UWP到Windows 10 IoT Core上的方式 (二) WinAppDeployCmd

  • WinAppDeployCmd工具隨著Windows 10 SDK安裝在C:\Program Files (x86)\Windows Kits\10\bin\x86目錄下
  • 詳細的使用方式可以參考這一篇:https://msdn.microsoft.com/zh-tw/library/mt203806.aspx
  • 確認Windows 10 IoT Core設備能以網路連線並且參考前一篇的方式產生UWP Package後
  • 打開Cmd切換目錄到WinAppDeployCmd安裝路徑輸入以下指令

WinAppDeployCmd.exe install -file "C:\Temp\App2\App2\AppPackagesv105\App2_1.0.7.0_Debug_Test\App2_1.0.7.0_arm_Debug.appxbundle" -ip 192.168.1.111

  • 其中App2_1.0.7.0_arm_Debug.appxbundle是剛剛用Visual Studio產生的Package檔案

幾個發布UWP到Windows 10 IoT Core上的方式 (一) Visual Studio/IoT Portal

算是把這幾天測試的心得做個筆記

  • Visual Studio遠端發布

這應該是開發人員最習慣的方式

    • 首先確認你的Visual Studio 2015已經安裝了Update 3以及Remote Debug Tool Update
    • 透過IoT Core的Portal確認Remote Debugger已經啟動

    • 如果沒看到msvsmon.exe,在Portal上設定啟動

      • 這裡如果還有錯誤,我的方式是更新到Windows最新版本,然後Reboot
    • 打開Visual Studio專案,在Debug設定如下;其中192.168.1.111是Windows 10 IoT Core設備的位址

    • 接著就可以用Visual Studio直接佈署了
  • 透過IoT Portal發布
    • 打開Visual Studio專案,選擇Store –> Package

    • 選擇No,因為IoT Core目前沒有OneStore可以用

    • 接著下一步直到Package建立完成;會看到以下幾個檔案

    • 打開瀏覽器,連到http://<IoT Device IP>:8080
    • 登入後,切換到Apps頁面;依序將剛剛產生的檔案上傳,最後按下Deploy等待佈署完成

2016年9月30日

在Windows Server上架設Mosquitto Server

筆記一下在Windows VM上安裝Mosquitto的步驟

openssl req -new -x509 -days 3650 -keyout m2mqtt_ca.key -out m2mqtt_ca.crt
openssl genrsa -des3 -out m2mqtt_srv.key 1024
openssl req -out m2mqtt_srv.csr -key m2mqtt_srv.key -new
openssl x509 -req -in m2mqtt_srv.csr -CA m2mqtt_ca.crt -CAkey m2mqtt_ca.key -CAcreateserial -out m2mqtt_srv.crt -days 3650

  • Mosquitto設定
    • 打開Mosquitto安裝目錄下的mosquitto.conf,加入以下設定

bind_address michi-win2012
port 8883
cafile C:\OpenSSL-Win32\mosquitto\m2mqtt_ca.crt
certfile C:\OpenSSL-Win32\mosquitto\m2mqtt_srv.crt
keyfile C:\OpenSSL-Win32\mosquitto\m2mqtt_srv.key
tls_version tlsv1

    • 打開command prompt,執行以下指令啟動Mosquitto

mosquitto -c mosquitto.conf –v

  • 記得打開Azure NSG

  • PAHO設定如下

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));
            }
        }
       
    }
}

Blog Archive

About Me