[IoT] Azure IoT整合應用五:透過WebApp轉送無法直接連接IoT Hub的裝置訊息

微軟的IoT Hub提供了許多IoT客戶端裝置作為訊息接收用的服務
不過使用IoT Hub有著一些限制,像是僅能使用HTTP、AMQP、MQTT這三種通訊協定進行訊息的發送
以及裝置中必須要能夠將Key值壓上Timespan,作為傳入IoT Hub訊息的識別

以目前一般僅有少量ROM的裝置來說,根本無法將微軟的Azure IoT SDK塞進裝置中,這時就只能透過WebApp作為轉送的中繼站,處理訊息轉送進IoT Hub的動作了
前提是,客戶端裝置可以透過修改程式的方式,將訊息送至WebAPI上,不過我相信透過程式碼的修改,目前絕大多數的客戶端裝置應該都可以達到這樣的功能

製作這樣的WebApp作為客戶端轉發的功能不難,但是相對的會多出一些工必須製作,像是必須要先將客戶端裝置的Id與Key先存在資料庫中等等

下面會將步驟完整的說明,當然,若是對系統架構較為熟悉的人應該很快就能上手了

首先,先建立一個資料庫,並加入一個DeviceFile的資料表,該資料表包含了DeviceId與DeviceKey兩個欄位

接著將在IoT Hub上,要透過WebApp轉送訊息的裝置編號與Key值寫進該資料庫中

這樣資料庫的準備動作就完成了,接著開啟Visual Studio,並建立一個新的Azure API App專案

在WebAPI的專案中,先加入[Microsoft.Azure.Devices.Client][Microsoft.Azure.Devices]兩個Nuget套件

接著在Models的資料夾下,建立一個Message.cs的類別庫,並將下面程式碼加入至該類別庫中

public class Message
{
    public string DeviceId { get; set; }
    public string MessageContent { get; set; }
}

這個類別庫主要是在定義要從裝置端透過POST方法傳送訊息至WebApp時的JSON物件格式,當然實際開發的情況可以視需要進行修改,在這裡,我們只將裝置代碼與訊息從客戶端裝置傳送至WebApp上

接著在Controllers的資料夾中,加入[MessagesController]這個控制器

將下面的程式碼放入至MessagesController.cs這一個控制器的程式碼中

static string iotHubUri = "[在這裡放入IoT Hub的Url]";

/// <summary>
/// 發送訊息至IoT Hub
/// </summary>
/// <param name="value">發送物件</param>
/// <returns></returns>
public async Task<HttpResponseMessage> Post([FromBody]Models.Message value)
{
    HttpStatusCode code = HttpStatusCode.OK;
    string strContent = "true";

    try
    {
        // 透過EF找出資料庫中該裝置的Key值
        Models.DeviceFile objDevice = new Models.IoTModel().DeviceFile.FirstOrDefault(x => x.DeviceId == value.DeviceId);

        if (objDevice != null)
        {
            string strDeviceKey = objDevice.DeviceKey;

            // 傳送訊息進入IoT Hub
            DeviceClient deviceClient = null;
            deviceClient = DeviceClient.CreateFromConnectionString
            (
                $"HostName={iotHubUri};DeviceId={objDevice.DeviceId};SharedAccessKey={objDevice.DeviceKey}",
                Microsoft.Azure.Devices.Client.TransportType.Amqp
            );

            await deviceClient.SendEventAsync(new Microsoft.Azure.Devices.Client.Message(Encoding.UTF8.GetBytes(value.MessageContent)));
        }
        else
        {
            code = HttpStatusCode.NotFound;
            strContent = "找不到該裝置,該裝置並無登錄於資料庫中";
        }

    }
    catch (Exception e)
    {
        code = HttpStatusCode.BadRequest;
        strContent = e.Message;
    }

    return Request.CreateResponse(code, strContent);
}

這一段程式碼主要的功能在,透過Entity Framework的方式,將客戶端裝置傳入的裝置代碼,找出對應的DeviceKey值,並將訊息轉送至IoT Hub中。整段程式碼其實不複雜,就只是很單純的取出Key值並轉送進IoT Hub中

DeviceClient.CreateFromConnectionString 的方法,請使用本範例中的連線字串格式,若是更改其他格式,可能會造成WebApp發生過多連線無法釋放的錯誤
Entity Framework的使用,請自行找尋其他參考文件設定,在本篇文章中不會特別說明該如何使用

當程式碼製作完成後進行測試一下,看是否能夠執行從上圖Swagger的測試畫面來看,Messages的控制器已經收到測試畫面送入的訊息,也回傳測試成功的訊息

接下來,就必須在客戶端的裝置,加上POST的副程式,將訊息送至WebAPI上,在這裡,我將模擬器的程式碼作了一些修改,並將POST訊息的功能加上去。
首先在模擬器上,加入一個要POST到WebAPI的按鈕

接著將下面的程式碼,加入至ButtonClick事件與表單程式碼中

/// <summary>
/// 將訊息送出至WebAPI的動作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSendToWebAPI_Click(object sender, EventArgs e)
{
    // 因為只要送到WebAPI,所以只要訊息跟裝置代碼就夠了
    MessageModel objMsg = new MessageModel()
    {
        DeviceId = txtDeviceId.Text,
        MessageContent = txtMessage.Text,
    };

    // 放在網路上的WebApp的Url
    string strUrl = "[WebAPI的Url]";

    // 送出至WebAPI
    HttpStatusCode code = HttpStatusCode.OK;
    string strResponse = CallAPI(strUrl, "POST", JsonConvert.SerializeObject(objMsg) , out code);

    if (code == HttpStatusCode.OK)
    {
        MessageBox.Show("發送完成");
    }
    else
    {
        MessageBox.Show("發送失敗," + strResponse);
    }
}

protected string CallAPI(string strUrl, string strHttpMethod, string strPostContent, out HttpStatusCode code)
{
    HttpWebRequest request = HttpWebRequest.Create(strUrl) as HttpWebRequest;
    request.Method = strHttpMethod;
    code = HttpStatusCode.OK;

    if (strPostContent != "" && strPostContent != string.Empty)
    {
        request.KeepAlive = true;
        request.ContentType = "application/json";

        byte[] bs = Encoding.ASCII.GetBytes(strPostContent);
        Stream reqStream = request.GetRequestStream();
        reqStream.Write(bs, 0, bs.Length);
    }

    string strReturn = "";
    try
    {
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        var respStream = response.GetResponseStream();
        strReturn = new StreamReader(respStream).ReadToEnd();
    }
    catch (Exception e)
    {
        strReturn = e.Message;
        code = HttpStatusCode.NotFound;
    }

    return strReturn;
}

這段程式碼主要的目的,就是當按下送出至WebAPI的按鈕時,會將裝置代碼與訊息轉換成MessageModel的物件JSON字串,再透過CallAPI這一個副程式將訊息POST到WebAPI上並顯示成功或是失敗的訊息

程式碼寫好後,實際執行看看結果,可以得到訊息確實送進IoT Hub了

IoT Hub也有確實收到訊息,這邊有兩筆訊息,分別是透過Swagger測試傳入的訊息,以及透過模擬器送入的訊息,所以總數為2筆

以目前的IoT裝置來說,由於壓低成本的關係,所以裝置本身無法包含IoT SDK的所有功能,但是透過WebApp進行IoT訊息轉送的設計,可以先解決老舊裝置無法直接支援Azure IoT Hub的問題

範例程式碼下載:
https://github.com/madukapai/maduka-Azure-IoT