嵌入式编程

使用 .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.comcyclistats.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 LawrencePatrick Butler Monterde