Today, I will present Xamarin Forms (XF) mobile application (iOS and Android) which talks to the vehicle. I will present demo app which reads vehicle speed, engine revolutions per minute-RPM and some other data, such as engine temperature.
For that I will use: Xamarin Forms as mobile development framework, On-board diagnostics – ODB2 as car diagnostics and reporting system and a Bluetooth as a communication technology.
To get you motivated, I will start from the end and I will show my mobile application first and how it works.
As you can see, my mobile app is reading speed, RPM and engine temperature parameters.
To start with, to build such type of application you need to know basics of:
- Xamarin.Forms and how to build mobile apps with C#. Furthermore, if you target Android or/and iOS then you need to know some details on how to tune up some platform sub-stacks, e.g. Bluetooth, permissions, etc…
- Bluetooth Light (BLE) technology and Generic Attribute Profile or GATT protocol,
- OBD2, Vehicle On-board diagnostics.
For my experiment I used OBD2 Bluetooth dongle to communicate with the car. I plugged into my car’s OBD2 connector, as shown on bottom pictures.
Here, I will not go into details how I manage to handle communication with BLE dongle, but you can read more about this in my blog post here: https://www.jenx.si/2020/08/13/bluetooth-low-energy-uart-service-with-xamarin-forms/.
Let’s do some coding
In Visual Studio 2019 I created new Xamarin Forms project with Shell template. Next, I added some awesome NuGet dependencies, like Prism.Forms, Prism.Unity.Forms and Xamarin.Essentials. Furthermore, for my speed and RPM gauges I used trial version of Syncfusion.Xamarin.SfGauge GUI library. For Bluetooth communication, as usually, I used super-awesome Ble.Plugin component.
Just a glimpse into my solution when everything was on place:
Let’s look at the App.xaml.cs
class and AppShell.xaml
where general structure of my app is defined:
My App and AppShell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
using Jenx.Obd2.Bluetooth; using Jenx.Obd2.Common; using Jenx.Odb2.SpeedGauge.Services; using Prism.Ioc; namespace Jenx.Odb2.SpeedGauge { public partial class App { public App() { InitializeComponent(); MainPage = new AppShell(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterSingleton<INavigationService, MySimpleNavigationService>(); containerRegistry.RegisterSingleton<IObd2BluetoothManager, Obd2BluetoothManager>(); containerRegistry.RegisterSingleton<IUartManager, UartBluetoothManager>(); } protected override void OnInitialized() { } } } |
Nothing unusual here, except that my application inherits from PrismApplication. This way I can use Prism.Unity Inversion of control (IoC) container functionality in my Xamarin Forms application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<?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 time (regarded to my past demos and blog post) my app uses MVVM pattern, service Dependency injection approach and other fancy features. With all these features, I am one step closer to more modular, organized and more extensible type of the application.
Now, let’s look basic app pages.
Connect Page
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
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); } } } } |
Look & Feel
Kmph page
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<?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://xamarin.com/schemas/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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
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 { } } } } } |
Look & Feel
RPM page
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
<?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://xamarin.com/schemas/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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
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 { } } } } } |
Look & Feel
Info page
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?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://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"> <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
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); } } } } |
Look & Feel
Code is more or less self-explanatory, so no extra comments are needed. Basic work here is reading OBD2 with BLE dongle, parse ODB2 responses and bind data to views.
Other small tricks
Navigation service
I frequently use Prism & Unity toolkits with my projects. For different purposes: Dependency Injection, as Service Locator, event aggregation, for Navigation, Popups, modularity and similar.
Currently, Prism is not yet fully compatible with XF Shell. For instance, navigation is not working with XF Shell. So, my first update was custom INavigationService
. The idea was to navigate Xamarin forms Shell from my ViewModels. Here is the one very quick fix:
1 2 3 4 5 6 7 8 9 |
using System.Threading.Tasks; namespace Jenx.Odb2.SpeedGauge.Services { public interface INavigationService { Task NavigateToRouteAsync(string route); } } |
And my implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 very simple – after injecting this singleton into ViewModel I can call it like this:
1 |
await _navigationService.NavigateToRouteAsync("//main-shell/rpm-page"); |
and I am navigated to the requested route defined in my Shell.
Activation or navigation aware ViewModels
In past, with Prism navigation service and INavigatedAware mechanism, I could use built-in mechanism which tells when some view is activated (navigate to) and deactivated (navigate from). Again, with broken Prism/XF Shell functionality, this is not currently available. So, I needed to cope with some other trick here. I created BaseContentPage
and wire up page life-cycle events to Page ViewModel binding context. Quick and efficient alternative to Prism’s INavigatedAware.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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(); } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
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 way I could control my page/viewmodel activation and deactivation.
OBD2 over BLE – communication layer
As stated before, ODB2 over BLE is described a little bit in more details here: https://www.jenx.si/2020/08/13/bluetooth-low-energy-uart-service-with-xamarin-forms/
Here, I will just show out two APIs definition how I managed to handle all the communication:
IObd2BluetoothManager
, which handles BLE communication (connect, disconnect, scan, persist connection, handles disconnect, etc…)
and
IUartManager
, which handles ELM327 and OBD2 communication on top of BLE layer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 } } |
Summary
Of course, my app if far from being perfect. But it’s a start. Now, I have basic template to work on it and put some additional features on top.
In this blog post I showed how with combining different technologies and tools one can easy achieve very interesting applications. In my case presented here- mobile application which talking to the cars!
Happy coding.
2 thoughts on “Xamarin Forms and ODB2: Talking with the vehicles”
Hello! I followed this guide but got an error. If it’s possible, can you send me the project? This is the only project I’ve found that is used in Xamarin.Forms!
Thanks in advance.
Hello, that was a super cool Post, but the question is, can you share the code?