使用Windows IoT将引擎数据流式传输到Azure(译文)
By S.F.
本文链接 https://www.kyfws.com/news/streaming-engine-data-to-azure-with-windows-iot/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 21 分钟阅读 - 10291 个词 阅读量 0使用Windows IoT将引擎数据流式传输到Azure(译文)
原文地址:https://www.codeproject.com/Articles/1255745/Streaming-Engine-Data-to-Azure-with-Windows-IoT
原文作者:Joel Ivory Johnson
译文由本站翻译
前言
使用OBD II端口和Windows IoT设备(DragonBoard 410c或Raspberry Pi)将实时引擎数据推送到Azure云进行分析
几周前,我乘车去了定期的时间表服务.当汽车在那儿时,我被告知有一个固件更新可用于我的变速器和汽车的ECU,并要求获得安装更新的许可.我允许进行更新,但是我想知道汽车的行为与更新之间是否确实存在任何差异.从主观上讲,我会说存在,但这可能是确认偏差在起作用.那让我想到,如果我有关于汽车运行状况的客观数据,那么我也将有可比较的数据并查看更新前后的比较.现在在我的车上进行这样的比较为时已晚,但我仍在思考如何收集这些数据.嗯,自1996年以来生产的每辆汽车在仪表板下方都有一个连接器,可用于访问有关汽车运行状况的实时数据.我现在有一个连接到ODB II端口的消费类设备,但是它收集的数据很少.它可以检测速度,头部断裂和加速度以及平均速度.汽车通过汽车中的各种传感器提供了更多数据,包括燃油油位,发动机某些部分的温度,氧气读数等. 那使我开始进行一个可以记录数据的项目.我想要一种可以安静运行且几乎不需要我干预的东西.我想要一些我可以插入,忘记并让它完成工作的东西.到目前为止,这里所编码的是我已经提出的解决方案.当我开始突如其来的项目时,这将是一项正在进行的工作.摆放核心功能是当务之急.我想添加很多额外的功能,但是在有限的时间内,最好将这些功能放在一旁并专注于核心功能.获取引擎数据并将其保存到可以分析的地方.我宁愿不换出存储设备来获取要归档的数据,因此将数据发送到Azure.发送到Azure后,可以使用数据完成许多操作.它可以保存,也可以分析.可以将其路由到另一台设备以进行实时读取.现在,我将数据路由到Azure数据湖. 有关解决所有这些问题的解决方案的几个部分,会有一些问题要问.
总而言之,对这些问题的答案对解决方案进行了高级描述.让我们探讨这些问题.
如何与ODB II端口接口
我有两种解决方案可用于基于已经拥有的某些硬件与ODB II端口进行接口连接.我有一台带有nVidia处理器的单板计算机,该处理器具有专门用于与汽车通信的内置端口.除了需要使用物理连接器将计算机连接到汽车的连接器之外,对此的硬件需求非常小.虽然我已经准备好了硬件,但是我希望该解决方案能够轻松,廉价地重现.使用这种单板计算机无法满足廉价要求.我已经拥有的另一个解决方案是ODB II附件,该附件可通过蓝牙RFCOMM连接使数据可用.此适配器的某些版本可通过RS232进行通信.说实话,由于与有线连接进行通信所需的代码较少,因此我将其中之一进行编码.但是我已经在家里有了蓝牙适配器.所以是蓝牙. 我使用的适配器受到ELM327芯片组的启发.我说"灵感来自"而不是"基于",是因为设备中的芯片组实际上并非来自ELM.市场上有许多适配器报告说实际上不是ELM芯片组.但是,当查询时,他们将标识自己来自ELM.根据您的性情,这些设备可能会被视为伪造设备或被仿真/兼容.有一些论点可以用来捍卫这两种立场.但这不是我要解决的辩论,我只分享这个观点,以便那些倾向于看到假冒等行为的人可以留意.我看到有报道说,其他制造商提供的某些设备不适用于某些汽车.就是说,我无法提前告知设备是使用真正的ELM芯片还是使用另一方的芯片. 这些设备有许多不同大小的变体.我用的那个有点大.但是我认为这可能是因为它更老了.根据ODB端口的位置,您可能需要确保获得较小的端口.否则,它可能会被驾驶员的膝盖撞掉.我认为可能落入驾驶员脚区域的任何物体都是危险的.您不希望一个人掉下来并被踩在踏板下,因为它被完全编码了.
与汽车通信需要什么协议
该问题的答案与第一个问题的答案相关.由于我使用的是ELM327启发式的连接器,因此我将使用ELM使用的协议.车辆可以将几种协议用于其ODB连接器.基于ELM的设备实现这些协议中的几种,并允许通过ELM创建的单个协议访问它们中的数据.该协议是基于文本的.对此的简化描述是,我发送特定读数的ID,然后适配器以该属性的值作为响应.大多数查询和响应是十六进制响应. ELM设备还支持许多AT调制解调器命令来更改设备设置.该协议非常容易使用,可以开始使用笔记本电脑或手机以及适配器进行一些查询.继续尝试一下.如果您使用的是Android手机,则此处的" Blue Term"应用程序非常有用.如果您使用的是PC,则可以使用PuTTY应用程序.
将您的ODB设备连接至汽车,然后将其与计算机或电话(从这里开始,我只是说"计算机")配对.在PC上,您必须查看设备管理器才能找到与设备关联的COM端口.在Android设备上,如果打开Bluetooth设置,则会在此处看到设备的名称.对于我的手机,该设备名为"ODBII
".无论哪种情况,请打开终端软件并连接到设备.在终端软件中,键入" AT",然后输入代码.如果一切正常,您将返回响应" OK".对于接受" AT"命令集的设备,大多数命令将以字符串" AT"开头. AT本身不执行任何操作,对于不做任何事情测试连接很有用.
下一个测试的安全守则.我敢肯定,你们中的许多人已经知道一氧化碳可能致命.对于涉及使汽车行驶的任何测试,您应将汽车从车库中取出并放在室外.我在此提醒您,是因为我不想让任何人兴奋地忘记在关门的情况下在车库里开车是致命的.
将汽车停在安全的地方让发动机运转.在引擎运行时,键入01到0C.赛车将以从41
0C``开始的十六进制数字序列做出响应.您使用的大多数命令都以" 01"开头.接下来的" 0C"用于引擎RPM.在返回的响应中," 0C"是所请求的属性被重复.如果将其后的十六进制数字转换为整数,则将获得引擎的RPM.查询其余的引擎读数只是知道属性的ID以及如何解释返回的数字的问题.在我的代码中,我有一个枚举,其中包含一些属性的ID和数值.我将这些值标记为" LiveProperty",因为它们是引擎实时状态的值.从事件触发汽车的计算机拍摄快照以进行诊断期间要分析的值以来,还保存了一些值.我根本不看这个项目中的快照值.
public enum LiveProperty:int
{
#region PidRange Queries
PidRange_00_32 = 0x00,//00
PidRange_33_64 = 0x20,//20
PidRange_65_96 = 0x40,//40
PidRange_97_128 = 0x60,//60
PidRange_129_160 = 0x80,//80
#endregion
HeadersOn = 1,
HeadersOff = 2,
FuelSystemStatus = 0x03,
EngineLoad = 0x04,
EngineCoolantTemperature = 0x05,
ShortTermFuelTrimBank1 = 0x06,
LongTermFuelTrimBank1 = 0x07,
ShortTermFuelTrimBank2 = 0x08,
LongTermFuelTrimBank2 = 0x09,
FuelPressure = 0x0A,
//IntakeManifoldPressure, = 0x0B
IntakeManifoldAbsolutePressure = 0x0B,
EngineRPM = 0x0C,
VehicleSpeed = 0x0D,
TimingAdvance = 0x0E,
AirIntakeTemperature = 0x0F,
Airflow = 0x10,
Throttle = 0x11,
SecondaryIntakeCircuit = 0x12,//maybe?
#region O2Sensor
O2SensorVoltsBank1Sensor1 = 0x14,
O2SensorVoltsBank1Sensor2 = 0x15,
O2SensorVoltsBank1Sensor3 = 0x16,
O2SensorVoltsBank1Sensor4 = 0x17,
O2SensorVoltsBank2Sensor1 = 0x18,
O2SensorVoltsBank2Sensor2 = 0x19,
O2SensorVoltsBank2Sensor3 = 0x1A,
O2SensorVoltsBank2Sensor4 = 0x1B,
#endregion
};
解决方案将需要什么计算硬件
尽管我不再考虑nVidia设备,但仍有许多其他设备可以使用.英特尔Edison很小(大约四分之一的大小),可以很好地完成工作.我家中也有许多Raspberry Pi,一台Arrow Dragon Board 401c单板计算机,以及其他一些设备.我决定使用Dragonboard 401c,主要是因为它内置了GPS.但是,由于我的OS/软件决定,自己实施此解决方案的人没有义务使用相同的硬件来使用相同的解决方案. Raspberry Pi II和Raspberry Pi III之间的主要区别在于Raspberry Pi II具有内置的无线适配器.此解决方案最重要的适配器是蓝牙适配器,因为它已用于与硬件通信.
解决方案需要什么软件/操作系统
我决定将Windows 10 IOT用于我的解决方案. Windows 10 IOT将在Raspberry Pi II和更高版本上运行.它还在Dragonboard 401c上运行.这就是使用我的解决方案的人可以使用任一板的原因.我正在开发的应用程序是UWP应用程序(通用Windows平台).除了在这些设备上运行外,它还可以在PC上运行.这是有帮助的,因为它允许进行一些调试,而无需直接绑定到汽车上.在开发此软件的过程中,有很多次我从端口捕获了通信并从台式机(或乘坐火车上班的笔记本电脑上)进行调试. 可以将基于UWP的应用程序编译为在ARM硬件,x86硬件或x64硬件上运行.
如何从设备中获取数据
解决方案的设备接收到数据后,仍然存在将其发送到外界的问题.有两种方法可以通过网络连接和通过存储设备来执行此操作.网络连接是我的主要解决方案,但我没有忽略存储设备.我已经制定了解决方案,以便将其写入存储设备并将数据发送到Azure.该解决方案可以无头运行.没有显示器,键盘或鼠标.我不得不考虑如何手动选择存储位置.当应用程序启动时,它将查找连接到该设备的外部存储设备.它将使用找到的第一个.如果没有存储设备,它将写入内部存储器. Windows IOT与您所知道的Windows相距不远,因为Windows中还有一个Documents文件夹.如果未连接任何外部存储设备,则这将是一个后备位置.如果将数据写入Documents文件夹,则可以通过网络连接进行浏览,这不是我想要做的,但是对感兴趣的人可用.但是,我没有做任何事情来处理设备内存已满的情况.如果您打算自己使用此解决方案并且没有足够的可用内存,则将要解决此问题.
将数据导入Azure很简单.将要保存的数据放入JSONstring
中之后,我使用Azure客户端库发送JSON消息.当我遍历代码时,您将看到它是如何工作的.
实作
使用Visual Studio 2017创建一个新项目.项目类型将是空白的UWP项目.我将其命名为" AutoTelemetry",这将在整个代码中得到体现.创建项目后,右键单击它,然后选择"新建文件夹".创建一个名为" ViewModels"的文件夹.这里需要添加一些基础代码.如果您熟悉MVVM模式,那么此代码将是基本的.此处不解释MVVM模式,因为已经有大量关于它的信息.右键单击ViewModels文件夹,然后选择"添加和新建项".对于项目的类型,选择Class并将其命名为ViewModelBase
.cs. ViewModelBase
的内容如下:
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;
}
}
在某些地方this类可能不符合规范.一种是在类中存在一个" CoreDispatcher".我将this放在这里,因为将有从其他线程修改的类,并且更改事件必须是在主线程上引发. SetValueIfChanged方法可能看起来最不熟悉.我已经厌倦了键入重复的代码模式,并且实现了一些缩短代码的方法.我已经在另一篇文章中写过[this](https://j2inet.blog/2018/07/22/simplified-uwp-view-mode-base/)的开发方式.有关说明,您可以阅读[this](https://j2inet.blog/2018/07/22/simplified-uwp-view-mode-base/). 添加另一个名为
EngineState`的类.此类具有许多属性,并继承自ViewModelBase.它的属性全部使用相同的模式实现.以下是一些属性的实现,以显示模式.
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();
}
}
添加另一个名为MainViewModel的类.这个类也应该继承自ViewModelBase.我们将在此类中添加大部分代码.
namespace AutoTelemetry.ViewModels
{
public class MainViewModel : ViewModelBase
{
// implementation to come later
}
}
微软基于XAML的UI技术中使用的另一个著名的类集是DelegateCommand
类.这些类可以在其他一些工具箱中找到.但是,由于这些是我要使用的工具箱中唯一的类,因此在这里包括了这些类,而不是引用了该工具箱.该课程还提供了大量信息.
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);
}
}
}
现在是时候进入解决方案的代码了.在UWP应用程序中,必须具有访问某些硬件和功能所需的功能声明.这背后的部分想法是为了确保应用程序商店中发布的应用程序的安全性.用户可以在安装该应用程序之前看到该应用程序请求访问的计算机的哪些功能,以便更明智地决定是否安装它.如果您看到也需要访问您的联系人的小费计算器,则可能是发生了一些可疑的事情.尽管此应用程序在应用商店中看不到日光,但仍受某些相同规则的约束.要访问Internet和Bluetooth适配器,需要进行声明.没有这些声明,您将得到一些奇怪的错误,这些错误可能会令人困惑.除了让您了解这些错误之外,我还将指导您完成现在进行的必要声明. Visual Studio具有用于处理声明的UI.尽管发现它有用,但我发现它没有暴露进行访问RFCOMM的声明所需要的内容. 右键单击名为Package.appxmanifest的项目中的文件,然后选择"查看代码".将打开一个XML文档.文档中将有一个带有元素的部分.它已经定义了"互联网"功能.更改此部分,使其外观如下所示:
<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>
这将使应用程序访问蓝牙RFCOMM和位置信息.该代码将需要扫描计算机上的可用设备,以找到连接至汽车的设备.我在这里硬编码了一段知识;我知道该设备名为" ODBII".当我从扫描中收到许多设备时,我选择了一个名为" ODBII"的设备并继续进行操作.对.
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);
//Other code to respond to the discovered hardware goes here
}
}
这可行,但是有一个主要问题.太慢了!如果过一会儿再在应用程序中运行它,它将最终找到蓝牙硬件.该扫描唯一需要的是蓝牙适配器的硬件ID.与设备建立成功连接后,无需每次都扫描一次,而是保存设备的ID.我正在做的伪代码如下所示:
IF(SavedDeviceIDFound)
var connected = TryConnectToDevice(DeviceID);
if(connected) return;
END IF;
DeviceID = SearchForDeviceID();
var connected = TryConnectToDevice(DeviceID)
IF(connected)
SaveDeviceID(DeviceID);
END IF;
我使用LocalSettings API保存设备ID.您可以在[CodeProject]上阅读有关其工作原理的更多信息(https://www.codeproject.com/Articles/1109667/Data-Storage-with-UWP#localsettings).一旦连接到设备,我就获得了到设备的流,以构造数据读取器和数据写入器以与设备进行交互.数据读取器和写入器彼此独立使用.
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);
//These following three methods kick off processes that
//run on their own threads.
ReceiveLoop();
QueryLoop();
StatusUpdateLoop();
await this.Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
IsConnected = true;
//Send any initialization messages here
});
}
}
在连接新方法的方法的末尾调用了三个方法.一个线程发送消息以请求各种读数(" QueryLoop()").它的操作是相当独立的,对其他所有事情都视而不见.另一个线程用于处理传入的数据并将其传递到解析器(" ReceiveLoop()").设备可能每秒查询信息几次.但是我想以较低的采样率保存引擎的状态. " StatusUpdateLoop()“将以某种频率获取当前读数的累积并进行存储. 接收数据时,不能保证可读取的数据是完整的消息.必须缓冲传入的数据,然后在已知该数据是完整响应后对其进行处理.完整的响应由换行符分隔.一旦检测到换行符,就处理缓冲的数据并清除缓冲区,以便继续进行数据累积.
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));
}
}
});
}
实现处理数据的方法的一种自然方法是弄清楚正在发送哪种类型的数据,然后"那么"具有较大的” switch"语句或" if"/“然后"列表来将接收到的数据分配给右边.数据对象上的属性.但这对于相对简单的东西来说太多了.我选择的路线不太重复.相反,在将十六进制字符串转换为字节数组之后,我利用了整数和"枚举"值之间的轻松转换.我也有字典映射的枚举值及其关联的属性.如果必须解析其他属性,则只需要确保有一个” Engine"属性,一个" enum"定义以及该属性和值的映射即可.
_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)));
返回到输入数据流的处理,从十六进制字符串转换为字节数组后处理数据的方法很简单.
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);
}
}
}
数据查询也很简单.我制作了一个包含要查询的属性的数组,并遍历它们,一次发送每个属性的查询消息.
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);
}
}
);
}
我尚未定义的最后一个循环是StatusUpdateLoop
(). " EngineState"类具有名为" LastUpdated"的属性,该属性是该类实例中的值最后一次更新的时间戳. StatusUpdateLoop
每5秒检查一次该类.它检查以确保当前对象状态上的时间戳记比上次读取对象状态上的时间戳记更新.如果是,则捕获对象状态并将其发送到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仪表板
我打算将此应用程序作为无头程序运行.但是我要添加UI的元素.这有什么目的?它提供调试信息.我想添加图形仪表,但是我不想花很多时间自己实现这些仪表.有一个包含仪表的UWP工具包,但是我遇到了使它们无法工作的问题.我最终使用了Telerik UI工具包.它包含径向和线性量规(易于使用!).这些仪表内置了动画支持.在添加对Telerik库的引用并将控件放到页面中之后,可以通过XAML配置控件,并通过XAML数据绑定设置值.管理量规的唯一代码是它们的声明.我不是要通过UI公开所有值.我的汽车的内置仪表板上也只显示了少数几个.转速,速度和燃油水平.
<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"/>
Azure设置
我正在使用Azure IOT集线器从设备中获取信息.我已经有一个Azure帐户.免费帐户将用于测试.在这里,我不讨论设置新帐户的过程.但是,如果您访问Azure网站,则会看到获取免费帐户的说明.进入帐户后,一个人可以采取的选择和行动的数量可能令人生畏.我将不会探讨所有这些选项,而是会专注于所需的内容.对于此项目,我想设置一个资源组,其中使用的其他资源将存在.这不是完全必要的,但是可以简化组织. 登录帐户后,在左侧菜单上,单击标题为"资源组"的项目.当"资源组"屏幕打开时,单击"添加".输入资源组的名称,然后选择"创建".
必须创建Windows AzureIoT
集线器资源.从左侧菜单中选择``创建资源''.在打开的屏幕中,在搜索窗口中键入" IoT"以缩小选项.选择" IoT"集线器,然后单击"创建".在打开的屏幕中,选择使用现有资源组的选项.选择您创建的资源组,并为IoT Hub实例命名,然后选择Review + Create.在下一个屏幕上,检查您的输入,然后选择"创建". IoT资源的创建将花费几分钟.如果单击通知图标(形状像铃铛),则可以查看创建过程的进度.
创建资源后,可以通过从左侧菜单中选择"所有资源",然后在出现的屏幕中选择刚创建的资源来使用它.我们需要为IoT设备创建记录(设备是唯一标识的).选择新创建的IoT中心实例后,从菜单中单击物联网设备,然后选择添加.为设备命名,然后选择保存.创建设备后,您可以在" IoT设备"屏幕中单击它以获取特定设置,例如设备的连接字符串.
下一个要创建的资源是一个存储传入数据的地方.单击创建资源,然后从可能创建的资源列表中选择Data Lake Storage Gen1.请记住,您可以通过在顶部窗口中键入资源名称来过滤选项.再次对于资源组,选择我们创建的现有组,并给数据湖存储命名,然后选择创建.同样,创建新资源将需要几分钟.
创建完成后,我们将获得一个设备记录和与真实(物理)设备共享的凭据.单击设备,我看到设备特定的连接"字符串".现在,我将string
嵌入代码中.
DeviceClient _iotDeviceClient;
DeviceClient IotDeviceClient
{
get
{
return _iotDeviceClient ?? (_iotDeviceClient =
DeviceClient.CreateFromConnectionString(DeviceConnectionString, TransportType.Http1));
}
}
我们有一个可以放置其数据的存储位置.但是我们还没有办法将数据从设备获取到存储.为此,我们需要一个流分析作业.使用"创建资源"选项创建新作业.
创建作业后,浏览至该作业.在使用它之前,还有其他属性需要更改.单击输入设置.对于输入,选择添加流输入,然后选择IoT中心作为输入源.系统将提示您输入名称来别名输入.输入名称,并确保选择JSON作为序列化格式.
还必须定义输出.单击输出菜单项,然后选择创建.系统将询问您所使用的输出类型.选择" Azure数据湖".给输出命名.在"路径前缀模式"下,必须输入一种模式以了解用于保存日志的文件夹结构.我正在使用路径engine/ecu/{date}.当有要处理的数据时,{date}将被替换为当前日期.由于我为"日期"格式选择了什么,因此将有一个用于年份的文件夹,一个用于月份的文件夹以及一个用于日期的文件夹.我将输出格式设置为JSON.选择所有这些之后,我单击"授权",稍等片刻,然后单击"保存". 要完成Stream Analytics作业,我们需要添加一个查询.由于目前暂不对数据执行任何转换,因此查询只需要允许数据通过即可.此处显示的默认查询即可.
至此,我们有了一个Azure配置,足以将引擎数据从汽车传输到数据湖以进行分析和收集.请注意,通过我们的配置流式传输的任何数据都将被临时保存.它会保留几天,然后才能删除.如果我们想将数据存档,这是足够的时间来获取数据并将其移至长期存储. 让我们重新访问状态循环更新中的一些代码.
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);
}
上面的代码获取包含引擎数据的对象的当前实例,并为要收集的其他数据创建一个新实例.注意,新实例被分配了一个调度程序.它是从另一个线程更新的,并且调度程序用于将INotifyPropertyChanged
事件发送回UI线程.对于刚刚被抓取的实例,我将其转换为JSON(对象的ToString()方法已被覆盖以在JavaScript中输出),并将其包装在Azure的Message对象中.然后使用Azure Client Toolkit中的DeviceClient.SendEventAsync将消息发送到Azure云.
运行代码
该项目已准备好运行.对于Internet连接,我的车上有一个专用的便携式蜂窝热点.我将它带入房屋,以便在将其连接到屏幕时将其与Windows IoT硬件配对.使用Microsoft IoT仪表板,我打开了设备的设置,并将应用程序设置为默认应用程序.现在,当设备通电时,我的应用程序将自动启动,连接到ODB II适配器,并开始收集要发送到云的数据.
好的,您正在收集数据,没有什么?
我可以在很多方面扩展我所拥有的解决方案.在项目的此阶段,我的主要目标是收集数据进行分析.最初的下一步是准备好一些东西,以便以一定的频率下载和存档引擎数据.如果您使用的是Raspberry Pi 3B,则可以考虑使用PiJuice. Windows 10 IoT尚未正式支持PiJuice,但源代码是公开的,乍一看,它易于使用. PiJuice包含电池,可在汽车熄火时保持设备开机.它具有内置的实时时钟,可以安排警报以唤醒设备.还可以添加其他传感器,例如加速度计,以很好地补充传感器数据.
历史
- 2019年8月8日
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# Windows .NET Dev Intermediate IoT Win10 新闻 翻译