A few weeks ago, I took my car in for regular schedule service. While the car was there, I was informed that there was a firmware update available for my transmission and the car's ECU and asked for permission to install the update. I allowed the update, but it had me wondering if there were really any difference in the car's behaviour from the update. Subjectively, I would say that there is, but that might be confirmation bias at play. That got me thinking that if I had objective data on what the car was doing, then I would also have data against which to compare and look at a before/after update comparison. It's too late to perform such a comparison on my car now, but I was still thinking about how I might collect that data. Well, every car that was made since 1996 has a connector under the dashboard that can be used to access real time data about what the car is doing. I've got a consumer device connected to my ODB II port right now, but the data it collects is minimal; it detects speed, head breaks and accelerations, and average speeds. The car makes a lot more data available through the various sensors that are in the car including the fuel level, temperatures at certain parts of the engine, oxygen readings, and a lot more.
That started me on a project to make something that would log data. I wanted something that would work quietly with almost no required interaction from me; I wanted something that I could plug in, forget, and let it do its job. What's coded here is the solution that I've put together so far. This will be a work in progress as I get to work on projects in spurts. Getting the core functionality in place was the highest priority; there are lots of extra pieces of functionality that I would like to add, but given limited time, it's best to set those aside and concentrate on the core functionality; get the engine data and get it saved to a place where it can be analyzed. I'd rather not be swapping out memory devices to get data to be archived, so instead the data is being sent to Azure. Once sent to Azure, there are a number of things that could be done with the data. It can be saved, it can be analyzed. It can be routed to another device for real time readings. For now, I'm routing the data to an Azure Data Lake.
There are some questions to be asked about several parts of a solution to take care of all of these things.
- How do I interface with the ODB II port?
- What protocol do I need to use to integrate the car for data?
- What computing hardware will be used for the solution?
- What software/OS is needed for the solution?
- How do I get the data off the device?
Collectively, the answers to these questions give a high level description of the solution. Let's explore each of these questions.
How Do I Interface with the ODB II Port
I had two solutions available for interfacing with the ODB II port based on some hardware that I already had in my possession. I have a single board computer with an nVidia processor that has a port built in specifically for communicating with a car. Other than needing to have the physical connectors to connect the computer to the car's connector, the hardware needs for this are very minimal. While I already have the hardware on hand, I wanted the solution to be easily and cheaply reproducible. Using this single board computer didn't meet the cheap requirement. The other solution that I already had on hand was an ODB II accessory that make the data available over a bluetooth RFCOMM connection. There are versions of this adapter that communicate via RS232. Truth be known, I would have codeferred one of these since there is less code necessary to communicate with a wired connection. But I already had the Bluetooth adapter in my house. So Bluetooth it is.
The adapter that I am using is inspired by the ELM327 chipset. I say "Inspired by" and not "based on" because the chipset in the device isn't actually from ELM. There are a number of adapters out on the market that report being ELM chipsets that actually are not. When queried, they will identify themselves as being from ELM though. Depending on your disposition, these may be viewed as counterfeit devices or as emulated/compatible. There are arguments that can be used to defend both stances. But that's not a debate that I'm trying to settle and I only share this so that those that are inclined to see such practices as counterfeiting can keep an eye out. I have seen reports that some of these devices from other manufacturers don't work with certain cars. That said, I know of no way to tell ahead of time whether a device uses a genuine ELM chip or one that is by some other party.
There are many variations of these devices in many different sizes. The one I used is a bit on the large size. But I think that may be because it is older. Depending on the positioning of your ODB port, you might want to make sure you get a smaller one. Otherwise, it may be in a position to be knocked out of place by the driver's knee. I regard any object that could come fall in the area of the driver's feet to be dangerous; you don't want one falling out and getting wedged under a pedal codevengint it from being fully codessed.
The OBD II Bluetooth adapter and a harness I used to power it from outside the car.
What Protocol Do I Need for Communicating With the Car
The answer to this question is linked to the answer to the first question. Since I'm using the ELM327 inspired connector, I'm going to be using the protocol that is used by ELM. There are several protocols that a vehicle could use for its ODB connector. The ELM based devices implement several of these protocols and allow the data from them to be accessed via a single protocol that was created by ELM. The protocol is text based. A simplified description of it is I send the ID for a specific reading and the adapter responds with the value for that attribute. Most of the querying and responses are hex responses. The ELM devices also support a number of AT modem commands to change the settings for the device. The protocol is easy enough to work with to start doing some querying with a laptop or phone and the adapter. Go ahead, give it a try. If you are using an Android phone, the application Blue Term is useful here. If you are using a PC, the application PuTTY works.
Connect your ODB device to your car and pair your computer or phone (I'll just say "computer" from here on) with it. On a PC, you'll have to look in the device manager to find the COM port associated with the device. On an Android device, if you open the Bluetooth settings, you'll see the name of the device there. For my phone, the device is named "ODBII
". In either case, open your terminal software and connect to the device. In the terminal software, type AT
and codess enter. If all is working fine, you will get back the response OK
. For devices that accept the AT command set, most of the commands will begin with the string AT
. AT
by itself performs no operation and is useful for testing a connection without doing anything.
A safety codecaution for this next test. I'm sure many of you already know carbon monoxide can be deadly. For any test that involves having the car running, you should take the car out of the garage and have it outside. I remind you here because I don't want anyone to forget in their excitement that running the car in the garage with the door closed can be deadly.
Take the car outside where it is safe to let the engine run. While the engine is running, type 01 0C
. The car will respond with a sequence of hex numbers starting with 41 0C
. Most of the commands that you use will begin with 01
. The 0C
that follows is for the engine RPM. In the returned response, the 0C
is the requested attribute being repeated. If the hex digits that follow it are converted to an integer, you'll have the engine's RPMs. Querying the rest of the engine readings is just a matter of knowing the ID for the attribute and how to interpret the number that is returned. Within my code, I have an enumeration that contains the IDs for some of the properties and the numerical values. I labelled these values as LiveProperty
because these are values on the real-time present state of the engine. There are also values saved from when an event triggered the car's computer taking a snapshot of the values to be analyzed during diagnostics. I don't look at the snapshot values within this project at all.
public enum LiveProperty:int
{
#region PidRange Queries
PidRange_00_32 = 0x00,
PidRange_33_64 = 0x20,
PidRange_65_96 = 0x40,
PidRange_97_128 = 0x60,
PidRange_129_160 = 0x80,
#endregion
HeadersOn = 1,
HeadersOff = 2,
FuelSystemStatus = 0x03,
EngineLoad = 0x04,
EngineCoolantTemperature = 0x05,
ShortTermFuelTrimBank1 = 0x06,
LongTermFuelTrimBank1 = 0x07,
ShortTermFuelTrimBank2 = 0x08,
LongTermFuelTrimBank2 = 0x09,
FuelPressure = 0x0A,
IntakeManifoldAbsolutePressure = 0x0B,
EngineRPM = 0x0C,
VehicleSpeed = 0x0D,
TimingAdvance = 0x0E,
AirIntakeTemperature = 0x0F,
Airflow = 0x10,
Throttle = 0x11,
SecondaryIntakeCircuit = 0x12,
#region O2Sensor
O2SensorVoltsBank1Sensor1 = 0x14,
O2SensorVoltsBank1Sensor2 = 0x15,
O2SensorVoltsBank1Sensor3 = 0x16,
O2SensorVoltsBank1Sensor4 = 0x17,
O2SensorVoltsBank2Sensor1 = 0x18,
O2SensorVoltsBank2Sensor2 = 0x19,
O2SensorVoltsBank2Sensor3 = 0x1A,
O2SensorVoltsBank2Sensor4 = 0x1B,
#endregion
};
What Computing Hardware Will be Needed for the Solution
While I've eliminated the nVidia device from further consideration, there are still a number of other devices that could work. The Intel Edison is tiny (about the size of a quarter) and could do the job well. I also have a number of Raspberry Pis in the house, an Arrow Dragon Board 401c single board computer, and some other devices. I decided to use the Dragonboard 401c, mainly because it has GPS build in. But someone implementing this solution for themselves isn't obligated to use the same hardware to use my same solution because of my OS/software decision. A key difference between the Raspberry Pi II and the Raspberry Pi III is the Raspberry Pi II has build in wireless adapters. The most important adapter for this solution is the Bluetooth adapter, since it is being used to communicate with the hardware.
What Software/OS Do I Need For the Solution
I decided to use Windows 10 IOT for my solution. Windows 10 IOT will run on the Raspberry Pi II and above. It also runs on the Dragonboard 401c. This is why someone using my solution could use either board. The application that I'm developing is a UWP application (Universal Windows Platform). In addition to running on these devices, it can also run on a PC. This is helpful as it allows for some amount of debugging without being directly tethered to the car. There were quite a few times during the development of this software in which I captured communications from the port and debugged from a desktop (or on a laptop while riding on the train to work).
UWP based applications can be compiled to run on ARM hardware, on x86 hardware, or on x64 hardware.
How Do I Get Data Off the Device
Once the device for the solution has received data, there's still the problem of getting it to the outside world. There are two ways to do this, over a network connection and via a storage device. The network connection is my primary solution but I haven't neglected storage devices. I've made the solution so that it will both write to a storage device and send the data to Azure. This solution is made to run headless; without a display, keyboard, or mouse. I had to think of how it will select a storage location before hand. When the application starts, it will look for external storage devices connected to the device. It will use the first one that it finds. If there are no storage devices, it will write to internal memory. Windows IOT doesn't depart far from the Windows that you know in that there is also a Documents folder. This will be a fallback location if there are no external memory devices attached. If data is written to the Documents folder, it is possible to browse for it over a network connection This is not something that I want to have to do, but it's available for those that are interested. I've not done anything to handle the case of the device's memory being full though. If you plan to use this solution for yourself and don't have a lot of memory available, this is a problem you will want to address.
Getting data into Azure is simple. After putting the data to be saved in a JSON string
, I use the Azure client library to send the JSON message. You'll see how this works as I walk through the code.
Implementation
Using Visual Studio 2017, create a new project. The project type will be a blank UWP project. I've named mine AutoTelemetry
and this will be reflected throughout the code. Once the project is created, right-click on it and select "New Folder." Create a folder called "ViewModels." There is some foundational code that will need to be added here. If you are familiar with the MVVM pattern, then this code will be basic. The MVVM pattern isn't explained here since there is already an abundance of information about it available. Right-click on the ViewModels folder and select Add and New Item. For the type of item, select Class and name the item ViewModelBase.cs. The contents to ViewModelBase
will be the following:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Core;
namespace AutoTelemetry.ViewModels
{
public abstract class ViewModelBase : INotifyPropertyChanged
{
public CoreDispatcher Dispatcher { get; set; }
protected void OnPropertyChanged<T>(Expression<Func<T>> expression)
{
OnPropertyChanged(((MemberExpression)expression.Body).Member.Name);
}
void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
if (this.Dispatcher != null)
{
Dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
() =>
{
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
});
}
else
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected bool SetValueIfChanged<T>(Expression<Func<T>> propertyExpression,
Expression<Func<T>> fieldExpression, object value)
{
var property = (PropertyInfo)((MemberExpression)propertyExpression.Body).Member;
var field = (FieldInfo)((MemberExpression)fieldExpression.Body).Member;
return SetValueIfChanged(property,field, value);
}
protected bool SetValueIfChanged(PropertyInfo pi,FieldInfo fi, object value)
{
var currentValue = pi.GetValue(this);
if ((currentValue == null && value == null)||
(currentValue!=null && currentValue.Equals(value)))
return false;
fi.SetValue(this, value);
OnPropertyChanged(pi.Name);
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
There are a few areas where this class might not conform to the norm. One is the presence of a CoreDispatcher
in the class. I've placed this here because there will be classes that are modified from other threads and change events must be raised on the primary thread. The SetValueIfChanged
methods probably look the most unfamiliar. I got tired of typing out a repetitive code pattern and I implemented something to shorten the code. I've already written on how this was developed in another post. For an explanation, you can read this.
Add another class named EngineState
. This class has a number of properties and inherits from ViewModelBase
. The properties on it are all implemented using the same pattern. Here are a few of the implementations of the properties to show the patterns.
int? _rpm;
public int? RPM
{
get { return _rpm; }
set
{
SetValueIfChanged(() => RPM, () => _rpm, value);
ResetLastUpdated();
}
}
int? _throttle;
public int? Throttle
{
get { return _throttle; }
set
{
SetValueIfChanged(() => Throttle, ()=>_throttle, value);
ResetLastUpdated();
}
}
int? _vehicleSpeed;
public int? VehicleSpeed
{
get { return _vehicleSpeed; }
set
{
SetValueIfChanged(() => VehicleSpeed, () => _vehicleSpeed, value);
ResetLastUpdated();
}
}
Add another class named MainViewModel
. This class should also inherit from ViewModelBase
. We will be adding most of our code within this class.
namespace AutoTelemetry.ViewModels
{
public class MainViewModel : ViewModelBase
{
}
}
Another well-known class set used in Microsoft's XAML based UI technologies is the DelegateCommand
classes. These classes can be found defined in some other toolkits. But since these are the only classes from the toolkits that I'd be using, I've included the classes here instead of referencing the toolkit. There is also an abundance of information available on this class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace AutoTelemetry.ViewModels
{
public class DelegateCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public DelegateCommand(Action execute)
: this(execute, null)
{
}
public DelegateCommand(Action execute, Func<bool> canExecute)
{
if ((_execute = execute) == null)
throw new ArgumentNullException("execute");
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
return true;
return _canExecute();
}
public void Execute()
{
if (CanExecute(null))
_execute();
}
void ICommand.Execute(object parameter)
{
Execute();
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
public class DelegateCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
public DelegateCommand(Action<T> execute)
: this(execute, null)
{
}
public DelegateCommand(Action<T> execute, Func<T, bool> canExecute)
{
if ((_execute = execute) == null)
throw new ArgumentNullException("execute");
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
return true;
return _canExecute((T)parameter);
}
public void Execute(T parameter)
{
if (CanExecute(parameter))
_execute(parameter);
}
void ICommand.Execute(object parameter)
{
Execute((T)parameter);
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
}
It's time to get into the code for the solution. In an UWP application, there are capability declarations that are necessary to get access to certain hardware and features. Part of the thinking behind this is in the safety for applications that are published in the app store. A user can see before installing the application what features of the machine that the application requests access to for making a more informed decision on whether or not to install it; if you see a tip calculator that also needs access to your contacts, then there may be something suspicious going on. While this application won't see daylight in the app store, it is still bound by some of the same rules. To have access to the Internet and the Bluetooth adapter, there are declarations that are needed. Without these declarations, you'll get some strange errors that can be confusing. Rather than expose you to those errors, I'll guide you through getting the necessary declarations made now. Visual Studio has a UI for handling declarations. As useful as it is, I've found that it doesn't expose what's needed to make the declaration for access to RFCOMM.
Right-click on the file in the project named Package.appxmanifest and select View Code. An XML document opens. There will be a section in the document with a <Capabilities>
element. It will already have an internet
capability defined. Change this section so that it looks like the following:
<Capabilities>
<Capability Name="internetClient" />
<DeviceCapability Name="bluetooth" />
<DeviceCapability Name="proximity" />
<DeviceCapability Name="location" />
<DeviceCapability Name="bluetooth.rfcomm">
<Device Id="any">
<Function Type="name:serialPort" />
</Device>
</DeviceCapability>
</Capabilities>
This will give the application access to Bluetooth RFCOMM and location information. The code will need to scan for the available devices on the computer to find the device that is connected to the car. I've hardcoded a piece of knowledge here; I know that the device is named ODBII
. While I receive a number of devices from the scan, I take the one that is named ODBII
and continue with it. Right.
string[] requestedProperties = new string[]
{ "System.Devices.Aep.DeviceAddress", "System.Devices.Aep.IsConnected" };
_deviceWatcher = DeviceInformation.CreateWatcher
("(System.Devices.Aep.ProtocolId:=\"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}\")",
requestedProperties,
DeviceInformationKind.AssociationEndpoint);
_deviceWatcher.Stopped += (sender,x)=> {
_isScanning = false;
Log("Device Scan Halted");
};
EngineDataList.Add("started");
_deviceWatcher.Added += async (sender, devInfo) =>
{
if (devInfo.Name.Equals("OBDII"))
{
DeviceAccessStatus accessStatus =
DeviceAccessInformation.CreateFromId(devInfo.Id).CurrentStatus;
if (accessStatus == DeviceAccessStatus.DeniedByUser)
{
Debug.WriteLine("This app does not have access to connect to the
remote device (please grant access in Settings > Privacy > Other Devices");
return;
}
var device = await BluetoothDevice.FromIdAsync(devInfo.Id);
}
}
This works, but there's a major problem with it. It's SLOW! If you run this in an application after several moments, it will eventually find the Bluetooth hardware. The only thing that is needed from this scan is the hardware ID for the bluetooth adapter. Instead of scanning for this every single time once a successful connection is made to the device, I'm saving the device's ID. The psuedo code for what I'm doing looks like this:
IF(SavedDeviceIDFound)
var connected = TryConnectToDevice(DeviceID);
if(connected) return;
END IF;
DeviceID = SearchForDeviceID();
var connected = TryConnectToDevice(DeviceID)
IF(connected)
SaveDeviceID(DeviceID);
END IF;
I make use of the LocalSettings
API to save the device ID. You can read more about how that works here on CodeProject. Once connected to a device, I acquire a stream to the device construct data readers and data writers to interact with the device. The data reader and writer are used independently of each other.
DataReader _receiver;
DataWriter _transmitter;
StreamSocket _stream;
async Task ConnectToDevice(string deviceId)
{
var device = await BluetoothDevice.FromIdAsync(deviceId);
Debug.WriteLine(device.ClassOfDevice);
var services = await device.GetRfcommServicesAsync();
if (services.Services.Count > 0)
{
Log("Connecting to device stream");
var service = services.Services[0];
_stream = new StreamSocket();
await _stream.ConnectAsync(service.ConnectionHostName,
service.ConnectionServiceName);
_receiver = new DataReader(_stream.InputStream);
_transmitter = new DataWriter(_stream.OutputStream);
ReceiveLoop();
QueryLoop();
StatusUpdateLoop();
await this.Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
IsConnected = true;
});
}
}
The three methods called close to the end of the method for connecting start new threads. One thread sends the messages to request the various readings (QueryLoop()
). It's operation is fairly independent and blind to everything else going on. Another thread is for handling incoming data and passing it to the parser (ReceiveLoop()
). The device may query information several times per second. But I want to save the state of the engine at a lower sample rate. The StatusUpdateLoop()
will at some frequency get the accumulation of current readings and will store them.
When receiving data, there is no guarantee that the data available for reading is a complete message. It is necessary to buffer data that comes in and then process it once that data is known to be a complete response. Complete responses are delimited by new line characters. Once a new line character is detected, the buffered data is processed and the buffer is cleared so that data accumulation can continue.
StringBuilder receiveBuffer = new StringBuilder();
void ReceiveLoop()
{
Task t = Task.Run(async () => {
Log("Starting listening loop");
while (true)
{
uint buf;
buf = await _receiver.LoadAsync(1);
if (_receiver.UnconsumedBufferLength > 0)
{
string s = _receiver.ReadString(1);
receiveBuffer.Append(s);
if (s.Equals("\n")||s.Equals("\r"))
{
try
{
ProcessData(receiveBuffer.ToString());
receiveBuffer.Clear();
}
catch(Exception exc)
{
Log(exc.Message);
}
}
}else
{
await Task.Delay(TimeSpan.FromSeconds(0));
}
}
});
}
A natural way to implement the method for processing the data is to figure out what type of data is being sent and then have a large switch
statement or if
/then
list to assign the received data to the right property on a data object. But that's too much code for something relatively simple. I took a route that is a little less repetitive. Instead, after converting the hex string to a byte array, I take advantage of the easy conversion between integers and enumerated values. I also have dictionary mapping enumerated values and the properties that they are associated with. If additional properties have to be parsed, I only need to make sure there is an Engine
property for it, an enum
definition, and a mapping for the property and the value.
_livePropertyMappings = new Dictionary<liveproperty, propertyinfo="">();
_livePropertyMappings.Add(LiveProperty.EngineRPM,
typeof(EngineState).GetProperty(nameof(EngineState.RPM)));
_livePropertyMappings.Add(LiveProperty.Throttle,
typeof(EngineState).GetProperty(nameof(EngineState.Throttle)));
_livePropertyMappings.Add(LiveProperty.VehicleSpeed,
typeof(EngineState).GetProperty(nameof(EngineState.VehicleSpeed)));
_livePropertyMappings.Add(LiveProperty.EngineCoolantTemperature,
typeof(EngineState).GetProperty(nameof(EngineState.EngineCoolantTemperature)));
Returning back to the processing of the incoming data stream, the method for processing the data after the conversion from the hex string to a byte array is simple.
void ProcessEngineDataMessage(byte[] message)
{
if (message[0] == 0x41)
{
LiveProperty prop = (LiveProperty)message[1];
if (_livePropertyMappings.ContainsKey(prop))
{
int val = GetInt(message, 2);
Log($"{prop} = {val}");
_livePropertyMappings[prop].SetValue(this.Engine, val);
}
}
}
Querying of the data is also simple. I make an array containing the properties to be queried and loop through them sending the query message for each property one at a time.
void QueryLoop()
{
Task t = Task.Run(async () =>
{
LiveProperty[] propertyList =
{
LiveProperty.EngineRPM,
LiveProperty.Throttle,
LiveProperty.VehicleSpeed,
LiveProperty.EngineCoolantTemperature
};
int count = 0;
while (true)
{
QueryPropertyCommand.Execute(propertyList[(++count)%propertyList.Length]);
await Task.Delay(50);
}
}
);
}
The last loop I have yet to define is the StatusUpdateLoop()
. The EngineState
class has a property named LastUpdated
that is a time stamp of the last time a value in an instance of the class was updated. The StatusUpdateLoop
checks on the class once every 5 seconds. It checks to make sure that the time stamp on the current object state is more recent than the last time the object state was read. If it is the object state is grabbed and sent to Azure .
void StatusUpdateLoop()
{
Task t = Task.Run(async () =>
{
while(true)
{
if(Engine.LastUpdated > _lastUpdate)
{
EngineState update;
lock(SyncRoot)
{
update = this.Engine;
this.Engine = new EngineState() { VIN = update.VIN,
LastUpdated = _lastUpdate = DateTime.Now, Location = this.LastPosition };
}
try
{
this.Dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
() =>
{
this._engineLog.Add(update);
});
Message iotMessage = new Message(Encoding.UTF8.GetBytes(update.ToString()));
await IotDeviceClient.SendEventAsync(iotMessage);
}
catch (Exception exc) {
Log(exc.Message);
}
}
await Task.Delay(5000);
}
});
}
UI Dashboard
I intend for this application to run as a headless application. But I'm adding elements of a UI. What purpose does this serve? It provides debugging information. I wanted to add graphical gauges but I didn't want to spend a lot of time implementing these myself. There's a UWP toolkit that contains gauges but I ran into problems getting these to work. I ended up using the Telerik UI toolkit. It contains radial and linear gauges (it's easy to use!). These gauges have built in animation support. After adding the reference to the Telerik libraries and dropping the controls into a page, the controls can be configured through XAML and the values are set through XAML data binding. The only code for managing the gauges is their declarations. I'm not trying to expose all the values through the UI. Only a few that are also displayed on my car's inbuilt dashboard. RPM, speed, and fuel level.
<telerik:RadRadialGauge
x:Name="Speedometer"
Grid.Row="1"
Grid.RowSpan="2"
Background="Gray"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
MinValue="0" MaxValue="120"
MinAngle="-45" MaxAngle="225"
LabelRadiusScale="0.8"
TickRadiusScale="0.85"
TickStep="10"
LabelStep="20" >
<telerik:RadRadialGauge.LabelTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontSize="20" FontWeight="Bold"
Foreground="#595959" Margin="0,0,0,10"></TextBlock>
</DataTemplate>
</telerik:RadRadialGauge.LabelTemplate>
<telerik:SegmentedRadialGaugeIndicator StartValue="0"
Value="{Binding Engine.VehicleSpeed}" telerik:RadRadialGauge.IndicatorRadiusScale="0.73">
<telerik:BarIndicatorSegment Thickness="20" Stroke="#8080FF" Length="80"/>
<telerik:BarIndicatorSegment Thickness="20" Stroke="Blue" Length="0"/>
<telerik:BarIndicatorSegment Thickness="20" Stroke="#000080" Length="2"/>
</telerik:SegmentedRadialGaugeIndicator>
<telerik:MarkerGaugeIndicator Value="70" Content="*" FontSize="17"
Foreground="#595959" telerik:RadRadialGauge.IndicatorRadiusScale="0.83"/>
</telerik:RadRadialGauge>
<TextBlock Grid.Row="1" Grid.RowSpan="2" VerticalAlignment="Center"
HorizontalAlignment="Center" >Speed</TextBlock>
<telerik:RadRadialGauge
x:Name="FuelLevelMeter"
Grid.Column="1"
Grid.Row="1"
MinValue="0" MaxValue="100"
MinAngle="-45" MaxAngle="225"
LabelRadiusScale="0.9"
TickStep="20"
LabelStep="20" >
<telerik:SegmentedRadialGaugeIndicator StartValue="0"
Value="{Binding Engine.FuelLevel}" telerik:RadRadialGauge.IndicatorRadiusScale="0.73">
<telerik:BarIndicatorSegment Thickness="20" Stroke="Orange" Length="80"/>
<telerik:BarIndicatorSegment Thickness="20" Stroke="Blue" Length="0"/>
<telerik:BarIndicatorSegment Thickness="20" Stroke="Black" Length="2"/>
</telerik:SegmentedRadialGaugeIndicator>
</telerik:RadRadialGauge>
<TextBlock Grid.Column="1" Grid.Row="1" VerticalAlignment="Center"
HorizontalAlignment="Center" Text="Fuel"/>
<telerik:RadRadialGauge
x:Name="RPMMeter"
Grid.Column="1"
Grid.Row="2"
MinValue="0" MaxValue="7000"
MinAngle="-45" MaxAngle="225"
LabelRadiusScale="0.9"
TickRadiusScale="0.85"
TickStep="1000"
LabelStep="1000" >
<telerik:SegmentedRadialGaugeIndicator StartValue="0" Value="{Binding Engine.RPM}"
telerik:RadRadialGauge.IndicatorRadiusScale="0.73">
<telerik:BarIndicatorSegment Thickness="20" Stroke="Purple" Length="80"/>
<telerik:BarIndicatorSegment Thickness="20" Stroke="Yellow" Length="0"/>
<telerik:BarIndicatorSegment Thickness="20" Stroke="Green" Length="2"/>
</telerik:SegmentedRadialGaugeIndicator>
</telerik:RadRadialGauge>
<TextBlock Grid.Column="1" Grid.Row="2" VerticalAlignment="Center"
HorizontalAlignment="Center" Text="RPM"/>
The Azure Settings
I'm using Azure IOT Hub for getting information off of the device. I've already got an Azure account. The free account will work for testing. Here, I don't talk about the procedures of setting up a new account. But if you visit the Azure web site, you will see the instructions for getting a free account. Once in the account, the amount of options and actions that one can take can be intimidating. I won't be exploring all of those options and will focus on what is needed. For this project, I want to setup a resource group in which the other resources that I use will exists. This isn't purely necessary, but it makes for easier organization.
Once you sign into the account, on the left menu, click on the item titled Resource Groups
. When the Resource Group screen opens, click on Add. Enter a name for the resource group and select "Create".
A Windows Azure IoT Hub resource must be created. From the menu on the left, select Create a Resource
. In the screen that opens, type IoT
in the search window to narrow the options. Select IoT Hub
and click on Create
. In the screen that opens, select the option to use an existing resource group. Select the resource group that you had created and give the IoT Hub instance a name and select Review+Create
. On the next screen, check your entries and select Create
. The creation of the IoT resource will take a few minutes. If you click on the notification icon (shaped like a bell), you can view the progress of the creation process.
Once the resource is created, you can get to it by selecting All Resources from the left menu and then selecting the resource that you just created in the screen that appears. We need to create a record for an IoT device (devices are uniquely identified). After selecting the newly created IoT Hub instance, click on Iot devices from the menu and then select Add. Give the device a name and select Save. After the device is created, you can click on it in the IoT Devices screen to get specific settings such as the device's connection string.
The next resource to create is a place to store the incoming data. Click on Create Resource and select Data Lake Storage Gen1 from the list of possible resources to create. Remember that you can filter the options by typing the name of the resource in the window at the top. Once again for the resource group, choose the existing group that we created and give the data lake storage a name and select Create. Once again, the creation of the new resource will take a few minutes.
After the creation finishes, we have a record for a device and credentials to share with the real (physical) device. Clicking on the device, I see the device specific connection string. For now, I am embedding the string
within the code.
DeviceClient _iotDeviceClient;
DeviceClient IotDeviceClient
{
get
{
return _iotDeviceClient ?? (_iotDeviceClient =
DeviceClient.CreateFromConnectionString(DeviceConnectionString, TransportType.Http1));
}
}
We have a storage location in which it can put its data. But we don't yet have a way to get the data from the device to the storage. To do this, we need a Stream Analytics Job. Use the Create Resource option to create a new job.
After the job is created, navigate to it; there are other properties to be changed on it before we can use it. Click on the Input setting. For the input, select Add stream input and select IoT Hub as the input source. you are prompted for a name to alias the input. Enter a name and ensure that JSON is selected as the serialization format.
An output also must be defined. Click on the Output menu item and select Create. You'll be asked what type of output you are using. Select Azure Data Lake. Give the output a name. Under Path prefix pattern, a pattern must be entered to know the folder structure in which to save the logs. I am using the path engine/ecu/{date}. When there is data to be processed, {date} will be replaced with the current date. Because of what I've selected for the Date format, there will be a folder for year, a folder for month, and a folder for the day. I've set the output format to JSON. After selecting all of these, I click on Authorize, wait a few moments, then click on Save.
To complete the Stream Analytics job, we need to add a query. Since no transform is going to be performed on the data for now, the query only needs to allow the data to pass through. The default query that shows up here will do.
At this point, we have an Azure configuration that is sufficient for streaming engine data from the car to the data lake for analysis and collection. Note that any data that streams through our configuration will be saved temporarily. It will persist for a number of days before it is deleted. If we want to keep the data archived, this is well more than enough time to grab the data and move it to longer term storage.
Let's revisit some code that was within the status loop update.
var update = this.Engine;
this.Engine = new EngineState() { VIN = update.VIN, LastUpdated = _lastUpdate = DateTime.Now,
Location = this.LastPosition, Dispatcher=this.Dispatcher };
try
{
Message iotMessage = new Message(Encoding.UTF8.GetBytes(update.ToString()));
await IotDeviceClient.SendEventAsync(iotMessage);
}
catch (Exception exc) {
Log(exc.Message);
}
The code above grabs the current instance of the object containing engine data and creates a new one for other data to be collected. Note that the new instance is given a dispatcher; it is updated from another thread and the dispatcher is used to send INotifyPropertyChanged
events back to the UI thread. With the instance that was just grabbed, I convert it to JSON (the object's ToString()
method has been overridden to output in JavaScript) and it is wrapped in an Azure Message
object. The message is then sent to the Azure cloud using DeviceClient.SendEventAsync
from the Azure Client Toolkit.
Running the Code
The project is ready to run. For an Internet connection, I've got a dedicated portable cellular hotspot in my car. I brought it into the house to pair it with my Windows IoT hardware while it was connected to a screen. Using the Microsoft IoT Dashboard, I opened the settings for the device and set my application as the default application. Now when the device gets powered, my application will automatically start, connect to the ODB II adapter, and start collecting the data to be sent to the cloud.
Okay, You Are Collecting Data, No What?
There are many directions in which I could extend the solution that I have. My primary objective for this phase of my project is to collect data for analysis. The initial next steps are to get something in place to download and archive the engine data on some regular frequency. If you are using the Raspberry Pi 3B, a solution to consider is PiJuice. PiJuice isn't officially supported for Windows 10 IoT, but the source code is public and at first glance, it looks easy to use. PiJuice contains a battery to keep the device turned on when the car turns off. It has a built in real time clock and allows alarms to be scheduled to wake the device back up. There are also additional sensors that could be added that compliment the sensor data well such as an accelerometer.
History
- 8th August, 2019: Initial publication