HTTP integration with Azure Functions (testing)


(Makerns) #1

Hello,

I am going to give you a short example for HTTP integration between TTN and Azure Functions. The main ides is creating brigde between TTN and Azure Services (Service bus, IoT Hub, Azure SQL server …). I will not going deep inside what you can do with Azure Functions, but the most important feature is output which can be routed to other Azure endpoints without writing heavy code…

Azure Function App is serverless platform for executing module written by different programming language (C#, NodeJS, Python…). Each function has their URL, and we are going to use it for HTTP integration.

I have a very simple test environment : LoRa endnode send every minute short message “MAKER Novi Sad” to the gateway. Payload is passed to the decoder script for creating JSON payload:

function Decoder(bytes, port) {
  // Decode an uplink message from a buffer
  // (array) of bytes to an object of fields.
  
  var decoded = {};
  decoded.tekst = String.fromCharCode.apply(null,bytes);

  return decoded;
}

Azure function App

Next step is creating Azure Function App as HTTP trigger function. For now, I left my function app unsecure (anonymous). This is first step, checking basic functionality…Next step is making all secure with keys… :slight_smile:

  • Here is code for Azure function:

    module.exports = function (context, req) {

      context.log(req.body.app_id);
      context.log(req.body.payload_fields.tekst);
      context.done();
    

    };

  • Set Azure function for anonymous access
    image

  • Get URL for Azure function and save it somewhere

HTTP integration for TTN application

  • Put Azure Function URL in URL field for TTN HTTP integration

image

If you are lucky, you will see news in Log window of Azure function:

2017-11-28T10:30:34.986 Function started (Id=5691f8d7-2a6f-4e17-8429-6cc3f76e1bc2)
2017-11-28T10:30:34.986 test-novi-sad
2017-11-28T10:30:34.986 MAKER Novi Sad
2017-11-28T10:30:34.986 Function completed (Success, Id=5691f8d7-2a6f-4e17-8429-6cc3f76e1bc2, Duration=2ms)
2017-11-28T10:31:36.870 Function started (Id=60ab43f0-9259-4f7e-88f0-e456f49e372e)
2017-11-28T10:31:36.870 test-novi-sad
2017-11-28T10:31:36.870 MAKER Novi Sad
2017-11-28T10:31:36.870 Function completed (Success, Id=60ab43f0-9259-4f7e-88f0-e456f49e372e, Duration=3ms)

HTTP Integration: pass only payload_fields
Data to Azure IoT Central
(Hylke Visser) #2

Apart from the fact that you’re sending plain ASCII text over LoRa (please don’t do that :sob:) this is really cool! Please consider writing a Labs story about it, so that others can use it as an example.

As you already mentioned, you should really use authorization. Make sure to add that to the Labs story to set a good example :slight_smile:


(Makerns) #3

Don’t worry, this is just for fast testing. Next step is reading and sending sensor data…

I want to test several options, and now in test production are AllThingsTalk and DataStorage integrations.


#4

Great explanation, thanks makerns. I tried the same with a binary payload and apparently once it’s decoded in the backend the integration forwards the decrypted (non-binary) payload. Which makes sense but was still nice to see.


#5

I’ve been struggling to get this working using C#, but I failed. I haven’t used C# for ages. I made the switch because using this in my example would be easier for the follow up steps, but the thing is that the contents are a key/value pair and I don’t manage to retrieve data that is in it. The default generated code tries to retrieve the value ‘name’ but if I interpret it correctly it tries to do so assuming that this ‘name’ value is in the header. Which is a different situation.

Can anyone point me to an example or explain where I make a mistake in understanding the concept? Why is the GetQueryNameValuePairs() even performed at the header instead of the contents at all?

edit: ok, I just realized that it’s not that obvious that the content is json. And that the examples I used didn’t expect that. Need to dig more into it :slight_smile:

using System.Net;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
   // log.Info("C# HTTP trigger function processed a request." + req.ToString());  // this shows non specific header content. 
    log.Info("C# HTTP trigger function processed a request.  v1.07");

    var Content = req.Content;

    log.Info("***");

    log.Info(Content.ToString());  // works too. It says "System.Web.Http.WebHost.HttpControllerHandler+LazyStreamContent"


    var jsonContent = Content.ReadAsStringAsync().Result;   
    log.Info("***");

    log.Info(jsonContent.ToString());   // this works, but I can't get the contents in a usable format. Like a specific value in a string variable.



    // parse query parameter
    string name = req.GetQueryNameValuePairs()  // here I get lost. Why even try to find the 'app_id' in the header?
        
        .FirstOrDefault(q => string.Compare(q.Key, "app_id", true) == 0)  // app_id used to be 'name' in the example
        .Value;

     
    if (name == null)
    {
        // Get request body
        log.Info("Get req body");
        dynamic data = await req.Content.ReadAsAsync<object>();
        name = data?.name;
        log.Info(name);
    }

     log.Info("Finished");

    return(name == null)
        ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
}

this is the content of jsonContent.ToString(). Looks useful to me, but breaking it up is the big problem.

{"app_id":"maaspoortmeet","dev_id":"maaspoortmeet6","hardware_serial":"0004F8F6D4F3483C","port":10,"counter":57389,"payload_raw":"BP1qApc=","payload_fields":{"payload":"64874;663"},"metadata":{"time":"2018-04-10T15:39:34.151650006Z","frequency":867.5,"modulation":"LORA","data_rate":"SF7BW125","coding_rate":"4/5","gateways":[{"gtw_id":"eui-fffeb827eb26fb3c","timestamp":2762172236,"time":"2018-04-10T15:39:34.127412Z","channel":5,"rssi":-117,"snr":-2.5,"rf_chain":0,"latitude":51.73578,"longitude":5.2825}]},"downlink_url":"https://integrations.thethingsnetwork.org/ttn-eu/api/v2/down/maaspoortmeet/mm_csharp?key=ttn-account-v2.vXxUrOMWWc4w7D_TIM9vSTKuC2BzCyh6QKgxXqlRbzU"}


(Makerns) #6

Hi @TijnOnlijn,

I’ve never used c#, but from your code and from other languages you shouldn’t transform jsonContent into string.

I suppose that var jsonContent = Content.ReadAsStringAsync().Result; produce object with fields like jsonContent.app_id and with value = maaspromeet … try with log.Info(jsonContent.app_id)

As I said I don’t understand C#, but found something useful


#7

It works!

using System.Net;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");

    // parse query parameter
    string app_id = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "app_id", true) == 0)
        .Value;

    if (app_id == null)
    {
        // Get request body
        dynamic data = await req.Content.ReadAsAsync<object>();
        app_id = data?.app_id;
    }

    log.Info("app_id: " + app_id);

    return app_id == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK, "Hello " + app_id);
}

I used this tutorial and now it works. Still I don’t understand why the code checks for the app_id twice… And how should the syntax app_id = data?.app_id; be read?


(Jac Kersing) #8

Assign app_id the value of data.app_id if data is not null, otherwise assign it null.


(Zhaozhenhong) #9

Hello,I try the code in my HTTP trigger function,but the code seems error,can’t run. Do you know why?

See the error below:
2018-12-29T14:21:20 Welcome, you are now connected to log-streaming service.
2018-12-29T14:21:28.831 [Information] Executing ‘Functions.HttpTrigger1’ (Reason=‘This function was programmatically called via the host APIs.’, Id=a91fa2bc-460d-4ef5-acbe-888e77d00f0a)
2018-12-29T14:21:29.015 [Error] Function compilation error
Microsoft.CodeAnalysis.Scripting.CompilationErrorException : Script compilation failed.
at async Microsoft.Azure.WebJobs.Script.Description.DotNetFunctionInvoker.CreateFunctionTarget(CancellationToken cancellationToken) at C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\DotNet\DotNetFunctionInvoker.cs : 314
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at async Microsoft.Azure.WebJobs.Script.Description.FunctionLoader`1.GetFunctionTargetAsync[T](Int32 attemptCount) at C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\FunctionLoader.cs : 55
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at async Microsoft.Azure.WebJobs.Script.Description.DotNetFunctionInvoker.GetFunctionTargetAsync(Boolean isInvocation) at C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\DotNet\DotNetFunctionInvoker.cs : 183
2018-12-29T14:21:29.060 [Warning] run.csx(3,75): warning CS0618: ‘TraceWriter’ is obsolete: ‘Will be removed in an upcoming version. Use Microsoft.Extensions.Logging.ILogger instead.’
2018-12-29T14:21:29.080 [Error] run.csx(8,25): error CS1061: ‘HttpRequestMessage’ does not contain a definition for ‘GetQueryNameValuePairs’ and no extension method ‘GetQueryNameValuePairs’ accepting a first argument of type ‘HttpRequestMessage’ could be found (are you missing a using directive or an assembly reference?)
2018-12-29T14:21:29.117 [Error] run.csx(22,15): error CS1501: No overload for method ‘CreateResponse’ takes 2 arguments
2018-12-29T14:21:29.166 [Error] run.csx(23,15): error CS1501: No overload for method ‘CreateResponse’ takes 2 arguments
2018-12-29T14:21:29.285 [Error] Executed ‘Functions.HttpTrigger1’ (Failed, Id=a91fa2bc-460d-4ef5-acbe-888e77d00f0a)
Script compilation failed.


(Zhaozhenhong) #10

Hello,I try the code in my HTTP trigger function,but the code seems error,can’t run. Do you know why?

See the error below:
2018-12-29T14:55:21 Welcome, you are now connected to log-streaming service.
2018-12-29T14:55:26.830 [Information] Executing ‘Functions.HttpTrigger1’ (Reason=‘This function was programmatically called via the host APIs.’, Id=21ef118b-d8ee-4b66-bc0b-5c0aa3787bbd)
2018-12-29T14:55:26.923 [Information] undefined
2018-12-29T14:55:26.960 [Error] Executed ‘Functions.HttpTrigger1’ (Failed, Id=21ef118b-d8ee-4b66-bc0b-5c0aa3787bbd)
Result: Failure
Exception: TypeError: Cannot read property ‘tekst’ of undefined
Stack: TypeError: Cannot read property ‘tekst’ of undefined
at module.exports (D:\home\site\wwwroot\HttpTrigger1\index.js:4:39)
at WorkerChannel.invocationRequest (D:\Program Files (x86)\SiteExtensions\Functions\2.0.12246\32bit\workers\node\worker-bundle.js:28862:26)
at ClientDuplexStream.WorkerChannel.eventStream.on (D:\Program Files (x86)\SiteExtensions\Functions\2.0.12246\32bit\workers\node\worker-bundle.js:28752:30)
at emitOne (events.js:116:13)
at ClientDuplexStream.emit (events.js:211:7)
at addChunk (_stream_readable.js:263:12)
at readableAddChunk (_stream_readable.js:250:11)
at ClientDuplexStream.Readable.push (_stream_readable.js:208:10)
at Object.onReceiveMessage (D:\Program Files (x86)\SiteExtensions\Functions\2.0.12246\32bit\workers\node\worker-bundle.js:42351:19)
at InterceptingListener.module.exports.InterceptingListener.recvMessageWithContext (D:\Program Files (x86)\SiteExtensions\Functions\2.0.12246\32bit\workers\node\worker-bundle.js:41678:19)