Plant Health evaluation using Raspberry Pi, Xamarin Forms App,Azure Function, Table,Storage, Key vault, Custom vision, Drone
Tags: azure, azurefunctions
Published: 2022-10-31 13:07:18

**- Problem Statement**
 
To Maintain healthiness of the Plant in larger area of farm is really challenging, Farmers needs to keep an eye on the plants they grow whether it is not affected by insects or disease, it requires manual inspection of plants by multiple farm workers and navigate through entire area of farm and inspecting them manually which is taking down many days for the farm workers and this needs to be repeated periodically which is consuming lots of time.

<img src="https://media.giphy.com/media/3og0ILgFOEXIL8Bsn6/giphy.gif" alt="busy farmer" width="350px" height="350px" loading="lazy">

**- Solution/ Architecture**

This problem can be solved using Azure based solution where drone can capture images of elephant ear plant and identify if plant is affected by Insect or diseases using Azure Storage account which will hold the image captured by drone and it will be processed against the custom vision in which has trained images of healthy, and insect or diseases affected plants
<img src="https://media.giphy.com/media/5bivKwxhVzshNk2Rjw/giphy.gif" alt="busy farmer" width="350px" height="350px" loading="lazy">

Please refer **Application flow**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/afptcxxv5d2buy1a3rv6.png)

we will need following. 

**Azure Subscription**: https://azure.microsoft.com/en-us/free/

**.Net sdk 6.0** : - https://dotnet.microsoft.com/en-us/download/dotnet/6.0

**Visual Studio 2022**: - https://visualstudio.microsoft.com/vs/community/

Visual Studio needs following workloads.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fkx3ltxwbrzv54y8sx1g.png)

**Azure Custom Vision**: -https://www.customvision.ai/ 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5lkul37r5bagapj5mmeg.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/90dwmtbw3xo152wieze5.png)
Click on create Project. Upload healthy and infected plant images and tag the uploaded images and perform the quick training.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wlgl4g26bbwh6m76z9ev.png)

Goto Performance Tab follow the steps and copy the url and key which will be used to access it.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8e8foizs54yusicir7a.png)

**Drone with Camera and GPS Module**:- Drone supported by Parrot SDK and having FTP Server , learn more here https://www.parrot.com/

Please find the Github Link for the Project, you will need to enter correct Azure resources (e.g. storage , table , custom vision end point , ftp) values to run the Project.

https://github.com/jaymin93/PlantHealthApp 

**-Technical Details and Implementation of solution**
<img src="https://media.giphy.com/media/ZVik7pBtu9dNS/giphy.gif" alt="busy farmer" width="350px" height="350px" loading="lazy">

**1. PlantHealthConsoleApp** :- **.net 6** Console App

App will monitor Drone created FTP which will have images captured by Drone from farm, app will periodically check FTP server and images will be copied to destination directory from where it will be uploaded to **Azure Storage as a Blob**

Start Visual studio 2022 follow below steps

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v1axlbebsgogn9cy1191.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n9dnddkrnvkkren75zse.png)

Please provide the Name and Path for the Project.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zh4b4nigfa6x9cppcciy.png)

Select **.net 6 LTS** (For Production workloads LTS is highly recommended) Click on Create it will create Console Project. 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixdr4o8ruogvql117dq5.png)

Once Project has created, we will need below Nuget Packages which can be added from the below option


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uje5ljaqsfe9q7ud4kfw.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hszzwprc4bl9t9hguvwn.png)

It will connect to Ftp server created by drone and get the values from the **_appsettings.json_** e.g. blob storage uri , ftp information and Secret Identifier 

We will need Storage account and Azure keyvault Secret information for that go to https://portal.azure.com/

Create new Storage account

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iydelj43cty627sx9bks.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ga3nh2ii3wq2om2zvjjl.png)

enter resource group name or select existing and add storage account name

select review+create.

Go to storage account you have created and select access key tab as below image

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vwuztyejdh82znc75d3s.png)

Create Container
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ukqr4bifmret8s54saz.png)

KeyVault 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7587wweld5496kr60ogm.png)

Please enter correct resource group name and keyvault name then click on review + create 

Navigate to keyvault and select Secret then Click on Generate Import
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gx8eifitjp1oqywk43k.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9cg2cswm33evgnkbsl0d.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebdnurk0glybq4svqbjz.png)

Please install nuget from the above menu by searching with the name.

_Azure.Storage.Blobs
Microsoft.Azure.KeyVault
Microsoft.Azure.KeyVault.Models
Microsoft.Extensions.Configuration
Microsoft.Extensions.Hosting
WinSCP_

**Program.cs**
<code>
using Azure.Storage.Blobs;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using WinSCP;

namespace PlantHealthConsoleApp
{
    public class Program
    {
        private static IConfiguration? config;
        private static string? connectionstring;
        private static string? storageaccounturi;
        private static string? containername;
        private static string secretIdentifier = string.Empty;
        private static string imageDirPath = string.Empty;
        private static SecretBundle? secret;
        private static KeyVaultClient? client;
        private static System.Timers.Timer? timer;
        private static string imageProcessPath = string.Empty;

        public async static Task Main(string[] args)
        {
            HostBuilder builder = new HostBuilder();

            config = new ConfigurationBuilder()
             .AddJsonFile("appsettings.json", true, true)
             .Build();
            CheckForNewFileAdditionToDirectory();
            InitTimer();

            await builder.RunConsoleAsync();
        }
        private static void InitTimer()
        {
            timer ??= new System.Timers.Timer();
            timer.Enabled = true;
            timer.Interval = 60000;
            timer.Elapsed += Timermr_Elapsed;
        }

        private static void Timermr_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
        {
            GetFilesFromDronFTPServer(GetvaluesFromConfig("droneFtpUrl"), GetvaluesFromConfig("ftpUsername"), GetvaluesFromConfig("ftpPassword"), Convert.ToInt32(GetvaluesFromConfig("ftpport")));
        }

        private static void CheckForNewFileAdditionToDirectory()
        {
            imageDirPath = GetvaluesFromConfig("imageDirPath");
            FileSystemWatcher watcher = new()
            {
                Path = GetDirectoryForImageUpload()
            };
            watcher.Created += FileSystemWatcher_FileCreatedEvent;
            watcher.EnableRaisingEvents = true;
        }

        private static string GetDirectoryForImageUpload()
        {
            imageProcessPath = $"{Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), imageDirPath)}";
            Console.WriteLine($"path is {imageProcessPath}");
            CreateDirectoryIfNotExist(imageProcessPath);
            return imageProcessPath;
        }

        private static void CreateDirectoryIfNotExist(string DirectoryPath)
        {
            if (!Directory.Exists(DirectoryPath))
            {
                Directory.CreateDirectory(DirectoryPath);
            }
        }

        private static string GetvaluesFromConfig(string key)
        {
            if (!string.IsNullOrEmpty(key) && config is not null)
            {
                return config[key];
            }
            return string.Empty;
        }

        private static void SetClientIDAndSecret()
        {
            TokenHelper.clientID ??= GetvaluesFromConfig("clientID");
            TokenHelper.clientSecret ??= GetvaluesFromConfig("clientSecret");
        }
        private async static void FileSystemWatcher_FileCreatedEvent(object sender, FileSystemEventArgs fileSystemEvent)
        {
            using (FileStream fileStream = new(fileSystemEvent.FullPath, FileMode.Open))
            {
                try
                {
                    storageaccounturi = GetvaluesFromConfig("storageaccounturi");
                    containername = GetvaluesFromConfig("containername");
                    secretIdentifier = GetvaluesFromConfig("secretIdentifier");
                    SetClientIDAndSecret();
                    client ??= new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(TokenHelper.GetAccessTokenAsync));
                    secret ??= await client.GetSecretAsync(secretIdentifier);
                    connectionstring ??= secret.Value;
                    if (!string.IsNullOrEmpty(fileSystemEvent.Name))
                    {
                        await UploadFileToAzureStorageAsync(connectionstring, fileSystemEvent.Name, containername, fileStream);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }

        private static async Task<bool> UploadFileToAzureStorageAsync(string connectionString, string fileName, string containerName, Stream fileStream)
        {
            BlobClient blobClient = new BlobClient(connectionString, containerName, fileName);
            await blobClient.UploadAsync(fileStream);
            Console.WriteLine($"file {fileName} uploaded successfully");
            return await Task.FromResult(true);
        }

        private static void GetFilesFromDronFTPServer(string droneFtpUrl, string ftpUsername, string ftpPassword, int ftpport)
        {
            try
            {
                imageProcessPath ??= GetDirectoryForImageUpload();
                SessionOptions sessionOptions = new SessionOptions
                {
                    Protocol = Protocol.Ftp,
                    HostName = droneFtpUrl,
                    UserName = ftpUsername,
                    Password = ftpPassword,
                    PortNumber = ftpport
                };
                using (Session session = new Session())
                {
                    string droneCapturedImagePath = "/home/prt85463/images";
                    session.Open(sessionOptions);
                    session.GetFiles(droneCapturedImagePath, imageProcessPath).Check();
                    session.RemoveFiles(droneCapturedImagePath);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}
</code>

Add new class by selecting from Solution explorer.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fcliysliv9y1ddng908n.png)
 
_This class will be used to Get AccessToken to connect to keyvault and Get secret values from it._

**ToeknHelper.cs**

<code>
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Diagnostics;

namespace PlantHealthConsoleApp
{
    public static class TokenHelper
    {
        public static string clientID;
        public static string clientSecret;
        public static async Task<string> GetAccessTokenAsync(string authority, string resource, string scope)
        {
            var context = new AuthenticationContext(authority);
            ClientCredential credential = new ClientCredential(clientID,clientSecret);
            AuthenticationResult result = await context.AcquireTokenAsync(resource, credential);
            Trace.WriteLine(result.AccessToken);
            if (result == null)
                throw new InvalidOperationException("Failed to obtain the JWT token");

            return result.AccessToken;
        }
    }
}
</code>

Click on solution explorer and Select below Option, this app will be executing on Rasbpian Linux arm 64 so same configuration will be selected to Publish it.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dq2b44c6wm87g4rjtk3h.png)

From the option select Target runtime Linux-arm64 and Deployment as self contained ,learn more here https://learn.microsoft.com/en-us/dotnet/core/deploying/
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/en2yuo2ffg319718xfcv.png)

Once Published copy executables to Rabpian and follow set permission chmod +x filename and then use ./filename to execute application, learn more from here https://learn.microsoft.com/en-us/dotnet/iot/deployment

**2. PlantHealthApp** :- **Azure Function Blob Trigger** with **.net 6** as Target Framework. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2e4zyg6oxgb1avc9nmhu.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f401eoetgeqsvefxysev.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zi7h76q8rxde0zf6rw7t.png)




Function will be triggered once there is new image uploaded by console app, which will be sent to **Azure Custom Vision** for prediction if plant is affected by insect or dieses, details of the affected plant will be stored in **Azure Table** 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ihzee0o33sg4axcbdgp5.png)

Select Azure Function
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m2eg4ik222sn5ywmxdws.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ewyk9e1wxsqhg3hxn6nc.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wfqwk7vwbng292rwcdd8.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mj127no1bhxgfw3oiqor.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0go1v5hksjofcr7dadq.png)

Provide Connection string name and click on finish.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qlk7391r91clubrzz12y.png)

Please install following nuget packages 

Azure.Security.KeyVault.Secrets
Microsoft.Azure.CognitiveServices.Vision.CustomVision.Prediction
Microsoft.Azure.CognitiveServices.Vision.CustomVision.Training
Microsoft.Azure.KeyVault
Microsoft.Azure.WebJobs.Extensions.Storage
Microsoft.Identity.Client
Microsoft.IdentityModel.Clients.ActiveDirectory
RestSharp

**GetPlantHealth.cs**

<code>
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net;
using Microsoft.Azure.KeyVault;

namespace GetPlantHealthDetails
{
    public class GetPlantHealth
    {
        private static CloudStorageAccount storageAccount = null;
        private static CloudTableClient tableClient = null;
        private static CloudTable table = null;
        private static KeyVaultClient client = null;
        private static Microsoft.Azure.KeyVault.Models.SecretBundle connectionstring = null;
        private static string tableName = "PlantHealthAppTable";
        private static string secretIdentifier = "https://planthealthappsecret.vault.azure.net/secrets/storageAccountConnectionString/92f4ed20ff4041ae8b05303f7baf79f7";

        [FunctionName("GetPlantHealth")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
            ILogger log)
        {
            SetClientIDAndSecret();
            client ??= new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(TokenHelper.GetAccessTokenAsync));
            connectionstring ??= await client.GetSecretAsync(secretIdentifier);
            storageAccount ??= CloudStorageAccount.Parse(connectionstring.Value);
            tableClient ??= storageAccount.CreateCloudTableClient();
            table ??= tableClient.GetTableReference(tableName);

            string rowkey = req.Query["RowKey"];
            if (string.IsNullOrEmpty(rowkey))
            {
                return new OkObjectResult(await GetPlantHealthDeatilsAsync(log));
            }
            else
            {
                return new OkObjectResult(await UpdatePlantHealthDeatilsByRowkeyAsync(rowkey, log));
            }
        }

        private static async Task<List<PlantHealthDeatils>> GetPlantHealthDeatilsAsync(ILogger logger)
        {
            try
            {
                List<PlantHealthDeatils> plantHealthDeatilsList = new List<PlantHealthDeatils>();
                TableQuery<PlantHealthDeatils> query;
                query = new TableQuery<PlantHealthDeatils>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, $"{tableName}"));
                TableContinuationToken token = null;
                do
                {
                    TableQuerySegment<PlantHealthDeatils> resultSegment = await table.ExecuteQuerySegmentedAsync(query, token).ConfigureAwait(false);
                    foreach (var entity in resultSegment.Results.OrderBy(x => x.Pesticidesprayed))
                    {
                        PlantHealthDeatils details = new PlantHealthDeatils
                        {
                            longitude = entity.longitude,
                            ImageURL = entity.ImageURL,
                            latitude = entity.latitude,
                            Pesticidesprayed = entity.Pesticidesprayed,
                            CapturedTime = entity.CapturedTime,
                            RowKey = entity.RowKey,
                            ETag = entity.ETag,
                            PartitionKey = entity.PartitionKey
                        };
                        plantHealthDeatilsList.Add(details);
                    }
                } while (token != null);
                return plantHealthDeatilsList;
            }
            catch (Exception exp)
            {
                logger.LogError(exp, "Unable to GetPlantHealthDeatils");
                return default;
            }
        }

        private static async Task<HttpResponseMessage> UpdatePlantHealthDeatilsByRowkeyAsync(string rowkey, ILogger logger)
        {
            try
            {
                PlantHealthDeatils plantHealthDeatilsList = new PlantHealthDeatils();
                TableQuery<PlantHealthDeatils> query;
                query = new TableQuery<PlantHealthDeatils>().Where(TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, $"{rowkey}"));
                TableContinuationToken token = null;

                TableQuerySegment<PlantHealthDeatils> resultSegment = await table.ExecuteQuerySegmentedAsync(query, token).ConfigureAwait(false);

                var plantdetail = resultSegment.Results.FirstOrDefault();
                plantdetail.Pesticidesprayed = true;
                var operation = TableOperation.Replace(plantdetail);
                await table.ExecuteAsync(operation);

                return new HttpResponseMessage(HttpStatusCode.NoContent);
            }
            catch (Exception exp)
            {
                logger.LogError(exp, "Unable to Update PlantHealthDeatils");
                return default;
            }
        }

        private static string GetEnviromentValue(string key)
        {
            return Environment.GetEnvironmentVariable(key);
        }

        private static void SetClientIDAndSecret()
        {
            TokenHelper.clientID ??= GetEnviromentValue("clientID");
            TokenHelper.clientSecret ??= GetEnviromentValue("clientSecret");
        }
    }
}

</code>

Add new class file as below 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hos71icsqae8y9cikm0.png)

**PlantHealthDeatils.cs** this class will be inherited by TableEntity class that will be used to communicate as model class for the Azure Table , please notice parameterised constructor has two important arguments PartitionKey ,  RowKey 

<code>
using Microsoft.WindowsAzure.Storage.Table;
using System;

namespace GetPlantHealthDetails
{
    public class PlantHealthDeatils : TableEntity
    {
        public PlantHealthDeatils()
        {

        }
        public PlantHealthDeatils(string skey, string srow)
        {
            PartitionKey = skey;
            RowKey = srow;
        }
        public DateTime CapturedTime { get; set; }
        public string longitude { get; set; }
        public string latitude { get; set; }
        public string ImageURL { get; set; }
        public bool Pesticidesprayed { get; set; } = false;
      

    }
}

</code>

For Publishing to the Azure we will be creating Azure function 

Click review + create
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86xa8pht5vcdnfq3xc8k.png)

Once Azure function is created, we can deploy it to Azure 

From Visual Studio select below 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98meg8etfaklyvlv244r.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/liie669sj0xy9x64do1k.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x59h1zyvzot4trpfkiws.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ipf4amzu8476bdu4b6uh.png)

local.settings.json contais key value pair for stoarge account url,custom vision and Table, this needs to be added to Azure function from Portal
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivsbruzult4eriqtqdw2.png)

Once Publish is successful, we can upload image to container and Blob Trigger will be executed, affected plant deatils will be stored in the Azure Table

**3. GetPlantHealthDetails** :- **Azure Function Http Trigger** with **.net 6** as Target Framework. 

Function will retrieve data of the affected plant from Azure Table and will serve the response to the **Xamarin Forms** Based App running on **Windows, Android, Ios**

rowkey based query string can be sent to Azure Function which will update the Pesticide spray status flag of particular record which can be done from the agricultural drone spraying pesticide.

Add new Project of Azure function (please refer above screen shot),we will be adding Http Trigger Azure function Trigger Type.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p7tm9gmmuxlt3rmzylx2.png)

Please install following nuget packages 

Azure.Identity
Azure.Security.KeyVault.Secrets
Microsoft.Azure.Functions.Extensions
Microsoft.Azure.KeyVault
Microsoft.Identity.Client
Microsoft.IdentityModel.Clients.ActiveDirectory
System.Configuration.ConfigurationManager

**GetPlantHealth.cs**
<code>
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.Storage.Table;
using Microsoft.WindowsAzure.Storage;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net;
using Microsoft.Azure.KeyVault;

namespace GetPlantHealthDetails
{
    public class GetPlantHealth
    {
        private static CloudStorageAccount storageAccount = null;
        private static CloudTableClient tableClient = null;
        private static CloudTable table = null;
        private static KeyVaultClient client = null;
        private static Microsoft.Azure.KeyVault.Models.SecretBundle connectionstring = null;
        private static string tableName = "PlantHealthAppTable";
        private static string secretIdentifier = "https://planthealthappsecret.vault.azure.net/secrets/storageAccountConnectionString/92f4ed20ff4041ae8b05303f7baf79f7";

        [FunctionName("GetPlantHealth")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
            ILogger log)
        {
            SetClientIDAndSecret();
            client ??= new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(TokenHelper.GetAccessTokenAsync));
            connectionstring ??= await client.GetSecretAsync(secretIdentifier);
            storageAccount ??= CloudStorageAccount.Parse(connectionstring.Value);
            tableClient ??= storageAccount.CreateCloudTableClient();
            table ??= tableClient.GetTableReference(tableName);

            string rowkey = req.Query["RowKey"];
            if (string.IsNullOrEmpty(rowkey))
            {
                return new OkObjectResult(await GetPlantHealthDeatilsAsync(log));
            }
            else
            {
                return new OkObjectResult(await UpdatePlantHealthDeatilsByRowkeyAsync(rowkey, log));
            }
        }

        private static async Task<List<PlantHealthDeatils>> GetPlantHealthDeatilsAsync(ILogger logger)
        {
            try
            {
                List<PlantHealthDeatils> plantHealthDeatilsList = new List<PlantHealthDeatils>();
                TableQuery<PlantHealthDeatils> query;
                query = new TableQuery<PlantHealthDeatils>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, $"{tableName}"));
                TableContinuationToken token = null;
                do
                {
                    TableQuerySegment<PlantHealthDeatils> resultSegment = await table.ExecuteQuerySegmentedAsync(query, token).ConfigureAwait(false);
                    foreach (var entity in resultSegment.Results.OrderBy(x => x.Pesticidesprayed))
                    {
                        PlantHealthDeatils details = new PlantHealthDeatils
                        {
                            longitude = entity.longitude,
                            ImageURL = entity.ImageURL,
                            latitude = entity.latitude,
                            Pesticidesprayed = entity.Pesticidesprayed,
                            CapturedTime = entity.CapturedTime,
                            RowKey = entity.RowKey,
                            ETag = entity.ETag,
                            PartitionKey = entity.PartitionKey
                        };
                        plantHealthDeatilsList.Add(details);
                    }
                } while (token != null);
                return plantHealthDeatilsList;
            }
            catch (Exception exp)
            {
                logger.LogError(exp, "Unable to GetPlantHealthDeatils");
                return default;
            }
        }

        private static async Task<HttpResponseMessage> UpdatePlantHealthDeatilsByRowkeyAsync(string rowkey, ILogger logger)
        {
            try
            {
                PlantHealthDeatils plantHealthDeatilsList = new PlantHealthDeatils();
                TableQuery<PlantHealthDeatils> query;
                query = new TableQuery<PlantHealthDeatils>().Where(TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, $"{rowkey}"));
                TableContinuationToken token = null;

                TableQuerySegment<PlantHealthDeatils> resultSegment = await table.ExecuteQuerySegmentedAsync(query, token).ConfigureAwait(false);

                var plantdetail = resultSegment.Results.FirstOrDefault();
                plantdetail.Pesticidesprayed = true;
                var operation = TableOperation.Replace(plantdetail);
                await table.ExecuteAsync(operation);

                return new HttpResponseMessage(HttpStatusCode.NoContent);
            }
            catch (Exception exp)
            {
                logger.LogError(exp, "Unable to Update PlantHealthDeatils");
                return default;
            }
        }

        private static string GetEnviromentValue(string key)
        {
            return Environment.GetEnvironmentVariable(key);
        }

        private static void SetClientIDAndSecret()
        {
            TokenHelper.clientID ??= GetEnviromentValue("clientID");
            TokenHelper.clientSecret ??= GetEnviromentValue("clientSecret");
        }
    }
}
</code>

**PlantHealthDeatils.cs**
<code>
using Microsoft.WindowsAzure.Storage.Table;
using System;

namespace GetPlantHealthDetails
{
    public class PlantHealthDeatils : TableEntity
    {
        public PlantHealthDeatils()
        {

        }
        public PlantHealthDeatils(string skey, string srow)
        {
            PartitionKey = skey;
            RowKey = srow;
        }
        public DateTime CapturedTime { get; set; }
        public string longitude { get; set; }
        public string latitude { get; set; }
        public string ImageURL { get; set; }
        public bool Pesticidesprayed { get; set; } = false;
     
    }
}
</code>

local.settings.json contais the information for the clientID,clientSecret,tableName,secretIdentifier , this needs to be added to Azure function from Portal

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivsbruzult4eriqtqdw2.png)

For publish please follow the above Function Publish steps to Azure.

**4. PlantHealthAppXam** :- **Xamarin Forms** based **Uwp , Android , Ios** app which will display information for the infected plants with images by querying the data to GetPlantHealthDetails ,as well as shows the Location on Map using the Longitude and Latitude **Azure Function Http Trigger**.

Use Add new Project(refer above screenshot) to solution and add Xamarin Forms Project


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/te6df5qm5rvwc6zyajxx.png)

Give correct Name and location to it then click on Next select below option

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w4cle3m2vcfuv7u02tv8.png)
Once Project is created you will see belows Projects added to solution 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enx1aoeomonr2gtl5kyd.png)

Goto 1 st Project from the above image in view folder we will be updating views as below.

Xamarin forms Project uses viewmodel and binding with MVVM , learn more from below link.

https://learn.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm

**ItemDetailPage.xaml**
<code>
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="PlantHealthAppXam.Views.ItemDetailPage"
             Title="{Binding Title}">

    <StackLayout Spacing="20" Padding="15">
        <Image Source="{Binding IMGURL}"></Image>
        <Button Text="Open Map" Command="{Binding OpenMapCommand}"></Button>
    </StackLayout>
</ContentPage>
</code>

**ItemDetailPage.xaml.cs**
<code>
using PlantHealthAppXam.ViewModels;
using System.ComponentModel;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xamarin.Forms;

namespace PlantHealthAppXam.Views
{
    public partial class ItemDetailPage : ContentPage
    {
        public ItemDetailPage()
        {
            InitializeComponent();
            BindingContext = new ItemDetailViewModel();
        }
    }
}
</code>

**ItemDetailViewModel.cs** in ViewModels folder
<code>
using PlantHealthAppXam.Models;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xamarin.Forms;

namespace PlantHealthAppXam.ViewModels
{
    [QueryProperty(nameof(IMGURL), nameof(IMGURL))]
    [QueryProperty(nameof(longitude), nameof(longitude))]
    [QueryProperty(nameof(latitude), nameof(latitude))]
    public class ItemDetailViewModel : BaseViewModel
    {
        private string imgurl;
        public Command OpenMapCommand { get; }
        public string longitude { get; set; }
        public string latitude { get; set; }

        public string Id { get; set; }

        public string IMGURL
        {
            get => imgurl;
            set => SetProperty(ref imgurl, value);
        }


        public ItemDetailViewModel()
        {
            OpenMapCommand = new Command(async () => await OpenMapByLongitudeLatitude(longitude,latitude));
        }

        public async Task OpenMapByLongitudeLatitude(string Longitude, string Latitude)
        {
            var location = new Location(Convert.ToDouble(Longitude), Convert.ToDouble(Latitude));
            await Map.OpenAsync(location);
        }
    }
}

</code>

**ItemsPage.xaml**
<code>
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="PlantHealthAppXam.Views.ItemsPage"
             Title="{Binding Title}"
             xmlns:local="clr-namespace:PlantHealthAppXam.ViewModels"  
             xmlns:model="clr-namespace:PlantHealthAppXam.Models"  
             x:Name="BrowseItemsPage">

    <ContentPage.ToolbarItems>
        <!--<ToolbarItem Text="Add" Command="{Binding AddItemCommand}" />-->
    </ContentPage.ToolbarItems>
    <!--
      x:DataType enables compiled bindings for better performance and compile time validation of binding expressions.
      https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/data-binding/compiled-bindings
    -->
    <RefreshView x:DataType="local:ItemsViewModel" Command="{Binding LoadItemsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
        <CollectionView x:Name="ItemsListView"
                ItemsSource="{Binding ItemsList}"
                SelectionMode="None">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <StackLayout Padding="10" Orientation="Horizontal" x:DataType="model:PlantHealthDeatils">
                        <StackLayout Orientation="Vertical">
                            <StackLayout Orientation="Horizontal">
                                <Label FontAttributes="Bold" Text="Longitude :"></Label>
                                <Label Text="{Binding longitude}" 
                            LineBreakMode="NoWrap" 
                            Style="{DynamicResource ListItemDetailTextStyle}" 
                            FontSize="13" />
                            </StackLayout>
                            <StackLayout Orientation="Horizontal">
                                <Label FontAttributes="Bold" Text="Latitude :"></Label>
                                <Label Text="{Binding latitude}" 
                            LineBreakMode="WordWrap" 
                            Style="{DynamicResource ListItemDetailTextStyle}"
                            FontSize="13" />
                            </StackLayout>
                            <StackLayout Orientation="Horizontal">
                                <Label FontAttributes="Bold" Text="Captured Time :"></Label>
                                <Label Text="{Binding CapturedTime}" 
                            LineBreakMode="WordWrap" 
                            Style="{DynamicResource ListItemDetailTextStyle}"
                            FontSize="13" />
                            </StackLayout>
                            <StackLayout Orientation="Horizontal">
                                <Label  FontAttributes="Bold" Text="Pesticide Sprayed :" Margin="0,5,0,0"></Label>
                                <CheckBox IsEnabled="False" IsChecked="{Binding Pesticidesprayed}" ></CheckBox>
                            </StackLayout>
                        </StackLayout>
                        <Image Source="{Binding ImageURL}"  HorizontalOptions="EndAndExpand" HeightRequest="100" WidthRequest="100"></Image>
                        <StackLayout.GestureRecognizers>
                            <TapGestureRecognizer 
                                NumberOfTapsRequired="1"
                                Command="{Binding Source={RelativeSource AncestorType={x:Type local:ItemsViewModel}}, Path=ItemTapped}"		
                                CommandParameter="{Binding .}">
                            </TapGestureRecognizer>
                        </StackLayout.GestureRecognizers>
                    </StackLayout>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </RefreshView>
</ContentPage>
</code>

**ItemsViewModel.cs** in ViewModels
<code>
using Newtonsoft.Json;
using PlantHealthAppXam.Models;
using PlantHealthAppXam.Views;
using RestSharp;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace PlantHealthAppXam.ViewModels
{
    public class ItemsViewModel : BaseViewModel
    {
        private PlantHealthDeatils _selectedItem;

        public ObservableCollection<PlantHealthDeatils> ItemsList { get; }
        public Command LoadItemsCommand { get; }
        public Command<PlantHealthDeatils> ItemTapped { get; }

        public ItemsViewModel()
        {
            Title = "Plant List";
            ItemsList = new ObservableCollection<PlantHealthDeatils>();
            LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());

            ItemTapped = new Command<PlantHealthDeatils>(OnItemSelected);
        }

        async Task ExecuteLoadItemsCommand()
        {
            IsBusy = true;

            try
            {
                ItemsList.Clear();
                var items = await GetDataAsync().ConfigureAwait(false);
                foreach (var item in items)
                {
                    ItemsList.Add(item);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }

        public void OnAppearing()
        {
            IsBusy = true;
            SelectedItem = null;
        }

        public PlantHealthDeatils SelectedItem
        {
            get => _selectedItem;
            set
            {
                SetProperty(ref _selectedItem, value);
                OnItemSelected(value);
            }
        }

        async void OnItemSelected(PlantHealthDeatils item)
        {
            if (item == null)
                return;

            // This will push the ItemDetailPage onto the navigation stack
            //Shell.Current.GoToAsync($"//home/bottomtab2?name={"Cat"}&test={"Dog"}");
            await Shell.Current.GoToAsync($"{nameof(ItemDetailPage)}?{nameof(ItemDetailViewModel.IMGURL)}={item.ImageURL}&{nameof(ItemDetailViewModel.longitude)}={item.longitude}&{nameof(ItemDetailViewModel.latitude)}={item.latitude}");
        }

        public async Task<List<PlantHealthDeatils>> GetDataAsync()
        {
            var client = new RestClient("https://getplanthealthdetails.azurewebsites.net/api/GetPlantHealth?code=Ffcqj7PbO68QaTg2zWRNN7yp76kyYXNr8YBC_qw-jUXSAzFuAIrvKw==");
            var request = new RestRequest();
            request.Method = Method.Get;
            var response = await client.ExecuteAsync(request);
            return JsonConvert.DeserializeObject<List<PlantHealthDeatils>>(response.Content.ToString());
        }

    }
}
</code>

**ItemsPage.xaml.cs**
<code>
using PlantHealthAppXam.Models;
using PlantHealthAppXam.ViewModels;
using PlantHealthAppXam.Views;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace PlantHealthAppXam.Views
{
    public partial class ItemsPage : ContentPage
    {
        ItemsViewModel _viewModel;

        public ItemsPage()
        {
            InitializeComponent();

            BindingContext = _viewModel = new ItemsViewModel();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            _viewModel.OnAppearing();
        }
    }
}
</code>

**PlantHealthDeatils.cs**
<code>
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;

namespace PlantHealthAppXam.Models
{
    public class PlantHealthDeatils 
    {
        public PlantHealthDeatils()
        {

        }
        
        public DateTime CapturedTime { get; set; }

        public string longitude { get; set; }

        public string latitude { get; set; }
        public string ImageURL { get; set; }
        public bool Pesticidesprayed { get; set; } = false;
        public string ETag { get; set; }
        public string RowKey { get; set; }
        public string PartitionKey { get; set; }

    }
}
</code>

App running on Windows 11, Android, Ios

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9t324nv6j96lvlu0lco5.png)


**- Challenges in implementing the solution**

Raspbian (Linux distro) is case sensitive for file naming, I have used camel casing in file name AppSettings.json on windows it worked fine but on Linux it was null values so after debugging the app on Linux I came to know about it and used same casing later in file name to fix it.

**- Business Benefit**

Human efforts can be saved with the Azure based solution which will reduce the human efforts and increase revenue generation for farmers as farm workers can focus on another task. Using agriculture drones using longitude and latitude we can spray pesticide to selected area.