DEV Community

jaymin93
jaymin93

Posted on

Plant Health evaluation using Raspberry Pi, Xamarin Forms App,Azure Function, Table,Storage, Key vault, Custom vision, Drone

- 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.

busy farmer

- 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
busy farmer

Please refer Application flow

Image description

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

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

Image description

Image description
Click on create Project. Upload healthy and infected plant images and tag the uploaded images and perform the quick training.

Image description

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

Image description

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
busy farmer

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

Image description

Please provide the Name and Path for the Project.
Image description

Select .net 6 LTS (For Production workloads LTS is highly recommended) Click on Create it will create Console Project.
Image description

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

Image description

Image description

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

Image description

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

Create Container
Image description

KeyVault

Image description

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

Image description

Image description

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


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

Enter fullscreen mode Exit fullscreen mode

Add new class by selecting from Solution explorer.
Image description

This class will be used to Get AccessToken to connect to keyvault and Get secret values from it.

ToeknHelper.cs


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

Enter fullscreen mode Exit fullscreen mode

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

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

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

Image description

Image description

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

Select Azure Function
Image description

Image description

Image description

Image description

Image description

Provide Connection string name and click on finish.
Image description

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


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


Enter fullscreen mode Exit fullscreen mode

Add new class file as below

Image description

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


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;


    }
}


Enter fullscreen mode Exit fullscreen mode

For Publishing to the Azure we will be creating Azure function

Click review + create
Image description

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

From Visual Studio select below

Image description

Image description

Image description

Image description

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

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

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


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

Enter fullscreen mode Exit fullscreen mode

PlantHealthDeatils.cs


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;

    }
}

Enter fullscreen mode Exit fullscreen mode

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

Image description

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

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

Image description
Once Project is created you will see belows Projects added to solution

Image description

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


<?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>

Enter fullscreen mode Exit fullscreen mode

ItemDetailPage.xaml.cs


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

Enter fullscreen mode Exit fullscreen mode

ItemDetailViewModel.cs in ViewModels folder


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


Enter fullscreen mode Exit fullscreen mode

ItemsPage.xaml


<?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>

Enter fullscreen mode Exit fullscreen mode

ItemsViewModel.cs in ViewModels


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

    }
}

Enter fullscreen mode Exit fullscreen mode

ItemsPage.xaml.cs


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

Enter fullscreen mode Exit fullscreen mode

PlantHealthDeatils.cs


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

    }
}

Enter fullscreen mode Exit fullscreen mode

App running on Windows 11, Android, Ios

Image description

- 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.

Top comments (0)