UltraDynamo (第二部分) - 一切都与传感器有关
在本节中,我们将介绍传感器、传感器管理器以及模拟。
目录
注意:本文是多部分文章。下载内容可在第一部分中找到。
引言
在我最初开发应用程序时,我只是根据需要创建一个新实例,无论是用于窗体还是趋势图等。这对于启动应用程序的初始开发来说还可以,但很快就证明这是一种效率低下且设计糟糕的方法。幸运的是,Pete O'Hanlon 与我分享了他作为比赛参赛作品的一部分开发的传感器代码库,这让我对如何使用单例模式来正确地做事有了一些见解。
单例模式(阅读更多 此处)使用双重检查锁定机制来提供对各种传感器实例的访问,并防止在多线程环境中创建您试图维护单个实例的对象。
我回去重写了我的传感器代码,最终创建了一个传感器管理器,它提供了对所有传感器的访问。每个传感器都派生自一个具有通用功能的基传感器类,从而提高了代码的可重用性。
传感器模拟
由于我是在没有传感器的机器上开发的,所以我也想提供一种模拟传感器的方法,用于测试等目的。该模拟可以随时启用,覆盖真实的传感器值,从而允许用户(和开发人员)测试布局或组件,而无需实际拥有实时传感器值。
您可能希望模拟传感器值的另一个原因是测试您不必反复执行的功能,因为这可能会对车辆造成磨损,或者使人员暴露于不必要的反复风险。例如,反复以每小时 150 英里的速度测试软件功能最终会让你吃不消。所以,如果我们可以在不离开舒适办公桌的情况下完成所有初步测试,那么这对每个人来说都更安全(也更便宜)。
传感器管理器
传感器管理器提供了对各种传感器实例的访问。目前,我包含了以下传感器:
- 加速度计
- 指南针
- GPS(地理位置)
- 陀螺仪
- 倾斜仪
- 环境光
目前应用程序并未完全利用所有功能。例如,环境光传感器可以为仪表板传感器概述提供读数,但对其不做任何处理。我确实希望将来能利用环境光传感器,也许是为了改变显示器的颜色主题,以便进行白天或夜晚操作。
下面的代码是传感器管理器(MySensorManager
),目前的状态如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace UltraDynamo.Sensors
{
public sealed class MySensorManager
{
//Object for SynLocking
private static readonly object syncLock = new Object();
//Sensor Manager Instance
private static volatile MySensorManager instance;
//Sensor Instances
private MyAccelerometer accelerometer;
private MyCompass compass;
private MyGeolocation geolocation;
private MyGyrometer gyrometer;
private MyInclinometer inclinometer;
private MyLightSensor lightSensor;
//Constructor
private MySensorManager() { }
//Sensor Manager Property
public static MySensorManager Instance
{
get
{
if (instance == null)
{
lock (syncLock)
{
if (instance == null)
{
instance = new MySensorManager();
}
}
}
return instance;
}
}
//Sensor Properties
public MyAccelerometer Accelerometer
{
get
{
if (accelerometer == null)
{
lock (syncLock)
{
if (accelerometer == null)
{
accelerometer = new MyAccelerometer();
}
}
}
return accelerometer;
}
}
public MyCompass Compass
{
get
{
if (compass == null)
{
lock (syncLock)
{
if (compass == null)
{
compass = new MyCompass();
}
}
}
return compass;
}
}
public MyGeolocation GeoLocation
{
get
{
if (geolocation == null)
{
lock (syncLock)
{
if (geolocation == null)
{
geolocation = new MyGeolocation();
}
}
}
return geolocation;
}
}
public MyGyrometer Gyrometer
{
get
{
if (gyrometer == null)
{
lock (syncLock)
{
if (gyrometer == null)
{
gyrometer = new MyGyrometer();
}
}
}
return gyrometer;
}
}
public MyInclinometer Inclinometer
{
get
{
if (inclinometer == null)
{
lock (syncLock)
{
if (inclinometer == null)
{
inclinometer = new MyInclinometer();
}
}
}
return inclinometer;
}
}
public MyLightSensor LightSensor
{
get
{
if (lightSensor == null)
{
lock (syncLock)
{
if (lightSensor == null)
{
lightSensor = new MyLightSensor();
}
}
}
return lightSensor;
}
}
}
}
传感器
传感器在适用的情况下派生自一个基传感器对象。它包含传感器的通用功能。提供“可用”和“模拟”属性,用于更改这些属性的方法,以及一个 `abstract` 方法 `TriggerEvent()`,它必须在实际传感器对象中进行重写。下面的代码是 `MySensorBase` 对象。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace UltraDynamo.Sensors
{
/// <summary>
/// Contains the base properties, methods and objects that all My.... Sensors will derive from
/// </summary>
abstract public class MySensorBase
{
/// <summary>
/// Is the sensor in a simulated state
/// </summary>
public bool Simulated { get; set; }
/// <summary>
/// Is the real sensor reporting available (i.e. not null when instantiated
/// </summary>
public bool Available { get; set; }
//Events
public event EventHandler<SimulatedEventArgs> SimulatedChanged;
protected virtual void OnSimulatedChanged(SimulatedEventArgs e)
{
EventHandler<SimulatedEventArgs> handler = SimulatedChanged;
if (handler != null)
{
handler(this, e);
}
}
public event EventHandler<AvailableEventArgs> AvailableChanged;
protected virtual void OnAvailableChanged(AvailableEventArgs e)
{
EventHandler<AvailableEventArgs> handler = AvailableChanged;
if (handler != null)
{
handler(this, e);
}
}
// Methods
abstract public void TriggerEvent();
/// <summary>
/// Switch the simulated mode on or off.
/// </summary>
/// <param name="simulated">Switch on (TRUE) or off (FALSE) simulation mode</param>
public void setSimulated(bool simulated)
{
Simulated = simulated;
//rasie event
OnSimulatedChanged(new SimulatedEventArgs(Simulated));
TriggerEvent();
}
/// <summary>
/// Set the status flag for availability of underlying sensor
/// </summary>
/// <param name="available">True == Available, False != Available</param>
public void setAvailable(bool available)
{
Available = available;
//raise event
OnAvailableChanged(new AvailableEventArgs(Available));
TriggerEvent();
}
}
}
现在我们来看一个传感器,就可以更好地了解情况了。下面的代码是 `MyLightSensor` 对象,这是最简单的传感器。
在代码顶部,您会看到该对象派生自 `MySensorBase`。然后是一些 `public` 属性,它们保存传感器的当前使用值、传感器的模拟值以及真实世界传感器的真实原始值。还有另外两个属性用于保存传感器的最小值和最大值(目前未使用,但当我们需要将趋势或显示值限制在最小/最大范围时,它们会派上用场)。
`TriggerEvent()` 方法用于将所需数据打包到 `EventArgs` 对象中,然后与事件一起传递给已订阅这些事件的所有对象。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Sensors;
namespace UltraDynamo.Sensors
{
public class MyLightSensor : MySensorBase
{
//Object for SynLocking
private static readonly object syncLock = new Object();
public float LightReading {get; private set; }
public float rawLightReading {get; private set; }
public float simLightReading {get; private set; }
public float Minimum { get; set; }
public float Maximum { get; set; }
//Source Sensor
private LightSensor lightSensor;
//Events
public event ChangeHandler LightReadingChange;
public delegate void ChangeHandler(MyLightSensor sender, LightReadingEventArgs e);
//default update interval (milliseconds)
private uint defaultUpdateInterval = 1000;
//Pseudo Initial Event Timer
System.Timers.Timer forceIntialUpdate;
public MyLightSensor()
{
//Base sensor
lightSensor = LightSensor.GetDefault();
Minimum = 0;
Maximum = 10000;
if (lightSensor != null)
{
setAvailable(true);
setSimulated(false);
//Set update interval
lightSensor.ReportInterval = defaultUpdateInterval;
EventHandling(true);
}
else
{
setAvailable(false);
setSimulated(true);
}
//Pseudo initial event
forceIntialUpdate = new System.Timers.Timer(500);
forceIntialUpdate.Elapsed += forceIntialUpdate_Elapsed;
forceIntialUpdate.Start();
}
void forceIntialUpdate_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
//Force the initial event
TriggerEvent();
//Stop and dispose of the timer as no longer required.
forceIntialUpdate.Stop();
forceIntialUpdate.Dispose();
}
void lightSensor_ReadingChanged(object sender, LightSensorReadingChangedEventArgs e)
{
rawLightReading = e.Reading.IlluminanceInLux;
if (!Simulated)
{
setLightLevel(rawLightReading);
}
TriggerEvent();
}
public override void TriggerEvent()
{
var args = new LightReadingEventArgs();
args.Available = this.Available;
args.Simulated = this.Simulated;
args.LightReading = this.LightReading;
args.RawLightReading = this.rawLightReading;
args.SimLightReading = this.simLightReading;
if (LightReadingChange != null)
{
LightReadingChange(this, args);
}
}
private void setLightLevel(float value)
{
LightReading = value;
}
public void setSimulatedValue(float value)
{
simLightReading = value;
if (Simulated)
{
setLightLevel(simLightReading);
}
TriggerEvent();
}
/// <summary>
/// Set the sensor to highspeed
/// </summary>
public void setUpdateIntervalToMinimum()
{
if (lightSensor != null)
{
EventHandling(false);
lightSensor.ReportInterval = lightSensor.MinimumReportInterval;
EventHandling(true);
}
}
/// <summary>
/// Set the sensor to a default update interval
/// </summary>
public void setUpdateIntervalDefault(uint milliseconds)
{
defaultUpdateInterval = milliseconds;
if (lightSensor != null)
{
EventHandling(false);
lightSensor.ReportInterval = defaultUpdateInterval;
EventHandling(true);
}
}
/// <summary>
/// Return the minimum update interval that the sensor provides
/// </summary>
/// <returns>uint - milliseconds</returns>
public uint getMinimumUpdateInterval()
{
if (lightSensor != null)
{
return lightSensor.MinimumReportInterval;
}
else
{
return 1000;
}
}
/// <summary>
/// Return the currently set update interval
/// </summary>
/// <returns></returns>
public uint getCurrentUpdateInterval()
{
if (lightSensor != null)
{
return lightSensor.ReportInterval;
}
else
{
return 1000;
}
}
/// <summary>
/// Set the update interval of the sensor
/// </summary>
/// <param name="milliseconds"></param>
public void setUpdateInterval(uint milliseconds)
{
if (lightSensor != null)
{
if (milliseconds > getMinimumUpdateInterval())
{
EventHandling(false);
lightSensor.ReportInterval = milliseconds;
EventHandling(true);
}
}
}
private void EventHandling(bool enable)
{
if (enable)
{
lightSensor.ReadingChanged += lightSensor_ReadingChanged;
}
else
{
lightSensor.ReadingChanged -= lightSensor_ReadingChanged;
}
}
}
}
EventArgs
对于我使用的每个传感器,我都创建了一个相关的 `EventArgs` 类,它派生自基类 `EventArgs`。这用于打包我想要使用的数据,然后可以在引发事件时将这些数据与事件一起传递。对于上面代码中的传感器,光传感器数据在 `LightReadingEventArgs` 中传递。该类的代码如下所示:
public sealed class LightReadingEventArgs : EventArgs
{
public bool Available { get; set; }
public bool Simulated { get; set; }
public float LightReading { get; set; }
public float RawLightReading { get; set; }
public float SimLightReading { get; set; }
public LightReadingEventArgs()
: base()
{ }
}
如您所见,传递的数据包含传感器是否可用以及传感器是否被模拟,以及正在使用的光读数,以及来自传感器的原始读数和正在注入的模拟读数。
伪事件 - 这是做什么用的?
如果您查看 `MyLightSensor` 构造函数,我生成了一个设置为 500ms 的计时器,然后启动该计时器。
原因很简单。在整个应用程序、GUI 等中,都会等待底层传感器中的数据更改事件发生,并使用 `EventArgs` 中的数据来更新 GUI。此伪事件有两个目的。第一个是,而不是在应用程序各处编写代码来设置初始显示等,我只需发送一个带有基本数据的事件,这可以减少窗体中的初始化代码量。第二个原因是,底层传感器不会在实际发生数据更改之前引发事件。例如,在发生实际移动之前,`Accelerometer` 不会引发任何事件。因此,为了在更新显示之前不等更改发生,我使用带有虚拟数据的伪事件触发。您不能在构造函数仍在执行时引发事件,因为此时还没有任何东西最终构建完成,因此需要使用计时器。500ms 的持续时间还确保了剩余的构造函数代码有时间执行,然后计时器才会触发(否则您会收到异常)。
在计时器事件处理程序中,我使用带有虚拟数据的传感器更改事件进行触发,然后停止计时器并将其处置,因为它不再需要了。
更新间隔
每个传感器都有一个可以设置的更新间隔。您不想将此值设置得太低,因为它会导致大量事件数据需要处理。同样,您也不想将其设置得太高,因为它会导致数据“不流畅”。在创建每个传感器实例时,我将其默认设置为一个看似合理的值,例如 1 秒(1000ms)。当应用程序运行时,用户还可以配置默认更新间隔。每当更改更新间隔时,代码还会检查其是否小于传感器 API 的内部最小值。
在更改更新间隔时,需要注意的一个重要因素是,在更改间隔之前禁用任何事件处理程序,然后在之后重新启用它们,这一点非常重要。我不确定为什么必须这样做,但 API 中有这样的说明。
如果传感器不可用怎么办?
当构造函数从运行时 API 获取真实传感器实例时,如果没有可用的传感器,API 将返回 `null`。如果发生这种情况,代码将自动将传感器设置为模拟模式并将其标记为不可用。
整合
如果我们查看主仪表板窗体上的传感器概述选项卡上使用的代码,我们可以看到传感器管理器、传感器和事件是如何组合在一起的。
在下面的代码中,我删除了所有其他传感器代码,仅保留了光传感器以提高清晰度。
在主窗体中,我们声明一个字段来包含传感器的引用。
public partial class FormMain : Form
{
MyLightSensor myLightSensor;
}
在 `FormMain` 的构造函数中,我们从传感器管理器获取传感器的实例并将其加载到 `private` 字段中。我们还附加了一个事件处理程序来处理 `LightReadingChanged` 事件。
public FormMain()
{
InitializeComponent();
//Light Sensor
myLightSensor = MySensorManager.Instance.LightSensor;
//Light Sensor
myLightSensor.LightReadingChange += MyLightSensor_LightReadingChange;
}
在 `MainForm` 的加载事件中,我们使用应用程序配置属性来更新光传感器的默认更新间隔。
private void FormMain_Load(object sender, EventArgs e)
{
//Light Sensor
numericLightSensorUpdate.Value = Properties.Settings.Default.LightSensorDefaultUpdateInterval;
myLightSensor.setUpdateIntervalDefault(Properties.Settings.Default.LightSensorDefaultUpdateInterval);
myLightSensor.setUpdateInterval(Properties.Settings.Default.LightSensorDefaultUpdateInterval);
最后,我们提供了当事件处理程序触发时执行的方法。
void MyLightSensor_LightReadingChange(MyLightSensor sender, LightReadingEventArgs e)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new MethodInvoker(delegate()
{ MyLightSensor_LightReadingChange(sender, e); }));
return;
}
checkLightSensorAvailable.Checked = e.Available;
checkLightSensorSimulated.Checked = e.Simulated;
labelLightReading.Text = e.LightReading.ToString("#0.00");
labelLightSensorUpdateInterval.Text = myLightSensor.getCurrentUpdateInterval().ToString();
labelLightSensorUpdateIntervalMinimum.Text = myLightSensor.getMinimumUpdateInterval().ToString();
}
一个非常重要的因素是,会检查跨线程调用,以确定是否需要针对将要执行它的线程调用代码。最初,我遇到了很多跨线程问题,直到我进行了这些 `InvokeRequired`/`BeginInvoke`/`MethodInvoker` 调用。
传感器概述
在应用程序窗口中,有一个选项卡提供了应用程序监控的 `sensor` 的状态概述和当前值。
管理模拟
下面的窗体是用于模拟光 `Sensor` 的窗体。
public partial class FormSimulateLightSensor : Form
{
MyLightSensor myLightSensor;
public FormSimulateLightSensor()
{
InitializeComponent();
//myLightSensor = new MyLightSensor();
myLightSensor = MySensorManager.Instance.LightSensor;
trackSimulateValue.Minimum = (int)myLightSensor.Minimum;
trackSimulateValue.Maximum = (int)myLightSensor.Maximum;
myLightSensor.LightReadingChange += MyLightSensor_LightReadingChange;
checkSimulateEnable.Checked = myLightSensor.Simulated;
}
void MyLightSensor_LightReadingChange(MyLightSensor sender, LightReadingEventArgs e)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new MethodInvoker(delegate()
{ MyLightSensor_LightReadingChange(sender, e); }));
return;
}
checkAvailable.Checked = e.Available;
checkSimulated.Checked = e.Simulated;
checkSimulateEnable.Checked = e.Simulated;
labelRawValue.Text = e.RawLightReading.ToString("#0.00");
labelUsedValue.Text = e.LightReading.ToString("#0.00");
labelSimulatedValue.Text = e.SimLightReading.ToString();
trackSimulateValue.Value = (int)e.SimLightReading;
}
private void trackSimulateValue_ValueChanged(object sender, EventArgs e)
{
myLightSensor.setSimulatedValue((float)trackSimulateValue.Value);
}
private void checkSimulateEnable_CheckedChanged(object sender, EventArgs e)
{
myLightSensor.setSimulated(checkSimulateEnable.Checked);
}
}
您可以看到上面的代码,我们创建了一个本地 `private` 字段来保存对光 `sensor` 的引用,并从 `SensorManager` 获取对 `sensor` 的引用。还建立了一个处理程序,以便在 `sensor` 值更改时执行。
当调整滑块时,会调用 `sensor` 的 `public` set simulated 方法,将值推回 `sensor` 对象。
该窗体还有一个复选框,用于更改 `sensor` 是在模拟模式还是真实模式下运行。
传感器融合
传感器融合是英特尔创造的一个术语,指的是使用多个 `sensor` 来提供更详细或更有用的信息。在此处阅读相关内容:[^]。
在此应用程序中,我应用了我自己的 `sensor` 融合。在上面的仪表板显示中,您会注意到右侧的刻度指示器显示净马力。这是一个基于以下因素计算得出的数值:
- 车辆重量
- 加速度和
- 车速
如果您参考 维基百科文章 并查看牵引马力的计算,您可以看到它们之间的关系。
詹姆斯·瓦特在 18 世纪引入了这个功率单位,他说 1 马力是将 550 磅的重物在 1 秒内提升所需的功率。可以使用显示的参数和 Ultrabook 传感器的数据来计算这一点,例如:
Net HP = (Weight (in lbs) x Acceleration (g) x Speed (mph) ) / 375
重量由用户在应用程序的配置页面提供,其他数据来自传感器。375 是一个常数,它是将 550 磅/秒转换为英里/小时的转换得出的。
很容易为传动系统损耗和阻力系数添加计算常数,但这将是另一天的工作!
已知的传感器问题
在开发过程中,我们中的几个人注意到地理位置未提供准确的位置数据,或未提供任何高度或速度值。在 Intel 的论坛上发布各种帖子以及在其支持网站上来回发送多封电子邮件后,Intel 最终承认传感器软件存在故障,但在此版本的开发平台中,该故障不会得到修复。这让我陷入了困境,因为一些计划中的功能依赖于 GPS 功能。
该平台实际提供的是基于 WiFi 信号的三角定位数据,这对我来说并不合适。
配置选项
用户可以设置各种配置选项,即默认传感器更新间隔、车辆和乘员体重(如马力计算器所用)。可以从配置选项卡访问配置。
进入下一部分
在下一节中,我们将重点讨论实时趋势。