Plant Health Assessment Using Raspberry Pi, Xamarin Forms App, Azure Functions, Table, Storage, Key Vault, Custom Vision, Drone

– Problem Description

Maintaining plant health over a large area of ​​farm is really challenging, farmers need to monitor the plants they grow even if it is not affected by insects or diseases, for this manual inspection of plants by many farm workers and Requires navigating the entire area. Field field and inspect them manually which is time consuming for many days for farm workers and it needs to be repeated from time to time which is very time consuming.

– Solutions/Architecture

This problem can be solved using an Azure based solution where the drone can capture images of the elephant ear plant and identify if the plant is affected by pests or diseases, using an Azure storage account that the drone Will keep the image captured by in which images of healthy, and pest or disease affected plants have been trained
busy farmer

Please refer application flow

picture description

We will need the 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 requires the following workload.

picture description

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

picture description

picture description
Click Create Project. Upload pictures of healthy and infected plants and tag the uploaded images and do quick training.

picture description

Goto Performance tab follow the steps and copy the URL and key that will be used to access it.

picture description

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

Please find the github link for the project, you need to enter the correct Azure resource (eg storage, tables, custom vision end points, ftp) values ​​to run the project.

https://github.com/jaymin93/PlantHealthApp

-Technical details and implementation of the solution
busy farmer

1. PlantHealthConsoleApp , .net 6 console app

The app will monitor the drone created FTP containing the images captured by the drone from the farm, the app will periodically check the FTP server and the images will be copied to the destination directory from where it will be uploaded Azure Storage as a Blob

start visual studio 2022 follow below steps

picture description

picture description

Please provide the name and path to the project.
picture description

to select .NET 6 LTS (LTS is highly recommended for production workloads) Click on Build This will create the console project.
picture description

Once the project is created, we will need the following Nuget package which can be linked with the below option:

picture description

picture description

It will connect to the Ftp server created by Drone and get the value from appsettings.json For example blob storage uri, ftp information and secret identifier

To visit https://portal.azure.com/ we will need a storage account and Azure keyvault secret information.

Create new storage account

picture description

picture description

Enter a resource group name or select Existing and add a storage account name

Select + Create Review.

Go to the storage account you have created and select the Access Key tab as in the image below

picture description

create container
picture description

key safe

picture description

Please enter correct resource group name and keyvault name and then click Review + Create

Navigate to KeyVault and select Secret and then click on Generate Import
picture description

picture description

picture description

Please install nuget from above menu by searching with name.

Azure.Storage.Blobs
Microsoft.Azure.KeyVault
Microsoft.Azure.KeyVault.Models
microsoft.extension.configuration
microsoft.extension.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 it from solution explorer.
picture description

This class will be used to get the AccessToken to connect to the KeyVault and get the secret value 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 execute on rasbbian linux arm64 so same configuration will be selected to publish it.

picture description

From the option choose Target Runtime Linux-arm64 and Deployment as Self Contained, learn more here https://learn.microsoft.com/en-us/dotnet/core/deploying/
picture description

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

2. PlantHealthApp , Azure Function Blob Trigger with .net 6 as the target framework.

The function will be triggered once there is a new image uploaded by the console app, which will be sent to Azure Custom Vision For prediction if the plant is affected by the pest or dies, the details of the affected plant will be stored Azure Table

picture description

Azure Select Functions
picture description

picture description

picture description

picture description

picture description

Provide the connection string name and click Finish.
picture description

Please install the 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

picture description

PlantHealthDeatils.cs This class will be inherited by TableEntity class which will be used to communicate as model class for Azure table, please note that parameterized 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

We will create an Azure function for publishing to Azure

Click Review + Create
picture description

Once Azure Function is created, we can deploy it in Azure

select below from visual studio

picture description

picture description

picture description

picture description

local.settings.json contains key value pair for storge account url, custom vision and table, need to be added to Azure Functions from portal
picture description

Once the publication is successful, we can upload the image to the container and the blob trigger will be executed, the details of the affected plants will be stored in the Azure table

3. GetPlantHealthDetails , Azure Function Http Trigger with .net 6 as the target framework.

The function will retrieve the affected plant’s data from the Azure table and respond Xamarin Form based app running Windows, Android, iOS

Roukey based query string can be sent to Azure function which will update pesticide spray status flag of particular record which can be done by agriculture drone spraying insecticide.

Add new project of Azure Function (please see above screen shot), we will add Http Trigger Azure Function trigger type.

picture description

Please install the 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 contains the information of ClientID,clientSecret,tableName,secretIdentifier, it needs to be added to Azure Functions from Portal

picture description

Please follow above function to publish publish steps to Azure.

4. PlantHealthAppXam , Xamarin Form based UWP, Android, iOS App that will display information for infected plants with images by querying data from GetPlantHealthDetails, as well as showing locations on a map using longitude and latitude. Azure Function Http Trigger,

Use Add New Project to Solution (see screenshot above) and Add Xamarin Forms Project

picture description

Give it the correct name and location then click on the next select option given below

picture description
Once the project is created you will see below Projects added to solution

picture description

Goto 1st project from above image in views folder we will update views as below.

The Xamarin Forms project uses ViewModel and Binding with MVVM, learn more from the link below.

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 viewmodel


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

picture description

Challenges in implementing the solution

Raspbian (Linux Distro) is sensitive to file naming, I have used camel wrapper in AppSettings.json filename on Windows it works fine but on Linux it was null value so after debugging the app on Linux I have Found out about it and used the same wrapper in the filename to fix it later.

– business profit

The Azure based solution can save human effort which will reduce the human effort and increase the revenue generation for the farmers as the farm workers can focus on other work. By using agricultural drone using longitude and latitude we can spray insecticide in selected area.

Leave a Comment