IoT Dash - 设备发现、命令和数据通道
在您的本地网络中发现和控制物联网设备。将设备数据通道传输到您的网站、SQL Server、Azure 表存储和 Azure 事件中心。
引言
IoT-Dash 将使您能够快速实现设备发现,用于任何支持 .NET Micro Framework 或 Node.js 的物联网平台的原型设计。只需几行代码和一个可以轻松扩展以满足您需求的简单协议。核心应用程序是一个运行在 Windows 上的 WPF 应用程序。
背景
在阅读了现有的物联网和通用设备发现标准后,我很好奇如何实现自己的。为什么?首先是为了学习体验,其次是需要一些简单的方法来快速原型化和测试本地网络上的设备发现。目标是让设备在网络上自我传播,所需的代码量和配置最少。另一个原因是,我想控制我的设备做什么,并能够以统一且可扩展的方式可视化它们的传感器(或任何有价值读取的数据)数据。当然,数据应该是可用于进一步分析的。这就是通道的作用所在。
IoTDash - 主应用程序
我想快速概述一下 IoTDash 本身。以下是它的外观和功能的屏幕截图:
主应用程序分为以下几个区域(从上到下):
- 首先是“文件”和“工具”菜单。
- 其次,发现和通道区域。
- 第三,设备区域。
文件菜单
“文件菜单”让您可以保存和加载传感器数据配置。一旦您保存了配置文件,您就可以为已发现的设备加载该配置。如果配置中的某个设备由于任何原因不可用,则与该特定设备相关的配置部分将不会被应用。
工具菜单
此菜单允许您定义应用程序启动时应加载的配置文件,以及是否应预加载任何内容。
这里也是您可以配置通信通道设置的地方,这些通道允许您(目前)将传感器数据保存(或发送)到:
- SQL Server(本地或 Azure,取决于您的连接字符串)
- 将事件数据推送到 SignalR 集线器。
- 将数据保存到 Azure 表(Azure 存储)。
- 或将数据发送到 Azure 事件中心以进行进一步的大规模数据处理。
发现和通道区域
在所有情况下,如果您不提供任何配置数据(例如 SQL Server),通信通道将不会被打开和激活。“发现”按钮将在自动发现过程中被隐藏,右侧的进度条将指示发现正在进行。您也可以在自动发现期间在状态栏中看到消息。自动发现完成后(启动时),“发现”按钮将再次可见。按下此按钮(一个切换按钮)将为可能已添加的设备运行发现过程。
如果任何通信通道存在有效配置,“开始通信通道”按钮将启用。否则,它将禁用。按下后,它将将所有已发现设备的事件数据发送到预配置的通信通道。再次按下将停止通道线程。
设备区域
这是您所有已发现设备的列表。在左侧,您可以看到已发现设备类型的列表。每台设备都将显示以下基本信息:
- 设备名称
- 已注册设备的图像
- 它是安全(SSL)还是不安全连接
- 设备是否仍在运行
- 设备 IP 地址
一旦您从列表中选择一个设备(自动发现后,它将是列表中的第一个设备),您将在左侧看到可用的设备命令和设备传感器。对于每个传感器,都可以添加传感器事件(如 GETTEMP)的图形表示。这种可视化表示目前是一个简单的用户控件,它显示带有标题和副标题的传感器事件的值。
IoTDash 实战!
这是一个简短的 YouTube 视频,将演示 IoTDash 的功能以及如何使用它。
实现细节
伪协议
这是来自 Wikipedia 关于通信协议确切含义的定义:
引用在电信中,**通信协议**是用于在计算机内部或计算机之间进行数据交换的数字规则系统。
通信系统使用明确定义的格式(**协议**)来交换消息。每条消息都有一个确切的含义,旨在从一系列针对特定情况预先确定的可能响应中引发一个响应。因此,协议必须定义通信的**语法**、**语义**和**同步**;指定的行为通常独立于其**实现**方式。因此,协议可以作为硬件、软件或两者兼而有之来实现。参与方必须就通信协议达成一致。[1] 为了达成一致,协议可以发展成**技术标准**。**编程语言**描述了计算的相同内容,因此协议和编程语言之间存在密切的类比:*协议之于通信,如同编程语言之于计算*。[2]
为 IoTDash 定义的“协议”并不完全符合所有这些规定。但它有一个基本且合乎逻辑的结构,需要一些步骤才能成功完成发现过程。这包括特定的数据集以及所有后端为可发现而应支持的特定命令的实现。
现在让我们逐一了解所需的部分。
系统功能
本质上,系统功能告诉 IoTDash 实际上发现了什么类型的设备。
- "**名称**" - 设备名称,例如“Fez Spider”
- "**后端**" - 例如“netmf”或“nodejs”
- "**GetCommands**" - 列出可用命令的命令
- "**GetSensors**" - 列出可用传感器的命令
- "**GetSysInfo**" - 获取此系统信息的命令
这样您就可以定义自己的命令短语,同时仍然能够确信系统能够正常工作。除了一个例外:**GetSysInfo**(目前)应始终设置为“SysInfo”。
可用命令
如果您在主板(运行 netmf 或 node.js 的设备)上连接了一个继电器,并且您将一个通风口连接到该继电器,您很可能想通过命令打开或关闭该通风口。命令只是字符串常量,例如:
- "**turnonvent**"
- "**togglerelay**"
- "**blink**"
- "**stopblink**"
等等。这取决于您。您的设备将监听这些命令并按照您的指令执行它们。
可用传感器
为了给任何类型的数据(无论是否来自真实传感器)增加一层抽象,传感器定义提供了定义您想要的传感器名称和类型的自由:
- **SensorId** - 例如 S00
- **SensorType** - 例如 DASS (Device Alive Sensor)
或
- **SensorId** - S01
- **SensorType** - TEMP
可用传感器事件
现在,我们来谈谈有趣的部分。对于每个传感器,您可以定义关联的事件:
- **EventId** - 例如 E00
- **EventName** - 例如 ALIVE
- **EventDataType** - 例如 INT
- **SensorName** - 例如 S00
您可以定义任意数量的事件。这些事件和关联的传感器稍后将在 IoTDash 中显示。这样您就可以查询这些事件并在 IOTDash 中为它们添加传感器控件。
上面列出的每个协议部分都以 **JSON** 格式提供,以便于处理。现在让我们看看这些部分是如何在代码中实现的。
使用 .NET Micro Framework 实现伪协议部分
**NetMF** 端伪协议部分的配置是通过实现 **IDiscoveryConfiguration** 接口来完成的(这样就不需要专门的配置文件)。
using System;
using Microsoft.SPOT;
namespace EasyDiscovery.Model
{
public interface IDiscoveryConfiguration
{
string Commands { get; set; }
string HostProperties { get; set; }
string MESSAGE_DELIMITER {get; set;}
string[] AvailableCommands { get; set; }
string[] AvailableSystemCommands { get; set; }
AvailableSensors AvailableSensors { get; set; }
SensorEvents AvailableEvents { get; set; }
SystemFeatures LocalSystemFeatures { get; set; }
string GetSystemFeaturesJson();
string GetAvailableCommandsJson();
string GetAvailableSensorsJson();
string GetAvailableEventsJson();
}
}
为了实际序列化和反序列化 JSON 消息,使用了 JSON.NetMF(请在资源部分查找 JSON.NetMF 的链接)。它能够高效且快速地序列化对象和对象数组。
您需要做的就是在您的 NetMF 设备上配置上述接口。当然,这取决于您想在网络上传播什么信息。这是一个示例实现:
public class DiscoveryConfiguration : IDiscoveryConfiguration
{
public string Commands { get;set;}
public string HostProperties { get;set;}
public string MESSAGE_DELIMITER { get; set; }
public string[] AvailableCommands { get;set;}
public string[] AvailableSystemCommands { get; set; }
public AvailableSensors AvailableSensors { get; set; }
public SensorEvents AvailableEvents { get; set; }
public SystemFeatures LocalSystemFeatures { get; set; }
//TODO: Add command chaining, not here, in the client! Based on events
public DiscoveryConfiguration()
{
MESSAGE_DELIMITER = "<EOF>";
//SYSTEM FEATURES
LocalSystemFeatures = new SystemFeatures();
LocalSystemFeatures.Features.Add("Name","Fez Spider");
LocalSystemFeatures.Features.Add("Backend", "netmf");
LocalSystemFeatures.Features.Add("GetCommands", "ListCommands");
LocalSystemFeatures.Features.Add("GetEvents", "ListEvents");
LocalSystemFeatures.Features.Add("GetSensors", "ListSensors");
LocalSystemFeatures.Features.Add("GetSysInfo", "SysInfo");
//AVAILABLE COMMANDS
AvailableCommands = new string[]{
"blink",
"blinkfast",
"stopblink",
"blinkgreen",
"stopblinkfast",
};
//AVAILABLE SYSTEM COMMANDS
AvailableSystemCommands = new string[]{
"ListCommands",
"ListEvents",
"ListSensors",
"SysInfo"
};
//SENSORS
AvailableSensors = new AvailableSensors()
{
Sensors = new ArrayList
{
//FIRST "SENSOR" THAT IS USED TO CHECK, IF THE DEVICE IS ALIVE!
{new Sensor() {SensorId = "S00", SensorType = "DASS"}},
{new Sensor() {SensorId = "S01", SensorType = "TEMP"}},
{new Sensor() {SensorId = "S02", SensorType = "MOIST"}}
}
};
//SENSOR EVENTS
AvailableEvents = new SensorEvents
{
Events = new ArrayList()
{
//FIRST ENTRY IS A SYSTEM SENSOR. THIS EVENT SHOWS US, IF THE DEVICE IS STILL AVAILABLE!
{ new SensorEvent() {EventId="E00" ,EventName = "ALIVE", EventDataType = "INT", SensorName = "S00"}},
{ new SensorEvent() {EventId="E01" ,EventName = "GETTEMP", EventDataType = "DEC", SensorName = "S01"}},
{ new SensorEvent() {EventId="E02", EventName = "GETMOIST", EventDataType = "DEC", SensorName = "S02"}}
}
};
}
public string GetSystemFeaturesJson()
{
return JsonSerializer.SerializeObject(LocalSystemFeatures.Features);
}
public string GetAvailableCommandsJson()
{
var commands = new AvailableCommands() {Commands = AvailableCommands};
return JsonSerializer.SerializeObject(commands);
}
public string GetAvailableSensorsJson()
{
return JsonSerializer.SerializeObject(AvailableSensors);
}
public string GetAvailableEventsJson()
{
return JsonSerializer.SerializeObject(AvailableEvents);
}
}
您也将在本文附加的源代码中找到此实现。如您所见,实现非常简单直接。
现在让我们看看 Node.js 中是如何定义伪协议部分的。
/***** COMMAND CONFIGURATION ****/
var commands ={Commands:[
"blinkfast",
"stopblink",
"stopblinkfast",
"onlyongalileo"]}
;
/****** HOST-PROPERTIES (SYSINFO)******/
var hostProps =
{
"Name": "Raspberry PI B+",
"Backend": "Node.js",
"GetCommands": "ListCommands",
"GetEvents": "ListEvents",
"GetSensors": "ListSensors",
"GetSysInfo": "SysInfo"
};
/****** EVENTS ************/
var events = { "Events":
[{ "EventId":"E00",
"EventName":"ALIVE",
"EventDataType":"INT",
"SensorName":"S00"
},
{
"EventId":"E0115",
"EventName":"GETTEMP",
"EventDataType":"DEC",
"SensorName":"S01"
},
{
"EventId":"E0215",
"EventName":"GETMOIST",
"EventDataType":"DEC",
"SensorName":"S02"
}]
};
/********** SENSORS *********/
var sensors =
{"Sensors":[
{
"SensorId":"S00",
"SensorType":"DASS"
},
{
"SensorId":"S01",
"SensorType":"TEMP"
},
{
"SensorId":"S02",
"SensorType":"MOIST"
}
]};
这里也没有什么特别引人注目的。您的 Node.js 后端配置为与 IoTDash 配合使用的方式就是这样。
关于伪协议,就是这些了。IOTDash 和客户端实现将大量使用它。为了理解这一点,让我们从设备的角度开始。
发现、事件、传感器和命令实现
嗯,对于所有这些“神奇”的东西,经典的套接字和 UDP 将发挥作用。即使是新的发现模型,包括物联网网关,也使用相同的成熟技术来连接您的设备。这是在支持原生套接字的任何平台上发现设备的可靠且快速的方式。
NetMF 实现
在 NetMF 设备上实现发现、事件、传感器和命令主要通过这两个类完成:
- **UdpSocketServer** - 处理基本系统部分,如 SysInfo、ListEvents,接收来自 IoTDash(或其他应用程序)的命令并在设备上执行,并将事件数据推送到能够处理事件数据的 IoTDash(或其他应用程序)。
- **UdpGrain** - 负责在网络上宣告设备的存在(UDP 广播)。
为了成功使用这两个类,您应该在 NetMF 应用程序中执行以下任务:
- 建立网络连接。
- 设置设备的当前时间。
- 创建并提供基于 IDeviceDiscoveryConfiguration 接口的发现配置。
- 将该配置传递给 UdpSocketServer 类实例。
- 绑定到 UdpSocketServer 类实例的 **DataReceived** 事件(以接收命令),并实现要监听的命令。
- 实例化一个 **UdpGrain** 实例,设置间隔(以毫秒为单位)、设备类型以及设备是否使用安全连接或不安全连接。
- 启动 Grain 定时器。
- 启动 UDP 服务器定时器。
**UdpServer** 和 **UdpGrain** 定义在名为 **EasyDiscovery** 的程序集中。要更改可能命令的列表(以避免接受所有类型的命令),
我已将一些命令硬编码到 **UdpServer** 类中。请更改 **EasyDiscovery** 项目(文件:**UdpServer.cs**)中的 **第 122-130 行**,以便您的设备只允许特定命令,或允许所有类型的命令。
还有一个硬编码的值,**用于发送广播消息的网络地址**。请更改同一文件第 43 行的地址,使其适合您的需求。
也请更改 UdpGrain.cs 文件第 27 行的此地址。.
为了不让这篇文章变成一本书,让我们看看 Program.cs 的源代码,其余的类留给您自己去探索。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using DeviceGrain.NetworkTools;
using EasyDiscovery;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using Microsoft.SPOT.Net.NetworkInformation;
using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using Gadgeteer.Modules.GHIElectronics;
namespace DeviceGrain
{
public partial class Program
{
private const int ListenPort = 11000;
private static UdpGrain _grain;
private static UdpSocketServer _server;
private static GT.Timer _isAliveTimer;
private static GT.Timer _fakeSensorTimer;
private static Random r;
void ProgramStarted()
{
Debug.Print("Program Started");
r = new Random();
_server = new UdpSocketServer();
_server.SetConfiguration(new DiscoveryConfiguration());
_server.DataReceived += _server_DataReceived;
InitNetwork();
var currentTime = TimeSync.GetNtpTime("europe.pool.ntp.org", 1);
Utility.SetLocalTime(currentTime);
//Set the device type and connection type
_grain = new UdpGrain(1000, "0x01:NOSSL");
_grain.StartTimer();
_server.Start();
_isAliveTimer = new GT.Timer(3000);
_isAliveTimer.Tick += _isAliveTimer_Tick;
_isAliveTimer.Start();
_fakeSensorTimer = new GT.Timer(1000);
_fakeSensorTimer.Tick += _fakeSensorTimer_Tick;
_fakeSensorTimer.Start();
#region testevents
//Thread t = new Thread(new ThreadStart(()=>{
// for(int i=1; i<50;i++)
// {
// _server.PushEvent("event" + i + "from t1","somedata");
// }
//}));
//t.Start();
//Thread t2 = new Thread(new ThreadStart(() =>
//{
// for (int i = 1; i < 100; i++)
// {
// _server.PushEvent("event" + i + "from t2", "somedata");
// }
//}));
//t2.Start();
//Thread t3 = new Thread(new ThreadStart(() =>
//{
// for (int i = 1; i < 80; i++)
// {
// _server.PushEvent("event" + i + "from t3", "somedata");
// }
//}));
//t3.Start();
#endregion
}
void _fakeSensorTimer_Tick(GT.Timer timer)
{
_server.PushEvent("E02", r.Next(1300).ToString());
_server.PushEvent("E01", r.Next(100).ToString() + " C");
}
void _isAliveTimer_Tick(GT.Timer timer)
{
_server.PushEvent("E00","1");
}
void _server_DataReceived(object sender, DataReceivedEventArgs e)
{
//TODO: Check for future release
//Stop broadcasting, we have a commander now.
//if (!_timerStopped)
//{
// grain.StopTimer();
// _timerStopped = true;
//}
var command = new string(Encoding.UTF8.GetChars(e.Data));
switch (command)
{
case "blink":
multicolorLED.BlinkRepeatedly(GT.Color.Blue, new TimeSpan(0, 0, 5), GT.Color.Red, new TimeSpan(0, 0, 5));
break;
case "blinkgreen":
multicolorLED.BlinkRepeatedly(GT.Color.Green, new TimeSpan(0, 0, 5), GT.Color.Red, new TimeSpan(0, 0, 5));
break;
case "blinkfast":
multicolorLED.BlinkRepeatedly(GT.Color.Blue, new TimeSpan(0, 0, 1), GT.Color.Red, new TimeSpan(0, 0, 1));
break;
case "stopblink":
multicolorLED.TurnOff();
break;
case "stopblinkfast":
multicolorLED.TurnOff();
break;
}
}
private void InitNetwork()
{
var ethernet = ethernetJ11D.NetworkInterface;
ethernet.Open();
ethernet.EnableDhcp();
ethernet.EnableDynamicDns();
while(ethernet.IPAddress == "0.0.0.0")
{
Thread.Sleep(100);
}
Debug.Print("Network ok!");
}
}
}
在这段简短的代码中,您可以看到启动 UDPServer、接收命令、发送事件以及如何启动发现套接字所需的所有步骤。
以下是 IoTDash 能够理解的设备类型列表:
- **0x01** - GHI Fez Spider
- **0x02** - GHI Raptor
- **0x03** - Intel Galileo Gen 1
- **0x04** - Intel Galileo Gen 2
- **0x05** - Intel Edison
- **0x06** - Raspberry PI B+
您可以在 DeviceGrain 构造函数中使用十六进制值来定义设备类型以及它是安全连接还是非安全连接,如下所示:
- **"0x01:NOSSL"** - GHI Fez Spider,不安全连接
- "**0x06:SSL**" - Raspberry PI B+,安全连接
IoTDash 实现了一个名为 **DeviceTypes** 的枚举,该枚举将通过简短的十六进制代码识别设备类型并加载所需的设备图像。您可以轻松修改此枚举以满足您的需求。
目前 NetMF 实现不支持 SSL,因为我无法弄清楚为什么我无法使用 NetMF 4.3 (QFE1) 将 SSL 服务器认证为安全服务器。SSL 服务器实现包含在源代码中,但未使用。如果您知道如何使用证书做到这一点,请告诉我。
基于 Node.js 的实现
在 Node.js 中实现基于套接字脚本非常直接。对于安全连接也是如此。您可以将此代码复制并粘贴到任何运行 node 的设备上。证书包含在源代码文件中,以便您可以在自己的设备上测试实现。根据您的需求调整所有内容,并替换广播客户端的子网地址,您可以在“SSL CLIENT SPEC”注释后的那一行找到该地址。
var dgram = require('dgram');
var tls = require('tls');
var fs = require('fs');
var events = require('events');
var eventChannel = new events.EventEmitter();
var eventSocket;
var eventInterval;
var eventIntervalAlive;
/****STANDARD PORTS********/
var DEFAULT_EVENT_PORT = 8090;
/*****SSL CLIENT SPEC********/
var message = new Buffer("0x06:SSL");
var client = dgram.createSocket("udp4");
client.bind(function(){client.setBroadcast(true);});
var broadcastInterval = setInterval(function(){
client.send(message,0,message.length,9100,"192.168.178.255");
},1000);
/***** COMMAND CONFIGURATION ****/
var commands ={Commands:[
"blinkfast",
"stopblink",
"stopblinkfast",
"onlyongalileo"]}
;
/****** HOST-PROPERTIES (SYSINFO)******/
var hostProps =
{
"Name": "Raspberry PI B+",
"Backend": "Node.js",
"GetCommands": "ListCommands",
"GetEvents": "ListEvents",
"GetSensors": "ListSensors",
"GetSysInfo": "SysInfo"
};
/****** EVENTS ************/
var events = { "Events":
[{ "EventId":"E00",
"EventName":"ALIVE",
"EventDataType":"INT",
"SensorName":"S00"
},
{
"EventId":"E0115",
"EventName":"GETTEMP",
"EventDataType":"DEC",
"SensorName":"S01"
},
{
"EventId":"E0215",
"EventName":"GETMOIST",
"EventDataType":"DEC",
"SensorName":"S02"
}]
};
/********** SENSORS *********/
var sensors =
{"Sensors":[
{
"SensorId":"S00",
"SensorType":"DASS"
},
{
"SensorId":"S01",
"SensorType":"TEMP"
},
{
"SensorId":"S02",
"SensorType":"MOIST"
}
]};
/******* EVENT, EVENTS*********/
eventChannel.addListener('sensorEvent',function(eventData){
//console.log("EventSocket");
if(eventSocket !== 'undefined' && eventSocket !== null)
{
eventSocket.write(eventData.eventToPush+'~'+eventData.data+"");
}
});
StartSecureListener();
StartEventPropagator();
var eventLoopStarted = false;
function StartEventPropagator()
{
try{
var options = {
key: fs.readFileSync('./certs/key.pem'),
cert: fs.readFileSync('./certs/cert.pem')
};
console.log("event l start");
tls.createServer(options, function(s){
eventSocket = s;
s.on('error', function(ex){
//console.log(ex);
});
if(eventLoopStarted === false)
{
StartEventLoopDeviceAliveSignal();
StartEventLoopForTestSensors();
eventLoopStarted = true;
}
}).listen(DEFAULT_EVENT_PORT);
}catch(ex)
{
console.log(ex);
}
}
function StartEventLoopForTestSensors()
{
eventInterval = setInterval(function(){
//Sending a simple moisture value event
var humDataString = Math.round(Math.random()* ((1000-100)+100));
eventChannel.emit('sensorEvent',{eventToPush:'E0215', data:humDataString});
var tempDataString = Math.round(Math.random() * ((100-1)+1)) + ' C';
//Sending out temperature between 1 and 100 C
eventChannel.emit('sensorEvent',{eventToPush:'E0115', data:tempDataString});
},1000);
}
function StartEventLoopDeviceAliveSignal()
{
eventIntervalAlive = setInterval(function(){
//That's enough to indicate that we are still alive here...
eventChannel.emit('sensorEvent',{eventToPush:'E00', data:'1'});
},3000);
}
function StartSecureListener()
{
var options = {
key: fs.readFileSync('./certs/key.pem'),
cert: fs.readFileSync('./certs/cert.pem')
};
tls.createServer(options, function(s){
//Stop broadcasting, we have been discovered
//clearInterval(broadcastInterval);
var sysInfo = JSON.stringify(hostProps);
var cmdList = JSON.stringify(commands);
var sensorList = JSON.stringify(sensors);
var eventList = JSON.stringify(events);
s.on('data',function(data){
var cmdData = data.toString('utf-8');
console.log(cmdData);
//check, what command we have received
switch(cmdData)
{
case 'ListCommands':
console.log("Command List Requested");
s.write(cmdList+"");
break;
case 'ListSensors':
console.log("Sensor List Requested");
s.write(sensorList+"");
break;
case 'ListEvents':
console.log("Event List Requested");
s.write(eventList+"");
break;
case 'blink':
console.log("blink command called");
break;
case 'stopblink':
console.log("blinkstop command called");
break;
case 'blinkfast':
console.log("blinkfast command called");
break;
case 'stopblinkfast':
console.log("stopblinkfast command called");
break;
case 'SysInfo':
console.log("SysInfo requested.");
s.write(sysInfo+"");
break;
case "onlyongalileo":
console.log("Only called");
break;
}
});
s.on('error', function(ex){
console.log(ex);
});
}).listen(3340);
}
很乐意收到真正 Node.js 专家对此代码的反馈。
IoTDash - 基本架构和通信通道
一篇文章不足以详细解释 IoTDash 的所有部分。因此,我将解释 IoTDash 和通信通道的最重要部分。
IoTDash 基本架构
IoTDash 引用两个主要项目:
- **DeviceDiscovery** - 用于在网络上发现设备。
- **CommChannels** - 在这里实现了所有通信通道。
DeviceDiscovery 项目实现了以下部分和功能:
- 事件、命令、传感器、设备类型和系统功能的模型类(POCO)。
- **EventReceivedEventArgs** - 包含特定传感器的事件数据。
- **DeviceAddedEventArgs** - 当发现新设备时传递。
- **DeviceListener** - 负责监听设备(安全和不安全设备,在发现期间使用),创建所需的 SSL 和非 SSL 命令客户端。
- **SSLClient** - 安全命令和事件监听客户端。
- **NoSSLClient** - 不安全命令和事件监听客户端。
此代码大量使用多线程,以便 IoTDash 即使在较小的平板电脑上也能流畅运行。其余部分使用简单的 MVVM 框架 MVVM-Light。
通信通道实现
通信通道代表到不同类型数据存储和服务的管道。使用通信通道,IoTDash 能够将来自 IoT 设备的数据传输到外部世界或您的本地 SQL Server。就像一个网关。
此版本的 IoTDash 包含到 **SignalR**(可与 **Azure-Websites** 一起使用)、**Azure Tables**、**Azure Event Hubs 和 SQL Server** 的通道。
所有通道都基于一个接口:**ICommChannel**。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommChannels
{
public interface ICommChannel<T,TU>
{
Task<bool> Connect();
Task<bool> Disconnect();
Task<bool> SendData(T data);
Task<TU> SetConfig(TU config);
Task<TU> LoadConfig();
Task<bool> SaveConfig(TU config);
}
}
这是用于打开到 Azure 事件中心的管道的示例实现:
using CommChannels.Config;
using Microsoft.ServiceBus.Messaging;
using Newtonsoft.Json;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommChannels.EventHubChannel
{
public class EventHubCommChannel : ICommChannel<EventHubChannelModel, EventHubConfig>
{
private EventHubConfig _configuration;
private readonly AsyncLock _mutex = new AsyncLock();
public EventHubConfig Configuration
{
get
{
return _configuration;
}
set
{
_configuration = value;
}
}
private EventHubClient _client;
public Task<bool> Connect()
{
var tcs = new TaskCompletionSource<bool>();
try
{
_client = EventHubClient.CreateFromConnectionString(_configuration.EventHubConnectionString,_configuration.EventHubName);
tcs.SetResult(true);
}
catch (Exception ex)
{
tcs.SetResult(false);
}
return tcs.Task;
}
public async Task<bool> Disconnect()
{
try
{
await _client.CloseAsync();
return true;
}
catch (Exception ex)
{
return false;
}
}
public async Task<bool> SendData(EventHubChannelModel data)
{
using (await _mutex.LockAsync())
{
try
{
var eventData = new EventData(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data)));
eventData.PartitionKey = "0";
eventData.Properties.Add("Type", "DeviceMessage");
await _client.SendAsync(eventData);
return true;
}
catch (Exception ex)
{
return false;
}
}
}
public async Task<EventHubConfig> SetConfig(EventHubConfig config)
{
return await Task.Run<EventHubConfig>(async () =>
{
await SaveConfig(config);
return config;
});
}
public async Task<EventHubConfig> LoadConfig()
{
return await Task.Run<EventHubConfig>(() =>
{
if (File.Exists(@".\ConfigFiles\EventHubConfig.json"))
{
var configData = File.ReadAllText(@".\ConfigFiles\EventHubConfig.json");
var config = JsonConvert.DeserializeObject<EventHubConfig>(configData);
}
return new EventHubConfig();
});
}
public async Task<bool> SaveConfig(EventHubConfig config)
{
return await Task.Run<bool>(() =>
{
if (config == null)
{
var configData = JsonConvert.SerializeObject(_configuration);
File.WriteAllText(@".\ConfigFiles\EventHubConfig.json", configData);
return true;
}
else
{
var configData = JsonConvert.SerializeObject(config);
File.WriteAllText(@".\ConfigFiles\EventHubConfig.json", configData);
return true;
}
});
}
}
}
就是这样!我对 CodeProject 上的 Microsoft Azure 竞赛的贡献!感谢阅读 :)
使用的组件和库的功劳
- **Syncfusion** 通过“Essential Studio Enterprise Edition Community License”赞助了 IoTDash 中使用的 WPF 组件 - 二进制文件未随本项目分发。要编译项目,请申请免费许可证。您可以在此处申请。
- **Oystein Bjorke** 和他的 **PropertyTools** for WPF。您可以在此处查看它们。
- 出色的 DI 框架 **Ninject**(用于 ViewModelLocator)。您可以在此处查看它。
- **Stephen Cleary** 和他出色的 **AsyncEx** 库。您可以在此处查看它。
- **Laurent Bugnion 的 MVVM-Light Toolkit**。您可以在此处查看它。
- 最后但同样重要的是,当然还有 **Azure 团队开发的优秀的 Azure 库**。您可以在此处查看它们。