嵌入式编程
使用 .NET Micro Framework 连接的设备
Colin Miller
如今,包含连接设备的应用程序越来越普及。事实上,就端点数量而言,据估计“物联网”(即设备通过互联网连接)的规模已超过万维网,而且预计在未来几年内将呈数量级增长。
不远的将来,与我们打交道更多的将是智能设备,而非可识别的计算机。看看您家中。能连接的有用物品包括电器(能量管理、软件更新及维护)、汽车(协调通过输电网为您的新电动车充电、自动检验及保养、软件更新)、灌溉系统(根据天气预报和水管理情况安排喷灌)、宠物(确定其位置、设置隐形障碍)、恒温调节开关(远程控制),等等。
这些设备可相互连接,可连接到智能控制器、路由器以及云。这对于 Microsoft .NET Framework 开发人员意义何在?目前,.NET 开发人员可以为小型设备所连接系统的所有部分开发应用程序。借助 .NET Micro Framework,.NET 开发人员可以开发出下至小型设备的整个系统。
.NET Micro Framework 是 .NET Framework 专门针对最小型设备的嵌入式编程需求的实现。为了最小化占用空间,它无需基础 OS,可直接运行于硬件上。有关常规信息,请参见 microsoft.com/netmf。另外,项目的开源社区网址如下:netmf.com。
数月前,我在 .NET Micro Framework 博客 (blogs.msdn.com/b/netmfteam) 上开始连载文章,介绍如何只使用 .NET Micro Framework、Visual Studio 和最少量的电子器件从头建立某个小型设备,如自行车计算机。我希望以此展示 .NET Framework 开发人员是如何为小型设备创建丰富应用程序的。(连载第一篇文章的链接:tinyurl.com/2dpy6rx。)图 1 显示了实际的计算机。我之所以选择自行车计算机,是因为提到自行车这个领域,几乎人人都是专家。该应用程序包括一个基于手势的 UI,可支持多个传感器并解决诸如电池电源管理等问题。
图 1 NETMF 自行车计算机
博客中讨论了每项功能的实现,项目代码可参见 CodePlex:netmfbikecomputer.codeplex.com/。不过,我把最精彩的部分留到了最后,在本文中,我将通过 Wi-Fi 将此设备连接到 Microsoft Windows Azure 承载的 Web 服务。设想以下场景:您刚刚完成了骑行,正把自行车送回车库。您的计算机中包含了骑行过程中收集的数据:距离、速度、节奏、坡度、时间等等。您翻到数据视图,并按下“上载”按钮。您的骑行数据将上载到云,在此与您的其他所有数据汇总,然后与朋友们分享。
本文的重点在于如何进行连接并上载数据,而不是您可能连接的云服务的形式。请浏览 bikejournal.com 和 cyclistats.com 等例子,了解如何跟踪您的骑车进度并与朋友们展开竞赛。我会让您知道进行连接是多么简单的事。
首先,一点背景知识
Web 服务模式可支持各种设备和服务交互方式(哪怕在创建应用程序时也可能不完全清楚),因此是连接设备的上佳之选。在设备端,我们使用完整 Web 服务基础结构的一个子集,名为 Web 服务设备配置文件 (DPWS)。有关其详细信息,请参见 en.wikipedia.org/wiki/Devices_Profile_for_Web_Services。DPWS 被视为联网设备的通用即插即用 (UPNP)。在 .NET Micro Framework 中,DPWS 支持 WS-Addressing、WS-Discovery、WS-MetaDataExchange 和 WS-Eventing 接口,建立在 SOAP、XML、HTTP、MTOM 和 Base64 编码基础技术之上。
借助 DPWS,您可以连接到客户端设备(即使用其他设备所提供服务的设备),或服务器设备(即为其他设备提供服务的设备),或同时连接二者。您可以通过元数据协商所提供的服务和所使用的服务,可以发布和订阅其他实体的变化通知。图 2 显示了 DPWS 堆栈。
图 2 DPWS 堆栈
.NET Micro Framework 的实现支持 DPWS 1.0 版(与 Windows 7 兼容)和 DPWS 1.1 版(与 Windows Communication Foundation (WCF) 4 兼容)。您可以指定连接所用的绑定,如通过 HTTP 发送 SOAP (ws2007HttpBinding),或想要支持的自定义绑定。
我们的自行车计算机应用程序确实非常简单——只需将数据上载到云。实际上 DPWS 可以实现更多功能。例如,假设我的公用事业公司安装了智能服务仪表,以限制任意时刻我能使用的资源。除此之外,我安装了本地能量管理服务,以控制如何使用这些有限的资源。我可以设置优先级和使用原则,让系统决定如何限制消耗,例如,淋浴热水具有高优先级。
然后我外出购买了一台新的洗碗机。我把它带回家,并插上插头。在后台,洗碗机找到本地网络,并“发现”管理服务。它会将各种状态下的功率消耗以及使用规则告知服务。稍后,当洗碗机正在运转时,我要进行淋浴,该来热水了。但是,我开动洗碗机后不想让它停机,以免盘子上的食物变硬。为了减少总能耗,管理服务告知洗碗机每隔 15 分钟冲洗一次碗碟以保持湿润,并在我结束淋浴后从洗碗周期停顿处重新开始。正如您所见,以上整个场景都可支持具备 DPWS 中所定义功能的任意端点集。
足够的背景:让它运行
Wi-Fi 无线电的配置非常简单。有两种方式:一种是使用 MFDeploy.exe 实用程序,还有一种是使用 GHI SDK(由 GHI Electronics 提供)支持的可编程接口。下面先采用 MFDeploy.exe,它是 .NET Micro Framework 附带的一个工具,位于 SDK 安装的工具部分。在“目标 | 配置 | 网络配置”对话框(请参见图 3)中,启用 DHCP,选择安全机制,然后输入家庭网络的密码及其他配置信息。
图 3 MFDeploy 网络配置对话框
DHCP 将处理填写的网络设置的网关和 DNS 字段。该信息通过 NetworkInterface 类型及其子类型 Wireless80211 供托管应用程序所用。HTTP 堆栈在发送和接收字节时将隐式使用该信息,不过它可能还需要其他信息片段才能使用代理——某些您的网络可能需要的信息。为帮助 HTTP 堆栈正确使用代理,最好将以下代码添加到程序中,以提示在何处连接:
WebRequest.DefaultWebProxy =
new WebProxy("<router IP Adress>");
如果您的网络中没有明确的代理,那么通常可以默认使用网关地址。 通过 GHI 编程接口,您可以使用图 4 中所示的部分派生代码。
图 4 带 GHI 编程接口的 Wi-Fi 配置
// -- Set up the network connection -- //
WiFi.Enable(SPI.SPI_module.SPI2, (Cpu.Pin)2, (Cpu.Pin)26);
NetworkInterface[] networks = NetworkInterface.GetAllNetworkInterfaces();
Wireless80211 WiFiSettings = null;
for (int index = 0; index < networks.Length; ++index)
{
if (networks[index] is Wireless80211)
{
WiFiSettings = (Wireless80211)networks[index];
Debug.Print("Found network: " + WiFiSettings.Ssid.ToString());
}
}
WiFiSettings.Ssid = "yourSSID";
WiFiSettings.PassPhrase = "yourPassphrase";
WiFiSettings.Encryption = Wireless80211.EncryptionType.WPA;
Wireless80211.SaveConfiguration(
new Wireless80211[] { WiFiSettings }, false);
_networkAvailabilityBlocking = new ManualResetEvent(false);
if (!WiFi.IsLinkConnected)
{
_networkAvailabilityBlocking.Reset();
while (!_networkAvailabilityBlocking.WaitOne(5000, false))
{
if (!WiFi.IsLinkConnected)
{
Debug.Print("Waiting for Network");
}
else
break;
}
}
Debug.Print("Enable DHCP");
try
{
if (!WiFiSettings.IsDhcpEnabled)
WiFiSettings.EnableDhcp(); // This function is blocking
else
{
WiFiSettings.RenewDhcpLease(); // This function is blocking
}
}
catch
{
Debug.Print("DHCP Failed");
}
图 4 示例假设您使用的是 WPA 或 WPA2 安全机制。此外也支持 WEP。您首先要做的是确定无线电使用的 SPI 端口和控制线。该配置表示硬件,即 GHI 的 FEZ Cobra 板的连接。要做的是设置 WiFiSettings、保存配置,然后调用 EnableDHCP。请注意,其中某些调用被拦截,可能需要一些时间,因此您应当确保用户了解所发生的事情。此外,该编程接口中无法列举可用的网络,以便您可以从中选择。我在图 4 示例中硬编码了网络信息。
对于自行车计算机的商业实现,我还需要编写一个集成的 Wi-Fi 配置 UI,以显示在图 3 所示对话框中输入的信息,以及进入该对话框的屏幕键盘。我可能会在本文发表之前,抽时间在另一篇博客文章中完成此项任务。眼下我正撰写一篇关于“时间服务”的文章,介绍如何在启动计算机时使用 Wi-Fi 连接获取日期和时间,从而不用通过保持设备运行来维持该信息,或者让用户(我)在启动时输入该信息。
设置服务连接
现在只剩下 DPWS 实现了。请注意,我的重点在设备端。我使用的 Windows Azure 服务提供了简单的“Hello World”模板。我从此模板入手,通过编写 UpLoad 和 Get 操作以添加需要保存的字段及操作来扩展约定。该过程会创建一个服务,接受并存储我的数据,并将存储的最新数据返回给我。显然,完整的服务还需要更多工作,不过那是另一篇文章的主题。下面简要了解下为该服务创建的约定。ServiceContract 包含两个操作,DataContract 包含多个字段(请参见图 5)。
图 5 服务约定
[ServiceContract]
public interface IBikeComputerService
{
[OperationContract]
BikeComputerData GetLastComputerData();
[OperationContract]
void UploadBikeComputerData(BikeComputerData rideData);
}
// Use a data contract as illustrated in the sample below
// to add composite types to service operations.
[DataContract]
public class BikeComputerData
{
DateTime _Date;
TimeSpan _StartTime;
TimeSpan _TotalTime;
TimeSpan _RidingTime;
float _Distance;
float _AverageSpeed;
float _AverageCadence;
float _AverageIncline;
float _AverageTemperature;
bool _TempIsCelcius;
[DataMember]
public DateTime Date…
[DataMember]
public TimeSpan StartTime…
[DataMember]
public TimeSpan TotalTime…
[DataMember]
public TimeSpan RidingTime…
[DataMember]
public float Distance…
[DataMember]
public float AverageSpeed…
[DataMember]
public float AverageCadence…
[DataMember]
public float AverageIncline…
[DataMember]
public float AverageTemperature…
[DataMember]
public bool TemperatureIsInCelcius…
}
该约定的实际实现至少可支持自行车计算机示例(请参见图 6)。
图 6 约定实现
public class BikeComputerService : IBikeComputerService
{
static BikeComputerData _lastData = null;
public BikeComputerData GetLastComputerData()
{
if (_lastData != null)
{
return _lastData;
}
return new BikeComputerData();
}
public void UploadBikeComputerData(BikeComputerData rideData)
{
_lastData = rideData;
}
}
WSDL 定义服务
根据我创建的约定和架构,Windows Azure 服务会自动生成一个 Web 服务描述语言 (WSDL) 文件。其中包含服务的服务建模语言 (SML) 定义。有关 WSDL 文档的 W3C 规范,请参见 w3.org/TR/wsdl。其中定义了服务所支持的操作和消息。WSDL 在网站上存储为 XML 说明,连接到服务的任何人都可访问。我们的文件地址如下:netmfbikecomputerservice.cloudapp.net/BikeComputerService.svc?wsdl。图 7 显示了 WSDL 文件的局部快照,以便您初步了解,但是请记住,该文件是自动生成的,只能供其他程序使用。您从不需要编写这个复杂而难缠的 XML 文件。
图 7 WSDL 文件
可以看到,WSDL 中包括数据输入、输出消息,以及获取和上载数据操作的定义。有了所定义和发布服务的简单接口后,该如何对它进行编程?这同样易于反掌。
用 MFSvcUtil.exe 生成代码
桌面上有个名为 ServiceModel MetadataUtility Tool (SvcUtil.exe) 的实用程序,可以根据元数据文档(如 WSDL)生成服务模型代码,反之亦可。.NET Micro Framework 中也有类似的实用程序,MFSvcUtil.exe。该实用程序是一个命令行工具,最好运行在项目目录下。那么,我们在发布的 WSDL 规范中指定以运行该工具:
<SDK_TOOLS_PATH>\MFSvcUtil.exe http://netmfbikecomputerservice.cloudapp.
net/BikeComputerService.svc?wsdl
该工具将生成三个文件(请参见图 8)。
图 8 执行 MFSvcUtil.exe 命令
BikeComputerService.cs 文件包含消息中数据的定义、用于定义服务所支持操作的类(因为我们的设备是客户端),以及用于序列化和反序列化数据的多个帮助程序函数(请参见图 9)。
图 9 BikeComputerService.cs 文件
namespace BikeComputer.org
{
[DataContract(Namespace="http://tempuri.org/")]
public class GetLastComputerData ...
public class GetLastComputerDataDataContractSerializer : DataContractSerializer…
[DataContract(Namespace="http://tempuri.org/")]
public class GetLastComputerDataResponse ...
public class GetLastComputerDataResponseDataContractSerializer : DataContractSerializer…
[DataContract(Namespace="http://tempuri.org/")]
public class UploadBikeComputerData ...
public class UploadBikeComputerDataDataContractSerializer : DataContractSerializer…
[ServiceContract(Namespace="http://tempuri.org/")]
[PolicyAssertion(Namespace="https://schemas.xmlsoap.org/ws/2004/09/policy",
Name="ExactlyOne",
PolicyID="WSHttpBinding_IBikeComputerService_policy")]
public interface IIBikeComputerService ...
}
namespace schemas.datacontract.org.BikeComputerServiceWebRole...
BikeComputerClientProxy.cs 文件包含 Web 服务的代理接口:
namespace BikeComputer.org
{
public class IBikeComputerServiceClientProxy : DpwsClient
{
private IRequestChannel m_requestChannel = null;
public IBikeComputerServiceClientProxy(Binding binding,
ProtocolVersion version) : base(binding, version)...
public virtual GetLastComputerDataResponse
GetLastComputerData(GetLastComputerData req) ...
public virtual UploadBikeComputerDataResponse
UploadBikeComputerData(UploadBikeComputerData req) ...
}
}
MFSvcUtil.exe 创建的第三个文件为 BikeComputerServiceHostedService.cs 文件,其中包含运行于接口服务端之上的接口逻辑。 在本例中,WSDL 是根据创建的服务和数据约定生成的,因而不需要该文件。 该文件包括您获取所发布的 WSDL 并希望根据其复制服务,或想要在其他设备上运行服务的方案。 请记住,设备可以是客户端、服务器或二者皆是。 令设备为其他设备提供服务这种选择,使某些有趣的应用程序成为可能。 以下是 BikeComputerServiceHostedService 包含的内容:
namespace BikeComputer.org
{
public class IBikeComputerServiceClientProxy : DpwsHostedService
{
private IIBikeComputerService m_service;
public IBikeComputerService(IIBikeComputerService service,
ProtocolVersion version) : base(version) ...
public IBikeComputerService(IIBikeComputerService service) :
this(service, new ProtocolVersion10())...
public virtual WsMessage GetLastComputerData(WsMessage request) ...
public virtual WSMessage UploadBikeComputerData(WsMessage request) ...
}
}
上载数据
如您所见,到目前为止所有的设备应用程序代码都是根据 WSDL 自动生成的。 那么,您实际上应当在客户端上编写什么代码来连接到服务、发布数据,然后读回数据以确保数据已收到? 只需寥寥几行。 以下是我为自行车计算机项目编写的代码。
在 RideDataModel 类中,我在构造函数中添加了以下代码,以设置 DPWS 连接:
public RideDataModel()
{
_currentRideData = new CurrentRideData();
_summaryRideData = new SummaryRideData();
_today = DateTime.Now; //change this to the time service later.
//--Setup the Web Service Connection
WS2007HttpBinding binding = new WS2007HttpBinding(
new HttpTransportBindingConfig(new Uri
("http://netmfbikecomputerservice.cloudapp.
net/BikeComputerService.svc")
));
m_proxy = new
IBikeComputerServiceClientProxy(
binding, new ProtocolVersion11());
_upload = new
UploadBikeComputerData();
_upload.rideData = new
schemas.datacontract.org.
BikeComputerServiceWebRole.
BikeComputerData();
}
接下来,我在该类中创建了一个方法,以将数据上载到 Web 服务。 此例程专为我的骑行摘要数据而设计,会将该数据放入 Web 服务架构的字段中(在 WSDL 中引用并体现在 BikeComputerService.cs 中)。 然后,我采用上载数据调用代理的 UploadBikeComputerData 方法,在 Web 服务上检索最新的骑行日期,以验证我的数据是否已收到(请参见图 10)。
图 10 将数据上载到 Web 服务
public bool postDataToWS()
{
//-- Load the ride summary data into the upload fields --//
_upload.rideData.AverageCadence = _summaryRideData.averageCadence;
_upload.rideData.AverageIncline = _summaryRideData.averageIncline;
_upload.rideData.AverageSpeed = _summaryRideData.averageSpeed;
_upload.rideData.AverageTemperature =
_summaryRideData.averageTemperature;
_upload.rideData.Date = _summaryRideData.rideDate;
_upload.rideData.Distance = _summaryRideData.distance;
_upload.rideData.RidingTime = _summaryRideData.ridingTime;
_upload.rideData.StartTime = _summaryRideData.startTime;
//-- Upload the data --//
m_proxy.UploadBikeComputerData(_upload);
//-- Validate the upload by retrieving the data and comparing --//
GetLastComputerData req = new GetLastComputerData();
GetLastComputerDataResponse back = m_proxy.GetLastComputerData(req);
if (back.GetLastComputerDataResult.Date == _upload.rideData.Date)
{
return false;
}
return true;
}
此处假定您每天只骑一次车,如果一天内骑多次,则需要更改比较逻辑。我希望在自行车计算机上使用“暂停”功能,并将一天内的所有骑行视为一个数据集。在我上一篇博客文章中讲到,我已经可以将数据文件保存在自行车的 SD 卡中。接下来我将添加新功能,跟踪哪些骑行数据集已发布到 Web 服务上,并发布遗漏的任何数据集。这样一来,即使我偶尔超出了无线连接范围,也仍可以在稍后更新 Web 服务。另一项改进之处是,当家庭网络不可用时连接到 PC 作为中介。
所以,我在应用程序中编写了 17 行代码(其中大多数用于将摘要数据映射到服务架构字段),我将数据上载到服务并执行了有效性检查。真不错。
现在结束骑行连接
当我结束骑行,回到车库时,可以用简单的手势导航至“保存数据”屏幕,并将数据发布到云。
结束后还可以做一些完善工作,让数据更有用,但那不在本文讨论范围之内了。
嵌入式设备已逐渐成为一个专业技术领域,在低占用空间/低成本与高性能要求之间取得编程简易性和灵活性的折衷。我们越来越多地见到,小型设备连接到其他设备及网络,创造出令人瞩目的解决方案。与此同时,处理器和内存价格持续降低,使我们无需放弃强大的桌面工具和语言,从而可以开发出更丰富、更富价格竞争力的设备。.NET 程序员会发现,从事小型设备研发所需的技能和机会都已摆在面前。
Web 服务模型为这些小型设备的连接提供了强大选择,因为通过元数据发现远程服务、订阅远程事件并交换服务信息使灵活连接大量设备成为可能。但代价是连接(使用 SOAP 和 XML)繁复,因此可能并不适用于所有情况。
自行车计算机项目表明,您可以通过 .NET Framework 编程技巧,为小型设备编写引人入胜的 UI、为各种传感器编写驱动程序并将这些设备连接到云。正如我所演示的,除定义 Web 服务之外,设备上运行的大部分代码都是由 MFSvcUtil.exe 自动生成的。要上载数据,只需添加数行代码。其他应用程序可能需要搜索 Web 服务 (WS_Discovery),或订阅其他端点上的事件 (WS_Eventing),或处理具不同功能的设备和服务 (WS_MetaDataExchange)。可以根据需要,将以上所有功能添加到基本数据交换模型中。
正如我一位朋友所说,.NET Framework 程序员现在可以在名片中加上“嵌入式程序员”这个头衔了。我衷心希望在 netmf.com 的讨论区中看到您采用 .NET 建立的设备,我还会在此讨论区或博客网站中回答您有关本文的任何问题。
祝您骑行愉快!
Colin Miller 的计算机职业生涯始于科学程序员,从事建立 8 位和 16 位实验控制系统的工作。除致力于小型设备之外,他在 PC 软件领域工作了 25 年(包括 15 年在 Microsoft),研究遍及数据库、桌面发布、消费品、Word、Internet Explorer、Passport (LiveID) 及联机服务。作为 .NET Micro Framework 的产品部经理,他最终(很高兴)将这些不同的职业经历糅合到了一起。
衷心感谢以下技术专家对本文的审阅:Jane Lawrence 和 Patrick Butler Monterde