OBD2 Bluetooth dongle plugged into car's OBD2 port
OBD2 Bluetooth dongle plugged into car's OBD2 port

Decoding Your Car: A Guide to OBD2 and Real-Time Vehicle Data with Xamarin Forms

In today’s connected world, even our vehicles are becoming increasingly communicative. The ability to access real-time data from your car opens up a realm of possibilities for diagnostics, performance monitoring, and even custom application development. This article delves into the fascinating world of OBD2 (On-Board Diagnostics II) and demonstrates how you can tap into your car’s data stream using Xamarin Forms and Bluetooth Low Energy (BLE) technology.

We’ll walk through the creation of a demo mobile application that reads essential vehicle parameters like speed, engine RPM (revolutions per minute), and engine temperature. This project leverages the power of Xamarin Forms for cross-platform mobile development, the standardized OBD2 system for vehicle communication, and Bluetooth for wireless connectivity.

To give you a glimpse of what’s achievable, here’s a preview of the application in action, displaying real-time vehicle data:

[Imagine a screenshot or GIF here showcasing the demo app displaying speed, RPM, and temperature gauges.]

Ready to get started? Building an application like this requires a foundational understanding of a few key areas:

  • Xamarin Forms: A cross-platform UI framework for building native mobile apps with C#.
  • OBD2 (On-Board Diagnostics II): The standardized system vehicles use for diagnostics and reporting.
  • Bluetooth Communication: Wireless technology for connecting your mobile device to an OBD2 Bluetooth dongle in your car.

For this practical demonstration, we utilized an OBD2 Bluetooth dongle. This device plugs directly into your car’s OBD2 port, typically located beneath the steering wheel. Here’s a look at the dongle in action:

While we won’t delve deeply into the intricacies of BLE dongle communication in this article, you can find a comprehensive guide in a dedicated blog post: https://www.jenx.si/2020/08/13/bluetooth-low-energy-uart-service-with-xamarin-forms/. This resource provides detailed information on establishing BLE communication with Xamarin Forms.

Diving into the Code: Building the OBD2 App

Let’s get our hands dirty with some code. We’ll be using Visual Studio 2019 to create a new Xamarin Forms project, starting with the Shell template. To enhance our development process and application features, we’ll incorporate several essential NuGet packages:

  • Prism.Forms & Prism.Unity.Forms: Powerful frameworks for building loosely coupled, maintainable, and testable Xamarin Forms applications, leveraging dependency injection and MVVM (Model-View-ViewModel).
  • Xamarin.Essentials: Provides essential cross-platform APIs for accessing device functionalities, simplifying common tasks in mobile development.
  • Syncfusion.Xamarin.SfGauge (Trial): A UI library offering feature-rich gauge controls for visually representing speed and RPM data. (A trial version is sufficient for development and evaluation).
  • Ble.Plugin: A crucial component for handling Bluetooth Low Energy communication, enabling interaction with the OBD2 Bluetooth dongle.

Here’s a glimpse into the Visual Studio Solution Explorer once these components are integrated:

Now, let’s examine the core structure of our application, starting with App.xaml.cs and AppShell.xaml. These files define the fundamental setup and navigation of our Xamarin Forms Shell application.

My App and AppShell Structure

This App.xaml.cs file is relatively standard for a Prism-based Xamarin Forms application. The key takeaway is that our application inherits from PrismApplication. This inheritance enables us to leverage the powerful Inversion of Control (IoC) container provided by Prism.Unity, promoting modularity and maintainability within our Xamarin Forms application.

Next, let’s look at the AppShell.xaml which defines the visual structure and navigation of our application using Xamarin Forms Shell:

<?xml version="1.0" encoding="UTF-8"?>
<Shell
    x:Class="Jenx.Odb2.SpeedGauge.AppShell"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:local="clr-namespace:Jenx.Odb2.SpeedGauge.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="Jenx.Odb2.SpeedGauge"
    mc:Ignorable="d">

    <Shell.Resources>
        <ResourceDictionary>
             <Color x:Key="NavigationPrimary">#2196F3</Color>
             <Style x:Key="BaseStyle" TargetType="Element">
                <Setter Property="Shell.BackgroundColor" Value="{StaticResource NavigationPrimary}" />
                <Setter Property="Shell.ForegroundColor" Value="White" />
                <Setter Property="Shell.TitleColor" Value="White" />
                <Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
                <Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
                <Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource NavigationPrimary}" />
                <Setter Property="Shell.TabBarForegroundColor" Value="White"/>
                <Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
                <Setter Property="Shell.TabBarTitleColor" Value="White"/>
             </Style>
             <Style BasedOn="{StaticResource BaseStyle}" TargetType="TabBar" />
        </ResourceDictionary>
    </Shell.Resources>

    <TabBar Route="main-shell">
        <Tab
            Title="Connect"
            Icon="tab_settings.png"
            Route="connect-page">
            <ShellContent ContentTemplate="{DataTemplate local:ConnectPage}" />
        </Tab>
        <Tab
            Title="Kmph"
            Icon="tab_speed.png"
            Route="kmph-page">
            <ShellContent ContentTemplate="{DataTemplate local:KmphPage}" />
        </Tab>
        <Tab
            Title="Rpm"
            Icon="tab_rpm.png"
            Route="rpm-page">
            <ShellContent ContentTemplate="{DataTemplate local:RpmPage}" />
        </Tab>
        <Tab
            Title="Car info"
            Icon="tab_info.png"
            Route="info-page">
            <ShellContent ContentTemplate="{DataTemplate local:InfoPage}" />
        </Tab>
    </TabBar>
</Shell>

This AppShell.xaml defines the tab-based navigation structure of our application. We’ve defined four tabs: “Connect,” “Kmph,” “Rpm,” and “Car info,” each associated with a specific page within the application.

In contrast to previous demonstrations, this application embraces the MVVM pattern, service Dependency Injection, and other modern development practices. This approach leads to a more modular, organized, and easily extensible application architecture.

Let’s now delve into the implementation of the basic application pages, starting with the “Connect” page.

Connect Page: Establishing OBD2 Connection

The “Connect” page is the entry point for establishing communication with the OBD2 Bluetooth dongle.

View (ConnectPage.xaml):

<?xml version="1.0" encoding="utf-8"?>
<gui:BaseContentPage
    x:Class="Jenx.Odb2.SpeedGauge.Views.ConnectPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:gui="clr-namespace:Jenx.Odb2.SpeedGauge.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
    Title="{Binding Title}"
    mvvm:ViewModelLocator.AutowireViewModel="True"
    mc:Ignorable="d">
    <Grid>
        <ScrollView>
            <StackLayout
                Padding="20,20,20,20"
                Orientation="Vertical"
                Spacing="5">
                <Button
                    Margin="0,10,0,0"
                    Command="{Binding ConnectToObd2DongleCommand}"
                    Text="Connect to my car" />
                <Label LineBreakMode="WordWrap" Text="{Binding Output}" />
            </StackLayout>
        </ScrollView>
    </Grid>
</gui:BaseContentPage>

ViewModel (ConnectPageViewModel.cs):

using Jenx.Obd2.Bluetooth;
using Jenx.Obd2.Common;
using Jenx.Odb2.SpeedGauge.Services;
using Jenx.Odb2.SpeedGauge.Utils;
using Prism.Services;
using System;
using System.Windows.Input;
using Xamarin.Forms;

namespace Jenx.Odb2.SpeedGauge.ViewModels
{    public class ConnectPageViewModel : BaseViewModel
    {
        private readonly IObd2BluetoothManager _obd2BluetoothManager;
        private readonly INavigationService _navigationService;
        private readonly IUartManager _uartManager;
        private readonly IPageDialogService _pageDialogService;

        public ConnectPageViewModel(
            IObd2BluetoothManager obd2BluetoothManager,
            INavigationService navigationService,
            IUartManager uartManager,
            IPageDialogService pageDialogService)
        {
            _obd2BluetoothManager = obd2BluetoothManager;
            _navigationService = navigationService;
            _uartManager = uartManager;
            _pageDialogService = pageDialogService;

            Title = "Connect to car";
        }

        public ICommand ConnectToObd2DongleCommand => new Command(async () =>
        {
            try
            {
                var hasPermission = await SecurityPermissions.CheckAndRequestLocationPermission();
                if (!hasPermission)
                {
                    await _pageDialogService.DisplayAlertAsync("Permission error",
                        "Location permission must be allowed in order to work with Bluetooth.", "OK");
                    return;
                }

                await _obd2BluetoothManager.StartScanForObd2DongleAsync();
                await _obd2BluetoothManager.ConnectToObd2DeviceAsync();

                if (_obd2BluetoothManager.Obd2BluetoothDongle.State != Plugin.BLE.Abstractions.DeviceState.Connected)
                {
                    await _pageDialogService.DisplayAlertAsync("Error", "Connection to BLE dongle failed.", "OK");
                    return;
                }

                var uartInitializationResult = await _uartManager.InitializeAsync();

                if (!uartInitializationResult)
                {
                    await _pageDialogService.DisplayAlertAsync("Error", "Error connecting to bluetooth dongle.", "OK");
                    return;
                }

                var initialCommandResult = await InitialConnectionFacade.ExecuteInitialCommandAsync(_uartManager);
                if (!initialCommandResult.Key)
                {
                    await _pageDialogService.DisplayAlertAsync("Error", "Error connecting to bluetooth dongle.", "OK");
                    return;
                }

                Output += await _uartManager.GetObdDataAsync<string>("010C") + Environment.NewLine;
                Output += await _uartManager.GetObdDataAsync<string>("010D") + Environment.NewLine;

                await _navigationService.NavigateToRouteAsync("//main-shell/rpm-page");
            }
            catch (Exception ex)
            {
                await _pageDialogService.DisplayAlertAsync("Error", ex.Message, "OK");
            }
            finally
            {
                IsBusy = false;
            }
        });

        private string output;
        public string Output
        {
            get { return output; }
            set
            {
                SetProperty(ref output, value);
            }
        }
    }
}

Visual Appearance:

The “Connect” page provides a button that, when pressed, triggers the ConnectToObd2DongleCommand in the ViewModel. This command handles the Bluetooth connection process, including requesting location permissions (necessary for Bluetooth scanning on some platforms), scanning for OBD2 dongles, connecting to the selected dongle, initializing the UART communication, and sending initial OBD2 commands. Upon successful connection, it navigates the user to the “RPM” page.

Kmph Page: Displaying Vehicle Speed

The “Kmph” page focuses on displaying the vehicle’s speed in kilometers per hour (km/h).

View (KmphPage.xaml):

<?xml version="1.0" encoding="utf-8"?>
<gui:BaseContentPage
    x:Class="Jenx.Odb2.SpeedGauge.Views.KmphPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://schemas.microsoft.com/winfx/2014/forms/design"
    xmlns:gauge="clr-namespace:Syncfusion.SfGauge.XForms;assembly=Syncfusion.SfGauge.XForms"
    xmlns:gui="clr-namespace:Jenx.Odb2.SpeedGauge.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
    Title="{Binding Title}"
    mvvm:ViewModelLocator.AutowireViewModel="True"
    mc:Ignorable="d">
    <Grid>
        <ScrollView>
            <StackLayout
                Padding="20,40,20,40"
                HorizontalOptions="CenterAndExpand"
                Orientation="Vertical"
                Spacing="10"
                VerticalOptions="CenterAndExpand">
                <StackLayout
                    BackgroundColor="White"
                    HorizontalOptions="Center"
                    VerticalOptions="Center">
                    <gauge:SfDigitalGauge
                        CharacterHeight="200"
                        CharacterStrokeColor="Blue"
                        CharacterType="SegmentSeven"
                        CharacterWidth="80"
                        DisabledSegmentAlpha="15"
                        DisabledSegmentColor="#146CED"
                        HeightRequest="300"
                        HorizontalOptions="End"
                        SegmentStrokeWidth="12"
                        VerticalOptions="FillAndExpand"
                        WidthRequest="300"
                        Value="{Binding Kmh, StringFormat='{0,3:##0}', Mode=TwoWay}" />
                    <Label
                        FontAttributes="Bold"
                        FontSize="Large"
                        HorizontalOptions="Center"
                        Text="km/h"
                        TextColor="Black" />
                </StackLayout>
            </StackLayout>
        </ScrollView>
    </Grid>
</gui:BaseContentPage>

ViewModel (KmphPageViewModel.cs):

using Jenx.Obd2.Common;
using System;
using System.Globalization;
using System.Threading.Tasks;

namespace Jenx.Odb2.SpeedGauge.ViewModels
{
    public class KmphPageViewModel : BaseViewModel
    {
        private bool _timerRunning = true;
        private readonly IUartManager _uartManager;

        public KmphPageViewModel(IUartManager uartManager)
        {
            Title = "Car speed";
            _uartManager = uartManager;
        }

        private int kmh;
        public int Kmh
        {
            get { return kmh; }
            set
            {
                SetProperty(ref kmh, value);
            }
        }

        public override void OnActivated()
        {
            _timerRunning = true;
        }

        public override void OnDeactivated()
        {
            _timerRunning = false;
        }

        public override async void OnInitialization()
        {
            while (true)
            {
                await Task.Delay(300);

                if (!_timerRunning) continue;

                try
                {
                    if (_uartManager.IsExecuting) return;

                    var odb2SpeedData = await _uartManager.GetObdDataAsync<string>("010D");
                    if (odb2SpeedData == null) return; // response depends on OBD2 protocol of the car

                    var aParameter = odb2SpeedData.Substring(9, 2);
                    int aParameterDecValue = int.Parse(aParameter, NumberStyles.HexNumber);

                    Kmh = Convert.ToInt32(aParameterDecValue);
                }
                catch
                {
                }
            }
        }
    }
}

Visual Appearance:

The “Kmph” page utilizes a SfDigitalGauge from the Syncfusion library to visually represent the speed. The ViewModel continuously fetches speed data from the OBD2 interface using the command “010D” (which corresponds to vehicle speed PID in OBD2). The received hexadecimal data is parsed and converted to an integer representing km/h, which is then bound to the Value property of the digital gauge.

RPM Page: Displaying Engine Revolutions

The “RPM” page displays the engine’s revolutions per minute, giving a real-time view of engine speed.

View (RpmPage.xaml):

<?xml version="1.0" encoding="utf-8"?>
<gui:BaseContentPage
    x:Class="Jenx.Odb2.SpeedGauge.Views.RpmPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://schemas.microsoft.com/winfx/2014/forms/design"
    xmlns:gauge="clr-namespace:Syncfusion.SfGauge.XForms;assembly=Syncfusion.SfGauge.XForms"
    xmlns:gui="clr-namespace:Jenx.Odb2.SpeedGauge.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
    Title="{Binding Title}"
    mvvm:ViewModelLocator.AutowireViewModel="True"
    mc:Ignorable="d">
    <Grid>
        <ScrollView>
            <StackLayout Padding="0,20,0,20" Orientation="Vertical">
                <gauge:SfCircularGauge
                    x:Name="circularGauge"
                    HorizontalOptions="FillAndExpand"
                    VerticalOptions="FillAndExpand">
                    <gauge:SfCircularGauge.Scales>
                        <gauge:Scale
                            x:Name="scale"
                            EndValue="6000"
                            Interval="1000"
                            LabelColor="Red"
                            LabelFontSize="20"
                            LabelOffset="0.55"
                            MinorTicksPerInterval="10"
                            RimThickness="1"
                            ScaleEndOffset="0.80"
                            ScaleStartOffset="0.90"
                            ShowTicks="True"
                            StartValue="0">
                            <gauge:Scale.MinorTickSettings>
                                <gauge:TickSettings
                                    EndOffset="0.75"
                                    StartOffset="0.7"
                                    Thickness="1"
                                    Color="#C62E0A" />
                            </gauge:Scale.MinorTickSettings>
                            <gauge:Scale.MajorTickSettings>
                                <gauge:TickSettings
                                    EndOffset="0.77"
                                    StartOffset="0.7"
                                    Thickness="3"
                                    Color="Blue" />
                            </gauge:Scale.MajorTickSettings>
                            <gauge:Scale.Ranges>
                                <gauge:Range
                                    EndValue="6000"
                                    StartValue="0"
                                    Thickness="15"
                                    Offset="0.90">
                                    <gauge:Range.GradientStops>
                                        <gauge:GaugeGradientStop Color="#30B32D" Value="0" />
                                        <gauge:GaugeGradientStop Color="#FFDD00" Value="2500" />
                                        <gauge:GaugeGradientStop Color="#F03E3E" Value="5000" />
                                    </gauge:Range.GradientStops>
                                </gauge:Range>
                            </gauge:Scale.Ranges>
                            <gauge:Scale.Pointers>
                                <gauge:NeedlePointer
                                    x:Name="needlePointer"
                                    KnobRadius="15"
                                    LengthFactor="1"
                                    Type="Triangle"
                                    Value="{Binding Rpm, Mode=TwoWay}" />
                            </gauge:Scale.Pointers>
                        </gauge:Scale>
                    </gauge:SfCircularGauge.Scales>
                </gauge:SfCircularGauge>
                <Label
                    FontAttributes="Bold"
                    FontSize="Large"
                    HorizontalOptions="Center"
                    Text="RPM"
                    TextColor="Black"
                    VerticalOptions="Center" />
            </StackLayout>
        </ScrollView>
    </Grid>
</gui:BaseContentPage>

ViewModel (RpmPageViewModel.cs):

using Jenx.Obd2.Common;
using System;
using System.Threading.Tasks;

namespace Jenx.Odb2.SpeedGauge.ViewModels
{
    public class RpmPageViewModel : BaseViewModel
    {
        private bool _timerRunning = true;
        private readonly IUartManager _uartManager;

        public RpmPageViewModel(IUartManager uartManager)
        {
            Title = "Engine RPM";
            _uartManager = uartManager;
        }

        private int rpm;
        public int Rpm
        {
            get { return rpm; }
            set
            {
                SetProperty(ref rpm, value);
            }
        }

        public override void OnActivated()
        {
            _timerRunning = true;
        }

        public override void OnDeactivated()
        {
            _timerRunning = false;
        }

        public override async void OnInitialization()
        {
            while (true)
            {
                await Task.Delay(300);

                if (!_timerRunning) continue;

                try
                {
                    if (_uartManager.IsExecuting) return;

                    var odb2RpmData = await _uartManager.GetObdDataAsync<string>("010C");

                    if (odb2RpmData == null) return;

                    var aParameter = odb2RpmData.Substring(9, 2);
                    var bParameter = odb2RpmData.Substring(11, 2);

                    int aParameterDecValue = int.Parse(aParameter, System.Globalization.NumberStyles.HexNumber);
                    int bParameterDecValue = int.Parse(bParameter, System.Globalization.NumberStyles.HexNumber);

                    Rpm = Convert.ToInt32((aParameterDecValue * 256 + bParameterDecValue) / 4);
                }
                catch
                {
                }
            }
        }
    }
}

Visual Appearance:

The “RPM” page uses a SfCircularGauge to visualize engine speed. The ViewModel retrieves RPM data using the OBD2 command “010C” (engine RPM PID). RPM data in OBD2 is typically returned as two bytes, so the code extracts two parameters (aParameter and bParameter), converts them from hexadecimal to decimal, and then combines them using a specific formula to calculate the RPM value. This calculated RPM value is then bound to the Value property of the circular gauge’s needle pointer.

Info Page: Displaying Additional Car Status

The “Info” page expands beyond speed and RPM to display other useful car parameters, in this case, engine coolant temperature and engine oil temperature.

View (InfoPage.xaml):

<?xml version="1.0" encoding="utf-8"?>
<gui:BaseContentPage
    x:Class="Jenx.Odb2.SpeedGauge.Views.InfoPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://schemas.microsoft.com/winfx/2014/forms/design"
    xmlns:gui="clr-namespace:Jenx.Odb2.SpeedGauge.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
    Title="{Binding Title}"
    mvvm:ViewModelLocator.AutowireViewModel="True"
    mc:Ignorable="d">
    <ScrollView>
        <StackLayout
            Padding="20,40,20,40"
            Orientation="Vertical"
            Spacing="10">
            <Label FontSize="Medium">
                <Label.FormattedText>
                    <FormattedString>
                        <Span Text="Engine coolant temperature: " />
                        <Span ForegroundColor="Red" Text="{Binding Temperature}" />
                        <Span Text=" ºC" />
                    </FormattedString>
                </Label.FormattedText>
            </Label>
            <Label FontSize="Medium">
                <Label.FormattedText>
                    <FormattedString>
                        <Span Text="Engine oil temperature: " />
                        <Span ForegroundColor="Red" Text="{Binding OilTemperature}" />
                        <Span Text=" ºC" />
                    </FormattedString>
                </Label.FormattedText>
            </Label>
        </StackLayout>
    </ScrollView>
</gui:BaseContentPage>

ViewModel (InfoPageViewModel.cs):

using Jenx.Obd2.Common;
using Prism.Services;
using System;
using System.Globalization;
using System.Threading.Tasks;

namespace Jenx.Odb2.SpeedGauge.ViewModels
{
    public class InfoPageViewModel : BaseViewModel
    {
        private bool _timerRunning = true;
        private readonly IUartManager _uartManager;
        private readonly IPageDialogService _pageDialogService;

        public InfoPageViewModel(IUartManager uartManager, IPageDialogService pageDialogService)
        {
            _uartManager = uartManager;
            _pageDialogService = pageDialogService;
            Title = "Car status";
        }

        public override void OnActivated()
        {
            _timerRunning = true;
        }

        public override void OnDeactivated()
        {
            _timerRunning = false;
        }

        public override async void OnInitialization()
        {
            while (true)
            {
                await Task.Delay(5000); // pull data every 5 secs - these data info do not fefresh so often...

                if (!_timerRunning) continue;

                try
                {
                    if (_uartManager.IsExecuting) return;

                    await PullObd2DataAsync();
                }
                catch
                {
                }
            }
        }

        private async Task PullObd2DataAsync()
        {
            try
            {
                var odb2TemperatureData = await _uartManager.GetObdDataAsync<string>("0105");
                if (odb2TemperatureData != null)
                {
                    var aParameter = odb2TemperatureData.Substring(9, 2);
                    int aParameterDec = int.Parse(aParameter, NumberStyles.HexNumber);
                    Temperature = Convert.ToInt32(aParameterDec - 40);
                }
            }
            catch
            {
                Temperature = null;
            }

            try
            {
                var odb2OilTemperatureData = await _uartManager.GetObdDataAsync<string>("015C");
                if (odb2OilTemperatureData != null)
                {
                    var aParameter = odb2OilTemperatureData.Substring(9, 2);
                    int aParameterDec = int.Parse(aParameter, NumberStyles.HexNumber);
                    OilTemperature = Convert.ToInt32(aParameterDec - 40);
                }
            }
            catch
            {
                OilTemperature = null;
            }
        }

        private int? temperature;
        public int? Temperature
        {
            get { return temperature; }
            set
            {
                SetProperty(ref temperature, value);
            }
        }

        private int? oilTemperature;
        public int? OilTemperature
        {
            get { return oilTemperature; }
            set
            {
                SetProperty(ref oilTemperature, value);
            }
        }
    }
}

Visual Appearance:

The “Info” page displays engine coolant and oil temperatures using simple labels. The ViewModel periodically (every 5 seconds) fetches this data using OBD2 commands “0105” (engine coolant temperature) and “015C” (engine oil temperature). Similar to the speed reading, the temperature data is received in hexadecimal format, parsed, converted to Celsius, and then bound to the Temperature and OilTemperature properties for display.

The code provided for these pages clearly demonstrates the process of reading OBD2 data via a Bluetooth dongle, parsing the responses, and displaying the information within a Xamarin Forms application.

Navigation Service and ViewModel Lifecycle Management

Custom Navigation Service

Prism and Unity are valuable toolkits frequently used to enhance application development through Dependency Injection, Service Locator patterns, event aggregation, navigation, and modularity. However, at the time of writing, Prism’s navigation service wasn’t fully compatible with Xamarin Forms Shell. Specifically, navigation functionality with XF Shell was not working as expected.

To address this, a custom INavigationService was implemented as a quick workaround to enable navigation within the Xamarin Forms Shell application from ViewModels:

using System.Threading.Tasks;

namespace Jenx.Odb2.SpeedGauge.Services
{
    public interface INavigationService
    {
        Task NavigateToRouteAsync(string route);
    }
}

And here’s the concrete implementation:

using System.Threading.Tasks;
using Xamarin.Forms;

namespace Jenx.Odb2.SpeedGauge.Services
{    public class MySimpleNavigationService : INavigationService
    {
        public Task NavigateToRouteAsync(string route)
        {
            return Shell.Current.GoToAsync(route);
        }
    }
}

Usage is straightforward. After injecting this singleton service into a ViewModel, navigation can be initiated like this:

await _navigationService.NavigateToRouteAsync("//main-shell/rpm-page");

This line of code navigates the application to the route defined in AppShell.xaml, in this case, the “rpm-page.”

Activation and Deactivation Aware ViewModels

In previous Prism-based applications, the INavigatedAware interface provided a built-in mechanism to track when a view is activated (navigated to) and deactivated (navigated from). However, due to the Prism/XF Shell compatibility issues, this functionality was not directly available.

To overcome this, a BaseContentPage was created to manually wire up page lifecycle events (Appearing, Disappearing) to corresponding methods in the Page’s ViewModel binding context. This provides a quick and effective alternative to Prism’s INavigatedAware.

using Xamarin.Forms;

namespace Jenx.Odb2.SpeedGauge.Views
{
    public abstract class BaseContentPage : ContentPage
    {
        public BaseContentPage()
        {
            Appearing += BaseContentPage_Appearing;
            Disappearing += BaseContentPage_Disappearing;
            BindingContextChanged += BaseContentPage_BindingContextChanged;
        }

        private void BaseContentPage_BindingContextChanged(object sender, System.EventArgs e)
        {
            if (BindingContext is ViewModels.BaseViewModel context)
            {
                context.OnInitialization();
            }
        }

        private void BaseContentPage_Disappearing(object sender, System.EventArgs e)
        {
            if (BindingContext is ViewModels.BaseViewModel context)
            {
                context.OnDeactivated();
            }
        }

        private void BaseContentPage_Appearing(object sender, System.EventArgs e)
        {
            if (BindingContext is ViewModels.BaseViewModel context)
            {
                context.OnActivated();
            }
        }
    }
}

The corresponding BaseViewModel class provides virtual methods (OnActivated, OnDeactivated, OnInitialization) that can be overridden in derived ViewModels to handle page lifecycle events:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Jenx.Odb2.SpeedGauge.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        private bool isBusy = false;
        public bool IsBusy
        {
            get { return isBusy; }
            set { SetProperty(ref isBusy, value); }
        }

        private string title = string.Empty;
        public string Title
        {
            get { return title; }
            set { SetProperty(ref title, value); }
        }

        protected bool SetProperty<T>(ref T backingStore, T value,
            [CallerMemberName]string propertyName = "",
            Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;

            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }

        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName]string propertyName = "")
        {
            var changed = PropertyChanged;
            if (changed == null)
                return;

            changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion INotifyPropertyChanged

        public virtual void OnActivated()
        {
        }

        public virtual void OnDeactivated()
        {
        }

        public virtual void OnInitialization()
        {
        }
    }
}

This combination of BaseContentPage and BaseViewModel allows for effective control over ViewModel activation and deactivation, mirroring the functionality previously provided by Prism’s INavigatedAware.

OBD2 Communication Layer over BLE

As mentioned earlier, the details of OBD2 communication over BLE are elaborated in a separate blog post: https://www.jenx.si/2020/08/13/bluetooth-low-energy-uart-service-with-xamarin-forms/.

Here, we’ll focus on the API definitions that structure the communication handling:

  • IObd2BluetoothManager: This interface is responsible for managing the Bluetooth Low Energy communication aspects. It handles tasks like scanning for OBD2 dongles, connecting and disconnecting, maintaining connection persistence, and managing disconnection events.
using Plugin.BLE.Abstractions.Contracts;
using System;
using System.Threading.Tasks;

namespace Jenx.Obd2.Bluetooth
{
    public interface IObd2BluetoothManager
    {
        event EventHandler Obd2DongleDisconncted;
        event EventHandler Obd2DongleConnected;
        IDevice Obd2BluetoothDongle { get; }
        Task DisconnectFromObd2DongleAsync();
        Task StartScanForObd2DongleAsync();
        Task<bool> ConnectToObd2DeviceAsync();
    }
}
  • IUartManager: This interface handles the higher-level communication with the ELM327 chip (commonly used in OBD2 Bluetooth dongles) and the OBD2 protocol itself, built upon the underlying BLE layer.
using System;
using System.Threading.Tasks;

namespace Jenx.Obd2.Common
{
    public interface IUartManager
    {
        bool IsExecuting { get; }
        Task<bool> InitializeAsync();

        #region generic command functions
        Task<T> GetObdDataAsync<T>(string command);
        Task<T> GetObdDataAsync<T>(string command, TimeSpan? timeOut);
        #endregion generic command functions

        #region RC currently supported OBD2 & ELM functions
        // ELM COMMANDS
        Task<bool> ElmSetDefaultsAsync();
        // code removed for brevity
        }
}

These interfaces abstract the complexities of Bluetooth and OBD2 communication, providing a clean and manageable API for interacting with vehicle data within the application.

Conclusion: Unlocking Vehicle Data with OBD2 and Xamarin Forms

While this application is a starting point and has room for further enhancements, it effectively demonstrates the potential of combining readily available technologies to create compelling applications. In this case, we’ve shown how to build a mobile application that communicates with cars using OBD2 and Bluetooth.

This project provides a solid foundation for further development and feature expansion. With this template, you can explore adding more OBD2 parameters, implementing advanced diagnostics, logging vehicle data, or even creating custom dashboards and alerts.

The world of OBD2 and vehicle data is vast and offers exciting possibilities for developers and car enthusiasts alike. By leveraging tools like Xamarin Forms and understanding the OBD2 standard, you can unlock a wealth of information from your car and create innovative applications that interact with the automotive world.

Happy coding and exploring the data within your drive!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *